☰
异步爬虫与AI解析实战:构建稳定高效的竞品监控系统
2026/10/17 7:51:27 网站建设 项目流程

前阵子有个做电商运营的朋友跑来问我:竞品改个价格、换张主图、上线个新活动,怎么才能第一时间知道?手动刷网页不现实,写个普通爬虫,人家反爬一升级就全线崩溃,数据还净是些没用的广告和导航噪音。聊完之后我意识到,这其实是个很典型的“数量、速度、结构化”三维问题——页面多、更新快、信息杂,而答案写在这个标题里:异步技术解决速度和资源占用,AI解析解决信息噪音和结构变化,合起来就是一个能长期跑的竞品智能监控系统。

这篇文章我就按实际落地顺序,把这套系统的设计思路、关键代码、以及我在维护过程中踩过的坑一次说清楚。内容不依赖任何特定目标站点,适配资讯、电商、SaaS产品页这类以列表页加详情页为主要结构的网站。想搭一套能睡个好觉的监控服务的朋友,可以直接参考这套方案。

1. 需求分析与项目整体设计

动手写代码之前,先花点时间把需求拆清楚。竞品监控看着简单,但“监控”这个词背后其实藏着三件完全不一样的事:采集、理解、通知。采集是把页面拿回来,理解是从页面里抽出“发生了什么事”,通知是把变化变成一条你能看到的消息。很多爬虫项目死在第二步——广度有了,深度没有,抓了一堆HTML,却回答不了“竞品到底变了什么”这个核心问题。

1.1 明确监控对象与频率

第一件事是给监控对象画个边界。我习惯用一张表格把所有要监控的信息列出来,标清楚类型、更新频率和重要程度。以电商类竞品站点为例,大概长这样:

监控维度具体内容建议轮询频率重要等级
价格信息售价、划线价、折扣力度5-10分钟高
库存状态有货/无货、采购限制10-30分钟中
内容变更标题、卖点文案、参数表1-6小时中
运营动作活动位、Banner、新上架商品1-6小时中
用户反馈评价数量、评分变化、新增评价30-60分钟高

频率不是越猛越好。某电商平台的商品接口,5分钟以内基本不会变价格,一分钟抓三次属于自己跟自己过不去,还容易触发风控。我的经验是:动态数据(价格、库存)用短周期,静态内容用长周期,运营动作用快照对比。把频率在工程上做成一张可配置的表,比写死在代码里强得多。

1.2 为什么选择异步框架而非并发多线程

选型的时候很多人第一反应是开线程池。线程池在背了大量HTTP请求的时候,线程切换开销和内存占用会变得很难看,几百上千个并发任务同时挂着,还没抓到页面,自己的机器先开始卡了。异步的优势在于单线程事件循环,一个进程可以挂住成千上万个等待中的请求,本质上是把“等人响应”的时间让给了别的任务。

用生活类比解释:多线程就像开几十个收银窗口,每个窗口配一个收银员;异步就像只有一个收银员,但他在等顾客掏钱的间隙,同时招呼后面排队的顾客先扫码。顾客多的时候,异步模型明显更省资源。配合连接池复用,网络连接数也能控制在一个合理范围内,对目标服务器也相对礼貌。

这套方案选aiohttp做HTTP客户端,原因有三个:一是原生支持异步,用起来直接;二是连接池的SSL会话复用能显著降低握手开销;三是配合代理轮换也方便,中间件挂载逻辑比较成熟。

1.3 整体架构:四层各司其职

一个能长期跑的系统,我建议切成四层。采集层负责并发抓取页面,核心指标是吞吐量和超时控制;解析层负责把HTML转成结构化数据,这一步是AI介入的主要位置;存储层负责把增量数据落库,我这边用的SQLite,数据量再大可以平滑切到PostgreSQL;通知层负责把变化通过企业微信、钉钉或者邮件推给你。

