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): 브라우저 내 행동 분석, 마우스/키보드 이벤트, 캔버스 핑거프린팅으로 봇을 탐지합니다.
이중 방어를 통과하려면 다음이 필요합니다.
- 주거용 프록시: 데이터센터 IP는 Akamai가 즉시 탐지합니다. 실제 ISP IP가 필요합니다.
- 헤드리스 브라우저: Playwright나 Puppeteer로 실제 브라우저 환경을 에뮬레이션합니다.
- 요청 속도 제한: 동일 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시간 차단
이를 고려한 안전한 스케줄링 전략:
- IP 회전: 요청마다 새 IP를 사용 (ProxyHat 회전 주거용 프록시)
- 요청 간격: 최소 2-5초 대기
- 시간대 분산: 대량 수집은 미국 새벽 시간대(EST 02:00-06:00)에 실행
- 지역 분산: 미국 내 여러 주 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 풀에 접근할 수 있습니다. 웹 스크레이핑 유스케이스 페이지에서 다른 이커머스 플랫폼 수집 사례도 확인해 보세요.






