🎯 カスタマイズ可能で検出回避型のクラウドブラウザ。自社開発のChromiumを搭載し、ウェブクローラーAIエージェント向けに設計されています。👉今すぐ試す
ブログに戻ります

Polarsウェブスクレイピング:ライブページから高速データフレームへ

James Thompson
James Thompson

Scraping and Proxy Management Expert

22-Jul-2026

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 Copy
pip install polars lxml

シェルで一度ScrapelessのAPIキーを設定します。実際のキーを実行時に使用し、プレースホルダーをソースから外します。

bash Copy
export SCRAPELESS_API_KEY="sk_your_key_here"

フェッチ、抽出、およびフレームの構築

最初のステージはページを取得し、各製品に対して1レコードを取得してDataFrameを構築します。PolarsはHTMLを取得または解析できないため、このステージではScrapelessとパーサーが作業を行い、Polarsは完了したレコードを受け取ります。

python Copy
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 Copy
抽出されたレコード数: 20
スキーマ: {'title': String, 'price_gbp': Float64, 'rating': Int64, 'in_stock': Boolean}

最初のページにある20個のプロダクトカードは、StringFloat64Int64、およびBooleanという型のスキーマを持つフレームになります。後で再解析しなければならないテキストの列はありません。

式を使った変換

Polarsはチェーンインデクシングを式APIで置き換え、これは正しく学ぶ価値のある部分です。pl.col("price_gbp").mean()のような式は、Polarsが効率的に実行する計算を記述しており、式はfiltergroup_by、およびaggの中で組み合わされます。

python Copy
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 Copy
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 Copy
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 Copy
抽出されたレコード: 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_renderTrueに設定すると、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サイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。

最も人気のある記事

カタログ