월마트 스크레이핑 완전 가이드: 상품 데이터 수집부터 우회까지

월마트의 Akamai + PerimeterX 안티봇을 우회하고 __NEXT_DATA__에서 가격·재고·평점·셀러 데이터를 추출하는 실전 가이드입니다. 주거용 프록시 설정과 속도 제한 스케줄링까지 모두 다룹니다.

월마트 스크레이핑 완전 가이드: 상품 데이터 수집부터 우회까지

CPG 브랜드 매니저, 리테일 인텔리전스 팀, 가격 모니터링 스타트업 — 이들에게 월마트 상품 데이터는 경쟁 분석의 핵심입니다. 하지만 월마트는 Akamai Bot Manager와 PerimeterX(현 HUMAN)를 결합한 이중 안티봇 스택으로 스크레이퍼를 차단합니다. 단순 requests.get() 한 번이면 403이 돌아오고, 데이터센터 IP는 몇 요청 만에 블록됩니다.

이 글에서는 월마트 스크레이핑의 실전 접근법을 다룹니다: 카탈로그 구조, 안티봇 우회 전략, __NEXT_DATA__ 파싱, 마켓플레이스(3P) vs 자체(1P) 처리, 그리고 속도 제한을 고려한 스케줄링까지.

월마트 카탈로그 구조 이해하기

월마트의 공개 카탈로그는 세 가지 진입점으로 구성됩니다.

상품 페이지: /ip/{slug}/{itemId}

개별 상품 페이지입니다. itemId는 숫자 ID이고, slug는 SEO용 텍스트입니다. slug가 틀려도 itemId만 맞으면 301 리다이렉트로 올바른 페이지로 이동합니다.

https://www.walmart.com/ip/Apple-AirPods-Pro-2nd-Gen/1752657045

핵심 데이터 — 가격, 재고 상태, 평점, 판매자 정보 — 가 모두 이 페이지에 포함됩니다.

카테고리 페이지

https://www.walmart.com/c/tp/{categorySlug} 형식입니다. 페이지네이션은 쿼리 파라미터 page={n}으로 제어합니다. 각 카테고리 페이지는 40개 아이템을 반환합니다.

검색 결과

https://www.walmart.com/search?q={query}&page={n} 형식입니다. 검색도 카테고리와 동일하게 페이지당 약 40개 아이템을 노출합니다.

API vs HTML 트레이드오프: 월마트는 내부 API(/api/product/{itemId} 등)를 운영하지만, 인증 토큰이 짧게 만료되고 엔드포인트가 예고 없이 변경됩니다. 반면 HTML 페이지는 안정적이며, __NEXT_DATA__ JSON이 이미 완전한 데이터를 담고 있습니다. 대부분의 경우 HTML + JSON 추출이 더 실용적입니다.

Akamai + PerimeterX: 왜 주거용 프록시가 필요한가

월마트의 안티봇 스택은 두 레이어로 동작합니다.

  • Akamai Bot Manager: TLS 핑거프린트, HTTP/2 프레임 순서, 헤더 순서를 검사합니다. Python requests의 기본 TLS 핑거프린트는 즉시 차단됩니다.
  • PerimeterX (HUMAN): 브라우저 내 행동 분석, 마우스/키보드 이벤트, 캔버스 핑거프린팅으로 봇을 탐지합니다.

이중 방어를 통과하려면 다음이 필요합니다.

  1. 주거용 프록시: 데이터센터 IP는 Akamai가 즉시 탐지합니다. 실제 ISP IP가 필요합니다.
  2. 헤드리스 브라우저: Playwright나 Puppeteer로 실제 브라우저 환경을 에뮬레이션합니다.
  3. 요청 속도 제한: 동일 IP에서 초당 1회 이상 요청하면 PerimeterX가 CAPTCHA를 트리거합니다.
