Cars and Bids 运营公开的汽车拍卖,每个拍卖页面都是一块整洁的结构化事实:车辆的年份、品牌和型号,当前最高出价,拍卖剩余时间,车辆所在位置,以及指向完整详情页的链接。对于追踪爱车市场的人来说,这张实时拍卖网格是最干净的公开信号之一,这也是为什么经销商、研究人员和分析师会通过它来观察价格走势、需求变化,以及哪些车型正在成交。

本指南介绍如何用 Python 抓取 Cars and Bids 拍卖列表。你将构建一个小型可运行爬虫,通过 Crawling API 获取拍卖列表页面,为每场拍卖解析出干净的记录,处理翻页,并将结果导出为 JSON 和 CSV。整个教程仅涉及公开拍卖数据:任何人无需登录即可在列表页面上看到的标题、出价、剩余时间和位置信息。

你将构建的内容

一个 Python 脚本,接收 Cars and Bids 列表 URL,通过 Crawling API 获取渲染后的页面,并为每张拍卖卡片提取一条结构化记录。我们以按品牌筛选的搜索页面作为贯穿全文的示例,与旧版教程使用相同的方式,并从每条列表中抽取以下字段:

  • Title(标题)拍卖标题,包含车辆的年份、品牌和型号。
  • Subtitle(副标题)标题下方的简短描述行(配置级别、值得关注的选项、底价状态)。
  • Current bid(当前出价)爬取时刻该拍卖的最高出价。
  • Time left(剩余时间)拍卖结束前的剩余时间。
  • Location(位置)车辆所在的城市和地区。
  • Link(链接)指向拍卖详情页的 URL。

为什么普通请求在 Cars and Bids 上会失败

如果你用裸 HTTP 客户端访问 Cars and Bids 列表 URL,很少能得到你想要的拍卖信息。两个因素对你不利。首先,列表网格是在客户端渲染的:网站先发送一个轻量外壳,随后由页面的 JavaScript 填充拍卖卡片,因此你收到的初始 HTML 往往是一个空框架,没有任何列表。其次,自动化流量会被迅速识别。数据中心 IP 段和不像真实浏览器的请求模式,在你访问到拍卖信息之前就会遭遇验证挑战或彻底封锁。

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

前置条件

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

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

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

Crawlbase 账号和 token。注册免费账号,打开控制台,复制你的 token。由于 Cars and Bids 依赖 JavaScript 来加载拍卖信息,你需要使用 JavaScript(JS)token 而不是普通 token。免费套餐包含 1,000 次请求,无需绑定信用卡,足以构建和测试这个爬虫。请像对待密码一样保管好 token,不要将其提交到版本控制系统。

项目设置

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

bash
python --version

python -m venv carsandbids-scraper
source carsandbids-scraper/bin/activate

pip install crawlbase beautifulsoup4

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

bash
touch carsandbids_scraper.py

了解列表页面

Cars and Bids 的搜索页面位于稳定的 URL。例如,按品牌筛选的列表 URL 是 https://carsandbids.com/search/bmw,其他品牌的模式相同。页面以网格形式排列拍卖卡片,每辆车对应一张,每张卡片包含同样几个字段:拍卖标题(年份、品牌、型号)、副标题、缩略图、当前出价、剩余时间、位置,以及指向拍卖详情页的链接。

在编写选择器之前,在浏览器中打开列表页面,右键单击一张拍卖卡片并选择"检查"。每场拍卖位于一个标有 auction-item 类的 li 元素中。其内部,标题位于 div.auction-title,副标题位于 p.auction-subtitle,位置位于 p.auction-loc,缩略图位于 img,链接位于卡片的锚元素中。这些就是你要定位的元素。

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

从获取完整页面开始。导入 CrawlingAPI 类,用你的 JS token 初始化它,设置列表 URL,并发起请求。在解析之前检查状态码,可以让失败情况明显暴露而不是悄然无声。

python
from crawlbase import CrawlingAPI

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

