eBay 스크래핑 튜토리얼은 수명이 길어야 석 달이에요. Thunderbit 팀이 그동안 지켜본 풍경은 늘 비슷했어요. 개발자들이 끊어진 코드 조각과 한물간 CSS 선택자, 겉보기엔 멀쩡한데 eBay가 두어 번 개편되고 나면 소리 소문 없이 멈춰버린 GitHub 저장소 사이를 헤매고 다니죠.
eBay에는 활성 리스팅이 약 25억 개 있어요. 오픈 웹에서 Amazon 다음으로 큰 롱테일 가격 데이터셋이라고 봐도 무방하죠. 리셀러 가격 책정부터 경쟁사 인텔리전스까지, 거의 모든 분석의 토대가 되는 데이터예요. 문제는 이걸 프로그램으로 빼오기가 만만치 않다는 점이에요. eBay의 React 프런트엔드는 CSS 클래스명을 수시로 갈아치우고, A/B 테스트는 사용자마다 다른 DOM을 보여줘요. 게다가 Akamai Bot Manager가 HTML 앞을 막고 서 있죠. 이 가이드에서는 지금 이 순간 실제로 도는 Python 코드를 드리고, 스크래퍼가 왜 깨지는지 짚어 더 튼튼하게 만드는 법을 알려드릴게요. eBay API와 직접 스크래핑 중 뭘 골라야 할지도 솔직하게 비교하고, Python 세팅이 부담스러울 때 쓸 노코드 대안까지 챙겨드려요.
Python으로 eBay를 스크래핑한다는 게 정확히 뭘까?
스크립트를 짜서 eBay 페이지를 프로그램으로 불러오고, HTML이나 숨은 JSON을 파싱한 뒤, 제목·가격·판매자·판매 날짜·옵션 같은 구조화 데이터를 뽑아내는 작업이에요. 결과물은 CSV나 스프레드시트, 데이터베이스처럼 바로 쓸 수 있는 형태로 떨어져요.
긁을 수 있는 eBay 페이지는 종류가 다양해요.
- 검색 결과(예: "AirPods Pro" 전체 리스팅)
- 개별 상품 상세 페이지(전체 사양, 이미지, 판매자 정보)
- 판매 완료/종료 리스팅(실거래 가격과 날짜)
- 판매자 프로필과 리뷰
이런 작업에 Python이 가장 많이 쓰이는 데는 이유가 있어요. Requests, BeautifulSoup, lxml, pandas로 이어지는 생태계가 워낙 탄탄해서, 페이지를 가져오고 HTML을 파싱하고 데이터를 주무르는 흐름이 자연스럽거든요. 다만 웹페이지 HTML을 직접 긁는 것과 eBay 공식 API를 쓰는 건 분명히 다른 길이에요. 이 차이는 바로 아래에서 다뤄요.
eBay는 왜 스크래핑할까? 비즈니스 팀의 실전 활용 사례
이 글을 펼쳤다면 이미 나름의 이유가 있으실 거예요. 그래도 비즈니스 가치와 한번 엮어보면 좋아요. eBay 데이터의 ROI가 생각보다 크거든요. Bain 분석에 따르면 실현 가격을 단 1%만 끌어올려도 이익이 11.1% 늘어난다고 해요. McKinsey는 리테일에서 동적 가격 책정이 매출을 최대 5%, 마진을 2~7% 개선한다고 봤어요.
자주 보이는 활용 사례를 정리하면 이래요.
| 활용 사례 | 필요한 데이터 | 비즈니스 효과 |
|---|---|---|
| 가격 모니터링 및 재가격 책정 | 활성 리스팅 가격, 배송비, 상품 상태 | 경쟁력 있는 가격 유지, 마진 보호 |
| 경쟁사 분석 | 상품 구색, 프로모션, 배송 조건 | 전략적 포지셔닝, 상품군 공백 파악 |
| 시장 조사 및 트렌드 탐색 | 리스팅 속도, 카테고리 트렌드, 수요 패턴 | 신제품 발굴, 수요 예측 |
| 리셀러 가격 산정 / 감정 | 판매 가격, 판매 날짜, 상태 | 적정 시세 판단, 구매 의사결정 |
| 감성 분석 | 리뷰, 평점, 반품 정책 | 제품 품질 인사이트, 고객 만족도 분석 |
| 리드 생성 | 판매자 프로필, 스토어 정보, 연락처 | 거래액이 큰 B2B 판매자에게 아웃리치 |

