Nstproxy 现已升级为 Nstdata。了解此次变化。 周一至周五 09:00 - 18:00(UTC+08:00) 公司地址
19H MAXGRAND PLAZA NO.3 TAI YAU STREET SAN PO KONG, KOWLOON HONG KONG
©2026 NST LABS TECH LTD. 保留所有权利。 房地产抓取:实用物业数据指南Lena ZhouGrowth & Integration Specialist
房地产抓取:公共产权数据收集指南
TL;DR
- 房地产抓取收集发布的物业信息,而MLS数据源、录制的销售和列表门户提供不同的数据集和权利。
- 保持列表身份与物业身份分开,并存储观察而不是覆盖历史。
- 在比较价格之前,标准化货币、销售与租赁、租赁期限、面积单位和时间戳。
- 本教程包括两个本地合成HTML页面的可运行解析器;它不访问物业门户。
- RESO定义标准,而不是免费MLS数据库。生产访问来自相关提供者和许可证。
什么是房地产抓取及其工作原理?
房地产抓取从被允许的源中提取与物业相关的字段,形成结构化的数据集。典型工作流程发现列表页面,获取批准的内容,解析字段,验证并去重记录,并存储观察以供后续比较。
区分主动询价、记录交易、租赁报价、评估和市场汇总。门户的询价并不是记录的销售价格,而评估可能使用不同的日期和估值方法。
你可以提取哪些房地产数据?
可用字段取决于源、区域和访问协议。在选择爬虫之前定义所需的数据集。
| 字段 | 示例意义 | 保持明确 |
|---|
| 列表ID | 一个源的报价记录 | 源命名空间和重新列表规则 |
| 物业ID | 实体资产身份 | 匹配信心和提供者标识符 |
| 价格 | 请求的销售/租赁金额 | 货币和期限 |
| 位置 | 发布的区域或地址 | 精确度、权利和隐私目的 |
| 面积 | 内部、总建筑面积或地块面积 | 单位和计量基础 |
| 卧室/浴室 | 源定义的计数 | 空值与零 |
| 状态 | 活动、撤回、待定、售出 | 源词汇和观察日期 |
| URL/时间 | 来源 | 捕获时间与列表发布时间 |
物业列表和定价数据
保留价格类型和单位。每月租金不能直接与销售询价或每周租金进行比较。保持“申请时价格”未知,而不是用零替换它。
在可行的情况下将源显示文本与标准化值存储在一起。将范围转换为一个数字的文本解析器可能会产生虚假的价格变化。税费、费用、激励以及停车是否包含在内也可能影响可比性。
位置、物业特征和列表历史
列表并不是物业。一个物业可以有多个代理列表,可以在另一个ID下重新列出,或者包含多个单元。
使用源ID进行源内去重。对于跨源实体匹配,结合适当的允许特征并保持信心;绝不要假设相似的标题证明身份。避免收集只需要市场级统计的项目的居住者或业主联系人详细信息。
房地产数据源:门户、MLS和公共记录
根据字段定义、更新频率、覆盖范围和权利选择源。
| 源 | 典型优势 | 限制 |
|---|
| 允许的门户页面 | 显示的当前报价 | 访问限制、重复、标记变化 |
| 许可的MLS/IDX数据 | 结构化列表数据 |
| 政府记录 | 记录的物业或交易事实 | 司法管辖权、延迟、模式和访问政策 |
房地产抓取方法比较
Python请求和Beautiful Soup
对于授权的静态HTML,HTTP客户端加上HTML解析器可能足够。更倾向于稳定的数据属性或结构化记录,而不是生成的呈现类。
下面的完整示例使用Python内置的HTMLParser,因此它在没有外部包的情况下运行。Beautiful Soup是一个可选的人性化替代品;它不会改变源权限或使选择器免受重新设计的影响。
Playwright用于JavaScript渲染的列表
对于批准的动态页面,浏览器自动化可以在提取之前等待列表容器。 Playwright库文档描述了浏览器生命周期。
设置一个有意义的等待条件,限制页面,并释放浏览器上下文。在登录、验证码或明确拒绝时停止。浏览器渲染增加了运行时和维护成本,不应仅仅因为页面包含 JavaScript 而使用。
管理的网络爬虫 API
它不应被描述为具有保证的特定属性架构。在构建诸如卧室、面积基准或上市状态等字段之前,请在批准的目标上验证其返回的内容。
逐步:使用 Python 构建房地产爬虫
该项目处理两个合成的本地页面,遵循有限的下一个链接,删除一个相同的重复列表,标准化面积,并导出 CSV/JSON 以及 SQLite 观察结果。
没有访问任何门户。示例域、位置、ID 和价格均为虚构。
第 1 步:检查列表页面结构
<article class="listing" data-id="demo-1" data-price="300000"
data-currency="USD" data-area="1000" data-area-unit="sqft"
data-location="Demo District" data-offer="sale">
<h2>虚构公寓 A</h2>
<a class="details" href="/listing/demo-1">详情</a>
</article>
真实页面可能使用结构化 JSON、不同的单位或可为空值。检查允许的源示例,并在编写选择器之前保留原始含义。
第 2 步:提取物业列表
python code/real_estate_scraper.py \
--fixtures code/property_fixture --db property.sqlite
"""从本地 HTML 页面提取合成列表;没有门户访问。"""
import argparse
import csv
import json
import sqlite3
from datetime import datetime, timezone
from decimal import Decimal
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urljoin
class Listings(HTMLParser):
def __init__(self):
super().__init__()
self.rows, self.next_page = [], None
self.row, self.title = None, False
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "article" and "listing" in a.get("class", "").split():
self.row = dict(listing_id=a["data-id"], title="",
amount=a["data-price"], currency=a["data-currency"],
area=a["data-area"], area_unit=a["data-area-unit"],
location=a["data-location"], offer_type=a["data-offer"])
if tag == "h2" and self.row is not None:
self.title = True
if tag == "a" and self.row is not None and a.get("class") == "details":
self.row["url"] = urljoin("https://property-demo.example/", a["href"])
if tag == "a" and "next" in a.get("rel", "").split():
self.next_page = a["href"]
def handle_data(self, data):
if self.title:
self.row["title"] += data
def handle_endtag(self, tag):
if tag == "h2":
self.title = False
if tag == "article" and self.row is not None:
self.rows.append(self.row)
self.row = None
def normalize(row):
price, area = Decimal(row.pop("amount")), Decimal(row.pop("area"))
if not price.is_finite() or not area.is_finite() or price <= 0 or area <= 0:
raise ValueError("无效的价格或面积")
unit = row.pop("area_unit")
if unit not in ("sqft", "sqm") or row["offer_type"] not in ("sale", "rent_monthly"):
raise ValueError("未知的单位或报价类型")
area_sqm = area * Decimal("0.09290304") if unit == "sqft" else area
return {**row, "title": row["title"].strip(), "price": str(price),
"area_sqm": str(area_sqm.quantize(Decimal("0.01")))}
def collect(root):
root = root.resolve()
current, seen_pages, rows = root / "page1.html", set(), {}
for _ in range(5):
current = current.resolve()
if not current.is_relative_to(root) or current in seen_pages:
raise ValueError("不安全或重复的页面路径")
seen_pages.add(current)
parsed = Listings()
parsed.feed(current.read_text())
for raw in parsed.rows:
row = normalize(raw)
key = row["listing_id"]
if key in rows and rows[key] != row:
raise ValueError("冲突的重复列表")
rows[key] = row
if not parsed.next_page:
return list(rows.values())
current = current.parent / parsed.next_page
raise ValueError("分页预算超出")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--fixtures", default="property_fixture")
parser.add_argument("--db", default="property.sqlite")
args = parser.parse_args()
rows = collect(Path(args.fixtures))
observed = datetime.now(timezone.utc).isoformat()
if not rows:
raise SystemExit("没有接受的房源")
with sqlite3.connect(args.db) as db:
db.execute("""CREATE TABLE IF NOT EXISTS observations(
listing_id TEXT, observed_at TEXT, payload TEXT,
PRIMARY KEY(listing_id,observed_at))""")
db.executemany("INSERT INTO observations VALUES(?,?,?)",
[(row["listing_id"], observed, json.dumps(row)) for row in rows])
Path("property.json").write_text(json.dumps(rows, indent=2))
with open("property.csv", "w", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
print(f"accepted_unique_listings={len(rows)}")
严格的夹具解析器在缺少必要属性时失败。生产解析器应该记录被拒绝的房源及其证据,而不是默默地写入不完整的接受记录。
第 3 步:处理分页和重复房源
本地下一页解析器强制执行夹具目录,拒绝页面循环,并有页面预算。它接受相同的重复房源 ID,但拒绝冲突的重复有效载荷。
对于经过批准的远程源,使用有界的 URL发现,限制导航到已审核的来源,验证重定向,跟踪访问过的 URL,并遵守每个来源的请求限制。在决定缺失的房源处于非活动状态之前,需要调和完整快照。
相同的房源 ID 随时间变化价格。在一次运行中去重;跨运行保持独立观察。
第 4 步:清理和标准化房产数据
对价格使用 Decimal,并进行明确的平方英尺到平方米的转换。在生产记录中保留原始面积单位和测量基础。
对于表格检查,以下 Pandas 示例读取生成的 JSON:
import pandas as pd
frame = pd.read_json("property.json", dtype=False)
frame["price_numeric"] = pd.to_numeric(frame["price"], errors="raise")
frame["area_sqm_numeric"] = pd.to_numeric(frame["area_sqm"], errors="raise")
assert frame["listing_id"].is_unique
assert frame["price_numeric"].gt(0).all()
assert frame["area_sqm_numeric"].gt(0).all()
print(frame[["listing_id", "offer_type", "currency", "price", "area_sqm"]])
Pandas 对于完整的抓取程序是可选的。如果使用此分析片段,请单独安装它。夹具检查包括一个平方英尺的房源和一个平方米的租赁;这两者应该保持为不同的报价类型。
第 5 步:以 CSV 或数据库存储房源
SQLite 示例存储观察时间和 JSON 有效载荷,而不是覆盖每个先前值。用源命名空间扩展生产键,并保留原始源 ID。
推荐的附加字段包括 listing_status, source_capture_time, first_seen, last_seen, parser_version 和 evidence location。CSV 导出是复审文档;历史存储应保留足够的上下文,以便重现比较。
房地产抓取挑战及解决方案
缺失的字段可能反映未披露的数据,而不是解析器失败。区分未知、不适用和被拒绝的值。
多个货币、十进制约定、面积定义、区域卧室规则和房源状态需要特定于来源的标准化。当模板的字段完整性发生变化时发出警报,并在生成误导市场报告之前隔离该运行。
如何建立房地产价格监控管道
建立一个 价格监控管道,安排批准的采集 → 字段验证 → 房源匹配 → 可比观察选择 → 变化检测 → 审查 → 报告。
仅在源身份、报价类型、货币、租赁期和物业基础匹配时计算价格变化。如果面积或标题也更改,调查是否为新单元或更改的解析器导致了明显的变动。
保留观察和变化原因。不要从未经验证的提取中生成警报。
房地产抓取用于市场分析
公开房源样本可以支持描述性研究,但它们并不自动代表所有交易或住房存量。
报告地理、来源覆盖、观察窗口、样本计数、重复方法和排除记录。要价中位数、交易价中位数和每面积租金描述不同的群体。
“观察到的天数”不一定是市场上的总天数,尤其是在重新上架和缺失历史捕获的情况下。收益计算还需要成本和实际可实现的租金,而不仅仅是广告值。
扩展房地产数据收集
遵循 生产收集实践,按批准来源和地理组划分,明确请求预算和可恢复任务。缓存稳定的元数据,更新变化的报价,并监控接受记录的比率。
重复的完整浏览器爬虫的成本可能高于增量许可数据的费用。在扩展城市之前,测量存储、使用的代理流量、被拒绝的字段、实体匹配工作和操作员小时数。
房地产抓取成本:API与自托管
比较每1,000个接受的独特列表的总获取、许可、呈现、存储、匹配、审核和维护成本。
API记录不一定是一个独特的物业。特定来源的ID也无法解决所有跨来源的重复项。在采购估算中包含匹配和审核阶段。
法律、隐私和数据许可考虑
审查门户条款、MLS/IDX协议、政府数据条件、照片权利、保留和再分发。RESO合规性描述的是一种标准,而不是使用每个数据源的权限。
与公共报价相关的地址在与居住者或所有者详细信息结合时,仍可能引发隐私问题。仅收集批准任务所需的位置精度和其他字段。提供者的访问许可可能允许内部分析,而限制公共再发布。
可靠房地产数据管道的最终检查清单
从提供的固定装置开始,然后验证一个具有已知字段定义的授权来源。确认列表身份、单位转换、历史、页面循环处理、拒绝原因和权利,然后再安排。
只有在一个完整更新周期能够区分真实列表变化与缺失数据、部分覆盖和解析漂移后,才可以扩展。
检查批准的物业页面
常见问题
它从允许的来源提取与物业相关的数据,形成带有来源上下文和时间戳的结构化记录。
问:我可以抓取Zillow或Realtor.com吗?
检查他们当前的许可访问和许可安排。公共列表并不建立无限制的自动访问或再分发权利。
不提供。RESO定义标准;MLS或其他数据提供商控制访问和凭证。
不相关。它是在两个包含的合成HTML页面上运行,演示了解析、分页、规范化和存储。
一个物业可能有多个列表或被重新列出。保持身份分离可以防止错误的去重和历史问题。
根据管辖区、所需字段、新鲜度、来源权利、更新/删除行为和接受记录成本进行选择。没有通用的选择。
Lena Zhou
Oct. 19th 2026
支持 JavaScript 渲染,成功率达 99.8%
将任意网站转换为 Markdown、HTML、JSON、链接、PDF 等格式,无需管理爬取基础设施。