in

如何使用Python爬取Zillow房地产数据

如何使用Python爬取Zillow房地产数据

在本网络抓取教程中,我们将了解如何抓取Zillow.com——美国最大的房地产市场。 在本指南中,我们将收集 Zillow.com 房产页面上显示的租金和销售房产信息,例如定价信息、地址、照片和电话号码。 我们将从简要概述网站的工作原理开始。然后我们将看看如何使用搜索系统来发现属性,最后,如何抓取所有属性信息。 我们将使用带有一些社区包的 Python,这将使这个网络抓取工具变得轻而易举——让我们开始吧!

为什么要抓取 Zillow.com?

Zillow.com包含一个庞大的房地产数据集:价格、位置、联系信息等。这些信息对于市场分析、房地产行业研究和一般竞争对手概况都是有价值的信息。 所以,如果我们知道如何从 Zillow 中提取数据,我们就可以访问美国最大的房地产数据集!

项目设置

在本教程中,我们将使用带有两个社区包的 Python:

  • httpx – HTTP 客户端库,可以让我们与 Zillow.com 的服务器进行通信
  • parsel – HTML 解析库,它将帮助我们解析网络抓取的 HTML 文件。

我们还可以选择使用loguru – 一个漂亮的日志库,可以帮助我们跟踪正在发生的事情。 这些包可以通过pip install命令轻松安装:

$ pip install httpx parsel loguru

或者,可以随意换成httpx任何其他 HTTP 客户端包,例如requests,因为我们只需要基本的 HTTP 功能,这些功能几乎可以在每个库中互换。至于,parsel另一个很好的选择是beautifulsoup包。

爬取 Zillow 属性

首先,让我们看一下如何从给定的 Zillow 页面 url 中抓取 Zillow 属性数据集。 首先,让我们看一下我们想要的数据在属性页中的位置,例如:zillow.com/b/1625-e-13th-st-brooklyn-ny-5YGKWY/ 如果我们查看属性列表的页面源代码,我们可以看到属性数据集作为 javascript 变量隐藏在 HTML 正文中:

捕获 Zillow 的属性页面的页面源
我们可以看到属性数据在脚本标签中作为 JSON 对象提供

这通常被称为隐藏的网络数据抓取。在此示例中,Zillow 的后端将属性数据集存储到一个 javascript 变量中,以便前端设计人员可以访问它以将其显示给最终用户。 在此特定示例中,Zillow 使用Next.js框架。 让我们将属性抓取和解析添加到我们的抓取器代码中:

import asyncio
from typing import List
import httpx
import json

from parsel import Selector

client = httpx.AsyncClient(
    # enable http2
    http2=True,
    # add basic browser like headers to prevent being blocked
    headers={
        "accept-language": "en-US,en;q=0.9",
        "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",
    },
)


async def scrape_properties(urls: List[str]):
    """scrape zillow property pages for property data"""
    to_scrape = [client.get(url) for url in urls]
    results = []
    for response in asyncio.as_completed(to_scrape):
        response = await response
        assert response.status_code == 200, "request has been blocked"
        selector = Selector(response.text)
        data = selector.css("script#__NEXT_DATA__::text").get()
        if data:
            # Option 1: some properties are located in NEXT DATA cache
            data = json.loads(data)
            property_data = data["props"]["initialReduxState"]["gdp"]["building"]
        else:
            # Option 2: other times it's in Apollo cache
            data = selector.css("script#hdpApolloPreloadedData::text").get()
            data = json.loads(json.loads(data)["apiCache"])
            property_data = next(
                v["property"] for k, v in data.items() if "ForSale" in k
            )
        results.append(property_data)
    return results

# example run:
if __name__ == "__main__":
    async def run():
        data = await scrape_properties(
            ["https://www.zillow.com/homedetails/1625-E-13th-St-APT-3K-Brooklyn-NY-11229/245001606_zpid/"],
        )
        print(json.dumps(data, indent=2))
    asyncio.run(run())

上面,为了从 Zillow 中提取数据,我们编写了一个小函数来获取属性 URL 列表。然后我们抓取他们的 HTML 页面,提取嵌入的 javascript 状态数据并解析地址、价格和电话号码等财产信息! 让我们运行这个属性抓取器并查看它生成的结果: 示例输出