프록시 유형Akamai 통과율속도비용월마트 적합도
데이터센터<5%빠름낮음부적합
주거용(회전)85-95%중간중간✅ 대량 수집
주거용(스티키)90-98%중간중간✅ 세션 유지
모바일95%+변동높음✅ 최고 통과율

ProxyHat 주거용 프록시 설정 예시:

# HTTP 주거용 프록시 — 미국 IP
http://user-country-US:PASSWORD@gate.proxyhat.com:8080

# 스티키 세션 (30분 유지)
http://user-country-US-session-abc123:PASSWORD@gate.proxyhat.com:8080

# SOCKS5 주거용 프록시
socks5://user-country-US:PASSWORD@gate.proxyhat.com:1080

__NEXT_DATA__: 가장 쉬운 파싱 경로

월마트는 Next.js로 구축되어 있으며, 모든 상품 페이지 HTML에 <script id="__NEXT_DATA__"> 태그로 완전한 JSON 데이터를 임베드합니다. 이 JSON에는 API를 별도로 호출할 필요 없이 다음 데이터가 모두 포함됩니다.

  • 가격 (정가, 할인가, 단위당 가격)
  • 재고 상태 및 배송 정보
  • 평점 및 리뷰 수
  • 판매자 정보 (1P vs 3P)
  • 상품 속성 (브랜드, 카테고리, 이미지 URL)

HTML에서 JSON을 추출하는 것은 정규식이나 BeautifulSoup으로 간단합니다.

import re
import json
from bs4 import BeautifulSoup

def extract_next_data(html: str) -> dict:
    """HTML에서 __NEXT_DATA__ JSON을 추출합니다."""
    soup = BeautifulSoup(html, "html.parser")
    script = soup.find("script", id="__NEXT_DATA__")
    if not script:
        raise ValueError("__NEXT_DATA__ not found in HTML")
    return json.loads(script.string)

CSS 셀렉터 대안:

# BeautifulSoup 방식
script = soup.select_one("script#__NEXT_DATA__")

# lxml/XPath 방식
tree = html.fromstring(page_html)
script_text = tree.xpath('//script[@id="__NEXT_DATA__"]/text()')[0]

샘플 응답 (truncated):

{
  "props": {
    "pageProps": {
      "initialData": {
        "data": {
          "product": {
            "itemId": "1752657045",
            "name": "Apple AirPods Pro 2nd Gen",
            "priceInfo": {
              "currentPrice": { "price": 189.00, "currencyUnit": "USD" },
              "originalPrice": { "price": 249.00 }
            },
            "rating": { "averageRating": 4.5, "totalReviews": 12453 },
            "availabilityStatus": "IN_STOCK",
            "sellerId": "0",
            "sellerName": "Walmart.com",
            "shortDescription": "Active Noise Cancellation...",
            "imageInfo": { "thumbnailUrl": "https://..." }
          }
        }
      }
    }
  }
}

sellerId: "0"은 월마트 자체(1P)를 의미합니다. 3P 셀러는 다른 숫자 ID를 가집니다.

Python으로 월마트 상품 데이터 추출하기

Playwright + ProxyHat 주거용 프록시를 결합한 전체 예시입니다.

import asyncio
import json
import re
from playwright.async_api import async_playwright

PROXY = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
ITEM_URL = "https://www.walmart.com/ip/Apple-AirPods-Pro-2nd-Gen/1752657045"

