Scrape TripAdvisor med Python (uden at blive blokeret)

Sidst opdateret den April 17, 2026
Scrape TripAdvisor med Python (uden at blive blokeret)

Sidste uge prøvede jeg at hente hotelbedømmelser og antal anmeldelser for omkring 200 overnatningssteder i tre europæiske byer fra TripAdvisor. Mit første script — et helt almindeligt requests.get() med standardheaders — gav mig et flot 403 Forbidden på hver eneste forespørgsel. Ikke en eneste byte brugbar data.

TripAdvisor er en af de mest værdifulde offentlige datakilder i rejsebranchen: over 1 milliard anmeldelser, mere end 8 millioner virksomhedsprofiler og omkring 460 millioner unikke månedlige besøgende. Platformen påvirker årligt rejseforbrug for mere end $60 milliarder. Men at hente de data programmatisk? Det er her, det bliver svært. TripAdvisor bruger DataDome bot-detektion, Cloudflare WAF, TLS-fingerprinting og JavaScript-udfordringer — et lagdelt forsvar, som stopper de fleste naive scraping-forsøg, før de overhovedet kommer i gang. Denne guide er den ene ressource, jeg selv ville ønske, jeg havde haft: en side-om-side sammenligning af tre Python-metoder til scraping (plus en no-code mulighed), komplet kode til hver metode, et struktureret afsnit om fejlfinding ved anti-bot og genbrugelige mønstre, der virker på tværs af hoteller, restauranter og seværdigheder. Uanset om du er ny i Python eller erfaren udvikler, bør det her spare dig for en masse spildte 403'ere.

Vil du ikke skrive kode? Så scrape TripAdvisor på den nemme måde

Jeg vil være helt ærlig. Mange, der søger på "scrape TripAdvisor med Python", er faktisk ikke særligt opsatte på at skrive kode. De vil bare have dataene — hotelnavne, bedømmelser, antal anmeldelser, priser — hurtigt og direkte ind i et regneark. Hvis det lyder som dig, er der en langt kortere vej.

Thunderbit er en AI-drevet Chrome-udvidelse, vi har bygget, som kan læse enhver TripAdvisor-side og automatisk foreslå de rigtige kolonner at udtrække. Arbejdsgangen er i praksis kun to klik:

  1. Åbn en TripAdvisor-liste-side (f.eks. søgeresultater for "Hotels in Paris").
  2. Klik på "AI Suggest Fields" i Thunderbit-sidepanelet. AI'en scanner siden og foreslår kolonner som Hotelnavn, Bedømmelse, Antal anmeldelser, Pris og Beliggenhed.
  3. Klik på "Scrape." Thunderbit henter data fra alle resultaterne på siden — og klarer automatisk pagination, hvis du har brug for flere resultater.
  4. Eksportér til Excel, Google Sheets, Airtable eller Notion. Eksport er gratis på alle planer.

Thunderbit virker på tværs af hoteller, restauranter og seværdigheder uden nogen ændringer i opsætningen — AI'en tilpasser sig det, der ligger på siden. Ved paginerede resultater genkender den automatisk "Next"-knapper og infinite scroll. Og fordi den kører i din rigtige Chrome-browser, arver den dine session cookies og browser fingerprint, hvilket giver den en naturlig fordel mod bot-detektion.

Du kan prøve den med Thunderbit Chrome Extension — gratisversionen giver dig 6 sider pr. måned, nok til at teste arbejdsgangen.

Hvis du har brug for programmatisk kontrol, specialiseret parsing-logik eller planlægger at scrape 10.000+ sider, er Python vejen frem. Læs videre.

Hvorfor scrape TripAdvisor med Python?

TripAdvisor-data har direkte og målbar forretningsværdi. Et Cornell University-studie viste, at en stigning på 1 point i et hotels Global Review Index på 100-point-skalaen giver en stigning på 0,89 % i gennemsnitlig dagspris og 1,42 % i Revenue Per Available Room. Et separat ScienceDirect-studie viste, at en udefrakommende stigning på 1 stjerne i TripAdvisor-vurderingen svarer til $55.000–$75.000 ekstra årlig omsætning for et gennemsnitligt hotel. Anmeldelser er ikke bare flotte tal — de driver omsætning.

Sådan bruger forskellige teams TripAdvisor-data:

AnvendelseHvem får gavnNødvendige data
Analyse af hotelkonkurrenterHotelkæder, revenue managersBedømmelser, priser, anmeldelsesvolumen, faciliteter
Markedsresearch for restauranterRestaurantgrupper, fødevarebrandsKøkkentyper, prisniveauer, sentiment i anmeldelser
Tracking af seværdighedstrendsRejsearrangører, turistrådPopularitetsrangeringer, sæsonmønstre
SentimentanalyseForskere, dataanalytikereFuld anmeldelsestekst, stjerner, datoer
LeadgenereringSalgsteams, rejsebureauerVirksomhedsnavne, kontaktoplysninger, placeringer

Hvorfor netop Python? Tre grunde. Først økosystemet: BeautifulSoup, Selenium, Playwright, Scrapy, httpx, pandas — Python har mere modne biblioteker til scraping og dataanalyse end noget andet sprog. For det andet bruger 71,7 % af udviklere inden for web scraping Python, hvilket betyder mere community-support, flere svar på StackOverflow og flere opdaterede guides. For det tredje pipeline-fordelen: du kan scrape med BeautifulSoup, rense data med pandas, køre sentimentanalyse med Hugging Face Transformers og bygge dashboards — alt sammen i ét sprog. Ingen kontekstskift.

