Office Depot 是美国最大的办公用品零售商之一。该公司拥有逾 1,400 家门店,员工超过 38,000 人,旗下除主营的椅子、桌子、文具、打印机和学校用品目录外,还拥有 OfficeMax 和 Grand & Toy 品牌。其公开的在线商店列出了数千个 SKU 的价格、评分、评论数和库存状态,这使其成为竞品价格追踪、品类研究和库存监控的有用数据来源。

本指南介绍如何使用 Python 以可靠的方式抓取 Office Depot。你将构建一个小型可运行的爬虫,通过 Crawling API 获取渲染后的产品页面,用 BeautifulSoup 解析,并为每件商品提取整洁记录:产品名称、价格、SKU 或型号、评分、评论数、供货状态和产品 URL。整个教程仅限于公开的目录数据,靠近末尾的合法性说明不是套话,请在正式抓取大量数据之前认真阅读。

你将构建什么

一个 Python 脚本:接收 Office Depot 产品 URL,通过 Crawling API 获取渲染后的页面,并提取结构化记录。我们以 Epson Expression Home XP-4200 无线打印机为贯穿全文的示例,提取以下字段:

  • 产品名称 商品列表标题,例如 "Epson Expression Home XP-4200 Wireless Color Inkjet All-In-One Printer"。
  • 价格 页面上显示的标价。
  • SKU / 型号 在 Office Depot 目录中标识该产品的商品编号。
  • 评分 平均星级评分(如有)。
  • 评论数 该评分背后的客户评论数量。
  • 供货状态 商品显示为有货还是缺货。
  • 产品 URL 该产品详情页的规范链接。

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

如果你用普通 HTTP 客户端请求 Office Depot URL,通常会得到状态 200 的响应,但 body 中几乎没有产品数据。有两个因素对你不利。第一,该网站的许多产品和价格内容是在客户端构建的:价格区块和其他几个字段是在初始 HTML 加载后由 JavaScript 填充的,因此原始获取看到的是占位符而非实际值。第二,Office Depot 会标记自动化流量。来自数据中心的 IP 和不像真实浏览器的请求模式,在到达渲染完成的页面之前就会被 CAPTCHA 挑战、限速或封锁。

因此,一个可用的 Office Depot 爬虫需要在单次请求中同时具备两点:一个能真正渲染页面的浏览器,以及一个被平台识别为真实购物者的 IP。你可以自己搭建无头浏览器加轮换住宅代理池,但维护这套组合才是大部分工作量所在。Crawling API 将两者合并为一次调用:你使用 JavaScript 令牌发送 URL,它在可信住宅 IP 后渲染页面,并将完整 HTML 返回给你解析。关于为什么客户端渲染的页面会让普通获取失效,可参考如何抓取 JavaScript 网站的说明。

为什么需要 JS 令牌

Crawlbase 提供两种令牌类型。普通令牌获取静态 HTML;JavaScript(JS)令牌先在真实浏览器中渲染页面。Office Depot 在客户端填充价格和多个字段,因此在这里需要 JS 令牌。普通令牌返回预渲染的薄壳,BeautifulSoup 去查找时价格和库存字段将为空。

前提条件

在编写代码之前,你需要准备好以下几样东西。每一项都不费时。

基础 Python 知识。你应该能够编写和运行 Python 脚本,并使用 pip 安装包。如果你是该语言的新手,我们关于使用 Python 进行网络爬虫的指南涵盖了本教程所假设的基础知识。

Python 3.8 或更高版本。使用 python --version 确认你的版本。如果尚未安装,可从 python.org 下载,或通过 Anaconda 等发行版安装。

Crawlbase 账号和 JS 令牌。注册账号,打开控制台,从账号文档页面复制你的 JavaScript(JS)令牌。请像保管密码一样保管该令牌:它用于验证你的请求身份,不要将其提交到版本控制系统。

搭建项目

创建虚拟环境以隔离项目依赖,然后安装爬虫所需的库。

bash
python --version

python -m venv office_depot_env
source office_depot_env/bin/activate

pip install crawlbase beautifulsoup4 pandas

在 Windows 上,将激活命令中的 source 行替换为 office_depot_env\Scripts\activate。三个依赖各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML,让你可以通过 CSS 选择器提取每个字段,pandas 在最后将记录写入 CSV。

理解 Office Depot 产品页面结构

Office Depot 的产品页面展示了你关心的同一批字段:标题在一个标题元素中,价格在一个专用的价格元素中,商品编号兼作 SKU,星级评分带有评论数,以及告知你商品是否有货的配送信息。这些字段下方是描述和规格表。