[
  {
    "address": {
      "streetAddress": "1065 2nd Ave",
      "city": "New York",
      "state": "NY",
      "zipcode": "10022",
      "__typename": "Address",
      "neighborhood": null
    },
    "description": "Inspired by Alvar Aaltos iconic vase, Aalto57s sculptural architecture reflects classic concepts of design both inside and out. Each residence in this boutique rental building features clean modern finishes. Amenities such as a landscaped terrace with gas grills, private and group dining areas, sun loungers, and fire feature as well as an indoor rock climbing wall, basketball court, game room, childrens playroom, guest suite, and a fitness center make Aalto57 a home like no other.",
    "photos": [
      "https://photos.zillowstatic.com/fp/0c1099a1882a904acc8cedcd83ebd9dc-p_d.jpg",
      "..."
    ],
    "zipcode": "10022",
    "phone": "646-681-3805",
    "name": "Aalto57",
    "floor_plans": [
      {
        "zpid": "2096631846",
        "__typename": "FloorPlan",
        "availableFrom": "1657004400000",
        "baths": 1,
        "beds": 1,
        "floorPlanUnitPhotos": [],
        "floorplanVRModel": null,
        "maxPrice": 6200,
        "minPrice": 6200,
        "name": "1 Bed/1 Bath-1D",
        ...
      }
    ...
  ]
}]

查找 Zillow 属性

现在我们知道如何抓取单个 Zillow 房产列表,我们可以看看如何使用 Zillow 的搜索栏查找列表。让我们看一下它的功能以及如何使用 Python 在 Zillow 网页抓取中使用它: 我们可以看到,一旦我们提交搜索,就会向 Zillow 的搜索 API 发出后台请求。我们发送带有一些地图坐标的搜索查询,并收到数百个列表预览。我们可以看到,要查询 Zillow,我们只需要几个参数输入:

{
  "searchQueryState":{
    "pagination":{},
    "usersSearchTerm":"New Haven, CT",
    "mapBounds":
      {
        "west":-73.03037621240235,
        "east":-72.82781578759766,
        "south":41.23043771298298,
        "north":41.36611033618769
      },
    },
  "wants": {
    "cat1":["mapResults"]
  },
  "requestId": 2
}

我们可以看到这个 API 非常强大,它允许我们在由 4 个方向值组成的两个位置点定义的任何地图区域中查找列表:北、西、南和东:

仅使用两点在地图上绘制区域的插图
使用这 4 个值,我们可以在地图的任何点绘制一个正方形或圆形区域!

这意味着只要我们知道其纬度和经度,我们就可以找到任何位置区域的属性。我们可以在我们的 python 抓取器中复制这个请求:

from urllib.parse import urlencode
import json
import httpx

# we should use browser-like request headers to prevent being instantly blocked
BASE_HEADERS = {
    "accept-language": "en-US,en;q=0.9",
    "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",
}


url = "https://www.zillow.com/search/GetSearchPageState.htm?"
parameters = {
    "searchQueryState": {
        "pagination": {},
        "usersSearchTerm": "New Haven, CT",
        # map coordinates that indicate New Haven city's area
        "mapBounds": {
            "west": -73.03037621240235,
            "east": -72.82781578759766,
            "south": 41.23043771298298,
            "north": 41.36611033618769,
        },
    },
    "wants": {
        "cat1": ["listResults", "mapResults"], "cat2": ["total"]
    },
    "requestId": 2,
}
response = httpx.get(url + urlencode(parameters), headers=BASE_HEADERS)
assert response.status_code == 200, "request has been blocked"
data = response.json()
results = response.json()["cat1"]["searchResults"]["mapResults"]
print(json.dumps(results, indent=2))
print(f"found {len(results)} property results")

我们可以看到我们可以相对轻松地复制此搜索请求。那么,让我们来看看如何正确地抓取它!

要抓取 Zillow 的搜索,我们需要地理位置详细信息,除非您熟悉地理编程,否则很难得出这些信息。但是,有一种简单的方法可以通过浏览 Zillow 的搜索页面本身来查找该位置的地理详细信息。 如果我们查看zillow.com/homes/New-Haven,-CT_rb/等搜索 URL ,我们可以看到隐藏在 HTML 正文中的地理详细信息:

捕获 Zillow 搜索寻呼机的页面源
我们可以看到隐藏在页面源评论中的此搜索的查询和地理数据

我们可以使用简单的正则表达式模式来提取这些详细信息并提交我们基于地理的搜索请求。让我们看看如何在 Python 抓取代码中做到这一点:

from loguru import logger as log
from urllib.parse import quote
import httpx

async def _search(query:str, session: httpx.AsyncClient, filters: dict=None, categories=("cat1", "cat2")):
    """base search function which is used by sale and rent search functions"""
    html_response = await session.get(f"https://www.zillow.com/homes/{query}_rb/")
    # find query data in search landing page
    query_data = json.loads(re.findall(r'"queryState":(\{.+}),\s*"filter', html_response.text)[0])
    if filters:
        query_data["filterState"] = filters

    # scrape search API
    url = "https://www.zillow.com/search/GetSearchPageState.htm?"
    found = []
    # cat1 - Agent Listings
    # cat2 - Other Listings
    for category in categories:
        full_query = {
            "searchQueryState": json.dumps(query_data),
            "wants": json.dumps({category: ["mapResults"]}),
            "requestId": randint(2, 10),
        }
        api_response = await session.get(url + urlencode(full_query, quote_via=quote))
        data = api_response.json()
        _total = data["categoryTotals"][category]["totalResultCount"]
        if _total > 500:
            log.warning(f"query has more results ({_total}) than 500 result limit ")
        else:
            log.info(f"found {_total} results for query: {query}")
        map_results = data[category]["searchResults"]["mapResults"]
        found.extend(map_results)
    return found


