Costco 在全球运营超过 800 家仓储超市,同样的商品目录也在 costco.com 上以公开商品页面和搜索结果的形式呈现。价格、商品编号、评分和库存状态全部显示在页面上,任何人无需账号即可查看。这些公开数据是价格追踪、竞争研究和库存监控的清晰信号,这也是为什么零售商、分析师和开发者会定期爬取它。

本指南介绍如何用 Python 抓取 Costco 商品数据。你将构建一个小型可运行爬虫,通过 Crawling API 获取 Costco 搜索页和商品页,为每件商品解析出干净的记录,处理搜索结果翻页,并将数据导出为 JSON 和 CSV。整个教程仅涉及公开商品数据:任何人无需登录即可在列表页面上看到的标题、价格、商品编号、评分和库存情况。

你将构建的内容

一个 Python 脚本,接收 Costco 搜索 URL 或商品 URL,通过 Crawling API 获取渲染后的页面,并为每件商品提取一条结构化记录。我们以沙发搜索作为贯穿全文的示例,与旧版教程使用相同的品类,并从每件商品中抽取以下字段:

  • Title(标题)列表或商品页面上显示的商品名称。
  • Price(价格)当商品有标价时显示的价格。
  • Item number(商品编号)Costco 的唯一商品标识符,可用于跨次运行追踪库存。
  • Rating(评分)从商品卡片或页面读取的平均星级评分。
  • Availability(库存情况)商品页面上的有货或缺货状态信号。
  • Product URL(商品链接)指向该商品详情页的链接。
  • Image URL(图片链接)商品图片来源地址。

为什么普通请求在 Costco 上会失败

如果你用裸 HTTP 客户端访问 Costco 搜索或商品 URL,很少能得到你想要的数据。两个因素对你不利。首先,Costco 的页面有大量内容是客户端渲染的:网站先发送一个轻量外壳,随后由页面的 JavaScript 填充商品网格和价格区块,因此初始 HTML 往往缺少你需要的字段。其次,Costco 会迅速识别自动化流量。数据中心 IP 段和不像真实浏览器的请求模式,在你访问到商品列表之前就会遭遇验证挑战、拦截页面或彻底封锁。

因此,一个能正常工作的 Costco 爬虫需要在单次请求中同时具备两点:能够渲染页面的浏览器,以及网站认为是真实购物者的 IP。你可以自己用无头浏览器加上轮换住宅代理池来实现,但维护这套架构才是大部分工作所在。Crawling API 将这两者合并为一次调用:你发送 URL,它在可信的住宅 IP 后面渲染页面,处理轮换和 CAPTCHA 验证,然后返回可供你解析的完整 HTML。

前置条件

在编写任何代码之前,你需要准备好以下几件事。每件都不会花太长时间。

基础 Python 知识。你应该熟悉编写和运行 Python 脚本,以及使用 pip 安装包。如果你是语言新手,官方 Python 文档或任何入门课程都能覆盖本教程所假设的水平。

Python 3.8 或更高版本。使用 python --version(或 python3 --version)确认你的版本。如果没有,请从 python.org 安装,并确保 Python 已加入系统 PATH。

Crawlbase 账号和 token。注册免费账号,打开控制台,复制你的 token。Costco 的页面大量依赖 JavaScript,因此这些请求需要使用 JavaScript token。免费套餐最多包含 20,000 次请求,无需绑定信用卡,足以构建和测试这个爬虫。请像对待密码一样保管好 token,不要将其提交到版本控制系统。

项目设置

创建虚拟环境以隔离项目依赖,然后安装爬虫所需的两个库。crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 用于解析返回的 HTML,让你能通过 CSS 选择器从页面中提取各个字段。

bash
python --version

python -m venv costco_env
source costco_env/bin/activate

pip install crawlbase beautifulsoup4

在 Windows 上,用 costco_env\Scripts\activate 替换 source 那行来激活环境。安装好两个库后,创建本指南其余部分将逐步构建的脚本文件:

bash
touch costco_scraper.py

了解 Costco 页面结构