Tre måder at scrape TripAdvisor med Python på (sammenlignet)

Alle konkurrerende guides vælger én metode og kører med den. Det hjælper ikke meget, når du skal beslutte dig før du skriver kode. Her er den sammenligningstabel, jeg selv ville ønske, nogen havde givet mig:

MetodeHastighedJS-understøttelseModstandsdygtighed mod anti-botKompleksitetBedst til
requests + BeautifulSoup⚡ Hurtig (~120–200 sider/min rå)❌ Ingen⚠️ LavLetStatiske lister, små projekter
Selenium / Headless browser🐢 Langsom (~8–20 sider/min)✅ Fuld⚠️ MellemMellemDynamisk indhold, "Læs mere"-klik, cookie-bannere
Skjult JSON / GraphQL API⚡⚡ Hurtigst (~200–600 sider/min rå)N/A✅ HøjereSværStor udtrækning af anmeldelser/hoteller
No-code (Thunderbit)⚡ Hurtig✅ Indbygget✅ IndbyggetNemmestIkke-udviklere, hurtige enkeltstående eksportopgaver

Et par vigtige forbehold. De rå hastigheder er teoretiske — TripAdvisors rate limits (~10–15 forespørgsler i minuttet pr. IP) begrænser den faktiske throughput til omkring 10 sider/minut pr. IP uanset metode. Den skjulte JSON-metode giver dig mest data pr. forespørgsel, hvilket betyder færre samlede forespørgsler og mindre eksponering for rate limiting. Selenium er 5 gange langsommere end request-baserede metoder i virkelige benchmarks, men det er den eneste mulighed, når du skal klikke på knapper eller rendere JavaScript.

Resten af guiden gennemgår alle tre Python-metoder med komplet kode. Vælg den, der passer til din situation, eller kombiner dem (jeg bruger ofte requests+BS4 til liste-sider og skjult JSON til detaljesider).

Klargøring af dit Python-miljø

Før vi går i gang, skal miljøet sættes op. Du skal bruge Python 3.10+ (jeg anbefaler 3.12 eller 3.13 — alle større pakker understøtter dem uden kendte problemer).

Installér alt på én gang:

pip install requests beautifulsoup4 selenium httpx parsel pandas curl-cffi

Bemærkninger til pakkerne:

  • requests (2.33.1) — HTTP-forespørgsler, kræver Python 3.10+
  • beautifulsoup4 (4.14.3) — HTML-parsing
  • selenium (4.43.0) — Browserautomatisering, kræver Python 3.10+
  • httpx (0.28.1) — Asynkron HTTP-klient
  • parsel (1.11.0) — CSS/XPath-selectorer (lettere end BS4)
  • pandas (3.0.2) — Dataeksport, kræver Python 3.11+
  • curl_cffi (0.15.0) — TLS-fingerprint impersonation (kritisk for at omgå Cloudflare)

ChromeDriver: Hvis du bruger Selenium, er der gode nyheder — siden Selenium 4.6 downloader og cacher Selenium Manager automatisk den korrekte ChromeDriver-binær. Ingen manuel installation er nødvendig. Versionstilpasningen håndteres dynamisk, så du slipper for at bekymre dig om uoverensstemmelser mellem Chrome-versioner.

Virtuelt miljø (anbefalet):

python -m venv tripadvisor-scraper
source tripadvisor-scraper/bin/activate  # macOS/Linux
tripadvisor-scraper\Scripts\activate     # Windows

Metode 1: Scrape TripAdvisor med Requests og BeautifulSoup

Det her er den enkleste metode. Den fungerer godt til listesider (hotel-søgeresultater, restaurantlister), hvor de data, du skal bruge, ligger i den statiske HTML. Ingen browser, ingen JavaScript-rendering, minimalt ressourceforbrug.

Forstå TripAdvisors URL-mønstre

TripAdvisor-URL'er følger forudsigelige mønstre efter kategori:

  • Hoteller: https://www.tripadvisor.com/Hotels-g{locationId}-{Location_Name}-Hotels.html
  • Restauranter: https://www.tripadvisor.com/Restaurants-g{locationId}-{Location_Name}.html
  • Seværdigheder: https://www.tripadvisor.com/Attractions-g{locationId}-Activities-{Location_Name}.html

Pagination bruger oa-parameteren (offset anchors), som indsættes i URL'en. Hver side viser 30 resultater:

  • Side 1: basis-URL (ingen oa-parameter)
  • Side 2: Hotels-g187768-oa30-Italy-Hotels.html
  • Side 3: Hotels-g187768-oa60-Italy-Hotels.html

For anmeldelsessider er offset-parameteren or med trin på 10:

  • Side 1: Reviews-or0-Hotel_Name.html
  • Side 2: Reviews-or10-Hotel_Name.html

For at få anmeldelser på alle sprog, tilføj ?filterLang=ALL til URL'en.

Send forespørgsler med realistiske headers

TripAdvisor tjekker headers aggressivt. En forespørgsel med standard Python-headers bliver blokeret med det samme. Du skal efterligne en rigtig Chrome-browser:

import requests
import time
import random

session = requests.Session()

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Referer": "https://www.tripadvisor.com/",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
    "Sec-CH-UA-Mobile": "?0",
    "Sec-CH-UA-Platform": '"Windows"',
}

session.headers.update(headers)

url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
response = session.get(url)

print(f"Status: {response.status_code}")
print(f"Content length: {len(response.text)} characters")

