Restaurant reviews contain valuable information about customer satisfaction, food quality, service, ambience, and overall dining experiences. For restaurant operators, market researchers, hospitality businesses, and data analysts, analyzing this feedback can help reveal recurring customer concerns, popular menu experiences, competitive strengths, and changes in customer sentiment.
OpenTable restaurant pages contain reviews from diners who have visited restaurants using the platform. Current restaurant pages can include an overall rating, individual review text, dining dates, and ratings for areas such as food, service, and ambience.
In this Python tutorial, we will build a small-scale workflow to:
- Load an OpenTable restaurant page.
- Identify restaurant review elements.
- Extract review text, ratings, reviewer names, and dining dates.
- Handle multiple review pages.
- Clean the collected data with Pandas.
- Export reviews to CSV and JSON.
- Perform basic sentiment analysis.
- Visualize rating and sentiment distributions.
The objective is not simply to collect review data. It is to transform publicly accessible information into a structured dataset that can support restaurant analytics and market research.
For enterprise-scale restaurant datasets, you can also explore our restaurant data scraping services.
Important: Website layouts and HTML attributes change over time. Always inspect the current OpenTable page before using a scraper in production. You should also review applicable terms, policies, legal requirements, and access restrictions before collecting website data.
What Data Can You Extract from OpenTable Restaurant Reviews?
Depending on the restaurant page and current OpenTable layout, a review may contain information such as:
- Reviewer display name
- Overall rating
- Dining date
- Review text
- Food rating
- Service rating
- Ambience rating
- Value rating
- Restaurant response
- Review count
- Restaurant average rating
For example, current OpenTable restaurant pages publicly display verified diner reviews containing dining dates, review text, overall ratings, and several category-level ratings.
A structured output could look like this:
| Reviewer | Overall Rating | Dining Date | Review |
| Reviewer A | 5 | March 13, 2026 | Excellent service and food… |
| Reviewer B | 4 | March 5, 2026 | Good experience overall… |
| Reviewer C | 3 | February 27, 2026 | Food was good but service… |
The table above is only an example of the expected structure. Your actual results will depend on the restaurant and reviews collected.
Prerequisites for Scraping OpenTable Reviews with Python
For this tutorial, we will use:
- Python – core programming language
- Selenium – renders and interacts with browser content
- Pandas – structures and cleans the dataset
- TextBlob – performs basic sentiment analysis
- Matplotlib – creates simple visualizations
BeautifulSoup is not required for this version because Selenium can extract the rendered review elements directly.
Install the Required Python Libraries
Run:
pip install -U selenium pandas textblob matplotlib
You can also create an isolated Python environment.
Windows
python -m venv opentable_env opentable_env\Scripts\activate
macOS/Linux
python -m venv opentable_env source opentable_env/bin/activate
Modern Selenium versions normally use Selenium Manager to locate or manage compatible browser drivers, so manually downloading ChromeDriver is generally unnecessary for standard installations.
A simple Chrome session can therefore usually be started with:
from selenium import web driver driver = webdriver.Chrome()
Understanding the OpenTable Restaurant Page Structure
Before writing extraction logic, open the target OpenTable restaurant page in Chrome.
A typical restaurant URL follows a structure similar to:
https://www.opentable.com/r/restaurant-name-city
Do not assume that CSS class names from an old tutorial will continue working.
OpenTable’s markup has changed over time, so selectors based on generic classes such as:
.review .review-text .reviewer-name
should not be invented or copied from outdated tutorials.
Instead:
- Open the restaurant page.
- Scroll to the reviews section.
- Right-click an individual review.
- Select Inspect.
- Identify stable attributes surrounding the review container.
- Confirm where the review text, rating, date, and reviewer name are stored.
Using attributes such as data-testid, accessible labels, and meaningful structural elements is generally preferable to relying on dynamically generated CSS class names.
Setting Up Selenium for OpenTable
Start by importing the libraries that will be used throughout the scraper.
import re import time from urllib.parse import urlsplit, urlunsplit, parse_qs, urlencode import pandas as pd from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions
as EC
Create a helper function for the browser:
def create_driver():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1400,1000")
driver = webdriver.Chrome(options=options)
return driverFor an educational demonstration, using a normal visible browser is useful because you can observe what Selenium is loading.
Choose an OpenTable Restaurant URL
Set the restaurant URL you want to analyze:
BASE_URL = “https://www.opentable.com/r/your-restaurant-slug”
Replace:
your-restaurant-slug
with the actual restaurant URL slug you intend to analyze.
Keep your initial test small. Two or three pages are enough to verify whether the workflow works correctly.
How to Build Pagination URLs
OpenTable review listings may expose additional review pages through a page query parameter.
Instead of manually constructing URLs, create a helper function:
def build_page_url(base_url, page_number): parts = urlsplit(base_url) query = parse_qs(parts.query) query["page"] = [str(page_number)] new_query = urlencode(query, doseq=True) return urlunsplit( ( parts.scheme, parts.netloc, parts.path, new_query, parts.fragment, )
For example:
print(build_page_url(BASE_URL, 2))
can produce:
https://www.opentable.com/r/your-restaurant-slug?page=2
You should confirm that pagination behaves this way for the restaurant page you are testing before relying on it.
Finding the OpenTable Review Section
One current OpenTable layout pattern uses a review-section container associated with a data-testid attribute.
We can wait for the review section like this:
def wait_for_reviews(driver):
return WebDriverWait(driver, 15).until(
EC.presence_of_element_located(
(
By.CSS_SELECTOR,
'div[data-testid="reviews-list-section-content"]',
)
)
)Because OpenTable can update its frontend, treat this selector as something that should be verified through Developer Tools before production use.
Identifying Individual Review Blocks
Instead of assuming a historical class such as .review, we can start from visible dining-date elements and locate their parent review blocks.
A typical OpenTable review contains wording such as:
Dined on March 13, 2026The following function searches for these date elements:
def find_review_date_elements(container):
xpath = (
".//*["
"contains(normalize-space(text()), 'Dined on ') "
"or starts-with(normalize-space(text()), 'Dined ')"
"]"
)
return container.find_elements(By.XPATH, xpath)Next, we can move upward through the DOM to identify a parent element containing the rest of the review.
def find_review_card(date_element):
for level in range(1, 8):
try:
card = date_element.find_element(
By.XPATH,
f"./ancestor::*[{level}]"
)
text = card.text.strip()
if (
"Overall" in text
and "Dined" in text
and len(text.splitlines()) >= 5
):
return card
except Exception:
continue
return NoneThis approach is intentionally based on the visible structure instead of an imaginary .review class.
However, any scraper that depends on a website DOM should still be checked whenever the target site changes its interface.
Extracting Review Information
Now create a parser for each review card.
def parse_review_card(card):
lines = [
line.strip()
for line in card.text.splitlines()
if line.strip()
]
if not lines:
return None
reviewer = lines[0]
date_text = None
overall_rating = None
for line in lines:
if line.startswith("Dined"):
date_text = line
rating_match = re.fullmatch(
r"Overall\s+([1-5](?:\.0)?)",
line
)
if rating_match:
overall_rating = float(
rating_match.group(1)
)
category_pattern = re.compile(
r"^(Overall|Food|Service|Ambience|Value)\s+[1-5](?:\.0)?$"
)
start_index = None
for index, line in enumerate(lines):
if category_pattern.match(line):
start_index = index + 1
if start_index is None:
return None
review_lines = []
for line in lines[start_index:]:
if line.lower().startswith("is this helpful"):
break
if "Responded on" in line:
break
review_lines.append(line)
review_text = " ".join(review_lines).strip()
return {
"Reviewer": reviewer,
"Overall_Rating": overall_rating,
"Dining_Date": date_text,
"Review": review_text,
}The extraction function returns structured data instead of leaving review information as raw browser text.
Scraping Reviews from One Page
Now combine the previous functions.
def scrape_review_page(driver, url):
driver.get(url)
container = wait_for_reviews(driver)
date_elements = find_review_date_elements(
container
)
reviews = []
processed_cards = set()
for date_element in date_elements:
card = find_review_card(date_element)
if card is None:
continue
card_id = card.id
if card_id in processed_cards:
continue
processed_cards.add(card_id)
parsed = parse_review_card(card)
if (
parsed
and parsed["Review"]
):
reviews.append(parsed)
return reviewsUsing processed_cards prevents the same parent review block from being processed several times if multiple matching elements exist inside it.
Scraping Multiple OpenTable Review Pages
Now create the main scraper.
def scrape_opentable_reviews(
restaurant_url,
max_pages=3
):
driver = create_driver()
all_reviews = []
try:
for page_number in range(
1,
max_pages + 1
):
page_url = build_page_url(
restaurant_url,
page_number
)
print(
f"Collecting page {page_number}: "
f"{page_url}"
)
page_reviews = scrape_review_page(
driver,
page_url
)
if not page_reviews:
print(
"No reviews found. "
"Stopping pagination."
)
break
all_reviews.extend(
page_reviews
)
# Conservative pause between pages.
time.sleep(2)
finally:
driver.quit()
return all_reviewsNotice that the pause is used to maintain a conservative collection pace—not to disguise automation or bypass anti-bot controls.
Do not attempt to circumvent authentication, CAPTCHAs, access controls, or technical restrictions.
Run the OpenTable Review Scraper
Run:
reviews = scrape_opentable_reviews(
BASE_URL,
max_pages=3
)
print(
f"Collected {len(reviews)} reviews"
)Then convert the result to a Pandas DataFrame:
df = pd.DataFrame(reviews)
print(df.head())Your columns should resemble:
Reviewer
Overall_Rating
Dining_Date
ReviewAlways manually inspect several rows before continuing with analysis.
A scraper returning data does not automatically mean that it extracted the data correctly.
Cleaning the OpenTable Review Data
Raw text frequently contains extra whitespace and duplicate records.
Clean the dataset with:
df["Review"] = (
df["Review"]
.astype(str)
.str.replace(
r"\s+",
" ",
regex=True
)
.str.strip()
)Remove records without meaningful review text:
df = df[
df["Review"].str.len() > 0
].copy()Remove duplicate reviews:
df = df.drop_duplicates(
subset=[
"Reviewer",
"Dining_Date",
"Review"
]
)Convert ratings into numeric values:
df["Overall_Rating"] = pd.to_numeric(
df["Overall_Rating"],
errors="coerce"
)Cleaning Dining Dates
OpenTable may display dates differently depending on how recent a review is.
For example:
Dined on March 13, 2026or a relative date such as:
Dined 7 days agoFirst remove the prefix:
df["Dining_Date_Clean"] = (
df["Dining_Date"]
.str.replace(
r"^Dined on\s+",
"",
regex=True
)
)You can convert absolute dates with:
df["Dining_Date_Parsed"] = pd.to_datetime(
df["Dining_Date_Clean"],
errors="coerce"
)Rows containing relative dates may require separate normalization if you need exact time-series analysis.
Do not silently assign an incorrect date.
Save OpenTable Reviews to CSV
Export the cleaned data:
df.to_csv(
"opentable_reviews.csv",
index=False
)The resulting CSV can be opened in Excel, Google Sheets, BI platforms, databases, or other analytics tools.
Export OpenTable Reviews to JSON
For applications, APIs, dashboards, or machine-learning pipelines, JSON may be more convenient.
df.to_json(
"opentable_reviews.json",
orient="records",
indent=2,
force_ascii=False
)This creates structured records such as:
[
{
"Reviewer": "Reviewer A",
"Overall_Rating": 5.0,
"Dining_Date": "Dined on March 13, 2026",
"Review": "Excellent service and food."
}
]The example above illustrates the format and is not presented as an actual scraped review.
Performing Sentiment Analysis on OpenTable Reviews
After cleaning the dataset, you can perform simple sentiment analysis.
TextBlob assigns a polarity score between:
- -1 – strongly negative
- 0 – neutral
- +1 – strongly positive
Import TextBlob:
from textblob import TextBlobCreate the sentiment function:
def get_sentiment(text):
return TextBlob(
str(text)
).sentiment.polarityApply it:
df["Sentiment_Score"] = (
df["Review"]
.apply(get_sentiment)
)Now classify each score.
def label_sentiment(score):
if score > 0.1:
return "Positive"
if score < -0.1:
return "Negative"
return "Neutral"Apply the labels:
df["Sentiment_Label"] = (
df["Sentiment_Score"]
.apply(label_sentiment)
)You now have both a numerical score and an easy-to-understand classification.
Important Limitation of TextBlob Sentiment Analysis
TextBlob is useful for demonstrations, but restaurant reviews often contain context that simple polarity models can misinterpret.
Consider:
The restaurant was beautiful, but we waited almost an hour for food.The sentence contains both positive and negative sentiment.
A simple sentiment model may assign a single overall score and miss the individual themes.
For more advanced restaurant review analysis, consider techniques such as:
- Aspect-based sentiment analysis
- VADER
- spaCy NLP pipelines
- Transformer-based sentiment models
- Topic extraction
- Keyword clustering
- Named entity recognition
These methods can help separate sentiment associated with:
- Food
- Service
- Price
- Ambience
- Waiting time
- Staff
- Menu items
Visualizing the Rating Distribution
Import Matplotlib:
import matplotlib.pyplot as pltCreate a rating distribution:
rating_counts = (
df["Overall_Rating"]
.value_counts()
.sort_index()
)
rating_counts.plot(
kind="bar"
)
plt.xlabel("Overall Rating")
plt.ylabel("Number of Reviews")
plt.title(
"OpenTable Review Rating Distribution"
)
plt.tight_layout()
plt.show()This visualization helps identify whether your dataset contains predominantly high, low, or mixed ratings.
Visualizing Sentiment Categories
Calculate sentiment totals:
sentiment_counts = (
df["Sentiment_Label"]
.value_counts()
)Plot them:
sentiment_counts.plot(
kind="bar"
)
plt.xlabel("Sentiment")
plt.ylabel("Number of Reviews")
plt.title(
"OpenTable Review Sentiment Distribution"
)
plt.tight_layout()
plt.show()Do not write conclusions such as:
“80% of customers were satisfied.”
unless your actual dataset and calculations support that statement.
Instead, calculate the percentage:
sentiment_percentage = (
df["Sentiment_Label"]
.value_counts(
normalize=True
)
.mul(100)
.round(2)
)
print(sentiment_percentage)Your published findings should come directly from this output.
Analyzing Review Ratings Over Time
If enough absolute dining dates are available, you can examine rating changes over time.
Remove rows without valid dates:
dated_df = df.dropna(
subset=[
"Dining_Date_Parsed",
"Overall_Rating"
]
).copy()Create a monthly period:
dated_df["Month"] = (
dated_df[
"Dining_Date_Parsed"
]
.dt.to_period("M")
)Calculate monthly averages:
monthly_rating = (
dated_df
.groupby("Month")[
"Overall_Rating"
]
.mean()
)Display the results:
print(monthly_rating)You can then visualize them:
monthly_rating.plot()
plt.xlabel("Month")
plt.ylabel("Average Rating")
plt.title(
"Average OpenTable Rating Over Time"
)
plt.tight_layout()
plt.show()Seasonal conclusions should only be made when the dataset contains enough reviews across enough months to support them.
Analyzing only a few recent pages does not justify statements about annual seasonal patterns.
How to Identify Common Customer Themes
Restaurant reviews often repeatedly mention topics such as:
- Service
- Staff
- Waiting time
- Portions
- Price
- Drinks
- Desserts
- Noise
- Ambience
- Specific menu items
A basic keyword frequency analysis can provide an initial view.
from collections import Counter
import re
all_text = " ".join(
df["Review"]
.dropna()
.str.lower()
)
words = re.findall(
r"\b[a-z]{4,}\b",
all_text
)
word_counts = Counter(words)
print(
word_counts.most_common(20)
)For meaningful business analysis, remove stop words and consider phrase extraction rather than relying only on individual-word frequency.
For example:
great service
slow service
friendly staff
long wait
excellent food
too noisycan be more informative than isolated words such as great, service, or food.
What Can Businesses Learn from OpenTable Review Data?
Once sufficient review data has been collected and validated, businesses can explore several areas.
Customer Satisfaction
Compare rating distributions and sentiment scores to identify whether customer experiences are improving or declining.
Restaurant Competitor Analysis
Compare multiple restaurants using:
- Average rating
- Review volume
- Customer sentiment
- Common complaints
- Common positive themes
Service Quality Analysis
Analyze reviews mentioning:
- Staff
- Servers
- Waiting time
- Reservation problems
- Customer support
Menu and Food Feedback
Extract recurring mentions of:
- Dishes
- Drinks
- Desserts
- Portion sizes
- Food quality
Reputation Monitoring
Repeatedly analyzing new reviews can help businesses detect changes in customer sentiment and identify issues requiring attention.
Scaling OpenTable Review Data Collection
The example in this guide intentionally uses a small number of pages.
A production data pipeline normally requires additional capabilities such as:
- URL management
- Request scheduling
- Error logging
- Retry handling
- Duplicate detection
- Schema validation
- Database storage
- Data-quality monitoring
- DOM-change detection
- Automated alerts
- Scheduled refreshes
A scalable restaurant review dataset could eventually support dashboards that track:
- Review volume
- Average ratings
- Sentiment trends
- Restaurant comparisons
- Location-level performance
- Frequently mentioned topics
- Customer complaints
Do not assume that a small demonstration scraper can simply be run at very high volume without additional engineering, governance, and compliance controls.
Should You Use an API Instead?
For approved integrations, an official API is generally preferable to collecting rendered website content.
OpenTable currently documents a Reviews API for partners that can provide restaurant review information and review summary statistics. Access is intended for approved partner integrations rather than being an unrestricted public API.
If your organization qualifies for an official OpenTable integration, investigate that option before building a large production scraping system.
For other public-data requirements, evaluate the permitted data source and collection method appropriate for your project.
Common Problems When Scraping OpenTable Reviews
No Reviews Are Found
Possible reasons include:
- The restaurant has no reviews.
- The review section has not loaded.
- OpenTable changed its HTML structure.
- Your selector is outdated.
Inspect the live page again rather than guessing another CSS class.
Selenium Times Out
Increase your explicit wait:
WebDriverWait(
driver,
20
)Also verify whether the review container is actually present on the page.
Duplicate Reviews Appear
Remove duplicates using:
df.drop_duplicates(
subset=[
"Reviewer",
"Dining_Date",
"Review"
],
inplace=True
)Ratings Are Missing
Check whether OpenTable changed the way rating information is displayed.
Do not substitute a missing value with an assumed rating.
Use:
Noneor:
NaNuntil the appropriate extraction logic is verified.
Pagination Stops Working
Check the live URL when manually switching review pages.
The website’s pagination implementation can change, so the scraper should not assume that one URL format will remain permanent.
Best Practices for Reliable Restaurant Review Data Collection
For better-quality results:
- Verify selectors against the live website.
- Start with a small test sample.
- Save raw records before extensive transformations.
- Keep missing values instead of inventing information.
- Remove duplicate reviews.
- Validate ratings against visible page content.
- Record the extraction date.
- Record the source restaurant URL.
- Monitor frontend changes.
- Respect applicable terms, policies, access restrictions, and legal requirements.
You should also document the environment used to generate the dataset.
For example:
Source:
OpenTable restaurant profile
Collection date:
YYYY-MM-DD
Pages processed:
3
Fields:
Reviewer
Dining date
Overall rating
Review text
Output:
CSV and JSON
Analysis:
TextBlob sentiment classificationThis makes the methodology easier to reproduce and audit.
Conclusion
Scraping restaurant reviews is only the first step in building useful restaurant intelligence.
In this tutorial, we created a Python workflow that demonstrates how to load OpenTable restaurant pages with Selenium, identify review content, extract structured review information, clean the records with Pandas, save the dataset to CSV and JSON, and perform basic sentiment analysis.
More importantly, the workflow emphasizes data validation.
Do not publish statistics, seasonal trends, sentiment percentages, or customer-behaviour conclusions unless your collected dataset actually supports them.
For a real project, document:
- Which restaurant pages were analyzed.
- When the data was collected.
- How many reviews were processed.
- Which fields were extracted.
- How missing and duplicate records were handled.
- Which sentiment or NLP methodology was used.
That makes the resulting analysis considerably more reliable and reproducible.
For businesses that need restaurant data at larger scale, iWeb Scraping provides restaurant data extraction solutions that can support requirements such as review monitoring, menu intelligence, pricing analysis, location analysis, competitive research, and structured data delivery.
Whether you are building a small research project or an enterprise restaurant intelligence system, the same principle applies: collect data responsibly, validate it carefully, and base every published insight on evidence from the actual dataset.

Vishva Dholaria

