Yelp에는 에 걸쳐 가 쌓여 있지만, 이 데이터를 실제로 쓸 수 있는 형태로 가져오는 일은 예전보다 훨씬 까다로워졌습니다. Yelp의 2024~2025년 안티봇 단속 때문에, 기존 Python 스크래핑 튜토리얼 대부분은 조용히 쓸모가 없어졌습니다.
최근에 Yelp 스크래퍼를 돌려보다가 403 오류, 빈 HTML 응답, 혹은 몇 달 전에는 없던 CAPTCHA에 막혔다면, 그건 기분 탓이 아닙니다. Yelp는 이제 TLS/JA3 지문 인식, 계속 바뀌는 난독화된 CSS 클래스명, 공격적인 IP 평판 점수화까지 적용하고 있습니다. 즉, 여전히 많은 튜토리얼이 추천하는 requests + BeautifulSoup 방식은 첫 요청부터 바로 실패합니다. 저는 몇 주 동안 Yelp의 현재 구조를 상대로 여러 방법을 테스트했고, 이 가이드에는 2025년에 실제로 통하는 내용만 담았습니다. 공식 Fusion API(왜 이것만으로는 부족한지까지), 계층형 차단 방지 전략을 적용한 완전한 Python 스크래핑 워크플로우, 그리고 디버깅 지옥 없이 데이터를 바로 얻고 싶은 분들을 위한 Thunderbit의 2클릭 노코드 대안까지 모두 소개합니다.
왜 Python으로 Yelp를 스크래핑해야 할까? 그리고 실제로 누가 이득을 볼까?
코드를 한 줄도 쓰기 전에 먼저 생각해볼 질문이 있습니다. 왜 Yelp 데이터가 필요한가요? 이 플랫폼은 단순한 맛집 리뷰 사이트가 아닙니다. 연락처 정보, 평점, 카테고리, 영업시간, 그리고 수억 건의 고객 리뷰를 구조화해 담고 있는, 사실상 지역 비즈니스 라이브 데이터베이스에 가깝습니다.