Vigtigt: TripAdvisor validerer, at dine User-Agent- og Sec-CH-UA-Client Hints-headers hænger sammen. Hvis du påstår at være Chrome 135 i User-Agent, men din Sec-CH-UA siger Chrome 120, bliver du markeret. Rotér altid hele header-sæt samlet, ikke enkelte headers.

Parse lister med BeautifulSoup

Når du har fået et succesfuldt svar, kan du udtrække data med BeautifulSoup. TripAdvisor bruger data-automation- og data-test-attribute-attributter, som er mere stabile end CSS-klassenavne (der ændrer sig ofte):

from bs4 import BeautifulSoup

soup = BeautifulSoup(response.text, "html.parser")

# Find alle hotelkort
cards = soup.select('div[data-test-attribute="location-results-card"]')

hotels = []
for card in cards:
    # Hotelnavn
    title_el = card.select_one('div[data-automation="hotel-card-title"]')
    name = title_el.get_text(strip=True) if title_el else None

    # Link til detaljeside
    link_el = card.select_one('div[data-automation="hotel-card-title"] a')
    link = "https://www.tripadvisor.com" + link_el["href"] if link_el else None

    # Bedømmelse
    rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
    rating = rating_el.get_text(strip=True) if rating_el else None

    # Antal anmeldelser
    review_el = card.select_one('[data-automation="bubbleReviewCount"]')
    review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None

    hotels.append({
        "name": name,
        "rating": rating,
        "review_count": review_count,
        "url": link,
    })

print(f"Fandt {len(hotels)} hoteller på denne side")
for h in hotels[:3]:
    print(h)

Om selectorer: TripAdvisor bruger obfuskerede CSS-klassenavne (som FGwzt, yyzcQ), der ændrer sig ved hver opdatering af sitet. data-automation- og data-test-target-attributterne er langt mere stabile. Foretræk altid data-attributter frem for klassenavne.

Håndtering af pagination

For at scrape flere sider kan du loop'e over offset-parameteren med en høflig pause mellem forespørgsler:

import pandas as pd

all_hotels = []
base_url = "https://www.tripadvisor.com/Hotels-g187147-oa{offset}-Paris_Ile_de_France-Hotels.html"

for page in range(5):  # Første 5 sider
    offset = page * 30
    url = base_url.format(offset=offset) if page > 0 else "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"

    response = session.get(url)
    if response.status_code != 200:
        print(f"Side {page + 1}: Fik status {response.status_code}, stopper.")
        break

    soup = BeautifulSoup(response.text, "html.parser")
    cards = soup.select('div[data-test-attribute="location-results-card"]')

    for card in cards:
        title_el = card.select_one('div[data-automation="hotel-card-title"]')
        name = title_el.get_text(strip=True) if title_el else None
        rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
        rating = rating_el.get_text(strip=True) if rating_el else None
        review_el = card.select_one('[data-automation="bubbleReviewCount"]')
        review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None

        all_hotels.append({"name": name, "rating": rating, "review_count": review_count})

    print(f"Side {page + 1}: {len(cards)} hoteller fundet")
    time.sleep(random.uniform(3, 7))  # Tilfældig pause for at undgå rate limiting

df = pd.DataFrame(all_hotels)
print(f"\nSamlet antal scrappede hoteller: {len(df)}")

time.sleep(random.uniform(3, 7)) er vigtig. TripAdvisors rate limit ligger cirka på 10–15 forespørgsler i minuttet pr. IP. Hvis du går hurtigere, udløser du CAPTCHA'er eller 429-fejl.

Begrænsninger ved denne metode

Hvornår bryder den sammen? Requests+BS4-metoden fejler, når:

  • TripAdvisor leverer JavaScript-renderet indhold (nogle søgeresultatsider kræver JS)
  • Anmeldelsestekst er afkortet bag "Læs mere"-knapper
  • Anti-bot-foranstaltninger eskalerer til JavaScript-udfordringer eller CAPTCHA'er
  • Du har brug for data, der først vises efter client-side rendering (priser, tilgængelighed)

I de tilfælde skal du enten bruge Selenium (Metode 2) eller den skjulte JSON-metode (Metode 3).

Metode 2: Scrape TripAdvisor med Selenium (headless browser)

Selenium starter en rigtig browser, hvilket betyder, at den kan rendere JavaScript, klikke på knapper, håndtere cookie-samtykke og interagere med dynamisk indhold. Prisen: den er cirka 5 gange langsommere og bruger 300–500 MB RAM pr. browserinstans.

Konfigurér Selenium med anti-detektion-indstillinger

Som standard er Selenium let at opdage. TripAdvisors fingerprinting fanger den med det samme. Du skal deaktivere automatiseringsflag:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = Options()
options.add_argument("--headless=new")  # Brug ny headless-tilstand (Chrome 112+)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--window-size=1920,1080")
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)

driver = webdriver.Chrome(options=options)

# Fjern webdriver-egenskaben fra navigator
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})

Er det nok til TripAdvisor? Til mindre scraping (under 50 sider) fungerer denne opsætning normalt sammen med residential proxies. Ved større mængder kan du få brug for undetected-chromedriver eller nodriver — TripAdvisors DataDome-beskyttelse analyserer over 1.000 signaler pr. forespørgsel, herunder TLS-fingerprints, som almindelig Selenium ikke kan efterligne.

Scrape søgeresultater for hoteller med Selenium

import time
import random

url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
driver.get(url)

# Vent på at hotelkortene indlæses
wait = WebDriverWait(driver, 15)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')))

# Håndter cookie-pop-up (hvis den vises)
try:
    cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
    cookie_btn.click()
    time.sleep(1)