编写选择器之前,在浏览器中打开一个产品页面,右键单击标题,选择"检查"。Office Depot 为大多数 class 名称加上了 od- 前缀,例如标题上的 od-heading 和价格上的 od-graphql-price-big-price。这些 class 名称就是你的目标。它们不时会变更,因此将以下选择器视为当前快照而非永久合同。

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

首先获取完整的页面。导入 CrawlingAPI 类,用你的 JS 令牌初始化,然后请求产品 URL。在解析之前检查状态码,可以让失败尽早暴露而非静默发生。

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_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__":
    url = "https://www.officedepot.com/a/products/8761287/Epson-Expression-Home-XP-4200-Wireless/"
    html = crawl(url)
    print(html[:500] if html else "No HTML returned")

两个等待选项对这类客户端渲染目标至关重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后固定等待指定毫秒数,使迟渲染的价格和库存字段在页面被捕获之前出现。五秒是合理的起始值;如果字段返回为空,可以增加。运行脚本,你应该能看到真实的产品标记,而不是普通获取返回的预渲染薄壳。这证明在你编写任何选择器之前,渲染已正常工作。

Crawlbase Crawling API

Office Depot 需要在单次调用中完成页面渲染和可信 IP 访问,以确保解析时价格和库存字段真正存在。Crawling API 接受 JS 令牌,在真实浏览器中运行页面,在服务端轮换住宅 IP,并将完整 HTML 交给你,让你跳过自行运行无头浏览器集群和代理池的麻烦。先在免费套餐中指向单个产品 URL 进行测试。

第二步:用 BeautifulSoup 解析字段

拿到渲染后的 HTML,将其加载到 BeautifulSoup 中,通过选择器提取每个字段。Office Depot 将标题暴露在 h1.od-heading.sku-heading,价格在 span.od-graphql-price-big-price,配送信息在 span.od-delivery-message-text。当某个元素在特定页面上缺失时,一个返回默认值的小辅助函数能防止提取崩溃。

python
from bs4 import BeautifulSoup

def text_of(soup, selector, default="N/A"):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else default

def parse_rating(soup):
    el = soup.select_one(".od-stars-inner")
    if not el or not el.get("style"):
        return None
    # style is like "width: 86%"; map percent of 5 stars
    percent = el["style"].split(":")[-1].strip().rstrip("%")
    try:
        return round(float(percent) / 20, 1)
    except ValueError:
        return None

def extract_product(html, url):
    soup = BeautifulSoup(html, "html.parser")
    reviews = text_of(soup, ".od-reviews-count-number").strip("()")
    sku = text_of(soup, ".od-product-card-region-product-number").split("#")[-1]
    delivery = text_of(soup, "span.od-delivery-message-text", "").lower()
    availability = "In Stock" if "in stock" in delivery else "Out of Stock"
    return {
        "name": text_of(soup, "h1.od-heading.sku-heading"),
        "price": text_of(soup, "span.od-graphql-price-big-price"),
        "sku": sku,
        "rating": parse_rating(soup),
        "review_count": reviews,
        "availability": availability,
        "product_url": url,
    }

text_of 辅助函数在元素缺失时返回默认值,而不是对空元素调用 .get_text() 引发异常。SKU 来自 .od-product-card-region-product-number,按 # 分割后只保留商品编号。Office Depot 将星级评分编码为 .od-stars-inner 上的 CSS 宽度百分比,因此 parse_rating 读取该百分比并除以 20 以还原满分五分的值。评论数位于 .od-reviews-count-number,包裹在括号中,strip("()") 会将其去除;供货状态从配送信息文本中推导得出。关于选择元素的入门知识,可参考如何在 Python 中使用 BeautifulSoup

有一点需要预期:Office Depot 的 od- class 名称(如价格元素 od-graphql-price-big-price、评分条 od-stars-inner)会无通知变更,页面布局也偶尔会调整。将上述选择器视为起始模板,而非固定合同。当某个字段在你知道有值的页面上返回 N/ANone 时,在浏览器开发者工具中重新检查线上页面并更新选择器即可。定期维护选择器是任何生产级爬虫的正常工作,不代表出了什么问题。

第三步:整合

现在将获取、解析和 CSV 写入串联成一个可运行的脚本。获取渲染后的页面,传给提取器,打印记录,并用 pandas 保存。

python
import json
import pandas as pd
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_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

def text_of(soup, selector, default="N/A"):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else default

def parse_rating(soup):
    el = soup.select_one(".od-stars-inner")
    if not el or not el.get("style"):
        return None
    percent = el["style"].split(":")[-1].strip().rstrip("%")
    try:
        return round(float(percent) / 20, 1)
    except ValueError:
        return None

