How to scrape restaurant menus and prices

price monitoring, Web Scraper Cloud, data quality, no-code scraping, Restaurant menu data

To scrape restaurant menus and prices, select each dish's container, then extract its name and price from inside that container. Keep the branch, menu period and portion size with the result so that each price describes the right offer.

With Web Scraper, you can build and test this structure in the browser extension, export a sample, then move the validated sitemap to Cloud for recurring collection.


Choose the menu you want to measure

Start with one public menu page for a specific restaurant branch. Decide whether you need dine-in, collection or delivery prices, and which menu period matters. A breakfast offer and a dinner offer should remain distinguishable even when their dish names match.

Check the source format before configuring the scraper:

  • HTML menu: dishes and prices are page elements that you can target with selectors.
  • Interactive menu: determine whether tabs, scrolling or item details reveal additional records or change prices.
  • PDF or menu photograph: use a separate document-extraction or OCR process, or find an HTML version. Collecting a file or image URL does not extract the dishes printed inside it.

Reload the page and confirm that you can reproduce the intended branch and menu state. Review the source's terms and access conditions for your collection and intended reuse before automating it.

For multiple restaurants, group pages by layout. Test reuse across similar branch pages; configure and validate different website structures separately.

Decide what one row represents

For a simple menu, use one row per dish. When sizes have separate prices, use one row per dish and size, with branch, channel and menu period retained as context.

These fictional rows illustrate three distinct offers from one branch's lunch menu:

DishSizeChannelPrice as displayedNormalised amount
MargheritaRegularCollection£12.5012.50
MargheritaLargeCollection£16.0016.00
MargheritaRegularDelivery£13.5013.50

The delivery row should not appear as a price increase over the collection row. They describe different offers.

Keep the original price text, currency, source menu URL and collection time. Capture a source item or variant ID when available. If the page does not expose a branch or channel label, attach verified collection metadata when storing the export instead of inventing a scraped field.

How to scrape restaurant menus and prices with Web Scraper

1. Create a sitemap for the selected menu

Install the Web Scraper browser extension and open the menu page. The AI Sitemap Wizard can generate an initial sitemap for common workflows. For direct control over section and dish relationships, use the Advanced Sitemap Builder:

  1. Open browser Developer Tools.
  2. Select the Web Scraper tab.
  3. Choose Create sitemap.
  4. Enter a name and use the specific menu URL as the start URL.

If branch or menu links lead to genuine destination pages, a Link selector can follow them, with destination fields beneath that link. A tab that changes content without changing the URL needs an interaction workflow instead.

2. Select section and dish wrappers

Create an Element selector named section. Use Select to target repeated containers that enclose both a section heading and its dishes. Select additional examples to check coverage, then enable Multiple.

Navigate into section and create another Element selector named dish. Select the whole repeated dish card, including its name and intended price, and enable Multiple again. During selection, press P to move to a parent element when necessary. Check the boundaries with Element Preview.

The dish wrapper keeps related fields together. Independent page-wide lists of names and prices do not tell Web Scraper to pair their matches by position.

If the page has no enclosing section containers, put dish directly under _root. Omit the section field until you have a reliable way to associate it with each dish.

3. Add fields inside the correct parent

Navigate into dish before adding Text selectors for the name, description and price. For each scalar field, choose Multiple type → First Record Only. Narrow the selector to the intended value: this setting cannot decide which of several amounts is the correct price.

This is an illustrative configuration. Replace its CSS examples with selectors matched to your target menu.

SelectorParentType and settingIllustrative CSS
branch_name_rootText; First Record Only.branch-name
section_rootElement; Multiple enabled.menu-section
section_namesectionText; First Record Only.section-title
dishsectionElement; Multiple enabled.menu-item
item_namedishText; First Record Only.item-name
price_rawdishText; First Record Only.item-price
descriptiondishText; First Record Only.item-description
availability_textdishText; First Record Only.availability

Add page-wide restaurant, menu-period and channel fields alongside section, where exposed. Keep section_name alongside dish, where the heading is accessible. Single-valued sibling fields repeat into the records generated by the repeated branch. A heading outside a dish card cannot be read by a child selector scoped inside that card. These relationships follow Web Scraper's selector and record-generation rules.

For an item ID stored in an attribute, add an Element Attribute selector. If the ID is on the dish wrapper itself, use _parent_ and select the actual attribute, such as data-id. Check that it remains suitable across visits.

4. Run and export a first sample

Use Element Preview to check the matched containers and Data Preview to inspect extracted values and combined rows. Check the first and last dishes, another section and an item with a missing optional field.

