自 20 世纪 90 年代末以来,Craigslist 一直是美国最大的分类信息平台,至今仍在房屋、二手物品、服务和社区等类别下承载着数百万条公开帖子。这些列表页是一个有用的公开市场信号来源:各社区租金如何变化、都市区里二手货卖什么价、哪里供给紧张。每条搜索结果都以可预测的、服务端渲染的布局呈现在页面上,这让公开字段的读取变得简单直接。

本指南将向你展示如何用 JavaScript 和 Node.js 配合 Cheerio 来抓取 Craigslist。你会构建一个小巧、可运行的抓取脚本,它通过 Crawling API 获取一个公开的 Craigslist 搜索结果页,解析出每条列表的标题、价格、地点、发布日期和链接,并把结果导出为 JSON 和 CSV。整个演示始终限定在公开的、非个人的列表数据范围内。卖家联系方式以及个人帖子里的自由文本内容属于个人数据,文末附近的合法性章节解释了为什么本抓取脚本刻意将它们排除在外,所以在把它指向任何真实规模的目标之前请先读完那一节。

你将构建什么

一个 Node.js 脚本,它接收一个公开的 Craigslist 搜索 URL,通过 Crawling API 取回 HTML,并为结果页上的每条列表提取一条结构化记录。我们以一次房地产搜索作为贯穿示例,逐条帖子提取以下字段:

  • 标题 结果卡片上显示的列表标题,例如“2 bedroom trailer for rent”。
  • 价格 按显示形式呈现的要价,比如“$675”,保留为文本,因为 Craigslist 的价格带有货币符号和逗号。
  • 地点 Craigslist 在列表旁显示的社区或区域提示。
  • 发布日期 列表的发布日期,当结果卡片展示它时。
  • 链接 指向单条列表页面的绝对 URL。

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

向某个 Craigslist 搜索 URL 发出一个裸 HTTP 请求,单次命中或许能成功,但一旦你以任何规模进行抓取,它就撑不住了。Craigslist 会监视自动化流量并对其加以防范:数据中心 IP 以及看起来不像真实浏览器的请求模式会被限速、收到 CAPTCHA,或被直接封禁。从单一地址跑一个紧凑的循环,你很快就会看到非 200 响应和验证页面,而不是列表。

因此,一个耐用的 Craigslist 抓取器需要一个被平台视为真实访客的 IP,并且需要表现得礼貌。你可以用一池轮换的住宅代理自己搭建这一切,但让这个代理池保持健康且未被封锁才是大部分工作量所在。Crawling API 把它收进单次调用:你把 URL 发给它,它在受信任的 IP 背后获取页面,内置 CAPTCHA 处理,并把 HTML 返回给你用 Cheerio 解析。

仅限公开数据

本指南中的一切都只读取任何访客都能在公开搜索结果页上看到的字段:标题、价格、地点提示、发布日期和链接。它不会打开单条帖子去采集卖家的电话号码或邮箱,也不会构建任何发帖者的画像。这一边界是有意为之,下方的合法性章节会加以解释。

前置条件

在写任何代码之前,你需要准备好几样东西。它们都不会花太久。

基本的 JavaScript 和 Node.js。 你应当能自如地编写并运行 Node 脚本,并用 npm 安装包。如果你刚接触 Node,官方文档或任何入门课程都能把你带到本教程所假定的水平。想要更完整的演示,可以参阅我们关于用 Node.js 构建网页抓取器的指南,其中涵盖了基础知识。

Node.js 16 或更高版本。node --version 确认你的版本。如果没有,请从 Node.js 官网或像 nvm 这样的版本管理器安装它。

一个 Crawlbase 账户和 token。 注册、打开你的仪表板,并从账户文档页复制你的 token。免费套餐为你提供 1,000 次请求,无需绑卡,而且你只为成功的请求付费。把 token 当作密码对待:它用于验证你的请求身份,所以请别把它放进版本控制里。

搭建项目

创建一个项目文件夹,初始化它,并安装抓取器所需的两个库。

bash
node --version

mkdir craigslist-scraper && cd craigslist-scraper
npm init -y

npm install crawlbase cheerio

两个依赖完成全部工作:crawlbase 是 Crawling API 的官方 Node 客户端,而 cheerio 用一套 jQuery 风格的 API 解析返回的 HTML,让你能按 CSS 选择器提取各个字段。本教程的原始版本用 jsdom 来解析保存下来的 HTML;Cheerio 以更轻量、更快的 API 完成同样的工作,更适合抓取管线。在这个文件夹里创建一个名为 scraper.js 的文件,并加入下文各步骤中的代码。

第 1 步:获取搜索结果页