가장 큰 이득을 보는 사람들과 그들이 얻는 데이터는 다음과 같습니다.
| 활용 사례 | 핵심 데이터 항목 | 중요한 이유 |
|---|---|---|
| 영업 및 리드 발굴 | 업체명, 전화번호, 웹사이트, 주소, 카테고리, 평점 | 지역 중소기업 대상의 정밀한 잠재고객 목록 구축 — Yelp 사용자의 5명 중 4명은 구매 가능성이 높은 상태로 방문 |
| 경쟁사 분석 | 리뷰, 별점, 리뷰 수, 감성 | 경쟁사 평판 모니터링, 서비스 공백 파악, 트렌드 추적 |
| 시장 조사 및 NLP | 전체 리뷰 텍스트, 날짜, 작성자 메타데이터 | 감성 분석, 토픽 모델링 — Yelp 리뷰는 학술 연구에서 가장 많이 쓰이는 NLP 말뭉치 중 하나 |
| 부동산 및 입지 선정 | 지역별 비즈니스 밀도, 카테고리 구성, 리뷰 품질 | 프랜차이즈 및 리테일 입지 선정 — Yelp는 이를 위해 Location Intelligence를 라이선스 B2B 상품으로 판매 |
| 이커머스 및 운영 | 가격 신호, 고객 불만, 서비스 시간 | 경쟁사가 어떻게 평가받는지 추적하고 운영 패턴 파악 |
핵심은 하나입니다. 진짜 목표는 구조화된 데이터이며, Python은 그 목적에 도달하는 여러 방법 중 하나일 뿐입니다. 어떤 분은 완전한 프로그래밍 제어가 필요하고, 어떤 분은 오스틴 지역 배관업체 연락처가 담긴 스프레드시트만 있으면 됩니다. 이 글은 두 경우 모두를 다룹니다.
Yelp Fusion API vs. Python 웹 스크래핑: 무엇을 선택해야 할까?
대부분의 가이드는 이 중요한 판단을 건너뛰고, 공식 (현재는 "Yelp Places API"로 리브랜딩됨)만으로도 충분했는지 따지지 않은 채 바로 코드부터 보여줍니다. 제 경험상 이 비교를 먼저 하면 수많은 시행착오를 줄일 수 있습니다. API는 어떤 용도에는 훌륭하지만, 다른 용도에는 완전히 부족합니다.
Fusion API가 실제로 제공하는 것
Fusion API는 구조화된 비즈니스 검색, 비즈니스 상세 정보, 자동완성, 그리고 리뷰 엔드포인트를 제공합니다. 공식 허가가 있고 문서도 잘 되어 있으며, 안티봇 우회를 따로 고민할 필요도 없습니다.
하지만 리뷰 엔드포인트에서 문제가 생깁니다. Yelp 직원이 GitHub에서 직접 확인한 내용은 다음과 같습니다.
"Yelp API는 전체 리뷰 텍스트를 반환하지 않습니다. 기본적으로 160자 분량의 리뷰 발췌문 3개만 제공됩니다." —
이건 버그가 아니라 설계입니다. API는 리뷰 발췌문 3개(프리미엄은 7개)로 제한되며, 각 항목도 약 160자 수준으로 잘립니다. 리뷰 메타데이터(useful/funny/cool 투표), 작성자 이력, 업주 답글도 없습니다. 게다가 , 예전의 5,000회보다 훨씬 줄었습니다. 시작 요금은 부터입니다.
선택 기준
| 기준 | Yelp Fusion API | Python 웹 스크래핑 | Thunderbit(노코드) |
|---|---|---|---|
| 전체 리뷰 | ❌ 3개 발췌문만 제공(~160자씩) | ✅ GraphQL로 전체 리뷰 가능 | ✅ 화면에 보이는 모든 리뷰 |
| 속도 제한 | 신규 300~500/일; 레거시 5,000 | 자체 관리(프록시 비용 필요) | 크레딧 기반 |
| 세팅 시간 | 약 15분(API 키 + SDK) | 수시간~수일 | 약 2분 |
| 비즈니스 필드 | 약 20개 구조화 필드 | 제한 없음(HTML/JSON 파싱) | AI가 제안하는 필드 |
| 안티봇 대응 | 해당 없음(공식 승인) | 직접 구현해야 함 | 자동 처리 |
| 법적 리스크 | ✅ 승인됨 | ⚠️ ToS 회색지대 | ⚠️ 스크래핑과 동일 |
| 비용 | 최소 월 $29 | 무료(+ 프록시 비용 $0.75~$4/GB) | 무료 플랜 있음 |
| 유지보수 | 낮음(API 안정적) | 높음(셀렉터가 깨지고 안티봇 강화) | 낮음(AI가 재적응) |
Fusion API를 쓰면 좋은 경우: 기본 비즈니스 정보만 필요하거나, 소규모 조회가 목적이거나, 공식 승인된 연동이 필요하고, 비즈니스당 리뷰 조각 3개면 충분할 때.
Python 스크래핑이 필요한 경우: 전체 리뷰 텍스트, 한 업체의 모든 리뷰, 리뷰 메타데이터, 검색 결과 240개 초과, 또는 예산이 월 $29 미만일 때.
Thunderbit를 쓰면 좋은 경우: 코드를 작성하거나 유지보수하지 않고도 빠르게 데이터를 얻고 싶을 때. 자세한 내용은 아래 노코드 섹션에서 설명합니다.
노코드 지름길: Thunderbit로 Yelp 스크래핑하기(Python 불필요)
Python 심화 설명에 들어가기 전에, 실제 목표가 코딩 연습이 아니라 데이터 확보인 독자들을 위한 가장 빠른 방법부터 소개합니다. 경쟁사 가이드는 대부분 Python 숙련도를 전제로 하지만, Thunderbit에서 일하면서 저는 "Yelp 스크래핑"을 검색하는 사람들 중 상당수가 영업 담당자, 운영 매니저, 소상공인이라는 점을 확인했습니다. 이들은 TLS 지문 인식 강의를 원하는 게 아니라, 지역 업체 정보가 들어 있는 스프레드시트가 필요할 뿐입니다.
에는 이미 Yelp용 템플릿이 준비되어 있습니다.
- — 업체명, 평점, 연락처, 주소, 영업시간, 카테고리 추출
- — 리뷰 작성자, 리뷰 내용, 평점, 날짜, 작성자 위치 추출
실제 사용 방법
- Chrome에서 Yelp 검색 결과 페이지 또는 비즈니스 페이지를 엽니다
- 에서 AI Suggest Fields를 클릭하면 AI가 페이지를 읽고 컬럼을 제안합니다(업체명, 평점, 리뷰 수, 가격대, 카테고리, 주소, 전화번호, URL 등)
- Scrape를 클릭하면 끝입니다
미리 만들어진 Yelp 템플릿을 쓰면 더 간단합니다. 템플릿을 열고 Scrape만 누르면 됩니다.
서브페이지 스크래핑은 정보 보강 루프를 자동으로 처리합니다. Yelp 검색 결과 페이지에서 시작해 서브페이지 스크래핑을 켜면, Thunderbit가 각 업체 페이지를 방문해 영업시간, 전체 리뷰, 웹사이트, 사진, 편의시설까지 가져옵니다. 추가 설정은 필요 없습니다.
페이지네이션도 자동 처리됩니다. 클릭 방식이든 스크롤 방식이든 기본 지원합니다. 자세한 내용은 를 참고하세요.
모든 플랜에서 내보내기가 무료입니다 — Excel, Google Sheets, Airtable, Notion, CSV, JSON 지원. pandas도, CSV 작성 코드도 필요 없습니다.
시간 비교
| 시간 | Python 스크래퍼 | Thunderbit |
|---|---|---|
| 첫 실행 | 수시간~수일(셀렉터 작성, 페이지네이션, 프록시, 재시도 로직 처리) | 미리 만들어진 Yelp 템플릿으로 약 30초 |
| Yelp 마크업 변경 시 | 셀렉터를 수동으로 다시 작성 | AI Suggest Fields를 다시 클릭하면 자동 재적응 |
| IP 차단 시 | 디버깅, 프록시 풀 교체, 재테스트 | 클라우드 모드가 IP 회전 처리 |
| Google Sheets로 내보내기 | OAuth + pandas 연결 코드 작성 | 한 번 클릭, 무료 |
Thunderbit를 먼저 써봤는데 요구사항을 다 충족한다면, 이 글의 나머지는 건너뛰어도 됩니다. 하지만 완전한 프로그래밍 제어, 맞춤 필드, 월 수천 건 이상의 확장이 필요하다면 계속 읽어보세요.
Yelp 스크래핑에 사용할 Python 라이브러리 고르기
"Scrapy를 써야 할까, BS4+requests를 써야 할까, 아니면 Selenium이 나을까?"는 r/webscraping에서 Yelp 관련 글마다 자주 나오는 질문입니다. 그런데 대부분의 튜토리얼은 자기들이 좋아하는 라이브러리만 고르고 이유 설명은 생략합니다. 솔직하게 정리해보겠습니다.
2025년의 현실: requests + BeautifulSoup는 Yelp에서 깨진다
정석 튜토리얼이 항상 추천하는 스택 — pip install requests beautifulsoup4 — 은 2025년 Yelp에서 첫 요청부터 차단됩니다. 50번째가 아니라 첫 번째 요청에서 막힙니다.
이유는 간단합니다. Python의 requests 라이브러리가 내보내는 TLS/JA3 지문은 실제 브라우저와 일치하지 않습니다. Yelp의 안티봇 계층은 User-Agent를 읽기도 전에 TLS 핸드셰이크 단계에서 이를 감지합니다. 저는 새 IP, 그럴듯한 헤더, 랜덤 지연까지 넣어 반복 테스트했지만, 기본 requests만으로는 여전히 즉시 403 Forbidden을 받았습니다.
라이브러리 선택표
| 라이브러리 | 적합한 용도 | JS 처리 | 안티봇 대응 | 학습 난이도 | 속도 |
|---|---|---|---|---|---|
requests + BeautifulSoup | ❌ | ❌ | 매우 낮음 | 빠름(차단 전까지) | |
httpx async + parsel | 대규모 비동기 스크래핑 | ❌ | ❌ | 낮음 | 매우 빠름 |
curl_cffi + parsel | Yelp 특화: TLS impersonation | ❌ | ✅ TLS/JA3/HTTP2 | 낮음 | 매우 빠름 |
Scrapy 2.14 | 페이지네이션이 있는 전체 크롤링 파이프라인 | 부분적(scrapy-playwright 통해) | AutoThrottle, retry middleware | 중간~높음 | 빠름 |
Selenium 4.43 / Playwright 1.58 | JS가 많은 페이지, CAPTCHA 우회 | ✅ | 부분적 | 중간 | 느림( |
| Thunderbit | 비개발자, 빠른 추출 | ✅(브라우저) | 내장(클라우드 모드) | 매우 낮음 | 빠름 |
curl_cffi가 바꾼 것
제 Yelp 스크래핑 방식을 가장 크게 바꾼 라이브러리는 입니다. 이는 curl-impersonate용 Python 바인딩으로, 실제 Chrome과 완전히 같은 TLS/JA3 + HTTP/2 지문을 내보내며, API도 requests와 거의 그대로 바꿔 쓸 수 있습니다.
1from curl_cffi import requests
2r = requests.get(
3 "https://www.yelp.com/biz/some-restaurant",
4 impersonate="chrome131",
5)
6print(r.status_code, len(r.text))
이 한 가지 변화 — from curl_cffi import requests와 impersonate="chrome131" 추가 — 만으로 브라우저를 띄우지 않고도 Yelp의 을 우회할 수 있습니다. 제 테스트에서는 즉시 403이 뜨느냐, 깔끔하게 200 응답을 받느냐의 차이였습니다.
2025년 Yelp에 추천하는 스택: curl_cffi + parsel + jmespath + residential proxies. 스케줄링이 포함된 전체 크롤링 파이프라인이 필요하다면, curl_cffi 기반 downloader middleware를 붙여 Scrapy 2.14로 감싸면 됩니다.
Yelp 스크래핑을 위한 Python 환경 설정
- 난이도: 중급
- 소요 시간: 세팅 약 15분, 작동하는 스크래퍼 완성까지 1~2시간
- 준비물: Python 3.10 이상(3.12 권장), 터미널, 그리고 선택적으로 residential proxy 제공업체
1단계: 가상 환경 만들고 패키지 설치하기
1python3.12 -m venv .venv
2source .venv/bin/activate # Windows에서는: .venv\Scripts\activate
3pip install "curl_cffi>=0.11" "parsel>=1.9" "jmespath>=1.0" pandas
각 패키지의 역할은 다음과 같습니다.
curl_cffi— Chrome의 TLS 지문으로 HTTP 요청을 보냅니다(안티봇 우회 핵심)parsel— HTML 파싱용 CSS/XPath 셀렉터( Scrapy와 같은 엔진, 더 가벼움)jmespath— 선언형 JSON 쿼리( Yelp의 내장 JSON을 다룰 때 중첩 딕셔너리보다 훨씬 깔끔함)pandas— CSV/Excel로 내보내기
선택 사항이지만 유용한 패키지:
1pip install fake-useragent # 참고: 저장소는 2026년 4월 보관되었지만 여전히 설치 가능
단계별 가이드: Python으로 Yelp를 스크래핑하는 방법
이 부분이 핵심 튜토리얼입니다. 모든 것을 더 탄탄하게 만드는 핵심 포인트는 이것입니다. CSS 셀렉터를 건너뛰고 숨겨진 JSON을 직접 가져오세요. Yelp는 빌드 시점마다 CSS 클래스명을 랜덤화합니다(y-css-14xwok2였다가 다음 주엔 y-css-hcq7b9로 바뀜). 그래서 이 클래스명에 고정된 스크래퍼는 몇 주 안에 깨집니다. 반면 내장 JSON 페이로드인 application/ld+json 스키마와 react-root-props는 안정적입니다.
2단계: Yelp 검색 결과 스크래핑
Yelp 검색 URL은 예측 가능한 형식을 따릅니다: https://www.yelp.com/search?find_desc={term}&find_loc={location}. 검색 결과 데이터는 CSS 클래스 덩어리 안이 아니라 <script data-id="react-root-props"> 태그 안에 JSON으로 들어 있습니다.
1import re, json, jmespath
2from curl_cffi import requests
3from parsel import Selector
4HEADERS = {
5 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
6 "AppleWebKit/537.36 (KHTML, like Gecko) "
7 "Chrome/124.0.0.0 Safari/537.36",
8 "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
9 "image/avif,image/webp,image/apng,*/*;q=0.8",
10 "accept-language": "en-US,en;q=0.9",
11 "accept-encoding": "gzip, deflate, br",
12 "cookie": "intl_splash=false",
13}
14def scrape_search(term: str, location: str, max_pages: int = 3):
15 results = []
16 for page in range(max_pages):
17 url = (f"https://www.yelp.com/search?"
18 f"find_desc={term}&find_loc={location}&start={page * 10}")
19 r = requests.get(url, headers=HEADERS, impersonate="chrome131")
20 if r.status_code != 200:
21 print(f"페이지 {page}에서 차단됨: {r.status_code}")
22 break
23 sel = Selector(text=r.text)
24 script = sel.xpath(
25 "//script[@data-id='react-root-props']/text()"
26 ).get() or ""
27 m = re.search(r"react_root_props\s*=\s*(\{.*?\});", script, re.S)
28 if not m:
29 print(f"페이지 {page}에서 react-root-props를 찾지 못함 — 소프트 차단 가능성")
30 break
31 data = json.loads(m.group(1))
32 businesses = jmespath.search(
33 "legacyProps.searchAppProps.searchPageProps"
34 ".mainContentComponentsListProps"
35 "[?searchResultBusiness].searchResultBusiness.{"
36 "name: name, url: businessUrl, rating: rating, "
37 "reviews: reviewCount, phone: phone, "
38 "neighborhoods: neighborhoods}",
39 data,
40 ) or []
41 results.extend(businesses)
42 import time, random
43 time.sleep(random.uniform(3, 7))
44 return results
이 코드를 실행하면 업체명, URL, 평점, 리뷰 수가 들어 있는 dict 리스트를 반환받게 됩니다. 응답에서 react-root-props가 없다면 블록용 빈 껍데기만 받은 것이므로, IP를 바꾸고 다시 시도해야 합니다.
Cookie: intl_splash=false 헤더는 Yelp의 국가 스플래시 리다이렉트를 우회하기 위한 표준 방법입니다. 이 헤더가 없으면 미국 외 IP가 스플래시 페이지로 이동하는데, 소프트 블록처럼 보이지만 실제 차단은 아닙니다.
3단계: Yelp 비즈니스 페이지 스크래핑
검색 결과에서 얻은 각 비즈니스 URL은 더 풍부한 데이터가 있는 상세 페이지로 연결됩니다. 가장 안정적인 추출 대상은 <script type="application/ld+json"> 블록입니다. 여기에는 Yelp가 SEO를 위해 유지하는 구조화된 schema.org 데이터가 들어 있고, 난독화도 거의 없습니다.
1def scrape_business(biz_url: str) -> dict:
2 url = f"https://www.yelp.com{biz_url}" if biz_url.startswith("/") else biz_url
3 r = requests.get(url, headers=HEADERS, impersonate="chrome131")
4 if r.status_code != 200:
5 return {"url": url, "error": r.status_code}
6 sel = Selector(text=r.text)
7 biz_id = sel.css('meta[name="yelp-biz-id"]::attr(content)').get()
8 for raw in sel.css('script[type="application/ld+json"]::text').getall():
9 try:
10 data = json.loads(raw)
11 except json.JSONDecodeError:
12 continue
13 for node in (data if isinstance(data, list) else [data]):
14 if node.get("@type") in (
15 "Restaurant", "LocalBusiness", "FoodEstablishment",
16 "HealthAndBeautyBusiness", "HomeAndConstructionBusiness",
17 ):
18 return {
19 "biz_id": biz_id,
20 "name": node.get("name"),
21 "rating": (node.get("aggregateRating") or {}).get("ratingValue"),
22 "review_count": (node.get("aggregateRating") or {}).get("reviewCount"),
23 "address": node.get("address"),
24 "telephone": node.get("telephone"),
25 "price_range": node.get("priceRange"),
26 "hours": node.get("openingHours"),
27 "url": url,
28 }
29 return {"biz_id": biz_id, "url": url}
meta[name="yelp-biz-id"] 값은 리뷰 엔드포인트에 필요할 인코딩된 비즈니스 ID입니다. 다음 단계에서 사용할 것이므로 여기서 꼭 가져오세요.
4단계: 페이지네이션으로 Yelp 리뷰 스크래핑하기
여기서 Fusion API의 한계가 드러나고 스크래핑의 장점이 빛납니다. Yelp 내부 GraphQL 배치 엔드포인트는 전체 리뷰 텍스트, 작성자 정보, 날짜, 평점, 투표 수까지 반환합니다. API가 숨기는 모든 것이 여기에 있습니다.
엔드포인트는 https://www.yelp.com/gql/batch이며, GetBusinessReviewFeed 작업에 대한 정적 documentId를 사용합니다. 페이지네이션은 base64로 인코딩된 cursor를 통해 이루어집니다.
1import base64
2GQL_URL = "https://www.yelp.com/gql/batch"
3DOC_ID = "ef51f33d1b0eccc958dddbf6cde15739c48b34637a00ebe316441031d4bf7681"
4def fetch_reviews(enc_biz_id: str, num_pages: int = 5):
5 all_reviews = []
6 for page in range(num_pages):
7 offset = page * 10
8 cursor = base64.b64encode(
9 json.dumps({"version": 1, "offset": offset}).encode()
10 ).decode()
11 payload = [{
12 "operationName": "GetBusinessReviewFeed",
13 "variables": {
14 "encBizId": enc_biz_id,
15 "reviewsPerPage": 10,
16 "after": cursor,
17 "sortBy": "DATE_DESC",
18 "language": "en",
19 },
20 "extensions": {
21 "operationType": "query",
22 "documentId": DOC_ID,
23 },
24 }]
25 r = requests.post(
26 GQL_URL,
27 json=payload,
28 headers={
29 **HEADERS,
30 "content-type": "application/json",
31 "x-apollo-operation-name": "GetBusinessReviewFeed",
32 "apollographql-client-name": "yelp-main-frontend",
33 },
34 impersonate="chrome131",
35 )
36 if r.status_code != 200:
37 print(f"오프셋 {offset}에서 리뷰 가져오기 실패: {r.status_code}")
38 break
39 data = r.json()
40 # 응답 구조를 따라가 리뷰 추출
41 try:
42 reviews = data[0]["data"]["business"]["reviews"]["edges"]
43 for edge in reviews:
44 node = edge.get("node", {})
45 all_reviews.append({
46 "reviewer": node.get("author", {}).get("displayName"),
47 "rating": node.get("rating"),
48 "date": node.get("localizedDate"),
49 "text": node.get("text", {}).get("full"),
50 })
51 except (KeyError, IndexError, TypeError):
52 break
53 import time, random
54 time.sleep(random.uniform(3, 7))
55 return all_reviews
각 페이지는 리뷰 10개씩 반환합니다. base64 cursor의 offset을 늘리면 페이지네이션이 됩니다. sortBy는 DATE_DESC(최신순), RATING_ASC, RATING_DESC 등 여러 값을 지원합니다.
5단계: 스크래핑한 Yelp 데이터 내보내기
1import pandas as pd
2# businesses와 reviews를 수집했다고 가정
3 df_businesses = pd.DataFrame(businesses)
4df_businesses.to_csv("yelp_businesses.csv", index=False)
5df_reviews = pd.DataFrame(all_reviews)
6df_reviews.to_csv("yelp_reviews.csv", index=False)
7# 유연하게 쓰려면 JSON으로 저장
8import json
9with open("yelp_data.json", "w") as f:
10 json.dump({"businesses": businesses, "reviews": all_reviews}, f, indent=2)
노코드 방식을 선호하는 독자라면 Thunderbit가 같은 데이터를 Excel, Google Sheets, Airtable, Notion으로 바로 내보내줍니다. pandas도, 파일 쓰기 코드도 필요 없습니다.
차단 없이 Yelp를 스크래핑하는 안티블록 전략
이 섹션이 이 글이 존재하는 이유입니다. Yelp의 안티봇 방어는 2024년 말 이후 크게 강화되었습니다. 이 모두 작동 중입니다. 기존 가이드가 구식인 이유는, 이 단속 이전에 작성된 경우가 많기 때문입니다.

전략은 층층이 쌓는 방식입니다. 각 층이 차단 확률을 낮추고, 함께 적용되면 지속 가능한 스크래핑이 가능합니다.
1층: 현실적인 요청 헤더
기본 Python requests 헤더는 User-Agent: python-requests/2.x를 보냅니다. 그러면 바로 차단됩니다. 하지만 현실적인 User-Agent만으로는 충분하지 않습니다. Yelp는 전체 헤더 세트의 일관성도 검사합니다.
1FULL_HEADERS = {
2 "authority": "www.yelp.com",
3 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
4 "AppleWebKit/537.36 (KHTML, like Gecko) "
5 "Chrome/124.0.0.0 Safari/537.36",
6 "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
7 "image/avif,image/webp,image/apng,*/*;q=0.8",
8 "accept-language": "en-US,en;q=0.9",
9 "accept-encoding": "gzip, deflate, br",
10 "sec-ch-ua": '\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"',
11 "sec-ch-ua-mobile": "?0",
12 "sec-ch-ua-platform": '\"Windows\"',
13 "sec-fetch-dest": "document",
14 "sec-fetch-mode": "navigate",
15 "sec-fetch-site": "same-origin",
16 "sec-fetch-user": "?1",
17 "upgrade-insecure-requests": "1",
18 "referer": "https://www.yelp.com/",
19 "cookie": "intl_splash=false",
20}
차단되는 흔한 실수 3가지:
- UA는 Chrome인데
sec-ch-ua가 없거나 UA 버전과 충돌함 sec-ch-ua-platform은 "Windows"인데 UA 문자열은 macOS를 가리킴- 수천 건의 요청에서 완전히 동일한 UA 사용 — 최근 Chrome/Firefox/Safari 문자열 10~20개 풀을 돌려 써야 함
2층: 속도 제한과 랜덤 지연
예측 가능한 타이밍 패턴은 경고 신호입니다. 랜덤 sleep 간격을 넣고, 오류 응답에는 지수 백오프를 적용하세요.
1import random, time
2def polite_get(client_get, url, attempt=0):
3 r = client_get(url, headers=FULL_HEADERS, impersonate="chrome131")
4 if r.status_code in (403, 429, 503):
5 if attempt >= 4:
6 raise RuntimeError(f"{url}에서 {attempt + 1}회 시도 후 차단됨")
7 backoff = 2 ** (attempt + 1) + random.random()
8 print(f" {r.status_code} 응답, {backoff:.1f}초 백오프 중(시도 {attempt + 1})")
9 time.sleep(backoff)
10 return polite_get(client_get, url, attempt + 1)
11 time.sleep(random.uniform(3, 7))
12 return r
| 매개변수 | 권장 값 |
|---|---|
| 요청 간 랜덤 sleep | random.uniform(3, 7)초 |
| 429/403/503 백오프 | 2 → 4 → 8 → 16초, 최대 5회 |
| IP당 동시 작업자 수 | 1개(IP별 직렬 처리; 병렬화는 프록시로) |
| residential IP의 지속 가능 요청 속도 | 약 1요청/5초(분당 약 12회) |
3층: User-Agent 및 세션 회전
실제 브라우저 User-Agent 문자열 풀을 돌려 사용하세요. 세션과 쿠키를 유지해 실제 브라우징 행동처럼 보이게 해야 합니다. Yelp는 쿠키 기반 탐지를 사용하므로, 요청마다 새 세션을 만드는 것 자체가 의심 신호입니다.
1UA_POOL = [
2 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
3 "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
4 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0",
5 "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.4; rv:125.0) Gecko/20100101 Firefox/125.0",
6 "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 Safari/17.4.1",
7 # 최근 문자열 5~10개 추가
8]
4층: 프록시 회전
실제 규모로 운영하려면 residential proxy가 필요합니다. 데이터센터 프록시와 무료 프록시는 Yelp에서 작동하지 않습니다. Yelp의 IP 평판 계층이 AWS, GCP, DigitalOcean IP 대역을 선제적으로 403 처리하기 때문입니다.
| 제공업체 | 초기 $/GB | 비고 |
|---|---|---|
| IPRoyal | $1.75/GB | 가장 저렴한 편; 가장 많이 인용되는 Yelp 튜토리얼을 운영 |
| Decodo(구 Smartproxy) | $3.20~$3.50 | 대량 사용 시 GB당 가성비 우수 |
| Bright Data | $4.00(PAYG) | 1억 5천만+ IP 풀; Yelp 전용 프록시 페이지 제공 |
| Oxylabs | $6.00~$8.00 | 프리미엄; 1천만+ IP |
| Aluvia(모바일 SIM) | $3.00 | 실제 미국 통신사 모바일 IP, Yelp 대상 포지셔닝 |
Rotating residential(요청마다 새 IP 사용)은 대규모 검색 크롤링에 가장 적합합니다. Sticky session(하나의 IP를 10분간 유지)은 비즈니스 페이지 → 리뷰 → 페이지네이션 흐름에서 쿠키를 유지할 때 더 좋습니다.
5층: 차단 탐지 및 대응
모든 차단이 같은 모습은 아닙니다. Yelp는 CAPTCHA 대신 일반적인 "page not available" 셸만 반환하는 경우가 많습니다. 그래서 순진한 스크래퍼는 데이터가 들어왔다고 착각하지만 실제로는 빈 응답을 받은 것일 수 있습니다.
1BLOCK_MARKERS = (
2 "captcha", "px-captcha", "page not available",
3 "access denied", "unusual traffic",
4)
5def is_blocked(resp):
6 if resp.status_code in (401, 403, 429, 503):
7 return True
8 body = resp.text.lower()
9 if any(m in body for m in BLOCK_MARKERS):
10 return True
11 # 검색/비즈니스 페이지인데 react-root-props가 없다면,
12 # Yelp가 내용을 제거한 차단 응답을 보낸 것
13 if "react-root-props" not in body and "/biz/" in str(resp.url):
14 return True
15 return False
| 신호 | 의미 |
|---|---|
| HTTP 403 | 강한 차단 — IP/헤더/TLS가 모두 노출됨 |
| HTTP 429 | 속도 제한 — 백오프로 복구 가능한 경우가 많음 |
| HTTP 503 | 일반 차단 또는 과부하 처리 |
/error로 리다이렉트되거나 본문에 "page not available" 표시 | 소프트 블록 |
| 가 비어 있고 | JS 대기형 챌린지 페이지 |
본문에 captcha / g-recaptcha / px-captcha 존재 | 차단 심화 — CAPTCHA 필요 |
목록 페이지에서 react-root-props가 없음 | 내용이 제거된 차단 응답 |
6층: 더 견고한 파싱 트릭 — CSS 셀렉터 대신 숨겨진 JSON 사용
다시 강조합니다. Yelp는 빌드 시점마다 CSS 클래스명을 랜덤화합니다. h3.y-css-14xwok2에 고정된 스크래퍼는 Yelp가 h3.y-css-hcq7b9로 재배포하는 순간 몇 주 안에 깨집니다.
그렇지 않은 페이로드는 다음과 같습니다.
<script type="application/ld+json">— schema.org 구조화 데이터(이름, 주소, 전화번호, 평점, 영업시간)<script data-id="react-root-props">— 검색 결과 전체를 담은 JSONhttps://www.yelp.com/gql/batch— 안정적인documentId를 사용하는 GraphQL 리뷰 엔드포인트
CSS 클래스를 파싱하는 것은 모래 위에 집을 짓는 것과 같습니다. JSON을 파싱하세요.
7층: 스텔스 브라우저 대체 경로
curl_cffi + residential proxy만으로도 안 될 때만 헤드리스 브라우저를 쓰세요. 보통은 Yelp가 JavaScript 챌린지 페이지나 CAPTCHA를 보여줄 때입니다.
비즈니스/검색/리뷰 스크래핑의 95%는 curl_cffi + 숨겨진 JSON + residential proxy 조합이 브라우저보다 빠르고, 저렴하고, 더 안정적입니다. 하지만 브라우저가 꼭 필요하다면:
| 도구 | 상태(2025) | 비고 |
|---|---|---|
| rebrowser-playwright | 권장 시작점 | CDP 누출을 수정한 패치형 Playwright |
| nodriver | Chrome 스텔스 최상급 | undetected-chromedriver의 후속, WebDriver 프로토콜 자체를 우회 |
| patchright | 활발히 유지보수되는 Playwright 포크 | 최신 탐지 테스트 통과 |
| playwright-stealth | 성숙함 | navigator.webdriver 패치, UA에서 HeadlessChrome 제거 |
Yelp에는 기본 Selenium을 쓰지 마세요. 지문이 너무 쉽게 드러납니다.
Yelp Fusion API vs. Python 스크래핑 vs. Thunderbit: 완전 비교
| 항목 | Yelp Fusion API | Python 스크래핑 | Thunderbit |
|---|---|---|---|
| 전체 리뷰 텍스트 | ❌ 3개 발췌문 × 약 160자 | ✅ 무제한(GraphQL) | ✅ 내장 리뷰 템플릿 |
| 리뷰 메타데이터(투표, 업주 답글) | ❌ | ✅ | ✅ AI 제안 필드로 가능 |
| 사진 | ❌(Base에는 0) | ✅ 무제한 | ✅ |
| 검색당 최대 결과 | 240(2024년 이전에는 1,000) | 무제한(페이지네이션) | 무제한 |
| 일일 속도 제한 | 신규 300~500 / 레거시 5,000 | 프록시 예산만 영향 | 크레딧 기반(Pro는 월 3,000) |
| 세팅 시간 | 약 15분 | 수시간~수일 | 약 2분 |
| 안티봇 처리 | 해당 없음 | 사용자의 몫 | 처리됨(클라우드 모드) |
| 법적 리스크 | 낮음(공식 승인) | 중간(ToS 회색지대) | 중간(스크래핑과 동일) |
| 비용(시작) | 월 $29 | 프록시 $0.75~$4/GB + 개발 시간 | 무료 플랜 |
| 비용(대량 사용) | 월 $643+ | 프록시 $50~$500/월 + 개발 시간 | 월 $38~$49 |
| 데이터 내보내기 | JSON | CSV/JSON(직접 작성) | Excel / Sheets / Airtable / Notion — 무료 |
| 유지보수 | 낮음 | 높음(셀렉터가 깨지고 안티봇이 강화됨) | 낮음(AI가 재적응) |
Yelp 스크래핑의 법적·윤리적 주의사항
저는 변호사가 아니며, 이것은 법률 자문이 아닙니다. 하지만 지난 2년 사이 법적 환경이 충분히 변했기 때문에, Yelp 스크래핑 프로젝트에 시간을 투자하기 전에 기본 사항은 알고 있어야 합니다.
Yelp 이용약관의 내용: 는 "로봇, 스파이더 또는 기타 자동화 장치"를 사용해 서비스의 일부에 "접근, 검색, 복사, 스크래핑, 색인화"하는 것을 명시적으로 금지합니다. 여기에 "AI Technologies 및/또는 기타 자동화 도구"에 대한 문구도 추가되었습니다.
: "Yelp는 사이트의 어떤 스크래핑도 허용하지 않습니다."
robots.txt의 내용: Yelp의 는 와일드카드 User-agent: * / Disallow: /를 포함하고 있으며, GPTBot, ClaudeBot, PerplexityBot, CCBot, Meta-ExternalAgent도 명시적으로 차단합니다. Googlebot, Bingbot, 일부 소셜 미디어 크롤러만 허용됩니다.
중요한 법적 선례: (N.D. Cal. 2024년 1월) 사건에서 법원은 공개적으로 접근 가능한 로그아웃 상태의 데이터를 스크래핑하는 것이 Meta의 이용약관을 위반하지 않는다고 판단했습니다. 핵심 구분은 로그아웃된 공개 데이터 vs. 로그인된 데이터입니다. 사건은 공개 데이터 스크래핑이 CFAA를 위반하지 않을 가능성이 크다고 봤지만, hiQ는 주 법률상 불법행위 주장(trespass to chattels, misappropriation)에서는 패소했고 50만 달러 판결을 받았습니다.
실무 가이드라인:
- 공개적으로 접근 가능한, 로그아웃 상태의 페이지만 스크래핑하세요
- 요청 속도를 제한하세요(이 가이드의 지연 시간은 윤리적 속도 제한 역할도 합니다)
- 특정 사용자 이름이 붙은 원문 리뷰 텍스트를 재판매하지 마세요 — 리뷰 작성자 프라이버시를 존중하세요
- 지역 데이터 보호법(CCPA, GDPR)을 준수하세요
- 로그인한 상태로 스크래핑하지 마세요 — 승인 범위를 넘는 행동입니다
- 업체 정보(이름/주소/전화번호/평점)는 공개 사실 데이터로, 리뷰 텍스트는 더 민감한 데이터로 취급하세요
구체적인 상황에 대해서는 법률 전문가와 상담하세요.
마무리
세 가지 경로, 하나의 목표.
Yelp Fusion API는 공식 승인된 저유지보수 옵션이지만, 리뷰 발췌문 3개로 제한되고 시작 요금이 월 $29입니다. Python 스크래핑은 Yelp의 모든 데이터 포인트를 완전히 제어할 수 있지만, TLS 지문 위장용 curl_cffi, residential proxy, 랜덤 지연, 숨겨진 JSON 파싱, 그리고 Yelp 방어가 진화할 때마다 이어지는 유지보수에 실제 투자가 필요합니다. Thunderbit는 코딩이나 프록시 설정 없이 약 30초 만에 "Yelp 데이터가 필요해요"에서 "스프레드시트가 나왔어요"로 바꿔줍니다.
2025년에 실제로 통하는 안티블록 핵심은 다음과 같습니다. 완전한 Client Hints를 포함한 현실적인 헤더, TLS 지문 위장을 위한 curl_cffi, 지수 백오프가 포함된 랜덤 지연, residential proxy 회전, 그리고 무엇보다 깨지기 쉬운 CSS 셀렉터 대신 숨겨진 JSON(application/ld+json과 react-root-props)을 파싱하는 것입니다.
어떤 경로가 맞는지 확신이 안 선다면? 먼저 을 써보세요. 필요를 충족한다면 시간을 아낀 셈입니다. 더 큰 제어가 필요하다면 — 완전한 프로그래밍 파이프라인, 맞춤 필드, CRM 연동이 중요하다면 — 위의 Python 가이드가 도움이 될 것입니다. 스크래핑 도구 시장을 더 깊게 보고 싶다면 정리나 가이드도 참고해 보세요.
FAQ
Python으로 Yelp를 무료로 스크래핑할 수 있나요?
네 — curl_cffi, parsel, jmespath 같은 무료 라이브러리를 사용하면 가능합니다. 다만 실제 규모(수십 페이지 이상)에서는 유료 residential proxy가 필요하며, 정도부터 시작합니다. Thunderbit도 빠른 노코드 추출을 위해 월 6페이지까지 가능한 무료 플랜을 제공합니다.
Yelp는 스크래퍼를 차단하나요?
네, 매우 적극적으로 차단합니다. Yelp는 를 사용합니다. 기본 requests는 첫 시도에 막힙니다. 이 가이드의 계층형 차단 방지 전략 — TLS 위장을 위한 curl_cffi, 현실적인 헤더, 랜덤 지연, residential proxy — 가 2025년에 실제로 통하는 방법입니다.
Yelp Fusion API가 스크래핑보다 더 나은가요?
필요에 따라 다릅니다. API는 공식 승인되어 있고 리스크가 낮지만, 하며 검색 결과는 240개로 제한되고 시작 요금이 월 $29입니다. 전체 리뷰 텍스트, 리뷰 메타데이터, 하루 수백 건 이상의 데이터가 필요하다면 스크래핑이 사실상 유일한 방법입니다.
Python으로 Yelp 리뷰를 스크래핑하려면 어떻게 하나요?
impersonate="chrome131"를 사용한 curl_cffi로 비즈니스 페이지를 가져오고, <meta name="yelp-biz-id">에서 인코딩된 비즈니스 ID를 추출한 뒤, GetBusinessReviewFeed 작업을 담아 https://www.yelp.com/gql/batch로 POST 요청을 보냅니다. 페이지네이션은 base64로 인코딩된 after cursor를 사용합니다. 단계별 코드는 위 튜토리얼 섹션에 있습니다. 도 훌륭한 참고 구현입니다.
코딩 없이 Yelp를 스크래핑할 수 있나요?
네 — 에는 및 템플릿이 미리 들어 있습니다. Yelp 페이지를 열고 AI Suggest Fields를 클릭한 뒤 Scrape를 누르세요. Google Sheets, Excel, Airtable, Notion으로의 내보내기는 무료 플랜을 포함한 모든 요금제에서 무료입니다.
더 알아보기