Feed Rescue: Raw Ulta Scrapes를 Google Merchant Center XML로 변환
Source: Dev.to
번역할 텍스트가 제공되지 않았습니다. 번역이 필요한 본문을 알려주시면 도와드리겠습니다.
Phase 1 – 원본 데이터 분석
XML을 생성하기 전에 원시 데이터를 이해해야 합니다.
Ulta.com‑Scrapers 저장소(셀레니움 및 Playwright 버전)는 각 라인이 동일한 ScrapedData 데이터클래스를 따르는 JSONL 파일을 출력합니다.
전형적인 원시 레코드:
{
"productId": "2583561",
"name": "CeraVe - Hydrating Facial Cleanser",
"brand": "CeraVe",
"price": 17.99,
"currency": "USD",
"availability": "in_stock",
"description": "Hydrating Facial Cleanser gently removes dirt...",
"images": [
{
"url": "https://media.ulta.com/i/ulta/2583561?w=2000&h=2000",
"altText": "Product Image"
}
],
"url": "https://www.ulta.com/p/hydrating-facial-cleanser-pimprod2001719"
}
왜 JSONL인가?
라인 구분 형식이기 때문에 전체(잠재적으로 500 MB) 데이터셋을 메모리에 로드하지 않고도 파일을 제품별로 스트리밍할 수 있습니다.
2단계 – Google Merchant Center 사양
Google Shopping은 g: 네임스페이스를 사용하는 RSS 2.0(또는 Atom) 피드만 허용합니다. 아래는 Ulta 스크래퍼 필드와 GMC XML 태그 간의 필수 매핑입니다.
| Ulta 스크래퍼 필드 | GMC XML 태그 | 요구 사항 / 비고 |
|---|---|---|
productId | g:id | 고유 식별자 |
name | g:title | 최대 150자 |
description | g:description | 일반 텍스트, 깨진 HTML 없음 |
url | g:link | 절대 URL |
images[0]['url'] | g:image_link | 고해상도 기본 이미지 |
price + currency | g:price | 형식: 17.99 USD |
availability | g:availability | in_stock, out_of_stock, preorder 중 하나여야 함 |
brand | g:brand | 선택 사항이지만 권장 |
| (static) | g:condition | 모든 제품에 대해 new 로 설정 |
Phase 3 – 필드 매핑 및 변환 로직
1. Price Normalization
def format_gmc_price(amount: float, currency: str) -> str:
"""
Return a price string in the format required by Google Merchant Center:
e.g. 17.99 USD
"""
return f"{amount:.2f} {currency}"
소수점 두 자리 정밀도를 보장하고 ISO‑4217 통화 코드를 추가합니다.
2. Availability Mapping
스크래퍼는 이미 in_stock, out_of_stock, 또는 preorder를 반환합니다.
안전을 위해 예상치 못한 값은 모두 out_of_stock으로 매핑합니다:
def map_availability(value: str) -> str:
allowed = {"in_stock", "out_of_stock", "preorder"}
return value if value in allowed else "out_of_stock"
3. Image Handling
Google은 기본 이미지 하나(g:image_link)와 최대 10개의 추가 이미지(g:additional_image_link)를 요구합니다.
리스트의 첫 번째 이미지를 기본 링크로 사용하고, 다음 아홉 개(있는 경우)를 추가 링크로 사용합니다.
def split_images(images: list) -> tuple[str | None, list[str]]:
"""
Returns (primary_image_url, list_of_additional_image_urls)
"""
if not images:
return None, []
primary = images[0].get("url")
additional = [img.get("url") for img in images[1:11] if img.get("url")]
return primary, additional
Phase 4 – 변환기 스크립트 구축
파이프라인은 JSONL 파일을 스트리밍하고 각 레코드를 변환한 뒤 xml.etree.ElementTree를 사용하여 pretty‑printed XML 피드를 작성합니다.
import json
import xml.etree.ElementTree as ET
from xml.dom import minidom
# ----------------------------------------------------------------------
# Helper functions (price, availability, image handling)
# ----------------------------------------------------------------------
def format_gmc_price(amount, currency):
return f"{float(amount):.2f} {currency}"
def map_availability(value):
allowed = {"in_stock", "out_of_stock", "preorder"}
return value if value in allowed else "out_of_stock"
def split_images(images):
if not images:
return None, []
primary = images[0].get("url")
additional = [img.get("url") for img in images[1:11] if img.get("url")]
return primary, additional
# ----------------------------------------------------------------------
def create_gmc_feed(input_jsonl: str, output_xml: str) -> None:
"""Read a JSONL file line‑by‑line and write a Google Merchant Center RSS feed."""
# Namespace registration
g_ns = "http://base.google.com/ns/1.0"
ET.register_namespace('g', g_ns)
# Root element
rss = ET.Element("rss", version="2.0")
channel = ET.SubElement(rss, "channel")
ET.SubElement(channel, "title").text = "Ulta Product Feed"
ET.SubElement(channel, "link").text = "https://www.ulta.com"
ET.SubElement(channel, "description").text = "Daily product updates from Ulta"
# Stream the JSONL file
with open(input_jsonl, "r", encoding="utf-8") as f:
for line in f:
if not line.strip():
continue # skip empty lines
product = json.loads(line)
# Container
item = ET.SubElement(channel, "item")
# Basic required fields
ET.SubElement(item, f"{{{g_ns}}}id").text = str(product.get("productId"))
ET.SubElement(item, f"{{{g_ns}}}title").text = product.get("name", "")[:150]
ET.SubElement(item, f"{{{g_ns}}}description").text = product.get("description", "")
ET.SubElement(item, f"{{{g_ns}}}link").text = product.get("url")
ET.SubElement(item, f"{{{g_ns}}}brand").text = product.get("brand")
ET.SubElement(item, f"{{{g_ns}}}condition").text = "new"
# Price
price = product.get("price")
currency = product.get("currency", "USD")
if price is not None:
ET.SubElement(item, f"{{{g_ns}}}price").text = format_gmc_price(price, currency)
# Availability
ET.SubElement(item, f"{{{g_ns}}}availability").text = map_availability(
product.get("availability", "out_of_stock")
)
# Images
primary_img, additional_imgs = split_images(product.get("images", []))
if primary_img:
ET.SubElement(item, f"{{{g_ns}}}image_link").text = primary_img
for img_url in additional_imgs:
ET.SubElement(item, f"{{{g_ns}}}additional_image_link").text = img_url
# Serialize and pretty‑print
raw_xml = ET.tostring(rss, encoding="utf-8")
pretty_xml = minidom.parseString(raw_xml).toprettyxml(indent=" ")
# Write to file
with open(output_xml, "w", encoding="utf-8") as out_f:
out_f.write(pretty_xml)
# ----------------------------------------------------------------------
# Example usage
# ----------------------------------------------------------------------
if __name__ == "__main__":
create_gmc_feed("ulta_products.jsonl", "ulta_gmc_feed.xml")
스크립트가 수행하는 작업
- Google에서 요구하는
g:네임스페이스를 등록합니다. - 입력 JSONL 파일을 라인 단위로 스트리밍하여(상수 메모리 사용) 처리합니다.
- Phase 2의 표에 따라 각 필드를 매핑하고, 가격, 재고 상태, 이미지에 대한 헬퍼 함수를 적용합니다.
- 깔끔하게 들여쓰기된 XML 파일을 작성합니다.
ulta_gmc_feed.xml 파일이 Google Merchant Center에 업로드할 준비가 되었습니다.
Phase 5 – 엣지 케이스 처리
웹 데이터는 지저분합니다. Ulta 스크래핑을 처리할 때 흔히 마주치는 세 가지 문제는 다음과 같습니다:
- 설명에 포함된 HTML – Ulta의 설명에는 때때로
와 같은 원시 HTML 태그나 엔티티가 포함됩니다. 스크래퍼가 대부분을 정리하지만, 설명을CDATA섹션에 감싸거나 남아 있는 태그를 제거하기 위해 정규식(regex)을 사용하는 것이 더 안전합니다. - 절대 URL – 리포지토리의
make_absolute_url로직을 사용하도록 스크래퍼를 설정하세요. Google은/p/product-name과 같은 상대 URL을 거부합니다. - 가격이 0이거나 누락된 경우 – 가끔 제품에 “Price Varies” 또는 “Out of Stock”와 같이 숫자 값이 없는 경우가 있습니다.
price가None이면:.2f포맷팅이 실패합니다. 가격이 없을 때는 항상0.00을 기본값으로 사용하거나 해당 항목을 건너뛰세요.
if __name__ == "__main__":
create_gmc_feed('ulta_data.jsonl', 'google_feed.xml')
마무리
원시 스크래퍼 데이터를 기능적인 마케팅 자산으로 전환하면 원시 데이터를 비즈니스 가치로 바꿀 수 있습니다. JSONL과 GMC XML 사이의 격차를 연결하면 스크래핑 파이프라인에서 직접 재고 업데이트를 자동화할 수 있습니다.
주요 내용
- 데이터 스트리밍: 대용량 데이터셋을 처리하려면 JSONL과 라인‑바이‑라인 처리를 사용하세요.
- 스키마 준수: Google은 형식에 엄격합니다. 가격에 통화 코드를 항상 포함하고 가용성을 세 가지 특정 열거형(enum) 중 하나에 매핑하세요.
- 파이프라인 자동화: 스크래퍼가 완료된 직후 이 스크립트를 트리거하여 손이 닿지 않는 데이터‑투‑광고 파이프라인을 구축하세요.
초기 추출에 대한 자세한 내용은 ScrapeOps Residential Proxy Aggregator와 Ulta.com‑Scrapers repository에서 전체 구현 범위를 확인하세요.