资讯详情

资讯详情

爬虫技能化与定时任务管理:CoPaw skill 架构落地实践

做爬虫这些年我最烦的不是反爬而是脚本越攒越多、任务调度基本靠手动cron、换台机器就得重新折腾一遍。后来把爬虫拆成 CoPaw 里的 skill 技能再把执行交给定时任务管理整套流程才算真正顺了。这篇东西我会从 skill 工程结构、核心设计、调度管理、踩坑记录到长期维护按我实际落地的顺序讲一遍适合正在用 CoPaw 或者类似框架做采集的同学参考。1. 为什么爬虫要拆成“skill技能”而不是写死脚本早期我做采集方式是每个网站一个 Python 脚本爬哪个就跑哪个。问题很典型解析规则和调度逻辑全揉在一起改一个选择器要在十几个文件里翻任务要每天跑靠系统 crontab日志到处散落丢了数据也不知道换台机器部署依赖版本冲突能折腾一晚上。后来把爬虫重构成 CoPaw 框架里的 skill 技能核心变化是爬虫只负责“给定目标返回结构化数据”框架统一接管触发时机、输入校验、运行沙箱、日志收集和失败重试。这个思路有点像把电器的插头做成了统一标准任何插座都能供电电器本身不用关心电从哪来。按我的使用经验有几种做法可以对比看看差异方案可复用性调度灵活性维护成本适用场景零散 Python 脚本低每站一套依赖 crontab扩展复杂高逻辑分散一次性采集独立爬虫服务中接口复用需要自建调度模块中独立部署较重固定几个长期项目CoPaw skill 技能高统一入口框架自带定时任务管理低职责边界清晰多站点、多变需求、需要和 AI 能力联动我个人判断什么样的爬虫适合做成 skill单任务量不大单次几万到几十万条量级、目标站点会持续变化需要频繁改解析规则、或者你希望爬到的数据能顺便接入更上层的分析流程。如果你是要做全网级抓取每秒上万请求那种那还是得上独立爬虫集群skill 这个形态处理不了那么重的工程问题这点要有清醒认识。2. 从零搭建 CoPaw 爬虫 skill目录结构、元信息与运行入口CoPaw 的 skill 机制简单说就是让你把一段“能力”封装成框架能识别的模块。我在某项目中跑通的工程结构长这样my_copaw_skills/ ├── skills/ │ ├── news_spider/ │ │ ├── skill.yaml # 技能元信息框架靠它发现技能 │ │ ├── main.py # 统一运行入口 │ │ ├── parser.py # 页面解析逻辑 │ │ ├── requirements.txt # 本技能依赖的第三方库 │ │ └── assets/ │ │ └── ua_list.txt # User-Agent 轮换池 │ └── ... ├── tasks/ │ └── schedules.yaml # 定时任务配置绑定 skill 和入参 └── copaw_config.yaml # 框架全局配置2.1 skill.yaml 怎么写得让框架认识你skill.yaml 是整个技能的身份证字段写对了CoPaw 才能正确注册并暴露给你的定时任务管理模块。我常用字段如下name: news_spider version: 1.2.0 description: 抓取某新闻站点列表页和详情页输出结构化标题、时间、正文摘要 author: anonymous_dev runtime: python3.10 entry: main.py tags: [spider, news, web] permissions: network: true filesystem: read input_schema: type: object required: [urls] properties: urls: type: array items: { type: string } max_pages: type: integer default: 10 delay_seconds: type: number default: 1.5 output_schema: type: object properties: status: { type: string } items: type: array items: type: object properties: title: { type: string } url: { type: string } publish_time: { type: string } summary: { type: string } task: timeout_seconds: 300 max_retries: 3有两点值得说。第一input_schema 和 output_schema 不是摆设CoPaw 的定时任务模块会在调度前做输入校验如果你的 cron 配置传参和 schema 不一致任务会在进入执行阶段之前就被拒绝这个特性能在源头拦住很多低级错误。第二task 块里的 timeout_seconds 和 max_retries 是技能层面的兜底——框架级的默认策略永远不如你在技能内部先声明一个合理的边界值因为只有写技能的人最清楚这个任务平均要跑多久。2.2 main.py 要遵守的统一接口契约CoPaw 执行 skill 时会调用 entry 指定的文件并约定一个统一的 main 函数签名。我经过几次迭代后总结出下面这套稳定的入口写法import json import logging import traceback from typing import Any, Dict logger logging.getLogger(news_spider) def main(payload: Dict[str, Any]) - Dict[str, Any]: payload 结构由 skill.yaml 的 input_schema 约束。 返回值必须是可 JSON 序列化的 dict。 urls payload.get(urls, []) max_pages payload.get(max_pages, 10) delay_seconds payload.get(delay_seconds, 1.5) results [] for url in urls: try: page_items crawl_list(url, max_pagesmax_pages, delaydelay_seconds) results.extend(page_items) except Exception as e: # 单 URL 失败不拖垮整个任务记录后继续 logger.warning(crawl %s failed: %s, url, traceback.format_exc()) return { status: ok, total: len(results), items: results[:50], # 完整数据可写入文件或数据库返回部分用于确认 }重点解释几个选择。为什么 main 的返回值只回传 items 的前 50 条而不是全量因为 CoPaw 的调度系统会把返回值写入任务日志如果数据量太大日志库会被撑爆。我在真实任务里是让爬虫把全量数据写到本地文件或数据库然后返回值里只包含 summary 和统计字段。定时任务管理页面要看的是“成功没有、抓了多少”不是让你回传整个数据库。框架本身不限制你只能在 main.py 里写逻辑但你确实应该在主入口保留一个薄薄的适配层负责解 payload、调核心逻辑、包装返回结果。这样当 CoPaw 框架升级导致调用约定变化时你只需要改 main.py 这一层不用动 parser 等功能模块。3. 爬虫 skill 的核心设计Schema 校验、解析策略与反爬控制skill 的壳做好了真正决定这东西好不好用的是内部的爬取与解析设计。这里我把跑的流程拆开讲清楚。3.1 先确定请求发送方式requests 和 httpx 我在这套架构里都用过。经验判断是这样的特性requestshttpxHTTP/2不支持支持异步接口需要另加线程池原生 async 支持连接复用一般较好生态成熟度非常高高对于单机定时采集的场景请求量不大用 requests 完全没问题。但如果你要在同一个 skill 里并发抓多个页面httpx 的 async 接口配合 asyncio.Semaphore 控制并发要比 requests ThreadPool 优雅很多。我后面的模板都是从 requests 改到 httpx 的少写不少线程安全的代码。3.2 页面解析的选择器优先级解析环节我踩过不少坑核心经验是能用 CSS 选择器就不用 XPath能用静态 HTML 解析就不上正则。CSS 选择器结构清晰、容错率高适合列表页、详情页通用结构的抽取XPath适合处理比较复杂的关系比如“当前节点的某个兄弟节点的文本”这种情况 CSS 写起来绕正则只用于提取埋点在 HTML 里的 JSON 数据比如 window.INITIAL_STATE这种别用于常规标签解析JsonPath目标站点有 JSON 接口响应的时候最爽配合 jmespath 库可以少写很多遍历代码。我在某新闻项目的解析流程里先用 CSS 选择器选出所有列表项容器再针对每个容器里的标题、链接、时间分别用选择器取。详情页里正文摘要我会选一个合适的 class 或 id 容器然后提取其文本并用空白符压缩。这里给一个 parser.py 核心片段from parsel import Selector def parse_listing(html: str, base_url: str): sel Selector(texthtml) items [] for card in sel.css(div.news-item): title_node card.css(h2 a::text).get() link card.css(h2 a::attr(href)).get() pub_time card.css(span.time::text).get() items.append({ title: title_node.strip() if title_node else , url: urljoin(base_url, link), publish_time: (pub_time or ).strip(), }) return items顺手说一下 parsel 的优点——它基于 lxml 封装选择了类似 CSS 的语法写起来很快而且对破损 HTML 的容错比标准库 html.parser 高不少这类“容错性”在处理真实站点页面时会省掉大量崩溃排查。3.3 反爬控制的三个层级爬虫跑得好不好一半在反爬策略。CoPaw 的定时任务管理能控制执行时机但控制不住目标网站的风控策略所以技能内部必须设计自己的反爬机制。我习惯分三级第一级基础层——UA 轮换、请求间隔、重试退避。UA 我维护了一个列表文件每次请求随机取一个浏览器 UA请求间隔通过 delay_seconds 参数控制既可以在 Cron 任务里按站点调整也可以在 skill 内部做公式化限速。遇到 429 或 5xx采用指数退避比如重试 3 次等待时间依次为 2 秒、4 秒、8 秒。第二级会话层——Cookie 和 Header 的一致性。只轮换 UA 但 Cookie 不变这套组合会被风控识别正确做法是同一个目标页面集合内用同一个 httpx.AsyncClient 实例发起请求维持会话的连贯性。如果遇到需要登录的站点把认证后的 Cookie 持久化到 skill 的数据目录并在每次请求前刷新。第三级流量层——代理池。说实话这套架构里我尽量不默认引入代理因为代理池的质量和维护成本会让一个轻量级爬虫技能变得很重。只有目标站点对单 IP 频率限制特别严时才配置一个简单的代理轮换表每次请求从表里取一个可用代理。注意代理连通性测试要放在任务入口否则会在失败重试上浪费时间。3.4 增量与去重避免每轮重复入库定时任务最尴尬的场景是同一篇新闻这轮抓到了下一轮又抓到了入库时造成重复。解决思路是给数据定义唯一性键。我倾向用“来源站 正文 URL 的 md5”作为天然唯一键在写入前先查询数据库是否已存在该键如果存在直接跳过。URL 做唯一键有个坑——同一篇新闻在不同页面可能有不同的追踪参数所以要把 query string 里常见的统计参数utm_source、from 之类的先剔除再计算 md5。更省事的方案是利用 URL 中自带的发布时间目录结构做增量判断比如某些站点 URL 里包含 /2024/05/xx/只在当天或当月日期范围内采集天然过滤掉旧内容。但不同站点的 URL 规律差异大这个办法依赖具体站点特性通用性不如去重键方案。4. 定时任务管理cron 表达式、任务生命周期与重试告警CoPaw 的定时任务模块核心价值是用来统一管理“什么时候跑”“跑完了没”“挂了怎么办”这三件事。下面按实际用法展开。4.1 cron 表达式与调度语义CoPaw 支持标准 6 段 cron秒、分、时、日、月、周和 5 段 cron我在配置里常见写法如下表达式含义使用场景0 30 8 * * ?每天 08:30 执行早间新闻采集0 0/30 * * * ?每半小时执行行情价格监控0 0 2 * * MON每周一凌晨 2 点执行每周报表数据0 15 9,15 * * ?每天 9:15 和 15:15 各一次定时汇总任务我建议初学者别在 cron 表达式里写死复杂的组合条件尽量用“每天固定时间”和“每周固定时间”两大体系可读性强、好排障。像“每个月倒数第 3 天”这样的特殊需求cron 很难表达不如在 skill 内部判断当月日期范围这样调度器只负责能说清楚的规则特殊规则留在执行逻辑里。4.2 定时任务的完整注册流程定时任务配置可以选择用 schedules.yaml 静态定义也可以用框架提供的 API 动态创建。静态配置的好处是随代码库走方便评审和回滚。我维护的典型写法- name: daily_news_crawler skill: news_spider cron: 0 30 8 * * ? timezone: Asia/Shanghai enabled: true input: urls: - https://news.example.com/tech max_pages: 20 delay_seconds: 1.5 notify: on_success: false on_failure: webhook: https://hooks.example.com/alert这里有一个值得注意的设计任务入参和 skill 的输入校验是解耦的CoPaw 在启动任务时会把 input 字段内容传给 skill但如果 input 不符合 schema 规范会直接产生校验失败错误。既然后面有校验层写 input 时就不要省略字段名——曾经有个同事把 max_pages 拼成 maxpage任务跑了半个月才发现一直用的是默认值。如果需要在运行时动态创建任务CoPaw 也提供了 Python 客户端或 HTTP API。我实际用了这个场景是“临时数据采集需求”业务方通过后台填表单框架自动生成一个只跑一次One-off 模式的定时任务跑完自动归档。这个模式比手动写脚本快不少后台操作者甚至不需要接触调度配置细节。4.3 任务状态机与失败重试CoPaw 任务的生命周期我用状态来理解pending任务已创建未到触发时间running正在执行 skill 的入口函数succeeded正常返回无异常failed执行异常如超时、网络不可达、Python 未捕获异常partly_failedskill 内部有子任务失败但整体返回了我们的 news_spider 单 URL 失败时就是这个状态定时任务管理系统的价值在这里体现得很明显它不只是触发器而是一个状态机。比如 failed 状态会自动触发重试默认策略是 job 失败后等待 5 分钟再重试最多 3 次。为什么不是立即重试因为很多爬虫失败的原因是目标站暂时性不可用立即重试大概率还是会失败反而给目标站造成额外请求压力。5 分钟这个间隔对“暂时性故障”来说足够缓解。我在上面 schedules.yaml 里配置了 notify.on_failure.webhook指的是失败超过重试阈值后CoPaw 会向指定 Webhook 推一条 JSON 事件包含任务名、skill 名、错误摘要和失败时间。这个告警链路配合钉钉或企业微信的机器人可以第一时间发现问题我实践的流程是把告警 Webhook 接入到一个内部群这样不用每天登录管理后台看任务状态。4.4 task 的并发与互斥定时任务按 cron 触发但一个任务上一轮没跑完下一轮触发了怎么办CoPaw 默认会跳过重叠触发不会在上一轮 running 未结束时强行开启新实例避免两个进程同时写数据库造成数据错乱。这个行为你在设计爬虫时要想清楚如果你打算并发跑多个实例来提速就需要每个实例有独立的输出路径或者幂等写入策略。我实际负责任务编排时会在 schedules.yaml 里额外加一条 max_parallel: 1把互斥作为底线策略宁可慢一点也不要脏数据。5. 实测踩坑记录五个最容易翻车的地方只要跑定时爬虫就有躲不开的坑。下面几个是我在不同项目反复踩过的每一个都有具体原因和解决办法。5.1 动态渲染页面抓不到数据很多站点列表数据是 JavaScript 异步加载的直接 requests 抓回来只有空壳 HTML选择器选不到任何元素。解决思路有两种一是直接找 XHR 接口。打开浏览器的开发者工具切到 Network 面板刷新页面找出返回 JSON 数据的请求地址很多站在列表页反而是接口直接给结构化的 JSON比正则解析 HTML 还稳定。这就是我在前面提到 JsonPath 解析的原因——有一次爬一个财经站纯 HTML 解析写了 200 行代码还总选错节点换成接口响应后 30 行就搞定正确率还更高。二是上浏览器渲染。如果目标是内容全部靠 JS 动态生成且不提供接口形式的同类数据就只能用 Playwright 或 Selenium 这类工具做真实浏览器操作。在 CoPaw skill 里集成 Playwright 的路径是在主入口先启动一个无头浏览器加载页面后等待特定元素出现显式等待再 dump 出渲染完毕的完整 HTML 交给 parsel 解析。但注意浏览器渲染的 CPU 和内存开销较大定时任务并发数量要下调我的经验是同一个节点上最多跑 2 个浏览器实例再多了会出现僵尸进程堆积。5.2 页面编码判断错导致乱码有些老站点不声明 charsetrequests 的响应默认编码是 ISO-8859-1拉丁编码中文页面就会变成一片乱码。解决办法很直接先看响应头里有没有 Content-Type 的 charset 字段没有的话用 chardet 或 charset-normalizer 检测字节流的编码再手动覆盖 response.encoding。我遇到过最离谱的是同一个站点首页是 UTF-8列表页是 GBK详情页又变成 UTF-8所以编码判断逻辑必须放在每个页面请求之后单独执行不能只在任务入口统一设置。还有一种情况HTML 里声明的是 GB2312但实际内容用了 GBK 扩展字符GB2312 解码会抛异常。处理方式是统一用 GB18030 代替 GB2312——前者是后者的超集向下兼容遇到特殊字符能稳定解码。这个小技巧在很多乱码问题里能一次解决。5.3 任务超时与会话泄漏如果目标站响应特别慢或者某个页面陷入无限重定向skill 进程可能会长时间卡住。CoPaw 的 timeout_seconds 配置会强制杀死任务进程但如果你用的是 httpx 或 requests 的默认超时设置即不设置超时在框架杀死进程之前这个 skill 可能已经连续占用了大量的网络连接和文件句柄。我现在的习惯是所有请求都显式设置连接超时和读取超时比如 10 秒和 30 秒同时用 with httpx.Client() 语法保证客户端实例关闭。另外在框架层面把技能的超时时间设置成比“预计正常耗时”多 50% 左右。比如某个抓取任务日常跑 5 分钟timeout_seconds 设 8~10 分钟留出抖动余量。不要设成 2 倍以上否则翻车时等待时间太久告警都不及时。5.4 幂等性问题导致重复入库这是定时任务的伴生问题同一轮任务失败重试或者手动补跑历史任务很容易把同一批数据再写一遍。除了前面提到的 md5 去重键还有一个更彻底的方案是给任务级别加 run_id——每次调度触发都生成全局唯一 ID入库的表里加一个 run_id 字段查询时按 run_id 先清理再插入。这样同一个任务即使重试多次最终数据库里保留的是最近一次运行的全量数据不会出现半新旧数据叠加的情况。这个 run_id 的价值在排查“数据怎么多了/少了”时尤其明显。5.5 时区问题导致定时偏差如果 CoPaw 部署的服务器是 UTC 时区而 cron 配置没指定 timezone 字段就会出现每天任务早跑 8 小时的诡异现象。我在 schedules.yaml 里每条任务都明确写 timezone: Asia/Shanghai不依赖服务器默认时区。这点在框架层面也许有默认处理但项目里如果有跨时区协作不同同学会按各自理解修改服务器配置显式声明时区才能防止别人“顺手”改挂你的任务。另外如果任务本身要判断“今天”是哪一天比如只抓今天的新闻代码里不要用 datetime.now() 直接取本机时间而要显式指定时区from datetime import datetime from zoneinfo import ZoneInfo today datetime.now(ZoneInfo(Asia/Shanghai)).date()否则由于服务器是 UTC抓到的“今天”的新闻其实可能是北京时间昨天深夜的文章漏采率很高还不好排查。6. 长期运行的性能维护并发、存储、幂等与观测skill 技能和定时任务能稳定跑起来之后真正的挑战变成了长期运维。以下是我在这套项目里沉淀出的维护清单。6.1 并发窗口和限速公式不要在所有 skill 里都用同样的并发策略。我给爬虫 skill 分了三档目标站类型并发数请求间隔说明大型门户10~200.5s~1s反爬相对宽松中小型站点3~52s~5s降低触发风控概率严格风控站点15s~10s必须低并发高间隔并发不是越高越好。在一个真实项目中我们对某个新闻站并发从 5 提到 20总耗时只缩短了 1 倍从 10 分钟到 5 分钟但触发了一次临时封 IP导致接下来两个小时任务完全失败。算下来总时间反而更长。后来我固定了一个规则优先级是稳定性大于速度并发增加的前提是每轮任务的请求失败率不能超过 2%。6.2 数据存储不要想着都放数据库并不是所有采集数据都需要进数据库。根据用途我把数据分成三路短期需要离线分析的写入本地 parquet 文件或 SQLite长期频繁查询的写入 PostgreSQL 或 MySQL并设置好唯一索引仅用于检查运行情况的摘要由 skill 返回值存到 CoPaw 的任务日志里。这样能极大降低数据库负载。很多实时监控场景对数据时效性要求没那么高定时一小时写一次 PostgreSQL 绰绰有余但如果你把每个请求的详情都实况写库定时任务还没跑完数据库先成了瓶颈。6.3 任务日志与观测CoPaw 会把每次任务触发的开始时间、结束时间、返回值和错误摘要记录到任务日志里。长期运行后这些日志是排障的第一手资料。我建议在这个基础上搭配一个直观的看板核心指标只有四行最近 24 小时任务成功率任务平均耗时变化曲线最近失败任务 Top 5每个 skill 的调用频率这四行足够覆盖大部分运维需求。比如“成功率掉到 80%”看失败列表发现全是同一个域名基本就判断目标站改版或封禁了。日志保留周期我建议至少 30 天太短的话你想回溯“上个月这个时候是不是也挂了”就只能抓瞎。6.4 技能和定时任务结合起来做更复杂的编排单个 skill 和定时任务跑通了之后可以尝试叠加成更复杂的自动化流程。比如一个采集任务结束之后触发另一个分析 skill 去处理数据。CoPaw 的定时任务管理也支持这种“结果回调”模式在任务配置里指定 on_success 的 webhook接收到成功事件后再通过 API 创建下一个任务。这样每个 skill 保持职责单一但链路可以很长。我做过的一个典型场景是每天 8 点半采集财经资讯到本地库9 点分析任务读取库中新增内容并生成摘要10 点再定时把摘要推送到工作群。整个过程不需要一个独立的“大调度器”靠的就是 CoPaw 定时任务的跨 skill 联动能力。写在最后的经验跑了大半年这套爬虫 skill 与定时任务的组合我最大的体会是采集系统稳定性的关键在幂等和可观测性而不是并发写得有多漂亮。给每个任务固定一个 run_id所有输出都能追溯给每个关键步骤加日志任何一次失败都有记录可查在 CoPaw 的 schedules.yaml 里把所有环境依赖时区、入参、超时、重试策略显式声明而不是依赖全局默认值。做到这几点几百个定时任务同时跑也不慌。如果你刚开始落地建议先拿一个简单站点的列表页采集练手把 skill.yaml、main.py、schedules.yaml 这三件套跑通再逐步加反爬、去重、告警这些能力。框架层面的东西越到后面越省心真正要花心思的永远是你对目标站点和数据结构本身的理解。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →