Etsy 抓取实战:POD 卖家与细分市场研究完全指南

从 Etsy 搜索结构、反爬机制到 Python 代码实战,手把手教你用住宅代理做细分市场研究——不抄袭设计,只挖掘数据洞察。

Etsy 抓取实战:POD 卖家与细分市场研究完全指南

为什么 POD 团队需要抓取 Etsy 数据

如果你在做按需印刷(Print-on-Demand)生意,Etsy 是最值得研究的平台之一。它拥有超过 700 万活跃卖家、近 1 亿件商品,覆盖从定制 T 恤到数字下载的每一个细分市场。问题是:Etsy 没有公开的数据导出 API,搜索结果页只展示 250 页,而且 Cloudflare 和内部限速让批量抓取变得棘手。

本文从 API vs HTML 的取舍开始,逐步拆解 Etsy 的页面结构、反爬机制、细分市场研究模式,并给出完整的 Python 代码。我们的原则很明确:抓取是为了研究市场,不是抄袭设计

Etsy 的页面结构与数据入口

Etsy 没有官方公开 API(Etsy Open API 已关闭新申请),所以所有数据都得从 HTML 页面提取。理解页面结构是高效抓取的前提。

搜索结果页

搜索 URL 格式:

https://www.etsy.com/search?q=custom+dog+mug&ref=search_bar

关键参数:

  • q — 搜索关键词
  • page — 页码(最多 250 页)
  • order — 排序方式:date_desc(最新)、price_asc(价格升序)等
  • min_price / max_price — 价格区间过滤
  • ship_to — 目的地国家

每个搜索结果页包含约 48 张 listing 卡片。卡片 HTML 中嵌入了 JSON-LD 结构化数据(script[type="application/ld+json"]),这是最可靠的数据来源。

商品详情页(Listing Page)

URL 格式:

https://www.etsy.com/listing/123456789/custom-dog-mug

详情页的关键数据点:

  • 标题与描述 — 直接在 DOM 中
  • 价格[data-selector="price-only"] 或 CSS 类 .wt-text-title-01
  • 销量徽章 — "x sales" 文本,在 .wt-text-caption 附近
  • 评价数与星级[aria-label*="out of 5 stars"]
  • 变体选项 — 下拉菜单中的尺寸、颜色等
  • 卖家信息 — 店铺名称和链接在 [data-shop-id] 属性附近

详情页同样包含 JSON-LD,里面有 offersaggregateRating 等结构化字段。

店铺页面(Shop Page)

URL 格式:

https://www.etsy.com/shop/ShopName

店铺页提供:

  • 总销量 — "XX,XXX Sales" 徽章
  • 在售商品数 — 列表计数
  • 评价概览 — 星级分布
  • 创建时间 — "On Etsy since 2019"

分类树

Etsy 的分类层级大致为:顶级分类(如 Jewelry & Accessories)→ 子分类(如 Necklaces)→ 更细分的标签。你可以从 https://www.etsy.com/c/jewelry 这样的分类页面出发,提取面包屑导航和侧边栏筛选器来构建分类树。

Etsy 的反爬机制与代理策略

Etsy 使用多层防护:

  1. Cloudflare — 默认开启 Bot Management,挑战请求头指纹异常的客户端
  2. 内部限速 — 同一 IP 短时间内请求超过约 30-50 次会触发 429 或验证码
  3. JavaScript 渲染 — 部分内容(如评价、价格)需要 JS 执行才能显示
  4. 行为检测 — 请求模式过于规律会被标记

这就是为什么住宅代理是抓取 Etsy 的首选。数据中心 IP 容易被 Cloudflare 识别并拦截,而住宅 IP 看起来像真实用户。

代理类型Etsy 兼容性速度成本适用场景
数据中心代理❌ 极易被封极快不推荐用于 Etsy
住宅代理✅ 高通过率中等中高搜索页、详情页抓取
移动代理✅ 最高通过率较慢高频请求、绕过严格检测

