如何使用 Scrapeless 建立 AI 训练数据管道
Advanced Data Extraction Specialist
TL;DR:
- 微调数据集是一个管道产品,而不是下载。 本指南从头到尾构建一个:通过 Scrapeless Universal Scraping API 获取渲染的公共页面,利用 Python 标准库提取标记对,并写入 OpenAI 准备好的聊天格式 JSONL,并进行训练/验证分割。
- 获取层是每个页面一个 HTTP POST。
unlocker.webunlocker角色从POST /api/v1/unlocker/request返回渲染的 HTML——无需管理浏览器,也不需要在您这边轮换代理池。 - 上传之前的一切只需要一个 Scrapeless 密钥。 演示爬虫覆盖一个公共引用网站的所有 10 个页面,产生 100 个监督示例;只有最后的微调任务需要 OpenAI 密钥和预算。
- 格式错误是最便宜的错误。 每个训练行都是一个
{"messages": [...]}对象,包含系统、用户和助手的回合——使用json.dumps写入,首次上传时文件即可解析。 - 出处是数据集的一部分。 仅限公共页面,限制数量,并在爬虫前检查网站条款和机器人指令——责任部分涵盖训练数据对一般爬虫问题的补充。
- 免费开始。 在 app.scrapeless.com 的免费计划上创建您的 API 密钥。
引言:数据集是难点
微调 LLM 管理上很简单——上传文件,创建任务,等待。任务不能解决的是文件。根据一百个格式良好、标记正确的任务示例调优的模型,可以超过根据一万个噪声线条调优的模型,这一差异在训练运行开始前就已决定,数据收集和整理的管道。
对于大多数团队来说,这条管道是一个爬虫问题,因为值得调优的领域知识存在于网页上:产品描述、文档、列表、评论、参考材料。此旅程的概念方面在关于 AI 模型训练的完整工作原理 的指南中涵盖;这篇文章是可执行的部分。您将针对一个公共演示网站构建完整管道——quotes.toscrape.com,这是一个为爬虫练习而构建的网站——并最终生成 OpenAI 微调端点所接受的 train.jsonl 和 val.jsonl 文件。
演示任务:教模型归属著名名言。足够小以便在几分钟内运行,结构与现实完全相同。
管道一览
该管道共有五个阶段,前四个阶段仅需 Scrapeless API 密钥即可实际运行:
- 获取——通过 Universal Scraping API 获取每个渲染页面,每个页面一个 POST。
- 发现——跟随网站自身的分页直到结束,并设定硬页面上限作为安全界限。
- 提取——使用 Python 标准库的 HTML 解析器从每个页面解析出引用文本和作者。
- 转换——将每对转换为聊天格式的训练示例,按 80/20 分割,写入 JSONL。
- 训练——上传两个文件并创建微调任务(此阶段需要 OpenAI 密钥;代码已按要求显示和标记)。
每页的流:渲染或获取 → 发现下一个页面 → 提取对 → 转换为示例 → 存储为 JSONL。
为什么选择 Scrapeless Universal Scraping API
Universal Scraping API 将页面收集转变为一个确定性的函数调用:您 POST 一个 URL,服务处理渲染、解锁和服务器端的代理路由,然后您将在 data 字段中收到页面 HTML。对于数据集构建者而言,这点尤其重要。首先,语料库保持可重现——无论源是静态演示网站还是 JavaScript 密集型产品目录,相同的请求格式都能正常工作,因此当目标发生变化时,管道代码无需更改。其次,没有本地浏览器网络:几百页的数据集只是一个函数的循环。
这里的演示网站故意友好。指南的关键是管道形状;在将其指向您真实语料库时,只需更换 URL 列表和解析器,并保持数量限制。
前提
- 具有
requests包的 Python 3——管道中的其他所有导入都是标准库;本指南中的运行使用的是 Python 3.12。 - 一个 Scrapeless API 密钥——开发者文档涵盖密钥创建。
- 仅针对第 5 阶段:具有微调访问权限和预算的 OpenAI API 密钥。第 1-4 阶段无需它。
导出Scrapeless密钥,以便脚本可以从环境中读取它:
bash
export SCRAPELESS_API_KEY="sk_your_key_here"
第一阶段 — 获取渲染页面
一个POST请求返回一个渲染页面。unlocker.webunlocker 角色在 input 中接收目标URL,并在响应的 data 字段中返回HTML:
python
# fetch_page.py — 阶段 1:通过Scrapeless检索一个渲染页面
import os
import requests
ENDPOINT = "https://api.scrapeless.com/api/v1/unlocker/request"
HEADERS = {
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
}
def fetch_page(url: str) -> str:
resp = requests.post(
ENDPOINT,
headers=HEADERS,
json={"actor": "unlocker.webunlocker", "input": {"url": url, "method": "GET"}},
timeout=120,
)
resp.raise_for_status()
return resp.json().get("data", "")
html = fetch_page("https://quotes.toscrape.com/page/1/")
print(f"获取到 {len(html):,} 个字符的HTML")
print("引用标记存在:", '<span class="text"' in html)
print("作者标记存在:", '<small class="author"' in html)
针对引用网站的第1页,这返回了11,021个字符的HTML,同时存在这两个标记。raise_for_status()保持故障显著:错误的密钥或无法访问的目标会阻止管道,而不是写入一个空的语料库。
第二阶段 — 发现完整语料库
该网站告诉你在哪里结束,因此爬虫跟随网站而不是猜测URL。演示网站的每一页都有一个 li class="next" 元素,直到最后一页;爬行循环会抓取,检查该标记,并提升页面计数器。两个边界保持阶段真实:循环在标记消失时停止,即使标记始终存在,MAX_PAGES 限制也会停止它。这个限制是数据集构建与无限爬行之间的区别——将其设置为您实际打算收集的语料库大小。
循环本身是四行代码,位于阶段4的完整管道脚本中。
第三阶段 — 提取引用–作者对
提取将HTML转换为标记的对,标准库能够完成此任务。html.parser.HTMLParser 在每个标签上触发回调;在遍历页面时跟踪两个标志,收集每个引用的文本和作者,而无需任何第三方依赖项:
python
# quote_parser.py — 阶段 3:标准库提取 (quote, author) 对
from html.parser import HTMLParser
class QuoteParser(HTMLParser):
"""从 quotes.toscrape.com 标记中收集 (text, author) 对。"""
def __init__(self):
super().__init__()
self.pairs, self._text, self._mode = [], "", None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "span" and a.get("class") == "text":
self._mode = "text"
elif tag == "small" and a.get("class") == "author":
self._mode = "author"
def handle_data(self, data):
if self._mode == "text":
self._text = data.strip("“”")
elif self._mode == "author":
self.pairs.append((self._text, data.strip()))
self._mode = None
def parse_quotes(html: str):
p = QuoteParser()
p.feed(html)
return p.pairs
选择器库也可以正常工作——展示标准库版本的原因是整个管道保持为两个依赖项的脚本(requests 加上Python本身),在管道移至调度程序时减少了一件事情的依赖。
第四阶段 — 转换为聊天格式的JSONL
OpenAI微调端点在聊天记录上进行训练:文件的每一行都是一个 {"messages": [...]} 对象,包含系统规则、用户输入和您希望模型学习的助手答案。格式是JSON Lines —— JSON Lines格式定义 恰好是“每行一个JSON值”——针对指令微调的研究,如 InstructGPT论文,就是导致这个形状看起来像对话的原因:在示例对上训练时,模型对任务的学习效果更好。
这个脚本是整个管道——组合阶段1到阶段4,最终生成两个文件:
python
# build_dataset.py — 阶段 1–4:爬取、提取、转换、存储
import json
import os
import requests
from html.parser import HTMLParser
ENDPOINT = "https://api.scrapeless.com/api/v1/unlocker/request"
HEADERS = {
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
}
BASE = "https://quotes.toscrape.com"
MAX_PAGES = 15 # 高于网站实际大小的安全界限
SYSTEM = "你归属著名引用。仅回复作者名字。"
class QuoteParser(HTMLParser):
def __init__(self):
super().__init__()
self.pairs, self._text, self._mode = [], "", None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "span" and a.get("class") == "text":
自._mode = "text"
elif tag == "small" 和 a.get("class") == "author":
self._mode = "author"
def handle_data(self, data):
if self._mode == "text":
self._text = data.strip("“”")
elif self._mode == "author":
self.pairs.append((self._text, data.strip()))
self._mode = None
def fetch_page(url: str) -> str:
resp = requests.post(
ENDPOINT,
headers=HEADERS,
json={"actor": "unlocker.webunlocker", "input": {"url": url, "method": "GET"}},
timeout=120,
)
resp.raise_for_status()
return resp.json().get("data", "")
def to_example(text: str, author: str) -> dict:
return {
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"谁说过:“{text}”"},
{"role": "assistant", "content": author},
]
}
pairs, page = [], 1
while page <= MAX_PAGES:
html = fetch_page(f"{BASE}/page/{page}/")
parser = QuoteParser()
parser.feed(html)
pairs.extend(parser.pairs)
if 'class="next"' not in html: # 阶段 2:网站会说明何时结束
break
page += 1
examples = [to_example(t, a) for t, a in pairs]
split = int(len(examples) * 0.8)
for name, rows in (("train.jsonl", examples[:split]), ("val.jsonl", examples[split:])):
with open(name, "w", encoding="utf-8") as f:
f.writelines(json.dumps(r, ensure_ascii=False) + "\n" for r in rows)
print(f"抓取的页面: {page} | 提取的对: {len(pairs)}")
print(f"train.jsonl: {split} 个例子 | val.jsonl: {len(examples) - split} 个例子")
print("第一行训练数据:")
print(json.dumps(examples[0], ensure_ascii=False)[:180])
运行抓取了网站的所有 10 个页面,提取了 100 对,并写入 80 个训练例子和 20 个验证例子。80/20 的比例为微调任务提供了一些测量泛化的依据——模型从未训练过的验证例子。两个文件都使用 ensure_ascii=False 写入,并且每行一个 json.dumps,这是最便宜的保险:格式错误的 JSONL 是失去上传端点往返的常见方式。
即使在小型语料库上,做一次质量检查也是值得的:阅读一些示例行,确认标签确实回答了输入。模型学习的是文件所展示的内容,包括错误。
阶段 5 — 提交微调任务
与以上所有相比,训练调用很小。上传两个文件,目的为 fine-tune,然后创建针对可微调模型的任务——当前的列表和参数在 OpenAI 监督微调指南 中。
注意:此阶段是管道的一个先决条件缺口——它需要具有微调访问权限和预算的
OPENAI_API_KEY,本指南并不假设这一点。以上所有内容均真实运行,仅使用 Scrapeless 密钥。
python
# submit_job.py — 阶段 5:上传数据集并创建任务(需要 OPENAI_API_KEY)
from openai import OpenAI
client = OpenAI() # 从环境中读取 OPENAI_API_KEY
train = client.files.create(file=open("train.jsonl", "rb"), purpose="fine-tune")
val = client.files.create(file=open("val.jsonl", "rb"), purpose="fine-tune")
job = client.fine_tuning.jobs.create(
training_file=train.id,
validation_file=val.id,
model="gpt-4.1-mini-2025-04-14",
)
print(job.id, job.status)
当任务完成时,生成的模型 ID 会放入您已经使用的相同聊天补全调用中——数据集决定该模型在看到从未见过的引用时是否会正确回答 "阿尔伯特·爱因斯坦"。
在免费计划上获取您的 API 密钥:app.scrapeless.com
负责任地抓取训练数据
训练数据携带源页面所携带的所有义务,还有一个:模型将重现您所提供的任何模式。四种做法使得收集方面具有可辩护性。
- 仅限公共页面,并查看条款。 仅收集无需账户即可呈现的内容,并在抓取之前检查目标网站的服务条款——越来越多的网站明确指出了训练使用。
- 遵守机器人指令。 机器人排除协议 (RFC 9309) 是站点允许爬虫接触哪些内容的标准机器可读声明;在收集之前检查目标路径。
- 最小化。 仅提取任务所需的字段——在这里是引用文本和作者——而不是整个页面的转储,这样会拖入用户评论、姓名或其他附带数据。设定页面上限是最小化的一部分。
- 跟踪来源和许可。 记录每个示例的来源和时间。公开可阅读的文本并不自动在每个司法管辖区内获得用于模型训练的许可;当语料库包含任何敏感内容而不仅仅是名言时,应在任务运行之前咨询法律顾问关于许可的问题。
您将获得的内容
两个文件,准备上传。每一行都是一个完整的监督示例——上面运行的 train.jsonl 的第一行:
text
{"messages": [{"role": "system", "content": "您归属著名的引用。请仅回复作者的名字。"}, {"role": "user", "content": "谁说过这句话:“我们创造的世界是我们思维的过程。没有改变我们的思维,它是无法改变的。”"}, {"role": "assistant", "content": "阿尔伯特·爱因斯坦"}]}
形状保持不变:真实语料库交换解析器和 URL 列表,系统规则描述您的任务而不是引用归属,JSONL 编写器、拆分和上传保持不变。如果您的目标是检索而不是权重更新,则相同的提取层为 clean-text RAG 管道 提供服务——使用块和嵌入,而不是训练示例。
结论
该管道在两端发挥作用。在前端,通用抓取 API 每个页面进行确定性 POST 集合,使语料库可重现,代码能够适应目标的变化。在后端,严谨的转换——确切的聊天格式、每行一个 JSON 对象、真实的验证拆分、人工标签审核——是区分提高答案的微调和消耗预算的微调的关键。在这两个端点之间,中间是您现在拥有的百行标准库 Python 代码。
准备构建您的训练数据管道了吗?
计划和包含的请求量请查看 定价页面,本指南中的提取层适用于免费计划——在 app.scrapeless.com 创建您的 API 密钥,阶段 1 将在一次 POST 中返回您的第一个渲染页面。
常见问题
问:微调模型需要多少数据?
如果示例清晰,所需数据量少于大多数团队的预期。监督微调在十到几百个标注良好的示例的语料库中显示出可测量的行为变化;而广泛的行为变化需要更多。首先使用最小的语料库代表任务,针对验证文件进行评估,并在模型失败的地方扩大数据集。
问:微调端点期望什么格式?
聊天格式 JSON 行:每一行都是一个独立的 JSON 对象,包含一个 messages 数组,包含系统、用户和助手的轮次,上传时目的为 fine-tune。本指南中的管道直接使用 json.dumps 编写该格式,每行一个对象,无尾部逗号或包装数组。
问:我应该微调还是使用 RAG?
当您希望模型改变行为——语气、格式、任务特定的反应——时请微调;当您希望它了解当前事实时请检索。权重更新保留模式但会过时;检索保持最新,但不会教会模型新的习惯。两者可以组合,并且都来自本指南构建的相同集合层。
问:在抓取数据上训练模型是否合法?
这取决于您收集的内容和您操作的地点,阅读公共页面并不自动意味着您可以在其上进行训练。网站条款、机器人指令、版权以及任何涵盖语料库中个人数据的隐私法律均适用——上面的责任部分列出了工作实践,任何超出明确定义的公共、非个人内容的许可证问题都应咨询法律顾问。
问:这个管道也适用于开放权重模型吗?
是的——集合和转换阶段与模型无关。聊天格式 JSONL 是调优堆栈的共同基础;开放权重工作流使用相同的对话结构,因此唯一变化的阶段是阶段 5,在该阶段,上传调用被您的训练框架的数据集加载器替代。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



