이베이 스크래핑 실전 가이드: API vs HTML, 프록시 전략, 코드까지

eBay Finding/Browse API의 할당량 한계와 스크래핑 대안, .s-item 셀렉터 파싱, 경매 데이터 수집, 셀러 분석까지—프록시 전략과 함께 정리합니다.

이베이 스크래핑 실전 가이드: API vs HTML, 프록시 전략, 코드까지

eBay에서 상품 데이터를 대량으로 수집하려는 순간, 가장 먼저 마주하는 질문은 "API를 쓸까, 아니면 HTML을 긁을까?"입니다. 두 접근은 서로 다른 한계를 가지고 있고, 대부분의 리셀러·시장조사 팀은 결국 둘 다 쓰게 됩니다. 이 글에서는 eBay Finding API와 Browse API의 실제 할당량, HTML 스크래핑의 타겟 구조, 프록시 선택 기준, 그리고 작동하는 Python 코드까지 맥락을 잃지 않고 다룹니다.

API vs HTML: eBay에서 데이터를 가져오는 두 가지 길

eBay는 공식적으로 두 가지 주요 API를 제공합니다.

  • Finding API — 키워드 검색, 카테고리 탐색, 항목 필터. findItemsByKeywords, findItemsAdvanced 등.
  • Browse API — 개별 아이템 상세, 카테고리 트리, 호환성 체크. REST 기반.

언뜻 보면 API가 깔끔해 보이지만, 실제로 쓰다 보면 몇 가지 벽에 부딪힙니다.

API 할당량과 접근 제한

Finding API의 기본 호출 한도는 앱당 하루 5,000건(App Check 토큰 기준)입니다. 프로덕션 승인을 받으면 하루 1,000,000건까지 올라가지만, 승인 과정이 까다롭고 사용 사례를 증명해야 합니다. Browse API는 OAuth 2.0 기반으로, 시간당 10,000건의 rate limit이 적용됩니다.

더 큰 문제는 API가 반환하지 않는 필드입니다. 셀러 피드백 상세, 경매 실시간 입찰 히스토리, 상세 배송비 계산, 변형(variation) 재고 상태 등은 HTML에만 노출됩니다. 리셀러 인텔리전스를 하려면 결국 HTML 스크래핑이 필요합니다.

기준Finding / Browse APIHTML 스크래핑
일일 한도5,000 ~ 1,000,000건프록시 대역폭에 의존
데이터 풍부도구조화됨, 일부 필드 누락페이지에 보이는 모든 데이터
인증OAuth 2.0 / App Check불필요 (프록시 필요)
안정성스키마 변경 드묾CSS 구조 변경 시 깨짐
차단 위험거의 없음높음 — 프록시 필수
경매 실시간 데이터제한적완전 접근

실무에서는 API로 기본 메타데이터를 싹 긁고, 누락 필드를 HTML 스크래핑으로 보완하는 하이브리드 방식이 가장 효율적입니다.

eBay HTML 구조: 무엇을, 어떻게 긁을 것인가

검색 결과 그리드 — .s-item

