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

JSON-LDウェブスクレイピング: 構造化データを抽出するためのPython

Emily Chen
Emily Chen

Advanced Data Extraction Specialist

30-Jul-2026

TL;DR:

  • JSON-LDは通常、ページから型付きレコードへの最短ルートです。 見出し、著者、発行日、画像、または製品フィールドのセレクタを書く前に <script type="application/ld+json"> を探してください。
  • すべてのJSON-LDブロックを読みましょう。 ページは、組織、パンくずリスト、記事、製品、およびFAQデータを複数のスクリプトに分割することがあります。
  • 3つのトップレベルの形状を正規化します。 JSON-LDブロックは、1つのオブジェクト、オブジェクトの配列、または@graphに役立つノードを含むオブジェクトのいずれかです。
  • 実際にはSchema.orgフィールドはオプションです。 ノードを@typeで選択し、欠損フィールドはNoneとして保持し、パイプラインが本当に必要とするフィールドのみを検証します。
  • Beautiful SoupはJavaScriptを実行しません。 ページがロード後にJSON-LDを挿入する場合、最初にレンダリングされたHTMLを取得し、その応答に同じパーサーを実行します。
  • クラウドモデルなしでパーサーをテストできます。 以下の完全なPython例では、1つの実際のArticleノードを読み取り、その見出しを可視のH1と比較し、出力を検証します。
  • 公開された限られたページから始めましょう。 対象がレンダリングされたHTMLを必要とする場合は、無料のScrapelessアカウントを作成してください。

JSON-LDは、スクレイパーが散在するページ要素から再構築する予定のフィールドを含むことがよくあります。ある実際のScrapeless記事では、単一のArticleオブジェクトが見出し、著者、出版社、日付、正規URL、ヒーロー画像、説明、およびキーワードを持っています。可視のページは依然として重要ですが、構造化メタデータは抽出パイプラインに型付きの出発点を提供します。

JSON-LDは、JSON-LD 1.1仕様に従い、ArticleProductBreadcrumbListのような語彙はSchema.orgの構造化データモデルから来ています。どちらの標準も、すべての出版社がすべてのプロパティを埋めることを保証するものではありません。あなたのパーサーはその不確実性を保持し、値を作り出さないようにする必要があります。

JSON-LDウェブスクレイピングが良い理由

JSON-LDは、ページが人間向けのレイアウトとともに機械可読な事実を公開する際に便利です。一般的なノードには以下が含まれます:

  • 見出し、日付、著者、画像、出版社のためのArticleおよびNewsArticle;
  • 名前、ブランド、オファー、評価、識別子のためのProduct;
  • 階層と正規カテゴリーのパスのためのBreadcrumbList;
  • エンティティメタデータのためのOrganizationPerson、およびLocalBusiness;
  • ドメイン特有のタイプのVideoObjectRecipeEventその他。

スクリプトブロックはプライベートエンドポイントではありません。それはページの応答の一部であり、検索クローラーなどの機械のために意図されています。それにより、生成されたCSSクラスよりも耐久性が高くなりますが、自動的に完全または正確というわけではありません。オラクルではなく、検証するためのソースとして扱いましょう。

Beautiful Soupのインストール

このガイドではPython 3.10以降、requests、およびbeautifulsoup4 4.15.0を使用します:

bash Copy
python -m pip install "requests>=2.32,<3" "beautifulsoup4==4.15.0"

Beautiful Soupのツリー検索APIは、属性でタグをフィルタリングできるので、すべての一致するスクリプトを収集するには十分です。JSONデコードはPythonの標準ライブラリに残ります。

ソースHTMLを取得

普通のHTTPリクエストから始めます。ここで使用するターゲットは初期応答でJSON-LDを公開しているため、JavaScriptのレンダリングは結果を変えずにコストを増加させることになります:

python Copy
import requests

URL = "https://www.scrapeless.com/ja/blog/what-is-web-scraping?utm_source=website&utm_medium=blog&utm_campaign=universalscrapingapi&utm_term=json-ld-structured-data-web-scraping"

response = requests.get(
    URL,
    headers={"User-Agent": "Mozilla/5.0"},
    timeout=30,
)
response.raise_for_status()
print("HTML bytes:", len(response.content))
text Copy
HTML bytes: 439487

