公开房源是网络上最有价值的数据之一。价格、卧室数量、卫生间数量、建筑面积和地址就在每个结果页面上,追踪这些数字随时间的变化,能告诉你哪个市场正在升温、哪里的租金在走软,以及哪些房源定价偏差。问题在于,单次快照几乎没有什么价值。房产数据只有在新鲜时才有价值,这意味着你必须一遍又一遍地按计划采集,而无需坐在终端前盯着每一次运行。

本指南将向你展示如何使用 JavaScript 和 Node.js 自动化房产数据提取。你将构建一个可运行的工作流,通过 Crawling API 获取公开房源列表,解析每个房源的价格、卧室数、卫生间数、建筑面积、地址和链接,然后以三种方式实现自动化:定期 cron 运行(用于稳定采集)、带 webhook 的异步 Crawler(用于高并发)以及结果存储。如果你只需要对单个站点进行一次抓取,下面链接的各站点专属指南更适合你。本文的重点是让任务反复运行。这里的所有内容都限定在公开房源数据范围内,文末的合规性章节并非样板文字,请在对真实流量使用之前仔细阅读。

你将构建什么

一个 Node.js 工作流,接受一个公开的房源搜索 URL,通过 Crawling API 获取渲染后的 HTML,为页面上的每个房源提取一条结构化记录,并按计划运行。我们为每个房源提取以下字段:

  • 价格 卡片上显示的标价,如"$2,400/mo"或"$525,000"。
  • 卧室数 卧室数量。
  • 卫生间数 卫生间数量。
  • 建筑面积 以平方英尺为单位的建筑面积。
  • 地址 房源上显示的街道地址。
  • 链接 指向单个房源页面的 URL。

在解析器之上,你将接入三层自动化:定时运行、异步高并发路径,以及一个带时间戳的 JSON 存储,让你可以对比前后两次采集的差异。

为什么普通请求在房产网站上行不通

如果你用普通 HTTP 客户端请求房源搜索 URL,很少能拿到房源列表。有两件事对你不利。首先,大多数现代房产门户使用 JavaScript 在浏览器中渲染搜索结果,因此初始 HTML 在页面脚本运行之前几乎是空壳。其次,这些网站会主动标记自动化流量:来自数据中心的 IP 以及不像真实浏览器的请求模式,在到达渲染后的房源列表之前就会被 CAPTCHA 拦截、限速或封锁。

因此,一个可用的房产爬虫需要在一次请求中同时具备两样东西:一个真正渲染页面的浏览器,以及一个被平台识别为真实访客的 IP 地址。你可以自己组合一个无头浏览器加上一个轮换住宅代理池,但将它们拼接在一起并持续维护是大部分的工作量,一旦按计划运行且请求量增加,情况会更糟。Crawling API 将两者合并到一次调用中:你发送 URL,它在可信 IP 后面渲染页面,并返回供你用 cheerio 解析的完整 HTML。

渲染预算

JavaScript 较多的房源页面需要 JavaScript token 才能让 API 在返回 HTML 之前运行真实浏览器。Crawlbase 提供 1,000 次免费请求,你只为成功的请求付费,普通请求和 JavaScript 请求消耗的积分不同。先在免费套餐上确认页面能正确渲染,再扩大规模。

前置条件

在编写任何代码之前,你需要准备几样东西,都不需要太长时间。

基础 JavaScript 和 Node.js 知识。 你应该能够编写和运行 Node 脚本,并使用 npm 安装包。能读懂函数就够了。

Node.js 16 或更高版本。node --version 确认你的版本。如果没有,请从 Node.js 官网安装,或通过 nvm 等版本管理工具安装。

Crawlbase 账户和 token。 注册账户,打开控制台,复制你的 token。免费套餐提供 1,000 次请求,无需信用卡。将 token 视为密码:它用于验证你的请求,请勿提交到版本控制系统。

设置项目

创建项目文件夹,初始化它,并安装工作流所需的库。

bash
node --version

mkdir real-estate-automation && cd real-estate-automation
npm init -y

npm install crawlbase cheerio node-cron express

