在本网络抓取教程中,我们将了解如何抓取Realtor.com——美国最大的房地产市场。 在本指南中,我们将抓取 Realtor.com 房产页面上显示的价格信息、地址、照片和电话号码等房地产数据。 Realtor.com 很容易被抓取,在本指南中,我们将利用其隐藏的网络数据系统快速抓取整个房地产数据集。我们还将了解如何使用搜索系统查找特定区域的所有房产列表。 最后,我们还将介绍跟踪以抓取新上市或出售的房产或更新价格的房产 – 让我们在房地产竞标中占据上风! 我们将使用 Python 和一些社区包,这将使这个网络抓取工具变得轻而易举。让我们开始吧!
为什么要抓取 Realtor.com?
Realtor.com 是美国最大的房地产网站之一,使其成为最大的公共房地产数据集。包含房地产价格、挂牌地点和销售日期以及一般财产信息等字段。 这对于市场分析、住宅行业研究和竞争对手的总体概况来说是有价值的信息。
项目设置
在本教程中,我们将使用带有两个社区包的 Python:
这些包可以通过pip install
命令轻松安装:
$ pip install httpx parsel
或者,可以随意换成httpx
任何其他 HTTP 客户端包,例如requests,因为我们只需要基本的 HTTP 功能,这些功能几乎可以在每个库中互换。至于,parsel
另一个很好的选择是beautifulsoup包。
抓取属性数据
让我们深入了解如何抓取 realtor.com 上列出的单个房产的数据。然后,我们还将看看如何找到这些属性并扩展我们的抓取工具。 例如,让我们先看一下列表页面,看看页面上存储的所有信息在哪里。让我们选择一个随机的属性列表,例如: realtor.com/realestateandhomes-detail/149-3rd-Ave_San-Francisco_CA_94118_M16017-14990 我们可以看到该页面包含大量数据,使用CSS 选择器或XPath 选择器解析所有内容将是一项繁重的工作。相反,让我们看看这个页面的页面源代码。 如果我们在页面源中查找一些唯一标识符(如房地产经纪人的电话号码或地址),我们可以看到该页面包含隐藏的 Web 数据,该数据包含整个属性数据集:
让我们看看如何使用 Python 抓取它。我们将检索属性 HTML 页面,找到<script>
包含隐藏 Web 数据的 并将其解析为 JSON 文档:
import asyncio import json from typing import List import httpx from parsel import Selector from typing_extensions import TypedDict # First, we sneed to establish a persisten HTTPX session # with browser-like headers to avoid instant blocking BASE_HEADERS = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "accept-language": "en-US;en;q=0.9", "accept-encoding": "gzip, deflate, br", } session = httpx.AsyncClient(headers=BASE_HEADERS) # type hints fo expected results - property listing has a lot of data! class PropertyResult(TypedDict): property_id: str listing_id: str href: str status: str list_price: int list_date: str ... # and much more! def parse_property(response: httpx.Response) -> PropertyResult: """parse Realtor.com property page""" # load response's HTML tree for parsing: selector = Selector(text=response.text) # find <script id="__NEXT_DATA__"> node and select it's text: data = selector.css("script#__NEXT_DATA__::text").get() if not data: print(f"page {response.url} is not a property listing page") return # load JSON as python dictionary and select property value: data = json.loads(data) return data["props"]["pageProps"]["initialProps"]["property"] async def scrape_properties(urls: List[str]) -> List[PropertyResult]: """Scrape Realtor.com properties""" properties = [] to_scrape = [session.get(url) for url in urls] # tip: asyncio.as_completed allows concurrent scraping - super fast! for response in asyncio.as_completed(to_scrape): response = await response if response.status_code != 200: print(f"can't scrape property: {response.url}") continue properties.append(parse_property(response)) return properties
运行代码和示例输出 async def run(): # some realtor.com property urls urls = [ "https://www.realtor.com/realestateandhomes-detail/12355-Attlee-Dr_Houston_TX_77077_M70330-35605" ] results = await scrape_properties(urls) print(json.dumps(results, indent=2)) if __name__ == "__main__": asyncio.run(run()) 生成的数据集太大,无法全部嵌入到本文中
我们可以看到,我们的简单抓取程序收到了一个广泛的财产数据集,其中包含我们在页面上看到的所有内容,如财产价格、地址、照片和房地产经纪人的电话号码,以及页面上不可见的元信息字段。 现在我们知道如何抓取单个属性,让我们看看接下来如何找到要抓取的属性!
查找 Realtor.com 属性
有多种方法可以在 Realtor.com 上查找房产,但最简单、最可靠的方法是使用他们的搜索系统。让我们来看看 Realtor.com 的搜索是如何工作的,以及我们如何抓取它。 如果我们在 Realtor 的搜索栏中输入一个位置,我们可以看到一些重要的信息:
搜索会自动将我们重定向到包含属性列表和分页元数据(该区域有多少列表可用)的结果 URL。如果我们进一步点击第二页,我们可以看到一个清晰的 URL 模式,我们可以在我们的爬虫中使用它:
realtor.com/realestateandhomes-search/<CITY>_<STATE>/pg-<PAGE>
知道了这一点,我们就可以编写我们的抓取工具,从给定的地理位置变量(城市和州)中抓取所有财产清单:
import asyncio import json import math from typing import List, Optional import httpx from parsel import Selector from typing_extensions import TypedDict # 1. Establish persisten HTTPX session with browser-like headers to avoid blocking BASE_HEADERS = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "accept-language": "en-US;en;q=0.9", "accept-encoding": "gzip, deflate, br", } session = httpx.AsyncClient(headers=BASE_HEADERS, follow_redirects=True) ... # Type hints for search results # note: the property preview contains a lot of data though not the whole dataset class PropertyPreviewResult(TypedDict): property_id: str listing_id: str permalink: str list_price: int price_reduces_amount: Optional[int] description: dict location: dict photos: List[dict] list_date: str last_update_date: str tags: List[str] ... # and more # Type hint for search results of a single page class SearchResults(TypedDict): count: int # results on this page total: int # total results for all pages results: List[PropertyPreviewResult] def parse_search(response: httpx.Response) -> SearchResults: """Parse Realtor.com search for hidden search result data""" selector = Selector(text=response.text) data = selector.css("script#__NEXT_DATA__::text").get() if not data: print(f"page {response.url} is not a property listing page") return data = json.loads(data) return data["props"]["pageProps"]["searchResults"]["home_search"] async def find_properties(state: str, city: str): """Scrape Realtor.com search for property preview data""" print(f"scraping first result page for {city}, {state}") first_page = f"https://www.realtor.com/realestateandhomes-search/{city}_{state.upper()}/pg-1" first_result = await session.get(first_page) first_data = parse_search(first_result) results = first_data["results"] total_pages = math.ceil(first_data["total"] / first_data["count"]) print(f"found {total_pages} total pages ({first_data['total']} total properties)") to_scrape = [] for page in range(1, total_pages + 1): assert "pg-1" in str(first_result.url) # make sure we don't accidently scrape duplicate pages page_url = str(first_result.url).replace("pg-1", f"pg-{page}") to_scrape.append(session.get(page_url)) for response in asyncio.as_completed(to_scrape): parsed = parse_search(await response) results.extend(parsed["results"]) print(f"scraped search of {len(results)} results for {city}, {state}") return results
运行代码和示例输出 async def run(): results_search = await find_properties("CA", "San-Francisco") print(json.dumps(results_search, indent=2)) if __name__ == "__main__": asyncio.run(run()) 将生成属性预览项目列表: [ { "property_id": "1601714990", "list_price": 4780000, "primary": true, "primary_photo": { "href": "https://www.jingzhengli.com/wp-content/uploads/2023/06/3cbf77fa7d09fc28cc037c55ddbfe875l-m4228927009s-1.jpg" }, "source": { "id": "SFCA", "agents": [ { "office_name": null } ], "type": "mls", "spec_id": null, "plan_id": null }, "community": null, "products": { "brand_name": "basic_opt_in", "products": [ "co_broke" ] }, "listing_id": "2949512103", "matterport": false, "virtual_tours": null, "status": "for_sale", "permalink": "149-3rd-Ave_San-Francisco_CA_94118_M16017-14990", "price_reduced_amount": null, "other_listings": { "rdc": [ { "listing_id": "2949512103", "status": "for_sale", "listing_key": null, "primary": true } ] }, "description": { "beds": 4, "baths": 5, "baths_full": 4, "baths_half": 1, "baths_1qtr": null, "baths_3qtr": null, "garage": null, "stories": null, "type": "single_family", "sub_type": null, "lot_sqft": 3000, "sqft": 2748, "year_built": 1902, "sold_price": 1825000, "sold_date": "2021-03-29", "name": null }, "location": { "street_view_url": "https://www.jingzhengli.com/wp-content/uploads/2023/06/3cbf77fa7d09fc28cc037c55ddbfe875l-m4228927009s.jpg" }, { "href": "https://www.jingzhengli.com/wp-content/uploads/2023/06/3cbf77fa7d09fc28cc037c55ddbfe875l-m3453981500s.jpg" } ], "tags": [ "central_air", "dishwasher", "family_room", "fireplace", "hardwood_floors", "laundry_room", "garage_1_or_more", "basement", "two_or_more_stories", "big_yard", "open_floor_plan", "floor_plan", "wine_cellar", "ensuite", "lake" ], "branding": [ { "type": "Office", "photo": null, "name": "Marcus & Millichap" } ], "home_photos": { "collection": [ { "href": "https://www.jingzhengli.com/wp-content/uploads/2023/06/3cbf77fa7d09fc28cc037c55ddbfe875l-m4228927009s-1.jpg" }, { "href": "https://www.jingzhengli.com/wp-content/uploads/2023/06/3cbf77fa7d09fc28cc037c55ddbfe875l-m3453981500s.jpg" } ], "count": 2 } }, ... ]
上面,我们的抓取器首先抓取第一页以获取结果以及此查询中总共有多少页。然后,它会同时抓取剩余的页面并返回属性 URL 列表。
跟随 Realtor.com 列表更改
Realtor.com 提供多种 RSS 提要,用于公布所列房产的变化,例如:
- Price Change Feed – 宣布物业何时更改价格。
- Open House Feed – 宣布开放日活动。
- Sold Property Feed – 宣布何时出售房产。
- New Property Feed – 在列出新属性时宣布。
如果我们想跟踪房地产市场的事件,这些都是很好的资源。我们可以实时观察楼价变化、新盘和销售! 让我们来看看我们如何为这些提要编写一个跟踪器抓取器,以便定期抓取它们。 这些提要中的每一个都按美国各州划分,它是一个简单的 RSS XML 文件,其中包含公告和日期。例如,让我们看一下加利福尼亚的价格变化提要:
<?xml version="1.0" encoding="utf-8"?> <rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> <channel> <title>Price Changed</title> <link>https://www.realtor.com</link> <description/> <atom:link href="https://pubsubhubbub.appspot.com/" rel="hub"/> <atom:link href="https://www.realtor.com/realestateandhomes-detail/sitemap-rss-price/rss-price-ca.xml" rel="self"/> <item> <link>https://www.realtor.com/realestateandhomes-detail/1801-Wedemeyer-St_San-Francisco_CA_94129_M94599-53650</link> <pubDate>Fri, 04 Nov 2022 08:54:48</pubDate> </item> <item> <link>https://www.realtor.com/realestateandhomes-detail/24650-Amador-St_Hayward_CA_94544_M96649-53504</link> <pubDate>Fri, 04 Nov 2022 08:55:03</pubDate> </item> ...
我们可以看到它包含指向属性和价格更改日期的链接。因此,要编写我们的跟踪器爬虫,我们所要做的就是:
- 每 X 秒抓取一次提要
- 解析
<link>
属性 URL 的元素 - 使用我们的财产抓取工具收集数据集
- 保存数据(到数据库或文件)并重复#1
让我们看看这在 Python 中会是什么样子。我们将每 5 分钟抓取一次此提要并将结果附加到 JSON 列表文件(每行 1 个 JSON 对象):
import asyncio import json import math from datetime import datetime from pathlib import Path from typing import Dict, List, Optional from typing_extensions import TypedDict import httpx from parsel import Selector ... # NOTE: include code from property scraping section async def scrape_feed(url) -> Dict[str, datetime]: """scrapes atom RSS feed and returns all entries in "url:publish date" format""" response = await session.get(url) body = response.content.read() selector = Selector(text=body.decode(), type="xml") results = {} for item in selector.xpath("//item"): url = item.xpath("link/text()").get() pub_date = item.xpath("pubDate/text()").get() results[url] = datetime.strptime(pub_date, "%a, %d %b %Y %H:%M:%S") return results async def track_feed(url: str, output: Path, interval: int = 60): """Track Realtor.com feed, scrape new listings and append them as JSON to the output file""" # to prevent duplicates let's keep a set of property IDs we already scraped seen = set() output.touch(exist_ok=True) # create file if it doesn't exist try: while True: # scrape feed for listings listings = await scrape_feed(url=url) # remove listings we scraped in previous loops listings = {k: v for k, v in listings.items() if f"{k}:{v}" not in seen} if listings: # scrape properties and save to file - 1 property as JSON per line properties = await scrape_properties(list(listings.keys())) with output.open("a") as f: f.write("\n".join(json.dumps(property) for property in properties)) # add seen to deduplication filter for k, v in listings.items(): seen.add(f"{k}:{v}") print(f"scraped {len(properties)} properties; waiting {interval} seconds") await asyncio.sleep(interval) except KeyboardInterrupt: # Note: CTRL+C will gracefully stop our scraper print("stopping price tracking")
运行代码和示例输出 async def run(): # for example price feed for California feed_url = f"https://www.realtor.com/realestateandhomes-detail/sitemap-rss-price/rss-price-ca.xml" # or await track_feed(feed_url, Path("track-pricing.jsonl")) if __name__ == "__main__": asyncio.run(run())
在上面的示例中,我们编写了一个 RSS 提要爬虫来抓取 Realtor.com 公告。然后我们编写了一个无限循环的抓取工具,它将这个提要和完整的属性数据集抓取到一个 JSON 列表文件中。
常问问题
为了总结本指南,让我们看一下有关网络抓取 Realtor.com 数据的一些常见问题:
抓取 Realtor.com 是否合法?
是的。Realtor.com 的数据是公开的;我们不会提取任何个人或私人信息。以缓慢、尊重的速度抓取 Realtor.com 属于道德抓取定义。 也就是说,在抓取非代理列表的个人数据(卖家姓名、电话号码等)时,应注意欧盟的 GDRP 合规性。
Realtor.com 是否有 API?
不,realtor.com 不提供财产数据的公共 API。然而,如本指南所示,使用 Realtro.com 的官方 RSS 提要,抓取房产数据和跟踪房产变化真的很容易。
如何抓取 Realtor.com?
像抓取一样,我们也可以通过跟踪每个属性页面上列出的相关租赁页面来抓取 realtor.com。为此,请参阅“抓取属性数据”部分relatedRentals
中抓取的数据集中的字段
Realtor.com 爬取摘要
在本教程中,我们使用 Python 构建了一个Realtor.com爬虫。我们首先了解如何通过提取隐藏的 Web 数据来抓取单个属性页。 然后,我们了解了如何使用 Realtor.com 的搜索系统查找房产。我们根据给定的参数构建一个搜索 URL,并抓取查询分页中列出的所有列表。 最后,我们了解了如何通过抓取官方 RSS 提要来跟踪 realtor.com 上的变化,例如价格变化、销售和新上市公告。