Realtor.com 是美国最大的房产门户网站之一,其房源页面包含价格追踪、市场研究和投资分析所需的精确结构化数据:挂牌价格、卧室数量、浴室数量、建筑面积、街道地址以及每套房源的链接。这些公开数据是深度了解本地房地产市场的原始素材,但由于页面采用客户端渲染,且该网站对自动化流量有严密防护,普通 HTTP 请求只会返回一个空壳,而非你所需的房源列表。
本指南将向你展示如何可靠地使用 Python 抓取 Realtor.com。我们将构建一个简洁可运行的爬虫,通过 Crawling API 获取渲染后的搜索页面,读取 Realtor.com 嵌入在隐藏 __NEXT_DATA__ 脚本中的房源数据,提取所需字段,处理分页,并导出为整洁的 JSON 和 CSV 格式。整个教程仅限于公开房源数据,文末的合法性章节并非套话,请在将其应用于实际大批量采集前务必阅读。
你将构建什么
一个 Python 脚本,接受某城市和州的公开 Realtor.com 搜索 URL,通过 Crawling API 获取渲染后的 HTML,解析嵌入的房源数据集,并为每套房产生成一条结构化记录。我们将以单个城市为示例,提取以下字段:
- Price 房源上显示的挂牌价格。
- Beds 卧室数量。
- Baths 综合浴室数量。
- Sqft 住宅室内建筑面积。
- Address 街道、城市、州和邮政编码。
- Link 通过固定链接重新构建的房源规范 URL。
为什么普通请求在 Realtor.com 上会失败
如果你用基础 HTTP 客户端请求 Realtor.com 的搜索 URL,你会得到一个状态码为 200 的响应,但可见标记中几乎没有任何房源数据。有两个原因。首先,Realtor.com 是一个 Next.js 应用,它在浏览器中注水加载房源,因此数据位于隐藏的 <script id="__NEXT_DATA__"> 标签中的 JSON 数据块内,而非可直接读取的渲染 HTML 元素。其次,该网站能快速识别自动化流量:数据中心 IP 和不像真实浏览器的请求模式,在到达完整页面之前就会受到挑战或被呈现 CAPTCHA。
因此,一个能正常工作的 Realtor.com 爬虫需要在单次请求中同时具备两点:一个真正渲染页面的浏览器,以及一个平台认为是真实访客的 IP。你可以自行搭建无头浏览器加轮换住宅代理池,但将它们拼接在一起并保持正常运行才是主要工作量。Crawling API 将两者整合为一次调用:你向它发送带有 JavaScript token 的 URL,它在受信任 IP 后面渲染页面,并返回包含完整 __NEXT_DATA__ 载荷的渲染后 HTML,供你解析。
右键点击 Realtor.com 页面并选择"检查",然后在 HTML 中搜索 __NEXT_DATA__。这个隐藏的单脚本包含了页面渲染所用的完整房源数据集,包括可见布局中从未显示的字段。读取它比抓取单个 DOM 元素更稳定,因为 JSON 键名的变化频率远低于其周围的 CSS 类名。
前提条件
在编写任何代码之前,你需要准备几样东西,都不会花太长时间。
基础 Python 知识。 你应该熟悉编写和运行 Python 脚本,以及使用 pip 安装包。如果你是 Python 新手,Python 网页抓取指南涵盖了本教程所假设的基础知识。
Python 3.8 或更高版本。 使用 python --version 确认你的版本。如果尚未安装,请从 python.org 或通过 Anaconda 等发行版安装。
Crawlbase 账户和 JS token。 注册后,打开控制台并复制你的 JavaScript (JS) token。前 1,000 次请求免费,无需信用卡。请像对待密码一样保管 token:它用于验证你的请求,因此不要将其提交到版本控制系统中。
设置项目
创建虚拟环境以隔离项目依赖,然后安装爬虫所需的两个库。
python --version python -m venv realtor_env source realtor_env/bin/activate pip install crawlbase
在 Windows 上,使用 realtor_env\Scripts\activate 替代 source 命令激活环境。这里只需一个第三方依赖:crawlbase 是 Crawling API 的官方客户端。由于房源数据以 JSON 形式嵌入在 __NEXT_DATA__ 脚本中,Python 内置的 json 和 re 模块无需额外的 HTML 库即可完成解析。
步骤 1:获取渲染后的搜索页面
首先获取渲染完成的页面。导入 CrawlingAPI 类,使用你的 JS token 初始化它,并请求 Realtor.com 的搜索 URL。在解析之前检查状态码,可以让失败情况清晰可见而非悄无声息。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": search_url = "https://www.realtor.com/realestateandhomes-search/Los-Angeles_CA/pg-1" html = crawl(search_url) print(html[:500] if html else "No HTML returned")
针对像 Next.js 这样的客户端渲染目标,两个等待选项非常重要。ajax_wait 告知 API 等待异步内容加载完成,page_wait 在加载后固定等待若干毫秒,确保注水数据在页面捕获前已就位。五秒是合理的起始值;如果数据集为空,可适当增加。运行 python realtor_scraper.py,你应该能看到包含 __NEXT_DATA__ 脚本的真实页面标记,而非普通请求返回的空壳。这证明在你编写任何解析代码之前,渲染功能已经正常工作。
Realtor.com 需要在受信任 IP 后面渲染 Next.js 页面,通过单次调用完成,否则 __NEXT_DATA__ 载荷根本无法读取。Crawling API 接受 JS token,在真实浏览器中运行页面,在服务器端轮换住宅 IP,并向你返回渲染完成的 HTML,省去了自行运行无头浏览器集群和代理池的麻烦。先在免费层级指向一个公开搜索页面试用。
步骤 2:提取嵌入的房源数据集
拿到渲染后的 HTML,从 __NEXT_DATA__ 脚本中提取 JSON,并找到其中包含搜索结果的部分。Realtor.com 将结果嵌套在 props.pageProps 下,对于使用替代格式的页面,还有一个备用路径位于 searchResults.home_search。封装好这些查找逻辑,使缺失的键返回 None 而非导致运行崩溃。
import re import json def extract_next_data(html): # The listing dataset lives in a hidden __NEXT_DATA__ script. match = re.search( r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html, re.DOTALL, ) if not match: print("No hidden web data found.") return None return json.loads(match.group(1)) def get_results(data): # Prefer the pageProps path, fall back to the home_search shape. page_props = data.get("props", {}).get("pageProps", {}) results = page_props.get("properties") if results: return results search = data.get("searchResults", {}).get("home_search", {}) return search.get("results", [])
extract_next_data 函数使用单个正则表达式抓取 __NEXT_DATA__ 脚本的内容并将其解析为 JSON,避免仅为读取 JSON 数据块而引入 HTML 解析器。get_results 辅助函数首先尝试 pageProps 下的 properties 数组,若失败则回退到 home_search.results,因为 Realtor.com 根据页面访问方式提供两种结构。每次查找都使用带有默认值的 dict.get,因此结构发生变化的页面会返回空列表,而非抛出 KeyError。
步骤 3:将每套房产解析为扁平记录
结果数组中的每个条目都包含 description 块(卧室、浴室、建筑面积)、location.address 块、list_price 以及可转换为完整房源 URL 的 permalink。将这些映射为扁平字典,便于写入 JSON 或 CSV。
def parse_property(item): description = item.get("description") or {} location = item.get("location") or {} address = location.get("address") or {} parts = [ address.get("line"), address.get("city"), address.get("state_code"), address.get("postal_code"), ] full_address = ", ".join(p for p in parts if p) permalink = item.get("permalink") link = ( f"https://www.realtor.com/realestateandhomes-detail/{permalink}" if permalink else None ) return { "price": item.get("list_price"), "beds": description.get("beds"), "baths": description.get("baths_consolidated"), "sqft": description.get("sqft"), "address": full_address or None, "link": link, }
or {} 防护很重要,因为 Realtor.com 会将缺少数据的房源中某些嵌套对象设为 null,对 None 调用 .get 会抛出异常。卧室、浴室和建筑面积直接来自 description 块,其中 baths_consolidated 是 Realtor.com 用来将整间和半间浴室合并为一个数字的字段。地址通过连接街道、城市、州代码和邮政编码构建,跳过缺失部分。链接由 Realtor.com 为每套房产分配的 permalink 重建。结果是每套房源对应一条扁平记录,正是导出所需的格式。
__NEXT_DATA__ 结构比 CSS 选择器更稳定,但并非一成不变。如果每条记录的 price 或 sqft 都返回 None,请转储一套房产的原始 JSON 并重新检查键名。使用 .get 进行防御性读取意味着键名重命名只会导致字段为空,而不会造成崩溃,这对于无人值守的运行来说正是你想要的效果。
步骤 4:组合分页功能
单页只是演示;实际工作需要遍历某个城市的全部搜索结果。Realtor.com 使用简洁的 /pg-<PAGE> 后缀进行分页,因此你可以根据城市和州构建每页 URL,抓取它,提取数据集,并解析每套房产。页面间的短暂延迟可以控制请求速率。
import re import json import time from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None def find_properties(city, state, max_pages=1): listings = [] for page in range(1, max_pages + 1): url = ( "https://www.realtor.com/realestateandhomes-search/" f"{city}_{state.upper()}/pg-{page}" ) html = crawl(url) if not html: continue data = extract_next_data(html) if not data: continue for item in get_results(data): listings.append(parse_property(item)) print(f"Page {page}: {len(listings)} listings so far") time.sleep(2) return listings def main(): listings = find_properties("Los-Angeles", "CA", max_pages=3) print(json.dumps(listings, indent=2)) if __name__ == "__main__": main()
find_properties 函数沿用了传统方式:循环遍历页面范围,为每页构建 {city}_{state}/pg-{page} URL,并将每套解析后的房产添加到运行列表中。页面之间的 time.sleep(2) 是刻意为之,它控制请求速率,避免对网站发起大量请求,这是保持畅通运行最有效的习惯。将前几个步骤中的 extract_next_data、get_results 和 parse_property 函数加入其中,这就是一个完整可运行的爬虫。
输出结果示例
运行完整脚本 python realtor_scraper.py,你将得到一份整洁的结构化记录列表,每套房源对应一条。
[ { "price": 139000000, "beds": 12, "baths": "17", "sqft": null, "address": "1200 Bel Air Rd, Los Angeles, CA, 90077", "link": "https://www.realtor.com/realestateandhomes-detail/1200-Bel-Air-Rd_Los-Angeles_CA_90077_M17839-35941" } ]
导出为 JSON 和 CSV
一旦每套房源都是扁平字典,导出只需两个简短函数。JSON 保留完整的嵌套友好结构;CSV 将其压平为每个字段一列、可直接在电子表格中使用的表格。
import csv import json def save_json(listings, path="realtor_listings.json"): with open(path, "w", encoding="utf-8") as f: json.dump(listings, f, indent=2) def save_csv(listings, path="realtor_listings.csv"): if not listings: return fields = ["price", "beds", "baths", "sqft", "address", "link"] with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() writer.writerows(listings)
在 main 末尾调用 save_json(listings) 和 save_csv(listings),两种格式即可写入磁盘。明确的 fields 列表保证了 CSV 列顺序在多次运行中保持一致,这对于将结果追加到同一文件或加载到期望固定表头的工具时非常重要。至此,数据已准备好用于笔记本、数据库或定价模型。
在规模化运行中保持畅通
即使渲染问题已得到解决,Realtor.com 仍会监测类似爬虫的流量,其布局和防护措施也会随时间变化。一些良好习惯可以让较长时间的运行保持健康,这些习惯适用于任何有强防护的商业目标。
-
控制请求速率。 在紧密循环中大量请求页面是最快被限速或触发 CAPTCHA 的方式。保持页面间的
sleep,避免以全速抓取同一城市。 - 善用轮换。 住宅 IP 池将请求分散到多个真实用户地址,使任何单个地址都不会触发速率限制。Crawling API 为你处理这一切;如果你自建栈,这正是需要做好的部分。
- 关注状态码。 运行开始返回挑战或错误,说明当前速率或 IP 层级已不再足够。将其视为需要退后的信号,而非可以忽略的噪音。
-
预期结构变化。 Realtor.com 会定期更新网站,因此当字段为空时,应重新检查
__NEXT_DATA__键名,而非假设爬虫已损坏。
更广泛的操作手册,请参阅如何在不被封锁的情况下抓取网站和抓取 JavaScript 网站指南。若要扩展到单个城市之外,请批量准备搜索 URL 并通过相同的 find_properties 循环提交。
抓取 Realtor.com 合法吗?
抓取 Realtor.com 是否被允许,取决于 Realtor.com 的服务条款、你所在的司法管辖区以及你对数据的用途。服务条款限制自动化访问,因此无论工具多么谨慎,抓取行为都可能违反这些条款。这里的代码并不改变这一点,它只是让技术部分得以实现。请阅读 Realtor.com 服务条款及其 robots.txt,并将两者视为你采集内容的边界。
几条值得坚守的原则。只采集公开房源数据:任何人无需账户即可看到的挂牌价格、卧室和浴室数量、建筑面积、地址以及房源链接。Realtor.com 的大量底层数据来自经过许可的 MLS 数据源,并非可自由使用,因此即使页面是公开的,房产记录也可能带有使用限制;将 MLS 来源的字段视为许可内容而非开放数据。避免涉及可识别个人信息,包括页面上显示的经纪人、中介或业主的姓名和联系方式,这些在 GDPR 和 CCPA 等法规下属于个人数据。如果你计划商业性地或大批量地再利用数据,应获得许可或使用授权数据源,而非认为沉默即是同意。
本指南刻意将范围限于公开房源页面,因为这是让工作站得住脚的底线。它不涵盖任何登录后才能访问的内容、已保存的搜索或账户数据、经纪人和业主的个人或联系信息,也不涉及任何绕过身份验证的尝试。仅限于公开房源数据。如果你的项目需要更多内容,Realtor.com 和其背后的 MLS 系统提供了官方数据合作和授权数据源,这才是生产量级的正确路径,而不是更巧妙的爬虫。
核心要点
-
Realtor.com 将数据隐藏在
__NEXT_DATA__中。 房源位于隐藏脚本中的 JSON 数据块内,因此你要读取该载荷而非抓取 DOM 元素。 -
你需要同时具备渲染能力和受信任的 IP。 带有 JS token 的 Crawling API 在一次调用中渲染 Next.js 页面并轮换 IP;
ajax_wait和page_wait控制等待时长。 -
防御性解析。 使用
.get和or {}防护来映射 price、beds、baths、sqft、address 和 link,使空字段或键名重命名只会导致值为空,而非崩溃。 -
使用
/pg-N分页并导出两种格式。 遍历页面后缀,解析每套房产,然后从相同的扁平记录写入 JSON 和 CSV。 - 坚守公开数据原则。 遵守 Realtor.com 的服务条款和 robots.txt,将 MLS 来源的字段视为许可内容,绝不采集经纪人或业主的个人信息。
常见问题
为什么普通请求不能从 Realtor.com 返回房源?
因为 Realtor.com 是一个在浏览器中注水加载房源的 Next.js 应用。数据并不在原始请求返回的静态 HTML 元素中,而是位于只有在页面渲染后才会出现的隐藏 __NEXT_DATA__ JSON 脚本中。要获取数据,必须先渲染页面,而这正是 Crawling API 的 JS token 所处理的工作,然后再从该脚本中读取 JSON。
__NEXT_DATA__ 脚本是什么,为什么要抓取它?
它是 Next.js 网站嵌入的 JSON 载荷,供页面在浏览器中注水使用。在 Realtor.com 上,它包含完整的搜索结果数据集,包括每套房源的价格、卧室、浴室、建筑面积、地址和固定链接。读取它比解析可见 HTML 更稳定,因为 JSON 键名的变化频率远低于其周围的 CSS 类名。
抓取 Realtor.com 需要普通 token 还是 JS token?
需要 JS token。普通 token 获取静态 HTML,而 Realtor.com 的静态 HTML 不包含你所需的已注水 __NEXT_DATA__ 内容。JS token 首先在真实浏览器中渲染页面,因此当你提取和解析时,嵌入的数据集已就位。
如何处理某个城市房源的分页?
Realtor.com 在搜索 URL 上使用 /pg-<PAGE> 后缀,因此为每页构建 {city}_{state}/pg-{page} 并循环页码即可。上面的 find_properties 函数正是如此:它抓取每页,提取数据集,解析每套房产,并在页面之间暂停,保持礼貌的请求速率。
我能从 Realtor.com 房源中提取哪些字段?
公开房源字段:挂牌价格、卧室和浴室数量、建筑面积、完整地址以及由固定链接重建的房源链接。坚守任何访客无需账户即可看到的数据,将 MLS 来源的字段视为许可内容,并避免采集经纪人、中介或业主的个人信息(这超出了本指南所涵盖的公开房源范围)。
我能随时间追踪价格和房源变化吗?
可以。按计划运行爬虫,以固定链接作为每套房源的键,并在两次运行之间对价格和状态字段进行差异比对,以捕获新上市、降价和已售出的房源。保持适度的请求速率,只存储你所需的公开房源字段。对于相关的房地产目标,请参阅抓取 Zillow和抓取 Redfin的指南。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