except:
    pass  # Ingen cookie-pop-up

# Udtræk hoteldata
cards = driver.find_elements(By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')

hotels = []
for card in cards:
    try:
        name = card.find_element(By.CSS_SELECTOR, 'div[data-automation="hotel-card-title"]').text
    except:
        name = None
    try:
        rating = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleRatingValue"]').text
    except:
        rating = None
    try:
        reviews = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleReviewCount"]').text
    except:
        reviews = None

    hotels.append({"name": name, "rating": rating, "review_count": reviews})

print(f"Scrapede {len(hotels)} hoteller")
for h in hotels[:3]:
    print(h)

Det tog omkring 8 sekunder for en enkelt side på min maskine — sammenlignet med under 1 sekund med requests+BS4. Den forskel vokser hurtigt, når du scraper hundredvis af sider.

Udvid "Læs mere" og scrape fulde anmeldelser

Anmeldelsessider afkorter lange anmeldelser bag en "Læs mere"-knap. Selenium kan klikke på den:

review_url = "https://www.tripadvisor.com/Hotel_Review-g187147-d188726-Reviews-Le_Marais_Hotel-Paris_Ile_de_France.html"
driver.get(review_url)

wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-reviewid]')))
time.sleep(2)

# Klik alle "Læs mere"-knapper
read_more_buttons = driver.find_elements(By.XPATH, '//button//*[contains(text(), "Læs mere")]/..')
for btn in read_more_buttons:
    try:
        driver.execute_script("arguments[0].click();", btn)
        time.sleep(0.3)
    except:
        pass

# Udtræk anmeldelser
review_elements = driver.find_elements(By.CSS_SELECTOR, 'div[data-reviewid]')

reviews = []
for rev in review_elements:
    try:
        title = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-title"]').text
    except:
        title = None
    try:
        body = rev.find_element(By.CSS_SELECTOR, 'q.IRsGHoPm span').text
    except:
        try:
            body = rev.find_element(By.CSS_SELECTOR, 'p.partial_entry').text
        except:
            body = None
    try:
        rating_class = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-rating"] span').get_attribute("class")
        # Rating er kodet i klassenavn som "ui_bubble_rating bubble_50" = 5.0
        rating_num = [c for c in rating_class.split() if "bubble_" in c][0].replace("bubble_", "")
        rating = int(rating_num) / 10
    except:
        rating = None

    reviews.append({"title": title, "body": body, "rating": rating})

print(f"Scrapede {len(reviews)} anmeldelser")

Tilføj proxy-rotation til Selenium

Ved vedvarende scraping får du brug for proxy-rotation. Da selenium-wire er udfaset siden januar 2024, bør du bruge Chromes indbyggede proxy-understøttelse:

# Uden autentificering på proxy
proxy = "http://your-proxy-address:port"
options.add_argument(f"--proxy-server={proxy}")

# For proxies med autentificering skal du bruge en Chrome-udvidelse eller Selenium 4's BiDi-protokol

Til programmatisk roterende proxies kan du oprette en ny driver-instans med en anden proxy for hver batch forespørgsler. Det er ikke elegant, men det virker stabilt.

Metode 3: Den skjulte JSON-metode (spring HTML-parsing helt over)

De fleste guides springer den metode over, og det er en skam — det er den hurtigste og reneste af de tre. TripAdvisor indlejrer strukturerede data som JSON direkte i HTML-siderne — inde i <script>-tags som JavaScript-variabler som pageManifest og urqlCache. Ved at udtrække denne JSON får du renere data (bedømmelser som tal, datoer i ISO-format) med færre forespørgsler og uden behov for JavaScript-rendering.

Find den indlejrede JSON i sidens kilde

Det centrale trick: du kan bruge et almindeligt requests.get() til at hente siden og derefter udtrække JSON fra den rå HTML uden nogensinde at rendere JavaScript.

import requests
import re
import json

headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Referer": "https://www.tripadvisor.com/",
    "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
    "Sec-CH-UA-Mobile": "?0",
    "Sec-CH-UA-Platform": '"macOS"',
}

url = "https://www.tripadvisor.com/Hotel_Review-g188590-d194317-Reviews-NH_City_Centre_Amsterdam.html"
response = requests.get(url, headers=headers)

# Udtræk pageManifest-JSON-blokken
match = re.search(r"pageManifest:({.+?})};", response.text)
if match:
    page_data = json.loads(match.group(1))
    print("Fandt data i pageManifest")
    print(f"Nøgler: {list(page_data.keys())[:10]}")

Sådan finder du selv variabelnavnet: Åbn en TripAdvisor-hotelside i Chrome, højreklik → Vis sidekilde, og søg derefter med Ctrl+F efter pageManifest eller urqlCache eller aggregateRating. Dataene ligger der og venter på at blive parsede.

Parse JSON og udtræk strukturerede data

TripAdvisor indlejrer også application/ld+json schema.org-data, som er endnu lettere at hente:

from parsel import Selector

sel = Selector(text=response.text)

# Udtræk JSON-LD-strukturerede data
json_ld_scripts = sel.xpath("//script[@type='application/ld+json']/text()").getall()

for script in json_ld_scripts:
    data = json.loads(script)
    if isinstance(data, dict) and data.get("@type") in ["Hotel", "Restaurant", "TouristAttraction"]:
        print(f"Navn: {data.get('name')}")
        print(f"Bedømmelse: {data.get('aggregateRating', {}).get('ratingValue')}")
        print(f"Antal anmeldelser: {data.get('aggregateRating', {}).get('reviewCount')}")
        print(f"Prisniveau: {data.get('priceRange')}")
        print(f"Adresse: {data.get('address', {}).get('streetAddress')}")
        print(f"Koordinater: {data.get('geo', {}).get('latitude')}, {data.get('geo', {}).get('longitude')}")
        break