def extract_product(html, url):
    soup = BeautifulSoup(html, "html.parser")
    reviews = text_of(soup, ".od-reviews-count-number").strip("()")
    sku = text_of(soup, ".od-product-card-region-product-number").split("#")[-1]
    delivery = text_of(soup, "span.od-delivery-message-text", "").lower()
    availability = "In Stock" if "in stock" in delivery else "Out of Stock"
    return {
        "name": text_of(soup, "h1.od-heading.sku-heading"),
        "price": text_of(soup, "span.od-graphql-price-big-price"),
        "sku": sku,
        "rating": parse_rating(soup),
        "review_count": reviews,
        "availability": availability,
        "product_url": url,
    }

def main():
    url = "https://www.officedepot.com/a/products/8761287/Epson-Expression-Home-XP-4200-Wireless/"
    html = crawl(url)
    if not html:
        return
    product = extract_product(html, url)
    print(json.dumps(product, indent=2))
    pd.DataFrame([product]).to_csv("office_depot_products.csv", index=False)
    print("Saved to office_depot_products.csv")

if __name__ == "__main__":
    main()

输出示例

使用 python scraper.py 运行完整脚本,你将得到一条整洁的记录,可写入 JSON、CSV 或数据库。JSON 打印输出如下所示:

json
{
  "name": "Epson Expression Home XP-4200 Wireless Color Inkjet All-In-One Printer",
  "price": "$99.99",
  "sku": "8761287",
  "rating": 4.3,
  "review_count": "512",
  "availability": "In Stock",
  "product_url": "https://www.officedepot.com/a/products/8761287/Epson-Expression-Home-XP-4200-Wireless/"
}

扩展至搜索页面和分页

单个产品只是演示;真实任务是收集多个产品。Office Depot 的搜索结果以产品卡片网格形式展示,每张卡片包含标题、价格、评分、评论数、商品编号和产品页面链接。你用同样的方式抓取该网格:选择每张卡片,按卡片提取相同字段,当你需要更深层详情时再跟进产品页面链接。搜索结果通过 &page= 参数分页,因此你通过递增该参数遍历页面,当某页不再返回卡片时停止。

python
import time

def extract_cards(html):
    soup = BeautifulSoup(html, "html.parser")
    grid = ".od-search-browse-products-vertical-grid-product"
    products = []
    for card in soup.select(grid):
        link_el = card.select_one(".od-product-card-region-description a")
        href = link_el["href"] if link_el else ""
        products.append({
            "name": text_of(card, ".od-product-card-region-description a"),
            "price": text_of(card, ".od-graphql-price-big-price"),
            "review_count": text_of(card, ".od-reviews-count-number").strip("()"),
            "product_url": "https://www.officedepot.com" + href if href else "N/A",
        })
    return products

def scrape_all_pages(base_url, max_pages=5):
    all_products = []
    for page in range(1, max_pages + 1):
        html = crawl(f"{base_url}&page={page}")
        if not html:
            break
        products = extract_cards(html)
        if not products:
            break
        all_products.extend(products)
        print(f"Page {page}: {len(products)} products")
        time.sleep(2)
    return all_products

max_pages 上限使一次运行有边界,避免宽泛查询无限运行;空结果时的 break 在搜索用完页面时提前结束。页面之间的 time.sleep(2) 控制请求节奏,避免频繁请求导致被限速。关于构建多页零售抓取的更多内容,我们关于电商网络爬虫的指南深入介绍了相关模式。

偏好结构化输出?

如果你不想维护 CSS 选择器,带自动解析的 Crawling API 可为常见电商页面返回现成的结构化 JSON,你无需编写提取代码即可获得名称、价格和评分等字段。当你希望数据格式由上游处理、选择器漂移问题由上游处理时,这是一个很好的选择。

保持不被封锁

即便渲染问题已解决,Office Depot 仍会监控形似爬虫的流量。以下几个习惯有助于保持任务顺畅运行,适用于任何难度较高的商业目标。

  • 控制请求频率。在页面之间加入延迟,对查询进行变化,而不是以全速抓取单一关键词。分页循环中的 time.sleep 是下限,不是上限。
  • 善用轮换。住宅 IP 池将请求分散到众多真实用户地址,使任何单一地址都不会触发速率限制。Crawling API 为你处理这一切;如果你自行搭建,这一环节最为关键。
  • 关注状态码。当运行过程中开始返回挑战或错误时,说明当前速率或 IP 层级已不够用。将其视为需要退让的信号,而非可以忽略的噪音。

关于更完整的应对策略,请参阅如何在不被封锁的情况下抓取网站。如果你更倾向于通过轮换池路由自己的流量而非使用托管 API,Smart AI Proxy(也称 AI Proxy)以直接代理接口的形式提供相同的住宅 IP 轮换。