四个依赖各司其职:crawlbase 是 Crawling API 和异步 Crawler 的官方 Node 客户端,cheerio 用类 jQuery 的 API 解析返回的 HTML,让你可以通过 CSS 选择器提取字段,node-cron 按计划运行抓取任务,express 接收异步 Crawler 回传的 webhook。在此文件夹中创建一个名为 scraper.js 的文件,并将以下步骤中的代码添加进去。

第一步:获取渲染后的房源页面

首先获取完整页面。导入 CrawlingAPI 类,用你的 token 初始化它,然后请求一个公开的搜索 URL。因为页面是 JavaScript 渲染的,传入 { ajax_wait: true, page_wait: 3000 } 让 API 等待房源卡片加载完成再返回。在解析之前检查状态码,可以让失败信息清晰可见而不是悄无声息。

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

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

const listingsURL = 'https://www.example-realty.com/homes-for-rent/ca/los-angeles';

api
  .get(listingsURL, { ajax_wait: true, page_wait: 3000 })
  .then((response) => {
    if (response.statusCode === 200) {
      console.log(response.body.slice(0, 500));
    }
  })
  .catch((error) => console.error('API request error:', error));

node scraper.js 运行脚本,你应该能在 body 顶部看到真实的房源标记,而不是一个空壳。这在编写任何选择器之前就确认了渲染是否正常工作。将 listingsURL 替换为你想要追踪的任意公开搜索页面:按城市、街区或价格区间筛选,在浏览器中进行搜索后显示的 URL 就是你要抓取的 URL。

Crawlbase Crawling API

第一次请求刚刚返回了一个完整渲染的房源页面,你无需在自己这一侧配置无头浏览器或代理。Crawling API 在真实浏览器中运行页面,在服务端轮换住宅 IP,并处理房产门户对爬虫抛出的 CAPTCHA,因此你从一次调用中获得完整的 HTML,而且当 cron 任务每天早晨触发时,同一次调用同样可靠。先在免费套餐上对一个公开搜索进行测试。

第二步:用 cheerio 解析每个房源

拿到渲染后的 HTML,将其加载到 cheerio 中并遍历房源卡片。结果页面以重复容器的形式排列每个房源,因此你选择所有卡片,再从每个卡片内部读取价格、卧室数、卫生间数、建筑面积、地址和链接。下面的确切选择器来自典型的卡片布局;你需要通过在浏览器开发者工具中检查一个卡片来调整它们以匹配你的目标站点。以防御性方式读取每个字段,防止单个缺失值导致整个运行崩溃,将价格解析为数字则让你可以对其进行排序和比较。

javascript
const cheerio = require('cheerio');

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

  $('li[data-testid="listing-card"]').each((i, el) => {
    const card = $(el);

    const price = card.find('span.listing-card-price').text().trim();
    const priceValue = parseFloat(price.replace(/[^0-9.]/g, ''));

    const beds = card.find('p:contains("Beds") strong').first().text().trim();
    const baths = card.find('p:contains("Baths") strong').first().text().trim();
    const sqft = card.find('p:contains("Sq Ft") strong').first().text().trim();

    const address = card.find('a.listing-card-address').text().trim();
    const href = card.find('a.listing-card-address').attr('href');
    const link = href
      ? new URL(href, 'https://www.example-realty.com').href
      : '';

    if (price && address) {
      properties.push({ price, priceValue, beds, baths, sqft, address, link });
    }
  });

  return properties;
}

有几个细节让这段代码忠实于页面。每个卡片位于重复的 li 容器内,价格来自价格 span 并同时被解析为数值型的 priceValue 以便按从低到高排序,卧室数、卫生间数和建筑面积通过带有 :contains() 选择器的标签块读取,在小幅重排后仍然有效。地址锚点同时也是房源链接,因此一次 find 就能得到两者,并将 href 解析为绝对 URL 使其在页面外部也可用。只有同时包含价格和地址的卡片才会被推入结果,这样可以过滤掉房产网站在结果网格中插入的促销磁贴。

选择器会发生变化

房源卡片的类名和 data-testid 值是生成的,可能在不通知的情况下发生变化。将上面的选择器视为起始模板而非固定约定。当某个字段返回为空时,重新在浏览器开发者工具中检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作,并不意味着出了什么问题。

第三步:组装抓取流程并存储结果

现在将获取和解析步骤串联成一个返回干净记录的函数,然后将每批结果写入磁盘并附带时间戳。将每次运行保存在各自带时间戳的文件中,是让你能对比前后两次采集、观察价格变化的关键。

javascript
const fs = require('fs');

async function scrape(url) {
  const response = await api.get(url, { ajax_wait: true, page_wait: 3000 });
  if (response.statusCode !== 200) {
    console.error(`Request failed: ${response.statusCode}`);
    return [];
  }
  return parseListings(response.body);
}

function save(properties) {
  properties.sort((a, b) => a.priceValue - b.priceValue);
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const file = `listings_${timestamp}.json`;
  fs.writeFileSync(file, JSON.stringify(properties, null, 2));
  console.log(`Saved ${properties.length} properties to ${file}`);
  return file;
}

async function runOnce() {
  const url = 'https://www.example-realty.com/homes-for-rent/ca/los-angeles';
  const properties = await scrape(url);
  if (properties.length) save(properties);
}

module.exports = { scrape, save, runOnce, parseListings };

将第二步的 parseListings 函数和第一步的 API 初始化代码粘贴到同一个文件中,这样 scrape 就能调用它们。运行 node -e "require('./scraper').runOnce()",你将得到一个排序后的、带时间戳的 JSON 文件,包含页面上每个公开房源的数据。这就是下面的自动化层会定期调度和重复执行的工作单元。

通过定时任务实现自动化

一次性抓取只能捕捉某一个时刻。房产数据只有在实时时才有价值,因此第一层自动化是定期运行。使用 node-cron,你可以保持进程活跃,并按 cron 表达式触发 runOnce。下面的示例在每天早上 7 点运行。

javascript
const cron = require('node-cron');
const { runOnce } = require('./scraper');

// Minute Hour DayOfMonth Month DayOfWeek
cron.schedule('0 7 * * *', async () => {
  console.log(`Scheduled run at ${new Date().toISOString()}`);
  try {
    await runOnce();
  } catch (error) {
    console.error('Scheduled run failed:', error.message);
  }
});

console.log('Scheduler started. Waiting for the next run...');

node schedule.js 启动它,并让它在小型服务器或容器上持续运行。每天早晨它都会抓取搜索页面并生成一个新的带时间戳的文件,逐渐积累一段历史记录,让你可以对比价格变化、新上架的房源以及已从市场下架的房源。如果你不想让进程一直保持活跃,同一个 runOnce 调用也可以从系统 cron 条目或任何任务调度器中运行;node-cron 只是进程内的选项。这个模式与 如何自动化 Amazon 抓取 指南中的模式完全相同,计划任务将单次抓取转变为追踪流水线。

通过异步 Crawler 和 webhook 实现规模扩展

对于少量几个搜索来说,定时循环就足够了。一旦你要追踪数十个城市或数千个房源页面,依次等待每个同步请求就会变慢,而长时运行的进程也是一个脆弱的地方来承载如此大量的工作。异步 Crawler 正是为此而生:你将 URL 推送给它,Crawlbase 在自己的基础设施上获取和渲染它们,并将每个完成的页面回传到你托管的 webhook。你的代码不再等待请求,只需在结果到达时处理它们。

首先,建立一个接收回调的小端点。Crawler 将渲染后的 HTML 推送给它,因此你直接在处理程序中进行解析和存储。

javascript
const express = require('express');
const { parseListings, save } = require('./scraper');

const app = express();
app.use(express.text({ type: '*/*', limit: '10mb' }));

app.post('/crawlbase-webhook', (req, res) => {
  const html = req.body;
  const properties = parseListings(html);
  if (properties.length) save(properties);
  res.sendStatus(200);
});

app.listen(3000, () => console.log('Webhook listening on :3000'));

然后将你的搜索 URL 推送到 Crawler,并将 webhook 指定为回调。Crawler 将每个 URL 排入队列,渲染它,并将结果发送到你的端点,因此你可以提交一大批并让响应流式返回。

javascript
const { Crawler } = require('crawlbase');

const crawler = new Crawler({ token: 'YOUR_CRAWLBASE_TOKEN' });

const searches = [
  'https://www.example-realty.com/homes-for-rent/ca/los-angeles',
  'https://www.example-realty.com/homes-for-rent/ca/san-diego',
  'https://www.example-realty.com/homes-for-rent/ca/san-francisco',
];

