Gumtree 是英国访问量最大的在线分类广告网站之一,这里是一个单一的市场,人们可以在本地发布汽车、家具、房产、电子产品和招聘信息。每个搜索结果页面都是当前在售商品的公开结构化数据源:每张卡片包含标题、要价、地点以及通往完整广告的链接。这使其成为价格比较、区域需求研究以及追踪列表随时间变化的干净数据源。

本指南展示如何使用 Python 抓取 Gumtree 分类广告列表。你将构建一个小型可运行爬虫,通过 Crawling API 获取 Gumtree 搜索页面,为每个列表卡片解析一条整洁记录,处理分页,并将结果导出为 JSON 和 CSV。整个教程范围限定在任何人无需登录即可在搜索结果页面上看到的公开列表数据:标题、价格、地点和链接。

你将构建什么

一个 Python 脚本,接收 Gumtree 搜索 URL,通过 Crawling API 获取已渲染的结果页面,并为每个列表卡片提取一条结构化记录。我们以耳机搜索作为贯穿全文的示例,与之前的教程使用相同的查询,并从每张卡片中提取以下字段:

  • 标题:卡片上显示的列表标题,例如汽车的品牌和型号。
  • 价格:卖家的要价,当卡片显示时。
  • 地点:商品所在地区,用于区域需求分析。
  • 链接:指向该广告详情页面的绝对 URL。

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

如果你将裸 HTTP 客户端指向 Gumtree 搜索 URL,很少能得到你所需的卡片。两个问题与你作对。首先,Gumtree 在客户端渲染大部分结果网格:服务器发送一个轻量级外壳,列表卡片在页面 JavaScript 运行时才填充进来,因此普通 requests.get 得到的初始 HTML 通常缺少你想解析的卡片。其次,Gumtree 会监控自动化流量。数据中心 IP 段和不像真实浏览器的请求模式会在你到达列表之前遭遇挑战页面、限速或彻底拦截。

因此,一个有效的 Gumtree 爬虫需要在单次请求中同时具备两项能力:一个能渲染页面的浏览器,以及一个被 Gumtree 视为真实访客的 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。免费层包含 1,000 次请求,无需信用卡,足以构建和测试这个爬虫。像对待密码一样保管 token,不要将其提交到版本控制中。

设置项目

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

bash
python --version

python -m venv gumtree_env
source gumtree_env/bin/activate

pip install crawlbase beautifulsoup4

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

bash
touch gumtree_scraper.py

检查搜索页面以获取选择器

在编写任何选择器之前,在浏览器中打开 Gumtree 搜索结果页面,右键点击一个列表卡片,选择"检查"。Gumtree 将每个结果包装在带有 data-q="search-result" 属性的 article 元素中,并在该容器内用各自稳定的 data-* 属性暴露字段。依靠这些结构性属性而不是脆弱的生成类名链:它们在视觉改版时存活性好得多。

通过检查结果页面,以下是之前教程所依赖的选择器,它们仍然是正确的起点:

  • 标题:带有 data-q="tile-title"<div>
  • 价格:带有 data-testid="price"<div>
  • 地点:带有 data-q="tile-location"<div>
  • 链接:带有 data-q="search-result-anchor"<a> 标签上的 href,它是相对路径,需要加上 https://www.gumtree.com 主机前缀。

步骤 1:获取已渲染的搜索页面

先获取完整页面。导入 CrawlingAPI 类,使用你的 token 初始化,设置搜索 URL,并发起请求。在解析之前检查状态码,可以让失败明显而不是无声无息。

python
from crawlbase import CrawlingAPI

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

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

if __name__ == "__main__":
    search_url = "https://www.gumtree.com/search?q=headset"
    html = crawl(search_url)
    print(html[:500] if html else "No HTML returned")

两个等待选项对于在页面加载时填充内容的网格至关重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后保持固定毫秒数,使迟渲染的卡片在页面被捕获前出现。运行脚本,你应该看到真实的列表标记,而不是挑战外壳。这在你编写任何选择器之前就确认了渲染是否正常工作。

Crawlbase Crawling API

