1. 从一次抓取翻车说起:Web 信息提取到底难在哪
很多人第一次写 Python 抓取脚本,都是这样的流程:requests.get()拿到 HTML,BeautifulSoup一顿find_all,打印出来看着挺美,结果一上量就崩。要么是目标站点结构稍微一变,选择器全废;要么是编码乱码、动态渲染拿不到内容;要么是解析出来的字段缺胳膊少腿,还得手动补。
Web 信息提取(Web Information Extraction)说白了就三件事:把网页拿下来、把有用的字段抠出来、把结果整理成结构化数据。听起来简单,但工程化落地时会遇到一堆细节问题:
- 请求层:超时、重试、UA、Cookie、编码识别,一个没处理好就拿到半截 HTML;
- 解析层:
lxml快但容错差,BeautifulSoup容错好但慢,re灵活但难维护,选哪个、怎么组合; - 结构化层:抽出来的字段怎么统一成 dict / JSON,怎么校验字段完整性;
- 凭证层:如果你还想在链路里接入大模型做字段补全、正文摘要、实体抽取,那 API Key 的管理又是一摊事。
这篇就聚焦最后这条链路:用 Python 把抓取和解析跑通,同时用 TaoToken 统一管理多工具调用凭证,让你不用在多个平台之间来回切换 Key。适合已经会写基础requests、想把这套流程做成可复用工程的同学。
我试过把抓取、解析、模型补全拆成三个独立脚本,结果 Key 散落在.env、环境变量、代码硬编码里,换台机器就报 401。后来统一走一个 API 通道,配置集中到config.toml,才算清爽。
2. TaoToken 前置:一个 Key 管住整条提取链路
在讲代码之前,先把凭证这层说清楚。Web 信息提取链路里,除了本地库(requests、bs4、lxml),你可能还会用到模型能力,比如:
- 把抽出来的零散文本丢给模型做字段归一化;
- 对正文做摘要或关键词提取;
- 用模型判断某个页面是不是文章类型。
这些调用如果各自去申请 Key、各自配 base_url,维护成本很高。TaoToken 的作用就是提供一个统一的 API 通道,你只需要一个 Key,就能在同一个入口下调用不同模型。
具体来说,你需要准备两样东西:
- 一个 TaoToken 账号,登录后在控制台创建 API Key;
- 一个
config.toml,把 base_url、Key、默认模型写进去,代码里只读配置,不硬编码。
相关入口我放在这里,按需取用:
- 官网了解整体能力:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- 创建和管理 Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys
- 接入文档(看参数和返回格式):https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
- 想先在网页里试模型效果:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat
注意:API 地址统一用
https://taotoken.net/api,不要带 UTM 参数,避免请求签名或路由异常。
这里要强调一点:TaoToken 是合规的 API 聚合通道,不是让你去绕过什么限制。它的价值在于把多工具调用的凭证收敛到一个地方,方便你在本地脚本、CI、服务器上复用同一套配置。
3. 可复制配置:config.toml 骨架与依赖安装
先把工程骨架搭起来。目录结构建议这样:
web-extract/ ├── config.toml ├── requirements.txt ├── extractor.py └── output/requirements.txt内容:
requests==2.32.3 beautifulsoup4==4.12.3 lxml==5.2.2 tomli==2.0.1安装命令:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txtconfig.toml骨架,这是整条链路的凭证中心:
[api] base_url = "https://taotoken.net/api" api_key = "sk-你的TaoTokenKey" default_model = "claude-3-5-sonnet" timeout = 30 [fetch] user_agent = "Mozilla/5.0 (compatible; WebExtractor/1.0)" retry = 3 retry_delay = 2 [parse] parser = "lxml" encoding_fallback = "utf-8" [output] dir = "output" format = "json"几个参数说明,用表格对照更清楚:
| 配置项 | 作用 | 建议值 |
|---|---|---|
api.base_url | 统一 API 入口 | https://taotoken.net/api |
api.api_key | 调用凭证 | 控制台生成,勿提交到 Git |
api.timeout | 请求超时秒数 | 30,抓取慢站点可调大 |
fetch.retry | 失败重试次数 | 3 |
parse.parser | 解析器 | lxml快,容错差时换html.parser |
output.format | 输出格式 | json便于下游消费 |
提示:
config.toml一定要加进.gitignore。Key 泄露比代码写错严重得多。
4. 抓取-解析-验证:一次完整的端到端动作
下面这段代码把请求、解析、结构化、模型补全串起来。核心思路是:抓取层只负责拿 HTML,解析层只负责抠字段,模型层只负责补全和校验,三层解耦,方便单独替换。
import json import time import tomli import requests from bs4 import BeautifulSoup from pathlib import Path # ---------- 配置加载 ---------- def load_config(path="config.toml"): with open(path, "rb") as f: return tomli.load(f) CFG = load_config() # ---------- 抓取层 ---------- def fetch_html(url, cfg): headers = {"User-Agent": cfg["fetch"]["user_agent"]} last_err = None for attempt in range(cfg["fetch"]["retry"]): try: resp = requests.get( url, headers=headers, timeout=cfg["api"]["timeout"], ) resp.raise_for_status() # 编码兜底:优先用 apparent_encoding if resp.encoding is None or resp.encoding.lower() == "iso-8859-1": resp.encoding = resp.apparent_encoding or cfg["parse"]["encoding_fallback"] return resp.text except requests.RequestException as e: last_err = e time.sleep(cfg["fetch"]["retry_delay"]) raise RuntimeError(f"抓取失败: {last_err}") # ---------- 解析层 ---------- def parse_article(html, cfg): soup = BeautifulSoup(html, cfg["parse"]["parser"]) title_tag = soup.find("h1") or soup.find("title") title = title_tag.get_text(strip=True) if title_tag else "" # 正文段落:取所有 p 标签,过滤过短文本 paragraphs = [ p.get_text(strip=True) for p in soup.find_all("p") if len(p.get_text(strip=True)) > 20 ] # 链接抽取 links = [ {"text": a.get_text(strip=True), "href": a.get("href")} for a in soup.find_all("a", href=True) ][:20] return { "title": title, "paragraph_count": len(paragraphs), "content": "\n".join(paragraphs), "links": links, } # ---------- 模型补全层(走 TaoToken 统一通道) ---------- def enrich_with_model(article, cfg): prompt = ( "下面是一篇网页提取出的正文,请输出 JSON,字段为 " "summary(不超过80字)、keywords(数组,最多5个)。\n\n" f"标题:{article['title']}\n正文:{article['content'][:2000]}" ) resp = requests.post( f"{cfg['api']['base_url']}/v1/chat/completions", headers={ "Authorization": f"Bearer {cfg['api']['api_key']}", "Content-Type": "application/json", }, json={ "model": cfg["api"]["default_model"], "messages": [{"role": "user", "content": prompt}], "temperature": 0.2, }, timeout=cfg["api"]["timeout"], ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] # ---------- 主流程 ---------- def run(url): html = fetch_html(url, CFG) article = parse_article(html, CFG) print(f"[解析] 标题: {article['title']}") print(f"[解析] 段落数: {article['paragraph_count']}") enriched = enrich_with_model(article, CFG) article["enriched"] = enriched out_dir = Path(CFG["output"]["dir"]) out_dir.mkdir(exist_ok=True) out_file = out_dir / "result.json" out_file.write_text( json.dumps(article, ensure_ascii=False, indent=2), encoding="utf-8", ) print(f"[输出] 已写入 {out_file}") if __name__ == "__main__": run("https://example.com/some-article")这段代码有几个设计点值得说:
第一,fetch_html里做了编码兜底。很多中文站点返回的Content-Type不带 charset,requests会默认按 ISO-8859-1 解码,结果全是乱码。用apparent_encoding能自动猜对。
第二,parse_article里过滤了长度小于 20 的段落。这是实战经验:导航栏、版权声明、按钮文字都会被find_all("p")抓进来,不过滤的话正文里全是噪音。
第三,模型补全走的是{base_url}/v1/chat/completions,这是标准 OpenAI 兼容格式,TaoToken 的通道直接支持。你换模型只需要改config.toml里的default_model,代码一行不用动。
5. 验证请求与成功结果:怎么确认链路真的通了
跑起来之后,你会看到类似输出:
[解析] 标题: 某篇文章标题 [解析] 段落数: 12 [输出] 已写入 output/result.json打开output/result.json,结构应该是这样的:
{ "title": "某篇文章标题", "paragraph_count": 12, "content": "第一段正文...\n第二段正文...", "links": [ {"text": "相关阅读", "href": "/related/1"} ], "enriched": "{\"summary\": \"...\", \"keywords\": [\"python\", \"爬虫\"]}" }如果enriched字段有内容,说明 TaoToken 通道调用成功。如果这一步报 401,先检查config.toml里的 Key 有没有多余空格;报 404,检查base_url是不是写成了带路径的形式,正确写法就是https://taotoken.net/api,后面拼/v1/chat/completions。
想单独验证模型通道是否可用,可以先用最简请求测一下:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":"ping"}]}'返回里有choices字段就说明通道正常。这一步能帮你快速区分是网络问题还是代码问题。
6. 本篇常见错排查:抓取和解析踩过的坑
坑一:lxml解析报错XMLSyntaxError。原因是目标页面 HTML 不规范,lxml容错性差。解决办法是把config.toml里的parser改成html.parser,速度慢一点但基本不报错。或者用lxml时加BeautifulSoup(html, "lxml")前先做一次html.strip()。
坑二:抓到的正文是空的。大概率是页面内容由 JavaScript 渲染,requests拿到的只是骨架 HTML。这种情况要么换用带渲染能力的方案,要么找页面里的 API 接口直接请求 JSON。别硬啃BeautifulSoup,它解析不了没渲染的 DOM。
坑三:模型返回的 JSON 解析失败。模型有时会在 JSON 外面包一层 ```json 代码块。稳妥做法是用正则先抠出{...}再json.loads,或者直接在 prompt 里要求「只输出 JSON,不要任何解释」。
坑四:Key 写进代码提交到了仓库。这是最危险的。正确做法是config.toml进.gitignore,仓库里放一个config.example.toml做模板。如果已经提交了,立刻去控制台吊销旧 Key 重新生成。
坑五:请求频率过高被封 IP。加time.sleep做间隔,或者用requests.Session复用连接。生产环境建议加代理池和限流,但那是另一个话题了。
排障时如果怀疑是凭证问题,直接去 API Keys 页面核对:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys
接入细节和返回字段含义,文档里写得更全:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
7. 把链路收进一个 Key:后续怎么扩展
这套骨架跑通之后,扩展方向很明确。抓取层可以加Scrapy做分布式;解析层可以针对不同站点写不同的parse_xxx函数,用字典做路由;模型层可以按任务类型切换模型,比如摘要用轻量模型、实体抽取用强模型,而切换成本只是改config.toml里的一行。
如果你打算长期做编码类或 Agent 类任务,比如让模型自动写解析规则、自动修选择器,那可以考虑 Coding Plan,把模型调用额度固定下来:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan
想先在网页里对比不同模型对同一段正文的抽取效果,用模型对话页面最方便:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat
最后给一个实用建议:把config.toml里的default_model做成可被环境变量覆盖,比如TAOTOKEN_MODEL,这样在 CI 里不用改文件就能切模型。抓取脚本最怕的就是配置散落各处,收拢到一个文件、一个 Key,后面维护会轻松很多。