环境配置
在开始抓取数据之前,您需要在计算机上安装 Node.js。您可以从官方网站下载最新版本,并根据您的操作系统按照相关说明进行安装。
然后,为您的项目创建一个新目录,并在终端或命令提示符中切换到该目录。运行以下命令来初始化一个新的 Node.js 项目:
npm init -y
这将在项目目录中创建package.json文件,其中将存储有关项目及其依赖项的信息。
要安装 TypeScript,请运行以下命令:
npm install typescript -save-dev
TypeScript 是 JavaScript 的超集,它增加了可选的静态类型检查及其他功能。它适用于大型项目,并能帮助更早地发现错误。TypeScript 使用名为tsconfig.json的配置文件来存储编译器选项和其他设置。要在项目中创建此文件,请运行以下命令:
npx tsc -init
请确保将“outDir”的值设置为“dist”。这样,我们就能将 TypeScript 文件与编译后的文件分开。
现在,在项目中创建一个名为“src”的目录,并新建一个名为“index.ts”的文件。我们将把爬取代码放在这里。要执行 TypeScript 代码,必须先进行编译,因此为了确保不会遗漏这一步,我们可以使用自定义命令。
请打开“package.json”文件,并将“scripts”部分修改为如下内容:
"scripts": {
"test": "npx tsc && node dist/index.js"
}
这样,在执行脚本时,只需在终端中输入 "npm run test"即可。
最后但同样重要的是,请运行以下命令将 Puppeteer 添加到项目的依赖项中:
npm install puppeteer
Puppeteer 是一个 Node.js 库,它提供了一个用于控制无头 Chrome 浏览器的高级 API,可用于网页抓取和自动化任务。鉴于当今许多网站都包含动态生成的内容,若您希望确保数据的完整性,强烈建议使用该库。
数据选择
既然环境已经搭建完毕,我们可以开始探讨如何提取数据了。在本文中,我选择抓取德克萨斯州普莱诺市可供出租的单间公寓列表:https://www.realtor.com/apartments/Plano_TX/beds-studio。
我们将从页面上的每个列表中提取以下数据:
- 该网址;
- 价格;
- 洗澡的次数;
- 面积(以平方英尺为单位);
- 物理地址
您可以在下面的截图中看到所有这些信息:

数据提取
要提取所有这些数据,我们首先需要找到它们的位置。右键单击高亮显示的区域,然后选择“检查”以打开开发者工具并查看 HTML 文档。将鼠标光标悬停在文档上,即可轻松查看每个区域对应的具体位置:

在本教程中,我将使用 CSS 选择器,因为这是最直观的方法。如果您对这种方法还不熟悉,不妨先阅读这篇通俗易懂的指南。
在开始编写脚本之前,让我们先确认一下 Puppeteer 是否已正确安装:
import puppeteer from 'puppeteer';
async function scrapeRealtorData(realtor_url: string): Promise<void> {
// Launch Puppeteer
const browser = await puppeteer.launch({
headless: false,
args: ['--start-maximized'],
defaultViewport: null
})
const page = await browser.newPage()
// Navigate to the channel URL
await page.goto(realtor_url)
// Close the browser
await browser.close()
}
scrapeRealtorData("https://www.realtor.com/apartments/Plano_TX/beds-studio")
在此,我们将打开一个浏览器窗口,创建一个新页面,导航至目标 URL,然后关闭浏览器。为了简化操作并便于可视化调试,我以非无头模式全屏打开浏览器。
由于每个房源信息都具有相同的结构和数据,因此我们的算法将提取整个房源列表中的每一项信息。在脚本结束时,我们将遍历所有结果,并将它们整合到一个列表中。
您可能已经注意到,在第一张截图中,房源链接并未显示,但在第二张截图中却被提及并标出了。这是因为点击该链接后,系统会自动跳转至该房源的页面。
// Extract listings location
const listings_location = await page.evaluate(() => {
const locations = document.querySelectorAll('a[data-testid="card-link"]')
const locations_array = Array.from(locations)
return locations ? locations_array.map(a => a.getAttribute('href')) : []
})
console.log(listings_location)
我们通过筛选具有“data-testid”属性且其值为“card-link”的锚点元素来定位 URL。然后,我们将结果转换为 JavaScript 数组,并将每个元素映射到“href”属性的值。
不过,生成的列表中每个 URL 都会出现两次。这是因为每个房源条目在两个部分(房产图片和租赁详情)中都使用了相同的锚点元素。我们可以通过使用 Set 数据结构轻松解决这个问题:
const unique_listings_location = [...new Set(listings_location)]
console.log(unique_listings_location)
对于房产价格,我们将提取具有“data-testid”属性且该属性的值为“card-price”的“div”元素。这些元素还需要转换为数组,然后映射到其文本内容。
// Extract listings price
const listings_price = await page.evaluate(() => {
const prices = document.querySelectorAll('div[data-testid="card-price"]')
const prices_array = Array.from(prices)
return prices ? prices_array.map(p => p.textContent) : []
})
console.log(listings_price)
为了获取浴室的数量和房产面积,我们将使用针对直接子元素的运算符。这意味着父元素具有唯一的标识,而子元素则使用更通用的 ID 或类名。除此之外,逻辑与之前相同:
// Extract listings baths
const listings_baths = await page.evaluate(() => {
const baths = document.querySelectorAll('li[data-testid="property-meta-baths"] > span[data-testid="meta-value"]')
const baths_array = Array.from(baths)
return baths ? baths_array.map(b => b.textContent) : []
})
console.log(listings_baths)
// Extract listings sqft
const listings_sqft = await page.evaluate(() => {
const sqfts = document.querySelectorAll('li[data-testid="property-meta-sqft"] > span[data-testid="screen-reader-value"]')
const sqfts_array = Array.from(sqfts)
return sqfts ? sqfts_array.map(s => s.textContent) : []
})
console.log(listings_sqft)
最后,对于列表中的地址,我们选择那些“data-testid”属性值设置为“card-address”的“div”元素。
// Extract listings address
const listings_address = await page.evaluate(() => {
const addresses = document.querySelectorAll('div[data-testid="card-address"]')
const addresses_array = Array.from(addresses)
return addresses ? addresses_array.map(a => a.textContent) : []
})
console.log(listings_address)
现在你应该有 5 个列表,每个列表对应我们抓取的一条数据。正如我之前提到的,我们应该将它们合并为一个列表。这样一来,我们收集到的信息就更容易进行后续处理了。
// Group the lists
const listings = []
for (let i = 0; i < unique_listings_location.length; i++) {
listings.push({
url: unique_listings_location[i],
price: listings_price[i],
baths: listings_baths[i],
sqft: listings_sqft[i],
address: listings_address[i]
})
}
console.log(listings)
最终结果应该大致如下:
[
{
url: '/realestateandhomes-detail/1009-14th-St-Apt-410_Plano_TX_75074_M92713-98757',
price: '$1,349',
baths: '1',
sqft: '602 square feet',
address: '1009 14th St Apt 410Plano, TX 75074'
},
{
url: '/realestateandhomes-detail/1009-14th-St-Apt-1_Plano_TX_75074_M95483-11211',
price: '$1,616',
baths: '1',
sqft: '604 square feet',
address: '1009 14th St Apt 1Plano, TX 75074'
},
{
url: '/realestateandhomes-detail/1009-14th-St_Plano_TX_75074_M87662-45547',
price: '$1,605 - $2,565',
baths: '1 - 2',
sqft: '602 - 1,297 square feet',
address: '1009 14th StPlano, TX 75074'
},
{
url: '/realestateandhomes-detail/5765-Bozeman-Dr_Plano_TX_75024_M70427-45476',
price: '$1,262 - $2,345',
baths: '1 - 2',
sqft: '352 - 1,588 square feet',
address: '5765 Bozeman DrPlano, TX 75024'
},
{
url: '/realestateandhomes-detail/1410-K-Ave-Ste-1105A_Plano_TX_75074_M97140-46163',
price: '$1,250 - $1,995',
baths: '1 - 2',
sqft: '497 - 1,324 square feet',
address: '1410 K Ave Ste 1105APlano, TX 75074'
}
]避免被识别为机器人
虽然初看之下抓取Realtor网站似乎很简单,但随着项目规模的扩大,这一过程可能会变得更加复杂和具有挑战性。该房地产网站采用了多种技术来检测和阻止自动化流量,因此当你扩大抓取规模时,你的抓取工具就会开始被封锁。
该房产中介网站采用由PerimeterX提供的“按住不放”式验证码(CAPTCHA),这种验证码以几乎无法通过代码破解而闻名。此外,该网站还会收集多种浏览器数据,以此生成并为您分配一个唯一的指纹标识。
在收集到的浏览器数据中,我们发现
- Navigator 对象的属性(deviceMemory、hardwareConcurrency、languages、platform、userAgent、webdriver 等)。
- 时间和性能检查
- WebGL
- WebRTCIP 嗅探
- 以及更多
要克服这些挑战并继续进行大规模数据抓取,一种方法是使用数据抓取 API。此类服务提供了一种简单可靠的方式,可从 Realtor.com 等网站获取数据,而无需自行开发和维护数据抓取工具。
WebScrapingAPI 就是这样一款产品。它的代理旋转机制完全避免了验证码,其扩展知识库可以随机化浏览器数据,使其看起来像真实用户。
设置简单快捷。你只需注册一个账户,就会收到 API 密钥。您可以在仪表板上访问该密钥,它用于验证您发送的请求。

