
Sora Fujimoto
AI Solutions Architect

AI注文システム用のレストランメニューデータは、料理名や価格のコレクションではなく、出典を重視したデータ製品として構築されるべきである。最も信頼性の高いパイプラインは、Google Business Profile Food Menusなどのオーナー制御ソース、POSメニューアプリケーションプログラミングインターフェース(API)、エクスポート、および第一-party構造化データから始まる。その後、場所、メニューセクション、項目、オプション、価格、通貨、言語、食事制限ラベル、アレルゲン、利用可能性、観測時間などを安定したスキーマに正規化する。各フィールドは、AIシステムが質問に答えたり、オプションを比較したり、注文を準備したりする際に、ソースに追跡可能である必要がある。レンダードページの収集は、低優先度のフォールバックであり、許可、レートリミット、および厳格なチャレンジ処理の下でのみ動作するべきである。このガイドでは、インジェストから検証およびレビューに至るまで、そのパイプラインを設計する方法を示す。
レストランは一度限りのメニューよりも頻繁に変更される。データは以下の点で変化する可能性がある:
これらの違いを平坦化するAI注文システムは、誤った価格を提示したり、必要なオプションを省略したり、食事情報に誤りを生じさせる可能性がある。
CapSolverのウェブスクレイピングブログは関連する抽出ガイドを提供し、ウェブスクレイピングFAQはソースと運用上の考慮事項を説明している。
最も権限があり構造化されたソースから始める。
| 優先度 | ソース | 強み | 主な制御 |
|---|---|---|---|
| 1 | オーナーまたはPOS API | 構造化され、権限がある | 認証と契約範囲 |
| 2 | オーナーのエクスポートまたはフィード | 安定したバッチインジェスト | バージョンと新鮮さのメタデータ |
| 3 | ビジネスプロフィールメニューアプリケーションプログラミングインターフェース(API) | 構造化された場所レベルのメニューデータ | アカウントと場所の認証 |
| 4 | 第三者JSON-LDまたは埋め込みデータ | 公開され、機械読み取り可能な | スキーマ検証とソースURL |
| 5 | 承認されたレンダープページ | フィードが存在しない場合に役立つ | レートリミット、ページ状態、証拠 |
| 6 | OCRまたは画像解析 | 最後の手段 | 低い信頼性と必須のレビュー |
第三者の集約者をレストランの独自メニューよりも同等と扱ってはならない。
Google Business ProfileのFoodMenusモデルは、メニューセクション、項目、ラベル、オプション、価格、料理、アレルゲン、食事制限、栄養、材料、調理方法、サービングサイズ、メディアキーを定義する。
Googleのfood-menu update guideも、場所の資格とオーナー制御の読み取り・更新フローを文書化している。
Schema.orgのMenu typeは、hasMenuSectionとhasMenuItemの関係を持つ構造化されたメニューを説明する。
ToastのMenus APIガイドは、メニューデータを取得する前にメタデータを確認することを推奨し、キャッシュされたデータが古いかどうかを判断する。
これらのモデルは、すべてを1つのテキストブロックに平坦化する代わりに、階層と新鮮さを保持する共通の設計をサポートする。
from datetime import datetime
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, Field, HttpUrl
class Money(BaseModel):
currency: str = Field(min_length=3, max_length=3)
amount: Decimal
tax_included: bool | None = None
class Evidence(BaseModel):
source_type: Literal[
"OWNER_API",
"OWNER_EXPORT",
"BUSINESS_PROFILE",
"JSON_LD",
"AUTHORIZED_PAGE",
"IMAGE_REVIEW",
]
source_url: HttpUrl | None = None
source_record_id: str | None = None
observed_at: datetime
source_modified_at: datetime | None = None
content_hash: str
language: str
class MenuOption(BaseModel):
option_id: str
name: str
price_delta: Money | None = None
available: bool | None = None
class MenuItem(BaseModel):
item_id: str
restaurant_id: str
location_id: str
menu_id: str
section_id: str
name: str
description: str | None = None
base_price: Money | None = None
options: list[MenuOption] = []
dietary_labels: list[str] = []
allergens: list[str] = []
ingredients: list[str] = []
available: bool | None = None
evidence: Evidence
各レコードにソース証拠を付随させる。個々の項目が異なるソースから来る場合、メニューレベルのタイムスタンプだけでは不十分である。
class Restaurant(BaseModel):
restaurant_id: str
brand_name: str
legal_name: str | None = None
class Location(BaseModel):
location_id: str
restaurant_id: str
address_line: str
city: str
region: str | None = None
postal_code: str | None = None
country_code: str
timezone: str
class Menu(BaseModel):
menu_id: str
location_id: str
name: str
service_modes: list[str]
dayparts: list[str]
valid_from: datetime | None = None
valid_until: datetime | None = None
language: str
メニューアイデンティティは、特定の場所と文脈を表すべきである。明示的なルールなしにランチとディナーの価格を組み合わせたり、配達と店内食事のメニューやをマージしてはならない。
from hashlib import sha256
import json
def canonical_hash(payload: dict) -> str:
encoded = json.dumps(
payload,
sort_keys=True,
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
return sha256(encoded).hexdigest()
async def ingest_owner_menu(api, location_id: str) -> list[MenuItem]:
metadata = await api.get_menu_metadata(location_id)
if metadata.not_modified:
return []
payload = await api.get_menu(location_id)
observed_at = utc_now()
records = []
for menu in payload["menus"]:
for section in menu.get("sections", []):
for item in section.get("items", []):
records.append(
normalize_owner_item(
location_id=location_id,
menu=menu,
section=section,
item=item,
observed_at=observed_at,
content_hash=canonical_hash(item),
)
)
return records
利用可能な場合、条件付きリクエスト、メタデータエンドポイント、ETag、またはソースの変更時間を使用する。変更されていないメニューよりも繰り返しダウンロードを避ける。
メニューアイテムの価格正規化は、元の表現を保持する必要がある。
class NormalizedPrice(BaseModel):
amount: Decimal
currency: str
original_text: str | None = None
price_type: Literal[
"FIXED",
"FROM",
"RANGE",
"MARKET_PRICE",
"INCLUDED",
"UNKNOWN",
]
upper_amount: Decimal | None = None
def normalize_price(raw: dict, currency: str) -> NormalizedPrice | None:
if raw.get("market_price"):
return NormalizedPrice(
amount=Decimal("0"),
currency=currency,
original_text=raw.get("display"),
price_type="MARKET_PRICE",
)
if raw.get("min") is not None and raw.get("max") is not None:
return NormalizedPrice(
amount=Decimal(str(raw["min"])),
upper_amount=Decimal(str(raw["max"])),
currency=currency,
original_text=raw.get("display"),
price_type="RANGE",
)
if raw.get("amount") is not None:
return NormalizedPrice(
amount=Decimal(str(raw["amount"])),
currency=currency,
original_text=raw.get("display"),
price_type="FIXED",
)
return None
下流の表示で「マーケットプライス」をゼロに変換してはならない。シグネットは実際の数値価格から区別されるべきである。
class ModifierChoice(BaseModel):
choice_id: str
name: str
price_delta: Money | None = None
available: bool | None = None
class ModifierGroup(BaseModel):
group_id: str
name: str
minimum_selections: int
maximum_selections: int
choices: list[ModifierChoice]
class OrderableItem(MenuItem):
modifier_groups: list[ModifierGroup] = []
注文システムはオプション名だけでなく、選択制約が必要である。「1つのサイズを選択してください」と「最大3つのトッピングを選択してください」は異なるルールである。
class LocalizedText(BaseModel):
language: str
value: str
source_value: str
translated: bool = False
translation_model: str | None = None
class LocalizedMenuItem(BaseModel):
item_id: str
names: list[LocalizedText]
descriptions: list[LocalizedText]
元のテキストを生成された翻訳で上書きしてはならない。ソース言語、翻訳フラグ、モデルバージョン、レビュー状態を保持する。
GoogleのFoodMenusモデルはアレルゲンと食事制限をサポートしているが、下流システムはソースでサポートされている主張のみを提示すべきである。
米国食品医薬品局のアレルギーに関するガイドラインは、アレルゲン情報が安全に関連している理由を示している。
class SafetyClaim(BaseModel):
claim: str
source_type: str
explicit_source_text: str
confidence: float
reviewed: bool
reviewer_id: str | None = None
def allow_safety_claim(claim: SafetyClaim) -> bool:
return (
claim.source_type in {"OWNER_API", "OWNER_EXPORT", "BUSINESS_PROFILE"}
and bool(claim.explicit_source_text.strip())
and claim.reviewed
)
材料、料理、画像、またはAIモデルの仮定から「ナッツフリー」、「グルテンフリー」、「ハラール」、「コーシャー」などの主張を推測してはならない。
import json
from bs4 import BeautifulSoup
def extract_json_ld_menu(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
menus = []
for script in soup.select('script[type="application/ld+json"]'):
try:
payload = json.loads(script.string or "")
except json.JSONDecodeError:
continue
nodes = payload if isinstance(payload, list) else [payload]
for node in nodes:
if not isinstance(node, dict):
continue
if node.get("@type") == "Menu":
menus.append(node)
graph = node.get("@graph", [])
menus.extend(
entry
for entry in graph
if isinstance(entry, dict) and entry.get("@type") == "Menu"
)
return menus
スキーマを検証し、各正規化レコードにソースURL、観測時間、コンテンツハッシュを保持する。
承認されたレストランがAPI、フィード、または構造化データを公開していない場合、レンダープページの収集が必要になる可能性がある。ナビゲートする前に:
CapSolverの法的ウェブスクレイピングガイドは、追加のコンプライアンスの文脈を提供している。
第一-partyメニューページは、承認された収集実行中にサポートされているチャレンジを提示する可能性がある。CapSolverは、APIと構造化データオプションが枯渇した後にパイプラインの狭い部分に適合する。
class CollectionDecision(BaseModel):
source_type: str
authorized: bool
public_fields_only: bool
challenge_type: str | None = None
rate_limit_ok: bool
def may_use_challenge_service(decision: CollectionDecision) -> bool:
return (
decision.source_type == "AUTHORIZED_PAGE"
and decision.authorized
and decision.public_fields_only
and decision.rate_limit_ok
and decision.challenge_type in {
"RECAPTCHA_V2",
"RECAPTCHA_V3",
"CLOUDFLARE_TURNSTILE",
"CLOUDFLARE_CHALLENGE",
}
)
CapSolver製品ページは、サポートされているタスクファミリーを確認するのに役立つ。チャレンジを欠如したメニューデータとして解釈してはならない。
class SourceLedgerEntry(BaseModel):
run_id: str
restaurant_id: str
location_id: str
source_type: str
source_url: str | None
permission_basis: str
observed_at: datetime
source_modified_at: datetime | None
record_count: int
content_hash: str
challenge_observed: bool
human_review_required: bool
レジスターにより、レビュアーは価格がどこから来たのか、いつ観測されたのか、どのパイプラインバージョンが生成したのかを回答できる。
from datetime import timedelta
FRESHNESS = {
"availability": timedelta(minutes=15),
"price": timedelta(hours=6),
"description": timedelta(days=7),
"dietary_labels": timedelta(days=7),
"allergens": timedelta(days=1),
"media": timedelta(days=30),
}
def is_fresh(field: str, observed_at: datetime, now: datetime) -> bool:
maximum_age = FRESHNESS[field]
return now - observed_at <= maximum_age
これらは例示的な内部ポリシーであり、普遍的な事実ではない。ソースの行動、契約条件、ユーザーの期待、リスクに応じて制限を設定する。
class MenuChange(BaseModel):
restaurant_id: str
location_id: str
item_id: str
field: str
before: object
after: object
source_before: str
source_after: str
requires_review: bool
HIGH_RISK_FIELDS = {"allergens", "dietary_labels", "availability", "price"}
def compare_items(previous: MenuItem, current: MenuItem) -> list[MenuChange]:
changes = []
for field in [
"name",
"description",
"base_price",
"options",
"dietary_labels",
"allergens",
"available",
]:
before = getattr(previous, field)
after = getattr(current, field)
if before != after:
changes.append(
MenuChange(
restaurant_id=current.restaurant_id,
location_id=current.location_id,
item_id=current.item_id,
field=field,
before=before,
after=after,
source_before=previous.evidence.content_hash,
source_after=current.evidence.content_hash,
requires_review=field in HIGH_RISK_FIELDS,
)
)
return changes
注文の順序の違いや空白のみの変更についてはアラートを出さないでください。正規化されたレコードを比較してください。
AIレイヤーは検証済みのレコードから回答する必要があります。別途確認ステップがない限り、注文を発注してはなりません。
class OrderProposal(BaseModel):
location_id: str
item_id: str
option_ids: list[str]
quoted_total: Money
menu_observed_at: datetime
source_record_id: str
safety_claims_reviewed: bool
def may_present_for_confirmation(proposal: OrderProposal, now: datetime) -> bool:
return (
is_fresh("price", proposal.menu_observed_at, now)
and proposal.safety_claims_reviewed
and proposal.quoted_total.amount >= 0
)
明示的なユーザー確認を求める前に、場所、商品、オプション、数量、小計、手数料、税金、ソースの時刻、キャンセル条件を表示してください。
| パイプライン設計 | ソースの質 | 新鮮さ | セーフティ | 推奨 |
|---|---|---|---|---|
| ページテキストを平坦化 | 低 | 知らない | 低 | 避けた方がよい |
| 第三者集約のみ | 変動 | 変動 | 中 | 注意して使用 |
| オーナーAPIと正規化された台帳 | 高 | 高 | 高 | 最も推奨 |
| オーナーAPIのフォールバックとレビュー | 中 | 測定可能 | 中〜高 | 承認された場合に使用 |
最も推奨されるアーキテクチャは、オーナー制御の構造化されたソースから始まり、各フィールドに証拠を添付します。
class QualityResult(BaseModel):
accepted: bool
reasons: list[str]
def validate_item(item: MenuItem) -> QualityResult:
reasons = []
if not item.name.strip():
reasons.append("missing_name")
if item.base_price and item.base_price.amount < 0:
reasons.append("negative_price")
if item.evidence.source_type == "IMAGE_REVIEW":
reasons.append("image_source_requires_review")
if item.allergens and item.evidence.source_type not in {
"OWNER_API",
"OWNER_EXPORT",
"BUSINESS_PROFILE",
}:
reasons.append("allergen_source_requires_review")
return QualityResult(accepted=not reasons, reasons=reasons)
生成されたテキストで静かに修復する代わりに、失敗したレコードを隔離してください。
以下の内容を追跡してください:
APIキー、クッキー、プライベートな顧客データ、支払いデータ、またはブラウザの完全なストレージ状態をログに記録しないでください。
CapSolver AIと自動化のFAQは、ツールと承認境界の設計をサポートします。
import pytest
def test_market_price_is_not_zero_price():
value = normalize_price(
{"market_price": True, "display": "Market price"},
"USD",
)
assert value.price_type == "MARKET_PRICE"
assert value.original_text == "Market price"
def test_unreviewed_allergen_claim_is_blocked():
claim = SafetyClaim(
claim="contains peanuts",
source_type="JSON_LD",
explicit_source_text="contains peanuts",
confidence=1.0,
reviewed=False,
reviewer_id=None,
)
assert allow_safety_claim(claim) is False
通貨の精度、ロケーションの分離、多言語ラベル、オプションの制約、古くなった価格、重複する商品ID、ソースの優先順位、チャレンジページの分類、注文確認ゲートもテストしてください。
ボーナスコード: CapSolverダッシュボードでコード WEBS を使用すると、すべての充電で追加の5%のボーナスを取得できます。
CapSolver CAPTCHA解決FAQは、承認されたチャレンジステップに関する追加のガイドラインを提供します。
オーナー制御、ライセンス、公開、または明示的に承認されたソースからのみレストランメニューデータを収集してください。APIの契約、利用規約、ロボットディレクティブ、レートリミット、著作権、プライバシー、データベースの権利を尊重してください。顧客、支払い、ロイヤルティ、またはプライベート注文データを収集しないでください。アレルゲンや食事の適合性を推測しないでください。AIシステムはソースの時刻と不確実性を提示し、重要な選択肢は人間が確認する必要があります。
AI注文システムのレストランメニューデータには階層、出典、新鮮さ、セーフティの制御が必要です。オーナーAPIやPOSフィードを優先し、ロケーションとメニューコンテキストを保持し、オプションと価格を正規化し、言語バリアントを関連付け、アレルゲンや食事ラベルに対して明示的な証拠を必要とします。サポートされているチャレンジ処理は、1つの狭いインフラストラクチャステップとして扱うべきであり、ソースへのアクセスを許可するものではありません。
CapSolverで承認されたパイプラインを構築してください。サポートされているチャレンジが承認されたメニューコレクションを中断する場合、AIシステムが結果を使用する前に、ソース、メニューアイテムの状態、フィールドの出典、および新鮮さを検証してください。
まずオーナー承認のAPI、POSフィード、またはエクスポートを使用してください。ビジネスプロファイルのメニューやオーナー制御の構造化データは、2次的な有用なソースです。
レストラン、ロケーション、メニューアイテム、セクション、オプション、価格、通貨、言語、食事ラベル、アレルゲン、利用可能性、ソース、観測時間などを含めてください。
できません。ソースが明示的に述べており、関連するレビュー政策が満たされている場合にのみその声明を提示してください。
フィールドごとのポリシーを設定してください。価格や利用可能性は説明やメディアよりも短い間隔が必要ですが、正確な間隔はソースの動作とリスクに依存します。
承認された1次的ページでサポートされているチャレンジを処理する場合、承認された構造化ソースが利用できないときにCapSolverを使用できます。結果は依然としてページとデータの検証が必要です。
スケーラブルなRustウェブスクレイピングアーキテクチャを学びましょう。リクエスト、スクレイパー、非同期スクレイピング、ヘッドレスブラウザスクレイピング、プロキシローテーション、およびコンプライアンス対応のCAPTCHA処理で。

2026年のデータ・アズ・ア・サービス(DaaS)を理解する。その利点、ユースケース、およびリアルタイムの洞察と拡張性を通じて企業を変革する方法について探る。