对于大多数 POD 研究场景,住宅代理 + 每次请求换 IP(rotating session)就足够了。如果需要维持登录态或购物车,用 sticky session。

💡 ProxyHat 的住宅代理支持按国家和城市定位,这对研究不同市场的 Etsy 定价非常有用。例如,用美国 IP 查看 Etsy 美国站的价格和运费。

细分市场研究:抓取模式与指标

POD 卖家最关心的三个指标:

1. 热门搜索词(Trending Keywords)

Etsy 搜索框的自动补全接口是一个金矿:

https://www.etsy.com/api/v3/ajax/member/search-suggestions?query=custom+dog

这个接口返回与关键词相关的热门搜索建议。你可以用字母 a-z 逐个前缀请求,挖掘长尾关键词。

2. 每个细分市场的卖家数量

搜索某个关键词后,Etsy 会显示结果总数(如 "Over 100,000 results")。结合筛选后的结果数,你可以估算市场饱和度:

  • 结果数多 + 卖家分散 → 竞争激烈但需求大
  • 结果数少 + 卖家集中 → 可能是蓝海,但需求有限

3. 平均价格点

从搜索结果页的 JSON-LD 中批量提取价格,计算分布。价格区间告诉你:

  • 低价区间($5-15)— 通常是数字下载或简单定制
  • 中价区间($15-35)— 标准 POD 产品的主战场
  • 高价区间($35+)— 手工定制或高端定位

Python 实战:搜索 → 列表解析 → 详情页抓取

下面是完整的 Python 脚本,使用 ProxyHat 住宅代理抓取 Etsy 搜索结果,然后逐个访问详情页。

import requests
from bs4 import BeautifulSoup
import json
import time
import random

# ProxyHat 住宅代理配置
PROXY = "http://user-country-US:PASSWORD@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/124.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9",
    "Accept-Language": "en-US,en;q=0.9",
}

def fetch_search(keyword, page=1):
    """抓取 Etsy 搜索结果页"""
    url = "https://www.etsy.com/search"
    params = {"q": keyword, "page": page}
    resp = requests.get(url, params=params, headers=HEADERS,
                        proxies=PROXIES, timeout=30)
    resp.raise_for_status()
    return resp.text

def parse_search_listings(html):
    """从搜索结果页提取 listing 卡片数据"""
    soup = BeautifulSoup(html, "html.parser")
    listings = []
    # 提取 JSON-LD 结构化数据
    for script in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(script.string)
            if isinstance(data, dict) and data.get("@type") == "ItemList":
                for item in data.get("itemListElement", []):
                    prod = item.get("item", {})
                    listings.append({
                        "title": prod.get("name", ""),
                        "url": prod.get("url", ""),
                        "price": float(
                            prod.get("offers", {})
                                .get("lowPrice", 0)
                        ),
                        "rating": float(
                            prod.get("aggregateRating", {})
                                .get("ratingValue", 0)
                        ),
                        "review_count": int(
                            prod.get("aggregateRating", {})
                                .get("reviewCount", 0)
                        ),
                    })
        except (json.JSONDecodeError, TypeError):
            continue
    return listings

def fetch_listing_detail(listing_url):
    """抓取单个 listing 详情页"""
    resp = requests.get(listing_url, headers=HEADERS,
                        proxies=PROXIES, timeout=30)
    resp.raise_for_status()
    return resp.text

def parse_listing_detail(html):
    """解析 listing 详情页,提取销量、评价等"""
    soup = BeautifulSoup(html, "html.parser")
    result = {}
    # 销量徽章 — 查找包含 "sales" 的文本
    sales_span = soup.find(string=re.compile(r"\d+\s+sales?"))
    if sales_span:
        result["sales"] = int(
            re.search(r"(\d+)", sales_span).group(1)
        )
    # 评价数
    review_el = soup.select_one("[aria-label*='out of 5 stars']")
    if review_el:
        result["stars"] = float(
            re.search(r"(\d+\.?\d*)", review_el["aria-label"]).group(1)
        )
    return result