JSON-LD-dataene ligger indlejret i den statiske HTML og kræver IKKE JavaScript-rendering. De giver dig ejendommens navn, samlet bedømmelse, antal anmeldelser, adresse, koordinater, prisniveau og fotolinks — alt sammen uden at parse et eneste HTML-tag.

For mere detaljerede data (individuelle anmeldelser, fordeling af bedømmelser, facilitetslister) skal du bruge urqlCache-objektet:

# Udtræk urqlCache for detaljerede anmeldelsesdata
cache_match = re.search(r'"urqlCache"\s*:\s*({.+?})\s*,\s*"redux"', response.text)
if cache_match:
    cache_data = json.loads(cache_match.group(1))
    # Gennemgå cachen for at finde anmeldelsesdata
    for key, value in cache_data.items():
        if "reviews" in str(value).lower()[:100]:
            reviews_data = json.loads(value.get("data", "{}")) if isinstance(value, dict) else None
            if reviews_data:
                print(f"Fandt review-cacheindgang: {key[:50]}...")
                break

De præcise JSON-stier ændrer sig fra tid til anden, når TripAdvisor opdaterer frontend, men den overordnede struktur — JSON-LD til opsummerede data, urqlCache til detaljerede data — har været stabil i årevis.

Reverse engineer TripAdvisors GraphQL API (avanceret)

Til storstilet udtrækning returnerer TripAdvisors GraphQL-endpoints strukturerede data direkte. Det er den hurtigste metode, men også den, der kræver mest vedligeholdelse.

import httpx
import random
import string

def generate_request_id():
    """Generér X-Requested-By-headerens værdi"""
    random_chars = ''.join(random.choices(string.ascii_letters + string.digits, k=180))
    return f"TNI1625!{random_chars}"

# Søg efter hoteller i Paris
search_payload = [{
    "variables": {
        "request": {
            "query": "hotels in Paris",
            "limit": 10,
            "scope": "WORLDWIDE",
            "locale": "en-US",
            "scopeGeoId": 1,
            "searchCenter": None,
            "types": ["LOCATION", "QUERY_SUGGESTION", "RESCUE_RESULT"],
            "locationTypes": ["GEO", "AIRPORT", "ACCOMMODATION", "ATTRACTION", "EATERY", "NEIGHBORHOOD"]
        }
    },
    "extensions": {
        "preRegisteredQueryId": "84b17ed122fbdbd4"
    }
}]

graphql_headers = {
    "Content-Type": "application/json",
    "Accept": "*/*",
    "Accept-Language": "en-US,en;q=0.9",
    "Origin": "https://www.tripadvisor.com",
    "Referer": "https://www.tripadvisor.com/Hotels",
    "X-Requested-By": generate_request_id(),
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
}

with httpx.Client() as client:
    response = client.post(
        "https://www.tripadvisor.com/data/graphql/ids",
        json=search_payload,
        headers=graphql_headers
    )
    if response.status_code == 200:
        results = response.json()
        print(json.dumps(results, indent=2)[:1000])
    else:
        print(f"GraphQL-forespørgsel mislykkedes: {response.status_code}")

Til hentning af anmeldelser via GraphQL:

review_payload = [{
    "variables": {
        "locationId": 194317,  # NH City Centre Amsterdam
        "offset": 0,
        "limit": 20,
        "filters": {},
        "sortType": None,
        "sortBy": "date",
        "language": "en",
        "doMachineTranslation": False,
        "photosPerReviewLimit": 3
    },
    "extensions": {
        "preRegisteredQueryId": "ef1a9f94012220d3"
    }
}]

with httpx.Client() as client:
    response = client.post(
        "https://www.tripadvisor.com/data/graphql/ids",
        json=review_payload,
        headers=graphql_headers
    )
    if response.status_code == 200:
        data = response.json()
        reviews = data[0]["data"]["locations"][0]["reviewListPage"]["reviews"]
        total = data[0]["data"]["locations"][0]["reviewListPage"]["totalCount"]
        print(f"Samlet antal anmeldelser: {total}")
        for r in reviews[:3]:
            print(f"  [{r['rating']}/5] {r['title']} - {r['createdDate']}")

Vigtigt forbehold: preRegisteredQueryId-værdierne (som 84b17ed122fbdbd4 til søgning og ef1a9f94012220d3 til anmeldelser) kan gå i stykker, når TripAdvisor redesigner eller deployer nye versioner. Når det sker, fejler dine forespørgsler ofte stille og roligt. Du skal genfinde query IDs ved at overvåge netværksforespørgsler i browserens DevTools.

Hvorfor denne metode reducerer behovet for proxies

Matematikken er enkel. Med requests+BS4 kræver scraping af 100 hotel-detaljesider 100 forespørgsler. Med den skjulte JSON-metode returnerer hver forespørgsel alle de data, du skal bruge, fra én sides indlæsning — ingen ekstra forespørgsler til at udvide anmeldelser eller hente dynamisk indhold. Med GraphQL kan ét API-kald returnere 20 anmeldelser ad gangen. Færre forespørgsler = mindre eksponering for rate limiting = mindre behov for proxy-rotation. Til små og mellemstore projekter (under 1.000 sider) har du måske slet ikke brug for proxies, hvis du tilføjer fornuftige pauser.