每层之间用消息带结构体传数据,不用全局变量共享。这四层协作起来,抓、解、存、报各管一段,某一段挂了不影响其他段重试补跑。这个结构最大的好处是,想换掉任何一层都容易——比如今天想从某平台的React页面切到另一个站点,只需要改采集和解析两层,存储和告警完全不用动。

2. 异步采集层的实现与并发控制

采集层是整个系统的发动机。这里我把目标站点当作一个典型的异步加载网站来处理:列表页用浏览器开发者工具找到接口地址,详情页通过请求参数拼接。整个过程不依赖浏览器模拟,性能和资源占用都更可控。

2.1 用aiohttp实现异步抓取的骨架

下面是一个可以直接跑的异步抓取骨架,所有请求共用一个ClientSession,这是异步爬虫性能的关键之一。新建连接、TLS握手这些操作都很贵,Session的cookie和连接状态是复用的,能省下大量时间。

import asyncio import aiohttp from typing import List, Dict async def fetch_one(session: aiohttp.ClientSession, url: str, retry: int = 3) -> str: for attempt in range(retry): try: async with session.get(url, timeout=10) as resp: if resp.status == 200: return await resp.text() elif resp.status in (403, 429): await asyncio.sleep(5 * (attempt + 1)) else: await asyncio.sleep(1) except asyncio.TimeoutError: continue except aiohttp.ClientError as e: continue return "" async def crawl_urls(urls: List[str]) -> Dict[str, str]: timeout = aiohttp.ClientTimeout(total=15) connector = aiohttp.TCPConnector(limit=20, limit_per_host=5, ttl_dns_cache=300) async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session: tasks = [fetch_one(session, url) for url in urls] results = await asyncio.gather(*tasks, return_exceptions=True) return {url: (results[i] if isinstance(results[i], str) else "") for i, url in enumerate(urls)} if __name__ == "__main__": test_urls = [f"https://example.com/products/{i}" for i in range(50)] pages = asyncio.run(crawl_urls(test_urls))

这里有个容易忽略的点:TCPConnector(limit=20, limit_per_host=5)表示全局并发连接控制在20条,单个host最多同时放5条连接。别小看这个参数——很多初学爬虫的朋友追求极致的并发,一上来就开几百个任务同时打同一个站,结果被封了IP再来问怎么回事。把全局并发压到20以下,单站并发控制在5左右,既是性能的甜点区,也是风控的舒适区。

2.2 优雅的重试机制与退避策略

网络请求不可能每次都成功,重试机制的写法决定了你在对方服务器日志里的画像。直接用for循环套sleep是最基础的做法,但有几个细节值得优化。

第一,不要固定间隔重试。用指数退避配合抖动,比如第n次重试等待min(60, 2^n + random.randint(0, 3))秒。第二,对状态码分类处理:403、429这类需要等待限流的,5秒起步,逐次翻倍;5xx这类服务器错误的,可以短间隔重试。第三,重试不是无限循环,三次不成功就放弃,把失败记录到日志里,让监控层去补采。

一个实测有效的退避模板长这样:

import random def retry_delay(attempt: int) -> float: if attempt >= 5: return 60 return min(30, (2 ** attempt) + random.randint(0, 3))

我见过最夸张的一个项目,重试间隔写死1秒,被限流了重试三次还是1秒,相当于自己把IP送给风控系统。退避策略不只是为了成功,更是为了显得“像个人”。

2.3 处理目标接口的签名加密与字段混淆

现在竞争比较激烈的站点普遍上了签名校验和字段混淆,这属于反爬的一个常规分支。常见的做法是通过JavaScript逆向定位参数生成逻辑,再用Python复现一遍,或者用某种动态执行方案直接跑原站脚本。

实操时有个更轻量级的思路:优先检查请求头里的签名参与参数,很多时候某平台用的是固定算法加时间戳,逆向难度并不高。比如某个常见套路是MD5拼接参数字典再转大写,签名结果带在URL参数里。用两个请求对比就能发现签名的变化规律。