eBay 검색 결과 페이지(https://www.ebay.com/sch/i.html?_nkw=키워드)의 각 아이템은 .s-item 클래스를 가진 <li> 요소에 담깁니다. 주요 하위 셀렉터:

  • .s-item__title — 상품명
  • .s-item__price — 가격 (경매가와 Buy It Now 가격이 분리될 수 있음)
  • .s-item__shipping — 배송비 정보
  • .s-item__bids — 입찰 수 (경매인 경우)
  • .s-item__time-left — 남은 시간
  • .s-item__buy-it-now — 즉시구매 뱃지
  • .s-item__seller-info — 셀러 정보
  • .s-item__link — 상세 페이지 URL

XPath를 선호한다면:

# 제목
//li[contains(@class,'s-item')]//div[contains(@class,'s-item__title')]

# 가격
//li[contains(@class,'s-item')]//span[contains(@class,'s-item__price')]

상세 페이지

개별 상품 페이지(https://www.ebay.com/itm/ITEM_ID)에서는:

  • #ItemSpecifics 또는 [data-testid='x-item-details-specs'] — 아이템 스펙 테이블
  • #vi-desc-mainframe — 상세 설명 (iframe으로 로드될 수 있음)
  • #bidBtn — 경매 입찰 버튼 유무로 경매 여부 판단
  • #prcIsum — Buy It Now 가격
  • #qtySubTxt — 재고 수량

셀러 프로필 페이지

https://www.ebay.com/usr/SELLER_NAME 구조:

  • .seller-info__feedback-score — 피드백 점수
  • .seller-info__positive-feedback — 긍정 피드백 비율
  • #seller_feedback — 최근 피드백 목록

프록시 선택: eBay는 데이터센터 IP를 공격적으로 차단합니다

eBay의 안티봇 시스템은 데이터센터 IP 대역을 매우 적극적으로 차단합니다. Akamai Bot Manager 기반으로, 요청 패턴과 IP 평판을 동시에 평가합니다. 데이터센터 프록시로 몇 분만 요청을 보내도 CAPTCHA 챌린지나 HTTP 429, 혹은 빈 페이지가 반환됩니다.

프록시 유형별 전략

  • 레지덴셜 프록시 — 대규모 스크래핑에 필수. IP가 실제 ISP 대역에서 나오므로 eBay가 차단하기 어렵습니다. 요청당 IP 로테이션이 기본 전략입니다.
  • 모바일 프록시 — 가장 자연스러운 핑거프린트. eBay 앱 트래픽과 유사하게 보이지만, 비용이 높고 대역폭이 제한적입니다.
  • 데이터센터 프록시 — API 호출에만 사용. HTML 스크래핑에는 거의 무용합니다.

지역 타겟팅: eBay.de, eBay.co.uk

eBay는 지역별로 다른 결과를 보여줍니다. 독일 이베이에서만 보이는 상품, 영국 이베이의 지역 가격 차이를 수집하려면 해당 국가 IP가 필요합니다.

# 미국 IP — 기본
http://user-country-US:pass@gate.proxyhat.com:8080

# 독일 IP — eBay.de 전용
http://user-country-DE:pass@gate.proxyhat.com:8080

# 영국 IP — eBay.co.uk 전용
http://user-country-GB:pass@gate.proxyhat.com:8080

# 독일 베를린 시티 레벨 타겟팅
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080

지역 이베이 도메인에 접근할 때는 URL과 프록시 국가를 일치시키세요. ebay.de를 미국 IP로 접속하면 리다이렉트나 지역 한정 상품 누락이 발생합니다.

Python 실전: 검색 결과 수집 + .s-item 파싱

가장 많이 쓰이는 패턴 — 키워드로 검색, .s-item에서 구조화된 레코드를 추출합니다.

import requests
from bs4 import BeautifulSoup
import json
import time
import re

PROXY = "http://user-country-US:pass@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY, "https": PROXY}

HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/125.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
}

def scrape_ebay_search(keyword, pages=3):
    """eBay 검색 결과를 페이지별로 수집합니다."""
    records = []
    base_url = "https://www.ebay.com/sch/i.html"

    for page in range(1, pages + 1):
        params = {
            "_nkw": keyword,
            "_pgn": page,
        }
        resp = requests.get(
            base_url,
            params=params,
            headers=HEADERS,
            proxies=PROXIES,
            timeout=30,
        )
        resp.raise_for_status()
        soup = BeautifulSoup(resp.text, "html.parser")
        items = soup.select("li.s-item")

        for item in items:
            title_el = item.select_one(".s-item__title")
            price_el = item.select_one(".s-item__price")
            bids_el = item.select_one(".s-item__bids")
            time_el = item.select_one(".s-item__time-left")
            bin_el = item.select_one(".s-item__buy-it-now")
            link_el = item.select_one(".s-item__link")
            shipping_el = item.select_one(".s-item__shipping")
            seller_el = item.select_one(".s-item__seller-info")

            price_text = price_el.get_text(strip=True) if price_el else None
            price_val = None
            if price_text:
                m = re.search(r'[\d,]+\.?\d*', price_text.replace(',', ''))
                if m:
                    price_val = float(m.group())

            record = {
                "title": title_el.get_text(strip=True) if title_el else None,
                "price": price_val,
                "price_raw": price_text,
                "bids": bids_el.get_text(strip=True) if bids_el else None,
                "time_left": time_el.get_text(strip=True) if time_el else None,
                "is_buy_it_now": bin_el is not None,
                "url": link_el["href"] if link_el else None,
                "shipping": shipping_el.get_text(strip=True) if shipping_el else None,
                "seller": seller_el.get_text(strip=True) if seller_el else None,
            }
            records.append(record)

        # 페이지 간 2~5초 랜덤 대기
        time.sleep(2 + (page % 3))

    return records


if __name__ == "__main__":
    results = scrape_ebay_search("vintage rolex", pages=2)
    print(json.dumps(results[:3], indent=2, ensure_ascii=False))

샘플 출력 (truncated):

[
  {
    "title": "Vintage Rolex Submariner 1680 Mat Dial",
    "price": 8950.0,
    "price_raw": "$8,950.00",
    "bids": null,
    "time_left": null,
    "is_buy_it_now": true,
    "url": "https://www.ebay.com/itm/12...",
    "shipping": "Free shipping",
    "seller": "luxury_watches_nyc (2,341)"
  },
  {
    "title": "Rolex Datejust 36mm Vintage 1978",
    "price": 4200.0,
    "price_raw": "$4,200.00",
    "bids": "18 bids",
    "time_left": "2h 15m left",
    "is_buy_it_now": false,
    "url": "https://www.ebay.com/itm/34...",
    "shipping": "+$25.00 shipping",
    "seller": "timekeeper_uk (587)"
  }
]

경매 데이터 다루기: 남은 시간, 입찰 수, Buy It Now 플래그

경매 아이템은 시간에 민감합니다. time-to-end, bid count, Buy It Now 여부 세 가지를 정확히 수집하는 것이 경매 분석의 핵심입니다.

검색 결과에서의 경매 신호

.s-item__bids가 존재하면 경매 아이템입니다. .s-item__time-left에서 "6d 4h left" 같은 문자열을 파싱합니다. .s-item__buy-it-now 요소가 있으면 BIN 옵션이 활성화된 경매입니다.

상세 페이지에서의 정밀 데이터

상세 페이지에서는 더 정확한 경매 정보를 얻을 수 있습니다:

import re
from datetime import datetime, timedelta

def parse_auction_detail(soup):
    """eBay 상세 페이지에서 경매 정보를 추출합니다."""
    result = {}

    # 경매 종료 시간 — ISO 포맷으로 제공됨
    time_el = soup.select_one("[data-testid='x-item-details-end-time']")
    if not time_el:
        # 폴백: 텍스트 파싱
        time_el = soup.select_one("#vi-cdown_timeLeft")
    if time_el:
        result["end_text"] = time_el.get_text(strip=True)

    # 현재 입찰가
    bid_el = soup.select_one("#prcIsum_bid")
    if not bid_el:
        bid_el = soup.select_one("[data-testid='x-item-details-bid-price']")
    if bid_el:
        bid_text = bid_el.get_text(strip=True)
        m = re.search(r'[\d,]+\.?\d*', bid_text.replace(',', ''))
        result["current_bid"] = float(m.group()) if m else None

    # 입찰 수
    bids_el = soup.select_one("[data-testid='x-item-details-bid-count']")
    if not bids_el:
        bids_el = soup.select_one("#qty-test")
    if bids_el:
        m = re.search(r'\d+', bids_el.get_text())
        result["bid_count"] = int(m.group()) if m else None

    # Buy It Now 여부
    bin_btn = soup.select_one("#bidBtn")
    bin_link = soup.select_one("[data-testid='x-item-details-buy-it-now']")
    result["has_buy_it_now"] = bin_link is not None

    # BIN 가격
    bin_price_el = soup.select_one("#prcIsum")
    if not bin_price_el:
        bin_price_el = soup.select_one("[data-testid='x-item-details-bin-price']")
    if bin_price_el:
        m = re.search(r'[\d,]+\.?\d*', bin_price_el.get_text().replace(',', ''))
        result["bin_price"] = float(m.group()) if m else None

    return result


# 사용 예
# soup = BeautifulSoup(detail_html, "html.parser")
# auction_info = parse_auction_detail(soup)
# print(auction_info)
# {'current_bid': 4150.0, 'bid_count': 18, 'has_buy_it_now': True, 'bin_price': 5500.0}

경매 종료 5분 전 "스니핑" 데이터를 수집하려면 스티키 세션 프록시가 필요합니다. 요청마다 IP가 바뀌면 세션이 끊깁니다. user-session-abc123 형식으로 고정 세션을 사용하세요.

셀러 분석: 피드백, 카테고리, 크로스 리스팅 패턴

리셀러 인텸리전스에서 셀러 단위 분석은 핵심입니다. 어떤 셀러가 어느 카테고리에 얼마나 많이 리스팅하는지, 피드백 트렌드는 어떤지 파악하면 시장 동향을 읽을 수 있습니다.

def scrape_seller_profile(seller_name, proxy_url=None):
    """셀러 프로필 페이지에서 분석 데이터를 수집합니다."""
    proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
    url = f"https://www.ebay.com/usr/{seller_name}"

    resp = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")

    profile = {"seller": seller_name}

    # 피드백 점수
    fb_score = soup.select_one(".seller-info__feedback-score")
    if fb_score:
        m = re.search(r'[\d,]+', fb_score.get_text().replace(',', ''))
        profile["feedback_score"] = int(m.group()) if m else None

    # 긍정 피드백 비율
    pos_fb = soup.select_one(".seller-info__positive-feedback")
    if pos_fb:
        m = re.search(r'([\d.]+)%', pos_fb.get_text())
        profile["positive_pct"] = float(m.group(1)) if m else None

    # 등록 기간
    since_el = soup.select_one(".seller-info__since")
    if since_el:
        profile["member_since"] = since_el.get_text(strip=True)

    # 최근 피드백 샘플
    feedbacks = []
    for fb in soup.select("#seller_feedback .fb-item")[:10]:
        fb_text = fb.select_one(".fb-item__comment")
        fb_item = fb.select_one(".fb-item__item-title")
        feedbacks.append({
            "comment": fb_text.get_text(strip=True) if fb_text else None,
            "item": fb_item.get_text(strip=True) if fb_item else None,
        })
    profile["recent_feedbacks"] = feedbacks

    # 리스팅 카테고리 분포 (프로필 페이지의 카테고리 링크)
    categories = []
    for cat_el in soup.select(".seller-info__category-link"):
        categories.append(cat_el.get_text(strip=True))
    profile["categories"] = categories

    return profile


# 사용 예
proxy = "http://user-country-US:pass@gate.proxyhat.com:8080"
data = scrape_seller_profile("luxury_watches_nyc", proxy_url=proxy)
print(json.dumps(data, indent=2, ensure_ascii=False))

크로스 리스팅 패턴 감지

셀러가 여러 카테고리에 동일한 상품을 리스팅하는 패턴을 발견하면, 해당 셀러의 재고 회전율과 경쟁 전략을 유추할 수 있습니다. 방법:

  1. 셀러의 활성 리스팅을 검색: https://www.ebay.com/sch/m.html?_ssn=SELLER_NAME&_pgn=1
  2. 각 리스팅의 카테고리와 가격을 수집
  3. 동일 상품명 + 다른 카테고리 조합을 필터링
  4. 시계열로 수집하면 리스팅-삭제-재리스팅 패턴도 포착 가능

eBay 안티봇 기술과 회피 전략

eBay는 Akamai Bot ManagerPerimeterX (HUMAN)을 혼합 사용합니다. 주요 탐지 벡터:

  • IP 평판 — 데이터센터 대역, 과거 스팸 기록이 있는 IP는 즉시 차단
  • 브라우저 핑거프린트 — TLS 핑거프린트, HTTP/2 프레임 순서, JavaScript 실행 여부
  • 요청 패턴 — 일정한 간격, 순차적 페이지 탐색, 빠른 속도
  • 헤더 일관성 — User-Agent와 실제 TLS 핸드셰이크 불일치

실무적 회피 전략:

  1. 레지덴셜 프록시 사용 — 가장 기본. 데이터센터 IP는 시작부터 불가합니다.
  2. 요청 간 2~8초 랜덤 대기 — 일정한 간격(정확히 3초 등)은 봇으로 탐지됩니다.
  3. 세션 유지 — 쿠키를 유지하고, 한 IP에서 자연스러운 탐색 시퀀스를 만듭니다.
  4. JavaScript 렌더링이 필요한 경우 — Playwright + 레지덴셜 프록시 조합. 하지만 속도가 느려지므로 꼭 필요한 경우만.
  5. rate limit 모니터링 — 429 응답이 오면 즉시 해당 IP를 쿨다운시킵니다.

curl로 빠르게 테스트하기

Python 코드를 작성하기 전에 프록시 연결과 응답 구조를 확인하려면:

# 기본 검색 — 미국 IP
curl -x http://user-country-US:pass@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://www.ebay.com/sch/i.html?_nkw=airpods+pro&_pgn=1" \
  -o ebay_search.html

# 독일 이베이 — 독일 IP
curl -x http://user-country-DE:pass@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://www.ebay.de/sch/i.html?_nkw=airpods+pro&_pgn=1" \
  -o ebay_de_search.html

# SOCKS5로 연결 (HTTP가 차단된 경우 대안)
curl -x socks5://user-country-US:pass@gate.proxyhat.com:1080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://www.ebay.com/sch/i.html?_nkw=airpods+pro" \
  -o ebay_socks5.html

합법성과 윤리: 지켜야 할 선

eBay 스크래핑은 기술적으로 가능하지만, 몇 가지를 반드시 고려해야 합니다:

  • robots.txt 준수 — eBay의 robots.txt는 특정 경로를 금지합니다. /sch/ 경로는 현재 크롤링을 명시적으로 금지하지 않지만, 이는 언제든 변경될 수 있습니다.
  • 이용약관 — eBay의 User Agreement는 스크래핑을 금지합니다. 실무적으로는 개인적·연구 목적의 소규모 수집은 대부분 무시되지만, 대규모 상업적 수집은 법적 리스크가 있습니다.
  • GDPR / CCPA — 셀러 개인정보(이름, 위치 등)를 수집·저장할 때는 해당 개인정보 보호법을 준수해야 합니다.
  • 속도 제한 — eBay 서버에 부하를 주는 속도로 수집하는 것은 서비스 장애를 유발할 수 있으며, 이는 법적 문제로 이어질 수 있습니다.

이 글의 목적은 교육적 정보 제공입니다. 실제 스크래핑 전에는 반드시 법적 자문을 받으세요.

Key Takeaways

  • eBay API는 하루 5,000~1,000,000건 한도가 있고, 경매 실시간 데이터·셀러 피드백 상세 등을 반환하지 않아 HTML 스크래핑 보완이 필수입니다.
  • .s-item 셀렉터로 검색 결과를, #prcIsum / #bidBtn 등으로 상세 페이지를 파싱합니다. eBay는 주기적으로 CSS 클래스명을 변경하므로 정기 모니터링이 필요합니다.
  • 데이터센터 프록시는 eBay에서 거의 작동하지 않습니다. 레지덴셜 프록시가 기본 선택입니다.
  • 지역 이베이(eBay.de, eBay.co.uk) 데이터를 수집하려면 해당 국가 IP로 접속하세요. ProxyHat 레지덴셜 프록시로 국가·도시 단위 타겟팅이 가능합니다.
  • 경매 스니핑 데이터를 수집할 때는 스티키 세션(user-session-xxx) 프록시를 사용해 IP를 유지하세요.
  • 요청 간 2~8초 랜덤 대기, 자연스러운 헤더 구성, rate limit 모니터링이 차단 회피의 핵심입니다.

대규모 eBay 데이터 수집을 시작할 준비가 되었다면, ProxyHat 요금제에서 레지덴셜 프록시를 확인하거나, 지원 국가 목록에서 타겟 지역의 IP 가용성을 확인하세요.

시작할 준비가 되셨나요?

AI 필터링으로 148개국 이상에서 5천만 개 이상의 레지덴셜 IP에 액세스하세요.

가격 보기레지덴셜 프록시
← 블로그로 돌아가기