Polarsウェブスクレイピング:ライブページから高速データフレームへ
Scraping and Proxy Management Expert
TL;DR:
- Polarsは高速でArrowバックのDataFrameライブラリで、Scrapelessと組み合わせてライブページを型付きのクエリ可能なフレームに変換します。
- PolarsにはHTTPクライアントやHTMLパーサーはないため、フェッチと抽出のステップはScrapelessとパーサーから来る;Polarsはレコードが存在するようになってから引き継ぎます。
pl.DataFrame(records)を使用して辞書のリストからフレームを構築し、抽出中に値を強制すると、スキーマは最初の行から型付きになります。- 式APIはpandasスタイルのチェーンインデックスを置き換えます:
pl.col("price_gbp").mean()をgroup_by(...).agg(...)の中で使用すると、読み取りと実行がクリーンに行えます。 - レイジーパス
df.lazy()...collect()は、Polarsがデータに触れる前に全体のクエリを計画・最適化できるようにし、これが大きなデータセットで優位に立つ理由です。 - Scrapelessの無料プランに登録し、フェッチのステップを自身のカタログに向けます。
Polarsは高速でAPIが快適なため、データ作業でしばしば目にしますが、ほとんどのチュートリアルはすでに存在するParquetファイルを使っています。スクレイピングはその逆の状況です。そのデータはライブであり、HTMLであり、型がないため、型を作成する必要があります。このガイドでは、URLから公共の本のカタログを取得し、グループ化や集約ができるPolars DataFrameに変換する方法を説明し、Polarsができない1つのこと、ページの取得について正直に述べています。
フェッチはScrapeless Universal Scraping APIから行い、公開URLのレンダリングされたHTMLを返します。パーサーがそのHTMLをレコードに変換します。Polarsは残りの作業を迅速に行います。
Polarsがスクレイピングワークフローに追加するもの
PolarsはApache Arrowのカラム型メモリフォーマットに基づいたDataFrameライブラリで、これにより型付きカラムとグループ化された集約が迅速に行われます。スクレイピングされたデータの場合、その利点は明確です:レコードがフレームに入ると、それをクリーンアップして要約するのが手書きのループではなく数式で済み、カラムがその型を保持します。Polarsはそのメモリ内レイアウトにApache Arrowのカラム形式を利用しているため、価格のカラムは本物の数値カラムであり、各操作で再解析する必要のある文字列のリストではありません。
インストール
Polarsがフレームを処理し、パーサーがHTMLを処理します。両方をインストールします。
bash
pip install polars lxml
シェルで一度ScrapelessのAPIキーを設定します。実際のキーを実行時に使用し、プレースホルダーをソースから外します。
bash
export SCRAPELESS_API_KEY="sk_your_key_here"
フェッチ、抽出、およびフレームの構築
最初のステージはページを取得し、各製品に対して1レコードを取得してDataFrameを構築します。PolarsはHTMLを取得または解析できないため、このステージではScrapelessとパーサーが作業を行い、Polarsは完了したレコードを受け取ります。
python
import json
import os
import urllib.request
import polars as pl
from lxml import html as lxhtml
API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
def fetch_html(url: str) -> str:
payload = json.dumps(
{"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
).encode()
request = urllib.request.Request(
API_URL,
data=payload,
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=90) as response:
return json.loads(response.read())["data"]
dom = lxhtml.fromstring(fetch_html("https://books.toscrape.com/"))
records = []
for card in dom.cssselect("article.product_pod"):
records.append(
{
"title": card.cssselect("h3 a")[0].get("title"),
"price_gbp": float(card.cssselect("p.price_color")[0].text_content().strip().lstrip("£")),
"rating": RATINGS[card.cssselect("p.star-rating")[0].get("class").split()[-1]],
"in_stock": "In stock" in card.cssselect("p.instock.availability")[0].text_content(),
}
)
df = pl.DataFrame(records)
print(f"抽出されたレコード数: {df.height}")
print("スキーマ:", dict(df.schema))
抽出では、lxmlのcssselectを通じて< a href="https://www.w3.org/TR/selectors-3/" rel="nofollow">CSSセレクター標準を使用し、各値を読み取る際に強制型変換を行います:価格はfloatになり、星評価の文字は整数になり、可用性はブール値になります。その強制型変換が重要な習慣です。本物のPython型でレコードを構築すると、Polarsはそこからリアルなスキーマを推論します。
text
抽出されたレコード数: 20
スキーマ: {'title': String, 'price_gbp': Float64, 'rating': Int64, 'in_stock': Boolean}
最初のページにある20個のプロダクトカードは、String、Float64、Int64、およびBooleanという型のスキーマを持つフレームになります。後で再解析しなければならないテキストの列はありません。
式を使った変換
Polarsはチェーンインデクシングを式APIで置き換え、これは正しく学ぶ価値のある部分です。pl.col("price_gbp").mean()のような式は、Polarsが効率的に実行する計算を記述しており、式はfilter、group_by、およびaggの中で組み合わされます。
python
summary = (
df.lazy()
.filter(pl.col("in_stock"))
.group_by("rating")
.agg(pl.len().alias("books"), pl.col("price_gbp").mean().round(2).alias("avg_price"))
.sort("rating")
.collect()
)
print(summary)
df.lazy()はイagerフレームをクエリプランに変換し、このチェーンが作業の内容を記述し、.collect()がそれを実行します。この小さなセットでは違いは見えませんが、レイジーパスによりPolarsはクエリ全体を分析し、単一の値を読み込む前に作業をスキップできるため、スケーラブルです。在庫がある書籍を星の評価でグループ化し、各グループのカウントと平均価格を報告します。
text
shape: (5, 3)
┌────────┬───────┬───────────┐
│ rating ┆ books ┆ avg_price │
│ --- ┆ --- ┆ --- │
│ i64 ┆ u32 ┆ f64 │
╞════════╪═══════╪═══════════╡
│ 1 ┆ 6 ┆ 40.02 │
│ 2 ┆ 3 ┆ 36.83 │
│ 3 ┆ 3 ┆ 42.32 │
│ 4 ┆ 4 ┆ 31.1 │
│ 5 ┆ 4 ┆ 39.75 │
└────────┴───────┴───────────┘
完全なスクリプト
段階をまとめ、最も安い5つ星の本を見つけるためにもう1つの式を追加し、フレームをParquet形式で保存します。
python
import json
import os
import urllib.request
import polars as pl
from lxml import html as lxhtml
API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
def fetch_html(url: str) -> str:
payload = json.dumps(
{"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
).encode()
request = urllib.request.Request(
API_URL,
data=payload,
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=90) as response:
return json.loads(response.read())["data"]
dom = lxhtml.fromstring(fetch_html("https://books.toscrape.com/"))
records = []
for card in dom.cssselect("article.product_pod"):
records.append(
{
"title": card.cssselect("h3 a")[0].get("title"),
"price_gbp": float(card.cssselect("p.price_color")[0].text_content().strip().lstrip("£")),
"rating": RATINGS[card.cssselect("p.star-rating")[0].get("class").split()[-1]],
"in_stock": "In stock" in card.cssselect("p.instock.availability")[0].text_content(),
}
)
df = pl.DataFrame(records)
print(f"抽出されたレコード: {df.height} | 列: {df.columns}")
summary = (
df.lazy()
.filter(pl.col("in_stock"))
.group_by("rating")
.agg(pl.len().alias("books"), pl.col("price_gbp").mean().round(2).alias("avg_price"))
.sort("rating")
.collect()
)
print(summary)
cheapest = df.filter(pl.col("rating") == 5).sort("price_gbp").select("title", "price_gbp").head(1)
print("最も安い5つ星:", cheapest.to_dicts())
df.write_parquet("books.parquet")
print(f"parquetバイト数: {os.path.getsize('books.parquet')}")
実行結果は各ステップを報告し、次のリーダーのためにスキーマを保持するParquetファイルで終わります。
text
抽出されたレコード: 20 | 列: ['title', 'price_gbp', 'rating', 'in_stock']
shape: (5, 3)
┌────────┬───────┬───────────┐
│ rating ┆ books ┆ avg_price │
│ --- ┆ --- ┆ --- │
│ i64 ┆ u32 ┆ f64 │
╞════════╪═══════╪═══════════╡
│ 1 ┆ 6 ┆ 40.02 │
│ 2 ┆ 3 ┆ 36.83 │
│ 3 ┆ 3 ┆ 42.32 │
│ 4 ┆ 4 ┆ 31.1 │
│ 5 ┆ 4 ┆ 39.75 │
└────────┴───────┴───────────┘
最も安い5つ星: [{'title': 'Set Me Free', 'price_gbp': 17.46}]
parquetバイト数: 2233
write_parquetは型付きの列をApache Parquetファイル形式で保存するため、次のプロセスはprice_gbpを再変換することなくfloatとして読み込みます。Polarsユーザーガイドは、この例を超えた場合の完全な式とレイジーAPIを文書化しています。
Polarsが行わないこと
Polarsはデータフレームエンジンであり、これが全体の境界です:HTTPクライアントもHTMLパーサーもありません。URLをリクエストすることもなく、JavaScriptをレンダリングすることもなく、<article>タグを行に変換することもありません。それは設計によるものであり、このワークフローが前に2つのツールを使用する理由です。Scrapelessがページを取得し、パーサーがマークアップをレコードに読み込みます。パースステップの深い処理が必要な場合は、ウェブスクレイピングのためのBeautifulSoupの使用に関するガイドがPythonにおけるHTML抽出を扱っており、同じレコードがそのままpl.DataFrameに入ります。
ターゲットがJavaScriptでコンテンツをレンダリングする場合、最初のフェッチは空のシェルを返し、抽出ループはカードを見つけません。リクエストでjs_renderをTrueに設定すると、Scrapelessがスクリプトが実行された後のページを返すので、マークアップがすでにサーバーによってレンダリングされている場合、すなわちこのカタログのように、Falseのままにします。
最初のページを超えてクロールを広げる前に、ターゲットのrobots.txtと利用規約を読みましょう。ロボット排除プロトコルは、サイトが自動クライアントに避けるよう求めるパスを示しており、その範囲内にとどまることでワークフローが持続可能になります。ボリュームを制限し、データを公開のままにしておきましょう。
自分のデータで試す準備はできましたか?無料のScrapelessアカウントを作成し、フェッチステージでURLとセレクターを変更してください。
結論
Polarsは、何か別のものがページに最初に到達すれば、わずかなコードでスクレイプしたレコードを型付きのクエリ可能なフレームに変換します。ScrapelessがHTMLをフェッチし、パーサーが実際の型でレコードを構築し、Polarsがそれらをグループ化、集計し、式と遅延APIを介して保存します。完全なスクリプトから始めて、自分のターゲットのためにURLとセレクターを入れ替え、データが増えるにつれて遅延パスを利用してください。
無料プランのScrapelessから始めると、自分のページを取得し、定期的なジョブを計画する際にはScrapelessの料金プランを確認してください。
FAQ
Q: Polarsは独自にウェブページをスクレイプしますか?
いいえ。PolarsはHTTPクライアントもHTMLパーサーもないデータフレームライブラリなので、URLをリクエストしたりマークアップを読み取ったりすることはできません。ScrapelessのようなフェッチレイヤーとlxmlやBeautifulSoupのようなパーサーと組み合わせて使用してください。レコードが存在するようになると、Polarsが引き継ぎます。
Q: スクレイプしたデータに対してPolarsを選ぶ理由は何ですか?
PolarsはApache Arrowのカラム形式に基づいて構築されており、遅延クエリエンジンを提供しているため、大きなフレームでのグループ集計やフィルタが高速で、式APIが一貫しています。スクレイピングに関連するトレードオフの一つは、pandasにはテーブル用のread_htmlがあるのに対し、Polarsはレコードから自分でフレームを構築することを期待しているため、カードスタイルのページにはテーブル専用のページよりも適しています。
Q: PolarsにおけるイagerとLazyの違いは何ですか?
イager DataFrameは各操作を即座に実行しますが、df.lazy()はクエリプランを構築し、.collect()メソッドを呼び出したときにのみ実行されます。遅延パスではPolarsが全体のクエリを最適化し、不必要な作業をスキップできるため、大きなデータセットでもスケールしやすく、結果は小さなデータセットでも同じです。
Q: なぜ私のスクレイプした数字がPolarsで文字列として表示されるのですか?
スクレイプされた値はテキストとして到着し、それらをpl.DataFrameに変更せずに渡すと、カラムタイプはStringになります。例では価格をfloatに、評価を整数にキャストしているように、抽出中に各値を強制的に型変換してください。これにより、スキーマは最初の行から型が指定され、後で再パースする必要がないカラムになります。
Q: スクレイプしたレコードのリストからPolars DataFrameをどのように構築しますか?
各レコードを一貫したキーを持つ辞書として収集し、そのリストをpl.DataFrame(records)に渡します。Polarsは値からスキーマを推測するため、辞書内の型付きの値が型付きのフレームを生み出し、式APIの準備が整います。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



