
Adélia Cruz
Neural Network Developer

Os dados de cardápio de restaurantes para sistemas de pedidos de IA devem ser construídos como um produto de dados com foco em proveniência, não como uma coleção de nomes de pratos e preços. O pipeline mais confiável começa com fontes controladas pelo proprietário, como Google Business Profile Food Menus, APIs de menu de POS, exportações e dados estruturados de primeira parte. Em seguida, normaliza localização, menu, seção, item, opção, preço, moeda, idioma, rótulos dietéticos, alérgenos, disponibilidade e horário de observação em um esquema estável. Cada campo deve permanecer rastreável até sua fonte, especialmente quando um sistema de IA responde a perguntas, compara opções ou prepara um pedido. A coleta de páginas renderizadas é uma alternativa de menor prioridade e deve operar apenas com permissão, limites de taxa e tratamento rigoroso de desafios. Este guia mostra como projetar esse pipeline desde a ingestão até a validação e revisão.
Um restaurante raramente tem um único cardápio imutável. Os dados podem variar por:
Um sistema de pedidos de IA que aplanar essas diferenças pode citar o preço errado, omitir uma opção necessária ou informar incorretamente informações dietéticas.
O blog de web scraping da CapSolver fornece orientações relacionadas à extração, enquanto a FAQ de web scraping explica considerações sobre fontes e operações.
Comece com a fonte mais autoritária e estruturada.
| Prioridade | Fonte | Força | Controle principal |
|---|---|---|---|
| 1 | API do proprietário ou POS | Estruturada e autoritária | Autenticação e escopo do contrato |
| 2 | Exportação ou feed do proprietário | Ingestão estável em lote | Metadados de versão e frescor |
| 3 | API de menu do perfil de negócio | Dados de menu nivelados por local | Conta e autorização de local |
| 4 | Dados JSON-LD ou embutidos de primeira parte | Público e legível por máquina | Validação de esquema e URL da fonte |
| 5 | Página renderizada autorizada | Útil quando nenhum feed existe | Limites de taxa, estado da página, evidência |
| 6 | OCR ou análise de imagem | Último recurso | Baixa confiança e revisão obrigatória |
Não trate agregadores de terceiros como equivalentes ao próprio cardápio do restaurante.
O modelo FoodMenus do Google Business Profile define menus, seções, itens, rótulos, opções, preço, culinária, alérgenos, restrições dietéticas, nutrição, ingredientes, métodos de preparo, tamanho das porções e chaves de mídia.
O guia de atualização de cardápio do Google também documenta a elegibilidade da localização e o fluxo de leitura e atualização controlado pelo proprietário.
O tipo Menu do Schema.org descreve um menu estruturado com relações hasMenuSection e hasMenuItem.
O guia da API de menus do Toast recomenda verificar metadados antes de recuperar menus para determinar se os dados em cache estão obsoletos.
Esses modelos apoiam um design comum: preservar hierarquia e frescor em vez de aplanar tudo em um único bloco de texto.
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
Mantenha a evidência da fonte anexada a cada registro. Um horário de nível de menu não é suficiente quando itens individuais vêm de fontes diferentes.
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
Um ID de menu deve representar um local e contexto específicos. Não combine preços de almoço e jantar ou funda menus de entrega e jantar no local sem regras explícitas.
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
Use solicitações condicionais, pontos de extremidade de metadados, ETags ou horários de modificação da fonte quando disponíveis. Evite baixar um menu inalterado repetidamente.
A normalização de preços de menu deve preservar a representação original.
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
Nunca converta "preço de mercado" em zero para exibição posterior. O valor de sentinela deve permanecer distinto de um preço numérico real.
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] = []
Um sistema de pedidos precisa de restrições de seleção, não apenas nomes de opções. "Escolha um tamanho" e "escolha até três coberturas" são regras diferentes.
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]
Não sobrescreva o texto original com uma tradução gerada. Preserve o idioma original, o sinalizador de tradução, a versão do modelo e o status de revisão.
O modelo FoodMenus do Google suporta alérgenos e restrições dietéticas, mas os sistemas de baixo nível devem apresentar apenas reivindicações apoiadas pela fonte.
O guia de alergias alimentares da FDA dos EUA ilustra por que a informação sobre alérgenos é relevante para a segurança.
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
)
Não infira reivindicações como "livre de nozes", "livre de glúten", "halal", "kasher" ou similares a partir de ingredientes, culinária, imagens ou suposições de um modelo de IA.
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
Valide o esquema e mantenha a URL da fonte, horário observado e hash de conteúdo com cada registro normalizado.
A coleta de páginas renderizadas pode ser necessária quando um restaurante autorizado publica nenhuma API, feed ou dados estruturados. Antes de navegar:
O guia legal de web scraping da CapSolver fornece contexto adicional de conformidade.
Uma página de menu de primeira parte pode apresentar um desafio suportado durante uma coleta autorizada. A CapSolver pode se encaixar nessa parte estreita do pipeline após esgotar opções de API e dados estruturados.
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",
}
)
A página de produtos da CapSolver ajuda a confirmar famílias de tarefas suportadas. Nunca interprete um desafio como dados de menu ausentes.
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
O registro permite que um revisor responda de onde um preço veio, quando foi observado e qual versão do pipeline o produziu.
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
Essas são políticas internas de exemplo, não fatos universais. Defina limites de acordo com o comportamento da fonte, termos do contrato, expectativas do usuário e risco.
class MenuChange(BaseModel):
restaurant_id: str
location_id: str
item_id: str
field: str
before: object
após: objeto
source_before: str
source_after: str
requires_review: bool
HIGH_RISK_FIELDS = {"alérgenos", "etiquetas dietéticas", "disponibilidade", "preço"}
def compare_items(previous: MenuItem, current: MenuItem) -> list[MenuChange]:
changes = []
for field in [
"nome",
"descrição",
"preço_base",
"opções",
"etiquetas dietéticas",
"alérgenos",
"disponível",
]:
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
Não alerte sobre diferenças na ordem ou mudanças apenas de espaçamento. Compare registros normalizados.
A camada de IA deve responder com registros verificados e nunca fazer um pedido sem um passo de confirmação separado.
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
)
Mostre a localização, o item, as opções, a quantidade, o subtotal, taxas, impostos, o horário da fonte e as condições de cancelamento antes de pedir confirmação explícita do usuário.
| Design do pipeline | Qualidade da fonte | Frescor | Segurança | Recomendação |
|---|---|---|---|---|
| Texto de página achatado | Baixa | Desconhecido | Baixa | Evitar |
| Agregado de terceiros apenas | Variável | Variável | Médio | Usar com cuidado |
| APIs do proprietário mais ledger normalizado | Alta | Alta | Alta | Preferido |
| Página de primeira parte com revisão | Média | Mensurável | Média a alta | Usar quando autorizado |
A arquitetura preferida começa com fontes estruturadas controladas pelo proprietário e mantém a evidência anexada a cada campo.
class QualityResult(BaseModel):
accepted: bool
reasons: list[str]
def validate_item(item: MenuItem) -> QualityResult:
reasons = []
if not item.name.strip():
reasons.append("nome_faltando")
if item.base_price and item.base_price.amount < 0:
reasons.append("preço_negativo")
if item.evidence.source_type == "REVISÃO_DE_IMAGEM":
reasons.append("fonte_de_imagem_requer_revisão")
if item.allergens and item.evidence.source_type not in {
"API_DO_PROPRIETÁRIO",
"EXPORTAÇÃO_DO_PROPRIETÁRIO",
"PERFIL_DE_NEGÓCIO",
}:
reasons.append("fonte_de_alérgenos_requer_revisão")
return QualityResult(accepted=not reasons, reasons=reasons)
Quarantine registros com falha em vez de repará-los silenciosamente com texto gerado.
Rastreie:
Não registre chaves de API, cookies, dados de clientes privados, dados de pagamento ou estado completo do armazenamento do navegador.
A Perguntas Frequentes da CapSolver sobre IA e automação pode apoiar o design de ferramentas e limites de aprovação.
import pytest
def test_price_de_mercado_não_é_preço_zero():
value = normalize_price(
{"market_price": True, "display": "Preço de mercado"},
"USD",
)
assert value.price_type == "PREÇO_DE_MERCADO"
assert value.original_text == "Preço de mercado"
def test_reclamação_de_alérgeno_não_revisada_está_bloqueada():
claim = SafetyClaim(
claim="contém amendoim",
source_type="JSON_LD",
explicit_source_text="contém amendoim",
confidence=1.0,
reviewed=False,
reviewer_id=None,
)
assert allow_safety_claim(claim) is False
Teste também precisão de moeda, separação de localização, rótulos multilíngues, restrições de opção, preços desatualizados, IDs de itens duplicados, prioridade de fonte, classificação de páginas de desafio e portas de confirmação de pedido.
Código Bônus: Use o código WEBS no Painel da CapSolver para obter um bônus adicional de 5% em cada recarga.
O Perguntas Frequentes da CapSolver sobre resolução de CAPTCHA fornece orientações adicionais para etapas de desafio autorizadas.
Colete dados de cardápio de restaurantes apenas de fontes controladas pelo proprietário, licenciadas, públicas ou explicitamente autorizadas. Respeite acordos de API, termos, diretrizes de robôs, limites de taxa, direitos de autor, privacidade e direitos de banco de dados. Não colete dados de clientes, pagamentos, fidelidade ou pedidos privados. Não infira alérgenos ou adequação dietética. Um sistema de IA deve apresentar o horário da fonte e a incerteza, e uma pessoa deve confirmar escolhas importantes.
Dados de cardápio de restaurantes para sistemas de pedidos de IA precisam de hierarquia, proveniência, frescor e controles de segurança. Prefira APIs de proprietário e feeds de POS, preservar o contexto de localização e cardápio, normalizar opções e preços de itens, manter variantes de idioma vinculadas e exigir evidência explícita para alérgenos e rótulos dietéticos. Use páginas renderizadas autorizadas apenas como fallback controlado e trate o tratamento de desafio suportado como uma única etapa de infraestrutura estreita — não como permissão para acessar uma fonte.
Construa um pipeline autorizado com CapSolver onde os desafios suportados interrompem a coleta de cardápio aprovada, depois verifique a fonte, o estado do cardápio, a proveniência do campo e a frescor antes que um sistema de IA use o resultado.
Use primeiro uma API autorizada pelo proprietário, feed de POS ou exportação. Cardápios de Perfil de Negócio e dados estruturados de primeira parte são fontes úteis secundárias.
Inclua restaurante, localização, cardápio, seção, item, opção, preço, moeda, idioma, etiquetas dietéticas, alérgenos, disponibilidade, fonte e horário de observação.
Não deve. Apresente essas reivindicações apenas quando a fonte as declare explicitamente e a política de revisão relevante seja atendida.
Defina políticas específicas por campo. Disponibilidade e preços geralmente precisam de limites mais curtos do que descrições ou mídia, mas o intervalo exato depende do comportamento da fonte e do risco.
A CapSolver pode lidar com um desafio suportado em uma página de primeira parte autorizada quando nenhuma fonte estruturada aprovada estiver disponível. O resultado ainda precisa de validação de página e dados.
Aprenda arquitetura de raspagem web escalável em Rust com reqwest, scraper, raspagem assíncrona, raspagem de navegador headless, rotação de proxies e tratamento de CAPTCHA compatível.

Compare o Selenium vs Puppeteer para resolver CAPTCHA. Descubra benchmarks de desempenho, notas de estabilidade e como integrar o CapSolver para o máximo de sucesso.