for (const url of searches) {
  crawler.post(
    url,
    { callback: 'true', callback_url: 'https://your-server.com/crawlbase-webhook' },
    { ajax_wait: true, page_wait: 3000 }
  );
}

这种解耦正是关键所在。Crawler 在 Crawlbase 这一侧承担缓慢、阻塞性的部分(渲染和重试),你的 webhook 只需执行快速的解析和存储步骤。这种解耦使同一工作流可以从三个搜索扩展到三千个,而你的进程不需要保持每个请求处于打开状态。在开发过程中,你的 webhook 需要一个公开 URL;隧道工具可以将 localhost:3000 暴露给 Crawler。

输出示例

无论记录来自定时运行还是异步 webhook,每批结果的格式都是相同的:每个房源一个对象,按价格从低到高排序,包含价格、卧室数、卫生间数、建筑面积、地址和链接。

json
[
  {
    "price": "$2,400/mo",
    "priceValue": 2400,
    "beds": "2",
    "baths": "1",
    "sqft": "850",
    "address": "1234 Sunset Blvd, Los Angeles, CA 90026",
    "link": "https://www.example-realty.com/property/1234-sunset-blvd"
  },
  {
    "price": "$3,150/mo",
    "priceValue": 3150,
    "beds": "3",
    "baths": "2",
    "sqft": "1,320",
    "address": "88 Maple Ave, Los Angeles, CA 90042",
    "link": "https://www.example-realty.com/property/88-maple-ave"
  }
]

由于每次运行都保存在各自带时间戳的文件中,对比两批数据只需对 link 字段做集合差运算(找出新增和已下架房源),以及对 link 字段做连接并比较 priceValue(找出价格变化)。这种差异对比正是自动化的全部意义:单次抓取告诉你今天的市场,定期历史记录告诉你市场走向。如果你想将相同的记录存入电子表格,传统版本的工作流直接写入 Excel(使用 ExcelJS),在 JSON 存储基础上增加这个导出功能只需几行代码。

在大规模运行中保持不被封锁

即使渲染已经处理好,房产门户仍然会监控爬虫式的流量,而每天触发一次的计划任务使规律更容易被发现。一些好习惯可以保持任务的健康运行。

  • 控制请求频率。 分散请求,而不是在紧密循环中猛烈请求页面。当你抓取多个搜索时,在它们之间添加延迟,或者依靠异步 Crawler,它会为你排队和控制节奏。
  • 依靠 IP 轮换。 住宅 IP 池将请求分散到许多真实用户的地址上,使单个地址不会触发速率限制或 CAPTCHA。Crawling API 和异步 Crawler 为你处理这些;如果你自己搭建技术栈,这是需要做好的部分。
  • 关注状态码。 一个开始返回拦截页面或非 200 响应的任务在告诉你当前的频率或 IP 层级已经不够用了。将其视为退出信号,而不是要忽略的噪声。

更完整的方案请参阅 如何在不被封锁的情况下抓取网站。如果你需要特定站点的详细说明而非本文的自动化重点,关于 如何抓取 Zillow如何抓取 Redfin 的专属指南涵盖了这些门户特定的卡片布局和分页方式。

抓取房产数据合法吗?

抓取某个房产网站是否被允许,取决于该网站的服务条款、你所在的司法管辖区以及你对数据的使用方式。大多数门户在其条款中限制了自动化访问,因此无论你的工具多么谨慎,抓取行为都可能与这些条款相冲突。这里的代码不会改变这一点,它只是让技术层面得以运作。请阅读网站的使用条款和 robots.txt,并将两者作为你采集内容和请求频率的边界。计划任务使频率纪律更加重要,而非更不重要。

只处理公开房源数据:任何人无需账户即可在公开结果页面上看到的价格、卧室数、卫生间数、建筑面积、地址和房源链接。不要采集经纪人、业主或买家的个人数据,超出公开商业房源已展示的范围,也不要基于这些数据构建个人档案。一旦涉及个人数据,GDPR 和 CCPA 就会适用,而附有个人姓名的公开街道地址可能符合条件,因此要偏向房产事实而非涉及人的信息。不要以自己的名义重新分发门户的受版权保护的媒体(如房源照片),也不要触碰任何需要登录的内容。

