Best Buy 的搜索页面和商品页面是北美最大消费电子目录的公开窗口。每条搜索结果和每个商品页面都包含同样几个任何人都能看到的字段:商品标题、价格、型号和 SKU 编号、星级评分,以及是否有货。这些字段正是价格分析师、市场研究人员和科技买家在监测电子产品价格走势或比较某品类库存情况时所追踪的信号。

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

你将构建的内容

一个 Python 脚本,接收 Best Buy 搜索 URL,通过 Crawling API 获取渲染后的页面,并为每个商品提取一条结构化记录。我们以搜索 "i phone" 作为贯穿全文的示例,与旧版教程使用同一查询,从每个商品卡片中抽取以下字段:

  • Title(标题)商品卡片上显示的商品名称。
  • Price(价格)当商品显示价格时,该商品的当前顾客价格。
  • Model / SKU(型号 / SKU)制造商型号和 Best Buy 自有 SKU 标识符。
  • Rating(评分)平均星级评分,附带评论数量。
  • Availability(库存状态)商品是否有货、售罄或可供发货。
  • Product URL(商品链接)指向该商品详情页的绝对链接。

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

如果你用裸 HTTP 客户端访问 Best Buy 的搜索 URL,几乎不会得到你想要的商品列表。Best Buy 的搜索结果是在客户端渲染的:服务器先发送一个轻量外壳,页面上的 JavaScript 随后填充商品卡片。因此,简单的 requests.get() 返回的原始 HTML 完全缺少商品列表,解析器会得到一个空列表。

第二个问题是机器人检测。Best Buy 能快速识别自动化流量。数据中心 IP 段和不像真实浏览器的请求模式,在你访问到商品之前就会遭遇速率限制、验证页面或彻底封锁。因此,一个能正常工作的 Best Buy 爬虫需要在单次请求中同时具备两点:能渲染页面的浏览器,以及 Best Buy 视为真实购物者的 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。免费套餐最多包含 20,000 次请求,无需绑定信用卡,足以构建和测试这个爬虫。Best Buy 是 JavaScript 渲染的网站,因此这里使用 JavaScript 请求 token。请像对待密码一样保管好 token,不要将其提交到版本控制系统。

项目设置

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

bash
python --version

python -m venv bestbuy_env
source bestbuy_env/bin/activate

pip install crawlbase beautifulsoup4

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

bash
touch bestbuy_scraper.py

了解 Best Buy 搜索页面

Best Buy 的搜索页面位于 searchpage.jsp 端点下的稳定 URL,你的查询词放在 st 参数中。搜索 "i phone" 的 URL 是 https://www.bestbuy.com/site/searchpage.jsp?st=i+phone。页面按顺序排列商品卡片,每个商品对应一张,每张卡片包含相同的字段:标题、价格、型号和 SKU、带评论数量的星级评分,以及库存状态。

在编写选择器之前,在浏览器中打开搜索页面,右键单击商品卡片并选择"检查"。Best Buy 将整个结果集包裹在 ol.sku-item-list 中,每个商品放在 li.sku-item 容器内,再将每张卡片分为 column-middle(标题、型号、SKU、评分)和 column-right(价格、库存状态)两部分。这些就是你要定位的元素。Best Buy 的类名会随时间变化,所以请将下面的选择器视为起始模板,而非永久约定,需定期与实际页面核对。

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

从获取完整页面开始。导入 CrawlingAPI 类,用你的 token 初始化它,构建搜索 URL,然后发起请求。在解析之前检查 Crawlbase 的 cb_status(legacy pc_status),可以让失败情况明显暴露而不是悄然无声。

python
from crawlbase import CrawlingAPI
from urllib.parse import quote_plus

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["headers"]["cb_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed. Crawlbase status: {response['headers']['cb_status']}")
    return None

if __name__ == "__main__":
    search_term = "i phone"
    search_url = f"https://www.bestbuy.com/site/searchpage.jsp?st={quote_plus(search_term)}"
    html = crawl(search_url)
    print(html[:500] if html else "No HTML returned")