バイト数はページバンドルが変更されると変更される場合があります。役立つ不変量は、レスポンスが少なくとも1つのデコード可能なapplication/ld+jsonスクリプトを含んでいることです。

すべてのJSON-LDブロックを解析

ページ契約が明示的に1つのブロックを約束していない限り、soup.find(...)は使用しないでください。find_allは、記事、パンくずリスト、および出版社が別々のスクリプトに存在する可能性を保持します:

python Copy
import json
from bs4 import BeautifulSoup

soup = BeautifulSoup(response.text, "html.parser")
scripts = soup.find_all("script", type="application/ld+json")

parsed_blocks = []
for script in scripts:
    raw = script.get_text(strip=True)
    try:
        parsed_blocks.append(json.loads(raw))
    except json.JSONDecodeError:
        continue

print("JSON-LD blocks:", len(scripts))
print("decoded blocks:", len(parsed_blocks))

不正なブロックを飛ばすのは、安全にスキップした数も記録する場合のみです。無言の except は、メタデータの後退を成功に見える空のデータセットに変えてしまう可能性があります。

オブジェクト、配列、および @graph を正規化する

JSON-LD の著者は、ノードをグループ化するためのいくつかの妥当な方法を持っています。このジェネレーターは、スクレイパーが最も頻繁に遭遇する3つの形状をフラット化します。

python Copy
def iter_nodes(value):
    if isinstance(value, list):
        for item in value:
            yield from iter_nodes(item)
    elif isinstance(value, dict):
        graph = value.get("@graph")
        if isinstance(graph, list):
            for item in graph:
                yield from iter_nodes(item)
        else:
            yield value

nodes = [node for block in parsed_blocks for node in iter_nodes(block)]
article = next(node for node in nodes if node.get("@type") == "Article")

print("正規化されたノードの数:", len(nodes))
print("選択されたタイプ:", article["@type"])

@type は配列でもあります。あなたのコーパスに "@type": ["Article", "NewsArticle"] を出す出版社が含まれている場合、メンバーシップをテストする前にそのフィールドを正規化してください。

Nullable レコードを構築する

ネストされたオブジェクトには、トップレベルフィールドと同様の注意が必要です。著者は辞書、リスト、文字列、または不在である可能性があります。このターゲットは辞書を使用するため、例は防御的に読み取り、欠落したオプションフィールドを None として保持します:

python Copy
visible_h1 = soup.find("h1").get_text(" ", strip=True)
author = article.get("author") or {}
publisher = article.get("publisher") or {}

record = {
    "type": article.get("@type"),
    "headline": article.get("headline"),
    "visible_h1": visible_h1,
    "author": author.get("name") if isinstance(author, dict) else None,
    "publisher": publisher.get("name") if isinstance(publisher, dict) else None,
    "published": article.get("datePublished"),
    "modified": article.get("dateModified"),
    "image": article.get("image"),
    "description": article.get("description"),
    "keywords": article.get("keywords"),
    "source_url": URL,
}

すべての行に source_url を保持することで、後の監査が可能になります。出所がなければ、修正されたパーサーは再構築が必要なレコードを特定できません。

パイプラインが必要とするフィールドの検証

検証は、すべてのプロパティが Schema.org に許可されるのではなく、下流の契約を反映するべきです:

python Copy
required = ("headline", "author", "published", "source_url")
missing = [field for field in required if not record.get(field)]
if missing:
    raise ValueError(f"必要なフィールドが欠落しています: {missing}")

print("ヘッドライン:", record["headline"])
print("表示された H1:", record["visible_h1"])
print("見出しが一致する:", record["headline"] == record["visible_h1"])
print("著者:", record["author"])
print("出版社:", record["publisher"])
print("発行日:", record["published"])
text Copy
ヘッドライン: ウェブスクレイピングとは? 定義ガイド 2025
表示された H1: ウェブスクレイピングとは? 定義ガイド 2025
見出しが一致する: True
著者: エミリー・チェン
出版社: スクレイプレス
発行日: 2025-09-17T08:35:31.224Z