Gumtree 的结果网格在客户端渲染,该网站会挑战看起来不像真实访客的流量。Crawling API 接收你的 token,在真实浏览器中运行搜索页面,在服务器端通过住宅 IP 轮换,并处理任何 CAPTCHA,然后将渲染完毕的 HTML 交给你。你无需自行运行无头浏览器集群和代理池。先在 1,000 次免费请求层将其指向搜索 URL 试试看。

步骤 2:使用 BeautifulSoup 解析列表卡片

获取到已渲染 HTML 后,将其加载到 BeautifulSoup,找到每个结果卡片,并通过选择器提取各字段。Gumtree 将每个结果包装在 article[data-q="search-result"] 容器中,并在其内用各自的 data-* 属性暴露标题、价格和地点。从卡片的锚点读取列表链接,并将其规范化为绝对 URL。将每张卡片包裹在 try/except 中,这样一个格式错误的列表不会中断整个运行。

python
from bs4 import BeautifulSoup

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

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

def parse_link(tile):
    a = tile.select_one('a[data-q="search-result-anchor"]')
    if not a or not a.get("href"):
        return None
    href = a["href"]
    return href if href.startswith("http") else BASE + href

def scrape_gumtree_search(html):
    soup = BeautifulSoup(html, "html.parser")
    tiles = soup.select('article[data-q="search-result"]')
    results = []
    for tile in tiles:
        try:
            results.append({
                "title": text_of(tile, 'div[data-q="tile-title"]'),
                "price": text_of(tile, 'div[data-testid="price"]'),
                "location": text_of(tile, 'div[data-q="tile-location"]'),
                "link": parse_link(tile),
            })
        except Exception as e:
            print(f"Skipped a tile: {e}")
    return results

text_of 辅助函数查询卡片内的一个元素,在其缺失时返回 None,而不是对空对象调用 .get_text() 引发异常。这使提取在字段缺失时保持鲁棒性,而不是每个列表都显示整洁价格,这种情况很常见。链接来自 a[data-q="search-result-anchor"] 锚点,并规范化为绝对 URL,因为 Gumtree 提供的是指向广告页面的相对 href

选择器会发生变化

Gumtree 的 data-qdata-testid 属性比生成的类名更耐久,但它们不是永久的:改版可能重命名或移除它们。将上述选择器视为起始模板,而非固定契约。当某个字段对每张卡片都返回 None 时,在浏览器开发者工具中重新检查实时搜索页面并更新选择器。定期维护选择器是任何生产爬虫的正常操作。

步骤 3:处理分页并导出 JSON 和 CSV

一个结果页面是演示;真实的任务需要遍历多页。Gumtree 使用 ?page=N 查询参数对搜索结果进行分页,因此你可以循环遍历固定数量的页面,获取每一页,并收集卡片。现在将获取、解析和分页循环整合为一个可运行脚本,然后将记录写入 JSON 和 CSV,以便你可以将其加载到笔记本或电子表格中。

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

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})
BASE = "https://www.gumtree.com"
FIELDS = ["title", "price", "location", "link"]

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

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

def parse_link(tile):
    a = tile.select_one('a[data-q="search-result-anchor"]')
    if not a or not a.get("href"):
        return None
    href = a["href"]
    return href if href.startswith("http") else BASE + href

def scrape_gumtree_search(html):
    soup = BeautifulSoup(html, "html.parser")
    tiles = soup.select('article[data-q="search-result"]')
    results = []
    for tile in tiles:
        try:
            results.append({
                "title": text_of(tile, 'div[data-q="tile-title"]'),
                "price": text_of(tile, 'div[data-testid="price"]'),
                "location": text_of(tile, 'div[data-q="tile-location"]'),
                "link": parse_link(tile),
            })
        except Exception as e:
            print(f"Skipped a tile: {e}")
    return results

def scrape_pages(base_url, max_pages):
    all_listings = []
    for page in range(1, max_pages + 1):
        page_url = f"{base_url}&page={page}"
        html = crawl(page_url)
        if not html:
            break
        found = scrape_gumtree_search(html)
        if not found:
            break
        all_listings.extend(found)
        print(f"Page {page}: {len(found)} listings")
        time.sleep(2)
    return all_listings

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

