AI辅助新闻定向爬虫:反爬策略与数据清洗全流程实践
发布时间:2026/10/17 5:22:56 锦皓数字建站

做新闻站点的定向内容抓取这几年我前前后后折腾过不少。这次这个项目目标很明确基于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 lxmlXPath和CSS选择器都支持解析速度比纯BeautifulSoup快数据处理pandas清洗阶段处理表格数据很方便数据库SQLitePython内置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/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: en-US,en;q0.5, } session.headers.update(HEADERS)这里有个容易踩坑的点很多人觉得User-Agent要每次随机换才安全其实大错特错。一个会话中频繁切换UA会导致“浏览器指纹”前后矛盾反而更容易被识别。正确做法是一整个会话周期内保持UA稳定只在会话重建时才更新。首页要先访问一次模拟真实用户进入站点的动作然后再请求搜索接口。这样请求的Cookie和会话状态会更完整。我实测下来直接跳过首页冷启动去请求内页碰到验证页的概率会高不少。3.2 列表页解析与链接提取新闻站点的搜索列表页结构不复杂但提取时我还是建议用相对宽松的选择器给自己留点容错空间。比如帖子列表项我不锁定某个极度具体的层级而是用一个相对通用的选择器去挨个尝试from parsel import Selector def parse_list(html: str): sel Selector(texthtml) 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_retries5): wait_time 5 for attempt in range(max_retries): try: resp session.get(url, timeout10) 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 高频问题速查表问题现象可能原因排查方向请求返回403UA异常或频控触发检查UA稳定性确认上一次请求与本次间隔查看返回页特征列表解析不到链接选择器过时或页面结构变化保存HTML到本地用浏览器开发者工具手动对照选择器正文抽取内容过短规则XPath命中错误容器或AI截断文本打印候选节点class调整AI输入长度检查是否命中评论区同一篇文章反复入库URL含跟踪参数去掉URL参数后做去重使用标题相似度去重时间字段解析失败源页面日期格式不统一写多套日期格式的解析规则失败时置空而不是抛异常AI抽取偶尔乱输出提示词不够明确或输入文本噪音过大添加上下文约束提前用规则清理HTML文本后再喂AI6.2 一次真实调试验收记录项目联调那天遇到一个挺典型的问题列表页能正常拿到详情页链接但访问详情页时连续出现403。排查的时候我没有直接加长休眠而是先保存了返回的HTML看一眼——果然是一个验证页。问题出在我前面用脚本直接访问内页而没有先访问首页做“用户入场”会话缺少正常的站点访问轨迹。处理方式是在每次新会话建立时先请求一次首页休眠几秒再进入列表页并且加上Referer指向搜索入口页。这个改动不起眼但效果立竿见影403出现频率骤降。后来我把这套“入场三连”首页预热、随机间隔、Referer补全写成了公共方法所有请求都走它之后再没被大规模拦截过。6.3 心得总结与其他建议跑完整个项目我最大的体会是爬虫工程化最核心的不是代码技巧而是“容错设计”。网络请求一定会失败反爬一定会出现数据一定会有脏的——这些问题都是必然事件不是偶然事件。所以写代码时要把“失败”当成第一公民来对待重试、熔断、降级、日志样样不能少。我给想做同类项目的朋友三个建议第一先把规则通道做扎实再上AI。AI是兜底策略不是主力别本末倒置。第二日志记录要详细到“哪个URL、失败原因、重试次数、当前时间”排查问题会轻松很多。第三准备好一个“退出机制”当数据量已经满足需求时程序要能自动停止而不是贪婪地一直抓下去。做技术的人对数据要有敬畏心够用就好别给目标站点造成不必要的负担。这个项目后续还可以继续扩展的方向不少比如用抓下来的语料做关键词共现分析、新闻热点趋势追踪甚至对接一个简单的推荐系统玩一玩。底子打好了往哪个方向长都不难。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。