有两类页面值得抓取,它们的标记结构各不相同。搜索结果页面(如 https://www.costco.com/s?dept=All&keyword=sofas)以网格形式排列商品卡片。商品详情页(如 https://www.costco.com/coddle-aria-fabric-sleeper-sofa.product.4000223041.html)则显示某件商品的完整详情视图,包括描述、规格和库存情况。

在编写选择器之前,在浏览器中打开每种页面,右键单击一件商品并选择"检查"。在搜索页面上,Costco 将商品列表包裹在 div[id="productList"] 中,每件商品位于 div[data-testid="Grid"] 下。每张卡片通过 data-testidText_ProductTile_ 开头的 div 显示标题,以 Text_Price_ 开头的显示价格,以 Rating_ProductTile_ 开头的显示评分,链接在 a[data-testid="Link"] 锚元素中,图片在 img 标签中。在商品详情页上,标题是 h1[automation-id="productName"],价格是 span[automation-id="productPriceOutput"],评分是 div[itemprop="ratingValue"]。这些就是你要定位的元素。

第一步:获取渲染后的 Costco 页面

从获取完整页面开始。导入 CrawlingAPI 类,用你的 token 初始化它,设置搜索 URL,并发起请求。Costco 异步加载商品网格,因此需传入 ajax_waitpage_wait 选项,让页面在被捕获前有时间完成渲染。在解析之前检查状态码,可以让失败情况明显暴露而不是悄然无声。

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    search_url = "https://www.costco.com/s?dept=All&keyword=sofas"
    html = crawl(search_url)
    print(html[:500] if html else "No HTML returned")

这两个等待选项对于加载后才填充的网格至关重要。ajax_wait 告知 API 等待异步内容加载完成,page_wait 在加载后再固定等待若干毫秒(这里是 5,000),确保延迟渲染的商品卡片在页面被捕获前全部出现。运行脚本后,你应该能看到真实的商品标记,而不是加载外壳。这确认了渲染可以正常工作,你才能开始编写选择器。

Crawlbase Crawling API

沙发网格只有在 Costco 的 JavaScript 运行后才会出现,而光有渲染还不够,请求还必须来自网站信任的 IP。Crawling API 接收你的 token,在真实浏览器中运行页面,在服务端轮换住宅 IP,并处理 CAPTCHA 验证,然后将完整的 HTML 交给你。你无需自己运行无头浏览器集群和代理池,这也是上面使用 ajax_waitpage_wait 选项就足够的原因。先在免费的最多 20,000 次请求套餐上试试。

第二步:用 BeautifulSoup 解析搜索列表

拿到渲染后的 HTML,将其加载到 BeautifulSoup 中,找到每张商品卡片,通过选择器提取各字段。Costco 将商品归于 div[id="productList"] > div[data-testid="Grid"] 下,每张卡片通过相应元素显示标题、价格、评分、链接和图片。对循环做存在性检查,让某字段缺失时返回干净的默认值,而不是崩溃,因为并非每张卡片都显示相同的数据。

python
from bs4 import BeautifulSoup
import re

def item_number_from_url(url):
    match = re.search(r"\.product\.(\d+)\.html", url or "")
    return match.group(1) if match else "N/A"

def scrape_search_listings(html):
    soup = BeautifulSoup(html, "html.parser")
    products = []
    items = soup.select('div[id="productList"] > div[data-testid="Grid"]')
    for item in items:
        title_el = item.select_one('div[data-testid^="Text_ProductTile_"]')
        price_el = item.select_one('div[data-testid^="Text_Price_"]')
        rating_el = item.select_one('div[data-testid^="Rating_ProductTile_"] > div')
        link_el = item.select_one('a[data-testid="Link"]')
        image_el = item.find("img")

        product_url = link_el["href"] if link_el else "N/A"
        products.append({
            "title": title_el.get_text(strip=True) if title_el else "N/A",
            "price": price_el.get_text(strip=True) if price_el else "N/A",
            "item_number": item_number_from_url(product_url),
            "rating": rating_el["aria-label"] if rating_el and rating_el.has_attr("aria-label") else "N/A",
            "product_url": product_url,
            "image_url": image_el["src"] if image_el and image_el.has_attr("src") else "N/A",
        })
    return products

每张卡片映射为一条干净的字典记录。标题来自 div[data-testid^="Text_ProductTile_"],价格来自 div[data-testid^="Text_Price_"],两者均使用 ^= 前缀匹配,因为 Costco 在这些测试 ID 上会附加每件商品的后缀。评分从嵌套评分 divaria-label 属性中读取,该属性包含"Average rating is 4.65 out of 5 stars"这样的可读文本。商品编号从商品 URL 中解析,因为 Costco 将其编码在 .product.NNNN.html 片段中,因此无需单独的选择器即可获取稳定的标识符。每个字段在缺失时回退到 "N/A",保持提取的健壮性,因为不是每张卡片都有价格或评分。

选择器会漂移

Costco 每件商品的 data-testid 后缀和类名会随网站更新而变化,而 div[id="productList"]Text_ProductTile_ 前缀等结构性标记则更为持久。将上面的选择器视为起始模板,而非约定。当某个字段对每张卡片都返回 "N/A" 时,在浏览器开发者工具中重新检查实际页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常的事。

第三步:处理搜索结果翻页

Costco 搜索结果跨越多页,网站通过在 URL 上附加 &currentPage= 参数来加载每一页。要收集某个品类的完整数据,需追加该参数并依次翻页,同时控制请求节奏,避免在紧密循环中高频访问网站。

python
import time

def scrape_all_pages(base_url, total_pages):
    all_products = []
    for page_num in range(1, total_pages + 1):
        paginated_url = f"{base_url}&currentPage={page_num}"
        print(f"Scraping page {page_num}")
        html = crawl(paginated_url)
        if not html:
            break
        found = scrape_search_listings(html)
        if not found:
            break
        all_products.extend(found)
        time.sleep(2)
    return all_products

当品类页面耗尽时,空结果中断会提前停止循环;time.sleep(2) 控制请求节奏,避免因连续快速发送请求而被标记。将 total_pages 设为你的关键词搜索所跨越的实际页数。

第四步:抓取单个商品详情页

搜索卡片提供基本字段,但商品详情页包含更多内容:完整描述、结构化规格,以及库存状态信号。该页面与搜索网格采用相同的渲染方式,因此可以复用 crawl 辅助函数,解析详情页选择器即可。

python
def scrape_product_page(html, url):
    soup = BeautifulSoup(html, "html.parser")

    title_el = soup.select_one('h1[automation-id="productName"]')
    price_el = soup.select_one('span[automation-id="productPriceOutput"]')
    rating_el = soup.select_one('div[itemprop="ratingValue"]')
    desc_el = soup.select_one('div[id="product-tab1-espotdetails"]')
    image_el = soup.find("img", class_="thumbnail-image")
    stock_el = soup.select_one('div[automation-id="productInventoryStatus"]')

    specifications = {}
    for row in soup.select("div.product-info-description .row"):
        name = row.select_one(".spec-name")
        value = row.select_one("div:not(.spec-name)")
        if name and value:
            specifications[name.get_text(strip=True)] = value.get_text(strip=True)

    return {
        "title": title_el.get_text(strip=True) if title_el else "N/A",
        "price": price_el.get_text(strip=True) if price_el else "N/A",
        "item_number": item_number_from_url(url),
        "rating": rating_el.get_text(strip=True) if rating_el else "N/A",
        "availability": stock_el.get_text(strip=True) if stock_el else "N/A",
        "description": desc_el.get_text(strip=True) if desc_el else "N/A",
        "image_url": image_el["src"] if image_el and image_el.has_attr("src") else "N/A",
        "specifications": specifications,
    }

商品详情页选择器直接来自 Costco 的详情页标记。标题是 h1[automation-id="productName"],价格是 span[automation-id="productPriceOutput"],评分是 div[itemprop="ratingValue"],描述块位于 div[id="product-tab1-espotdetails"]。规格循环遍历描述表格中的每个 .row,从 .spec-name 读取标签,并与相邻的值单元格配对,最终得到一份包含框架材质、尺寸等属性的干净字典。库存状态从库存状态块读取;复用同一个 item_number_from_url 辅助函数,确保标识符与搜索记录保持一致。

第五步:组装脚本并导出 JSON 和 CSV

现在将获取、翻页和解析整合成一个可运行的完整脚本,然后将记录写入 JSON 和 CSV,方便加载到 notebook 或电子表格中。共享字段列表确保 CSV 的列顺序与字典键保持一致。

python
import csv
import json
import re
import time
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})
FIELDS = ["title", "price", "item_number", "rating", "product_url", "image_url"]

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

