PythonでRespondoを使用してHTMLからデータを抽出する
Expert in Web Scraping Technologies
TL;DR:
- RespondoはScrapelessのオープンソースPythonライブラリで、HTMLとJSONをレコードに変換します。 既存のページやAPIレスポンスで機能し、ランタイム依存関係はありません。
- GitHubからバージョン0.6をインストールします。 PyPIの
respondoパッケージはまだ0.4.0で、フィールドレシピ、JSON Linesヘルパー、バッチモードの前です。 - フィールドレシピは各列をセレクタにマッピングします。 レシピはテキストの代わりに属性を読み取ることができ、コードの隣にJSONファイルとして保存できます。
- CSV出力はデフォルトでスプレッドシート安全です。 数式のように始まる値にはアポストロフィ接頭辞が付加され、数値は数値のままです。
- Respondoはページを取得したりレンダリングしたりしません。 Scrapeless Universal Scraping APIがHTMLを収集し、Respondoがそれを行に変換します。
- コレクションステップをScrapelessの無料プランで試して、数分で最初のページを抽出しましょう。
HTMLからPythonでデータを抽出するには、2つのものが必要です:HTML自体と、タグをフィールドに変換するコードです。2番目の部分は、各サイトごとに異なるループやCSVコードに成長しがちです。
Respondoはその2番目の部分をパッケージ化します。各フィールドをセレクタとオプションの属性で1度説明し、RespondoはCSVまたはJSON Linesに書き込むことができる辞書のリストを返します。ガイドでは、公共のプラクティスサイトから小さな書籍カタログを構築し、その後、コレクションステップにScrapelessと組み合わせます。
What Respondo Is
Respondoは、Scrapeless GitHub組織の下で維持され、MITライセンスの下で公開されているローカル抽出ツールキットです。そのGitHubのソースリポジトリは、フィールドを説明する1行で分割を示します:Scrapelessで収集し、Respondoで抽出、変換、エクスポートします。
このライブラリは、既に持っているコンテンツで機能します。そのHTML関数は標準ライブラリのhtml.parserモジュールを基に構築されているため、インストールされた環境にはRespondo自体以外のものは含まれていません。バージョン0.6は4種類の作業をカバーします:
- CSSスタイルのセレクタと再利用可能なフィールドレシピを使用したHTMLからの反復レコード;
- JSONドキュメントでのクエリ、フラット化、投影、マージパッチ;
- ページの要約、フィード、サイトマップ;
- 21のモードを持つ
respondoコマンド、フォルダー全体のバッチ処理を含む。
RespondoはURLを取得したりブラウザを起動したりせず、Scrapeless APIクライアントは含まれていません。パースが何を含むかまだ決めかねている場合は、データパースとは何かの概要で概念をカバーしています。
Install Respondo 0.6
GitHubからRespondoを直接インストールし、コミットにピン留めします。Python 3.9以降が必要です:
bash
python -m pip install "git+https://github.com/scrapeless-ai/respondo@be376d112ecf681011a079e809acae46b7e1ff59"
respondo --version
text
respondo 0.6.0
コミットピンは、このガイドがテストされた正確なコードの上で環境を維持します。プレーンなpip install respondoはPyPIからバージョン0.4.0をインストールし、以下で使用される関数は含まれていません。インストール後、pip listはrespondoとpipのみを表示します。
Define a Field Recipe
フィールドレシピは、各出力列を反復アイテム内でそれを見つけるためのルールにマッピングする辞書です。プレーンな文字列は、そのテキストが値になるセレクタです。辞書はオプションを追加します:
selectorは、現在のアイテム内の要素を見つけます。attrはテキストの代わりにhrefやtitleのような属性を読み取ります。required: Trueは、アイテムに一致がない場合にエラーを発生させます。many: Trueはリストを返し、defaultは欠落した値のフォールバックを設定します。
セレクタはタグ、クラス、ID、属性テスト、子孫や子のコンビネータ、カンマグループをカバーします。これはW3C Selectors仕様の意図的なサブセットです::nth-childのような擬似クラスや兄弟コンビネータはValueErrorによって拒否され、無視されるのでありません。
レシピはJSONファイル内にも存在でき、これによりセレクタをコードから切り離して、コマンドラインで再利用できます。これをbook-fields.jsonとして保存します:
json
{
"title": {"selector": "h3 a", "attr": "title", "required": true},
"price": ".price_color",
"availability": ".availability",
"url": {"selector": "h3 a", "attr": "href"}
}
titleフィールドはリンクのtitle属性を意図的に読み取ります。プラクティスサイトの可視リンクテキストは長い名前のために短縮される一方、属性は完全なタイトルを保持しています。
Extract Records and Write CSV
extract_recordsはHTML、反復アイテムのためのセレクタ、レシピを受け取り、アイテムごとに1つの辞書を返します。この例では、プラクティスサイトのミステリーカテゴリからコピーした2つのアイテムを使用します:
python
from respondo import extract_records, normalize_url, records_to_csv
PAGE_URL = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
html = """
<article class="product_pod">
<h3><a href="../../../sharp-objects_997/index.html" title="Sharp Objects">Sharp Objects</a></h3>
<p class="price_color">£47.82</p>
<p class="instock availability"><i class="icon-ok"></i> In stock</p>
</article>
<article class="product_pod">
<h3><a href="../../../in-a-dark-dark-wood_963/index.html" title="In a Dark, Dark Wood">In a Dark, Dark ...</a></h3>
<p class="price_color">£19.63</p>
<p class="instock availability"><i class="icon-ok"></i> In stock</p>
</article>
"""
books = extract_records(html, "article.product_pod", {
"title": {"selector": "h3 a", "attr": "title", "required": True},
"price": ".price_color",
"availability": ".availability",
"url": {"selector": "h3 a", "attr": "href"},
})
for book in books:
book["url"] = normalize_url(book["url"], base=PAGE_URL)
print(records_to_csv(books), end="")
text
title,price,availability,url
Sharp Objects,£47.82,In stock,https://books.toscrape.com/catalogue/sharp-objects_997/index.html
"In a Dark, Dark Wood",£19.63,In stock,https://books.toscrape.com/catalogue/in-a-dark-dark-wood_963/index.html
三つの詳細が処理されます。.availabilityのテキストはトリムされ、アイコン要素は含まれません。相対hrefは、URI仕様の参照解決ルールが示すように、ページアドレスに対して解決された絶対URLに変わります。そして、カンマを含むタイトルはCSVで引用されます。
records_to_csvはスプレッドシートの数式インジェクションに対しても保護します。このリスクはOWASPのCSVインジェクションに関するエントリーで説明されています。=、+、-、@、タブまたは改行で始まる文字列は、先頭にアポストロフィを付けられ、=HYPERLINK(1)は'=HYPERLINK(1)として書かれますが、-5のような実数はそのまま保持されます。ファイルがスプレッドシートに到達しない場合のみescape_formulas=Falseを渡します。
さらに進む:ページ要約、JSON Lines、およびCLI
レコードは一つの出力です。同じパッケージはページ全体を要約し、JSON Linesを処理し、respondoコマンドはシェルからのレコード抽出を実行します。1つのファイルまたはフォルダー全体のために。
ページ全体を要約する
extract_pageは、ドキュメントのタイトル、テキスト、メタデータ、見出し、リンク、画像、表を一度の呼び出しで返します。ミステリーカテゴリーページの保存されたコピーで実行してください:
python
from respondo import extract_page
with open("mystery-page-1.html", encoding="utf-8") as handle:
page = extract_page(
handle.read(),
base="https://books.toscrape.com/catalogue/category/books/mystery_3/index.html",
)
print(sorted(page))
print(page["headings"][:2])
print(len(page["links"]), "links,", len(page["images"]), "images")
text
['headings', 'images', 'links', 'meta', 'tables', 'text', 'title']
[{'level': 1, 'id': '', 'text': 'Mystery'}, {'level': 3, 'id': '', 'text': 'Sharp Objects'}]
95 links, 20 images
テキスト、見出し、リンクはスクリプト、スタイル、および文書のヘッダーをスキップし、相対リンクはbaseに対して解決されます。
JSON Linesをクエリする
jsonl_dumpsは、JSON Linesとしてレコードを書き込むもので、1行に1つのコンパクトなオブジェクトが含まれ、iter_jsonlはそれを遅延読み込みします。json_queryは、小さなパス言語を用いて値を取り出し、常にリストを返します:
python
from respondo import iter_jsonl, json_query
with open("mystery-books.jsonl", encoding="utf-8") as handle:
books = list(iter_jsonl(handle))
print(len(books), "records")
print(json_query(books, "$[*].title")[:3])
print(json_query(books, "[-1].price"))
text
20 records
['Sharp Objects', 'In a Dark, Dark Wood', 'The Past Never Ends']
['£20.89']
パス構文はドットキー、引用キー、負のインデックス、*ワイルドカードをカバーします。フィルターや再帰的降下はなく、何にもマッチしないパスは空のリストを返します。
コマンドラインから同じレシピを実行する
respondoコマンドはローカルファイルを読み取り、デフォルトではJSONを書き込みます。--formatを使用するとCSVおよびJSON Linesにも対応します:
bash
respondo records mystery-page-1.html --selector article.product_pod --fields book-fields.json --format csv | head -4
text
title,price,availability,url
Sharp Objects,£47.82,In stock,../../../sharp-objects_997/index.html
"In a Dark, Dark Wood",£19.63,In stock,../../../in-a-dark-dark-wood_963/index.html
The Past Never Ends,£56.50,In stock,../../../the-past-never-ends_942/index.html
recordsモードでは、URLはページに表示される通りにそのまま保持されます。--baseが渡された場合でも残ります。絶対リンクが必要な場合には、normalize_urlでPythonで解決してください。
保存されたページのフォルダーを処理する
バッチモードは、ディレクトリ内のすべての一致ファイルに対して1つのレシピを実行し、ファイル名の順で整列し、ファイルごとに1つの結果行を書き込みます:
bash
respondo records responses/ --batch --pattern '*.html' \
--selector article.product_pod --fields book-fields.json \
--format jsonl --output results.jsonl
python -c "import json; [print(row['source'], row['status'], len(row['result'])) for row in map(json.loads, open('results.jsonl'))]"
text
mystery-page-1.html ok 20
mystery-page-2.html ok 12
各行にはsource、status、result、およびerrorが含まれているため、一つの読めないファイルが他を妨げることはありません。この2つのページには、カテゴリ内のすべての32冊の本が含まれています。バッチモードは決して上書きしません:同じコマンドを再度実行すると、respondo: batch output existsで停止し、終了ステータス1になります。そして、入力フォルダー内の出力パスは安全でないとして拒否されます。
Respondoが停止する場所:Scrapelessでページを収集する
Respondoは与えられたものだけを解析し、それ以上は行いません。ページをダウンロードしたりJavaScriptを実行したりしないため、そのセレクタは受け取ったHTMLしか見ることができません。そのステップでは、Scrapeless Universal Scraping APIがURLを取得してページを返します。そのため、二つの部分は分離されたままです:APIキーは収集呼び出しに属し、抽出はローカルで実行されます。
これを今設定していますか?Scrapelessの無料プランが最初のリクエストをカバーします。
このスクリプトはunlocker.webunlockerアクターを通じてライブのミステリーカテゴリーページを収集し、その上で同じレシピを実行します。SCRAPELESS_API_KEY環境変数からあなたのキーを読み込みます:
python
import json
import os
import urllib.request
from respondo import extract_records, jsonl_dumps, normalize_url, records_to_csv
PAGE_URL = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
BOOK_FIELDS = {
"title": {"selector": "h3 a", "attr": "title", "required": True},
"price": ".price_color",
"availability": ".availability",
"rating": {"selector": "p.star-rating", "attr": "class"},
"url": {"selector": "h3 a", "attr": "href"},
}
def fetch_html(url):
payload = {"actor": "unlocker.webunlocker", "input": {"url": url, "method": "GET", "js_render": False}}
request = urllib.request.Request(
"https://api.scrapeless.com/api/v2/unlocker/request",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json", "x-api-token": os.environ["SCRAPELESS_API_KEY"]},
)
with urllib.request.urlopen(request, timeout=120) as response:
body = json.load(response)
if body.get("code") != 200:
raise RuntimeError(f"Scrapeless returned code {body.get('code')}")
return body["data"]
html = fetch_html(PAGE_URL)
books = extract_records(html, "article.product_pod", BOOK_FIELDS)
for book in books:
book["url"] = normalize_url(book["url"], base=PAGE_URL)
book["rating"] = book["rating"].split()[-1]
print(len(books), "books")
print(records_to_csv(books[:3]), end="")
with open("mystery-books.jsonl", "w", encoding="utf-8") as handle:
handle.write(jsonl_dumps(books))
text
20 books
title,price,availability,rating,url
Sharp Objects,£47.82,In stock,Four,https://books.toscrape.com/catalogue/sharp-objects_997/index.html
"In a Dark, Dark Wood",£19.63,In stock,One,https://books.toscrape.com/catalogue/in-a-dark-dark-wood_963/index.html
The Past Never Ends,£56.50,In stock,Four,https://books.toscrape.com/catalogue/the-past-never-ends_942/index.html
APIはページをJSON封筒にラッピングし、{"code": 200, "data": "<html>…"}、urlopenがHTTPエラーのためのHTTPErrorを引き起こします。このエンベロープが読み込まれる前に。評価はp.star-ratingのクラスリストから取得され、その最後のクラス名は星の数を示します。Universal Scraping APIの入門ガイドは、プロキシ国やリダイレクトハンドリングなど、他のリクエストオプションを示しています。
トラブルシューティング
| あなたが見るもの | 原因 | 修正 |
|---|---|---|
ValueError: required field has no matches |
項目にrequiredとしてマークされたフィールドが欠けています |
ページに対してセレクターを確認するか、requiredを削除してdefaultを使用してください |
ValueError: unsupported selector syntax |
セレクタは :nth-child のような擬似クラスを使用します |
クラス、ID、または属性で選択してください |
ValueError: expected a tag, class, ID or attribute selector |
セレクタは + または ~ を使用します |
子孫または子コンビネータを使用します |
| すべての行に対して列が空です | コンテンツはロード後にJavaScriptによって追加されます | js_render を有効にしてページをリクエストします |
| CLI出力内の相対URL | records モードは属性値をそのまま保持します |
Pythonで normalize_url を使って解決します |
| 一部のCSV値の前のアポストロフィ | フォーミュラのエスケープはデフォルトでオンです | それを保持するか、信頼できる消費者用に escape_formulas=False を渡してください |
respondo: batch output exists |
出力ファイルはすでに存在します | 新しいファイル名を選択してください |
respondo: batch unsafe output path |
出力ファイルは入力フォルダ内にあります | 結果を他の場所に書き込みます |
ブラウザでコンテンツを構築するページについては、Universal Scraping APIでのページのレンダリングがオプションを説明し、JS Renderのドキュメントがパラメータをリストします。料金をチェックして、レンダリング要求のコストを確認してください。
結論
Respondoはスクレイピング作業の抽出部分を構成設定に変えます:繰り返し項目のためのセレクタとそのフィールドのレシピです。そこから、extract_records は records_to_csv または jsonl_dumps を使用してファイルに変換する辞書を返します。respondo コマンドはフォルダ全体で同じレシピを実行し、各ページの結果を報告します。
二つの部分を分けておきます。GitHubから0.6をインストールし、Universal Scraping APIを使用してページを収集し、Respondoが戻ってきたものに取り組むようにします。ネットワーク接続や独自の認証情報は必要ありません。
Respondoに実際のページを供給する準備はできましたか? Scrapelessの無料プランから始める そして最初のページを収集してください。
FAQ
Q: Respondoとは何ですか?
Respondoは、すでに持っているHTMLおよびJSONからデータを抽出、変換、エクスポートするScrapelessのオープンソースPythonライブラリです。ランタイム依存はなく、完全にあなたのマシンで動作します。
Q: Respondo 0.6をどうやってインストールしますか?
python -m pip install "git+https://github.com/scrapeless-ai/respondo@be376d112ecf681011a079e809acae46b7e1ff59" を使ってGitHubからインストールします。PyPIパッケージは0.4.0で、このガイドの機能は含まれていません。
Q: Respondoはウェブページをダウンロードできますか?
いいえ。Respondoはあなたが渡したコンテンツのみを解析します。ダウンロードステップにはScrapeless Universal Scraping API、または他のHTMLソースを使用してください。
Q: Respondoを使ってHTMLからCSVにデータを抽出するにはどうすればよいですか?
extract_records を呼び出し、繰り返し項目のためのセレクタとフィールドレシピを指定し、その結果を records_to_csv に渡します。シェルからは、respondo records page.html --selector … --fields recipe.json --format csv が同じことを行います。
Q: RespondoはどのCSSセレクタをサポートしていますか?
タグ、クラス、ID、属性テスト、子孫および子コンビネータ、カンマグループです。擬似クラスや兄弟コンビネータは ValueError を引き起こします。
Q: なぜ私のCSVにはいくつかの値の前にアポストロフィがあるのですか?
Respondoは =、+、-、@、タブまたは改行で始まる文字列にプレフィックスを付けるので、スプレッドシートはそれらを数式として実行しません。数字はそのままにされ、escape_formulas=False がプレフィックスをオフにします。
Q: RespondoはJavaScriptでレンダリングされたページを扱いますか?
Respondoは受け取ったHTMLを解析し、スクリプトを実行しません。そのようなページは、Universal Scraping APIでJavaScriptレンダリングを有効にして取得し、レンダリングされたHTMLをRespondoに渡します。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



