
Emma Foster
Machine Learning Engineer

Data menu restoran untuk sistem pesan makanan AI seharusnya dibangun sebagai produk data berbasis asal, bukan sekumpulan nama hidangan dan harga. Pipeline yang paling andal dimulai dari sumber yang dikendalikan pemilik seperti Google Business Profile Food Menus, API menu POS, ekspor, dan data terstruktur pihak pertama. Selanjutnya, normalisasi lokasi, menu, bagian, item, opsi, harga, mata uang, bahasa, label diet, alergen, ketersediaan, dan waktu pengamatan ke dalam skema yang stabil. Setiap bidang harus tetap dapat dilacak ke sumbernya, terutama ketika sistem AI menjawab pertanyaan, membandingkan opsi, atau menyiapkan pesanan. Pengumpulan halaman yang dirender adalah cadangan prioritas rendah dan hanya boleh beroperasi dengan izin, batas kecepatan, dan penanganan tantangan yang ketat. Panduan ini menunjukkan cara merancang pipeline tersebut dari penerimaan hingga validasi dan tinjauan.
Sebuah restoran jarang memiliki satu menu yang tetap. Data dapat bervariasi berdasarkan:
Sistem pesan makanan AI yang menyederhanakan perbedaan ini dapat memberikan harga yang salah, mengabaikan opsi yang diperlukan, atau menyatakan informasi diet yang salah.
Blog web-scraping CapSolver memberikan panduan ekstraksi terkait, sementara FAQ web-scraping menjelaskan pertimbangan sumber dan operasional.
Mulai dengan sumber yang paling otoritatif dan terstruktur.
| Prioritas | Sumber | Kekuatan | Kontrol Utama |
|---|---|---|---|
| 1 | API Pemilik atau POS | Terstruktur dan otoritatif | Otorisasi dan cakupan kontrak |
| 2 | Ekspor atau feed pemilik | Pemrosesan batch yang stabil | Metadata versi dan keterbaruan |
| 3 | API menu profil bisnis | Data menu tingkat lokasi terstruktur | Akun dan otorisasi lokasi |
| 4 | Data JSON-LD atau terbenam pihak pertama | Publik dan dapat dibaca mesin | Validasi skema dan URL sumber |
| 5 | Halaman yang diizinkan | Berguna ketika tidak ada feed | Batas kecepatan, keadaan halaman, bukti |
| 6 | OCR atau pemrosesan gambar | Terakhir | Kepercayaan rendah dan tinjauan wajib |
Jangan anggap agregator pihak ketiga setara dengan menu sendiri restoran.
Google Business Profile's Model FoodMenus mendefinisikan menu, bagian, item, label, opsi, harga, kue, alergen, batasan diet, nutrisi, bahan, metode persiapan, ukuran porsi, dan kunci media.
Panduan update menu makanan Google juga mencatat kelayakan lokasi dan alur baca dan perbaruan yang dikendalikan pemilik.
Schema.org's tipe Menu menggambarkan menu terstruktur dengan hubungan hasMenuSection dan hasMenuItem.
Toast's pedoman API Menus menyarankan memeriksa metadata sebelum mengambil menu untuk menentukan apakah data yang disimpan sudah usang.
Model-model ini mendukung desain umum: pertahankan hierarki dan keterbaruan alih-alih menyederhanakan semuanya menjadi satu blok teks.
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
Jaga bukti sumber terkait dengan setiap catatan. Timestamp level menu tidak cukup ketika item individual berasal dari sumber berbeda.
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
ID menu harus merepresentasikan lokasi dan konteks tertentu. Jangan menggabungkan harga siang dan malam atau menggabungkan menu pengiriman dan makan di tempat tanpa aturan eksplisit.
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
Gunakan permintaan kondisional, endpoint metadata, ETags, atau waktu modifikasi sumber ketika tersedia. Hindari mengunduh menu yang tidak berubah berulang kali.
Normalisasi harga menu harus mempertahankan representasi asli.
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
Jangan ubah "market price" menjadi nol untuk tampilan di bawahnya. Nilai sentinel harus tetap berbeda dari harga numerik nyata.
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] = []
Sistem pemesanan membutuhkan batasan seleksi, bukan hanya nama opsi. "Pilih satu ukuran" dan "pilih hingga tiga topping" adalah aturan yang berbeda.
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]
Jangan menimpa teks asli dengan terjemahan yang dihasilkan. Pertahankan bahasa sumber, flag terjemahan, versi model, dan status tinjauan.
Model FoodMenus Google mendukung alergen dan batasan diet, tetapi sistem di bawahnya hanya boleh menampilkan klaim yang didukung sumber.
Panduan alergi makanan FDA AS menunjukkan mengapa informasi alergen relevan dengan keselamatan.
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
)
Jangan mengasumsikan klaim seperti "tanpa kacang", "tanpa gluten", "halal", "kasher", atau yang serupa dari bahan, kue, gambar, atau asumsi model 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
Validasi skema dan pertahankan URL sumber, waktu pengamatan, dan hash konten dengan setiap catatan yang dinormalisasi.
Pengumpulan halaman yang dirender mungkin diperlukan ketika restoran yang diizinkan tidak mempublikasikan API, feed, atau data terstruktur. Sebelum menavigasi:
Panduan web-scraping legal CapSolver memberikan konteks kepatuhan tambahan.
Halaman menu pihak pertama mungkin menampilkan tantangan yang didukung selama pengumpulan yang diizinkan. CapSolver dapat sesuai dengan bagian sempit dari pipeline ini setelah opsi API dan data terstruktur habis.
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",
}
)
Panduan produk CapSolver membantu memverifikasi keluarga tugas yang didukung. Jangan menginterpretasi tantangan sebagai data menu yang hilang.
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
Buku catatan ini memungkinkan peninjau menjawab dari mana harga berasal, kapan diamati, dan versi pipeline mana yang menghasilkannya.
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
Ini adalah kebijakan internal contoh, bukan fakta universal. Tetapkan batas sesuai perilaku sumber, ketentuan kontrak, harapan pengguna, dan risiko.
class MenuChange(BaseModel):
restaurant_id: str
location_id: str
item_id: str
field: str
before: object
setelah: objek
sumber_sebelumnya: str
sumber_setelah: str
memerlukan_ulasan: bool
HIGH_RISK_FIELDS = {"alergen", "label_diet", "ketersediaan", "harga"}
def compare_items(previous: MenuItem, current: MenuItem) -> list[MenuChange]:
perubahan = []
for field in [
"nama",
"deskripsi",
"harga_pokok",
"opsi",
"label_diet",
"alergen",
"tersedia",
]:
sebelum = getattr(previous, field)
sesudah = getattr(current, field)
if sebelum != sesudah:
perubahan.append(
MenuChange(
restaurant_id=current.restaurant_id,
location_id=current.location_id,
item_id=current.item_id,
field=field,
sebelum=sebelum,
sesudah=sesudah,
sumber_sebelumnya=previous.evidence.content_hash,
sumber_setelah=current.evidence.content_hash,
memerlukan_ulasan=field in HIGH_RISK_FIELDS,
)
)
return perubahan
Jangan beri peringatan tentang perbedaan urutan atau perubahan hanya whitespace. Bandingkan catatan yang dinormalisasi.
Lapisan AI harus menjawab dari catatan yang diverifikasi dan jangan pernah memesan tanpa langkah konfirmasi terpisah.
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
)
Tampilkan lokasi, item, opsi, kuantitas, subtotal, biaya, pajak, waktu sumber, dan ketentuan pembatalan sebelum meminta konfirmasi pengguna secara eksplisit.
| Desain Pipeline | Kualitas Sumber | Segar | Keamanan | Rekomendasi |
|---|---|---|---|---|
| Teks halaman yang dijelaskan | Rendah | Tidak diketahui | Rendah | Hindari |
| Agregat pihak ketiga saja | Variabel | Variabel | Menengah | Gunakan dengan hati-hati |
| API pemilik plus ledger yang dinormalisasi | Tinggi | Tinggi | Tinggi | Disarankan |
| Halaman pihak pertama dengan ulasan | Menengah | Terukur | Menengah hingga tinggi | Gunakan ketika diizinkan |
Arsitektur yang disarankan dimulai dari sumber terstruktur yang dikendalikan pemilik dan tetapkan bukti pada setiap bidang.
class QualityResult(BaseModel):
accepted: bool
reasons: list[str]
def validate_item(item: MenuItem) -> QualityResult:
reasons = []
if not item.name.strip():
reasons.append("nama_kosong")
if item.base_price and item.base_price.amount < 0:
reasons.append("harga_negatif")
if item.evidence.source_type == "IMAGE_REVIEW":
reasons.append("sumber_gambar_memerlukan_ulasan")
if item.allergens and item.evidence.source_type not in {
"OWNER_API",
"OWNER_EXPORT",
"BUSINESS_PROFILE",
}:
reasons.append("sumber_alergen_memerlukan_ulasan")
return QualityResult(accepted=not reasons, reasons=reasons)
Karantina catatan yang gagal alih-alih memperbaikinya secara diam-diam dengan teks yang dihasilkan.
Lacak:
Jangan log kunci API, cookie, data pelanggan pribadi, data pembayaran, atau status penyimpanan browser penuh.
FAQ AI dan otomasi CapSolver dapat mendukung desain alat dan batas persetujuan.
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="mengandung kacang pohon",
source_type="JSON_LD",
explicit_source_text="mengandung kacang pohon",
confidence=1.0,
reviewed=False,
reviewer_id=None,
)
assert allow_safety_claim(claim) is False
Juga uji presisi mata uang, pemisahan lokasi, label multibahasa, batasan opsi, harga yang usang, ID item yang duplikat, prioritas sumber, klasifikasi halaman tantangan, dan penghalang konfirmasi pesanan.
Kode Bonus: Gunakan kode WEBS di Dasbor CapSolver untuk mendapatkan bonus tambahan 5% pada setiap recharge.
FAQ Penyelesaian CAPTCHA CapSolver memberikan panduan tambahan untuk langkah tantangan yang diizinkan.
Kumpulkan data menu restoran hanya dari sumber yang dikendalikan pemilik, berlisensi, publik, atau secara eksplisit diizinkan. Hormati kesepakatan API, ketentuan, direktif robots, batas kecepatan, hak cipta, privasi, dan hak basis data. Jangan kumpulkan data pelanggan, pembayaran, loyalitas, atau pesanan pribadi. Jangan menebak alergen atau kesesuaian diet. Sistem AI harus menampilkan waktu sumber dan ketidakpastian, dan manusia harus memverifikasi pilihan yang konsekuensial.
Data menu restoran untuk sistem pemesanan AI memerlukan hierarki, asal usul, segar, dan kontrol keamanan. Pilih API pemilik dan aliran POS, pertahankan konteks lokasi dan menu, normalisasi opsi item dan harga, pertahankan variasi bahasa yang terkait, dan harus ada bukti eksplisit untuk alergen dan label diet. Gunakan halaman pihak pertama sebagai cadangan yang dikendalikan, dan anggap penanganan tantangan yang didukung sebagai satu langkah infrastruktur sempit—bukan sebagai izin untuk mengakses sumber.
Bangun pipeline yang diizinkan dengan CapSolver di mana tantangan yang didukung mengganggu pengumpulan menu yang disetujui, lalu verifikasi sumber, status menu, asal usul bidang, dan segar sebelum sistem AI menggunakan hasilnya.
Gunakan API yang diizinkan pemilik, aliran POS, atau ekspor terlebih dahulu. Menu Bisnis Profil dan data terstruktur pihak pertama sangat berguna sebagai sumber sekunder.
Masukkan restoran, lokasi, menu, bagian, item, opsi, harga, mata uang, bahasa, label diet, alergen, ketersediaan, sumber, dan waktu pengamatan.
Tidak. Hanya tampilkan klaim tersebut ketika sumber menyatakannya secara eksplisit dan kebijakan ulasan yang relevan terpenuhi.
Atur kebijakan berdasarkan bidang. Ketersediaan dan harga biasanya memerlukan batas yang lebih pendek daripada deskripsi atau media, tetapi interval pasti tergantung pada perilaku sumber dan risiko.
CapSolver dapat menangani tantangan yang didukung pada halaman pihak pertama yang diizinkan ketika tidak ada sumber terstruktur yang disetujui. Hasilnya tetap memerlukan validasi halaman dan data.
Pelajari arsitektur pengambilan data web Rust yang dapat diskalakan dengan reqwest, scraper, pengambilan data asinkron, pengambilan data browser tanpa tampilan, rotasi proxy, dan penanganan CAPTCHA yang sesuai aturan.

Mengotomasi penyelesaian CAPTCHA dengan Nanobot dan CapSolver. Gunakan Playwright untuk menyelesaikan reCAPTCHA dan Cloudflare secara otomatis.