不过我不建议把精力全部投入对抗签名。更聪明的策略是“避开中心,走侧门”——找找移动端接口、旧版接口或公共搜索接口,这些地方往往防护弱、数据一样完整。我做过一个项目,PC端接口签名逆了三天,最后发现移动端接口裸奔,十分钟就对接完了。

注意:合规采集的前提是尊重目标网站的robots协议和访问限制,控制频率,避免影响对方正常服务。这既是技术建议,也是底线原则。

3. AI解析层的接入与稳定输出

HTML解析是每个爬虫项目里最头疼的一环。传统方案是写XPath或CSS选择器,但竞品站点改版频繁,选择器一断,整个监控就哑了。AI解析的思路完全不同:不关心结构,只关心内容和语义,把整块文本丢给模型,让它按需要的字段吐JSON。

3.1 为什么用AI替代正则与XPath

选择器方案的维护成本,我算过一笔账。目标站点平均每三个月改一次前端结构,每次改版,平均需要两天重新定位元素、调试选择器、回归历史数据兼容。一年下来,维护解析层的成本占了整个项目的三分之一以上。AI方案把“定位元素”这件事,换成了“理解页面语义”,前端怎么改都不影响提取结果,这个优势太明显了。

另外,竞品站点的信息里面通常夹杂着推荐商品、广告、相关文章等大量噪声。用CSS选择器提取时这些干扰需要逐一排除,而AI可以直接做到语义级别的过滤——“只保留商品标题、价格、口碑信息,忽略导航和广告内容”。

代价是成本和耗时,所以工程上要做取舍。我的方案是分桶处理:动态核心内容(价格、库存)用轻量正则截取,只把结构复杂的那部分交给AI解析。说到底,剑是用来打开突破口的,不是用来拍蒜的。

3.2 设计稳定的解析提示词与输出格式

AI解析要稳定,80%的功夫花在提示词上。一个合格的解析提示词应该包含四个要素:角色限定、输入示例、输出格式、容错说明。下面是一个通用模板,适配大多数商品详情页:

你是一个页面信息提取助手。请从给定的HTML纯文本中提取结构化数据。 要求: 1. 只返回JSON,不要输出任何解释性文字。 2. JSON字段固定为:{"title": "...", "price": "...", "original_price": "...", "stock_status": "...", "rating": 4.5, "description": "..."} 3. 如果某个字段无法提取,用null填充,不要自行编造。 4. 注意过滤导航、广告、推荐位等无关内容。 HTML文本如下: <HTML_COTENT_PLACEHOLDER>

这里有个细节,用,分隔5个字段的时候,模型偶尔会在JSON里加多一个逗号,导致解析报错。我一般会把正则多写一层兜底,把JSON前后的大括号内容抓出来再交给json.loads,成功率能从95%拉到99%以上。

有个更省成本的办法:不是所有页面都需要AI解析。我的策略是先用正则抽取带特征的结构化数据,比如用"price": "¥198.00"这种带货币符号和两位小数的固定模式;只有遇到匹配失败的页面,才进入AI解析,这样每千次请求里AI调用量会少很多。AI做的是兜底,不是所有请求的主通道。

3.3 向量化与语义去重:识别真正的变化

监控系统的核心价值不在“抓到了内容”,而在“发现变更”。但实时采集时,同一条内容回传多次,怎么判断哪次是真正的变化?传统方案是字段比对,价格变了就是变了;内容上却要处理“改了个标点也算变化吗”这类问题。

这里我引入了一个低成本语义去重方案。文本内容先用哈希摘要过滤完全相同的,再用嵌入式模型把标题和描述转成向量,计算余弦相似度,低于阈值才算“语义变了”。实测下来,单纯改标点、改错字、微调语序的变更基本能过滤掉,真正改卖点、改配置的变更会准确触发告警。

向量化这一层不需要频繁调用,每条内容入库时算一次即可。我用的是本地运行的小模型,响应速度在毫秒级,不增加多少成本。如果你跑的量不大,也可以直接用编辑距离做一个简化版本——效果差一些,但成本更低。

