
Anh Tuan
Data Science Expert

Dữ liệu thực đơn nhà hàng cho hệ thống đặt hàng AI nên được xây dựng như một sản phẩm dữ liệu dựa trên nguồn gốc, không phải chỉ là một tập hợp tên món ăn và giá. Đường dẫn đáng tin cậy nhất bắt đầu từ các nguồn do chủ sở hữu kiểm soát như Google Business Profile Food Menus, API thực đơn POS, xuất dữ liệu và dữ liệu cấu trúc từ bên thứ nhất. Sau đó, nó chuẩn hóa địa điểm, thực đơn, phần, mục, tùy chọn, giá, tiền tệ, ngôn ngữ, nhãn chế độ ăn, chất gây dị ứng, khả dụng và thời gian quan sát vào một lược đồ ổn định. Mỗi trường phải vẫn có thể truy xuất nguồn gốc, đặc biệt khi hệ thống AI trả lời câu hỏi, so sánh các tùy chọn hoặc chuẩn bị đơn hàng. Việc thu thập trang được hiển thị là phương án dự phòng có độ ưu tiên thấp và chỉ nên hoạt động với sự cho phép, giới hạn tốc độ và xử lý thử thách nghiêm ngặt. Hướng dẫn này cho thấy cách thiết kế đường truyền đó từ việc tiếp nhận đến kiểm tra và xem xét.
Một nhà hàng hiếm khi có một thực đơn duy nhất và vĩnh cửu. Dữ liệu có thể khác nhau theo:
Một hệ thống đặt hàng AI làm phẳng những khác biệt này có thể đưa ra giá sai, bỏ lỡ tùy chọn cần thiết hoặc cung cấp thông tin chế độ ăn sai.
Blog thu thập dữ liệu web của CapSolver cung cấp hướng dẫn liên quan về trích xuất, trong khi FAQ thu thập dữ liệu web giải thích các yếu tố nguồn và vận hành.
Bắt đầu từ nguồn đáng tin cậy và cấu trúc nhất.
| Ưu tiên | Nguồn | Sức mạnh | Điều khiển chính |
|---|---|---|---|
| 1 | API chủ sở hữu hoặc POS | Cấu trúc và đáng tin cậy | Xác thực và phạm vi hợp đồng |
| 2 | Xuất dữ liệu hoặc nguồn của chủ sở hữu | Tiếp nhận hàng loạt ổn định | Phiên bản và thông tin thời gian mới nhất |
| 3 | API thực đơn hồ sơ doanh nghiệp | Dữ liệu thực đơn cấp địa điểm cấu trúc | Tài khoản và cấp phép địa điểm |
| 4 | Dữ liệu JSON-LD hoặc nhúng từ bên thứ nhất | Công khai và có thể đọc bởi máy | Xác minh lược đồ và URL nguồn |
| 5 | Trang được cấp phép | Hữu ích khi không có nguồn tồn tại | Giới hạn tốc độ, trạng thái trang, bằng chứng |
| 6 | OCR hoặc phân tích hình ảnh | Phương án cuối cùng | Độ tin cậy thấp và kiểm tra bắt buộc |
Không xem các nhà cung cấp bên thứ ba là tương đương với thực đơn của nhà hàng.
Mô hình FoodMenus của Google Business Profile định nghĩa thực đơn, phần, mục, nhãn, tùy chọn, giá, ẩm thực, chất gây dị ứng, giới hạn chế độ ăn, dinh dưỡng, thành phần, phương pháp chế biến, kích thước phần và khóa phương tiện.
Hướng dẫn cập nhật thực đơn của Google cũng tài liệu về điều kiện địa điểm và luồng đọc và cập nhật do chủ sở hữu kiểm soát.
Loại Menu của Schema.org mô tả một thực đơn có cấu trúc với mối quan hệ hasMenuSection và hasMenuItem.
Hướng dẫn API thực đơn của Toast khuyên kiểm tra dữ liệu mô tả trước khi truy xuất thực đơn để xác định xem dữ liệu được lưu trữ có lỗi thời hay không.
Các mô hình này hỗ trợ thiết kế chung: giữ nguyên thứ bậc và thời gian mới nhất thay vì làm phẳng tất cả thành một khối văn bản.
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
Giữ bằng chứng nguồn gắn với mỗi bản ghi. Một mốc thời gian cấp thực đơn không đủ khi các mục riêng lẻ đến từ các nguồn khác nhau.
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
Một ID thực đơn nên đại diện cho một địa điểm và bối cảnh cụ thể. Không kết hợp giá trưa và tối hoặc gộp thực đơn giao hàng và ăn tại chỗ mà không có quy tắc rõ ràng.
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
Sử dụng các yêu cầu có điều kiện, điểm cuối dữ liệu mô tả, ETags hoặc thời gian sửa đổi nguồn khi có sẵn. Tránh tải xuống thực đơn không thay đổi nhiều lần.
Chuẩn hóa giá thực đơn nên giữ nguyên biểu diễn ban đầu.
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
Không bao giờ chuyển "giá thị trường" thành 0 cho hiển thị phía sau. Giá trị này nên được giữ riêng biệt với giá số thực tế.
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] = []
Hệ thống đặt hàng cần các quy tắc chọn lựa, không chỉ tên tùy chọn. "Chọn một kích thước" và "chọn tối đa ba topping" là các quy tắc khác nhau.
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]
Không ghi đè văn bản gốc bằng bản dịch được tạo. Giữ nguyên ngôn ngữ nguồn, cờ dịch, phiên bản mô hình và trạng thái kiểm tra.
Mô hình FoodMenus của Google hỗ trợ chất gây dị ứng và giới hạn chế độ ăn, nhưng các hệ thống phía sau chỉ nên trình bày các tuyên bố được hỗ trợ bởi nguồn.
Hướng dẫn về dị ứng thực phẩm của Cục Quản lý Thực phẩm và Dược phẩm Hoa Kỳ minh họa tại sao thông tin về chất gây dị ứng là quan trọng đối với an toàn.
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
)
Không suy luận các tuyên bố như "không chứa hạt", "không chứa gluten", "hàlal", "koshê" hoặc tương tự từ thành phần, ẩm thực, hình ảnh hoặc giả định của mô hình 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
Xác minh lược đồ và giữ URL nguồn, thời gian quan sát và băm nội dung với mỗi bản ghi được chuẩn hóa.
Việc thu thập trang được hiển thị có thể cần thiết khi một nhà hàng được cấp phép không công bố API, nguồn hoặc dữ liệu cấu trúc. Trước khi điều hướng:
Hướng dẫn pháp lý thu thập dữ liệu web của CapSolver cung cấp bối cảnh tuân thủ bổ sung.
Một trang thực đơn từ bên thứ nhất có thể đưa ra thử thách được hỗ trợ trong một lần thu thập được cấp phép. CapSolver có thể phù hợp với phần hẹp này của đường truyền sau khi hết các tùy chọn API và dữ liệu cấu trúc.
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",
}
)
Trang sản phẩm CapSolver giúp xác nhận các nhiệm vụ được hỗ trợ. Không bao giờ hiểu thử thách là dữ liệu thực đơn bị thiếu.
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
Sổ cái cho phép người kiểm tra trả lời nơi giá đến từ đâu, khi nào được quan sát và phiên bản đường truyền nào đã tạo ra nó.
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
Đây là các chính sách nội bộ ví dụ, không phải sự thật phổ biến. Đặt giới hạn theo hành vi nguồn, điều khoản hợp đồng, kỳ vọng người dùng và rủi ro.
class MenuChange(BaseModel):
restaurant_id: str
location_id: str
item_id: str
field: str
before: object
sau: 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
Không cảnh báo về sự khác biệt trong thứ tự hoặc thay đổi chỉ có khoảng trắng. So sánh các bản ghi đã chuẩn hóa.
Lớp AI nên trả lời từ các bản ghi được xác minh và không bao giờ đặt hàng mà không có bước xác nhận riêng biệt.
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
)
Hiển thị vị trí, mục, tùy chọn, số lượng, tổng phụ, phí, thuế, thời gian nguồn và điều khoản hủy bỏ trước khi yêu cầu xác nhận từ người dùng rõ ràng.
| Thiết kế luồng | Chất lượng nguồn | Tính mới | An toàn | Đề xuất |
|---|---|---|---|---|
| Văn bản trang phẳng | Thấp | Không xác định | Thấp | Tránh |
| Tổng hợp từ bên thứ ba | Biến đổi | Biến đổi | Trung bình | Sử dụng cẩn trọng |
| API chủ sở hữu cộng với sổ cái đã chuẩn hóa | Cao | Cao | Cao | Ưa tiên |
| Trang đầu tiên với fallback và xem xét | Trung bình | Đo lường được | Trung bình đến cao | Sử dụng khi được ủy quyền |
Kiến trúc được ưa chuộng bắt đầu từ các nguồn có cấu trúc do chủ sở hữu kiểm soát và giữ bằng chứng gắn với mỗi trường.
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)
Đưa các bản ghi thất bại vào khu cách ly thay vì sửa chữa thầm lặng bằng văn bản được tạo.
Theo dõi:
Không ghi nhật ký khóa API, cookie, dữ liệu khách hàng riêng tư, dữ liệu thanh toán hoặc trạng thái lưu trữ trình duyệt đầy đủ.
Hướng dẫn FAQ về AI và tự động hóa của CapSolver có thể hỗ trợ thiết kế công cụ và ranh giới phê duyệt.
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
Cũng kiểm tra độ chính xác của tiền tệ, phân tách vị trí, nhãn đa ngôn ngữ, ràng buộc tùy chọn, giá lỗi thời, ID mục trùng lặp, ưu tiên nguồn, phân loại trang thách thức và rào cản xác nhận đơn hàng.
Mã thưởng: Sử dụng mã WEBS tại Bảng điều khiển CapSolver để nhận thêm 5% thưởng cho mỗi lần nạp tiền.
Hướng dẫn FAQ về giải CAPTCHA của CapSolver cung cấp thêm hướng dẫn cho các bước thách thức được ủy quyền.
Thu thập dữ liệu thực đơn nhà hàng chỉ từ các nguồn do chủ sở hữu kiểm soát, được cấp phép, công khai hoặc được ủy quyền rõ ràng. Tôn trọng các thỏa thuận API, điều khoản, chỉ thị robots, giới hạn tốc độ, bản quyền, quyền riêng tư và quyền sở hữu cơ sở dữ liệu. Không thu thập dữ liệu khách hàng, thanh toán, trung thành hoặc đơn hàng riêng tư. Không suy luận về chất gây dị ứng hoặc tính phù hợp của chế độ ăn. Một hệ thống AI nên trình bày thời gian nguồn và mức độ không chắc chắn, và người dùng nên xác nhận các quyết định quan trọng.
Dữ liệu thực đơn nhà hàng cho các hệ thống đặt hàng bằng AI cần có cấu trúc phân cấp, nguồn gốc, tính mới và các biện pháp an toàn. Ưu tiên API chủ sở hữu và nguồn dữ liệu POS, giữ nguyên bối cảnh vị trí và thực đơn, chuẩn hóa tùy chọn và giá mục, giữ liên kết các biến thể ngôn ngữ và yêu cầu bằng chứng rõ ràng cho các chất gây dị ứng và nhãn chế độ ăn. Sử dụng trang đầu tiên được ủy quyền chỉ như một fallback có kiểm soát, và xem xử lý thách thức được hỗ trợ là một bước hạ tầng hẹp - không phải là sự cho phép truy cập vào nguồn.
Xây dựng luồng được ủy quyền với CapSolver nơi các thách thức được hỗ trợ ngắt quá trình thu thập thực đơn được phê duyệt, sau đó xác minh nguồn, trạng thái thực đơn, nguồn gốc trường và tính mới trước khi hệ thống AI sử dụng kết quả.
Sử dụng API được chủ sở hữu ủy quyền, nguồn dữ liệu POS hoặc xuất trước tiên. Các thực đơn từ Hồ sơ Doanh nghiệp và dữ liệu có cấu trúc từ trang đầu tiên hữu ích như nguồn phụ.
Bao gồm nhà hàng, vị trí, thực đơn, phần, mục, tùy chọn, giá, tiền tệ, ngôn ngữ, nhãn chế độ ăn, chất gây dị ứng, khả năng có sẵn, nguồn và thời gian quan sát.
Không. Chỉ trình bày các tuyên bố đó khi nguồn nêu rõ và chính sách xem xét liên quan được đáp ứng.
Thiết lập chính sách cụ thể cho từng trường. Giá và khả năng có sẵn thường cần giới hạn ngắn hơn so với mô tả hoặc phương tiện, nhưng khoảng thời gian chính xác phụ thuộc vào hành vi nguồn và rủi ro.
CapSolver có thể xử lý một thách thức được hỗ trợ trên trang đầu tiên được ủy quyền khi không có nguồn cấu trúc được phê duyệt. Kết quả vẫn cần xác minh trang và dữ liệu.
Học kiến trúc gỡ mã web Rust có thể mở rộng với reqwest, scraper, gỡ mã bất đồng bộ, gỡ mã trình duyệt không đầu, xoay proxy và xử lý CAPTCHA tuân thủ.

Tự động hóa việc giải CAPTCHA với Nanobot và CapSolver. Sử dụng Playwright để giải reCAPTCHA và Cloudflare tự động.