Scrape hoteller, restauranter og seværdigheder med ét genbrugeligt script

Fire ud af fem konkurrerende guides dækker kun hoteller. Men TripAdvisor har tre centrale indholdskategorier, og URL-mønstrene og datafelterne er forskellige. Sådan bygger du én funktion, der håndterer dem alle.

Datafelter tilgængelige pr. kategori

FeltHotellerRestauranterSeværdigheder
Navn
Bedømmelse
Antal anmeldelser
Pris/PrisintervalNogle gange
Adresse
Køkkentype
Varighed/turtype
Faciliteter
Koordinater

Byg en genbrugelig scrape_tripadvisor()-funktion

import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
import re
import json

def scrape_tripadvisor(category, location_id, location_name, num_pages=3):
    """
    Scrape TripAdvisor-lister for hoteller, restauranter eller seværdigheder.

    Args:
        category: "hotels", "restaurants" eller "attractions"
        location_id: TripAdvisor geo ID (f.eks. "187147" for Paris)
        location_name: URL-venligt navn (f.eks. "Paris_Ile_de_France")
        num_pages: Antal sider, der skal scrapes
    """
    url_patterns = {
        "hotels": "https://www.tripadvisor.com/Hotels-g{geo}-oa{offset}-{name}-Hotels.html",
        "restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-oa{offset}-{name}.html",
        "attractions": "https://www.tripadvisor.com/Attractions-g{geo}-oa{offset}-Activities-{name}.html",
    }

    first_page_patterns = {
        "hotels": "https://www.tripadvisor.com/Hotels-g{geo}-{name}-Hotels.html",
        "restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-{name}.html",
        "attractions": "https://www.tripadvisor.com/Attractions-g{geo}-Activities-{name}.html",
    }

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Referer": "https://www.tripadvisor.com/",
        "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
        "Sec-CH-UA-Mobile": "?0",
        "Sec-CH-UA-Platform": '"Windows"',
    }

    session = requests.Session()
    session.headers.update(headers)

    all_items = []

    for page in range(num_pages):
        offset = page * 30
        if page == 0:
            url = first_page_patterns[category].format(geo=location_id, name=location_name)
        else:
            url = url_patterns[category].format(geo=location_id, offset=offset, name=location_name)

        response = session.get(url)
        if response.status_code != 200:
            print(f"  Side {page + 1}: Status {response.status_code}, stopper.")
            break

        soup = BeautifulSoup(response.text, "html.parser")
        cards = soup.select('div[data-test-attribute="location-results-card"]')

        for card in cards:
            item = {"category": category}
            title_el = card.select_one('div[data-automation="hotel-card-title"]') or card.select_one('a[data-automation]')
            item["name"] = title_el.get_text(strip=True) if title_el else None
            rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
            item["rating"] = rating_el.get_text(strip=True) if rating_el else None
            review_el = card.select_one('[data-automation="bubbleReviewCount"]')
            item["review_count"] = review_el.get_text(strip=True) if review_el else None
            all_items.append(item)

        print(f"  Side {page + 1}: {len(cards)} elementer fundet")
        time.sleep(random.uniform(3, 7))

    return pd.DataFrame(all_items)

# Eksempler på brug
print("=== Hoteller i Paris ===")
hotels_df = scrape_tripadvisor("hotels", "187147", "Paris_Ile_de_France", num_pages=2)
print(hotels_df.head())

print("\n=== Restauranter i Rom ===")
restaurants_df = scrape_tripadvisor("restaurants", "187791", "Rome_Lazio", num_pages=2)
print(restaurants_df.head())

print("\n=== Seværdigheder i Barcelona ===")
attractions_df = scrape_tripadvisor("attractions", "187497", "Barcelona_Catalonia", num_pages=2)
print(attractions_df.head())

Én funktion, tre kategorier, nul duplikering af kode. Hvis TripAdvisor ændrer en selector, retter du det ét sted.

Hvad gør du, når TripAdvisor blokerer dig? (anti-bot-fejlfinding)

Det her er det afsnit, jeg selv havde mest brug for, da jeg begyndte at scrape TripAdvisor, og det er det afsnit, ingen konkurrerende guide præsenterer struktureret. TripAdvisor bruger DataDome (som analyserer 5+ trillioner datapunkter dagligt) og Cloudflare WAF sammen. Her er en diagnosticeringstabel for de mest almindelige fejltyper:

SymptomSandsynlig årsagLøsning
HTTP 403-svarManglende eller mistænkelige headers; Cloudflare JS-udfordringSæt realistiske User-Agent, Accept-Language, Referer og Sec-CH-UA-headers. Sørg for, at headers er konsistente.
CAPTCHA-side i stedet for dataRate limiting eller browser fingerprintingRotér residential proxies, tilføj tilfældige pauser (2–7 sekunder mellem forespørgsler)
Tom HTML eller tom sidebodyJavaScript ikke rendret af requestsSkift til Selenium eller udtræk fra skjult JSON i sidens kilde
Delvise anmeldelser / "Læs mere" udvider ikkeIndhold indlæses ved klik-eventBrug Seleniums .click() eller udtræk fra indlejret JSON-blok
Anmeldelser kun på ét sprogMangler sprogparameterTilføj ?filterLang=ALL til anmeldelses-URL'en
Data stopper efter N siderSession-baseret rate limitRotér sessioner, ryd cookies mellem batches
HTTP 1020 Access DeniedIP/ASN er blokeret af CloudflareSkift fra datacenter til residential proxies
Uendelig challenge-løkke (CAPTCHA igen og igen)Brudt cookie-persistensVarm sessionen op ved først at besøge forsiden; behold cookie jar