4. 监控逻辑、数据存储与告警通知

抓下来的数据放在哪、怎么判断该不该通知你,这俩问题看上去简单,实际写起来全是细节。这里重点说一说快照比对和告警防抖,这两块是决定监控系统体验好不好的关键。

4.1 增量快照与变化比对策略

数据入库不是无脑覆盖,而是存“快照”。我用的表结构大致包含:页面唯一标识、内容hash、价格、标题、描述、采集时间、变化类型。每次新抓取的数据进来,先按照页面ID取上一次的记录,对比内容hash,不同则再逐字段比对,标记变化类型。

整体流程可以理解为:写入一条新记录,和上一版本比较,更新版本号并记录变化字段。这里必须强调的是,变化类型要分细:价格变化、标题变化、描述变化、库存变化、新增页面、页面下架。粗粒度只会告诉你“变了”,但运营要的是“价格降了50块,库存从缺货变成有货”。

存储层我用SQLite起步,注意设置WAL模式,否则写并发一上来,整个库会被锁得死死的,读都读不了。

PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, page_key TEXT NOT NULL, content_hash TEXT, title TEXT, price REAL, stock_status TEXT, rating REAL, raw_data TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, change_type TEXT DEFAULT 'unknown' ); CREATE INDEX IF NOT EXISTS idx_page_time ON snapshots(page_key, created_at DESC);

4.2 告警触发条件与防抖设计

告警是最容易“狼来了”的地方。刚开始做的时候,我在测试阶段把一个低频站点设成五分钟抓一次,结果一个平台改了页面版权字串,我收到了几十条“内容变更”的通知。后续就加了一个防抖机制:同一页面,同一变化字段,默认在15分钟内只通知一次。

防抖的实现很简单,维护一张变更记忆表,记录页面Key、变化字段、发布时间。新告警请求进来时,先查这张表,如果存在且未过期就跳过,否则更新表并触发通知。阈值也可以做成列表页级别的配置,比如高价商品的库存变化,五分钟通知一次就够了,活动页的Banner变化,两个小时汇总一次更合适。

4.3 多通道通知接入与消息格式

通知通道我建议同时接两个:一个用于即时告警,一个用于日报汇总。企业微信机器人、钉钉机器人或者飞书机器人,都是比较常见的即时通道。消息格式用Markdown即可,通常包括:监控对象名称、变化类型、旧值、新值、页面链接、采集时间。

另外建议加一档日汇总报告——把当天所有监控对象的变化汇总到一个表格发出来。早晨到公司扫一眼日报,比被二三十条即时通知轰炸要舒服得多。我自己的策略是:高优先级动态字段走即时通知,低优先级内容字段走日报。

5. 部署运行与长期稳定性维护

系统写完只是起点,让它稳定跑三个月不塌才是考验。这里讲部署的形态和数据质量的保障,都是我自己跑线上服务时积累下来的经验。

5.1 定时调度与任务编排

线上跑监控,最稳妥的调度方式是用系统级定时任务,而不是自己在进程里写while循环。我习惯把采集任务按站点、按模块拆成独立脚本,再通过cron表达式控制频率。比如:

*/5 * * * * cd /opt/price-monitor && /usr/bin/python3 run_crawler.py --task price >> logs/crawler.log 2>&1 */30 * * * * cd /opt/price-monitor && /usr/bin/python3 run_crawler.py --task stock >> logs/crawler.log 2>&1

这样有个额外的好处:某一类任务写挂了,其他任务照常跑,互不干扰。配合日志切割工具,日志归档和清理也可以自动化管理。

任务编排要注意错峰。不要整点整分的节奏执行,而是给每个任务加上一个随机偏移。比如计划是每5分钟执行,实际执行的分钟偏移可以设定为2-4分钟之间的随机数。这样多站点采集时请求时间自然错落,能明显降低被集中识别的风险。