先取到页面 HTML。导入 CrawlingAPI 类,用你的 token 初始化它,并请求一个公开的 Craigslist 搜索 URL。挑选一个你想抓取的搜索列表页,例如带图库视图的待售房地产搜索,并在解析之前检查状态码,这样失败会响亮地暴露出来,而不是悄无声息。

javascript
const { CrawlingAPI } = require('crawlbase');
const fs = require('fs');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

const craigslistPageURL =
  'https://chicago.craigslist.org/search/rea?hasPic=1';

api
  .get(craigslistPageURL)
  .then((response) => {
    if (response.statusCode === 200) {
      fs.writeFileSync('response.html', response.body);
      console.log('HTML saved to response.html');
    } else {
      console.error(`Request failed: ${response.statusCode}`);
    }
  })
  .catch((error) => console.error('API request error:', error));

node scraper.js 运行脚本。成功时它会把页面写入 response.html,这让你能检查标记,并针对一份稳定副本开发选择器,而不必在每次改动时都去访问网络。Crawling API 在受信任的 IP 背后获取页面,所以你拿回的 HTML 里有真实的列表,而不是一个拦截页。

Crawlbase Crawling API

那第一个请求刚刚返回了一个真实的 Craigslist 结果页,而你这边无需代理池或 CAPTCHA 求解。Crawling API 在服务端通过轮换的住宅 IP 获取页面,并处理 Craigslist 抛给抓取器的各种挑战,所以你一次调用就能得到可用的 HTML。先在免费套餐上把它指向一个公开搜索,然后再加上你的解析器。

第 2 步:用 Cheerio 解析每条列表

手里有了保存下来的 HTML,就把它加载进 Cheerio 并遍历列表。Craigslist 把它的静态搜索结果渲染在一个 ol.cl-static-search-results 列表里,每条帖子在它自己的 li.cl-static-search-result 条目中,所以你选取每个条目,并从其内部读取标题、价格、地点、发布日期和链接。以防御性的方式读取每个字段,能避免一个缺失值就让整次运行崩溃。

javascript
const cheerio = require('cheerio');

function parseListings(html) {
  const $ = cheerio.load(html);
  const listings = [];

  $('ol.cl-static-search-results li.cl-static-search-result').each((_, el) => {
    const item = $(el);

    const title = item.find('.title').text().trim();
    const price = item.find('.price').text().trim();
    const location = item.find('.location').text().trim();
    const postDate = item.find('.meta time').attr('datetime') || '';
    const link = item.find('a').attr('href') || '';

    if (title) {
      listings.push({
        title,
        price: price || 'N/A',
        location: location || 'N/A',
        postDate,
        url: link,
      });
    }
  });

  return listings;
}

这些选择器直接对应到页面上。每条列表的标题来自 .title,要价来自 .price,社区提示来自 .location,链接来自条目锚点的 href。发布日期是从列表 .meta 行里 time 元素的 datetime 属性读取的,这给你一个干净的机器可读日期,而不是相对文本。价格刻意保留为字符串,因为 Craigslist 的值包含货币符号和千位分隔符;如果你的分析需要,稍后再把它转换为数字。

选择器会漂移

Craigslist 会不时调整它的标记,而各个城市子域之间也可能略有差异。把这些选择器当作一个起始模板,而非一份契约。当某个字段返回为空时,打开 response.html 或在浏览器开发者工具里打开实时页面,并更新选择器。对任何生产级抓取器来说,定期的选择器维护都属正常,不是出了什么问题的迹象。

第 3 步:组装完整脚本,并导出 JSON 和 CSV

现在把抓取和解析接成一个可运行的脚本,然后把记录以 JSON 和 CSV 两种形式写入磁盘。

javascript
const fs = require('fs');
const { CrawlingAPI } = require('crawlbase');
const cheerio = require('cheerio');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

