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

월마트의 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 풀에 접근할 수 있습니다. 웹 스크레이핑 유스케이스 페이지에서 다른 이커머스 플랫폼 수집 사례도 확인해 보세요.

자주 묻는 질문

월마트 스크레이핑 시 데이터센터 프록시를 사용해도 되나요?

권장하지 않습니다. 월마트는 Akamai Bot Manager를 사용해 데이터센터 IP를 즉시 탐지하고 차단합니다. 주거용 프록시(Residential Proxy)를 사용해야 하며, 모바일 프록시가 가장 높은 통과율을 보입니다.

__NEXT_DATA__ 외에 월마트 상품 데이터를 가져오는 다른 방법이 있나요?

월마트 내부 API 엔드포인트(/api/product/{itemId} 등)가 존재하지만, 인증 토큰이 짧게 만료되고 엔드포인트가 예고 없이 변경됩니다. __NEXT_DATA__ 추출이 가장 안정적이고 유지보수 비용이 낮습니다.

월마트 1P 상품과 3P 마켓플레이스 상품을 어떻게 구분하나요?

__NEXT_DATA__ JSON에서 sellerId가 "0"이면 월마트 자체(1P) 상품입니다. 0이 아닌 숫자는 서드파티 셀러의 3P 상품입니다. 3P 상품은 동일 itemId에 여러 오퍼가 존재할 수 있습니다.

PerimeterX CAPTCHA가 트리거되면 어떻게 대응해야 하나요?

해당 IP를 즉시 폐기하고 새 주거용 IP로 전환하세요. ProxyHat 회전 프록시를 사용하면 요청마다 자동으로 새 IP가 할당됩니다. 연속으로 CAPTCHA가 발생하면 요청 속도를 5-10초 간격으로 낮추세요.

월마트 스크레이핑은 합법인가요?

공개 접근 가능한 웹페이지에서 데이터를 수집하는 것 자체는 일반적으로 합법이지만, 월마트 이용약관(Terms of Use)은 스크레이핑을 금지하고 있습니다. 수집한 데이터의 용도, 저작권, 경쟁법 등을 검토하고 법적 자문을 받으세요. robots.txt도 준수해야 합니다.

차단 없이 가격과 경쟁사를 추적하세요

이커머스 데이터를 위한 안정적인 주거용 프록시. 가입하고 깨끗한 데이터를 수집하세요.

시작하기
← 블로그로 돌아가기