Retry-logik med eksponentiel backoff

Ingen konkurrerende artikel viser faktisk denne kode. Her er en genbrugelig retry-funktion:

import time
import random
import requests

def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
    """
    Hent en URL med eksponentiel backoff og jitter.
    Rotér User-Agent ved hvert retry.
    """
    user_agents = [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
    ]

    for attempt in range(max_retries):
        if attempt > 0:
            session.headers["User-Agent"] = random.choice(user_agents)

        try:
            response = session.get(url, timeout=30)

            if response.status_code == 200:
                return response

            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", base_delay * (2 ** attempt)))
                print(f"  Rate limited (429). Venter {retry_after}s...")
                time.sleep(retry_after)
                continue

            if response.status_code in (403, 503):
                wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
                print(f"  Fik {response.status_code}. Retry {attempt + 1}/{max_retries} om {wait:.1f}s...")
                time.sleep(wait)
                continue

            print(f"  Uventet status {response.status_code} for {url}")
            return response

        except requests.exceptions.Timeout:
            wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
            print(f"  Timeout. Retry {attempt + 1}/{max_retries} om {wait:.1f}s...")
            time.sleep(wait)

    print(f"  Alle {max_retries} retries er opbrugt for {url}")
    return None

Rotér headers, proxies og sessioner

Ved vedvarende scraping skal du vedligeholde en pulje af header-sæt og rotere dem samlet:

import random

HEADER_SETS = [
    {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
        "Sec-CH-UA-Platform": '"Windows"',
    },
    {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
        "Sec-CH-UA-Platform": '"macOS"',
    },
    {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
        "Sec-CH-UA": '"Google Chrome";v="134", "Not-A.Brand";v="8", "Chromium";v="134"',
        "Sec-CH-UA-Platform": '"Windows"',
    },
]

PROXY_LIST = [
    "http://user:pass@residential-proxy-1:port",
    "http://user:pass@residential-proxy-2:port",
    # Tilføj flere residential proxies
]

def get_rotated_session():
    """Opret en ny session med roterede headers og proxy."""
    session = requests.Session()

    header_set = random.choice(HEADER_SETS)
    base_headers = {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
        "Referer": "https://www.tripadvisor.com/",
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-CH-UA-Mobile": "?0",
    }
    base_headers.update(header_set)
    session.headers.update(base_headers)

    if PROXY_LIST:
        proxy = random.choice(PROXY_LIST)
        session.proxies = {"http": proxy, "https": proxy}

    return session

Proxytype betyder noget. Datacenter-proxies bliver næsten øjeblikkeligt blokeret af TripAdvisor (HTTP 1020 Access Denied). Residential proxies er nødvendige til vedvarende scraping — de går gennem almindelige internetudbydere og er svære at skelne fra rigtige brugere. Forvent at betale $2,50–$8,40/GB afhængigt af udbyderen.

Eksport og lagring af dine scrape-de TripAdvisor-data

Når du har dataene, er det ligetil at få dem ind i et brugbart format.

CSV-eksport (mest almindeligt)

import pandas as pd

df = pd.DataFrame(all_hotels)
df.to_csv("tripadvisor_hotels_paris.csv", index=False, encoding="utf-8-sig")
print(f"Eksporterede {len(df)} rækker til CSV")

encoding='utf-8-sig' er vigtigt — det sikrer, at Excel viser ikke-latinske tegn korrekt (franske accenter, kinesiske tegn osv.), når CSV-filen åbnes.

JSON-eksport (til nestede data)

Når du har anmeldelser nestet under hoteller, bevarer JSON hierarkiet:

hotel_data = {
    "property_id": "d194317",
    "name": "NH City Centre Amsterdam",
    "rating": 4.0,
    "reviews": [
        {"title": "Great location", "rating": 5, "date": "2025-03-15", "text": "..."},
        {"title": "Average stay", "rating": 3, "date": "2025-03-10", "text": "..."},
    ]
}

flat_reviews = pd.json_normalize(
    hotel_data,
    record_path="reviews",
    meta=["property_id", "name"]
)
flat_reviews.to_csv("reviews_flat.csv", index=False)

To-filers tilgang til relationelle data

Til store datasæt bruger jeg to CSV-filer:

  • hotels.csv — Én række pr. overnatningssted (flad struktur)
  • reviews.csv — Én række pr. anmeldelse, med property_id som foreign key

Det gør det nemt at joine i pandas, indlæse i en database eller importere i BI-værktøjer.

Hvis du ikke vil bøvle med eksportlogik, kan Thunderbit eksportere scrape-de data direkte til Excel, Google Sheets, Airtable eller Notion — alt gratis, helt uden kode. Praktisk, når du skal dele resultater med kolleger uden teknisk baggrund.

Tips til ansvarlig og effektiv TripAdvisor-scraping