由于您已经设置了 Node.js 环境,我们可以使用相应的 SDK。运行以下命令将其添加到项目依赖项中:
npm install webscrapingapi
现在只需根据 API 调整之前的 CSS 选择器即可。提取规则的强大功能使我们可以在不做重大修改的情况下解析数据。
import webScrapingApiClient from 'webscrapingapi';
const client = new webScrapingApiClient("YOUR_API_KEY");
async function exampleUsage() {
const api_params = {
'render_js': 1,
'proxy_type': 'datacenter',
'timeout': 60000,
'extract_rules': JSON.stringify({
locations: {
selector: 'a[data-testid="card-link"]',
output: '@href',
all: '1'
},
prices: {
selector: 'div[data-testid="card-price"]',
output: 'text',
all: '1'
},
baths: {
selector: 'li[data-testid="property-meta-baths"] > span[data-testid="meta-value"]',
output: 'text',
all: '1'
},
sqfts: {
selector: 'li[data-testid="property-meta-sqft"] > span[data-testid="screen-reader-value"]',
output: 'text',
all: '1'
},
addresses: {
selector: 'div[data-testid="card-address"]',
output: 'text',
all: '1'
}
})
}
const URL = "https://www.realtor.com/apartments/Plano_TX/beds-studio"
const response = await client.get(URL, api_params)
if (response.success) {
const unique_listings_location = [...new Set(response.response.data.locations)]
// Group the lists
const listings = []
for (let i = 0; i < unique_listings_location.length; i++) {
listings.push({
url: unique_listings_location[i],
price: response.response.data.prices[i],
baths: response.response.data.baths[i],
sqft: response.response.data.sqfts[i],
address: response.response.data.addresses[i]
})
}
console.log(listings)
} else {
console.log(response.error.response.data)
}
}
exampleUsage();结论
在本教程中,我们详细介绍了如何使用 Node.js 和 Puppeteer 抓取 realtor.com 的数据。此外,我们还探讨了如何提高抓取工具的可靠性和效率,并解释了在某些使用场景下,为何选择专业的抓取服务可能是更好的选择。
Realtor.com 是一个广受欢迎且极具价值的房地产数据来源。凭借你在本教程中掌握的技能和知识,现在你应该能够利用网络爬虫技术提取这些数据,并将其应用到自己的项目中。
无论您是希望获得竞争优势的房地产从业者、寻求新机遇的投资者,还是正在寻找理想房源的购房者,网页抓取都能为您从realtor.com获取宝贵的洞察和数据。我们希望本教程对您有所帮助,并期待您能借助realtor.com的网页抓取功能,进一步提升您的房地产业务水平。