async def scrape_walmart_item(url: str) -> dict:
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            proxy={"server": PROXY},
            headless=True
        )
        context = await browser.new_context(
            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"
            ),
            viewport={"width": 1920, "height": 1080},
        )
        page = await context.new_page()
        await page.goto(url, wait_until="networkidle", timeout=30000)

        # __NEXT_DATA__ 추출
        raw = await page.evaluate(
            "() => document.getElementById('__NEXT_DATA__')?.textContent || '{}'"
        )
        next_data = json.loads(raw)
        await browser.close()

    product = next_data["props"]["pageProps"]["initialData"]["data"]["product"]

    # 가격
    current_price = product["priceInfo"]["currentPrice"]["price"]
    original_price = product["priceInfo"].get("originalPrice", {}).get("price")

    # 재고
    availability = product.get("availabilityStatus", "UNKNOWN")

    # 평점
    avg_rating = product["rating"]["averageRating"]
    total_reviews = product["rating"]["totalReviews"]

    # 판매자
    seller_id = product.get("sellerId", "")
    seller_name = product.get("sellerName", "")
    is_1p = seller_id == "0"

    return {
        "item_id": product["itemId"],
        "name": product["name"],
        "current_price": current_price,
        "original_price": original_price,
        "availability": availability,
        "avg_rating": avg_rating,
        "total_reviews": total_reviews,
        "seller_id": seller_id,
        "seller_name": seller_name,
        "is_1p": is_1p,
    }

# 실행
result = asyncio.run(scrape_walmart_item(ITEM_URL))
print(json.dumps(result, indent=2))

출력 예시:

{
  "item_id": "1752657045",
  "name": "Apple AirPods Pro 2nd Gen",
  "current_price": 189.0,
  "original_price": 249.0,
  "availability": "IN_STOCK",
  "avg_rating": 4.5,
  "total_reviews": 12453,
  "seller_id": "0",
  "seller_name": "Walmart.com",
  "is_1p": true
}

마켓플레이스(3P) vs 자체 카탈로그(1P)

월마트 상품의 약 60-70%가 서드파티 셀러의 3P 아이템입니다. 1P와 3P를 구분하는 것은 가격 모니터링에서 매우 중요합니다.

1P (Walmart 자체)

  • sellerId = "0", sellerName = "Walmart.com"
  • 가격이 가장 경쟁력적
  • 배송 정책이 Walmart+와 연동

3P (마켓플레이스 셀러)

  • sellerId가 0이 아닌 숫자
  • 동일 상품에 여러 셀러가 존재할 수 있음 ("다른 판매자" 섹션)
  • 가격, 배송비, 재고가 셀러마다 다름

3P 셀러 데이터는 __NEXT_DATA__offers 배열에 포함되어 있습니다:

# 3P 오퍼 추출
offers = product.get("offers", [])
for offer in offers:
    seller_id = offer.get("sellerId")
    seller_name = offer.get("sellerName")
    offer_price = offer.get("priceInfo", {}).get("currentPrice", {}).get("price")
    print(f"셀러: {seller_name} (ID: {seller_id}), 가격: ${offer_price}")

3P 데이터 수집 시 주의사항:

  • 3P 오퍼는 동적 로딩될 수 있어, 페이지를 끝까지 스크롤해야 __NEXT_DATA__에 반영되는 경우가 있습니다.
  • 일부 3P 셀러는 상품 페이지가 아닌 검색 결과에서만 노출됩니다.
  • 셀러별 배송비는 fulfillment 객체에 포함되며, 무료 배송 임계값이 다를 수 있습니다.

속도 제한을 고려한 스케줄링

월마트의 속도 제한은 대략 다음과 같습니다:

  • 동일 IP 기준: 초당 1-2요청 이상 시 CAPTCHA 트리거
  • 동일 IP 시간당: 약 100-200요청 후 차단 가능
  • PerimeterX CAPTCHA: 한 번 트리거되면 해당 IP는 12-24시간 차단

이를 고려한 안전한 스케줄링 전략:

  1. IP 회전: 요청마다 새 IP를 사용 (ProxyHat 회전 주거용 프록시)
  2. 요청 간격: 최소 2-5초 대기
  3. 시간대 분산: 대량 수집은 미국 새벽 시간대(EST 02:00-06:00)에 실행
  4. 지역 분산: 미국 내 여러 주 IP로 분산
import asyncio
import random
from playwright.async_api import async_playwright

