ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

全网网站信息采集实战:从公开数据源到SQLite索引

2026/9/13 5:14:52 拓冰建站 浏览量
全网网站信息采集实战:从公开数据源到SQLite索引 先说一个很多人容易踩的误区“全网所有网站”在实操中并不是一个能一网打尽的名单而是一个可以通过公开数据、合规采集和合理存储无限逼近的集合。我长期做信息收集和站点观测经常要给项目补齐“全量网站清单”和“基础信息库”。刚开始我也以为要自己写爬虫把全网爬一遍后来才发现真正靠谱的路径是先吃透公开数据源再针对性补采最后落成可检索的索引。这篇就把我从需求拆解到最终落库的完整流程写出来包含可直接复制的代码、参数和避坑经验。1. 先把需求拆清楚“全网”到底指什么1.1 “所有网站”的口径与量级做技术方案最忌上来就接需求。当你听到“获取全网可访问的所有网站网址和网站信息”时第一件要做的事不是写爬虫而是把需求拆成可度量的口径。我一般会先确认三个问题是指要拿到世界上所有域名的注册列表还是指拿到所有能被搜索引擎抓到的URL是要保存每个网站的标题、描述这类元信息还是还要包含页面正文内容允许使用的数据源是只能自己采集还是可以借助第三方开放数据以域名数量来看全球活跃域名大约有数亿级别如果算上所有二级目录和动态参数URL这个量级直接到了百亿以上。Common Crawl公开释放的数据里单月抓取的URL数量差不多就在这个规模。如果你真的试图直接从零开始抓取全网按每秒100个请求的速度计算光把所有URL请求一遍就需要几十年。所以“全网”的合理操作定义一定是分层的先拿到域名层再按优先级拿URL层最后才谈得上页面信息层。1.2 你要收集的信息其实就这几类把“网站信息”展开来看无非是四类数据基础元信息包括页面标题、描述、关键词、语言、发布时间这类数据主要用于展示和检索站点结构信息包括robots.txt、sitemap.xml、内链外链关系这类数据告诉你站点允许被怎么访问以及它的内容组织方式网络层信息包括IP、DNS解析记录、HTTP响应头、SSL证书信息、服务器技术栈这类数据常用于资产梳理和状态监控内容级信息包括正文文本、图片链接、结构化数据这是最重的一类数据存储和分析成本也最高。我建议在做全量清单时优先把前三类做扎实内容级信息按需补采。因为对于一个“网站清单站点信息”类项目用户大多数时候只关心“这个网站是否存在、是什么、还能不能访问”而不是需要全文镜像。先把数据结构化成表后面再扩展也很容易。2. 站在巨人的肩膀上先用现成的公开数据源2.1 Common Crawl不用爬也能拿到 URL 索引很多人不知道其实每个季度都有机构把全网抓取结果公开出来Common Crawl就是其中最典型的一个。它是一个非营利组织定期抓取互联网上的网页并把原始数据、元数据、文本索引全部公开托管在亚马逊S3上。这意味着你完全不需要自己从头爬取就能拿到一份接近全网的URL清单。我常用的方式有两种。第一种是直接下载Common Crawl的URL索引文件这个索引记录了每个URL的抓取时间、响应码、文件偏移位置等信息一列一列排好用简单的脚本就能过滤出符合要求的URL。第二种是根据索引定位到具体的WARC文件再抽取其中的HTML元信息。实操时我一般用Python的requests配合pandas来处理示例代码如下import requests import pandas as pd # 获取某次抓取任务的索引文件列表 index_url https://index.commoncrawl.org/collinfo.json resp requests.get(index_url, timeout30) collections resp.json() print(collections[0][id], collections[0][cdx-api]) # 用CDX API查询某个域名下的URL cdx_api collections[0][cdx-api] query_url f{cdx_api}?urlexample.comoutputjsonflurl,status,mimefilterstatus:200collapseurlkey data requests.get(query_url, timeout60).json() print(data[:10])这里有个关键点CDX API支持collapse参数可以根据URL的去重键把同一条URL只保留一条记录能帮你极大压缩数据体积。我处理上百万条URL时加了collapseurlkey之后数据量能减少约三成。如果你需要更多字段比如响应头、页面标题可以加fl参数指定字段列表避免下载整个WARC文件。2.2 更多公开名单与数据备选方案除了Common Crawl还有几类公开数据源值得关注。域名注册局的顶级域名列表例如CZDSCentralized Zone Data Service提供顶级域的区域文件申请入口通过审核后就能拿到完整域名列表这是最接近“全网域名”的数据来源之一。威胁情报社区维护的恶意域名名单这类名单虽然以安全检测为目的但覆盖面很广经常包含大量长尾域名。公开的学术研究数据集部分大学和安全研究机构会定期释放网络空间测绘数据通常包含IP、端口、域名、证书等多种维度的信息非常适合作为基础底稿。我的建议是不要把数据源单一化。先申请一份区域文件再拿Common Crawl的URL索引去重最后用自己写的爬虫补采小部分长尾站点这样得到的数据覆盖度会好很多。纯粹依赖任何一个单一数据源都会因为它的抓取策略偏差而漏掉大量站点。3. 需要自建采集时怎么写一个边界清晰的最小爬虫3.1 先处理 robots.txt 与站点地图虽然公开数据源能覆盖大部分需求但总有业务需要自己定向采集比如要监控某个行业的所有站点或者要补采公开数据源里缺失的新生网站。这时候就需要自建采集程序。很多人一上来就写好请求逻辑直接跑结果要么被对方服务器封IP要么抓回一堆无用页面。我习惯先把“边界”处理好也就是先看目标站点允许什么、禁止什么。robots.txt是站点和爬虫之间的握手协议里面用User-agent和Disallow两条指令划定了权限边界。合法合规的爬虫应该无条件尊重它。另外很多站点会提供sitemap.xml里面直接列出了允许抓取的URL列表这可比自己去HTML里挖链接高效得多。下面是一个处理这两个文件的函数import requests from urllib.robotparser import RobotFileParser from urllib.parse import urljoin import re def parse_robot_sitemap(domain, user_agentMyBot/1.0): rp RobotFileParser() rp.set_url(fhttps://{domain}/robots.txt) rp.read() sitemap_urls [] try: robots_txt requests.get(fhttps://{domain}/robots.txt, timeout10).text sitemap_urls re.findall(r(?i)^Sitemap:\s*(\S), robots_txt) except Exception: pass allowed_urls [] for url in sitemap_urls: if rp.can_fetch(user_agent, url): allowed_urls.append(url) return rp, allowed_urls rp, sitemaps parse_robot_sitemap(example.com) print(sitemaps)这里有个小坑sitemap里给的可能不是实际页面URL而是sitemap索引文件里面还嵌套了子sitemap链接。必须写递归逻辑把嵌套的sitemap解析到底否则会漏掉大量URL。解析sitemap时优先用xml.etree.ElementTree比正则更稳因为sitemap本身是XML格式有命名空间。3.2 采集字段与解析实现当拿到了允许采集的URL列表后下一步就是抓取页面并抽取信息。对于“网站信息”这个目标我通常就把下面这几个字段拿全就够了页面标题、Meta描述、Meta关键词、语言、字符集、最后修改时间、页面大小、内链数、外链数。具体解析时用BeautifulSoup解析HTML用lxml作为解析器速度和容错率都比默认的html.parser好不少。import requests from bs4 import BeautifulSoup import hashlib def fetch_page_info(url, timeout10): resp requests.get(url, timeouttimeout, headers{User-Agent: Mozilla/5.0}) soup BeautifulSoup(resp.text, lxml) title soup.title.string.strip() if soup.title and soup.title.string else description keywords for meta in soup.find_all(meta): name meta.get(name, ).lower() if name description: description meta.get(content, ).strip() elif name keywords: keywords meta.get(content, ).strip() lang soup.html.get(lang, ) if soup.html else content_len len(resp.content) url_hash hashlib.md5(url.encode(utf-8)).hexdigest() return { url: url, url_hash: url_hash, title: title[:200], description: description[:300], keywords: keywords[:200], lang: lang, content_len: content_len, status: resp.status_code, }这个函数看着简单但实际踩过几个坑之后才稳定下来第一个是title可能是空字符串必须加判空第二个是有些站点返回的编码不规范导致resp.text乱码可以先用resp.encoding resp.apparent_encoding强制再解析一次第三个是请求头最好带上浏览器的User-Agent不少站点对默认的Python请求头做了拦截。字段长度也要做截断不然数据库里会插入超长内容导致异常。4. 把采集规模化队列、去重与限速缺一不可4.1 队列与去重设计单页面抓取写好了接下来就是规模化的问题。如果只抓几十个页面写个for循环就完了。一旦要抓几万、几十万个页面就必须考虑三个问题URL去重、任务队列、失败重试。我见过不少人直接用列表存待抓URL结果跑一晚上内存就爆了。正确的做法是“内存只放待处理任务已处理任务放磁盘或数据库”。最简单的架构是使用Pythonqueue.Queue配合一个Bloom Filter或哈希集合做去重。哈希集合内存占用较大几百万条URL会吃掉几百MB内存Bloom Filter只需约十分之一的容量。如果不想引入额外依赖可以用一个Redis实例来存去重键天然支持超大规模。下面是一个基础的去重队列实现import queue import hashlib class UrlQueue: def __init__(self): self.q queue.Queue() self.seen set() def seen_key(self, url): return hashlib.sha256(url.encode(utf-8)).hexdigest() def push(self, url): key self.seen_key(url) if key not in self.seen: self.seen.add(key) self.q.put(url) def pop(self): return self.q.get() def done(self, url): # 可在这里将已完成URL写回磁盘 pass uq UrlQueue() uq.push(https://example.com/a) uq.push(https://example.com/a) # 重复URL会被过滤掉 print(uq.q.qsize())注意seen集合只增不减对于超大规模采集会占用大量内存所以实际项目里我一般把这个集合换成Redis的SADD命令。另外采集完成后的URL列表要定期落盘避免程序意外崩溃后需要从头再来。常用的做法是每处理完1000个URL就把这批结果写入JSON行文件同时记录游标位置。4.2 限速与重试的正确姿势限速是采集寿命的关键。很多人觉得访问快点效率高实际上当你把对方服务器打崩或者IP被封之后整个采集任务就得中断反而更慢。我一般把请求间隔设置在1到3秒之间对大中型站点可以适当放宽到5秒。使用time.sleep是最简单的方式但更好的办法是用requests.Session配合自定义适配器让连接复用降低单次握手开销。重试机制也需要注意不能无脑重试。对于网络超时连接超时、读超时可以重试3次每次等待时间翻倍对于HTTP 5xx错误也可以重试同样要在间隔后重试但HTTP 4xx错误一般不会因为重试而成功应该直接跳过并记录错误日志。下面是我常用的限速和重试配置import requests import time from requests.adapters import HTTPAdapter session requests.Session() adapter HTTPAdapter(max_retries3, pool_connections20, pool_maxsize20) session.mount(https://, adapter) session.mount(http://, adapter) def fetch_with_retry(url, max_retries3): for i in range(max_retries): try: resp session.get(url, timeout(3, 10)) if resp.status_code 200: return resp elif resp.status_code in (403, 404, 410): return resp except requests.exceptions.RequestException as e: print(f请求失败: {url}, 错误: {e}) time.sleep(2 ** i) return None这里有个经验timeout不要只填一个数字要填(连接超时, 读超时)元组否则读响应阶段卡住很难排查。而且我把403和404单独处理是因为403往往表示对方已经识别到爬虫再换UA也可能没有用需要放到待观察列表里人工检查404则说明URL本身已经失效没必要重试。5. 数据落地从 JSON 行到 SQLite 索引5.1 字段标准化与存储方案采集到的原始信息如果只是堆成JSON文件后期查询会非常痛苦。我的习惯是采集阶段以JSON行JSON Lines格式暂存每条记录一行方便并行写入和断点续传。采集全部完成后再导入SQLite做索引和查询。SQLite单文件、零配置、轻量非常适合几百万条以内的小型项目。字段标准化必须在入库前完成不然后面处理脏数据会耗费大量时间。比如URL必须做归一化把https://www.example.com/变成example.com的形式方便按域名分组标题里的空白字符要替换成单个空格状态码要统一转为整数lang字段要转小写。下面是一个简单的归一化函数import re from urllib.parse import urlparse def normalize_url(url): url url.strip() url re.sub(r^[a-zA-Z][a-zA-Z0-9.-]*://, //, url) # 去掉协议 url re.sub(r^/, , url) # 去掉开头的斜杠 netloc url.split(/)[0].lower() netloc netloc.split(:)[0] # 去掉端口 if netloc.startswith(www.): netloc netloc[4:] return netloc print(normalize_url(https://www.Example.com:443/path?a1))5.2 构建一个可检索的全站信息索引入库之后SQLite的索引设计直接决定查询速度。我通常把url_hash设为主键天然去重再给domain、lang、status建普通索引这样无论按域名批量查还是按语言筛选反应速度都在毫秒级。建表语句如下CREATE TABLE IF NOT EXISTS website_info ( url_hash TEXT PRIMARY KEY, url TEXT NOT NULL, domain TEXT NOT NULL, title TEXT, description TEXT, keywords TEXT, lang TEXT, content_len INTEGER, status INTEGER, collected_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_domain ON website_info(domain); CREATE INDEX IF NOT EXISTS idx_lang ON website_info(lang); CREATE INDEX IF NOT EXISTS idx_status ON website_info(status);导入时我推荐使用事务批量提交一次提交1000条提交间隔里做一次PRAGMA optimize速度比逐条插入快几十倍。如果数据量超过千万级别SQLite的性能会明显下降这时候建议换用PostgreSQL或者ClickHouse但底层设计思路完全一样。6. 小范围实测抓了 5000 个页面之后我得到了什么6.1 实测环境与参数理论讲再多不如跑一次。为了验证这套流程我拿了一个中小型行业站列表做测试种子域名大约50个加上sitemap里的URL总共产生约5000个待抓页面。采集程序跑在一台2核4G的普通云服务器上使用Python 3.10单线程加1.5秒固定延时不开并发。之所以选择单线程是为了先验证稳定性和解析正确率后续需要提速再上多线程。6.2 结果统计与质量观察跑完之后我把数据导出来做了几项统计。最终抓取成功HTTP 200的页面有4472个成功率约89.4%跳转页面301/302有388个404和403加起来约140个。常规信息完整度方面93%的页面有标题82%有meta描述71%有meta关键词。这个完整度已经足够支撑后续做搜索和分类展示。还有一个值得注意的现象大约6%的页面标题或描述里带了大量乱码字符。排查后发现基本都是网页编码声明不规范导致有的页面声明的字符集是UTF-8实际内容却是GBK编码。解决方案是在解析前用chardet或charset-normalizer做一次编码检测虽然会增加一点耗时但对数据质量的提升非常明显。7. 避坑指南与经验教训7.1 高频请求被拦截后怎么办我最早踩的坑就是不加节制地发请求结果IP被对方防火墙封了一个小时。后来总结出三步应对策略第一步发现请求失败率异常增高时立即暂停任务检查日志里的错误码重点是429和403第二步把请求间隔从固定值改成带随机抖动的区间值比如time.sleep(random.uniform(1.5, 3.5))这样更接近人的操作习惯第三步如果仍然被拦截不要急着换IP硬闯而是检查是不是自己的请求头、Cookie或访问路径暴露了自动化特征优化这些细节往往比换IP更有效。7.2 编码和解析异常的处理解析异常分两类一类是网络抓取阶段拿到的是反爬页面或错误页这会导致解析结果里没有标题和正文另一类是解析阶段因为HTML结构不规范导致BeautifulSoup找错节点。我的处理方式是给每个页面保存一个HTML结构指纹也就是把标签层级关系转成哈希值后续统计时如果发现某类页面的指纹高度集中说明极大概率是同一套模板产生的页面这时就可以针对模板做批量解析优化而不用每一个都单独处理。7.3 合规红线什么能碰什么不能碰做全网信息收集必须时刻把合规放在第一位。公开可访问的网页数据虽然技术上能抓但抓取行为是否合规取决于多个因素包括robots.txt是否允许、抓取频率是否合理、以及后续数据用途是否正当。我给自己定的规则是只收集已经公开在互联网上的基础信息不碰任何需要登录才能访问的后台数据抓取必须遵守robots.txt采集频率设置为对方可接受的正常访问节奏收集到的数据只用于合规的业务分析不用于任何商业骚扰或侵犯他人权益的行为。如果这些基础线没把控好整个项目在技术和商业上都会埋雷。我见过不少团队因为追求“全网数据”而踩线最后基础采集能力还没成熟先把自己拖进了麻烦。真正可持续的全网信息采集靠的是公开数据源加合规定向采集合约出来的结果而不是暴力抓取。最后再分享一个我自己的习惯每次跑完采集任务不要把数据丢在那里不管而是固定维护一份“采集任务台账”记录数据源的版本时间、抓取参数、覆盖规模和异常情况。这样隔几个月重新生成全量清单时你就能快速知道哪些数据源需要更新哪些需要补采。这比每次从头折腾高效得多。