Then select Scrape from the sitemap dropdown, configure the request interval and page load delay, and start a local job. Keep its scraping window open. After completion, inspect the records and export CSV or XLSX. The preview and validation guide recommends checking an actual export before scheduling or scaling.

Compare the export with the menu: names must match their own prices, categories must be correct, and branch and channel must match the intended state. Watch for duplicate desktop and mobile menus or prices collected from a sticky basket rather than a dish.

5. Handle tabs, scrolling and item details

First check whether a click is necessary. Reveal a tab manually, identify its field selectors, reload, then run Data Preview without clicking. If the values are returned, extract them directly.

For content that genuinely requires interaction:

  • Menu tabs: use Element Click with the result container in Selector and the controls in Click selector. Use Click Once, enabling Multiple for repeated state extraction. Place dependent fields beneath it, including the active menu label. Test control uniqueness, delays and initial-record handling to avoid skipped states or duplicates.
  • Infinite scrolling: enable Scroll on the repeated Element selector. Check later dishes and ensure an element limit is not truncating the result.
  • Load more: create a Pagination selector for the button and begin with the automatic pagination type. Put the extraction branch beneath it, keeping any section and dish nesting intact. Confirm that clicks add records and execution stops appropriately. The pagination and scrolling guide explains these different paths.
  • Item detail panels: test successive dishes, their identities and the close or return sequence. A panel inserted elsewhere in the page needs an interaction scope that can reach it; children inside the original dish wrapper cannot simply select outside it.

When a branch must be selected first, Website State Setup can perform preliminary actions. Set Perform when not found selector to evidence that the intended branch is active. A generic location label could also match the wrong branch. Verify the exported branch after a fresh run.

Embedded ordering widgets and complex option flows need site-specific testing. An interaction sequence that works on one menu is not a universal restaurant template.

6. Separate sizes, add-ons and price qualifications

For repeated size-price rows inside a dish, add a variant Element selector beneath dish and enable Multiple. Put the size label and price beneath variant, moving price_raw into that branch. Keep the single dish name alongside variant so it accompanies each size record.

For click-driven sizes, capture the selected label and resulting price in the same state. Check whether one option resets another.

Keep “extra cheese +£1.50” separate from a complete dish price. If modifiers matter, collect related option rows containing the parent dish ID, option name and surcharge. Distinguish required choices from optional extras. A set-menu total should not become the price of every included course.

Preserve price_raw and create a separate numeric amount with an explicit currency. Keep “from”, ranges, “per person” and market-price wording as qualifications. Separate promotional prices and order-level charges where relevant. Leave missing amounts empty rather than converting them to zero.

Web Scraper Cloud's Parser can transform correctly extracted values and create derived columns. Check varied downloaded records after applying rules for currency symbols or decimal separators.

Validate coverage before automating

After adding interactions or variants, repeat the sample export. Count dishes by section, inspect the final expected section and verify branch, channel and menu period. Test another branch before reusing the sitemap across locations.

Treat these observations differently:

ObservationWhat to do
The menu explicitly says “sold out”Retain that observed availability label.
A dish is present but its extracted price is blankCheck the source, selector and selected options.
A previously collected dish is missingConfirm menu state and collection coverage before calling it removed.
Most dishes disappear togetherInvestigate loading, navigation or access problems before accepting menu changes.

A completed job does not establish a correct dataset. For recurring runs, configure data quality controls around a known-good baseline, including record count and required-field population. Investigate significant deviations before using the results.

Schedule menu collection in Web Scraper Cloud

Import or synchronise the tested sitemap into Cloud, then validate its intended driver, request interval, page load delay and proxy settings with Test scrape.

Use FullJS, the browser-based driver, when JavaScript or interactions are required. Fast reads returned HTML without executing page JavaScript and cannot run sitemaps containing scrolling, Element Click or Website State Setup. Check the driver documentation before switching a compatible sitemap to Fast.

In Parser, select + Add time scraped and choose the time zone and format. This records collection time, not the restaurant's own last-update time.

Open Schedule, choose the time zone and cadence, and save the tested run settings. Choose timing that observes the intended menu period. If the preceding scheduled job is still running, the next waits, so allow for the sitemap's normal duration.

Configure Data Export, connect a supported destination and confirm that a completed batch arrives correctly. Automatic export delivers the complete dataset after each job finishes.

Preserve dated snapshots or append validated observations in your own storage. Compare the same source item, branch, channel, menu period and variant; similar names alone do not establish equivalent dishes. Historical storage, matching and price-change calculations belong in your spreadsheet or downstream system. See building a price history dataset for that next stage.

Start with one menu and an export you can check against the source. Build the sitemap with the Web Scraper browser extension, then move it to Cloud once the dishes, prices and context consistently match.


Go back to blog page