这两个等待选项对于加载后才填充的列表至关重要。ajax_wait 告知 API 等待异步商品网格加载完成,而 page_wait 在加载后再固定等待 5,000 毫秒,确保延迟渲染的卡片在页面被捕获前全部出现。Crawlbase 在 cb_status 响应头中返回上游状态码,因此你检查的是这个字段,而不是顶层的状态字段。运行脚本后,你应该能看到真实的商品标记,而不是验证页面的外壳。这确认了渲染可以正常工作,你才能开始编写选择器。

Crawlbase Best Buy Scraper

Best Buy 通过 JavaScript 填充搜索网格,并封锁非浏览器流量,这正是上面那个原始请求返回空结果的原因。Crawling API 接收你的 token,使用 ajax_waitpage_wait 在真实浏览器中运行搜索页面,确保卡片加载完毕,在服务端轮换住宅 IP,并处理 CAPTCHA 验证,然后将完整的 HTML 交给你。你无需自己运行无头浏览器集群和代理池。从免费的最多 20,000 次请求套餐开始体验。

第二步:用 BeautifulSoup 解析商品卡片

拿到渲染后的 HTML,将其加载到 BeautifulSoup 中,找到每张商品卡片,通过选择器提取各字段。Best Buy 将每个商品列为 ol.sku-item-list 内的 li.sku-item,标题和评分在 column-middle 中,价格和库存状态在 column-right 中。用 try/except 包裹每张卡片,这样一条格式错误的记录不会导致整次运行崩溃。

python
from bs4 import BeautifulSoup

BASE = "https://www.bestbuy.com"

def text_of(card, selector):
    el = card.select_one(selector)
    return el.get_text(strip=True) if el else None

def parse_card(card):
    title_el = card.select_one("div.column-middle h4.sku-title > a")
    href = title_el["href"] if title_el and title_el.has_attr("href") else None
    return {
        "title": title_el.get_text(strip=True) if title_el else None,
        "price": text_of(card, 'div.column-right div.sku-list-item-price div[data-testid="customer-price"] > span'),
        "model": text_of(card, "div.column-middle div.sku-model span.sku-value"),
        "sku": card.get("data-sku-id"),
        "rating": text_of(card, "div.column-middle div.ratings-reviews div.c-ratings-reviews > p"),
        "review_count": text_of(card, "div.column-middle div.ratings-reviews span.c-reviews"),
        "availability": text_of(card, "div.column-right div.fulfillment-add-to-cart-button button"),
        "product_url": BASE + href if href else None,
    }

def scrape_bestbuy_listing(html):
    soup = BeautifulSoup(html, "html.parser")
    cards = soup.select("ol.sku-item-list li.sku-item")
    results = []
    for card in cards:
        try:
            results.append(parse_card(card))
        except Exception as e:
            print(f"Skipped a card: {e}")
    return results

text_of 辅助函数查询卡片内的一个元素,当元素缺失时返回 None,而不是在调用 .get_text() 时抛出异常。这让提取过程在字段缺失时依然健壮,而这种情况很常见,因为并非每条商品都显示价格或评分。标题和商品链接均来自 h4.sku-title > a 锚元素,价格来自 customer-price test-id span,SKU 来自卡片自身的 data-sku-id 属性。库存状态从履行按钮的文字中读取,有货时显示 "Add to Cart",否则显示 "Sold Out" 或 "Coming Soon",因此按钮标签同时充当库存信号。

选择器会漂移

Best Buy 的类名和 data-testid 值会在无预告的情况下发生变化。ol.sku-item-listli.sku-item 以及 column-middle / column-right 这类结构性标记,通常比深层的类名链条更持久。当某个字段对每张卡片都返回 None 时,在浏览器开发者工具中重新检查实际搜索页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常的事。

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

现在将获取和解析两个步骤串联成一个可运行的脚本,然后将记录写入 JSON 和 CSV,方便加载到 notebook 或电子表格中。获取渲染后的搜索页面,传给解析器,再输出结构化数据行。

python
import csv
import json
from urllib.parse import quote_plus
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})
BASE = "https://www.bestbuy.com"
FIELDS = ["title", "price", "model", "sku", "rating", "review_count", "availability", "product_url"]

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

def text_of(card, selector):
    el = card.select_one(selector)
    return el.get_text(strip=True) if el else None

