最近整理了一个新的 Skill,名字很直白:把网页数据直接变成一张能编辑的表格。给 Claude Code、Codex 这类编程代理丢一个链接,它就能自动把页面里的数据抓下来,整理成 Excel 或 CSV,打开就能改。听起来就是“网页采集”四个字,但真正要把稳定性和通用性做出来,里面全是细节。今天把整个设计思路、脚本实现、调参过程和踩过的坑完整拆开,给准备做 Skill 开发或者正在找网页采集方案的朋友一份能直接落地的参考。
1. 这个 Skill 到底解决什么问题
1.1 从“网页上有数据”到“表格里能编辑”
很多人都有过这种经历:看到一个网页上有表格,第一反应是鼠标选中、复制、粘到 Excel。数据少的时候没问题,但页面稍微复杂一点就原形毕露。
我之前接过一个需求,要整理某个公开页面里的成绩排名。页面上一共二十多行数据,看起来很简单,但复制到 Excel 后表头错位、合并单元格里的内容丢失、序号列带着一堆空格,最麻烦的是页面里还混着几个广告块,粘贴进来后全变成了垃圾行。最后只能手动清理,原本十分钟能搞定的事,硬是折腾了一个下午。
这个 Skill 要解决的就是这个场景:把“网页数据采集 + 表格化 + 可编辑”这条链路变成一个 Agent 能直接调用的技能包。用户只需要给一个 URL,Agent 自动抓取、解析、清洗、导出,最后生成一个能直接用 Excel、WPS 或者 Google Sheets 打开编辑的.xlsx文件。
这里说的“可编辑”,不是生成一张图片或者一段截图,而是真正结构化后的数据文件:每一列有表头,每一行是一条记录,金额、日期、超链接这种字段类型尽量保留,打开后可以排序、筛选、修改、另存。
1.2 为什么用 Skill,而不是普通提示词
先用一个比喻解释。普通提示词像是你每次都跟 Agent 说“帮我把这个网页里的表格整理出来”,但没说怎么抓、用哪个库、遇到动态页面怎么办。Agent 每次都要现场猜,效果全看模型的临场发挥。Skill 则像是一个工具箱,你把抓取逻辑、解析规则、依赖清单、输出格式全部提前装好,Agent 接到任务时只需要按图索骥地调用。
Skill 和 Agent 的区别也在这里。Agent 是一个能规划、能决策、能调工具的执行主体;Skill 是它手里一个高度封装的“技能包”。两者不是替代关系,而是配合关系。一个 Agent 里通常可以装多个 Skill,比如网页转表格、代码审查、数据处理、报告生成。每个 Skill 都负责一类相对固定的工作。
很多人找“省 token 的 Skill”,本质逻辑就是:固定流程进了脚本和说明,不再是靠对话轮次来回试探。这个思路在网页转表格这种任务上特别适用。抓网页、解析 HTML、清洗数据这一套流程完全可以确定性执行,没必要让模型每一步都靠推理,失败率低,速度还快。
2. 整体设计思路:从网页 URL 到可编辑 Excel
2.1 先定输出格式:什么样的表格才算“能编辑”
动手写代码前,我纠结过输出格式。最后反复对比下来,常见的选择有四种:
| 输出格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSV | 通用、体积小、任何表格软件都能打开 | 不支持多 Sheet、样式、超链接 | 简单数据交换 |
| XLSX | 支持多 Sheet、单元格格式、超链接、公式 | 文件稍大,依赖 openpyxl | 大多数业务场景,推荐默认 |
| Markdown 表格 | 轻量、方便贴进文档 | 编辑能力弱,不适合大数据量 | 快速预览 |
| HTML 表格 | 浏览器里就能编辑 | 不通用,数据交互弱 | 内部临时页面 |
我最终的方案是以.xlsx为默认输出,同时保留--format csv选项。原因很简单:.xlsx能承载的信息量最大,打开体验最接近用户对“可编辑表格”的认知。多张表格还能放进不同的 Sheet,而不是全部挤在一起。
2.2 抓取链路拆解:静态页面、动态渲染与数据清洗
网页转表格的抓取链路,本质上就三步:拿 HTML、解析结构、导出数据。难点在于每一步都有很多分支。
第一步“拿 HTML”,有静态和动态之分。静态页面用 Python 的requests就能拿到完整 HTML,速度最快。但今天很多页面是前端渲染的,requests拿到的只是空壳,真正的表格数据要等 JavaScript 执行完才出现在 DOM 里。这时候要么用 Playwright 模拟浏览器,要么退而求其次找页面里是否暴露了 JSON 数据接口。
第二步“解析结构”,我的选择是 BeautifulSoup 加 pandas。BeautifulSoup 负责定位<table>节点,pandas 的read_html负责把 HTML 表格转换成 DataFrame。为什么选这两个库?因为它们是 Python 生态里最成熟、社区资料最多的组合,遇到问题搜一下就有答案。解析不能只处理规整的表格,还要处理嵌套标签、合并单元格、表头重复这些脏情况。
第三步“导出数据”相对最简单,但有一个关键点:编码和格式。CSV 一定要用utf-8-sig,不然 Excel 打开中文会乱码;Excel 导出时尽量把表头字符串里的空白字符清理掉,否则后面groupby、筛选都觉得别扭。
2.3 Skill 的目录结构怎么设计
主流的 Agent Skill 机制基本都遵循同一个约定:一个目录里放一个SKILL.md作为入口,然后按需放脚本和依赖文件。我这个 Skill 的工程结构是这样:
webpage-to-table/ ├── SKILL.md ├── requirements.txt ├── scripts/ │ └── page_to_table.py └── examples/ └── demo_result.xlsxSKILL.md是 Agent 的“使用说明书”,里面要写清楚这个 Skill 什么时候触发、怎么调用、有哪些限制。requirements.txt一定要有,很多 Agent 在装依赖时如果发现缺少依赖声明会直接拒绝执行,这是我在实际使用中踩过的坑。scripts/放核心脚本,examples/放一个生成好的示例文件,方便别人一眼看懂输出长什么样。
目录结构不要搞得太深。Skill 的核心价值在于快速复用,文件太多、层级太深,Agent 反而不容易正确引用。
3. 手把手实现一个 Skill:核心文件与脚本拆解
3.1 准备环境与依赖
建议先用虚拟环境隔离依赖:
python -m venv .venv source .venv/bin/activate # Windows 上执行 .venv\Scripts\activate然后安装依赖:
pip install requests beautifulsoup4 lxml pandas openpyxl如果你的目标页面是动态渲染的,再补一个 Playwright:
pip install playwright playwright install chromium依赖版本不需要追新。我自己的经验是pandas2.x 和requests2.31.x 组合已经非常稳定,不需要每次升级都跟着改。
3.2 SKILL.md 写什么才能让 Agent 准确触发
SKILL.md的description是整个 Skill 里最重要的字段。写得太宽泛,Agent 会在不合适的时候调用;写得太具体,Agent 又容易漏掉。以下是我实测后比较顺手的写法:
--- name: webpage-to-table description: 当用户给出网页链接,并希望把网页中的表格、排名、清单或结构化数据导出成 Excel/CSV 等可编辑表格时使用。典型说法包括“把网页做成表格”“抓取这个页面的数据”“导出为 Excel”。 --- # 网页转表格 Skill ## 功能 - 根据 URL 抓取公开网页内容 - 自动识别页面中的表格节点并解析为 DataFrame - 清理表头空格、空列、重复行 - 默认输出 .xlsx,支持 CSV ## 调用步骤 1. 先运行:`python scripts/page_to_table.py --url <URL> --output 输出文件路径` 2. 脚本成功后,把生成文件的位置告诉用户 3. 如果页面需要登录或动态渲染,脚本输出会有提示,不要强行承诺成功 ## 注意 - 默认请求超时 15 秒 - 只处理公开可访问的页面 - 遵守目标网站的访问规则和 robots.txt - 如果脚本返回 err_empty_response,先重试或稍后再运行这里每一句都在给 Agent 传递边界信息:哪些事能做,哪些事不能做,失败后怎么处理。
3.3 核心脚本:解析与导出
page_to_table.py的核心逻辑可以分成三块:抓取 HTML、解析表格、导出文件。
#!/usr/bin/env python3 import argparse from pathlib import Path from io import StringIO import pandas as pd import requests from bs4 import BeautifulSoup DEFAULT_HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0 Safari/537.36" } def fetch_html(url, timeout=15): """抓取 HTML,并尝试修正编码问题。""" resp = requests.get(url, headers=DEFAULT_HEADERS, timeout=timeout) resp.raise_for_status() # 有些服务器没正确声明编码,pandas 解析会出现乱码 if not resp.encoding or resp.encoding.lower() in ("iso-8859-1", "ascii"): resp.encoding = resp.apparent_encoding return resp.text def parse_tables(html): """从 HTML 中提取所有表格,并做基础清洗。""" soup = BeautifulSoup(html, "lxml") table_nodes = soup.find_all("table") frames = [] for table_node in table_nodes: df = pd.read_html(StringIO(str(table_node)))[0] # 清掉整列为空、行重复、表头空格 df = df.dropna(axis=1, how="all") df = df.drop_duplicates() df.columns = [str(c).strip() for c in df.columns] frames.append(df) return frames def save_output(frames, output_path, fmt="xlsx", table_index=None): """导出为 xlsx 或 csv。""" if table_index is not None and table_index >= 0: frames = [frames[table_index]] if fmt == "csv": frames[0].to_csv(output_path, index=False, encoding="utf-8-sig") return with pd.ExcelWriter(output_path, engine="openpyxl") as writer: for idx, df in enumerate(frames): sheet_name = f"Sheet{idx + 1}" df.to_excel(writer, sheet_name=sheet_name, index=False) def main(): parser = argparse.ArgumentParser(description="网页数据转可编辑表格") parser.add_argument("--url", required=True, help="目标网页 URL") parser.add_argument("--output", required=True, help="输出文件路径") parser.add_argument("--format", default="xlsx", choices=["xlsx", "csv"]) parser.add_argument("--table-index", type=int, default=None, help="指定导出第几张表,默认全部导出") parser.add_argument("--timeout", type=int, default=15) args = parser.parse_args() html = fetch_html(args.url, args.timeout) frames = parse_tables(html) if not frames: raise SystemExit("未找到表格节点,可能需要动态渲染或更换解析方式") output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) save_output(frames, output_path, args.format, args.table_index) print(f"完成:共导出 {len(frames)} 张表,保存到 {output_path}") if __name__ == "__main__": main()这里的清洗逻辑是我反复调试后留下来的最小集合。dropna(axis=1, how="all")能干掉整列为空的干扰列;drop_duplicates()处理页面中重复渲染的行;df.columns清理表头空格,避免后面用列名取值时踩坑。
3.4 配置参数与 Agent 调用示例
脚本支持的参数不多,但每个都有实用价值:
| 参数 | 作用 | 使用建议 |
|---|---|---|
--url | 目标页面地址 | 必填 |
--output | 输出文件路径 | 建议带清晰文件名,比如scores.xlsx |
--format | 输出格式 | 默认 xlsx,简单字段可用 csv |
--table-index | 指定第几张表 | 页面有多张表时用,从 0 开始 |
--timeout | 请求超时时间 | 默认 15 秒,大页面可调大 |
Agent 实际调用时的场景大概是这样:
用户说:“帮我把这个公开页面的获奖名单整理成表格。”Agent 识别到需求匹配webpage-to-table,先判断 URL,再调用脚本,最后把生成的获奖名单.xlsx路径返回给用户。
如果用户只想导出其中一张表,Agent 会先跑一次完整解析,然后根据输出里列出的表索引,补一个--table-index 1重新导出。整个过程用户不用管脚本是怎么跑的,体验上就像是发了一个指令,然后拿到一个文件。
4. 实测中的坑:网络异常、脏数据与 Agent 调用问题
4.1 err_empty_response:最常见的网络假故障
我在测试过程中遇到过很典型的情况:页面在浏览器里打开正常,但脚本请求时抛错,错误信息类似“未发送任何数据”或者err_empty_response。
说实话,这种错误很多时候不是代码问题。目标服务器在 TCP 连接建立后没有返回完整 HTTP 响应,直接断开了连接。常见原因包括:服务器临时过载、请求频率太高被限流、CDN 边缘节点不稳定、本地网络到目标服务器之间的链路抖动。
我的排查顺序是:先确认 URL 在浏览器里能正常打开;再用脚本加长超时时间试一次;然后换一个 User-Agent 试试;最后在请求之间加一个短重试。不要在同一个请求上来回死磕,往往等一段时间就恢复了。
4.2 表格识别错乱与脏数据
网页表格最麻烦的不是抓不到,而是抓到之后结构是乱的。常见情况有三种:
第一种是页面里有多张表,默认全部导出时,用户发现多出来一堆不相关的 Sheet。解决办法就是--table-index参数,按索引导出指定表。
第二种是合并单元格。pandas 解析合并单元格后,部分行会出现NaN,不是真缺失,只是因为合并导致父行的值没被重复填充。处理时要先fillna(method="ffill"),再去掉重复行。
第三种是表头不固定。有的页面第一行是页面标题,第二行才是真正的表头。我一般会加一步判断:如果第一列都是中文长文本且后续行看起来像数据,就用下一行做表头。这个判断虽然简单,但在真实页面上非常管用。
4.3 Agent 调用 Skill 失败或效果不稳定
代码本身没问题,但 Agent 就是不调用或者调用出错,这类问题也很常见。我在开发初期踩过的坑主要集中在三个方面。
第一,SKILL.md的description写得太抽象。Agent 判断是否调用 Skill,主要就看描述和相关关键词。如果写的是“网页数据处理”这种泛泛的说法,Agent 可能把它用在完全不相干的场景上。正确做法是写清楚触发场景、典型用户说法和输出结果。
第二,依赖缺失。很多 Agent 在准备执行 Skill 时会先看有没有requirements.txt或依赖声明。如果脚本里import pandas但没有任何依赖文件,Agent 可能直接报错,或者花很长时间现场装环境。我的习惯是把依赖声明得越明确越好。
第三,脚本路径问题。Agent 的当前工作目录不一定和 Skill 目录一致,在SKILL.md里写调用命令时,最好用相对 Skill 根目录的路径,或者在命令里明确cd到 Skill 目录。
4.4 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
err_empty_response | 服务器断开连接、链路问题 | 等待重试、加长超时、换 UA |
| 请求超时 | 目标页面响应慢 | 调大--timeout,配合重试 |
| 403 Forbidden | 被目标站拦截 | 检查访问规则,降低频率,不要硬闯 |
| 中文乱码 | 编码声明缺失或错误 | 用resp.apparent_encoding修正 |
| 导出的表为空 | 页面是动态渲染 | 改用 Playwright 方案或查找接口 |
| 多导出了无关表 | 页面包含多个 table | 用--table-index指定 |
| Agent 不调用 Skill | description 不够具体 | 重写描述,增加触发关键词 |
| 提示缺少依赖 | 没有声明 requirements | 补全依赖文件 |
5. 发布与扩展:让你的网页转表格 Skill 更值钱
5.1 不只抓表格:扩展到列表、卡片和 JSON 数据
很多页面没有<table>标签,数据是卡片或者列表布局。这个 Skill 其实可以扩展成更通用的“网页结构化数据提取”。
一个思路是,优先查页面里的 JSON-LD 数据。很多站点会在<script type="application/ld+json">里输出结构化数据,解析出来本身就是干净的 JSON,转换成 DataFrame 非常容易。另一个思路是配合 CSS Selector 提取重复的卡片节点,比如div.product-item这种,再逐字段解析。
我自己后期加了一个--selector参数,允许用户指定提取区域。这样即使页面再乱,也能限定在某个模块里找表格,误报率下降很多。
5.2 值得做的几个增强功能
如果你打算长期维护这个 Skill,下面几个增强方向优先级比较高:
- 动态渲染降级:脚本先用 requests 抓,如果发现表格数量为 0,自动切换到 Playwright。
- 多页合并:很多列表数据分了好几十页,可以加一个
--pages 5参数,自动翻页并合并结果。 - 输出预览:先打印前几行数据,让用户确认字段对不对,再导出完整文件。
- 增量更新:适合周期性数据,比如每日排名,可以在脚本里对比旧文件,只追加新的行。
这些功能不用一次做完,按实际遇到的需求往里面加就行。好的 Skill 不是一上来就大而全,而是能在一个职责边界内不断长出来的。
5.3 发布到社区时要注意的细节
Skill 这东西,自己做出来只是个半成品,真正让它发挥价值的是别人能不能顺利复现。发布到社区时,我一般会在包里额外放三样东西:一个写完的README.md、一个真实可跑的示例 URL 和对应的输出文件。
README.md里一定要写清楚“这个 Skill 能做什么、不能做什么、需要哪些依赖、调用示例是什么”。不要默认别人知道你的目录结构和使用意图。示例 URL 尽量选稳定、公开、不需要登录的页面,避免给使用者添堵。
还有个安全提醒:Skill 本质上是可执行代码,网络上找来的第三方 Skill 一定要检查脚本内容再运行。尤其是那种要你输入账号、Cookie、Token 的 Skill,更需要多留个心眼。自己在发布时,也不要往脚本里写任何私密信息。
最后再分享一个很实际的技巧:当你准备发布或者更新 Skill 时,先拿一段最朴素的用户话术去测一遍,比如“把这个网页里的数据做成表”,看 Agent 能不能准确触发并完成任务。如果连这种最直接的调用都顺畅,那这个 Skill 的可用性就基本合格了。后续再慢慢打磨边界情况,比一开始憋大招要靠谱得多。