async function crawl(pageUrl) {
  const response = await api.get(pageUrl);
  if (response.statusCode === 200) return response.body;
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

function toCsv(rows) {
  const headers = ['title', 'price', 'location', 'postDate', 'url'];
  const escape = (value) =>
    `"${String(value).replace(/"/g, '""')}"`;
  const lines = [headers.join(',')];
  for (const row of rows) {
    lines.push(headers.map((h) => escape(row[h])).join(','));
  }
  return lines.join('\n');
}

async function main() {
  const url = 'https://chicago.craigslist.org/search/rea?hasPic=1';
  const html = await crawl(url);
  if (!html) return;

  const listings = parseListings(html);
  fs.writeFileSync('listings.json', JSON.stringify(listings, null, 2));
  fs.writeFileSync('listings.csv', toCsv(listings));
  console.log(`Saved ${listings.length} listings to JSON and CSV`);
}

main();

把第 2 步里的 parseListings 函数粘贴到同一个文件中,这样 main 才能调用它。用 node scraper.js 运行它,你会得到两个文件:包含完整结构化记录的 listings.json,以及可直接在电子表格中打开的 listings.csvtoCsv 辅助函数会为每个字段加引号并对内嵌的引号进行加倍处理,这在这里很重要,因为列表标题里经常包含逗号。

输出长什么样

JSON 文件每条列表保存一个对象,每个对象都带有标题、价格、地点、发布日期和链接。下面的值仅供示意,取自一次房地产搜索。

json
[
  {
    "title": "2 bedroom trailer for rent",
    "price": "$675",
    "location": "165th & Kennedy",
    "postDate": "2024-04-05 09:12",
    "url": "https://chicago.craigslist.org/nwi/reo/d/hammond-bedroom-trailer-for-rent/7732856568.html"
  },
  {
    "title": "Barrington Village Home",
    "price": "$439,000",
    "location": "northwest suburbs",
    "postDate": "2024-04-04 16:48",
    "url": "https://chicago.craigslist.org/nwc/reo/d/barrington-barrington-village-home/7734168844.html"
  }
]

CSV 以一行表头镜像同样的行,所以它能直接放进 Excel、Google Sheets,或任何读取分隔文件的数据管线。

csv
title,price,location,postDate,url
"2 bedroom trailer for rent","$675","165th & Kennedy","2024-04-05 09:12","https://chicago.craigslist.org/nwi/reo/d/hammond-bedroom-trailer-for-rent/7732856568.html"
"Barrington Village Home","$439,000","northwest suburbs","2024-04-04 16:48","https://chicago.craigslist.org/nwc/reo/d/barrington-barrington-village-home/7734168844.html"

处理分页

一个搜索页只是演示;真正的任务会拉取每一页结果。Craigslist 用一个数字偏移量来给它的搜索 URL 分页,每次推进 120 条结果,所以你可以遍历各个偏移量,通过 Crawling API 获取每一页,用同一个函数解析它,并在某一页返回零条列表时停止。因为每个结果页都共享相同的条目结构,你已经写好的解析器无需改动就能在所有页面上运行。

javascript
async function scrapeAllPages(baseUrl, maxPages) {
  const all = [];

  for (let page = 0; page < maxPages; page++) {
    // Craigslist pages search results in steps of 120
    const offset = page * 120;
    const pageUrl = `${baseUrl}&s=${offset}`;
    const html = await crawl(pageUrl);
    if (!html) break;

    const listings = parseListings(html);
    if (listings.length === 0) break; // no more results

    all.push(...listings);
    console.log(`Page ${page + 1}: ${listings.length} listings`);

    // Pace requests so you stay under the rate limit
    await new Promise((r) => setTimeout(r, 2000));
  }

  return all;
}

确切的分页参数可能会变,所以在你的浏览器里查看几个真实的“下一页”链接并匹配其模式。重要的习惯对任何目标都适用:循环直到结果耗尽,并在请求之间放一个短暂延迟,这样你就不会猛攻该站点。关于这类工作的更多内容,参阅我们关于爬取 JavaScript 网站的指南;如果你在长期追踪价格,可以看看我们关于将网页抓取用于价格情报的笔记。

保持不被封锁

Craigslist 会对抓取器加以防范,所以几个习惯能让一次运行保持健康。它们适用于任何难啃的目标。

  • 给你的请求设定节奏。 在各页获取之间引入延迟,而不是在一个紧凑的循环里猛攻搜索。把请求分散开来,是保持在 Craigslist 限速之下的最大单一因素。
  • 依靠轮换。 一池住宅 IP 会把请求分散到许多真实用户地址上,这样就没有任何单个地址会触发限制或 CAPTCHA。Crawling API 替你处理这件事;如果你自己搭建技术栈,这就是要做好的那一部分。
  • 读懂状态码。 一次运行开始返回验证或非 200 响应,就是在告诉你当前的速率或 IP 等级已经不够了。把它当作后退的信号,而不是可忽略的噪声。

关于更宽泛的策略手册,参阅如何在不被封锁的情况下抓取网站。如果你想从其他分类信息和租房站点获取类似的列表数据,同样的“先抓取再解析”模式也可迁移到抓取 Apartments.com

抓取 Craigslist 合法吗?

抓取 Craigslist 是否被允许,取决于 Craigslist 的服务条款、你所在的司法辖区,以及你拿数据做什么。这件事在 Craigslist 上比在大多数站点上更要紧:Craigslist 积极防范自动化访问,并有长期在法庭上追究抓取者的记录。它的使用条款禁止自动化采集,所以无论你的工具有多谨慎,抓取都可能违背这些条款。这里的任何代码都改变不了这一点;它只是把技术部分做通而已。请阅读 Craigslist 的使用条款及其 robots.txt,尊重它们所隐含的速率限制,并把两者都当作你采集内容的边界。

本指南刻意限定在公开的、非个人的列表数据:任何人不登录就能在搜索结果页上看到的标题、价格、地点提示、发布日期和链接。这与平台上的个人数据不同。卖家的姓名、电话号码、邮箱,或他们在帖子里写下的自由文本内容都属于个人数据。不要采集卖家联系方式,不要拼凑发帖者的画像,也不要转载与某个可识别个人相关联的帖子。一旦某个项目触及可识别的个人,像 GDPR 和 CCPA 这样的隐私法就会适用,而那已明确超出本文范围。像“这个社区的两居室租金大致在 X 附近”这样的聚合事实是可以的;而一份列出谁在卖什么、附带其联系方式的清单则不行。

Craigslist 没有发布通用的公开 API,尽管某些类别会暴露 RSS 源以供有限的、经认可的访问。在存在源或明确数据协议的地方,优先选择它:经认可的途径带来清晰的使用条款,而不是抓取一个奋力抵抗的站点所伴随的法律与技术风险。当你不确定某种用途是否被允许时,去获取许可或数据协议,而不要把沉默当作同意;并让你采集内容的规模与范围都与一个正当的、非个人的研究目的相称。

回顾

核心要点

  • Craigslist 会对抓取器加以防范。 来自数据中心 IP 的紧凑循环会被限速、验证或封禁,所以要在受信任的、轮换的 IP 背后获取页面,并给你的请求设定节奏。
  • Crawling API 一次调用就完成了最难的部分。 它在住宅 IP 背后获取页面,并在服务端处理 CAPTCHA,返回供你用 Cheerio 解析的 HTML。
  • Cheerio 提取字段。ol.cl-static-search-results 里选取每个 li.cl-static-search-result,然后读取标题、价格、地点、发布日期和链接,并预期标记会随城市和时间而漂移。
  • 分页并导出。 遍历 Craigslist 的偏移量参数直到结果耗尽,给你的请求设定节奏,并把结构化记录同时写入 JSON 和 CSV。
  • 坚守公开的、非个人的数据。 只采集列表字段,绝不采集卖家联系信息或与某个人相关联的帖子正文,尊重 ToS 和 robots.txt,并记住一旦涉及个人数据,GDPR 和 CCPA 就会适用。

常见问题

Craigslist 有官方 API 吗?

Craigslist 没有提供用于访问其数据的通用公开 API。某些版块提供 RSS 源以供有限访问,但并没有一个全面的 API。在存在经认可的源或针对你所需内容的数据协议的地方,优先选用它而不是抓取,因为它带有清晰的、被许可的使用条款。

我可以用 JavaScript 以外的语言构建 Craigslist 抓取器吗?

可以。本指南使用 JavaScript 搭配 Cheerio,但同样的方法在任何语言里都行得通。Crawling API 为多种语言提供库和 SDK,所以你以相同的方式获取 HTML,再用你技术栈偏好的任何 HTML 解析器去解析它,比如 Python 里的 BeautifulSoup。选择器和字段保持不变;只有解析语法会变。

我的选择器返回了空值。是什么变了?

几乎可以肯定是 Craigslist 的标记,或者是各城市子域之间的差异。在你的浏览器开发者工具里打开保存下来的 response.html 或一个实时页面,确认列表容器仍是带有 li.cl-static-search-result 条目的 ol.cl-static-search-results,并更新 parseListings 里的内部选择器。对任何生产级抓取器来说,定期的选择器维护都属正常。

抓取 Craigslist 时我会被封吗?

有可能,尤其是在 Craigslist 上,如果你从一个地址发送太多请求且速度太快。Crawling API 通过轮换住宅 IP 并替你处理 CAPTCHA 来降低这种风险,但你仍应给你的请求设定节奏,在各页之间加上延迟,并留意状态码,以便在验证出现时后退。

我可以从帖子里抓取卖家的电话号码和联系方式吗?

不行,而且这个抓取器就是为了不去做这件事而构建的。卖家的姓名、电话号码、邮箱,以及他们写下的自由文本正文都属于个人数据。采集它们、构建发帖者画像,或转载与某个人相关联的帖子,会牵涉到像 GDPR 和 CCPA 这样的隐私法,并违背 Craigslist 的条款。把你的采集范围限定在这里所涵盖的公开的、非个人的列表字段。

Craigslist 数据有什么用?

公开的列表数据支撑市场研究和定价分析:追踪租金和二手货价格如何在各社区和都市区之间变动、发现供给缺口,以及随时间研究本地需求。其价值在于跨众多列表的聚合性、非个人的信号,而不在于任何单个发帖者的身份或联系方式。

开始构建

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

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

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