결국 핵심은 하나예요. eBay에 데이터는 분명히 있지만, 그게 웹페이지 안에 갇혀 있다는 거죠.
스크래핑은 그 갇힌 데이터를 경쟁 우위로 끄집어내는 작업이에요.
eBay 공식 API vs. Python 웹 스크래핑: 뭘 골라야 할까?
많은 튜토리얼이 슬쩍 피해 가는 질문이에요. eBay는 공식 API, 특히 Browse API를 제공하고, 사용자들은 늘 API냐 직접 스크래핑이냐를 두고 고민하죠. 답은 결국 어떤 데이터가 필요하냐에 달려 있어요.
| 기준 | eBay Browse/Finding API | Python 웹 스크래핑 |
|---|---|---|
| 판매 완료/종료 리스팅 | 제한적 — Marketplace Insights API가 있지만 접근이 자주 거절됨 | LH_Sold=1&LH_Complete=1 URL 파라미터로 전체 접근 가능 |
| 요청 제한 | 기본 티어에서 하루 5,000회 호출 | 자체 관리(프록시 의존) |
| 데이터 필드 | 사전 정의됨(제목, 가격, 카테고리, 기본 판매자 정보) | 페이지에 보이는 것은 무엇이든 가능(리뷰, 전체 사양, 옵션 매트릭스) |
| 설정 복잡도 | OAuth 2.0, 앱 등록, API 키 | pip install + 코드 |
| 안정성 | 엔드포인트가 안정적 | HTML이 바뀌면 깨짐 |
| 비용 | 무료 티어 제공, 대량 사용은 유료 | 코드는 무료지만 대규모에선 프록시 비용 발생 |
| 옵션/MSKU 데이터 | 부분적 — 많은 경우 부모 SKU만 제공 | 전체 가능(숨겨진 JSON 파싱) |
| 페이지네이션 깊이 | 10,000개 항목 상한 | 이론상 무제한 |
하나만 짚고 갈게요. 예전 Finding API는 findCompletedItems까지 포함해 2025년 2월에 완전히 문을 닫았어요. ebaysdk-python이나 Finding 모듈을 호출하는 라이브러리를 쓰고 있다면, 지금 프로덕션에서는 이미 멈춰 있을 가능성이 높아요.
제 추천은 이래요. 활성 리스팅처럼 안정적이고 중간 규모의 구조화 카탈로그 조회라면 Browse API가 맞아요. 반대로 판매 완료 가격, 리뷰, 옵션 데이터, API가 안 열어주는 필드가 필요하면 Python 스크래핑으로 가세요. 많은 팀이 둘을 같이 써요.
Python으로 eBay를 스크래핑할 때 챙길 도구와 라이브러리
코드를 짜기 전에 도구부터 점검해요. 다행히 대부분의 eBay 페이지는 헤드리스 브라우저까지 동원할 필요가 없어요. 데이터가 서버 렌더링 HTML 안에 이미 들어 있거든요.
| 라이브러리 | 용도 |
|---|---|
requests 또는 httpx | eBay 페이지를 내려받는 HTTP 클라이언트 |
curl_cffi | 실제 브라우저 TLS 지문을 흉내 내는 HTTP 클라이언트(Akamai 우회에 중요) |
beautifulsoup4 | CSS 선택자로 추출하기 위한 HTML 파서 |
lxml | BeautifulSoup용 고속 파서 백엔드 |
jmespath | 중첩 JSON 블롭을 조회하는 쿼리 언어 |
pandas | 데이터 처리 및 CSV/Excel 내보내기 |
gspread | Google Sheets 연동 |
한 번에 설치하려면 아래 명령을 쓰세요.
pip install requests httpx curl_cffi beautifulsoup4 lxml jmespath pandas gspread
Python은 3.11 이상을 권해요. pandas 3.0이 3.10 이상을 요구하고, 3.11은 I/O 중심 작업에서 10~60% 빨라지거든요.
특별히 눈여겨볼 건 **curl_cffi**예요. 2026년 기준으로 eBay 스크래퍼에서 가장 체감되는 업그레이드예요. eBay는 Akamai Bot Manager를 쓰는데, Akamai가 봇을 잡아내는 핵심 무기가 바로 TLS 지문 분석이에요. 일반 requests는 Python 특유의 JA3 지문을 그대로 내보내서 거의 즉시 들켜요. 반면 curl_cffi는 진짜 Chrome의 TLS 핸드셰이크를 흉내 내요. 덕분에 헤드리스 브라우저 없이도 Akamai 보호 대상의 90% 정도를 뚫어내요.
단계별 가이드: Python으로 eBay 검색 결과 스크래핑하기
이 파트가 튜토리얼의 알맹이예요. eBay 검색 결과 페이지에서 상품 리스팅을 가져와요.
- 난이도: 초급~중급
- 소요 시간: 첫 성공까지 약 30분
- 준비물: Python 3.11+, 위에서 소개한 라이브러리, 터미널, 대상 eBay 검색 URL
1단계: Python 프로젝트 준비하기
프로젝트 디렉터리를 만들고 의존성을 깔아요.
mkdir ebay-scraper && cd ebay-scraper
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install requests curl_cffi beautifulsoup4 lxml pandas
scrape_ebay.py 파일을 하나 만드세요. 여기가 작업 공간이에요.
2단계: eBay 검색 URL 만들기
eBay 검색 URL은 구조가 단순해요. 핵심 파라미터는 _nkw(키워드)예요.
import urllib.parse
keyword = "airpods pro"
base_url = "https://www.ebay.com/sch/i.html"
params = {
"_nkw": keyword,
"_ipg": "120", # 페이지당 항목 수: 60, 120, 또는 240(240은 봇 플래그를 유발할 수 있음)
"_pgn": "1", # 페이지 번호
}
url = f"{base_url}?{urllib.parse.urlencode(params)}"
print(url)
# https://www.ebay.com/sch/i.html?_nkw=airpods+pro&_ipg=120&_pgn=1
자주 쓰는 다른 파라미터도 정리해둘게요.
LH_BIN=1— Buy It Now만 보기_sacat=175673— 특정 카테고리_sop=12— 베스트 매치 정렬(10 = 가격+배송 최저, 13 = 신규 등록순)LH_Complete=1&LH_Sold=1— 판매 완료/종료 리스팅(아래 전용 섹션에서 다뤄요)
3단계: 요청 보내고 응답 처리하기
여기서 curl_cffi가 진가를 발휘해요. 평범한 requests.get()은 Akamai한테 403을 받기 일쑤죠. curl_cffi를 쓰면 진짜 Chrome처럼 행세할 수 있어요.
from curl_cffi import requests as cffi_requests
import random, time
USER_AGENTS = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0",
]
HEADERS = {
"User-Agent": random.choice(USER_AGENTS),
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
}
def fetch_page(url, max_retries=5):
delay = 2
for attempt in range(max_retries):
try:
r = cffi_requests.get(url, impersonate="chrome124", headers=HEADERS, timeout=30)
if r.status_code == 200:
return r.text
if r.status_code in (403, 429, 503):
retry_after = r.headers.get("Retry-After")
sleep_for = float(retry_after) if retry_after else delay + random.uniform(0, 1)
print(f" 상태 {r.status_code}, {sleep_for:.1f}초 후 재시도...")
time.sleep(sleep_for)
delay *= 2
continue
r.raise_for_status()
except Exception as e:
print(f" 요청 오류: {e}, 재시도 중...")
time.sleep(delay)
delay *= 2
raise RuntimeError(f"{max_retries}번 재시도 후 실패: {url}")
지수 백오프에 지터를 더하는 게 포인트예요. 대기 시간을 딱 고정하면 그 규칙성 자체가 봇 지문이 돼버리거든요.
4단계: 검색 페이지에서 상품 리스팅 파싱하기
eBay는 지금 검색 결과 레이아웃 두 개를 오가는 중이에요. 튼튼한 스크래퍼라면 둘 다 받아내야 해요.
| 필드 | 기존 레이아웃 | 새 레이아웃 |
|---|---|---|
| 카드 컨테이너 | li.s-item | li.s-card 또는 div.su-card-container |
| 제목 | .s-item__title | .s-card__title |
| URL | a.s-item__link[href] | a.su-link[href] |
| 가격 | span.s-item__price | .s-card__price |
두 레이아웃을 모두 처리하는 파싱 코드는 이래요.
from bs4 import BeautifulSoup
def parse_search_results(html):
soup = BeautifulSoup(html, "lxml")
cards = soup.select("li.s-item, li.s-card, div.su-card-container")
results = []
for card in cards:
# 제목 — 두 레이아웃 모두 시도
title_el = card.select_one(".s-item__title, .s-card__title")
title = title_el.get_text(strip=True) if title_el else None
# 가짜 "Shop on eBay" 플레이스홀더 카드 제외
if not title or "Shop on eBay" in title:
continue
# 가격
price_el = card.select_one("span.s-item__price, .s-card__price")
price = price_el.get_text(strip=True) if price_el else None
# URL
link_el = card.select_one("a.s-item__link[href], a.su-link[href]")
url = link_el["href"].split("?")[0] if link_el else None
# 이미지
img_el = card.select_one("img.s-item__image-img, .s-card__image img")
image = None
if img_el:
image = img_el.get("src") or img_el.get("data-src")
# 배송
ship_el = card.select_one("span.s-item__shipping, span.s-item__logisticsCost, .s-card__attribute-row")
shipping = ship_el.get_text(strip=True) if ship_el else None
results.append({
"title": title,
"price": price,
"url": url,
"image": image,
"shipping": shipping,
})
return results
첫 카드 함정은 단골 실수예요. eBay 검색 페이지의 첫 li.s-item은 "Shop on eBay" 제목만 달린 숨김 플레이스홀더인 경우가 많고, 실제 가격이 없어요. 꼭 걸러내세요.
5단계: 페이지네이션 처리해서 여러 페이지 긁기
eBay는 _pgn 파라미터로 페이지를 나눠요. 다음 페이지 링크는 a.pagination__next를 써요.
import urllib.parse
def scrape_ebay_search(keyword, max_pages=5):
all_results = []
for page_num in range(1, max_pages + 1):
params = {"_nkw": keyword, "_ipg": "120", "_pgn": str(page_num)}
url = f"https://www.ebay.com/sch/i.html?{urllib.parse.urlencode(params)}"
print(f"페이지 {page_num} 스크래핑 중: {url}")
html = fetch_page(url)
results = parse_search_results(html)
if not results:
print(f" {page_num}페이지에 결과가 없습니다. 중단합니다.")
break
all_results.extend(results)
print(f" {len(results)}개 리스팅 발견(누적: {len(all_results)})")
# 예의 있는 대기 — 지터 포함 3~8초
time.sleep(random.uniform(3, 8))
return all_results
3~8초 랜덤 지터는 옵션이 아니라 필수예요.
eBay의 Akamai 레이어는 한 IP가 1초에 한 번 이상 계속 두드리면 곧바로 플래그를 세워요.
6단계: 스크래핑한 데이터를 CSV나 JSON으로 내보내기
import pandas as pd
results = scrape_ebay_search("airpods pro", max_pages=3)
df = pd.DataFrame(results)
df.to_csv("ebay_airpods.csv", index=False)
df.to_json("ebay_airpods.json", orient="records", indent=2)
print(f"{len(df)}개 리스팅을 CSV와 JSON으로 내보냈습니다.")
이제 eBay 리스팅이 깔끔한 스프레드시트로 정리됐을 거예요. 제 환경에서는 3페이지(360개 리스팅)를 긁는 데 지연 시간까지 합쳐 45초쯤 걸렸어요.
Python으로 eBay 상품 상세 페이지 스크래핑하기
검색 결과는 요약만 보여줘요. 정작 알맹이인 전체 설명, 판매자 피드백 점수, 상세 사양, 이미지 캐러셀, 옵션 데이터는 상품 상세 페이지에 들어 있어요.
단일 상품 상세 페이지 파싱하기
eBay 상품 페이지는 /itm/<ITEM_ID> 형태 URL에 있어요. 가장 안정적인 추출 경로는 JSON-LD이에요. eBay가 거의 모든 CSS 변경을 견디는 Product 스키마 블록을 심어두거든요.
import json
def parse_item_page(html):
soup = BeautifulSoup(html, "lxml")
item = {}
# 1. JSON-LD — 가장 안정적인 추출 경로
for tag in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(tag.string or "")
except (json.JSONDecodeError, TypeError):
continue
if isinstance(data, dict) and data.get("@type") == "Product":
item["title"] = data.get("name")
item["brand"] = (data.get("brand") or {}).get("name")
item["images"] = data.get("image")
offers = data.get("offers") or {}
item["price"] = offers.get("price")
item["currency"] = offers.get("priceCurrency")
break
# 2. JSON-LD에 없는 필드는 CSS 폴백 사용
def first_text(selectors):
for sel in selectors:
el = soup.select_one(sel)
if el and el.get_text(strip=True):
return el.get_text(strip=True)
return None
item.setdefault("title", first_text([
"h1.x-item-title__mainTitle",
"h1.x-item-title__mainTitle .ux-textspans--BOLD",
]))
item["condition"] = first_text([
".x-item-condition-text .ux-textspans",
])
item["seller"] = first_text([
".x-sellercard-atf__info__about-seller a .ux-textspans",
])
item["shipping"] = first_text([
"div.ux-labels-values--shipping .ux-textspans--BOLD",
])
# 3. 상품 상세 항목
specifics = {}
for dl in soup.select(".ux-layout-section-evo__item--table-view dl.ux-labels-values"):
k = dl.select_one(".ux-labels-values__labels-content .ux-textspans")
v = dl.select_one(".ux-labels-values__values-content .ux-textspans")
if k and v:
specifics[k.get_text(strip=True).rstrip(":")] = v.get_text(strip=True)
item["specifics"] = specifics
return item
여기서 기억할 패턴은 JSON-LD를 먼저, CSS 폴백을 나중에 두는 순서예요. 이래야 분기마다 스크래퍼가 죽는 일을 크게 줄일 수 있어요. 자세한 건 아래에서 더 풀어드릴게요.
eBay 상품 옵션(MSKU 데이터) 스크래핑하기
eBay 리스팅 중에는 색상·사이즈·저장 용량이 다른 옵션이 여러 개인 경우가 있어요. 화면 DOM은 사용자가 옵션을 누르기 전까지 "$899 to $1,099" 같은 가격 범위만 보여줘요. 진짜 옵션별 가격은 MSKU라는 숨은 JavaScript 객체에 들어 있어요.
이 영역은 eBay API가 부모 SKU 정도만 부분적으로 제공하니, 오히려 스크래핑이 한 수 위인 곳이에요.
import re, json
def extract_variants(html):
# 비탐욕적 매칭이 핵심입니다 — 탐욕적 .+는 페이지 전체를 먹어버립니다
m = re.search(r'"MSKU"\s*:\s*(\{.+?\})\s*,\s*"QUANTITY"', html, re.DOTALL)
if not m:
return []
try:
msku = json.loads(m.group(1))
except json.JSONDecodeError:
return []
item_labels = {str(k): v["displayLabel"] for k, v in msku.get("menuItemMap", {}).items()}
skus = []
for combo_key, variation_id in msku.get("variationCombinations", {}).items():
option_ids = combo_key.split("_")
options = [item_labels.get(oid, oid) for oid in option_ids]
var = msku.get("variationsMap", {}).get(str(variation_id), {})
bin_model = var.get("binModel", {})
price_spans = bin_model.get("price", {}).get("textSpans", [{}])
price = price_spans[0].get("text") if price_spans else None
qty = var.get("quantity")
skus.append({
"options": options,
"price": price,
"quantity_available": qty,
"variation_id": variation_id,
})
return skus
이 정규식의 비탐욕적 (.+?)가 바로 수많은 eBay 스크래퍼가 발이 걸려 넘어지는 자리예요. 탐욕적 .+는 페이지 마지막 "QUANTITY"까지 통째로 삼켜서 깨진 JSON을 토해내요. "작동한다"는 튜토리얼에서 이 버그를 적어도 세 번은 봤어요.
Python으로 eBay 판매 완료·종료 리스팅 스크래핑하기
API 대신 스크래핑을 써야 하는 이유를 가장 잘 보여주는 사례예요. 무엇이, 얼마에, 언제 팔렸는지 알려주는 판매 완료 데이터는 시장 조사와 리셀러 가격 책정, 감정 평가의 기본 자료예요. 그런데 eBay Browse API는 이걸 대놓고 안 줘요. Marketplace Insights API가 이론상 가능은 하지만, 접근이 "Limited Release"라 거절당하기 일쑤예요.
필요한 URL 파라미터는 LH_Complete=1(종료 리스팅)과 LH_Sold=1(실제 판매분만)이에요. 반드시 둘 다 넣어야 해요. LH_Sold=1만 넣으면 일부 카테고리에서 슬그머니 활성 리스팅으로 돌아가버려요. 커뮤니티에서 가장 흔히 꼽는 실수죠.
def scrape_sold_listings(keyword, max_pages=3):
all_sold = []
for page_num in range(1, max_pages + 1):
params = {
"_nkw": keyword,
"_ipg": "120",
"_pgn": str(page_num),
"LH_Complete": "1",
"LH_Sold": "1",
}
url = f"https://www.ebay.com/sch/i.html?{urllib.parse.urlencode(params)}"
print(f"판매 완료 페이지 {page_num} 스크래핑 중...")
html = fetch_page(url)
soup = BeautifulSoup(html, "lxml")
cards = soup.select("li.s-item")
for card in cards:
title_el = card.select_one(".s-item__title")
title = title_el.get_text(strip=True) if title_el else None
if not title or "Shop on eBay" in title:
continue
# 실제로 판매된 항목만 포함(초록색 POSITIVE 가격)
sold_tag = card.select_one(
".s-item__title--tag .POSITIVE, .s-item__caption--signal.POSITIVE"
)
if sold_tag is None:
continue # 팔리지 않은 종료 리스팅은 제외
price_el = card.select_one("span.s-item__price")
price = price_el.get_text(strip=True) if price_el else None
# 판매 날짜 파싱
sold_date = None
import re, datetime as dt
card_text = card.get_text()
m = re.search(r"Sold\s+([A-Z][a-z]{2}\s+\d{1,2},\s*\d{4})", card_text)
if m:
sold_date = dt.datetime.strptime(m.group(1), "%b %d, %Y").strftime("%Y-%m-%d")
link_el = card.select_one("a.s-item__link[href]")
url = link_el["href"].split("?")[0] if link_el else None
all_sold.append({
"title": title,
"sold_price": price,
"sold_date": sold_date,
"url": url,
})
if not cards:
break
time.sleep(random.uniform(3, 8))
return all_sold
HTML에서 결정적 차이는 색깔이에요. 팔린 항목은 가격이 초록색(.POSITIVE 래퍼 안)이고, 안 팔린 종료 리스팅은 빨간색 취소선으로 표시돼요. 항상 .POSITIVE 클래스로 걸러내세요.
eBay 스크래퍼는 왜 깨지나, 그리고 오래 버티게 만드는 법
eBay 스크래퍼가 어느 날 갑자기 멈췄다면, 당신만 그런 게 아니에요. 제가 본 거의 모든 eBay 스크래핑 포럼 스레드에서 가장 큰 고통이 바로 이거였어요. 질문은 스크래퍼가 깨질지가 아니라 언제 깨질지예요.
왜 이런 일이 벌어질까요?
- eBay는 React 렌더링과 함께, 배포할 때마다 바뀌는 동적 클래스명을 써요.
- A/B 테스트로 사용자마다 다른 DOM을 보여줘요(지금도
s-item/s-card이중 레이아웃이 실제 사례죠). - 주기적인 사이트 개편이 HTML 중첩 구조를 바꿔요. 데이터는 그대로인데 구조만 달라지는 거죠.
#itemTitle,#prcIsum같은 옛날 선택자는 몇 년 전에 사라졌는데도, 튜토리얼에는 아직 남아 있어요.
Scrapfly의 2026 가이드도 같은 얘기를 해요. "eBay 웹 스크래핑의 진짜 과제는 eBay의 CSS 선택자 변경을 처리하는 것이다. eBay는 프런트엔드를 정기적으로 업데이트해 특정 클래스명에 의존하는 스크래퍼를 깨뜨린다."