这个行业有一个特殊之处:最丰富的房产数据通常来自 MLS(多重上市服务),MLS 数据几乎都是授权的,而非可以自由获取的。如果你的项目需要全面、准确、可再分发的房源数据,正确的途径是授权数据源或官方 API,而非爬虫。几家大型门户正是出于这个原因运营合作伙伴计划或开发者 API。当你需要大规模、有保证的结构以及商业使用权时,请使用这些渠道。本指南刻意将范围限定在公开搜索页面上的公开房源,因为这是工作可辩护的边界。

回顾

核心要点

  • 房产数据只有在新鲜时才有价值。 单次抓取只是快照;按计划自动化运行将其转变为可对比价格变动和新上架房源的历史记录。
  • 先在可信 IP 后面渲染,再解析。 门户在客户端渲染房源列表且严格封锁,普通请求返回空壳或 CAPTCHA;Crawling API 在一次调用中渲染页面并轮换住宅 IP。
  • cheerio 负责提取字段。 选择每个房源卡片,然后读取价格、卧室数、卫生间数、建筑面积、地址和链接,将价格解析为数字以便排序和比较;预期生成的类名会发生变化。
  • 用异步 Crawler 和 webhook 实现规模扩展。 将 URL 推送到 Crawler,让它在 Crawlbase 这一侧渲染,并将完成的页面推送到你的端点,使工作流可以从三个搜索扩展到数千个而无需保持请求连接。
  • 只处理公开数据。 遵守各网站的服务条款和 robots.txt,只采集公开房产事实而非个人数据,对于全面或商业用途优先使用授权 MLS 数据源或官方 API。

常见问题

如何按计划自动化房产数据提取?

将抓取逻辑封装成一个函数,并从调度器中调用它。最简单的进程内选项是 node-cron:给它一个如 0 7 * * * 的 cron 表达式,它就会每天早晨触发你的 runOnce 函数。每次运行都会生成一个新的带时间戳的文件,逐渐积累你可以对比的历史记录。如果你不想让 Node 进程一直保持活跃,同一函数也可以从系统 cron 条目或任何任务调度器中运行。

何时应该使用异步 Crawler 而非 Crawling API?

当你只抓取少量搜索且希望在同一次调用中拿到结果时,使用同步 Crawling API。当你追踪数十个城市或数千个房源页面时,切换到异步 Crawler:你推送 URL,Crawlbase 在自己的基础设施上渲染,并将每个完成的页面推送到你托管的 webhook。这种解耦使你的进程不必依次等待每个缓慢的请求。

为什么普通请求从房产网站返回的数据不完整?

因为大多数门户使用 JavaScript 在客户端渲染房源列表,并以 CAPTCHA 拦截自动化流量。来自数据中心 IP 的原始 HTTP 请求通常返回空壳或拦截页面,而不是房源卡片。要获取完整页面,你必须在可信 IP 后面渲染它,这正是当你传入 JavaScript 选项时 Crawling API 为你处理的事情。

我可以从公开房源列表中提取哪些字段?

结果卡片上的公开事实:价格、卧室数、卫生间数、建筑面积、街道地址以及完整房源页面的链接。本指南正好解析了这些字段。请远离经纪人、业主或买家的个人数据,以及受版权保护的媒体(如房源照片),这两者都有公开房产事实所没有的法律和授权约束。

我的选择器返回空值,发生了什么?

几乎可以肯定是网站的标记发生了变化。房源卡片的类名和 data-testid 值是生成的,可能在不通知的情况下发生变化,因此上个月有效的选择器可能会失效,尤其是在无人值守运行的计划任务中。重新在浏览器开发者工具中检查实时卡片并更新选择器。定期维护选择器是任何生产爬虫的正常工作。

使用 MLS 数据源更好,还是自己抓取更好?

对于全面、准确、可再分发的房源数据,授权 MLS 数据源或官方门户 API 是正确的工具,因为 MLS 数据几乎都是授权的而非可以自由获取的。抓取公开搜索页面适合追踪适量的公开房源事实、研究和价格变动分析,但需限定在公开数据范围内且符合各网站条款。根据用途匹配数据源:公开事实和适量请求倾向使用爬虫,全面或商业用途倾向使用授权数据源。

开始构建

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

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

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