def main():
    base_url = "https://www.gumtree.com/search?q=headset"
    rows = scrape_pages(base_url, max_pages=5)
    export(rows)

if __name__ == "__main__":
    main()

使用 python gumtree_scraper.py 运行完整脚本。它遍历耳机搜索的最多五个页面,为每个列表卡片解析一条记录,并写入 gumtree_listings.jsongumtree_listings.csvscrape_pages 循环在搜索 URL 后追加 &page=N,当某页没有卡片时提前中断(避免获取空页面),而页面之间的 time.sleep(2) 控制运行节奏。共享的 FIELDS 列表使 CSV 列顺序与字典键保持一致,让两种导出格式永远不会产生偏差。

输出示例

你将得到一个整洁的列表记录,按页面顺序排列,可直接写入 JSON、CSV 或数据库。

json
[
  {
    "title": "SteelSeries Arctis 7 Wireless Gaming Headset",
    "price": "£65.00",
    "location": "Manchester, Greater Manchester",
    "link": "https://www.gumtree.com/p/headphones/steelseries-arctis-7/1488114476"
  },
  {
    "title": "Sony WH-1000XM4 Noise Cancelling Headphones",
    "price": "£180.00",
    "location": "Leeds, West Yorkshire",
    "link": "https://www.gumtree.com/p/headphones/sony-wh-1000xm4/1483456978"
  }
]

CSV 将这些行以每条列表一行的形式镜像,标题为 title,price,location,link。从那里你可以按价格排序,按地点分组查看特定商品最集中的地区,或对比连续运行的结果以追踪列表和要价随时间的变化。有关将列表导出转化为对比视图的实际案例,请参阅电商网络爬取

扩展到更多搜索

同样的模式可以扩展到你需要的任意多个搜索。保持一个查询名称到搜索 URL 的映射,遍历它,并为每个查询运行分页爬虫,以查询为键存储输出以保持列表分离。如果你想要更丰富的记录,跟随每张卡片的 link 到其广告页面并解析更完整的字段(完整描述、发布日期、图片 URL),然后将这些合并到列表记录中。无论哪种方式,保持请求速率适中,并依赖 Crawling API 的轮换,这样在大量搜索中展开时就不会触发速率限制。

对于本地市场研究,地点字段是杠杆:按地区筛选,或在多个城市搜索中抓取同一查询,让你可以逐地区比较价格和供应。这正是 Gumtree 公开列表最适合的区域性信号类型,也是为什么以结构化形式收集分类数据比手动浏览网站更有价值。

保持不被封锁

即使渲染已处理,Gumtree 仍会监控爬虫形态的流量。以下几个习惯能让运行保持健康,它们适用于任何分类广告或市场目标。

  • 控制请求节奏。在页面和搜索之间加入延迟分散请求,而不是以全速爬取所有内容。将繁重的任务安排在非高峰时段,以减轻 Gumtree 服务器的负担。
  • 依赖 IP 轮换。住宅 IP 池将请求分散到众多真实用户地址,使任何一个都不会触发速率限制。Crawling API 为你处理这些;如果你自建方案,这是需要重点投入的部分。
  • 只保留你需要的内容。存储你的项目使用的公开列表字段,丢弃其余内容。定期检查你的选择器,使爬虫跟上标记变化的节奏。

有关避免封锁的更广泛操作手册,请参阅如何不被封锁地抓取网站;有关渲染在这里为何重要的更多内容,请参阅如何爬取 JavaScript 网站。如果你想深入了解 BeautifulSoup 部分,关于在 Python 中使用 BeautifulSoup 的指南详细介绍了该解析库。

抓取 Gumtree 合法吗?

抓取 Gumtree 是否被允许,取决于 Gumtree 的服务条款、你所在的司法管辖区以及你对数据的使用方式。Gumtree 的条款对自动化访问设有限制,因此无论你的工具多么谨慎,抓取行为都可能违反这些条款。这里的任何代码都不改变这一事实,它只是使技术部分可行。请阅读 Gumtree 的服务条款和 robots.txt,并将两者视为你收集范围的边界。对于商业或竞争性使用,法律情况更为复杂,咨询法律专家了解你的具体情况是明智之举。