# === 主流程 ===
keyword = "custom dog mug"
print(f"正在抓取关键词: {keyword}")
html = fetch_search(keyword, page=1)
listings = parse_search_listings(html)
print(f"找到 {len(listings)} 个商品")

# 取前 10 个 listing 的详情
for i, listing in enumerate(listings[:10]):
    print(f"[{i+1}] {listing['title'][:50]}... "
          f"${listing['price']}")
    detail_html = fetch_listing_detail(listing["url"])
    detail = parse_listing_detail(detail_html)
    listing.update(detail)
    # 随机延迟,避免触发限速
    time.sleep(random.uniform(2, 5))

# 输出结果摘要
avg_price = sum(l["price"] for l in listings[:10]) / 10
print(f"\n平均价格: ${avg_price:.2f}")
for l in listings[:10]:
    print(f"  {l.get('sales', 'N/A')} sales | "
          f"${l['price']} | {l['title'][:40]}")

这个脚本的流程是:搜索 → 解析 JSON-LD → 逐个访问详情页 → 提取销量和评价。每次请求都通过 ProxyHat 住宅代理发出,IP 轮换避免被封。

关键注意事项

  • 延迟很重要 — 即使有代理轮换,请求间隔至少 2-5 秒
  • JSON-LD 优先 — 比解析 CSS 类名更稳定,Etsy 改版时 JSON-LD 通常不变
  • 错误处理 — 遇到 429 或 403 时,增加延迟并重试

店铺分析:销量、评价与竞争情报

了解一个细分市场不仅要看商品,还要看卖家。Etsy 在店铺页面暴露了几个关键数据点:

销量徽章解析

Etsy 用 "X Sales" 徽章显示店铺总销量。这个数字是近似值(如 "5,000+ Sales"),但对于市场研究来说已经足够。解析方法:

import re
from bs4 import BeautifulSoup

def parse_shop_sales(html):
    soup = BeautifulSoup(html, "html.parser")
    # 查找销量徽章
    sales_text = soup.find(string=re.compile(r"[\d,]+\s+Sales?",
                                             re.IGNORECASE))
    if sales_text:
        num_str = re.search(r"([\d,]+)", sales_text).group(1)
        return int(num_str.replace(",", ""))
    return None

def parse_shop_reviews(html):
    soup = BeautifulSoup(html, "html.parser")
    # 评价星级分布
    review_summary = soup.select_one(
        ".wt-align-items-center .wt-mb-xs-1"
    )
    stars = {}
    for row in soup.select("[data-review-stars]"):
        star_count = row.get("data-review-stars")
        count_text = row.get_text(strip=True)
        try:
            stars[star_count] = int(
                re.search(r"(\d+)", count_text).group(1)
            )
        except (AttributeError, ValueError):
            pass
    return stars

店铺研究的实用指标

  • 在售商品数 — 店铺活跃度指标
  • 总销量 / 在售商品数 — 平均每个商品的销量,反映产品效率
  • 店铺创建时间 — "On Etsy since YYYY",结合销量可算月均增速
  • 评价分布 — 5 星占比高说明产品满意度好,值得研究其设计方向

用 curl 快速验证代理连通性

在写完整脚本之前,先用 curl 测试代理是否能正常访问 Etsy:

# 测试 ProxyHat 住宅代理访问 Etsy
curl -x http://user-country-US:PASSWORD@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
  -H "Accept: text/html" \
  "https://www.etsy.com/search?q=custom+dog+mug" \
  -o etsy_test.html -w "%{http_code}\n"

如果返回 200,说明代理工作正常。如果返回 403 或 503,可能需要切换 IP 或增加请求间隔。

SOCKS5 代理:更高匿名度的选择

对于需要更高匿名度的场景(如频繁访问同一店铺页面),可以使用 ProxyHat 的 SOCKS5 代理:

import requests

SOCKS5_PROXY = "socks5://user-country-US:PASSWORD@gate.proxyhat.com:1080"
proxies = {"http": SOCKS5_PROXY, "https": SOCKS5_PROXY}

