用 Trae Skill 驯化 AI 爬虫:从 6 个省份实战中提炼的标准化流程
1. 引言
Skill 是 Trae IDE 中的 AI 标准操作流程,存储在 .trae/skills/ 目录下,当用户触发特定场景时自动加载。
本文重点分享:我如何用一个 Skill 文件,让 AI 按标准流程采集 6 个省份的饮用水水源水质数据。
| 省份 | 记录数 | 时间范围 | 复杂度 |
|---|---|---|---|
| 上海 | - | - | ★ |
| 湖南 | - | - | ★★★ |
| 天津 | - | - | ★★★ |
| 陕西 | - | - | ★★★★ |
| 黑龙江 | 3271 | 2016~2026 | ★★★★★ |
| 广东 | 3759 | 2021~2026 | ★★ |
2. 核心:5 步标准流程
步骤 1:反爬测试 + 列表页分析
设计思路:
- 先测试连通性,确定最低 headers 要求
- 再判断列表页类型(静态 HTML / JSON API / 动态渲染)
- 设置止损点,避免在无解问题上浪费 token
应用场景:
- 广东:静态 HTML,仅需 UA
- 黑龙江:JSON API,需要完整 headers + XHR
关键代码:
python
# 三级 headers 递进测试
L1_裸请求 = {}
L2_UA = {"User-Agent": "Mozilla/5.0 ..."}
L3_完整 = {"User-Agent": ..., "Accept": ..., "Referer": ...}
步骤 2:链接筛选 + 年月提取
设计思路:
- 标题关键词筛选(INCLUDE_KW / EXCLUDE_KW)
- 年月正则提取
- URL 去重 + 标题去重
应用场景:
- 广东:包含季度报告,需要排除
- 湖南:包含年度报告,需要排除
关键代码:
python
INCLUDE_KW = ["饮用水", "水源水质"]
EXCLUDE_KW = ["季度", "年度", "全年", "清理整治", "排名"]
RE_YEAR_MONTH = re.compile(r"(\d{4})\s*年\s*(\d{1,2})\s*月")
步骤 3:详情页格式分析
设计思路:
- 必须逐条检查,不能假设所有年份统一
- 抽样分析(首、中、尾各 3 条)
- 归纳格式变体
应用场景:
- 黑龙江:3 种列结构(7 列含省份、7 列含供水人口、6 列标准)
- 陕西:3 种 Era 时代
关键发现:
- 负索引
row[-2]/row[-1]可以兼容所有列结构 - 表头关键词匹配比硬编码索引更可靠
步骤 4:数据解析 + xlsx 导出
设计思路:
- 选择解析架构(纯 HTML / 纯 PDF / 双路由)
- 表头关键词动态映射
- 16 字段统一格式输出
应用场景:
- 黑龙江:PDF + HTML 双数据源
- 上海:纯 HTML
关键代码:
python
# 双路由
if row["pdf_url"]:
records = parse_from_pdf(row["pdf_url"], ...)
else:
records = parse_from_html(url, ...)
步骤 5:输出策略文档
设计思路:
- 记录数据源概况、列表页结构、筛选策略、格式分析、已知的坑
- 为后续维护提供参考
应用场景:
- 每个省份都有独立的策略文档
- 新增省份时可以参考已有文档
3. 关键设计点
设计点 1:止损机制
问题:AI 容易在无解问题上浪费 token
解决方案:在每个步骤设置止损点
python
# 步骤 1 止损
- 列表页需要验证码 / Cookie 认证流程
- JSON API 返回加密数据
- 页面为 JS 动态渲染且找不到底层数据 API
# 步骤 3 止损
- PDF 表格无法被正确提取
- 详情页同时存在 3 种以上完全不兼容的格式变体
- 关键字段无法可靠定位
价值:遇到无解问题时及时停止,等待人工介入
设计点 2:代码压缩
问题:大量代码模板塞进上下文,浪费 token
解决方案:核心流程 + 附录分离
markdown
本文档结构:
- 步骤 1~5:核心流程(必读)
- 附录 A~F:代码模板 + 已踩坑参考(按需查阅)
价值:用什么读什么,节省上下文
设计点 3:负索引兼容多列结构
问题:不同年份列数不同,硬编码索引会失效
解决方案:用负索引定位最后两列
python
# 2016 年:7 列含省份
# 2017-2018 年:7 列含供水人口
# 2019-2026 年:6 列标准
# 一行代码兼容所有格式
compliance = row[-2] # 倒数第二列
exceedance = row[-1] # 最后一列
价值:避免硬编码列索引导致的格式失效
设计点 4:rowspan 展开
问题:HTML 表格中城市名跨行合并,导致城市名丢失
解决方案:pending 队列算法
python
pending = {} # col_idx -> [(value, remaining_rows)]
for row in rows:
for cell in cells:
while col in pending and pending[col]:
val, remaining = pending[col].pop(0)
expanded.append(val)
价值:正确处理合并单元格
设计点 5:城市名前向填充
问题:PDF 中 pdfplumber 对 rowspan 输出空字符串
解决方案:用 last_city 填充
python
last_city = ""
for row in data_rows:
city = row[city_col].strip()
if city:
last_city = city
else:
city = last_city
价值:处理 PDF 中的空值
4. 实战案例
案例 1:上海(★ 最简)
特点:
- 静态 HTML 列表页
- 统一 7 列表格
- 无特殊处理
价值:最简单的案例,适合入门
案例 2:湖南(★★★ 中等)
特点:
- 2 种报告类型
- 表格位置变化(新表插入)
- 城市名重复问题
踩坑:
- 硬编码表格索引 → 新表插入后取错表
- 解决方案:表头关键词匹配
案例 3:黑龙江(★★★★★ 最复杂)
特点:
- JSON API 列表页
- PDF + HTML 双数据源
- 3 种列结构
- rowspan 展开
- ★/☆ 符号水源类型
踩坑:
- 列映射全部偏移(compliance 显示取水量数字)
- 原因:2017-2018 年有 7 列(多了"供水人口"列)
- 解决方案:负索引
row[-2]/row[-1]
数据:
- 总记录数:3271 条
- 时间范围:2016~2026 年
- 城市数量:13 个
案例 4:广东(★★ 较简单)
特点:
- 静态 HTML 列表页
- 统一 7 列表格
- 无 rowspan
数据:
- 总记录数:3759 条
- 时间范围:2021~2026 年
- 城市数量:21 个
5. 踩坑记录
| 省份 | 坑 | 现象 | 解决方案 |
|---|---|---|---|
| 黑龙江 | 列映射偏移 | compliance 显示取水量数字 | 负索引 row[-2] / row[-1] |
| 黑龙江 | rowspan | 城市名丢失 | pending 队列算法 |
| 黑龙江 | PDF 空值 | 城市名为空字符串 | 前向填充 |
| 湖南 | 表格位置变化 | 新表插入后取错表 | 表头关键词匹配 |
| 陕西 | 3 种 Era | 不同时代格式不同 | 时代分类 |
| 通用 | 硬编码索引 | 不同列数格式失效 | 负索引 |
6. 成果展示
数据统计:
| 省份 | 记录数 | 时间范围 | 城市数 | 复杂度 |
|---|---|---|---|---|
| 上海 | - | - | - | ★ |
| 湖南 | - | - | - | ★★★ |
| 天津 | - | - | - | ★★★ |
| 陕西 | - | - | - | ★★★★ |
| 黑龙江 | 3271 | 2016~2026 | 13 | ★★★★★ |
| 广东 | 3759 | 2021~2026 | 21 | ★★ |
质量检查:
- 能直接
python fetch_{province}.py运行 - 16 字段完整且无空 city
- 全部列表页都已遍历
- 所有格式变体都已覆盖
- compliance 只有合理值
7. 总结
Skill 系统的价值:
- 标准化流程,减少 AI 犯错
- 止损机制,避免浪费 token
- 代码压缩,节省上下文
- 已踩坑记录,避免重复踩坑
适用场景:
- 需要采集多个省份/城市的数据
- 每个省份网站结构不同
- 需要标准化流程
- 需要避免 AI 犯错
如若需要完整skills的,可关注私信我。