def parse_card(card):
    title_el = card.select_one("div.column-middle h4.sku-title > a")
    href = title_el["href"] if title_el and title_el.has_attr("href") else None
    return {
        "title": title_el.get_text(strip=True) if title_el else None,
        "price": text_of(card, 'div.column-right div.sku-list-item-price div[data-testid="customer-price"] > span'),
        "model": text_of(card, "div.column-middle div.sku-model span.sku-value"),
        "sku": card.get("data-sku-id"),
        "rating": text_of(card, "div.column-middle div.ratings-reviews div.c-ratings-reviews > p"),
        "review_count": text_of(card, "div.column-middle div.ratings-reviews span.c-reviews"),
        "availability": text_of(card, "div.column-right div.fulfillment-add-to-cart-button button"),
        "product_url": BASE + href if href else None,
    }

def scrape_bestbuy_listing(html):
    soup = BeautifulSoup(html, "html.parser")
    cards = soup.select("ol.sku-item-list li.sku-item")
    results = []
    for card in cards:
        try:
            results.append(parse_card(card))
        except Exception as e:
            print(f"Skipped a card: {e}")
    return results

def export(rows, name="bestbuy_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():
    search_term = "i phone"
    url = f"{BASE}/site/searchpage.jsp?st={quote_plus(search_term)}"
    html = crawl(url)
    if not html:
        return
    rows = scrape_bestbuy_listing(html)
    export(rows)

if __name__ == "__main__":
    main()

使用 python bestbuy_scraper.py 运行完整脚本。它获取渲染后的搜索页面,为每个商品解析一行数据,并写入 bestbuy_products.jsonbestbuy_products.csv。共享的 FIELDS 列表使 CSV 的列顺序与字典键保持一致,让两个导出文件永远不会出现偏差。

输出结果示例

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

json
[
  {
    "title": "Apple - iPhone 14 128GB (Unlocked) - Midnight",
    "price": "$729.99",
    "model": "MPUA3LL/A",
    "sku": "6507555",
    "rating": "Rating 4.9 out of 5 stars with 155 reviews",
    "review_count": "(155)",
    "availability": "Add to Cart",
    "product_url": "https://www.bestbuy.com/site/apple-iphone-14-128gb-unlocked-midnight/6507555.p?skuId=6507555"
  },
  {
    "title": "Apple - iPhone SE (3rd Generation) 64GB (Unlocked)",
    "price": "$429.99",
    "model": "MMX73LL/A",
    "sku": "6507470",
    "rating": "Rating 4.5 out of 5 stars with 111 reviews",
    "review_count": "(111)",
    "availability": "Add to Cart",
    "product_url": "https://www.bestbuy.com/site/apple-iphone-se-3rd-generation-64gb-unlocked/6507470.p?skuId=6507470"
  }
]

处理结果页翻页

一页搜索只是演示;真正的研究任务需要遍历完整结果集。Best Buy 将搜索结果分布在多个页面,并通过 cp URL 参数跟踪当前页码:&cp=1 是第一页,&cp=2 是第二页,以此类推。要收集完整数据集,按顺序翻页,当某页不返回商品时停止,并控制请求节奏,避免在短时间内高频访问 Best Buy。

python
import time

def scrape_all_pages(search_term, max_pages=5):
    base_url = f"{BASE}/site/searchpage.jsp?st={quote_plus(search_term)}"
    all_rows = []
    for page_number in range(1, max_pages + 1):
        page_url = f"{base_url}&cp={page_number}"
        html = crawl(page_url)
        if not html:
            break
        rows = scrape_bestbuy_listing(html)
        if not rows:
            print(f"No products on page {page_number}, stopping.")
            break
        all_rows.extend(rows)
        print(f"Page {page_number}: {len(rows)} products")
        time.sleep(2)
    return all_rows

if __name__ == "__main__":
    rows = scrape_all_pages("i phone", max_pages=5)
    export(rows)

当搜索结果耗尽时,空结果中断会提前停止循环;time.sleep(2) 控制请求节奏,避免因连续快速发送请求而被标记。将搜索词替换为任何你想要的查询,把同一个解析器指向单个商品页面而非搜索 URL,就能将其扩展为价格追踪管道。关于如何将此类数据流转化为监控工具的更宏观视角,请参阅利用网络抓取实现价格情报以及构建价格比较工具的指南。

保持不被封锁

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

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

更宏观的操作手册请参阅如何抓取网站而不被封锁,以及关于渲染为何重要的如何抓取 JavaScript 网站。如果你的项目属于更广泛的零售场景,电商网络抓取综述涵盖了 Best Buy 和 Amazon 商品数据等各大商店的共同模式。

抓取 Best Buy 是否合法?

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

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

本指南刻意将范围限定在公开的搜索和商品页面,因为这是保持工作可辩护性的界限。它不涉及登录后的任何内容、账号或订单数据、个人信息,也不涉及任何绕过身份验证或你无权通过的 CAPTCHA 的尝试。Best Buy 提供了附属合作伙伴计划和官方商品数据 feed,供有许可需求的用户使用,当你需要大量数据、保证结构或商业权利时,那才是正确的途径,而不是更聪明的爬虫。

回顾

核心要点

  • Best Buy 是一个公开的电子产品目录。其搜索和商品页面提供标题、价格、型号、SKU、评分和库存状态,这正是它对价格追踪和市场研究如此有用的原因。
  • 你需要同时具备渲染能力和可信 IP。Best Buy 在客户端填充搜索网格,并封锁爬虫流量,因此 Crawling API 通过一次调用在住宅 IP 后面渲染页面,并设置 ajax_waitpage_wait
  • BeautifulSoup 负责提取。遍历 ol.sku-item-list li.sku-item 卡片,将每个字段映射到对应选择器,并预期随着 Best Buy 标记变化,选择器会发生漂移。
  • cp 参数翻页。通过 &cp=N 遍历页面,在空页时停止,并在页面间加入短暂延迟控制节奏。
  • 坚守公开数据。遵守 Best Buy 的服务条款和 robots.txt,对于有许可或批量需求的数据使用官方 feed,切勿触碰账号、订单或个人信息。

常见问题

为什么普通请求从 Best Buy 返回不了商品?

Best Buy 在客户端渲染搜索网格,因此普通的 requests.get() 拿到的是一个没有商品卡片的外壳,这就是为什么普通解析器返回空列表。此外,Best Buy 还会挑战或封锁非真实浏览器的流量。通过 Crawling API 在可信 IP 后面渲染页面,并设置 ajax_waitpage_wait 选项,可以同时解决这两个问题,这也是本爬虫通过它发起请求的原因。

我可以从 Best Buy 商品列表中抓取哪些字段?

从每个搜索结果卡片,你可以读取商品标题、当前顾客价格、制造商型号、Best Buy 自有 SKU、星级评分和评论数、来自履行按钮的库存状态,以及指向商品详情页的链接。本指南的爬虫将所有这些提取到每个商品的一条记录中,然后写入 JSON 和 CSV。

如何处理 Best Buy 上的翻页?

Best Buy 通过 cp URL 参数跟踪当前搜索页码,因此 &cp=2 请求第二页,以此类推。循环遍历页码,每次在搜索 URL 后追加 &cp=N,当某页不返回商品时停止。在请求之间加入短暂延迟,控制运行节奏,避免连续快速发送请求。

Crawling API 如何处理 Best Buy 的 JavaScript 内容?

Crawling API 在返回页面之前,会在真实浏览器中渲染页面,ajax_waitpage_wait 选项控制等待异步内容的时长。将 ajax_wait 设为 true 会等待异步商品网格加载完成,page_wait 则在加载后固定等待若干毫秒,确保延迟渲染的卡片出现在你拿到的 HTML 中。

抓取 Best Buy 时如何避免被封锁?

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

我可以追踪 Best Buy 随时间的价格变化吗?

可以。按计划运行爬虫,为每次导出标注日期,并保存快照。比较连续运行的结果,可以看出哪些商品价格发生了变化,或进出了有货状态,这就是价格监控和竞争分析的基础。同一个解析器也适用于单个商品页面,因此你可以将追踪范围缩小到你关注的特定 SKU。

开始构建

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

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

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