このページのヘッドラインは一致しています。その結果を一般化しないでください: 編集者は時々、構造化メタデータを更新せずに表示H1を更新したり、JSON-LDで短い検索ヘッドラインを使用したりします。両方の表面を比較することは、質のチェックに役立ちます。

レンダリングされたレスポンスで同じパーサーを実行する準備はできましたか? Scrapeless アカウントを開く そして、解析コードは変更しません。

完全な実行可能な抽出器

完全なスクリプトは、発見、正規化、選択、Nullable マッピング、検証を結合します:

python Copy
import json
import requests
from bs4 import BeautifulSoup

URL = "https://www.scrapeless.com/ja/blog/what-is-web-scraping?utm_source=website&utm_medium=blog&utm_campaign=universalscrapingapi&utm_term=json-ld-structured-data-web-scraping"


def iter_nodes(value):
    if isinstance(value, list):
        for item in value:
            yield from iter_nodes(item)
    elif isinstance(value, dict):
        graph = value.get("@graph")
        if isinstance(graph, list):
            for item in graph:
                yield from iter_nodes(item)
        else:
            yield value


response = requests.get(
    URL,
    headers={"User-Agent": "Mozilla/5.0"},
    timeout=30,
)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")

nodes = []
invalid_blocks = 0
scripts = soup.find_all("script", type="application/ld+json")
for script in scripts:
    try:
        nodes.extend(iter_nodes(json.loads(script.get_text(strip=True))))
    except json.JSONDecodeError:
        invalid_blocks += 1

article = next(node for node in nodes if node.get("@type") == "Article")
author = article.get("author") or {}
publisher = article.get("publisher") or {}
visible_h1 = soup.find("h1").get_text(" ", strip=True)