오래가는 eBay 스크래퍼를 위한 방어 전략
eBay의 분기별 개편에도 살아남는 전략이 네 가지 있어요.
1. CSS 선택자보다 JSON-LD를 먼저 쓰세요. eBay는 모든 상품 페이지에 구조화된 Product 스키마를 넣어둬요. 데이터 계층은 표현 계층보다 훨씬 덜 바뀌어요. 디자이너는 분기마다 CSS 클래스를 손대지만, price, name, seller 같은 백엔드 필드명은 내부 API에 매핑돼 있어서 이름이 잘 안 바뀌어요.
2. 계단식 폴백 선택자를 두세요. 선택자 하나에만 기대면 안 돼요. 항상 대안을 준비하세요.
def first_text(soup, selectors):
for sel in selectors:
el = soup.select_one(sel)
if el and el.get_text(strip=True):
return el.get_text(strip=True)
return None
title = first_text(soup, [
"h1.x-item-title__mainTitle",
"h1.x-item-title__mainTitle .ux-textspans--BOLD",
"[data-testid='x-item-title'] h1",
])
3. 숨은 JSON 블롭을 파싱하세요. MSKU 옵션 객체나 인라인 JavaScript 데이터는 서버 사이드에서 만들어지니 CSS 변경 영향을 덜 받아요. <script> 태그에서 정규식으로 빼내는 방식은 초기 손은 더 가지만, 유지보수 부담을 확 줄여줘요.
4. 선택자 실패를 기록하세요. 데이터가 비었다는 사실만 알지 말고, 언제 선택자가 안 맞았는지 잡아낼 모니터링을 넣으세요.
if title is None:
print(f"경고: {url}에서 title 선택자 실패")
5. 브라우저 impersonation과 함께 curl_cffi를 쓰세요. 헤드리스 브라우저 없이도 Akamai의 TLS 지문 분석을 피하는 데 도움이 돼요.
AI 기반 대안: 선택자 유지보수에서 손 떼기
선택자를 몇 달마다 손보는 일이 지긋지긋하다면, 아예 결이 다른 접근법도 있어요. Thunderbit 같은 도구는 페이지를 매번 새로 읽고, 그때그때 추출 로직을 AI가 직접 짜내요. McGill University 연구에서 AI 방식과 선택자 기반 스크래퍼를 3,000개 페이지로 붙여봤더니, 레이아웃이 바뀐 뒤에도 AI 방식이 98.4% 정확도를 지켰어요. 업계 벤치마크에서는 스크래퍼 유지보수 비용이 60~80% 줄었다는 보고도 나와요.
| 접근 방식 | eBay HTML이 바뀌면 깨지나? | 유지보수 노력 |
|---|---|---|
| 하드코딩된 CSS 선택자 | 예, 분기별로 | 높음 — 지속적인 패치 필요 |
| 숨겨진 JSON / JSON-LD 추출 | 드묾 | 낮음 |
| AI 기반 스크래핑(Thunderbit) | 아님 — 매번 AI가 선택자를 다시 도출 | 없음 |
AI로 eBay 데이터 스크래핑하기 Get Started Free
Thunderbit 워크플로는 뒤에서 더 깊게 다룰게요. 일단 기억할 건 이거예요. 몇 달을 돌릴 스크래퍼라면 JSON 우선 추출과 폴백 선택자에 투자하세요. 선택자 유지보수 자체가 싫다면 AI 방식도 충분히 한 번 고려할 만해요.
가격 모니터링을 위한 반복 eBay 스크래핑 자동화하기
한 번 긁는 것도 쓸모 있어요. 하지만 가격 모니터링, 재고 추적, 경쟁사 분석에는 반복적인 데이터 수집이 필요하죠. 경쟁사 글들을 보면 가격 모니터링을 활용 사례로는 꼭 언급하면서, 정작 자동화하는 법까지 보여주는 경우는 드물어요.
옵션 1: Cron Job(Linux/macOS) 또는 작업 스케줄러(Windows)
가장 단순한 길이에요. Python 스크립트를 cron job으로 감싸면 돼요. cron은 최소 환경에서 도니까, 가상환경 Python의 절대 경로를 꼭 써주세요.
crontab -e
# 매일 오전 8시 15분
15 8 * * * /Users/me/ebay/venv/bin/python /Users/me/ebay/scrape_ebay.py >> /Users/me/ebay/scrape.log 2>&1
Windows에서는 PowerShell을 써요.
$A = New-ScheduledTaskAction -Execute "C:\Users\me\ebay\venv\Scripts\python.exe" -Argument "C:\Users\me\ebay\scrape_ebay.py"
$T = New-ScheduledTaskTrigger -Daily -At 8:15am
Register-ScheduledTask -TaskName "eBayScraper" -Action $A -Trigger $T
이 방식은 늘 켜져 있는 머신이 있어야 하고, 프록시와 봇 차단 대응도 직접 챙겨야 해요.
옵션 2: 클라우드 함수(Serverless)
AWS Lambda나 Google Cloud Functions를 쓰면 전용 서버 없이 스크래퍼를 돌릴 수 있어요. 대신 설정 난이도가 더 높아요. 의존성을 패키징해야 하고, 타임아웃(Lambda는 최대 15분)도 신경 써야 하며, 프록시 관리는 여전히 필요해요. 서버 유지보수가 없다는 게 보상이죠.
옵션 3: Thunderbit로 노코드 예약 실행하기
Thunderbit의 Scheduled Scraper를 쓰면 "매일 오전 8시"처럼 평범한 말로 간격을 지정하고, eBay URL을 넣고, 예약만 누르면 끝이에요. 봇 차단 대응이 내장된 채로 클라우드에서 돌아가요.
| 접근 방식 | 설정 난이도 | 서버 필요 여부 | 봇 차단 대응 |
|---|---|---|---|
| Cron + Python 스크립트 | 중간 | 예(항상 켜진 머신) | 프록시 직접 관리 |
| 클라우드 함수(Lambda) | 높음 | 아니오(serverless) | 프록시 직접 관리 |
| Thunderbit Scheduled Scraper | 낮음(말로 설명) | 아니오(클라우드 기반) | 내장 |
반복 스크래핑 데이터를 저장할 때는 로컬 SQLite가 가격 이력 관리에 가장 좋아요. INSERT OR REPLACE는 외래 키를 깨고 컬럼을 날려버리니 ON CONFLICT ... DO UPDATE를 쓰세요.
CREATE TABLE IF NOT EXISTS listings (
item_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
price REAL,
last_price REAL,
first_seen_at TEXT DEFAULT (datetime('now')),
last_seen_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS price_history (
item_id TEXT NOT NULL,
observed_at TEXT NOT NULL DEFAULT (datetime('now')),
price REAL NOT NULL,
PRIMARY KEY (item_id, observed_at)
);
Thunderbit Scheduled Scraper 체험하기
코딩 없이 가고 싶다면? Thunderbit로 2분 만에 eBay 스크래핑하기
여기까지 Python 코드를 2,000단어 가까이 풀었어요. 이제는 솔직하게, 언제 Python이 굳이 필요 없는지 말씀드릴 차례예요.
일회성 시장 조사를 하는 비즈니스 사용자, 비교 시세만 확인하면 되는 리셀러, 개발 스프린트 없이 오늘 당장 데이터가 필요한 이커머스 팀이라면 Python은 과해요. 세팅에 선택자 유지보수, 프록시 관리까지 따지면 "스프레드시트에 리스팅 200개만 넣으면 된다"는 일치고는 오버헤드가 너무 커요.
Thunderbit가 eBay를 긁는 방식(단계별)
- Thunderbit Chrome 확장 프로그램을 설치하세요 — 신용카드 필요 없어요.
- Chrome에서 eBay 검색 결과나 상품 페이지로 이동해요.
- Thunderbit 사이드바에서 **"AI Suggest Fields"**를 누르세요. AI가 페이지를 읽고 Title, Price, Condition, Shipping, Seller, Rating 같은 열을 제안해요.
- **"Scrape"**를 누르세요. 확장 프로그램이 페이지네이션을 따라가며 데이터 테이블을 채워줘요. eBay라면 바로 쓸 수 있는 스크래퍼 템플릿이 이미 있어서 클릭 한 번이면 돼요.
- Google Sheets, Airtable, Notion, CSV, JSON, Excel로 무료 내보내기가 돼요.
전체 과정이 2분도 안 걸려요.
직접 재봤어요.
하위 페이지 보강: 추가 코드 없이 상세 페이지 데이터 가져오기
검색 결과를 긁은 뒤, Thunderbit는 각 리스팅 상세 페이지로 들어가 전체 사양, 판매자 정보, 설명, 모든 이미지를 덧붙일 수 있어요. 앞에서 작성한 20줄 넘는 Python 하위 페이지 코드를 버튼 한 번으로 대체하는 셈이죠.
그래도 Python을 써야 할 때
이런 경우엔 Python이 더 유리해요.
- 대규모 스크래핑(실행당 수만 페이지)
- 아주 세밀한 파싱 로직 또는 데이터 변환
- 기존 데이터 파이프라인(Airflow, dbt, Kafka)에 통합
- 고급 봇 대응을 위한 세밀한 TLS/세션 제어
- 단위 경제성 — 수백만 행 규모에서는 유지보수되는 스택이 크레딧 기반 SaaS보다 나을 수 있음
대부분의 일회성·중간 규모 프로젝트에서는 Thunderbit가 더 빠르고 간단해요. 반대로 대규모 프로덕션 파이프라인이라면 Python이 완전한 제어권을 줘요.
Python으로 eBay 스크래핑할 때 차단 피하는 팁
eBay의 Akamai 레이어는 진짜로 있어요. 실전에서 먹히는 방법을 정리할게요.
impersonate="chrome124"와 함께curl_cffi를 쓰세요 — 일반requests보다 효과가 가장 커요- 현재 브라우저 버전 User-Agent를 순환하세요(Chrome 143, Firefox 124, Safari 26)
- 요청 사이에 3~8초 랜덤 지연을 두세요(출처) — 고정 간격은 지문이 돼요
- 수십 페이지를 넘긴다면 레지덴셜 또는 로테이팅 프록시를 쓰세요. AWS, GCP, DigitalOcean 같은 데이터센터 IP는 Akamai에 금방 걸려요.
- robots.txt를 존중하세요 — 필터링된 browse URL은 대부분 명시적으로 Disallow지만, 상품 상세 페이지(
/itm/<id>)는 아니에요 - CAPTCHA에 우아하게 대응하세요 — 감지되면 다른 IP로 재시도하거나 CAPTCHA 해결 서비스를 쓰세요
- 서버를 두드려대지 마세요. eBay v. Bidder's Edge 판례는 실제로 서버 성능을 떨어뜨리는 스크래핑에 trespass to chattels가 적용될 수 있음을 보여줘요. IP당 1초 1회 수준이면 그 기준과는 한참 멀어요.
대규모 상업적 용도라면 활성 리스팅엔 Browse API를, 판매 완료 비교자료와 API 미노출 데이터엔 타깃 스크래핑만 쓰는 하이브리드가 더 깔끔해요. 기술적으로도, 법적으로도 나아요.
Python으로 eBay 스크래핑하는 건 합법일까?
저는 변호사가 아니고, 이 글은 법률 자문도 아니에요. 그러니 짧게만 짚을게요.
법적 환경은 공개 데이터 스크래핑에 유리한 쪽으로 흘러가고 있어요. 핵심 판례는 이래요.
- hiQ v. LinkedIn(제9순회항소법원, 2022): 공개 접근 가능한 데이터 스크래핑은 CFAA 위반이 아님
- Van Buren v. United States(미 연방대법원, 2021): CFAA의 "허가된 접근을 초과했다" 조항을 좁게 해석
- Meta v. Bright Data(캘리포니아 북부지방법원, 2024): 로그인하지 않은 상태의 스크래핑은 스크래퍼가 "사용자"가 아니므로 플랫폼 약관 위반이 아님
다만 eBay의 2026년 2월 사용자 계약 업데이트는 "대신 구매 에이전트, LLM 기반 봇, 인간 검토 없이 주문을 시도하는 엔드투엔드 흐름"을 명시적으로 금지해요. 선은 분명해요. 공개 페이지의 읽기 전용 스크래핑은 비교적 안전한 영역이지만, 결제를 자동화하는 건 다른 얘기예요.
실무에서는 공개적으로 보이는 데이터만 긁고, 가짜 계정을 만들거나 로그인 벽을 우회하지 말고, 저작권 있는 리스팅 이미지를 무단으로 대량 재판매하지 마세요. 상업 규모 프로젝트라면 법률 전문가와 상의하세요.
결론 및 핵심 요약
Python은 eBay 스크래핑에서 가장 유연한 길이지만, 사이트 HTML이 바뀌는 만큼 꾸준한 유지보수를 요구해요. 선택 기준을 정리하면 이래요.
- 활성 리스팅의 안정적·중간 규모 구조화 조회에는 eBay Browse API
- 판매 완료 리스팅, 리뷰, 옵션 데이터, API가 안 주는 정보에는 Python 스크래핑
- 코드를 짜거나 유지보수하기 싫다면 Thunderbit
이 가이드의 코드는 복원력을 최우선으로 삼았어요. JSON-LD를 먼저 뽑고, 그다음 CSS 폴백을 대고, 옵션은 숨은 JSON을 파싱하는 식이죠. 이렇게 계층을 쌓아두면 eBay 프런트엔드 팀이 리디자인을 밀어붙여도 스크래퍼가 쉽게 죽지 않아요.
노코드 방식을 한번 시험해보고 싶다면 Thunderbit 무료 플랜으로 지금 바로 eBay 페이지에서 테스트할 수 있어요. eBay 스크래퍼 템플릿이 어떻게 도는지 보고 싶다면 클릭 한 번이면 돼요.
웹 스크래핑 도구를 더 알고 싶다면 최고의 자동 웹 스크래핑 도구, 웹사이트 데이터를 Excel로 스크래핑하는 방법, 최고의 Python 웹 스크래핑 도구 가이드를 참고하세요. Thunderbit YouTube 채널에서도 튜토리얼을 볼 수 있어요.
eBay 스크래핑에 Thunderbit 사용해보기 Get Started Free
FAQ
1. Python으로 eBay를 무료로 스크래핑할 수 있나요?
네. Requests, BeautifulSoup, curl_cffi, pandas 같은 라이브러리는 전부 무료 오픈소스예요. 비용은 대규모에서 생겨요. 고용량 스크래핑용 레지덴셜 프록시는 대역폭에 따라 보통 월 $50500(약 7만70만 원) 정도 들어요. 수백 페이지 수준 소규모 프로젝트라면 속도 제한만 조심하면 집 IP로도 가능해요.
2. Python으로 eBay 판매 완료·종료 리스팅을 어떻게 스크래핑하나요?
검색 URL 파라미터에 LH_Complete=1&LH_Sold=1를 더하세요. 둘 다 넣어야 해요. LH_Sold=1만 넣으면 일부 카테고리에서 슬그머니 활성 리스팅으로 돌아가요. 실제 판매를 나타내는 가격 요소의 .POSITIVE CSS 클래스를 확인해 결과를 걸러내세요.
3. eBay는 웹 스크래핑을 차단하나요?
eBay는 Akamai Bot Manager를 쓰며, 주로 TLS 지문과 행동 분석으로 스크래퍼를 잡아요. 일반 requests 호출은 403을 받는 경우가 많아요. 브라우저 impersonation이 되는 curl_cffi, User-Agent 순환, 요청 사이 3~8초 랜덤 지연을 쓰면 대부분의 차단을 피할 수 있어요. 대규모 작업에는 레지덴셜 프록시가 도움이 돼요.
4. eBay API와 웹 스크래핑 중 뭘 써야 하나요?
활성 리스팅의 안정적·중간 규모 조회에는 Browse API(하루 최대 5,000회 호출)를 쓰세요. 판매 가격 이력, 전체 옵션/MSKU 데이터, 리뷰, 또는 API가 안 주는 필드가 필요하면 스크래핑이에요. Marketplace Insights API가 이론상 판매 완료 데이터를 주긴 하지만, 접근이 제한적이고 자주 거절돼요.
5. 코딩 없이 eBay를 스크래핑하는 가장 쉬운 방법은 뭔가요?
Thunderbit Chrome 확장 프로그램이라면 AI가 eBay 페이지를 읽고, 데이터 열을 제안하고, 클릭 한 번에 리스팅을 뽑아내요. 페이지네이션, 하위 페이지 보강, Google Sheets·Excel·Airtable·Notion 내보내기를 지원해요. 미리 만들어둔 eBay 스크래퍼 템플릿을 쓰면 더 빨라요.
더 알아보기