Ansvarlig scraping på seks punkter:

  • Tjek robots.txt: TripAdvisors robots.txt blokerer AI-træningsbotter (GPTBot, ClaudeBot osv.) helt. Almindelige crawlers møder selektive sti-begrænsninger. Se den på tripadvisor.com/robots.txt.
  • Tilføj pauser: 3–7 sekunder mellem forespørgsler er et sikkert interval. Går du hurtigere end 10–15 forespørgsler i minuttet pr. IP, rammer du rate limiting.
  • Scrape kun offentlige data. Log ikke ind for at få adgang til begrænset indhold.
  • Opbevar data sikkert og overhold GDPR/CCPA, hvis du håndterer personoplysninger (anmeldernavne osv.).
  • Overvej TripAdvisors officielle API, hvis du har brug for data i kommerciel skala. Developer Portal giver adgang til virksomhedsoplysninger samt op til 5 anmeldelser og 5 fotos pr. lokation — begrænset, men lovligt og stabilt.
  • Vær opmærksom på den juridiske kontekst: EU-Domstolens Ryanair-dom (december 2025) styrkede ToS-baserede scraping-forbud i EU. TripAdvisors vilkår forbyder eksplicit scraping. Scrape ansvarligt og på eget ansvar.

Afslutning

Det er hele billedet.

  • Requests + BeautifulSoup er den enkleste vej. Den virker til statiske listesider, kræver minimal opsætning og er hurtig. Start her, hvis du scraper færre end 100 sider og ikke har brug for JavaScript-renderet indhold.
  • Selenium håndterer alt det, requests ikke kan: dynamisk indhold, "Læs mere"-knapper og cookie-bannere. Den er 5 gange langsommere og tungere på ressourcer, men den er den eneste mulighed, når du skal interagere med siden.
  • Skjult JSON / GraphQL er den reneste og hurtigste metode. Den giver strukturerede data uden HTML-parsing, reducerer antallet af forespørgsler og dermed behovet for proxies, og returnerer data i et format, der er klar til analyse. Den kræver mere reverse engineering i starten og lejlighedsvis vedligeholdelse, når TripAdvisor ændrer sin datastruktur.

Den genbrugelige scrape_tripadvisor()-funktion dækker hoteller, restauranter og seværdigheder. Du burde ikke få brug for en ekstra tutorial.

Og hvis du undervejs beslutter, at kodning ikke er noget for dig — eller du bare skal have 50 hoteller i et regneark inden fyraften — kan Thunderbits Chrome-udvidelse klare det med to klik med AI-drevet feltdetektion, automatisk pagination og gratis eksport til Excel eller Google Sheets. Ingen Python nødvendig.

Hvis du vil dykke dybere, har vi flere guides på Thunderbit-bloggen og vores YouTube-kanal.

Ofte stillede spørgsmål

1. Er det lovligt at scrape TripAdvisor?

TripAdvisors vilkår for brug forbyder eksplicit scraping. Domstole har dog generelt vurderet, at scraping af offentligt tilgængelige data (ikke bag login) ikke overtræder Computer Fraud and Abuse Act i USA. Når det er sagt, styrkede EU-Domstolens Ryanair-afgørelse i 2025 ToS-baserede begrænsninger i Europa. Scrape kun offentlige data, respekter robots.txt, genudgiv ikke ophavsretligt beskyttet indhold, og tal med en jurist, hvis du bruger data kommercielt.

2. Kan jeg scrape TripAdvisor uden Python?

Ja. No-code-værktøjer som Thunderbit kan scrape TripAdvisor direkte fra din browser med AI-baseret feltgenkendelse og automatisk pagination. Du kan også bruge browserudvidelser, Google Sheets-add-ons eller kommercielle scraping-API'er. Python giver mest kontrol og fleksibilitet, men det er ikke den eneste mulighed.

3. Hvordan undgår jeg at blive blokeret, når jeg scraper TripAdvisor?

De vigtigste taktikker: brug realistiske og konsistente headers (især User-Agent og Sec-CH-UA), rotér residential proxies (datacenter-IP'er bliver blokeret med det samme), tilføj tilfældige pauser på 3–7 sekunder mellem forespørgsler, brug den skjulte JSON-metode for at minimere det samlede antal forespørgsler, implementér retry-logik med eksponentiel backoff, og varm sessionerne op ved at besøge forsiden før dybe sider.

4. Hvilke data kan jeg scrape fra TripAdvisor?

Hoteller, restauranter og seværdigheder — inklusive navne, bedømmelser, antal anmeldelser, prisintervaller, adresser, koordinater, faciliteter (hoteller), køkkentyper (restauranter), turvarigheder (seværdigheder) samt fuld anmeldelsestekst med individuelle bedømmelser og datoer. De skjulte JSON- og GraphQL-metoder giver den rigeste data pr. forespørgsel.

5. Hvor mange sider kan jeg scrape fra TripAdvisor pr. dag?

Med én IP og fornuftige pauser: cirka 600–1.000 sider om dagen. Med 20 roterende residential proxies: cirka 200.000–300.000 sider om dagen ved request-baserede metoder. Selenium er langsommere — regn med 8.000–12.000 sider om dagen pr. proxy. Den skjulte JSON/GraphQL-metode giver dig mest data pr. forespørgsel, så du kan få den samme mængde information med langt færre sider.

Læs mere

Ke
Ke
CTO hos Thunderbit | Senior Data Scientist og ML-ekspert Med næsten et årtis erfaring inden for machine learning og data science er Ke Shen tidligere studerende fra Columbia University og tidligere Senior Data Scientist hos Walmart Labs. Med dyb, anerkendt ekspertise i Python, R, Java og statistik deler han afprøvede indsigter i, hvordan komplekse AI-algoritmer føres fra teori til produktionsklar arkitektur.
Indholdsfortegnelse

Hent en webside bare ved at spørge

Sig, hvad du har brug for, på helt almindeligt engelsk. Eller endnu bedre: sig ingenting.

Prøv Thunderbit gratis
Udtræk data med AI
Overfør nemt data til Google Sheets, Airtable eller Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week