做新闻站点的定向内容抓取,这几年我前前后后折腾过不少。这次这个项目,目标很明确:基于AI辅助的方式,对ChinaDaily的新闻内容做关键词定向爬取,并且把反爬策略和数据清洗完整地串成一条流水线。项目本身不复杂,但踩过的坑、积累下来的套路,我觉得比单纯的“能跑到数据”更有价值。为什么选AI辅助?核心原因只有一个:传统爬虫用XPath或者正则去抠正文,页面结构一改就全盘崩掉,维护成本极高;而AI介入之后,抽取逻辑从“死板的节点定位”变成了“语义理解”,抗页面改版的能力强了不止一个档次。
这个项目解决的痛点很实在:人工去新闻网站复制素材效率太低,关键词分散搜索又不好整理。它可以做到输入一个新闻关键词,自动把相关的标题、摘要、正文、发布时间、原文链接都拉回来,还能顺手完成去重、清洗、结构化存储,最后给你一份干净的表格。适合谁看?有一定Python基础、想把爬虫从“玩具”做到“工具”的同学,或者做内容分析、舆情监测、行业素材收集的从业者。下面我把整个项目从思路到落地完整拆一遍。
1. 项目整体设计拆解:为什么这么搭
1.1 目标站点分析与需求聚焦
先说选型逻辑。做新闻爬虫,目标站点可选的很多,我最后锁定在ChinaDaily,原因有三:一是页面结构相对规范,文章链接和发布时间这些核心字段在HTML里有迹可循;二是内容以英文为主,中文为辅,处理起来字符编码问题少;三是它作为一个新闻门户,关键词搜索路径很清晰,适合做“关键词→列表→详情→结构化字段”这条完整链路。
需求聚焦也很重要。项目真正要的不是“全站爬取”,而是“关键词命中后的定向收集”。所以我把范围严格界定为:搜索关键词、提取相关文章列表、抓取详情页关键字段、清洗入库。这个界定避免了漫无目的地全站扫描,既降低了对目标站点的压力,也让后续数据质量好控制得多。
这里我要多说一句合规意识。做爬虫不是做渗透,项目定位在“公开数据的个人学习与研究使用”,严格遵守目标网站的服务条款,控制请求频率,不碰用户非公开数据,这是底线。项目里的反爬策略也不是为了“绕”什么,而是为了让程序在合理频率下表现得更像真实用户,减少对站点资源的干扰。
1.2 AI辅助到底辅助在哪
很多人一听“AI爬虫”就觉得是让AI代替爬虫去抓网页,这是误解。AI在项目里的定位是“解析层的外挂大脑”,而不是“抓取引擎”。抓取还是用请求库完成,AI做的是两件事:一是正文抽取的降级方案,二是字段的智能补全。
具体来说,传统方案里写死一个XPath,比如定位正文的div节点,页面改版后XPath失效,整个爬虫瘫痪。AI辅助方案里,我先把页面里的正文区域用通用规则粗筛出来,比如去掉导航、广告、评论区这些噪音节点,把剩下的HTML文本交给AI模型,用提示词让它“从这段HTML文本中提取出新闻正文、发布时间、作者”等结构化信息。即使页面结构调整了,只要正文语义还在,AI就能靠语义把它捞出来。这就是稳定性的根源。
但AI也有自己的问题,比如延迟高、调用成本不低、偶尔抽风。所以项目的设计原则是“规则优先,AI兜底”:先用轻量的XPath和正则抽一遍,如果抽取结果的有效性校验没通过,再走AI。这样既保速度,又保稳定。
1.3 整体技术路线一览
整个项目的流程可以拆成五段:
- 关键词构造搜索URL,拿到搜索结果列表页。
- 解析列表页,提取文章详情页链接和摘要信息。
- 逐个访问详情页,抽取标题、发布时间、正文、作者等核心字段。
- 清洗去重,把无效内容、重复文章、异常日期处理掉。
- 结构化存储到本地数据库,方便后续检索和分析。
这五段里的每一步都有独立的异常处理。比如列表页抽不到链接就触发重试,详情页请求失败就记录日志并跳过,清洗阶段发现正文内容过短就直接丢弃。整体设计思路就是“每一层都不信任上一层的输出”,这是工程化爬虫和一次性脚本之间最大的区别。
2. 环境准备与关键选型细节
2.1 开发环境与依赖库
这个项目不需要重型的分布式环境,一台普通开发机就能跑。我用的Python版本是3.10,核心依赖如下:
| 用途 | 库名 | 说明 |
|---|---|---|
| HTTP请求 | requests | 够轻量,会话管理方便,适合中小规模抓取 |
| HTML解析 | parsel + lxml | XPath和CSS选择器都支持,解析速度比纯BeautifulSoup快 |
| 数据处理 | pandas | 清洗阶段处理表格数据很方便 |
| 数据库 | SQLite(Python内置sqlite3) | 单文件存储,零配置,适合这个项目的数据量 |
| AI辅助 | 开源的预训练模型库 | 用来跑文本抽取模型,或者对接通用大模型API |
为什么不用Scrapy?不是说Scrapy不好,而是这种体量的项目用Scrapy有点“杀鸡用牛刀”。Scrapy的异步引擎非常强,但它的调试链路长、中间件配置复杂,对新手来说出了问题不好定位。请求量不大时,requests配合合理的休眠策略完全够用,而且代码直白、好维护。等到将来数据量大了,再把核心解析逻辑平移到Scrapy也不费劲。
2.2 请求边界与频率控制
这里聊一个爬虫项目里最容易被忽略的参数设计:请求频率。目标站点的搜索接口和详情页接口承受能力有限,我给自己定的规则是:两次请求间隔不低于3秒,且加入随机抖动,比如random.uniform(3, 6)秒。这样平均请求量能控制在每分钟12次左右,既不会触发站点风控,也不会给目标服务器造成压力。
为什么要加随机抖动而不是固定间隔?固定间隔哪怕是很慢的频率,也容易被风控系统识别出“机器节律”。真实用户的操作间隔本来就不是固定值,有长有短、偶尔连续,所以随机抖动是反爬对抗中最基础也最实用的一招。
2.3 项目目录与数据落地规划
项目结构我分得比较清晰:
- project/ - crawler/ # 爬虫核心逻辑 - fetcher.py # 请求与重试 - parser.py # 列表页与详情页解析 - ai_extractor.py # AI辅助抽取 - cleaning/ # 数据清洗模块 - cleaner.py - storage/ # 存储模块 - sqlite_store.py - config.py # 全局配置:关键词、请求间隔、开关 - main.py # 入口这样的分层好处很明显:每一块都能单独测试。比如解析逻辑出问题时,不需要跑完整流程,直接喂一段保存下来的HTML文件给解析函数就能复现。这个习惯帮我省了大量排查时间。
3. 爬虫主体实现:从请求到解析
3.1 请求会话与头信息管理
写爬虫第一课,就是别裸奔。直接用一个空请求头去访问站点,对方一眼就能看出是脚本。我的做法是构建一个“看起来正常”的浏览器会话:
import requests from fake_useragent import UserAgent session = requests.Session() ua = UserAgent() HEADERS = { "User-Agent": ua.chrome, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", } session.headers.update(HEADERS)这里有个容易踩坑的点:很多人觉得User-Agent要每次随机换才安全,其实大错特错。一个会话中频繁切换UA,会导致“浏览器指纹”前后矛盾,反而更容易被识别。正确做法是一整个会话周期内保持UA稳定,只在会话重建时才更新。
首页要先访问一次,模拟真实用户进入站点的动作,然后再请求搜索接口。这样请求的Cookie和会话状态会更完整。我实测下来,直接跳过首页冷启动去请求内页,碰到验证页的概率会高不少。
3.2 列表页解析与链接提取
新闻站点的搜索列表页结构不复杂,但提取时我还是建议用相对宽松的选择器,给自己留点容错空间。比如帖子列表项,我不锁定某个极度具体的层级,而是用一个相对通用的选择器去挨个尝试:
from parsel import Selector def parse_list(html: str): sel = Selector(text=html) items = sel.css("div.item, article, .news-item") # 尝试多套容器 results = [] for it in items: title = it.css("a::text").get() url = it.css("a::attr(href)").get() if title and url: results.append({"title": title.strip(), "url": url.strip()}) return results这里同时匹配多个可能的容器类名,就是为了应对站点前端小范围调整。哪怕它加了新的class包装层,只要其中一个选择器还在,就能兜底拿回数据。当然,这种宽松匹配偶尔会混入广告或者推荐位链接,所以我会在清洗环节统一做过滤。
3.3 AI辅助的正文抽取与字段补全
详情页解析是重头戏。我设计的是“规则优先、AI兜底”的双通道方案。
规则通道长这样:先用XPath定位常见的正文容器,比如div.article_content、div#Content,然后校验抽取出来的文本长度。如果正文超过200个字符,且文本中没有大量“登录”“注册”“广告”这类噪音词,就判定为有效,直接使用。
规则通道判定失败时,进入AI通道。流程是先把整个详情页的可见文本提取出来,按长度截断到一定范围,然后调用模型做字段抽取。提示词我写得非常具体:
你是一个新闻正文提取器。下面是某新闻网页的可见文本(已经去除了脚本和样式)。 请从中抽取以下字段:1. 新闻标题;2. 发布日期;3. 新闻正文完整内容;4. 作者。 正文内容必须逐字保持原样,不要改写、不要总结、不要翻译。 如果某个字段不存在,输出为空字符串。输出为JSON格式。返回的JSON再做一层字段校验,防止模型漏字段。这里有个心得:给AI的文本块不要有冗余导航信息和大量超链接文本,先用规则粗筛一遍噪音,再丢给模型,准确率至少能提高两成。前期我贪省事,直接把整页文本丢过去,模型经常把侧边栏的“热门新闻标题”也当成正文主体提取出来,后来加了预处理才治好。
4. 反爬实战:识别、规避与应对
4.1 常见的反爬现象与识别方法
这一节是纯实战经验。新闻站点对爬虫的检测主要集中在三块:请求频率、请求头特征、以及访问路径是否符合用户习惯。
- 请求频率触发:短时间内请求数超过阈值,站点直接返回403或者跳转验证页。
- 请求头异常:缺少Referer、Accept-Language等常规字段,或者Accept字段跟浏览器差异过大。
- 行为异常:打开网页后没有加载图片、没有停留时间就直接跳转下一页;或者只请求列表页而不请求首页静态资源,这种机器特征非常明显。
怎么识别自己被盯上了?我总结了几个标志:返回状态码突然变成403或302;页面内容里出现机器人验证关键词;响应时间异常变长,比平时多了好几倍;拿到的HTML里既没有列表项也没有错误页内容,而是验证JavaScript脚本。遇到这些情况,第一反应不是换代理猛怼,而是立刻停止当前任务,进入熔断状态。
4.2 优雅的请求节奏与异常降级
我在请求模块里写了一个“指数退避重试”机制,效果非常稳。核心逻辑是:连续请求失败后,每次重试的等待时间按指数增长,比如第1次5秒,第2次10秒,第3次20秒,直到达到预设上限。如果连续5次都失败,就直接放弃这个请求,标记失败并写入日志,而不是死磕。
import time import random def robust_get(session, url, max_retries=5): wait_time = 5 for attempt in range(max_retries): try: resp = session.get(url, timeout=10) if resp.status_code == 200: return resp.text elif resp.status_code in (403, 429): print(f"[warning] {resp.status_code} on {url}, wait {wait_time}s") time.sleep(wait_time + random.uniform(1, 3)) wait_time *= 2 else: return resp.text except requests.RequestException as e: print(f"[error] {e}, retry in {wait_time}s") time.sleep(wait_time) wait_time *= 2 return None这套机制的亮点在于:它不是“死等固定时间”的傻瓜重试,而是根据失败严重程度动态调整节奏。遇到403这种风险信号时,等待时间会更长,相当于主动降温,让站点的风控系统“忘掉”你。实测下来,使用指数退避后,项目跑完全程被验证页拦下的次数几乎变成了零。
4.3 验证码场景的稳妥处理
这个项目里,我不建议做任何自动破解验证码的操作,没意义也不合规。正确的思路是“降低触发概率”和“触发后的人工接管”。
降低触发概率的措施:保持会话的User-Agent稳定;两次爬取之间加随机暂停;控制单个关键词的抓取总数,不要一个关键词疯狂翻几十页;以及最重要的——抓取过程加一个时间窗口,比如每轮任务只跑20分钟,然后休息半小时,把整体请求分布拉平。
触发验证后怎么办?程序会检测到验证页特征,然后暂停任务,给出日志提示,等待人工在浏览器里打开同站点完成验证后,再把新的Cookie喂回程序,继续未完成任务。这个“人机协同”的方式虽然原始,但可靠,而且完全在法律和平台规则允许的范围内。
5. 数据清洗与存储完善
5.1 清洗规则与标准化流程
爬虫拿回来的原始数据一定是不干净的,这一步别省。我做了一个独立的清洗模块,按顺序执行五类清洗动作:
- 字符清洗:去掉HTML实体、零宽字符、连续空白符,统一全半角。
- 空值处理:标题、链接、正文任一字段为空就标记异常;正文长度小于100字符的视为无效样本直接丢弃。
- 日期标准化:新闻发布时间格式五花八门,统一转成
YYYY-MM-DD HH:MM:SS,转换失败就置空并记录。 - 广告与推荐位过滤:正文里混入的“相关阅读”“推荐”“订阅新闻”等噪声词,用规则库加关键词匹配剔除。
- 语言一致性检查:如果目的是收集英文新闻,就把夹杂的纯中文段落过滤掉;反之亦然。
这里有个特别实用的小技巧:用html2text这类库先把HTML正文转成纯文本,再用正则做清洗,比直接在HTML字符串上做正则要方便得多,很多嵌套标签带来的脏数据会在转换过程中被自然抹平。
5.2 去重与增量爬取设计
去重是新闻爬虫最容易忽略的坑。同一篇文章可能被搜索接口重复返回,也可能详情页上的“相关推荐”又给了一遍。我的方案是双重去重:
第一层是URL去重,维护一张哈希表,记录所有已抓取的文章链接。第二层是标题相似度去重,因为有的文章URL带跟踪参数会变,但标题一致。具体做法是抽取标题的前20个字符做归一化后作为去重键,匹配到就跳过。
增量爬取这块,我在存储表里加了last_scraped_at字段,每次任务启动时先读取上次抓取的时间范围,只处理这个时间范围之后发布的内容。这样重复任务跑起来就不用从头全量抓一遍,省时省力,也减少了对站点的访问。
5.3 存储结构设计
存储我选了SQLite,不是因为它的功能有多强,而是天然适合这种单机、单项目、数据量在十万级以下的使用场景。表结构很简单:
CREATE TABLE IF NOT EXISTS news_articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT NOT NULL, -- 触发抓取的关键词 title TEXT NOT NULL, -- 文章标题 url TEXT UNIQUE, -- 原文链接 publish_date TEXT, -- 规范后的发布时间 author TEXT, -- 作者 content TEXT, -- 清洗后的正文 raw_hash TEXT, -- 正文哈希,辅助去重 scraped_at TEXT DEFAULT (datetime('now', 'localtime')) );url字段加上UNIQUE约束,数据库层面又兜了一层去重,比只在代码里维护集合更稳妥。raw_hash字段用来做内容级去重,即使同一个URL下正文内容发生变化也能感知到。
抓取完成后,我还会把结果导出成CSV和JSON两种格式,方便后续用数据分析工具做词频统计、情感分析之类的工作。这一步的体验被很多爬虫教程忽略,但实际做完会发现,当你需要把数据交给别的程序时,一个干净的标准格式文件比什么都有用。
6. 常见问题与排查心得
6.1 高频问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 请求返回403 | UA异常或频控触发 | 检查UA稳定性;确认上一次请求与本次间隔;查看返回页特征 |
| 列表解析不到链接 | 选择器过时或页面结构变化 | 保存HTML到本地,用浏览器开发者工具手动对照选择器 |
| 正文抽取内容过短 | 规则XPath命中错误容器,或AI截断文本 | 打印候选节点class;调整AI输入长度;检查是否命中评论区 |
| 同一篇文章反复入库 | URL含跟踪参数 | 去掉URL参数后做去重;使用标题相似度去重 |
| 时间字段解析失败 | 源页面日期格式不统一 | 写多套日期格式的解析规则;失败时置空而不是抛异常 |
| AI抽取偶尔乱输出 | 提示词不够明确或输入文本噪音过大 | 添加上下文约束;提前用规则清理HTML文本后再喂AI |
6.2 一次真实调试验收记录
项目联调那天遇到一个挺典型的问题:列表页能正常拿到详情页链接,但访问详情页时,连续出现403。排查的时候我没有直接加长休眠,而是先保存了返回的HTML看一眼——果然是一个验证页。问题出在我前面用脚本直接访问内页,而没有先访问首页做“用户入场”,会话缺少正常的站点访问轨迹。
处理方式是在每次新会话建立时,先请求一次首页,休眠几秒再进入列表页,并且加上Referer指向搜索入口页。这个改动不起眼,但效果立竿见影,403出现频率骤降。后来我把这套“入场三连”(首页预热、随机间隔、Referer补全)写成了公共方法,所有请求都走它,之后再没被大规模拦截过。
6.3 心得总结与其他建议
跑完整个项目,我最大的体会是:爬虫工程化最核心的不是代码技巧,而是“容错设计”。网络请求一定会失败,反爬一定会出现,数据一定会有脏的——这些问题都是必然事件,不是偶然事件。所以写代码时要把“失败”当成第一公民来对待,重试、熔断、降级、日志样样不能少。
我给想做同类项目的朋友三个建议:
第一,先把规则通道做扎实,再上AI。AI是兜底策略,不是主力,别本末倒置。
第二,日志记录要详细到“哪个URL、失败原因、重试次数、当前时间”,排查问题会轻松很多。
第三,准备好一个“退出机制”,当数据量已经满足需求时,程序要能自动停止,而不是贪婪地一直抓下去。做技术的人对数据要有敬畏心,够用就好,别给目标站点造成不必要的负担。
这个项目后续还可以继续扩展的方向不少,比如用抓下来的语料做关键词共现分析、新闻热点趋势追踪,甚至对接一个简单的推荐系统玩一玩。底子打好了,往哪个方向长都不难。