def item_number_from_url(url):
    match = re.search(r"\.product\.(\d+)\.html", url or "")
    return match.group(1) if match else "N/A"

def scrape_search_listings(html):
    soup = BeautifulSoup(html, "html.parser")
    products = []
    items = soup.select('div[id="productList"] > div[data-testid="Grid"]')
    for item in items:
        title_el = item.select_one('div[data-testid^="Text_ProductTile_"]')
        price_el = item.select_one('div[data-testid^="Text_Price_"]')
        rating_el = item.select_one('div[data-testid^="Rating_ProductTile_"] > div')
        link_el = item.select_one('a[data-testid="Link"]')
        image_el = item.find("img")
        product_url = link_el["href"] if link_el else "N/A"
        products.append({
            "title": title_el.get_text(strip=True) if title_el else "N/A",
            "price": price_el.get_text(strip=True) if price_el else "N/A",
            "item_number": item_number_from_url(product_url),
            "rating": rating_el["aria-label"] if rating_el and rating_el.has_attr("aria-label") else "N/A",
            "product_url": product_url,
            "image_url": image_el["src"] if image_el and image_el.has_attr("src") else "N/A",
        })
    return products

def scrape_all_pages(base_url, total_pages):
    all_products = []
    for page_num in range(1, total_pages + 1):
        paginated_url = f"{base_url}&currentPage={page_num}"
        print(f"Scraping page {page_num}")
        html = crawl(paginated_url)
        if not html:
            break
        found = scrape_search_listings(html)
        if not found:
            break
        all_products.extend(found)
        time.sleep(2)
    return all_products

