Scrapelessを使ったAIトレーニングデータパイプラインの構築方法
Advanced Data Extraction Specialist
TL;DR:
- ファインチューニングデータセットはダウンロードではなく、パイプライン製品です。 このガイドでは、全体を通して一つを構築します:Scrapeless Universal Scraping APIを通じてレンダリングされた公開ページを取得し、Python標準ライブラリでラベル付きペアを抽出し、トレイン/バリデーションの分割を含むOpenAI準拠のチャット形式のJSONLを書く。
- フェッチ層はページごとに1回のHTTP POSTです。
unlocker.webunlockerアクターは、POST /api/v1/unlocker/requestからレンダリングされたHTMLを返します — ブラウザの管理も、プロキシプールのローテーションも必要ありません。 - アップロードまでのすべてがScrapelessキーだけで実行されます。 デモクロールは公共の引用サイトの10ページをカバーし、100の監視された例を生成します。最終的なファインチューニングジョブのみがOpenAIのキーと予算を必要とします。
- フォーマットエラーは最も安価に防ぐことができます。 各トレーニングラインは
{"messages": [...]}オブジェクトで、システム、ユーザー、アシスタントのやりとりがあります —json.dumpsで書けば、ファイルは最初のアップロードで解析されます。 - 出処はデータセットの一部です。 公開ページのみ、制限されたボリューム、クロールの前にサイトの利用規約とロボット指令を確認します — 責任セクションは、トレーニングデータが通常のスクレイピングに関する質問に追加する内容をカバーします。
- 開始は無料です。 無料プランで自分のAPIキーをapp.scrapeless.comで作成します。
導入:データセットは難しい部分です
LLMのファインチューニングは管理的には簡単です — ファイルをアップロードし、ジョブを作成し、待つだけ。ジョブが修正できないのはファイルです。タスクの整然とした正しくラベル付けされた100の例で調整されたモデルは、10,000のノイジーな行で調整されたモデルを上回ることがありますし、その違いはトレーニングランが始まる前、データを収集し形成したパイプラインで決まります。
そのパイプラインはほとんどのチームにとってスクレイピングの問題です。なぜなら、調整する価値のあるドメイン知識はウェブページに存在するからです:製品説明、文書、リスト、レビュー、参考資料。旅の概念的側面は、AIモデルのトレーニングがどのように行われるかのガイドでカバーされており;この投稿は実行可能な半分です。公開デモサイトに対して完全なパイプラインを構築します — quotes.toscrape.com、スクレイピング練習のために構築されたサイトです — そしてOpenAIファインチューニングエンドポイントがそのまま受け入れるtrain.jsonlとval.jsonlファイルで終わります。
デモタスク:モデルに有名な引用を帰属させることを教えます。数分で実行可能で、実際のものと正確に同じ構造です。
パイプラインの概要
パイプラインは5段階で構成されており、最初の4段階はこのガイド内でScrapeless APIキーだけで実行されます:
- フェッチ — Universal Scraping APIを通じて、各レンダリングページを取得します。ページごとに1回のPOST。
- ディスカバー — サイト自身のページネーションを追跡し、終了まで進みます。安全境界としてのハードページ制限があります。
- 抽出 — Pythonの標準ライブラリのHTMLパーサーを使用して各ページから引用テキストと著者を解析します。
- 変換 — 各ペアをチャット形式のトレーニング例に変換し、80/20に分割してJSONLに書き込みます。
- トレーニング — 両方のファイルをアップロードし、ファインチューニングジョブを作成します(この段階はOpenAIキーが必要です;コードは示されており、適切にラベル付けされています)。
ページごとのフロー: レンダリングまたは取得 → 次のページを発見 → ペアを抽出 → 例に変換 → JSONLとして保存。
なぜScrapeless Universal Scraping APIなのか
Universal Scraping APIは、ページ収集を決定論的な関数呼び出しに変換します:URLをPOSTすると、サービスがサーバーサイドでレンダリング、ロック解除、プロキシルーティングを処理し、dataフィールドにページHTMLが返されます。データセットビルダーにとっては非常に重要です。第一に、コーパスは再現可能です — 同じリクエスト形式が、元が静的なデモサイトであろうとJavaScriptが多い製品カタログであろうと機能するため、ターゲットが変わってもパイプラインコードは変わりません。第二に、ローカルブラウザの群れは不要です:数百ページのデータセットは、1つの関数をループするだけです。
ここでのデモサイトは意図的にフレンドリーです。ガイドの目的はパイプラインの形状です;URLリストとパーサーを実際のコーパスに向けるときに交換し、ボリューム制限を維持してください。
前提条件
requestsパッケージを使用したPython 3 — パイプライン内の他のインポートは標準ライブラリです;このガイドで実行されたのはPython 3.12でした。- Scrapeless APIキー — 開発者ドキュメントにはキー作成が説明されています。
- ステージ5のみ:ファインチューニングアクセスと予算を持つOpenAI APIキー。ステージ1〜4はそれなしで実行されます。
スクレイプレスのキーをエクスポートして、スクリプトが環境から読み込めるようにします:
bash
export SCRAPELESS_API_KEY="sk_your_key_here"
ステージ 1 — レンダリングされたページを取得する
1つのPOSTリクエストは、1つのレンダリングされたページを返します。unlocker.webunlockerアクターは、inputにターゲットURLを取り、レスポンスのdataフィールドにHTMLを返します:
python
# fetch_page.py — ステージ 1: Scrapelessを通じて1つのレンダリングページを取得する
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"取得したHTMLの文字数: {len(html):,} 文字")
print("引用マークアップが存在:", '<span class="text"' in html)
print("著者マークアップが存在:", '<small class="author"' in html)
引用サイトのページ1に対して、これは両方のマーカーが存在するHTMLの11,021文字を返します。raise_for_status()は、失敗を大きな声で知らせます:悪いキーや到達不可能なターゲットは、空のコーパスを書き込む代わりにパイプラインを停止させます。
ステージ 2 — 完全なコーパスを発見する
サイトは終了地点を教えてくれるので、クローラーはURLを推測するのではなく、サイトに従います。デモサイトの各ページには、最後のページまでli class="next"要素があります。クローリングループは、マーカーを取得し、そのマーカーをチェックし、ページカウンタを進めます。2つの制約がステージを誠実に保ちます:ループはマーカーが消えると停止し、MAX_PAGESの上限は、マーカーが決して消えなくても停止させます。この上限は、データセット構築と無制限のクローリングの違いです — 実際に収集したいコーパスサイズに設定してください。
ループ自体は、ステージ4の完全なパイプラインスクリプトの中に4行で含まれています。
ステージ 3 — 引用–著者ペアを抽出する
抽出はHTMLをラベル付きペアに変換し、標準ライブラリで十分です。html.parser.HTMLParserは、タグごとにコールバックを発火させます;ページを歩きながら2つのフラグを追跡することで、各引用のテキストと著者を第三者依存なしで収集します:
python
# quote_parser.py — ステージ 3: (引用, 著者)ペアの標準ライブラリ抽出
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自体)でとどまるためで、パイプラインをスケジューラに移行する際に固定すべきものが一つ減ります。
ステージ 4 — チャットフォーマットJSONLに変換する
OpenAIのファインチューニングエンドポイントはチャットのトランスクリプトで学習します:ファイルの各行は1つの{"messages": [...]}オブジェクトであり、システムルール、ユーザーの入力、モデルに学習させたいアシスタントの回答を含みます。フォーマットはJSON Linesです — JSON Linesフォーマット定義は正確に「1行あたり1つのJSON値」です — instruction tuningに関する研究が InstructGPT論文のように見える理由は、モデルはデモペアで訓練されるとタスクによりよく従うからです。
このスクリプトは、ステージ1から4を組み合わせた全体のパイプラインで、2つのファイルに終わります:
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":
自己._モード = "テキスト"
elif タグ == "小" and a.get("クラス") == "作者":
自己._モード = "作者"
def handle_data(self, データ):
if 自己._モード == "テキスト":
自己._テキスト = データ.strip("“”")
elif 自己._モード == "作者":
自己.ペア.append((自己._テキスト, データ.strip()))
自己._モード = None
def fetch_page(url: str) -> str:
resp = requests.post(
エンドポイント,
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},
]
}
ペア, ページ = [], 1
while ページ <= 最大ページ数:
html = fetch_page(f"{BASE}/page/{ページ}/")
parser = QuoteParser()
parser.feed(html)
ペア.extend(parser.ペア)
if 'class="next"' not in html: # ステージ2: サイトが終了を告げる
break
ページ += 1
例 = [to_example(t, a) for t, a in ペア]
分割 = int(len(例) * 0.8)
for name, rows in (("train.jsonl", 例[:分割]), ("val.jsonl", 例[分割:])):
with open(name, "w", encoding="utf-8") as f:
f.writelines(json.dumps(r, ensure_ascii=False) + "\n" for r in rows)
print(f"クロールしたページ数: {ページ} | 抽出したペア数: {len(ペア)}")
print(f"train.jsonl: {分割} 例 | val.jsonl: {len(例) - 分割} 例")
print("最初のトレーニング行:")
print(json.dumps(例[0], ensure_ascii=False)[:180])
この実行では、サイトの10ページすべてをクロールし、100対を抽出し、80のトレーニング例と20の検証例を書き込みます。80/20の分割は、ファインチューニング作業に一般化を測定するためのものを提供します—モデルが訓練されない検証例。両方のファイルはensure_ascii=Falseで書き込まれ、一行につき一つのjson.dumpsがあり、これは非常に安価な保険です: 整形されていないJSONLは、アップロードエンドポイントへの往復を失う一般的な方法です。
おもちゃのコーパスでも手動で品質チェックを行う価値があります: 行のサンプルを読み、ラベルが本当に入力に応えているか確認します。モデルは、ファイルが示すことを学び、誤りも含まれます。
ステージ5 — ファインチューニングジョブの提出
トレーニングコールはそれに比べて小さいです。目的「fine-tune」で両方のファイルをアップロードし、ファインチューニング可能なモデルに対してジョブを作成します — 現在のリストとパラメータはOpenAIの監視付きファインチューニングガイドにあります。
注: このステージはパイプラインの1つの前提条件のギャップです —
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
トレーニングデータの責任あるスクレイピング
トレーニングデータはソースページが持っていたすべての義務を含み、さらに1つ: モデルは与えられたものでパターンを再生します。4つの実践が収集の側を防御可能にします。
- 公開ページのみを対象とし、規約を読みます。 アカウントなしでレンダリングされるもののみを収集し、クロールの前にターゲットサイトのサービス規約を確認します — トレーニング使用が明示的に呼び出されるサイトが増えています。
- ロボットの指示を守る。 ロボット排除プロトコル (RFC 9309) は、サイトがクローラーに触れることを許可する標準の機械可読ステートメントです; 収集の前にターゲットパスを確認します。
- 最小限に抑える。 タスクに必要なフィールドのみを取得します — ここでは引用テキストと作者 — ユーザーのコメント、名前、または他の付随的なデータを引きずり込む全ページのダンプは不要です。制限されたページの上限は最小限に抑える一部です。
- 出典とライセンスを追跡する。 各例の出所と日時を記録します。公に読めるテキストは、すべての法域でモデルのトレーニングに自動的にライセンスされるわけではありません。有名な引用以上に敏感なコーパスの場合は、ジョブが実行される前に法務にライセンスの問題を確認してもらってください。
返還されるもの
アップロードの準備が整った2つのファイル。各行が1つの完全な教師あり例であり、上記の実行からのtrain.jsonlの最初の行は以下の通りです:
text
{"messages": [{"role": "system", "content": "有名な引用を atribu します。著者の名前のみで返信してください。"}, {"role": "user", "content": "この言葉を言ったのは誰ですか: “私たちが作り上げた世界は私たちの思考のプロセスです。それは私たちの思考を変えなければ変えることはできません。”"}, {"role": "assistant", "content": "アルバート・アインシュタイン"}]}
形状は変わらずにスケールします:実際のコーパスはパーサーとURLリストを入れ替え、システムルールは引用の帰属ではなくあなたのタスクを説明し、JSONLライター、分割、およびアップロードは同一のままです。目標がウェイトの更新ではなく取得である場合、同じフェッチレイヤーがクリーンテキストRAGパイプラインに供給されます—トレーニング例ではなくチャンクと埋め込みです。
結論
パイプラインは両端でその価値を映します。前面では、ユニバーサルスクレイピングAPIがページごとに決定論的なPOSTを行うため、コーパスは再現可能で、ターゲットの変更からコードは生き残ります。後面では、規律ある変換 — 正確なチャット形式、行ごとの1つのJSONオブジェクト、実際の検証分割、ラベルの人間による確認 — が答えを改善するファインチューニングと予算を消耗するものを分ける要素です。その両端の間には、今あなたが持っている100行のスタンダードライブラリPythonがあります。
トレーニングデータパイプラインを構築する準備はできましたか?
計画と含まれるリクエストボリュームは料金ページにあり、このガイドのフェッチレイヤーは無料プランで機能します — app.scrapeless.comでAPIキーを作成し、ステージ1で最初のレンダリングされたページを1つのPOSTで返します。
よくある質問
Q: モデルを微調整するのにどれくらいのデータが必要ですか?
例がクリーンであれば、ほとんどのチームが期待するよりも少ないです。教師あり微調整は、狭いタスクに対して十分にラベルが付けられた例が数十から数百までのコーパスで測定可能な行動変化を示します;広い行動変化にはより多くが必要です。タスクを表す最小のコーパスから始め、検証ファイルに対して評価し、モデルが実際に失敗するところでデータセットを増やします。
Q: 微調整エンドポイントはどのようなフォーマットを期待しますか?
チャット形式のJSONライン:各行はシステム、ユーザー、アシスタントのターンのmessages配列を持つ独立したJSONオブジェクトであり、目的はfine-tuneでアップロードされます。このガイドのパイプラインは、その形式をjson.dumpsを用いて直接書き込み、1行ごとに1つのオブジェクト、末尾のコンマやラッピング配列はありません。
Q: 微調整するべきですか、それともRAGを使うべきですか?
モデルの行動を変えたい場合は微調整を行い — トーン、形式、タスク特有の反応 — 現在の事実を知ってほしい場合は取得を行います。ウェイトの更新はパターンを組み込みますが、時が経つにつれて古くなります;取得は最新の状態を保ちますがモデルに新しい習慣を教えません。この二つは組み合わさり、どちらもこのガイドが構築する同じコレクションレイヤーから始まります。
Q: スクレイピングしたデータでモデルをトレーニングするのは合法ですか?
収集するものと運営する場所によりますし、公にページを読むことは、それらでトレーニングするための自動的なライセンスではありません。サイトの条件、ロボットの指令、著作権、コーパス内の個人データに関するプライバシー法がすべて適用されます — 上記の責任セクションには作業慣行がリストされていますし、明らかに公共の非個人コンテンツを超えるものについては、ライセンスの問題は法務に所属します。
Q: このパイプラインはオープンウェイトモデルにも適用できますか?
はい — 収集と変換の段階はモデルに依存しません。チャット形式のJSONLは調整スタック全体での共通の分母です;オープンウェイトのワークフローは同じ会話構造を消費するため、唯一変わるステージはステージ5で、アップロードコールがあなたのトレーニングフレームワークのデータセットローダーに置き換えられます。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



