Redfin 在全美和加拿大各地列出待售和出租的房屋,每个房源页面都恰好携带了驱动价格追踪、市场调研和房地产分析的结构化数据:挂牌价、卧室数、卫生间数、建筑面积、街道地址,以及指回房源的链接。把这些拉遍一个城市或一个地区,你就有了一个可以对比、绘制图表并随时间观察的数据集。
本指南将向你展示如何用 Python 以可靠的方式抓取 Redfin 房产数据。你会构建一个小巧、可直接运行的抓取脚本,它通过 Crawling API 获取渲染后的 Redfin 房源,用 BeautifulSoup 解析你需要的字段,遍历搜索分页,并把结果导出为 JSON 和 CSV。整个演示始终限定在公开房源数据范围内,而结尾附近的合法性章节并非套话,所以在把它指向任何真实规模的抓取之前,请先阅读那一节。
你将构建什么
一个 Python 脚本:它接收一个公开的 Redfin 房源 URL,通过 Crawling API 取回渲染后的 HTML,并为该房产提取一条结构化记录。我们以单条待售房源作为贯穿全文的示例,抓取以下字段:
- 价格 房产页面上显示的挂牌售价。
- 卧室数 卧室的数量。
- 卫生间数 卫生间的数量。
- 建筑面积 房屋的已完工建筑面积。
- 地址 街道地址、城市、州和邮编。
- 链接 房源本身的规范 URL。
为什么普通请求在 Redfin 上会失败
如果你用一个简单的 HTTP 客户端请求 Redfin 房源 URL,你通常会得到两种令人失望的结果之一:一个价格、卧室数和卫生间数字段仍然为空的单薄 HTML 空壳,或者一个在你触及房源之前就出现的拦截页面。有两点对你不利。首先,Redfin 用 JavaScript 在浏览器中渲染它的大部分房源详情,所以普通抓取看到的最初 HTML 还没有包含你想要的那些数字。其次,Redfin 运行着反抓取措施,包括 IP 速率限制、CAPTCHA 和 user-agent 检测,所以数据中心 IP 以及看起来不像真实浏览器的请求模式会很快被发起验证质询。
所以一个能正常工作的 Redfin 抓取器在一次请求中需要两样东西:一个真正能渲染页面的浏览器,以及一个被平台读作真实访客的 IP。你可以自己用无头浏览器加上一池轮换住宅代理把它拼起来,但把它们缝合在一起并保持其健康运行才是大部分工作量所在。Crawling API 把两者折叠进单次调用:你把带 JavaScript token 的 URL 发给它,它在一个可信 IP 背后渲染页面,并把处理完的 HTML 返回给你解析。
Crawlbase 提供两种 token。普通 token 获取静态 HTML;JavaScript(JS)token 会先在真实浏览器中渲染页面。Redfin 在客户端填充其房源字段,所以这里你需要 JS token。普通 token 返回的是和普通抓取一样的部分空壳,里面几乎没有有用的东西可供解析。
前置条件
在写任何代码之前,你需要准备好几样东西。它们都花不了多少时间。
Python 基础。你应当能够自如地编写并运行 Python 脚本,以及用 pip 安装软件包。如果你刚接触这门语言,Python 网页抓取指南涵盖了本教程默认你已掌握的基础知识。
Python 3.8 或更高版本。用 python --version 确认你的版本。如果你还没有,请从 python.org 安装,或通过 Anaconda 这类发行版安装。
一个 Crawlbase 账户和 JS token。注册、打开你的仪表盘,并复制你的 JavaScript(JS)token。前 1,000 次 Crawling API 请求免费,且无需信用卡。把这个 token 当成密码对待:它用于认证你的请求,所以不要把它纳入版本控制。
搭建项目
创建一个虚拟环境,让项目依赖保持隔离,然后安装抓取器所需的两个库。
python --version python -m venv redfin_env source redfin_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,用 redfin_env\Scripts\activate 激活环境,而不是那一行 source。两个依赖各司其职:crawlbase 是 Crawling API 的官方客户端,而 beautifulsoup4 解析返回的 HTML,让你能用 CSS 选择器逐个抽取字段。如果你以前没用过这个解析器,BeautifulSoup 指南是本教程的好搭档。
第 1 步:获取渲染后的房源
先从拿到处理完的页面开始。导入 CrawlingAPI 类,用你的 JS token 初始化它,并请求房源 URL。在解析之前检查状态,能让失败显式暴露而不是悄无声息。
from crawlbase import CrawlingAPI crawling_api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = crawling_api.get(page_url, options) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Failed to fetch the page. Crawlbase status: {response['headers']['pc_status']}") return None if __name__ == "__main__": listing_url = "https://www.redfin.com/CA/North-Hollywood/6225-Coldwater-Canyon-Ave-91606/unit-106/home/5104172" html = crawl(listing_url) print(html[:500] if html else "No HTML returned")
对于像这样客户端渲染的目标,两个等待选项很关键。ajax_wait 告诉 API 等待异步内容加载完成,而 page_wait 在加载后固定等待若干毫秒,让那些延迟渲染的元素在页面被捕获前出现。5 秒是个合理的起点;如果价格或详情字段返回为空就把它调高。Crawlbase 库在 response["headers"]["pc_status"] 下返回状态;值为 "200" 意味着页面已被获取并渲染。运行脚本,你应当看到真实的房源标记,而不是一个拦截页面,这能在你写下任何一个选择器之前确认渲染是有效的。
Redfin 需要在一次调用里,于一个可信 IP 背后渲染页面,而且它会主动对任何看起来自动化的东西做速率限制和 CAPTCHA 质询。Crawling API 接收一个 JS token,在真实浏览器中运行页面,在服务端轮换住宅 IP,并交给你处理完的 HTML,于是你就省去了自己运行无头浏览器集群和代理池的麻烦。你的前 1,000 次请求是免费的。
第 2 步:用 BeautifulSoup 解析房源字段
拿到渲染后的 HTML 后,把它加载进 BeautifulSoup 并按选择器抽取每个字段。Redfin 把核心房源详情布局成一个可预测的结构,所以你可以把价格、地址和关键事实映射到各自的选择器上。Redfin 的待售页面没有公开 API,所以这是 XPath 与 CSS 选择器的活:价格位于一个 data-rf-test-id="abp-price" 区块中,地址拆分在 .street-address 和 .bp-cityStateZip 之间,而标题事实(卧室数、卫生间数、建筑面积)住在 .keyDetails-value 行里。把抽取包在一个当元素缺失时返回 None 的助手中,这样单个缺失字段不会让整次运行崩溃。
from bs4 import BeautifulSoup def text_of(soup, selector): el = soup.select_one(selector) return el.get_text(strip=True) if el else None def find_detail(details, keyword): for value in details: if keyword in value.lower(): return value return None def parse_property(html, listing_url): soup = BeautifulSoup(html, "html.parser") price = text_of(soup, 'div[data-rf-test-id="abp-price"] div') street = text_of(soup, ".street-address") city_state_zip = text_of(soup, ".bp-cityStateZip") address = " ".join(filter(None, [street, city_state_zip])) details = [d.get_text(strip=True) for d in soup.select(".keyDetails-value")] return { "address": address or None, "price": price, "beds": find_detail(details, "bed"), "baths": find_detail(details, "bath"), "sqft": find_detail(details, "sq ft"), "link": listing_url, }
text_of 助手查询一个元素,当它缺失时返回 None 而不是对着空对象抛出异常。地址由 Redfin 分开渲染的两块重建而成,即街道行和城市/州/邮编行,用一个空格连接。标题事实以一个扁平的 .keyDetails-value 字符串列表返回;find_detail 按关键词挑出卧室、卫生间和建筑面积的条目,而不是依赖一个固定位置,这能挺过布局中的小幅重排。这种结构让抽取在某个字段在给定房源上缺失时仍保持稳健。
Redfin 的类名和测试 ID(abp-price 区块、.street-address、.bp-cityStateZip,以及 .keyDetails-value 行)会在没有任何通知的情况下变化。把上面的选择器当成一个起始模板,而不是一份契约。当某个字段返回为 None 时,在浏览器开发者工具里重新检视实时房源并更新选择器。对任何生产级抓取器来说,定期维护选择器都是常态。
第 3 步:把它们组合起来
现在把获取和解析接进一个可直接运行的脚本。获取渲染后的 HTML,把它交给解析器,并打印出结构化记录。
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup crawling_api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = crawling_api.get(page_url, options) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Failed to fetch the page. Crawlbase status: {response['headers']['pc_status']}") return None def text_of(soup, selector): el = soup.select_one(selector) return el.get_text(strip=True) if el else None def find_detail(details, keyword): for value in details: if keyword in value.lower(): return value return None def parse_property(html, listing_url): soup = BeautifulSoup(html, "html.parser") price = text_of(soup, 'div[data-rf-test-id="abp-price"] div') street = text_of(soup, ".street-address") city_state_zip = text_of(soup, ".bp-cityStateZip") address = " ".join(filter(None, [street, city_state_zip])) details = [d.get_text(strip=True) for d in soup.select(".keyDetails-value")] return { "address": address or None, "price": price, "beds": find_detail(details, "bed"), "baths": find_detail(details, "bath"), "sqft": find_detail(details, "sq ft"), "link": listing_url, } def main(): listing_url = "https://www.redfin.com/CA/North-Hollywood/6225-Coldwater-Canyon-Ave-91606/unit-106/home/5104172" html = crawl(listing_url) if not html: return data = parse_property(html, listing_url) print(json.dumps(data, indent=2)) if __name__ == "__main__": main()
输出长什么样
运行完整脚本,你会为该房源得到一条干净的结构化记录,可直接写入 JSON、CSV 或数据库。
{ "address": "6225 Coldwater Canyon Ave #106 Valley Glen, CA 91606", "price": "$627,000", "beds": "2 beds", "baths": "2 baths", "sqft": "1,209 sq ft", "link": "https://www.redfin.com/CA/North-Hollywood/6225-Coldwater-Canyon-Ave-91606/unit-106/home/5104172" }
跨搜索页和分页进行扩展
一条房源只是演示;真正的任务会跑遍整个搜索。Redfin 为它的搜索结果分页,而每个城市或地区都有一个你可以追加页码的结果路径,比如 /page-2。这个模式有两层:爬取每个搜索结果页以采集房源 URL,然后通过你已经写好的同一个 parse_property 函数获取每条房源。Redfin 搜索页上的房源卡片通过一个带 .bp-Homecard__Photo--image 包裹层的锚点或卡片的链接元素暴露其 URL,所以你可以采集这些 href 并对照 Redfin 域名解析它们。
import time from urllib.parse import urljoin BASE = "https://www.redfin.com" def collect_listing_urls(search_html): soup = BeautifulSoup(search_html, "html.parser") cards = soup.select("a.bp-Homecard") urls = [urljoin(BASE, a["href"]) for a in cards if a.get("href")] return list(dict.fromkeys(urls)) def scrape_search(search_url, pages): listings = [] for page in range(1, pages + 1): page_url = search_url if page == 1 else f"{search_url}/page-{page}" search_html = crawl(page_url) if not search_html: continue for url in collect_listing_urls(search_html): html = crawl(url) if html: listings.append(parse_property(html, url)) time.sleep(2) print(f"Scraped {len(listings)} listings") return listings
dict.fromkeys 这一步会去掉当一张卡片不止一次链接到同一房源时出现的重复 URL。房源获取之间的 time.sleep(2) 是有意为之:它为运行设置节奏,让你不至于猛打 Redfin,而这是保持不被封禁最有效的单一习惯。调整页数和搜索 slug 来匹配你的目标地区。
导出为 JSON 和 CSV
一旦你有了一份记录列表,把它们写出来就是两个简短的函数。JSON 为下游代码保留完整结构;CSV 则是每个电子表格和 BI 工具都能读的格式。
import csv def save_json(records, path="redfin_listings.json"): with open(path, "w", encoding="utf-8") as f: json.dump(records, f, indent=2, ensure_ascii=False) def save_csv(records, path="redfin_listings.csv"): if not records: return fields = ["address", "price", "beds", "baths", "sqft", "link"] with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() writer.writerows(records) if __name__ == "__main__": results = scrape_search("https://www.redfin.com/city/11203/CA/Los-Angeles", pages=3) save_json(results) save_csv(results)
CSV 写入器固定一个明确的列顺序,让表头在每次运行间都保持稳定,而两个写入器都使用 UTF-8,让带重音字符的地址能在往返中幸存。有了磁盘上的 JSON 和 CSV,你就可以把数据直接喂进一个 notebook、一个仪表盘或一次数据库加载。
保持不被封禁
即便处理好了渲染,Redfin 仍会用 IP 速率限制、CAPTCHA 和 user-agent 检查来监视具有抓取器特征的流量。几个习惯能让一次运行保持健康,且它们适用于任何难啃的商业目标。
-
给请求设置节奏。在一个紧凑的循环里猛打房源是最快招致限速或被送上 CAPTCHA 的方式。把请求分散开,就像上面的
sleep那样,并把目标变换一下,而不是全速爬取同一条路径。 - 依靠轮换。一池住宅 IP 把请求分散到许多真实用户地址上,让没有任何单个 IP 触发速率限制。Crawling API 替你处理这件事;如果你自己搭建技术栈,这就是要做对的部分。
-
读懂状态码。一次开始返回验证质询或非 200
pc_status值的运行,是在告诉你当前的速率或 IP 档位已经不够了。把它当成该退一步的信号,而不是可以忽略的噪声。
关于更广的实战手册,请参阅如何在不被封禁的情况下抓取网站。如果你更愿意让自己的流量经由一个轮换池,而不是使用托管 API,那么 Smart AI Proxy 会以一个即插即用的代理端点形式,给你与之相同的住宅 IP 轮换。接下来要处理其他房地产门户?同样的先渲染再解析模式可延续到抓取 Zillow 和抓取 Realtor.com,只是选择器会变。
抓取 Redfin 合法吗?
抓取 Redfin 是否被允许,取决于 Redfin 的服务条款、你所在的司法管辖区,以及你拿数据做什么。Redfin 的条款限制自动化访问,所以无论你的工具多么谨慎,抓取都可能违反那些条款。这里的代码不会改变这一点;它只是把技术部分跑通而已。请阅读 Redfin 的服务条款及其 robots.txt,尊重它们所暗含的速率预期,并把两者都当成你所采集内容的边界。
有几条值得守住的底线。只采集公开的房源数据:任何人无需账户即可看到的价格、卧室数、卫生间数、建筑面积、地址和房源链接。把你的请求量保持得足够低,让你不至于给 Redfin 的服务器造成压力。远离任何与可识别个人相关的内容,包括出现在页面上的房源经纪人或业主的姓名和联系方式;一个公开的房产房源并不是一份去构建附着其上的人的画像的许可。房地产门户上的很多底层房产数据来自在特定条款下授权的 MLS 数据源,所以即便页面本身是公开的,批量再发布或转售它也可能撞上授权限制。
本指南有意限定在公开房源页面,因为那是让这项工作站得住脚的界线。它不涉及登录之后的任何内容、保存搜索或账户数据、经纪人或业主的个人数据,也不涉及任何绕过认证的尝试。只针对公开房产数据。如果你的项目需要一个稳定、受认可的数据源,Redfin 和其他门户提供数据合作,且为此目的恰好存在获得授权的 MLS 数据源;对于生产级的量,一份授权安排或一个房地产数据提供商才是正确的路径,而不是更巧妙的抓取器。
核心要点
- Redfin 是客户端渲染且防御严密的。普通抓取返回一个部分空壳或一个拦截页面,所以你必须先在一个可信 IP 背后渲染页面再解析它。
-
Crawling API 在一次调用里同时做到两点。一个 JS token 渲染页面并在服务端轮换住宅 IP;
ajax_wait和page_wait控制它等待内容的时长,而pc_status告诉你获取是否成功。 -
由 BeautifulSoup 完成抽取。把价格、地址、卧室数、卫生间数、建筑面积和房源链接映射到像
abp-price和.keyDetails-value这样的当前选择器上,并预期这些选择器会漂移。 - 通过为搜索分页、再循环房源来扩展。从每个结果页采集 URL,用同一个解析器获取每条房源,用一个短暂的 sleep 给运行设置节奏,并导出为 JSON 和 CSV。
- 守在公开数据上。尊重 Redfin 的 ToS 和 robots.txt,只采集公开房产字段,记住 MLS 数据常常是获得授权的,并且永远不要触碰账户、登录,或经纪人和业主的个人详情。
常见问题
为什么普通请求从 Redfin 返回不到数据?
因为 Redfin 用 JavaScript 在客户端渲染它的大部分房源详情,而且它会对自动化流量发起质询。一个原始的 HTTP 请求常常返回一个价格、卧室数和卫生间数字段为空的单薄空壳,或者一个在房源加载之前就出现的拦截页面。要拿到真实数据,你必须在一个可信 IP 背后渲染页面,而这正是 Crawling API 的 JS token 替你处理的事。
对 Redfin 我该用普通 token 还是 JS token?
JS token。普通 token 获取静态 HTML,而在 Redfin 上那就是普通抓取返回的同一个部分空壳。JS token 会先在真实浏览器中渲染页面再交回 HTML,所以当 BeautifulSoup 解析时房源字段都在。
我能从一条 Redfin 房源抓取哪些数据?
公开的房源字段:价格、卧室数和卫生间数、建筑面积、街道地址,以及房源链接。守在任何访客无需账户即可看到的数据上,并避开经纪人或业主的姓名和联系方式,那些落在本指南所涵盖的公开房源范围之外。
我的选择器返回 None。是什么变了?
几乎可以肯定是 Redfin 的标记变了。abp-price 区块、.street-address 和 .bp-cityStateZip 元素,以及 .keyDetails-value 行都会在没有任何通知的情况下变化,所以上个月还能用的选择器可能会失效。在浏览器开发者工具里重新检视一条实时房源并更新选择器。对任何生产级抓取器来说,定期维护选择器都是常态。
我该如何处理一次 Redfin 搜索里的分页?
Redfin 在一个地区的搜索路径后追加一个像 /page-2 这样的页码片段,所以你依次爬取每个结果页,从上面的卡片采集房源链接,并用同一个解析器获取每条房源。请求之间保留一个短暂的 sleep,并在一个页面返回不到新卡片时停止。上面的 scrape_search 函数展示了完整的循环。
我能用同样的方法抓取 Redfin 的出租和待售页面吗?
能。先渲染再解析的模式对两者都一样;只是选择器不同,因为出租和待售页面以不同方式布局它们的字段。通过 Crawling API 获取渲染后的 HTML,然后把 BeautifulSoup 指向匹配你正在抓取的页面类型的选择器。对于其他门户,同样的流程可延续到抓取 Trulia 和其他房地产站点。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
