Get In Touch

Share your goals and requirements with us, and our expert team will contact you to discuss the right solutions for your business.

    Let’s Understand Your Data Requirements

    How to Scrape OpenTable Restaurant Reviews with Python: Step-by-Step Guide

    scrape-opentable-restaurant-reviews-python

    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:

    ReviewerOverall RatingDining DateReview
    Reviewer A5March 13, 2026Excellent service and food…
    Reviewer B4March 5, 2026Good experience overall…
    Reviewer C3February 27, 2026Food 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:

    1. Open the restaurant page.
    2. Scroll to the reviews section.
    3. Right-click an individual review.
    4. Select Inspect.
    5. Identify stable attributes surrounding the review container.
    6. 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 driver

    For 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, 2026

    The 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 None

    This 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 reviews

    Using 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_reviews

    Notice 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
    Review

    Always 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, 2026

    or a relative date such as:

    Dined 7 days ago

    First 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 TextBlob

    Create the sentiment function:

    def get_sentiment(text):
        return TextBlob(
            str(text)
        ).sentiment.polarity

    Apply 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 plt

    Create 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 noisy

    can 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:

    None

    or:

    NaN

    until 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:

    1. Verify selectors against the live website.
    2. Start with a small test sample.
    3. Save raw records before extensive transformations.
    4. Keep missing values instead of inventing information.
    5. Remove duplicate reviews.
    6. Validate ratings against visible page content.
    7. Record the extraction date.
    8. Record the source restaurant URL.
    9. Monitor frontend changes.
    10. 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 classification

    This 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.

    Frequently Asked Questions

    Continue Reading

    alternative-data-guide
    Business
    What Is Alternative Data? A Complete Guide for Businesses and Investors

    What if the most valuable business insights aren’t sitting in traditional financial reports or market databases? They could be hidden …

    iWeb Scraping iWeb Scraping Read Time: 10 min
    price-monitoring-vs-price-intelligence
    Other
    Price Monitoring vs. Price Intelligence: What’s the Difference?

    Forrester Research found that 62% of online shoppers compare prices before buying, making price visibility more important than ever for …

    iWeb Scraping iWeb Scraping Read Time: 7 min
    competitor-price-monitoring-profit-margins
    Other
    How Competitor Price Monitoring Helps Retailers Increase Profit Margins

    How do you know whether your prices are still competitive without giving away too much margin? In retail, prices can …

    iWeb Scraping iWeb Scraping Read Time: 9 min

    Build the Right Solution for You

    Share your requirements, and we will definitely deliver a solution that will satisfy your needs perfectly!

    linkedin
    Quick Response

    Fast replies guaranteed

    linkedin
    Expert Team

    Driven by expertise

    linkedin
    Secured Process

    Built with strong security

    linkedin
    Ongoing Support

    Support whenever you need

    Save Time & Money

    Bulk data delivery in less time.

    Complex & Varied Data

    Hassle-free handling of JavaScript, logins, APIs, and dynamic.

    Custom-Built Pipeline

    Designed as per your requirements and scalability.

    Social Media :

      Let’s Understand Your Data Requirements

      Scroll to Top