5.2 数据质量校验与失败补偿机制

数据采集最怕的不是抓不到,而是抓到脏数据。价格字段偶尔会抓到“查看详情”这样的非数字文本,标题可能截断半句,这些脏数据一旦进入快照库,后续所有变化比对都会跟着错。

我的解法是入库前加一层强校验规则。价格字段必须匹配十进制数字格式,标题长度在5到200个字符之间,库存状态必须是枚举值集合之一,字段缺失时直接标记为无效记录,不写入快照表。校验失败的数据进入延迟队列,下一个采集周期会重新抓取。

失败补偿机制也很重要。每个任务的失败记录都会写入对应表里常驻,补偿任务定期扫描该表,把失败的URL重新加入待抓队列。衡量系统健康的指标不应该是“抓取了多少页面”,而是“解析失败率”和“校验通过率”。这两个指标始终控制在合理范围内,比单纯页数增加要重要得多。

5.3 日志规范与监控看板

日志写得好不好,直接影响排障效率。我的日志规范是固定的JSON格式:时间戳、任务名、级别、URL、异常信息、耗时。每次抓取和解析完成后都输出一条结构化日志,这样无论排查问题还是做数据统计都很方便。

在这个基础上加了一个简单的健康看板,展示几个关键指标:最近一轮抓取成功率、解析任务队列长度、告警触发次数、平均响应耗时。当成功率跌到某个阈值以下、请求失败率异常升高、平均耗时翻倍时,说明系统的状态不太对劲,需要去日志里找原因了。看板不需要做得太花哨,一屏能看到五六项关键指标就够了。

6. 常见坑位与排障速查

最后把这些年踩过的坑集中整理成速查表。每一个都是真实线上环境里遇到过的,按频率排序,你可能迟早也会撞上其中一个。

问题现象可能原因解决思路
异步代码偶发报错,重跑就正常触发了连接池并发上限调低limit_per_host,增加随机启动偏移
页面解析出来的价格偶尔是NoneHTML结构临时变动接入AI兜底解析,校验失败自动重试
告警频繁轰炸同一个页面未做防抖增加15-30分钟的防抖窗口
数据库读写冲突,跑批卡死SQLite未开WAL或锁等待开启WAL模式,写操作串行化
代理IP采集时好时坏代理池权重分配不合理对失败率高的实例降权,周期性探活
内存占用持续上涨协程未释放或日志无限增长检查任务引用,配日志轮转策略
站点接口签名频繁变化加密逻辑可能带随机因子优化逆向策略,转向移动端接口
定时任务偶发重叠,数据重复上一次任务未结束,下一轮已启动加全局锁,任务执行超时强制解锁
AI解析输出非法JSON输出受文本长度或无关噪声干扰增加兜底正则,截取{...}区间再解析
变更比对大量误报hash粒度太细改用语义去重,忽略标点和微调差异

提到代理IP,这里有句经验之谈:如果目标站点没有明显的IP限制,就不要上代理,一条稳定住宅线路比一堆透明代理池强得多。代理这东西是双刃剑,质量不稳定的时候不仅不帮你绕风控,还会给你自己的任务添乱。很多刚开始做监控的朋友一上来就接代理池,结果排查了半天发现是代理自身的响应超时。

关于合规再强调一句,做竞品监控请控制抓取频率,设置合理的限速,“礼貌采集”不仅能让你的系统更稳定,也是技术工作者该有的基本素养。

我个人的体会是,这套系统的真正难点从来不在怎么写爬虫,而在于怎么把异步采集、AI解析、变化判断、可靠通知这几个环节拧成一股绳。之前的项目里,我花了大约一星期完成第一版,后续的大多数改动几乎都集中在解析提示词优化和防抖策略调参上,采集层反而一直稳定运行着。如果你也想搭一套类似的监控,建议从一个小范围、高价值的场景切入,跑通全流程后再扩展站点数量,比一上来就追求大而全要靠谱得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询