# 用 SOCKS5 抓取店铺页面
resp = requests.get(
    "https://www.etsy.com/shop/ShopName",
    headers=HEADERS,
    proxies=proxies,
    timeout=30
)
print(resp.status_code)

SOCKS5 的优势在于它工作在更底层,不暴露 HTTP 头中的代理特征。但速度可能略慢于 HTTP 代理。

细分市场研究工作流

把上面的技术组合起来,一个完整的 Etsy 细分市场研究流程如下:

  1. 关键词发散 — 用搜索建议 API 和 Google Trends 找出 50-100 个候选关键词
  2. 搜索量估算 — 抓取每个关键词的搜索结果数,作为需求代理指标
  3. 竞争分析 — 抓取前 3 页的 listing,提取价格分布、评价数、卖家信息
  4. 店铺深挖 — 对头部卖家做店铺分析,看他们的销量、商品数、创建时间
  5. 机会判断 — 结合需求量、竞争度、价格区间,判断是否值得进入

🔑 这个流程的核心是:用数据验证直觉,而不是靠猜。Etsy 上的数据是公开的,只要合规抓取,你就能比 90% 的卖家更了解市场。

伦理边界:研究而非抄袭

这一点非常重要,需要单独强调:

  • 不要复制设计 — Etsy 卖家大多是独立创作者,他们的设计受版权保护。抓取数据是为了了解市场趋势,不是照搬产品图或文案
  • 尊重 robots.txt — 检查 https://www.etsy.com/robots.txt 了解哪些路径不建议爬取
  • 控制请求频率 — 不要对 Etsy 服务器造成过大负担
  • 遵守 ToS — 仔细阅读 Etsy 的服务条款,了解自动化数据采集的规定
  • GDPR/CCPA — 如果抓取涉及用户个人信息(如评价者姓名),需遵守相关隐私法规

合理的使用场景包括:市场趋势分析、定价策略研究、关键词优化、竞争格局了解。不合理的使用包括:批量复制产品图片、盗用文案、自动化抢购。

常见问题与故障排除

Cloudflare 挑战(403/503)

如果频繁遇到 Cloudflare 挑战页:

  • 确认使用的是住宅代理,而非数据中心 IP
  • 增加请求间隔到 5-10 秒
  • 检查 User-Agent 和 Accept 头是否完整
  • 考虑使用 requests.Session() 保持 cookie 一致性

数据不完整

Etsy 的部分内容依赖 JavaScript 渲染。如果 JSON-LD 中缺少某些字段:

  • 对比浏览器中查看页面源代码(Ctrl+U)和渲染后 DOM 的差异
  • 对于必须 JS 渲染的数据,考虑使用 Playwright 或 Selenium
  • 优先使用 JSON-LD,它是 Etsy 向搜索引擎暴露的结构化数据,最稳定

IP 被封

即使使用住宅代理,如果请求模式太规律也可能被封:

  • 使用 ProxyHat 的每次请求换 IP 模式(不带 session 参数)
  • 随机化请求间隔和顺序
  • 限制并发数(建议不超过 3-5 个同时请求)

关键要点

Etsy 抓取要点回顾:

  • Etsy 搜索结果最多 250 页,每页约 48 个 listing
  • JSON-LD 是最稳定的数据提取来源,优先于 CSS 选择器
  • 住宅代理是抓取 Etsy 的必要条件,数据中心 IP 会被 Cloudflare 拦截
  • 搜索建议 API 是挖掘长尾关键词的利器
  • 店铺销量徽章提供市场规模的粗略估计
  • 始终遵守伦理边界:研究市场,不抄袭设计
  • 请求间隔 2-5 秒,并发不超过 5,是安全的基本参数

如果你想快速开始,可以在 ProxyHat 定价页 选择适合你规模的住宅代理套餐。更多抓取技巧,参考我们的 网页抓取最佳实践网页抓取用例 页面。

准备开始了吗?

通过AI过滤访问148多个国家的5000多万个住宅IP。

查看价格住宅代理
← 返回博客