Apartments.com 是网络上最大的租房平台之一,其房源页面承载着为房地产基准租金分析、市场研究和潜在客户开发所需的精确结构化数据:物业名称和地址、月租金、卧室数、卫浴数、建筑面积以及配套设施列表。问题在于,这些页面是客户端渲染的,而且该平台对自动化流量防御严密,因此一个普通的 HTTP 请求只会返回一个几乎空白的壳,而非你所需要的房源信息。
本指南将展示如何用可靠的方式通过 Python 爬取 Apartments.com。你将构建一个小型、可运行的爬虫:通过 Crawling API 抓取渲染后的房源页面,用 BeautifulSoup 解析目标字段,并输出干净的结构化数据。整个教程范围仅限于公开房源数据,末尾的合法性部分不是样板文字,在将此脚本对准任何真实规模的目标前,请务必阅读。
你将构建什么
一个 Python 脚本,接受一个公开的 Apartments.com 房源 URL,通过 Crawling API 获取渲染后的 HTML,并为该物业提取结构化记录。我们将以一个租房房源作为贯穿示例,提取以下字段:
- 名称与地址物业名称及街道地址。
- 租金房源上显示的月租金或租金区间。
- 卧室数卧室数量。
- 卫浴数卫浴数量。
- 面积单元的建筑面积(平方英尺)。
- 配套设施功能列表,如空调、停车或套内洗衣机。
为何普通请求在 Apartments.com 上会失败
如果你用裸 HTTP 客户端请求 Apartments.com 的房源 URL,你会得到一个状态码为 200 的响应,但响应体中几乎没有任何房源数据。有两个因素对你不利。第一,Apartments.com 用 JavaScript 在浏览器中渲染其大部分房源内容,因此初始 HTML 只是一个薄壳,只有在页面脚本运行后才会填充。第二,该平台会迅速标记自动化流量:数据中心 IP 以及看起来不像真实浏览器的请求模式,会在到达渲染内容之前就被质询或收到验证码。
因此,一个有效的 Apartments.com 爬虫在一次请求中需要两样东西:一个真正能渲染页面的浏览器,以及一个平台识别为真实访客的 IP。你可以自行组装:使用无头浏览器加上轮换住宅代理池,但将这两者拼合在一起并保持健康运行,占据了大部分工作量。Crawling API 将两者整合进一次调用:你发送带有 JavaScript token 的 URL,它在受信任 IP 后面渲染页面,并将完整的 HTML 返回给你解析。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 先在真实浏览器中渲染页面。Apartments.com 的房源字段是客户端渲染的,所以这里需要 JS token。使用普通 token 会返回与普通请求相同的空壳,其中没有任何有用的内容可供解析。
前置条件
在编写任何代码之前,你需要准备好以下几样东西。它们都不需要太长时间。
Python 基础。你应该能够编写和运行 Python 脚本,并用 pip 安装包。如果你是这门语言的新手,官方 Python 文档和任何初级课程都能让你达到本教程所需的水平。
Python 3.8 或更高版本。用 python --version 确认你的版本。如果没有,从 python.org 或 Anaconda 等发行版安装。
Crawlbase 账号和 JS token。注册后,打开控制台,从账号文档页复制你的 JavaScript(JS)token。像对待密码一样对待这个 token:它用于验证你的请求,所以不要提交到版本控制中。
搭建项目
创建一个虚拟环境,使项目依赖保持隔离,然后安装爬虫所需的两个库。
python --version python -m venv apartments_env source apartments_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,用 apartments_env\Scripts\activate 替换 source 行来激活环境。两个依赖项各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 通过 CSS 选择器解析返回的 HTML,让你可以提取各个字段。如果你以前没有用过这个解析器,BeautifulSoup 指南是本教程很好的配套读物。
第一步:抓取渲染后的房源页面
首先获取完整的页面。导入 CrawlingAPI 类,用你的 JS token 初始化它,并请求房源 URL。在解析之前检查状态码,可以让失败信息清晰可见而不是悄悄忽略。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_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__": page_url = "https://www.apartments.com/2630-n-hamlin-ave-chicago-il/kvl7tm9/" html = crawl(page_url) print(html[:500] if html else "No HTML returned")
两个等待选项对于这类客户端渲染目标非常重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后等待固定的毫秒数,让延迟渲染的元素在页面捕获前出现。五秒是一个合理的起点;如果房源字段返回为空,可以适当增加。用 python scraper.py 运行脚本,你应该能在前 500 个字符中看到真实的房源标记,而不是普通请求返回的空壳,这确认了渲染在你编写任何选择器之前就已经生效。
Apartments.com 需要在受信任 IP 后面的渲染页面,且要在一次调用中完成。Crawling API 接受 JS token,在真实浏览器中运行页面,在服务端轮换住宅 IP,并将完整的 HTML 交给你,省去了自行运行无头浏览器集群和代理池的麻烦。先在免费套餐上对一个公开房源进行测试。
第二步:用 BeautifulSoup 解析房源字段
拿到渲染后的 HTML,将其加载到 BeautifulSoup 中,通过选择器提取各个字段。Apartments.com 以可预测的结构排列核心房源详情,因此你可以将名称、租金、卧室数、卫浴数、面积和配套设施映射到各自的选择器。将整个提取过程用辅助函数包装,当元素缺失时返回 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 detail_at(soup, index): rows = soup.select(".rentInfoDetail") return rows[index].get_text(strip=True) if index < len(rows) else None def scrape_listing(html): soup = BeautifulSoup(html, "html.parser") address = soup.select_one(".propertyAddress") location = ", ".join( s.get_text(strip=True) for s in address.select("span") ) if address else None amenities = [ s.get_text(strip=True) for s in soup.select("#amenitiesSection .specInfo span") ] return { "name": text_of(soup, "#propertyName"), "location": location, "rent": detail_at(soup, 0), "beds": detail_at(soup, 1), "baths": detail_at(soup, 2), "size": detail_at(soup, 3), "amenities": amenities, }
text_of 和 detail_at 辅助函数在两种形式下做同样有用的事:查询元素,并在元素缺失时返回 None,而不是对空值调用方法时抛出异常。地址通过连接 .propertyAddress 内每个 span 的文本来构建,因为 Apartments.com 将街道、城市和州分割在不同元素中。配套设施以列表形式返回,因为一个房源可能有零到几十项。这种结构使提取在某个字段在特定房源中缺失时仍能保持健壮,这种情况很常见,因为并非每个物业都列出了面积或完整的配套设施。
Apartments.com 的类名(rentInfoDetail 行、#amenitiesSection 包装器、地址 span)会在不事先通知的情况下变更。将上面的选择器视为起始模板,而非固定合约。当某个字段返回 None 或空列表时,在浏览器开发者工具中重新检查线上房源并更新选择器。定期进行选择器维护对任何生产级爬虫来说都是正常的,不是出了什么问题的迹象。
第三步:整合在一起
现在将抓取和解析整合成一个可运行的脚本。抓取渲染后的 HTML,交给解析器,并打印结构化记录。
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_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 text_of(soup, selector): el = soup.select_one(selector) return el.get_text(strip=True) if el else None def detail_at(soup, index): rows = soup.select(".rentInfoDetail") return rows[index].get_text(strip=True) if index < len(rows) else None def scrape_listing(html): soup = BeautifulSoup(html, "html.parser") address = soup.select_one(".propertyAddress") location = ", ".join( s.get_text(strip=True) for s in address.select("span") ) if address else None amenities = [ s.get_text(strip=True) for s in soup.select("#amenitiesSection .specInfo span") ] return { "name": text_of(soup, "#propertyName"), "location": location, "rent": detail_at(soup, 0), "beds": detail_at(soup, 1), "baths": detail_at(soup, 2), "size": detail_at(soup, 3), "amenities": amenities, } def main(): page_url = "https://www.apartments.com/2630-n-hamlin-ave-chicago-il/kvl7tm9/" html = crawl(page_url) if not html: return data = scrape_listing(html) print(json.dumps(data, indent=2)) if __name__ == "__main__": main()
输出示例
用 python scraper.py 运行完整脚本,你将得到该房源的干净结构化记录,可以直接写入 JSON、CSV 或数据库。
{ "name": "2630 N Hamlin Ave", "location": "2630 N Hamlin Ave, Chicago, IL, 60647", "rent": "$2,350", "beds": "2 bd", "baths": "1 ba", "size": "1,000 sq ft", "amenities": ["Air Conditioning", "Dishwasher", "Basement", "Laundry Facilities"] }
跨房源和分页扩展
单个房源只是演示;真实任务需要覆盖整个搜索结果。Apartments.com 对搜索结果进行分页,因此模式分为两层:爬取每个搜索结果页以收集房源 URL,然后用你已经写好的同一个函数抓取每个房源。由于每个房源共享相同的结构,解析器无需修改即可适用于所有房源。
import time def collect_listing_urls(search_html): soup = BeautifulSoup(search_html, "html.parser") cards = soup.select("article.placard a.property-link") return [a["href"] for a in cards if a.get("href")] def scrape_search(base_url, pages): listings = [] for page in range(1, pages + 1): search_html = crawl(f"{base_url}{page}/") if not search_html: continue for url in collect_listing_urls(search_html): html = crawl(url) if html: listings.append(scrape_listing(html)) time.sleep(2) return listings results = scrape_search("https://www.apartments.com/chicago-il/", pages=3) with open("listings.json", "w") as f: json.dump(results, f, indent=2)
Apartments.com 在搜索路径后附加页码,因此遍历 page 可以遍历整个结果集。房源抓取之间的 time.sleep(2) 是有意为之:它控制了运行节奏,避免轰炸站点,这是保持不被封锁的最有效单一习惯。根据你的目标调整页数和城市 slug。
保持不被封锁
即使渲染已经处理好,Apartments.com 仍然会监视爬虫特征的流量。一些习惯能保持运行健康,它们适用于任何防御严密的商业目标。
-
控制请求节奏。在紧密循环中轰炸房源是触发频率限制或验证码最快的方式。如上面的
sleep所示,将请求分散开来,并分散目标而非全速爬取单一路径。 - 依赖轮换。住宅 IP 池将请求分散到众多真实用户地址上,使单个地址不会触发频率限制。Crawling API 为你处理这一点;如果你自己搭建技术栈,这是需要做好的部分。
- 读取状态码。开始返回挑战或错误的运行在告诉你当前的频率或 IP 层级已不再足够。将其视为退出信号,而非需要忽略的噪音。
更广泛的策略手册,请参阅如何在不被封锁的情况下抓取网站和关于如何在网络爬取中绕过验证码的深入分析。如果你更倾向于通过轮换池路由自己的流量而不使用托管 API,Smart AI Proxy(也称为 AI Proxy)可以作为直连代理端点提供相同的住宅 IP 轮换。
爬取 Apartments.com 合法吗?
爬取 Apartments.com 是否被允许,取决于 Apartments.com 的服务条款、你所在的司法管辖区以及你对数据的用途。其条款限制自动访问,因此无论你的工具多么谨慎,爬取都可能与这些条款相抵触。这里的任何代码都不能改变这一点;它只是让技术部分能够运行。请阅读 Apartments.com 的服务条款及其 robots.txt,并将两者视为你收集内容的边界。
几条值得坚守的原则。只收集公开房源数据:任何人无需账号就能看到的物业名称和地址、租金、卧室数、卫浴数、建筑面积以及公开显示的配套设施列表。遵守 Apartments.com 的频率预期,保持请求量足够低,避免给其服务器造成压力。避免任何与可识别个人相关的内容,包括页面上列出的房东、中介或物业管理人员的联系方式。如果你计划商业用途或批量重用数据,应获得许可或正式协议,而不是假设沉默即为同意。
本指南故意将范围限定在公开房源页面,因为这是保持工作可辩护性的界限。它不涵盖登录后的任何内容、已保存搜索或账号数据、个人或个人联系方式、登录墙后的页面,以及任何试图绕过身份验证的行为。仅限公开房源数据。如果你的项目需要的不止于此,授权安排或房地产数据提供商才是正确路径,而非更聪明的爬虫。
核心要点
- Apartments.com 是客户端渲染的。普通请求返回空壳,因此必须在解析之前先渲染页面。
-
你需要同时具备渲染能力和受信任的 IP。带有 JS token 的 Crawling API 在一次调用中完成两者;
ajax_wait和page_wait控制等待内容的时长。 - BeautifulSoup 负责提取。将名称、地址、租金、卧室数、卫浴数、面积和配套设施映射到当前选择器,并预期这些选择器会漂移。
- 通过搜索分页、然后循环房源来扩展。从每个结果页收集 URL,用同一个解析器抓取每个房源,并通过短暂的 sleep 控制节奏。
- 坚守公开数据。遵守 Apartments.com 的服务条款和 robots.txt,只收集公开房源字段,绝不触碰账号、登录或个人联系方式。
常见问题
为什么普通请求从 Apartments.com 返回不了数据?
因为 Apartments.com 用 JavaScript 在客户端渲染其房源内容。初始 HTML 是一个壳,只有在页面脚本在浏览器中运行后才会填充,所以一个原始 HTTP 请求返回状态码 200,但租金、卧室数、卫浴数和配套设施字段都是空的。要获得真实数据,必须先渲染页面,这正是 Crawling API 的 JS token 为你处理的事情。
对 Apartments.com 我需要普通 token 还是 JS token?
JS token。普通 token 获取静态 HTML,在 Apartments.com 上与普通请求返回的空壳相同。JS token 在将 HTML 返回之前先在真实浏览器中渲染页面,因此当 BeautifulSoup 解析时,房源字段是存在的。
我可以从 Apartments.com 房源中抓取哪些数据?
公开房源字段:物业名称和街道地址、月租金或租金区间、卧室数和卫浴数、建筑面积以及配套设施列表。坚守任何无需账号的访客可见的数据,避免房东、中介或物业管理人员的个人联系方式,这些超出了本指南涵盖的公开房源范围。
我的选择器返回 None 或空列表,发生了什么变化?
几乎可以肯定是 Apartments.com 的标记变了。它的 rentInfoDetail 行、#amenitiesSection 包装器以及地址 span 会在不事先通知的情况下变更,所以上个月有效的选择器可能现在已经失效。在浏览器开发者工具中重新检查线上房源并更新选择器。定期进行选择器维护对任何生产级爬虫来说都是正常的。
如何处理一个城市所有房源的分页?
Apartments.com 在搜索路径后附加页码,所以你依次爬取每个结果页,从其中的卡片收集房源链接,然后用同一个解析器抓取每个房源。请求之间保持短暂的 sleep,并在某页没有返回新卡片时停止。上面的 scrape_search 函数展示了完整的循环逻辑。
爬取 Apartments.com 时如何避免被封锁?
保持低单 IP 请求频率,用短暂延迟控制请求节奏,分散目标而非循环单一路径,并通过轮换住宅 IP 路由,使单个地址不触发频率限制。Crawling API 为你管理轮换和受信任的 IP 池;如果你自己搭建技术栈,这是需要投入的部分。监控状态码,当开始看到挑战时退出。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