async def search_sale(query: str, session: httpx.AsyncClient):
    """search properties that are for sale"""
    log.info(f"scraping sale search for: {query}")
    return await _search(query=query, session=session)


async def search_rent(query: str, session: httpx.AsyncClient):
    """search properites that are for rent"""
    log.info(f"scraping rent search for: {query}")
    filters = {
        "isForSaleForeclosure": {"value": False},
        "isMultiFamily": {"value": False},
        "isAllHomes": {"value": True},
        "isAuction": {"value": False},
        "isNewConstruction": {"value": False},
        "isForRent": {"value": True},
        "isLotLand": {"value": False},
        "isManufactured": {"value": False},
        "isForSaleByOwner": {"value": False},
        "isComingSoon": {"value": False},
        "isForSaleByAgent": {"value": False},
    }
    return await _search(query=query, session=session, filters=filters, categories=["cat1"])

上面,我们定义了用于抓取租金和销售搜索的搜索功能。我们注意到的第一件事是租金和销售页面使用相同的搜索端点。唯一的区别是租金搜索应用额外的过滤来过滤掉出售的房产。 让我们运行这个 Zillow 数据抓取器,看看我们收到什么结果: 运行代码和示例输出

import json
import asyncio

async def run():
    limits = httpx.Limits(max_connections=5)
    async with httpx.AsyncClient(limits=limits, timeout=httpx.Timeout(15.0), headers=BASE_HEADERS) as session:
        data = await search_rent("New Haven, CT", session)
        print(json.dumps(data, indent=2))


if __name__ == "__main__":
    asyncio.run(run())
[
  {
    "buildingId": "40.609608--73.960045",
    "lotId": 1004524429,
    "price": "From $295,000",
    "latLong": {
      "latitude": 40.609608,
      "longitude": -73.960045
    },
    "minBeds": 1,
    "minBaths": 1.0,
    "minArea": 1200,
    "imgSrc": "https://photos.zillowstatic.com/fp/3c0259c716fc4793a65838aa40af6350-p_e.jpg",
    "hasImage": true,
    "plid": "1611681",
    "isFeaturedListing": false,
    "unitCount": 2,
    "isBuilding": true,
    "address": "1625 E 13th St, Brooklyn, NY",
    "variableData": {},
    "badgeInfo": null,
    "statusType": "FOR_SALE",
    "statusText": "For Rent",
    "listingType": "",
    "isFavorite": false,
    "detailUrl": "/b/1625-e-13th-st-brooklyn-ny-5YGKWY/",
    "has3DModel": false,
    "hasAdditionalAttributions": false,
  },
...
]

搜索返回了很多关于每个列表的有用预览数据。它包含地址、地理位置和一些元数据等字段。但是,要检索所有列表数据,我们需要抓取每个属性列表页面,我们可以在现场找到这些页面detailUrl。 因此,对于我们的抓取器,我们可以通过位置名称(城市、邮政编码等)发现属性,抓取属性预览,然后拉取所有字段detailUrl以抓取所有属性数据。接下来,让我们看看我们如何做到这一点。 我们编写了一个快速的 python 抓取器,它从给定的查询字符串中找到 Zillow 的属性,然后抓取每个属性页面以获取属性信息。

常问问题

为了总结本指南,让我们看一下有关网络抓取 Zillow 数据的一些常见问题:

是的。Zillow 的数据是公开的;我们不会提取任何个人或私人信息。以缓慢、尊重的速度抓取 Zillow.com 属于道德抓取定义。 也就是说,在抓取非代理列表的个人数据(卖家姓名、电话号码等)时,应注意欧盟的 GDRP 合规性。

Zillow.com 有 API 吗?

是的,但它非常有限且不适合数据集收集,并且没有可用的 Zillow API Python 客户端。相反,我们可以使用 Python 和 httpx 抓取 Zillow 数据,这是完全合法且容易做到的。

如何爬取 Zillow?

我们可以使用本教程中介绍的主题轻松创建 Zillow 爬虫。我们可以从种子链接(任何 Zillow URL)中抓取 Zillow 属性,而不是显式搜索属性,并跟踪循环中提到的相关属性。有关爬网的更多信息,请参阅如何使用 Python 爬网

Zillow 抓取摘要

在本教程中,我们通过在 Python 中构建一个爬虫来深入研究Zillow数据提取。 我们使用搜索来发现任何特定地区待售或出租的房地产。为了抓取财产数据,例如价格和建筑信息、联系方式等,我们通过从 HTML 页面中提取 Zillow 的状态缓存来使用隐藏的 Web 数据抓取。为此,我们将 Python 与httpxparsel包一起使用。

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。