抓取 Office Depot 是否合法?

抓取 Office Depot 是否被允许,取决于 Office Depot 的服务条款、你所在的司法管辖区以及你对数据的用途。Office Depot 的条款限制自动化访问,因此无论你的技术手段多么谨慎,抓取行为都可能与这些条款相抵触。本文中的代码无法改变这一点,它只是让技术层面的工作得以实现。请阅读 Office Depot 的使用条款和 robots.txt,并将两者视为你能收集内容的边界。

以下几条底线值得坚守。仅收集公开目录数据:任何人无需账号即可看到的产品名称、价格、SKU、评分、评论数、供货状态和商品链接。将你的请求量控制在不给 Office Depot 服务器造成压力的水平。避免个人数据,包括任何与可识别购物者、评论者或门店员工相关的内容,不要将受版权保护的产品图片或描述当作你自己的内容转载。内部研究或价格比较用途比大规模抓取以转载或转售目录容易辩护得多。

本指南有意将范围限定为公开产品和搜索页面,因为这条线使工作保持在可辩护的范围内。它不涵盖登录后的内容、账号或订单数据、支付或结账流程,或任何绕过身份验证的尝试。对于授权或大批量访问,正确路径是官方数据源、API 或与 Office Depot 签订的数据协议,而非更聪明的爬虫。如果你的项目需要有保证的结构、大批量数据或商业权限,请先申请官方授权;托管抓取工具只负责获取,不提供使用数据的许可。

回顾

核心要点

  • Office Depot 在客户端填充关键字段。普通获取返回价格和库存为空的预渲染薄壳,因此在解析之前必须先渲染页面。
  • 需要同时具备渲染和可信 IP。带 JS 令牌的 Crawling API 在单次调用中完成两件事;ajax_waitpage_wait 控制等待迟渲染字段出现的时长。
  • BeautifulSoup 负责提取。将名称、价格、SKU、评分、评论数、供货状态和产品 URL 映射到当前的 od- 选择器,并预期这些选择器会漂移。
  • 通过搜索和分页扩展。遍历结果卡片网格,逐页递增 &page= 直到某页不再返回卡片,通过延迟控制请求节奏,并设置最大页面数上限。
  • 坚守公开数据。遵守 Office Depot 的服务条款和 robots.txt,授权或批量数据请优先使用官方数据源或 API,且绝不触碰账号、订单或个人信息。

常见问题

为什么普通请求从 Office Depot 获取不到价格?

因为 Office Depot 使用 JavaScript 在客户端填充价格和其他几个字段。初始 HTML 是预渲染的薄壳,因此原始 HTTP 请求返回状态 200,但价格和库存字段为空。要获取真实值,必须先渲染页面,这正是 Crawling API 的 JS 令牌为你处理的事情。

抓取 Office Depot 需要普通令牌还是 JS 令牌?

需要 JS 令牌。普通令牌获取静态 HTML,在 Office Depot 上价格和供货状态字段将不被填充。JS 令牌在返回 HTML 之前在真实浏览器中渲染页面,使这些字段在 BeautifulSoup 解析时已存在。

可以从 Office Depot 产品页面提取哪些字段?

页面上显示的公开字段:产品名称、价格、兼作 SKU 或型号的商品编号、星级评分、评论数、供货状态以及规范的产品 URL。搜索结果卡片每条列表都暴露了大部分相同字段,这就是你在深入查看各产品页面之前收集众多产品的方式。

如何抓取 Office Depot 搜索的所有页面?

搜索结果通过 URL 上的 &page= 参数分页。在循环中递增它,用同一卡片解析器抓取每一页,当某页不再返回产品时停止。设置页面数上限,并在请求之间添加短暂延迟,以控制节奏并避免被限速。

我的选择器返回 N/A,发生了什么变化?

几乎可以肯定是 Office Depot 的标记发生了变化。其 od- class 名称(如价格的 od-graphql-price-big-price 和评分条的 od-stars-inner)会无通知变更,页面布局也偶尔会调整。在浏览器开发者工具中重新检查线上页面并更新选择器。定期维护是任何生产级爬虫的正常工作。

可以从 Office Depot 抓取账号或订单数据吗?

不可以,本指南也不涉及这一内容。账号详情、订单历史和结账流程位于登录后,因此不属于公开数据。抓取登录后的内容,或绕过身份验证来访问它,超出了本文的范围,且与 Office Depot 的条款相抵触。如需访问更丰富的数据,正确途径是官方数据源、API 或数据协议。

开始构建

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

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

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