def make_crawlbase_request(url, options):
    response = crawling_api.get(url, options)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Failed to fetch the page. Crawlbase status: {response['headers']['pc_status']}")
    return None

if __name__ == "__main__":
    listing_url = "https://carsandbids.com/search/bmw"
    options = {"ajax_wait": "true", "page_wait": 10000}
    html = make_crawlbase_request(listing_url, options)
    print(html[:500] if html else "No HTML returned")

这两个选项对于加载后才填充的网格至关重要。ajax_wait 告知 API 等待异步内容加载完成,page_wait 在加载后再固定等待若干毫秒(这里是 10,000),确保延迟渲染的拍卖卡片在页面被捕获前全部出现。状态检查读取 Crawling API 返回的 pc_status 响应头,值为 200 表示渲染成功。运行脚本后,你应该能看到真实的列表标记,而不是空外壳。这确认了渲染可以正常工作,你才能开始编写选择器。

Crawlbase Crawling API

Cars and Bids 的拍卖列表网格只有在 JavaScript 运行后才会出现,而光有渲染还不够,请求还必须来自网站信任的 IP。Crawling API 接收你的 token,使用你刚刚设置的 ajax_waitpage_wait 选项在真实浏览器中运行页面,在服务端轮换住宅 IP,并处理 CAPTCHA 验证,然后将完整的 HTML 交给你。你无需自己运行无头浏览器集群和代理池。先在免费的 1,000 次请求套餐上试试。

第二步:用 BeautifulSoup 解析拍卖卡片

拿到渲染后的 HTML,将其加载到 BeautifulSoup 中,找到每张拍卖卡片,通过选择器提取各字段。每张卡片是一个 li.auction-item;标题、副标题、位置、当前出价、剩余时间、缩略图和链接都位于其内部。对每次查询加上存在性检查,可以让提取在字段缺失时依然健壮,因为并非每张卡片都显示相同的数据。

python
from bs4 import BeautifulSoup

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

def text_of(listing, tag, css_class):
    el = listing.find(tag, class_=css_class)
    return el.text.strip() if el else None

def scrape_listing_page(html_content):
    soup = BeautifulSoup(html_content, "html.parser")
    car_listings = soup.find_all("li", class_="auction-item")

    extracted_data = []
    for listing in car_listings:
        link_tag = listing.find("a")
        thumbnail = listing.find("img")
        extracted_data.append({
            "title": text_of(listing, "div", "auction-title"),
            "sub_title": text_of(listing, "p", "auction-subtitle"),
            "current_bid": text_of(listing, "span", "bid-value"),
            "time_left": text_of(listing, "span", "td-time"),
            "location": text_of(listing, "p", "auction-loc"),
            "thumbnail": thumbnail["src"] if thumbnail else None,
            "link": BASE + link_tag["href"] if link_tag else None,
        })
    return extracted_data

text_of 辅助函数查询卡片内的一个元素,当元素缺失时返回 None,而不是在调用 .text 时抛出异常。标题来自 div.auction-title,包含年份、品牌和型号;副标题来自 p.auction-subtitle;位置来自 p.auction-loc;当前出价来自出价值 span;剩余时间来自时间显示 span。链接通过在卡片相对 href 前加上网站域名来构建,因此存储的是绝对 URL。

选择器会漂移

网站标记会在无预告的情况下变化,上面的拍卖卡片类名只是起始模板,不是约定。li.auction-item 容器以及 auction-title / auction-subtitle / auction-loc 类是较为持久的锚点;出价和剩余时间的 span 最容易发生变化。当某个字段对每张卡片都返回 None 时,在浏览器开发者工具中重新检查实际列表并更新选择器。定期维护选择器对任何生产爬虫来说都是正常的事。

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

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

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

crawling_api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})
BASE = "https://www.carsandbids.com"
FIELDS = ["title", "sub_title", "current_bid", "time_left", "location", "thumbnail", "link"]

def make_crawlbase_request(url, options):
    response = crawling_api.get(url, options)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Failed to fetch the page. Crawlbase status: {response['headers']['pc_status']}")
    return None