def export(rows, name="costco_products"):
    with open(f"{name}.json", "w", encoding="utf-8") as f:
        json.dump(rows, f, indent=2, ensure_ascii=False)
    with open(f"{name}.csv", "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows(rows)
    print(f"Saved {len(rows)} products to {name}.json and {name}.csv")

def main():
    base_url = "https://www.costco.com/s?dept=All&keyword=sofas"
    products = scrape_all_pages(base_url, total_pages=5)
    export(products)

if __name__ == "__main__":
    main()

使用 python costco_scraper.py 运行完整脚本。它遍历搜索页面,每件商品解析一行数据,并写入 costco_products.jsoncostco_products.csv。共享的 FIELDS 列表确保 CSV 列顺序与字典键同步,使两个导出文件永远不会出现偏差。若要用描述、规格和库存情况丰富数据,将每个 product_url 传给第四步中的 crawlscrape_product_page 即可。

输出结果示例

你将得到一份干净的商品记录列表,可直接写入 JSON、CSV 或数据库。

json
[
  {
    "title": "Coddle Aria Fabric Sleeper Sofa with Reversible Chaise Gray",
    "price": "$1,299.99",
    "item_number": "4000223041",
    "rating": "Average rating is 4.65 out of 5 stars. Based on 1668 reviews.",
    "product_url": "https://www.costco.com/coddle-aria-fabric-sleeper-sofa-with-reversible-chaise-gray.product.4000223041.html",
    "image_url": "https://cdn.bfldr.com/U447IH35/at/nx2pbmjk76t8c5k4h3qpsg6/4000223041-847_gray_1.jpg"
  },
  {
    "title": "Larissa Fabric Chaise Sofa",
    "price": "$1,899.99",
    "item_number": "4000052035",
    "rating": "Average rating is 4.03 out of 5 stars. Based on 87 reviews.",
    "product_url": "https://www.costco.com/larissa-fabric-chaise-sofa.product.4000052035.html",
    "image_url": "https://cdn.bfldr.com/U447IH35/as/ck2h3n29gz2j6m7c9f7x4rhm/4000052035-847_gray_1"
  }
]

商品详情页的运行会返回更丰富的记录,包含描述、库存状态信号,以及从详情表格中直接提取的规格字典,涵盖靠背款式、框架材质和整体尺寸等属性。

保持不被封锁

即使渲染问题已经解决,Costco 仍会监测爬虫特征的流量。以下几个习惯能让运行保持健康,适用于任何难度较高的商业目标。

  • 控制请求节奏。在页面间加入延迟分散请求,而不是全速爬取所有内容,并将较重的任务安排在非高峰时段,以减轻 Costco 服务器的负担。
  • 依赖 IP 轮换。住宅 IP 池将请求分散到众多真实用户地址,确保没有单一地址触发速率限制。Crawling API 为你处理这一切;如果你自己搭建方案,这是最需要做好的部分。
  • 只保留所需数据。存储你的项目实际使用的商品字段,丢弃其余内容,并定期检查选择器,让爬虫跟上标记变化。

关于避免封锁的更宏观操作手册,请参阅如何抓取网站而不被封锁,以及关于渲染为何重要的如何抓取 JavaScript 网站。你收集的数据可直接用于价格情报工作,更广泛的模式也适用于本电商网络抓取指南中涵盖的其他商店。

抓取 Costco 是否合法?

抓取 Costco 是否被允许,取决于 Costco 的使用条款、你所在的司法管辖区,以及你对数据的使用方式。Costco 的条款约束了自动化访问,因此抓取行为可能与这些条款相抵触,无论你的工具多么谨慎。这里的任何代码都不会改变这一点,它只是让技术层面的事情能够运作。请阅读 Costco 的使用条款及其 robots.txt,并将两者视为你采集内容的边界。对于商业或竞争性用途,法律层面会更为复杂,针对你的具体情况咨询法律专家是明智之举。

有几条底线值得坚守。只收集公开数据:任何人无需账号即可在 Costco 页面上看到的标题、价格、商品编号、评分、库存情况和列表链接。将请求量控制在不会给 Costco 服务器造成压力的范围内,避免涉及个人数据,包括任何与可识别会员、评价者或卖家相关的信息(超出公开列出的范围)。如果你计划将数据用于商业目的,应获得许可或官方协议,而不是假设沉默即为同意。

本指南刻意将范围限定在公开商品页和搜索页,因为这是保持工作可辩护性的界限。它不涉及任何登录后的内容、会员或订单数据、仅对已登录会员显示的价格,也不涉及任何绕过身份验证或你无权通过的 CAPTCHA 的尝试。如果你的项目需要超出公开商品数据范围的内容,官方数据协议或合作伙伴计划才是正确路径,而不是更聪明的爬虫。

回顾

核心要点

  • Costco 数据是清晰的零售信号。公开的标题、价格、商品编号、评分和库存情况可为价格追踪、市场研究和库存监控提供数据支撑。
  • 你需要同时具备渲染能力和可信 IP。Costco 在客户端加载商品网格并封锁爬虫流量,因此 Crawling API 通过一次调用在住宅 IP 后面渲染页面,并配合 ajax_waitpage_wait 使用。
  • BeautifulSoup 负责提取。循环遍历搜索页上的 div[id="productList"] > div[data-testid="Grid"] 卡片以及商品详情页上的 automation-id 选择器,并预期选择器会发生漂移。
  • 用 currentPage 翻页并导出两种格式。遍历 &currentPage= 参数覆盖某个完整品类,然后用共享字段列表将 JSON 和 CSV 同步写出。
  • 坚守公开数据。遵守 Costco 的使用条款和 robots.txt,控制请求节奏,不要触碰会员账号、订单或个人信息。

常见问题

为什么普通请求从 Costco 返回不了商品信息?

Costco 的商品网格和价格区块在客户端随页面加载渲染,因此原始请求拿到的往往是缺少你所需字段的外壳。此外,Costco 还会挑战或封锁不像真实浏览器的流量。通过 Crawling API 在可信 IP 后面渲染页面,并设置 ajax_waitpage_wait 选项,可以同时解决这两个问题,这也是爬虫通过它发起请求的原因。

用这个爬虫可以从 Costco 提取哪些数据?

从搜索列表可以获取每张卡片的标题、价格、商品编号、评分、商品链接和图片链接。从单个商品详情页还可以获取描述、完整规格表和库存状态信号。所有数据都是公开商品信息,可存储为 JSON 或 CSV 用于分析。

如何抓取 Costco 搜索结果的多个页面?

Costco 通过 URL 上的 &currentPage= 参数加载每一页搜索结果。追加该参数并循环遍历页码,依次解析每页,当某页不返回商品时提前中断。在请求间加入短暂延迟,控制运行节奏,而不是高频访问网站。

如何获取 Costco 商品编号?

Costco 将商品编号编码在商品 URL 的 .product.NNNN.html 片段中。爬虫通过一个小型正则辅助函数解析它,因此无论从搜索卡片还是商品详情页都能获得稳定的标识符,无需依赖可能变化的独立页面选择器。

抓取 Costco 需要 JavaScript token 吗?

是的。Costco 的页面依赖 JavaScript 渲染商品网格、价格和库存情况,因此调用 Crawling API 时需要 JavaScript token 并设置等待选项。免费套餐最多包含 20,000 次请求可供构建和测试,当前费率可在定价页面查看。

抓取 Costco 时如何避免被封锁?

降低单个 IP 的请求速率,在页面间加入延迟,并通过轮换住宅 IP 发起请求,确保没有单一地址触发速率限制。Crawling API 为你管理轮换、可信 IP 池和 CAPTCHA 处理;如果你自己搭建方案,这是最值得投入的部分。监控状态码,当开始出现验证挑战时及时回退。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量