ITEMS = ["1752657045", "1234567890", "9876543210"]  # itemId 목록
BASE_URL = "https://www.walmart.com/ip/_/{item_id}"

# ProxyHat: 요청마다 다른 미국 IP 회전
PROXY_TEMPLATE = "http://user-country-US-session-{session}:PASSWORD@gate.proxyhat.com:8080"

async def scrape_with_rate_limit(item_id: str, session_id: str):
    proxy = PROXY_TEMPLATE.format(session=session_id)
    url = BASE_URL.format(item_id=item_id)

    async with async_playwright() as p:
        browser = await p.chromium.launch(
            proxy={"server": proxy}, headless=True
        )
        context = await browser.new_context(
            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"
            )
        )
        page = await context.new_page()
        try:
            await page.goto(url, wait_until="networkidle", timeout=30000)
            raw = await page.evaluate(
                "() => document.getElementById('__NEXT_DATA__')?.textContent || '{}'"
            )
            data = json.loads(raw)
            product = data["props"]["pageProps"]["initialData"]["data"]["product"]
            return {"item_id": item_id, "name": product.get("name"), "price": product["priceInfo"]["currentPrice"]["price"]}
        except Exception as e:
            return {"item_id": item_id, "error": str(e)}
        finally:
            await browser.close()

async def main():
    results = []
    for i, item_id in enumerate(ITEMS):
        session = f"wmt{i:04d}"
        result = await scrape_with_rate_limit(item_id, session)
        results.append(result)
        # 3-6초 랜덤 대기
        await asyncio.sleep(random.uniform(3, 6))
    return results

results = asyncio.run(main())
for r in results:
    print(r)

실패 처리 팁:

  • CAPTCHA 페이지가 반환되면 해당 IP를 즉시 폐기하고 새 세션으로 전환하세요.
  • 429(Too Many Requests) 응답 시 최소 60초 대기 후 재시도하세요.
  • 연속 3회 실패 시 해당 itemId를 큐의 뒤로 미루세요.
  • 프록시 속도 제한 관리에 대한 상세 가이드도 참고하세요.

curl로 빠른 테스트하기

Playwright 환경 구성 전, curl로 프록시 연결을 먼저 확인하세요:

curl -x http://user-country-US:PASSWORD@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  -H "Accept: text/html,application/xhtml+xml" \
  "https://www.walmart.com/ip/_/1752657045" \
  -o walmart_test.html

# __NEXT_DATA__ 추출 확인
grep -o '__NEXT_DATA__.*</script>' walmart_test.html | head -c 500

curl은 Akamai TLS 핑거프린트 검사를 통과하지 못하므로, 403이 반환될 수 있습니다. 이는 정상입니다 — 실제 스크레이핑은 Playwright/Puppeteer로 진행하세요. curl 테스트는 프록시 연결 자체가 정상인지 확인하는 용도입니다.

핵심 요약

  • __NEXT_DATA__가 최우선: HTML에서 JSON을 추출하는 것이 API 호출보다 안정적이고 간단합니다.
  • 주거용 프록시는 필수: 데이터센터 IP로는 Akamai를 통과할 수 없습니다.
  • Playwright + 회전 프록시: 요청마다 새 IP를 사용하고 3-6초 간격을 유지하세요.
  • 1P vs 3P 구분: sellerId == "0"으로 월마트 자체 상품을 식별합니다.
  • 속도 제한 준수: 초당 1요청 이하, IP당 시간당 100요청 이하를 권장합니다.
  • CAPTCHA 대응: PerimeterX CAPTCHA가 트리거되면 즉시 IP를 교체하세요.

월마트 상품 데이터를 대규모로 수집해야 한다면, ProxyHat 주거용 프록시로 안정적인 미국 IP 풀에 접근할 수 있습니다. 웹 스크레이핑 유스케이스 페이지에서 다른 이커머스 플랫폼 수집 사례도 확인해 보세요.

시작할 준비가 되셨나요?

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

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