def text_of(listing, tag, css_class):
    el = listing.find(tag, class_=css_class)
    return el.text.strip() if el else None

def scrape_listing_page(html_content):
    soup = BeautifulSoup(html_content, "html.parser")
    car_listings = soup.find_all("li", class_="auction-item")

    extracted_data = []
    for listing in car_listings:
        link_tag = listing.find("a")
        thumbnail = listing.find("img")
        extracted_data.append({
            "title": text_of(listing, "div", "auction-title"),
            "sub_title": text_of(listing, "p", "auction-subtitle"),
            "current_bid": text_of(listing, "span", "bid-value"),
            "time_left": text_of(listing, "span", "td-time"),
            "location": text_of(listing, "p", "auction-loc"),
            "thumbnail": thumbnail["src"] if thumbnail else None,
            "link": BASE + link_tag["href"] if link_tag else None,
        })
    return extracted_data

def export(rows, name="carsandbids_listings"):
    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)} auctions to {name}.json and {name}.csv")

def main():
    url = "https://carsandbids.com/search/bmw"
    options = {"ajax_wait": "true", "page_wait": 10000}
    html = make_crawlbase_request(url, options)
    if not html:
        return
    rows = scrape_listing_page(html)
    export(rows)

if __name__ == "__main__":
    main()

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

输出结果示例

你将得到一份干净的拍卖记录列表,按页面顺序排列,可直接写入 JSON、CSV 或数据库。

json
[
  {
    "title": "2014 BMW 335i Sedan",
    "sub_title": "No Reserve: Turbo 6-Cylinder, M Sport Package, California-Owned",
    "current_bid": "$9,500",
    "time_left": "2 days",
    "location": "Los Angeles, CA 90068",
    "thumbnail": "https://media.carsandbids.com/cdn-cgi/image/width=768/photos/rkVPlNqQ.jpg",
    "link": "https://www.carsandbids.com/auctions/9QxJ8nV7/2014-bmw-335i-sedan"
  },
  {
    "title": "2009 BMW 328i Sports Wagon",
    "sub_title": "No Reserve: Inspected 3.0-Liter 6-Cylinder, Premium Package",
    "current_bid": "$12,750",
    "time_left": "5 hours",
    "location": "San Diego, CA 92120",
    "thumbnail": "https://media.carsandbids.com/cdn-cgi/image/width=768/photos/3g6kOmG9.jpg",
    "link": "https://www.carsandbids.com/auctions/30n7Yqaj/2009-bmw-328i-sports-wagon"
  }
]

处理翻页

单页搜索只是演示;真正的研究任务需要遍历所有结果页面。Cars and Bids 通过 ?page= 参数进行分页,因此你可以递增页码,直到某页不返回任何拍卖卡片时停止。在请求之间加入短暂延迟,避免在紧密循环中高频访问网站。

python
import time

def scrape_all_pages(search_url, max_pages=10):
    options = {"ajax_wait": "true", "page_wait": 10000}
    all_rows = []
    for page in range(1, max_pages + 1):
        page_url = f"{search_url}?page={page}"
        html = make_crawlbase_request(page_url, options)
        if not html:
            break
        found = scrape_listing_page(html)
        if not found:
            print(f"No auctions on page {page}; stopping.")
            break
        all_rows.extend(found)
        print(f"Page {page}: {len(found)} auctions")
        time.sleep(2)
    return all_rows

当搜索结果耗尽时,空结果中断会提前停止循环;time.sleep(2) 控制请求节奏,避免因连续快速发送请求而被标记。在 main 中将单次获取替换为对 scrape_all_pages("https://carsandbids.com/search/bmw") 的调用,其余管道(解析、导出)可以直接处理合并后的列表。要随时间追踪某场拍卖,按计划运行任务并为每次导出标注日期,再对比连续快照,即可看出出价和剩余时间的变化。

保持不被封锁

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

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