以下几条值得坚守。只收集公开列表数据:任何人在搜索结果页面上无需账号即可看到的标题、价格、地点和广告链接。不要收集私人卖家的个人数据:姓名、电话号码、电子邮件地址或任何可以识别个人身份的信息,即使这些信息出现在公开广告上。将你的请求量控制在不会给 Gumtree 服务器造成压力的范围内,永远不要抓取任何需要登录的内容,或者那些用于屏蔽卖家详情的联系显示步骤后面的内容。

本指南刻意将范围限定在公开搜索列表字段,因为这是保持工作合理性的边界。它不涵盖账号或消息数据、卖家的个人联系方式,或任何试图绕过你无权通过的身份验证或 CAPTCHA 的操作。如果你的项目需要的不仅仅是公开列表数据,正确的路径是获得许可或数据协议,而不是更聪明的爬虫。在网站提供官方 API 或数据源的情况下,优先使用它来进行授权或批量访问。

回顾

核心要点

  • Gumtree 搜索页面是公开的分类广告数据源。每张卡片包含标题、价格、地点和广告链接,这正是其数据对价格比较和区域需求研究有价值的原因。
  • 你需要同时具备渲染能力和可信 IP。Gumtree 在客户端填充结果网格并挑战机器人流量,因此 Crawling API 在单次调用中于住宅 IP 后面渲染页面。
  • BeautifulSoup 完成提取工作。遍历 article[data-q="search-result"] 卡片,从各自的 data-* 属性映射标题、价格、地点和链接,并预期这些属性会发生变化。
  • 分页是一个查询参数。追加 &page=N 遍历多个页面,当某页没有卡片时中断,并用短暂延迟控制请求节奏。
  • 坚守公开列表数据。遵守 Gumtree 的服务条款和 robots.txt,永远不要收集私人卖家的个人联系方式或任何需要登录的内容。

常见问题

为什么普通请求从 Gumtree 返回没有列表的页面?

两个原因。Gumtree 在页面加载时在客户端填充大部分结果网格,因此原始的 requests.get 通常只得到一个缺少列表卡片的外壳。此外,Gumtree 会挑战或拦截不像真实浏览器的流量。通过 Crawling API 在可信 IP 后面渲染页面可以解决这两个问题,这正是本文的爬虫通过它路由请求的原因。

我可以从 Gumtree 搜索页面抓取哪些数据?

从每个结果卡片,你可以读取列表显示的公开字段:标题、要价、地点以及完整广告的链接。本教程恰好提取这四个字段。跟随链接到广告页面可以获取更完整的字段,如描述、发布日期和图片 URL,但收集内容应限于公开列表内容,避免私人卖家的个人联系方式。

如何在 Gumtree 上处理分页?

Gumtree 使用 ?page=N 查询参数对搜索结果进行分页,因此你在搜索 URL 后追加 &page=2&page=3 等并获取每一页。本指南中的 scrape_pages 函数遍历一个页码范围,当某页没有卡片时提前停止,并在请求之间加入短暂延迟,避免对网站造成过大压力。

爬虫使用哪些选择器?

每个结果是一个 article[data-q="search-result"]。在其内部,标题是 div[data-q="tile-title"],价格是 div[data-testid="price"],地点是 div[data-q="tile-location"],广告链接是 a[data-q="search-result-anchor"] 上的 href。这些 data-* 属性比生成的类名更耐久,但如果某个字段开始返回空值,请重新检查实时页面。

我可以将数据导出到电子表格吗?

可以。export 函数写入 JSON 文件和带有 title,price,location,link 标题的 CSV 文件,每条列表一行。直接在 Excel、Google Sheets 或任何电子表格工具中打开 CSV,或将 JSON 加载到 pandas DataFrame 或笔记本中进行分析和图表制作。

如何避免在抓取 Gumtree 时被封锁?

保持低频率的每 IP 请求,在页面和搜索之间加入延迟,并通过轮换住宅 IP 路由,这样没有单个地址会触发速率限制。Crawling API 为你管理轮换、可信 IP 池和 CAPTCHA 处理;如果你自建方案,这是需要投入的部分。监控状态码,当你开始看到挑战时降速。

开始构建

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

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

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