record = {
    "type": article.get("@type"),
    "headline": article.get("headline"),
json Copy
{
    "visible_h1": visible_h1,
    "author": author.get("name") if isinstance(author, dict) else None,
    "publisher": publisher.get("name") if isinstance(publisher, dict) else None,
    "published": article.get("datePublished"),
    "image": article.get("image"),
    "keywords": article.get("keywords"),
    "source_url": URL
}

required = ("headline", "author", "published", "source_url")
missing = [field for field in required if not record.get(field)]
if missing:
    raise ValueError(f"必要なフィールドが欠落しています: {missing}")

print(f"HTMLバイト数: {len(response.content)}")
print(f"JSON-LDブロック数: {len(scripts)}")
print(f"正規化ノード数: {len(nodes)}")
print(f"無効なブロック数: {invalid_blocks}")
print(f"タイプ: {record['type']}")
print(f"見出し: {record['headline']}")
print(f"表示H1: {record['visible_h1']}")
print(f"見出しが一致: {record['headline'] == record['visible_h1']}")
print(f"著者: {record['author']}")
print(f"出版社: {record['publisher']}")
print(f"出版日: {record['published']}")
print(f"キーワードの文字数: {len(record['keywords'] or '')}")

実行結果として、1つの有効なブロック、1つの正規化されたArticleノード、無形成ブロックはなく、一致する見出し、著者Emily Chen、出版社Scrapeless、およびキーワード文字列に140文字がありました。

JSON-LDがレンダリング後にのみ表示される場合

Beautiful Soupは受け取ったバイトを解析します; ページのJavaScriptは実行しません。簡単な診断は、未加工のレスポンスをブラウザのDOMと比較することです。ブラウザがapplication/ld+jsonスクリプトを表示している場合、しかしrequestsがそれを見つけられない場合は、解析する前にレンダリングされたHTMLを取得します。

注: 以下のリクエストには、資金提供されたScrapelessアカウントが必要です。検証アカウントは、最終確認中に残高不足の応答を返しましたので、このHTTP呼び出しは前提条件のギャップです; 上記のパーサーは実際の公開ページに対して完全に実行されました。

python Copy
import os
import requests

rendered = requests.post(
    "https://api.scrapeless.com/api/v2/unlocker/request",
    headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"]},
    json={
        "actor": "unlocker.webunlocker",
        "input": {
            "url": URL,
            "method": "GET",
            "js_render": True,
        },
    },
    timeout=90,
)
rendered.raise_for_status()
html = rendered.json()["data"]

htmlBeautifulSoupに渡し、同じ正規化器を再利用します。ユニバーサルスクレイピングAPIはレンダリングされたレスポンスを提供します; JSON-LD契約は変更されません。

一般的なJSON-LDデータ問題

ページに一致するノードがいくつかある

タイプとアイデンティティの両方で選択します。商品バリエーションの場合、最初のProductノードを取得するのではなく、@id、URL、SKU、または他の安定したフィールドを使用します。

@typeが配列である

メンバーシップを確認する前にセットに変換します。文字列に対する厳密な等価性テストは、有効な複数タイプノードを見逃します。

スクリプトにHTMLエンティティまたはコメントが含まれている

JSON-LDは有効なJSONテキストであるべきです。出版社が無効な構文でそれをラップする場合、そのブロックを無形成として記録し、その既知のソースに対してパーサーを修正します; 正当な値を破損する可能性のある広範な文字列置換は適用しないでください。

構造化メタデータが表示テキストと矛盾する

両方の値を保存し、使用ケースの優先順位を定義します。検索監査はJSON-LD見出しを好むかもしれません; コンテンツ監視は表示H1を好むかもしれません。ミスマッチはデータであり、単なるエラーではありません。

フィールドがオブジェクトからリストに変化する

境界で正規化します。著者と画像は、出版社のCMSが進化するにつれて通常、一つのオブジェクトと配列の間で切り替わります。

結論

信頼できるJSON-LDスクレイパーは4つのことを行います: 一致するスクリプトをすべて収集し、無形成ブロックを隠すことなくデコードし、辞書、リスト、@graphを正規化し、そして小さな下流契約を検証します。その道は短く、通常、ページレイアウトセレクタから同じレコードを再構築するよりも安定しています。表示DOMをクロスチェックとして保持し、ソースの由来を保持し、初期HTMLがそれを必要とすることを証明したときにのみレンダリングを導入します。

Scrapelessの無料プランから始める開発者ドキュメントを見直す、およびScrapelessの価格を確認して、レンダリングされたコーパスを本番環境に移行する前に行ってください。

FAQ

Q: JSON-LDは可視HTMLよりもスクレイピングしやすいですか?

Copy
はい、出版社が必要なフィールドを含めている場合です。JSON-LDは名前付きプロパティとタイプを提供しますが、可視HTMLはしばしばセレクタやテキストのクリーンアップを必要とします。重要なフィールドは、レンダリングされたページと比較するべきです。

**Q: なぜすべての `application/ld+json` スクリプトを解析する必要があるのですか?**

ページは異なるエンティティを別々のブロックに配置することがあります。最初のスクリプトだけを読むと、組織やパンくずリストが取得されますが、欲しい記事や製品を見逃すかもしれません。

**Q: 抽出における `@graph` の意味は何ですか?**

`@graph` は複数のJSON-LDノードを1つのオブジェクト内でグループ化します。グラフをフラットにし、次に `@type`、`@id`、URL、またはその他の安定した識別子でノードを選択します。

**Q: JSON-LDプロパティが欠けている場合はどうなりますか?**

オプションのプロパティは `None` として保持し、下流の契約で必要とされるフィールドが欠けているときだけエラーにします。Schema.orgは可能なプロパティを説明していますが、出版社がすべてを埋めることを強いるわけではありません。

**Q: JSON-LDは可視ページと異なる可能性がありますか?**

はい。メタデータと可視コンテンツは異なるスケジュールで更新されるか、異なる媒体向けに最適化されることがあります。違いが重要な場合は、両方の値を保存し、優先順位を明確にしてください。

**Q: JSON-LDを抽出するのにブラウザは必要ですか?**

初期HTMLにスクリプトがある場合は必要ありません。クライアント側のJavaScriptが生のレスポンスが到着した後に構造化データを挿入または変更する場合にのみ、レンダリングが必要です。

**Q: 公共のJSON-LDを抽出することは常に許可されていますか?**

すべての収集が合法または許可されているという一律のルールはありません。サイトの利用規約と<a href="https://datatracker.ietf.org/doc/html/rfc9309" rel="nofollow"><strong>ロボットの指示</strong></a>を確認し、リクエストの量を制限し、必要なフィールドだけを収集し、機密性または商業利用に関して法的アドバイスを取得してください。

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

最も人気のある記事

カタログ