关于避免封锁的更宏观操作手册,请参阅如何抓取网站而不被封锁,以及关于渲染为何重要的如何抓取 JavaScript 网站。如果你需要将这类数据用于定价工作,网络抓取与价格情报指南介绍了如何将原始列表数据转化为可用信号。

抓取 Cars and Bids 是否合法?

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

有几条底线值得坚守。只收集公开拍卖数据:任何人无需账号即可在搜索页面上看到的标题、副标题、当前出价、剩余时间、位置和列表链接。将请求量控制在不会给网站服务器造成压力的范围内,避免涉及个人数据,包括任何与可识别的卖家、竞拍者或评论者相关的信息(超出公开列出的范围)。不要将列表照片或描述作为你自己的内容再分发,因为这些媒体受版权保护。

本指南刻意将范围限定在公开列表页面,因为这是保持工作可辩护性的界限。它不涉及登录后的任何内容、账号或竞拍数据、个人信息,也不涉及任何绕过身份验证或你无权通过的 CAPTCHA 的尝试。如果你的项目需要超出公开列表数据范围的内容,正确的途径是与网站签署官方数据协议,而不是更聪明的爬虫。

回顾

核心要点

  • Cars and Bids 列表是实时拍卖信号。每个搜索页面包含每辆车当前的标题、出价、剩余时间和位置,这正是它对市场研究和定价如此有用的原因。
  • 你需要同时具备渲染能力和可信 IP。列表网格在客户端加载,爬虫流量会被封锁,因此 Crawling API 通过一次调用在住宅 IP 后面渲染页面,并设置 ajax_waitpage_wait
  • BeautifulSoup 负责提取。遍历 li.auction-item 卡片,将标题、副标题、当前出价、剩余时间、位置和链接映射到当前选择器,并预期选择器会发生漂移。
  • 遍历页面并导出。递增 ?page= 参数直到某页不返回卡片,然后用共享字段列表将合并后的数据行写入 JSON 和 CSV,保持两个文件同步。
  • 坚守公开数据。遵守网站的服务条款和 robots.txt,保持适度的请求量,不要触碰账号、出价、个人数据或你会再分发的受版权保护媒体。

常见问题

为什么普通请求从 Cars and Bids 返回不了拍卖信息?

列表网格在客户端渲染:网站先发送一个近乎空白的外壳,随后由 JavaScript 填充拍卖卡片,因此原始请求拿到的往往是一个没有列表的框架。此外,网站还会挑战或封锁非真实浏览器的流量。通过 Crawling API 在可信 IP 后面渲染页面,并设置 ajax_waitpage_wait,可以同时解决这两个问题,这也是本爬虫通过它发起请求的原因。

抓取 Cars and Bids 应该使用哪个 Crawlbase token?

使用 JavaScript(JS)token。Cars and Bids 动态加载拍卖信息,因此需要 JS token 启用的渲染能力;普通 token 只会返回未渲染的外壳。免费套餐包含 1,000 次请求,无需信用卡,足以构建和测试爬虫。

如何在 Cars and Bids 上抓取特定品牌或搜索结果?

将爬虫指向你想要的搜索 URL。品牌筛选只是一个路径,例如 https://carsandbids.com/search/bmw 对应 BMW,替换末尾的品牌名即可定位不同的拍卖集合。要覆盖完整结果集,遍历 ?page= 参数直到某页不返回卡片。

可以从 Cars and Bids 列表中提取哪些字段?

从每张拍卖卡片可以提取标题(年份、品牌、型号)、副标题、当前出价、剩余时间、位置、缩略图和指向拍卖详情页的链接。解析器通过 CSS 选择器映射每个字段,你可以从输出字典中删去不需要的字段。

如何处理多场拍卖的翻页?

Cars and Bids 通过 ?page= 参数进行分页。循环递增页码,获取并解析每页,当某页不返回拍卖卡片时中断。在请求之间加入短暂延迟,控制运行节奏而不是连续高频访问,再将所有数据行汇总到一个列表后再导出。

抓取 Cars and Bids 时如何避免被封锁?

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

开始构建

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

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

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