房产数据对比爬虫:同时爬取链家+贝壳+安居客,做房价横向对比

引言

"同一套房子,在链家上挂牌520万,在贝壳上显示505万,在安居客上又变成了498万------到底哪个价格是真的?"这是无数购房者在看房过程中都会遇到的困惑。

三大房产平台的房源数据各有侧重:链家/贝壳同源但展示维度不同,安居客聚合了更多中小中介的房源。同一小区、同一户型,不同平台的挂牌价可能相差数万甚至数十万。如果能同时爬取三个平台的房源数据,进行交叉对比,就能看清真实的价格区间,识别"虚高挂牌"和"低价引流"的套路。

然而,三个平台的反爬机制各不相同,技术方案无法"一套代码通吃"。本文将系统介绍如何设计适配层架构,突破各平台的反爬体系,实现跨平台房价横向对比。

免责声明:本文仅限技术学习与交流,请勿将爬虫技术用于商业用途或对目标网站造成过大压力。请遵守各平台的robots.txt协议及相关法律法规。

一、三大平台反爬特征对比

在动手写代码之前,必须先搞清楚三个平台分别"防什么"------它们的反爬策略各有侧重,需要区别对待。

1.1 链家/贝壳:JS动态渲染 + 字体反爬

链家与贝壳同属一个体系,数据互通但前端展示不同。其核心反爬手段包括:

动态渲染:房源列表和详情页的核心数据通过JavaScript异步加载,直接请求HTML只能拿到空壳骨架。

字体反爬:部分关键字段(如租金、面积)使用自定义字体映射加密,直接提取DOM文本会得到乱码。

IP频率限制:单IP高频请求会触发封禁,且平台会检测请求行为是否像真人。

行为验证:过于规律的请求间隔、缺少鼠标移动事件、固定User-Agent等,都可能触发滑块验证码。

1.2 安居客:请求头校验 + 动态Cookie

安居客的反爬策略与链家/贝壳有显著差异:

请求头完整性要求高 :User-Agent、Referer、Accept-Language等字段缺失或不合法会被直接拒绝。部分接口要求traceparent、X-K-HEADER等签名参数。

动态Cookie :核心接口需要携带__lg_stoken__、user_trace_token等动态参数,这些参数由前端JS实时生成。

IP频率限制:单IP的请求频率阈值比链家更低,密集请求更容易触发封禁。

页面结构频繁调整:安居客的HTML结构会不定期变化,昨天能用的CSS选择器今天可能就失效了。

1.3 反爬对比速览

维度 链家/贝壳 安居客
核心反爬 字体加密 + JS渲染 动态Token + 请求头校验
IP封禁严格度 极严 极严
数据加载方式 Ajax异步 Ajax异步
签名参数 部分接口 __lg_stoken__等
页面稳定性 相对稳定 频繁调整
破解难度 ⭐⭐⭐⭐ ⭐⭐⭐⭐

二、技术架构设计:适配器模式

面对三个平台差异巨大的反爬策略,一套"大一统"的爬虫代码是不现实的。正确的做法是分层架构 + 平台适配器。

2.1 整体架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      调度层(Scrapy-Redis)                 │
│                   任务队列、去重、分布式扩展                 │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    平台适配层(Adapter Pattern)            │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 链家适配器    │  │ 贝壳适配器    │  │ 安居客适配器  │      │
│  │ Playwright   │  │ Playwright   │  │ requests+    │      │
│  │ +字体解密    │  │ +字体解密    │  │ Token管理    │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                   代理层(站大爷隧道代理)                   │
│              自动IP轮换、地域选择、故障自愈                  │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                   数据层(清洗 + 标准化)                    │
│         统一数据模型、跨平台房源匹配、SQLite存储             │
└─────────────────────────────────────────────────────────────┘

核心优势:各平台采集逻辑相互隔离,修改一个不影响另一个;代理层统一管理IP,所有平台的请求共享同一个隧道代理出口。

2.2 统一数据模型

跨平台对比的关键在于数据标准化。三个平台的字段命名和格式各不相同,需要映射到统一的数据模型:

复制代码
# 统一房源数据模型
standard_house = {
    "platform": "lianjia" | "beike" | "anjuke",
    "house_id": "平台房源ID",
    "title": "房源标题",
    "community": "小区名称",       # 跨平台匹配的关键字段
    "district": "所属区域",
    "total_price": 520.0,          # 总价(万元)
    "unit_price": 58000,           # 单价(元/平米)
    "area": 89.5,                  # 面积(平米)
    "layout": "3室2厅",            # 户型
    "floor": "中楼层",             # 楼层
    "orientation": "南北",         # 朝向
    "decoration": "精装",          # 装修
    "build_year": 2015,            # 建成年份
    "collected_at": "2026-09-15 10:00:00"
}

跨平台对比的核心不是"爬",而是对齐 ------同一套房源在三个平台的ID不同、标题写法不同、面积精度不同,需要通过小区名称 + 户型 + 面积的组合进行模糊匹配。

三、爬虫实战:三平台适配器实现

3.1 环境准备

复制代码
pip install playwright scrapy scrapy-redis requests pandas pillow
playwright install chromium

3.2 链家/贝壳适配器(Playwright方案)

链家/贝壳的核心数据通过JS动态渲染,使用Playwright驱动真实浏览器是最可靠的方案。

复制代码
from playwright.sync_api import sync_playwright
import time
import random
import pandas as pd

class LianjiaAdapter:
    """链家/贝壳平台适配器"""
    
    def __init__(self, use_proxy=True):
        self.use_proxy = use_proxy
        if use_proxy:
            self.proxy_config = {
                "server": "http://用户名:密码@tps.zdaye.com:8080"
            }
        else:
            self.proxy_config = None
        
        self.user_agents = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        ]
    
    def scrape_city(self, city_url, max_pages=10):
        """采集指定城市的二手房数据"""
        results = []
        
        with sync_playwright() as p:
            browser = p.chromium.launch(
                headless=False,
                proxy=self.proxy_config
            )
            context = browser.new_context(
                user_agent=random.choice(self.user_agents),
                viewport={"width": 1440, "height": 900}
            )
            context.add_init_script("""
                Object.defineProperty(navigator, 'webdriver', {
                    get: () => undefined
                });
            """)
            
            page = context.new_page()
            
            for page_num in range(1, max_pages + 1):
                url = f"{city_url}/pg{page_num}/"
                print(f"[链家] 正在采集第{page_num}页...")
                
                page.goto(url, wait_until="networkidle")
                time.sleep(random.uniform(2, 4))
                
                items = page.query_selector_all(".sellListContent li")
                for item in items:
                    try:
                        title = item.query_selector(".title a").inner_text()
                        total_price = item.query_selector(".totalPrice span").inner_text()
                        unit_price = item.query_selector(".unitPrice span").inner_text()
                        house_info = item.query_selector(".houseInfo").inner_text()
                        position = item.query_selector(".positionInfo").inner_text()
                        
                        results.append({
                            "platform": "lianjia",
                            "title": title.strip(),
                            "total_price": float(total_price.replace("万", "")),
                            "unit_price": int(unit_price.replace("元/平米", "").replace(",", "")),
                            "house_info": house_info.strip(),
                            "position": position.strip()
                        })
                    except:
                        continue
                
                time.sleep(random.uniform(2, 5))
            
            browser.close()
        
        return results

3.3 安居客适配器(requests + Token方案)

安居客的接口需要携带动态Token,通过抓包分析可以复用部分固定参数。

复制代码
import requests
import time
import random

class AnjukeAdapter:
    """安居客平台适配器"""
    
    def __init__(self, cookie, use_proxy=True):
        self.session = requests.Session()
        self.cookie = cookie
        self.use_proxy = use_proxy
        
        if use_proxy:
            self.proxies = {
                "http": "http://用户名:密码@tps.zdaye.com:8080",
                "https": "http://用户名:密码@tps.zdaye.com:8080"
            }
        else:
            self.proxies = None
        
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Referer": "https://www.anjuke.com/",
            "Cookie": cookie,
            "Accept": "application/json, text/plain, */*"
        }
    
    def scrape_city(self, city_url, max_pages=10):
        """采集指定城市的二手房数据"""
        results = []
        
        for page_num in range(1, max_pages + 1):
            url = f"{city_url}/p{page_num}/"
            print(f"[安居客] 正在采集第{page_num}页...")
            
            for attempt in range(3):
                try:
                    resp = self.session.get(
                        url,
                        headers=self.headers,
                        proxies=self.proxies,
                        timeout=15
                    )
                    if resp.status_code == 200:
                        # 解析HTML提取房源信息
                        from bs4 import BeautifulSoup
                        soup = BeautifulSoup(resp.text, 'html.parser')
                        items = soup.find_all('div', class_='property-content')
                        
                        for item in items:
                            try:
                                title = item.find('h3').text.strip()
                                price = item.find('span', class_='property-price-total-num')
                                total_price = float(price.text) if price else None
                                
                                results.append({
                                    "platform": "anjuke",
                                    "title": title,
                                    "total_price": total_price,
                                    "raw_html": str(item)[:500]
                                })
                            except:
                                continue
                        break
                    elif resp.status_code in [403, 429]:
                        time.sleep(3 * (attempt + 1))
                except Exception as e:
                    print(f"请求异常: {e}")
                    time.sleep(3)
            
            time.sleep(random.uniform(2, 5))
        
        return results

3.4 统一采集调度

复制代码
class HouseCompareTool:
    """跨平台房价对比工具"""
    
    def __init__(self, lianjia_cookie=None, anjuke_cookie=None):
        self.lianjia = LianjiaAdapter(use_proxy=True)
        self.anjuke = AnjukeAdapter(anjuke_cookie, use_proxy=True)
        self.all_data = []
    
    def collect_all(self, city_config):
        """采集所有平台的房源数据"""
        # 链家
        lianjia_data = self.lianjia.scrape_city(
            city_config["lianjia_url"], 
            max_pages=5
        )
        self.all_data.extend(lianjia_data)
        
        # 安居客
        anjuke_data = self.anjuke.scrape_city(
            city_config["anjuke_url"],
            max_pages=5
        )
        self.all_data.extend(anjuke_data)
        
        # 保存
        df = pd.DataFrame(self.all_data)
        df.to_csv("house_compare.csv", index=False, encoding="utf-8-sig")
        print(f"共采集 {len(self.all_data)} 条房源数据")
        return df


# 使用示例
if __name__ == "__main__":
    tool = HouseCompareTool(anjuke_cookie="你的Cookie")
    config = {
        "lianjia_url": "https://bj.lianjia.com/ershoufang",
        "anjuke_url": "https://beijing.anjuke.com/sale"
    }
    df = tool.collect_all(config)

3.5 跨平台房源匹配

同一套房源在三个平台的标题和描述不同,需要通过小区名称 + 户型 + 面积的组合进行模糊匹配:

复制代码
import re
from difflib import SequenceMatcher

def extract_community(title):
    """从标题中提取小区名称"""
    # 实际规则需根据平台标题格式调整
    patterns = [
        r'^([^\s]+小区)',
        r'^([^\s]+花园)',
        r'^([^\s]+苑)',
    ]
    for pattern in patterns:
        match = re.search(pattern, title)
        if match:
            return match.group(1)
    return title.split()[0] if title else ""

def match_houses(df):
    """跨平台匹配同一房源"""
    matched = []
    
    # 按小区分组
    df["community"] = df["title"].apply(extract_community)
    
    for community, group in df.groupby("community"):
        if len(group) < 2:
            continue
        
        # 同小区内按面积匹配
        for _, row_a in group.iterrows():
            for _, row_b in group.iterrows():
                if row_a["platform"] == row_b["platform"]:
                    continue
                
                # 面积差异在5%以内视为同一房源
                area_a = extract_area(row_a.get("house_info", ""))
                area_b = extract_area(row_b.get("house_info", ""))
                
                if area_a and area_b:
                    diff = abs(area_a - area_b) / max(area_a, area_b)
                    if diff < 0.05:
                        matched.append({
                            "community": community,
                            "platform_a": row_a["platform"],
                            "price_a": row_a["total_price"],
                            "platform_b": row_b["platform"],
                            "price_b": row_b["total_price"],
                            "price_diff": row_a["total_price"] - row_b["total_price"]
                        })
    
    return pd.DataFrame(matched)

四、站大爷隧道代理:跨平台采集的基础设施

4.1 为什么跨平台比价必须用代理?

三个平台都对单IP的请求频率有严格限制。跨平台采集意味着请求量是单平台的3倍------同样的采集任务,IP被封的风险成倍增加。

更棘手的是,不同平台的风控系统可能通过IP关联识别爬虫行为。如果同一个IP在短时间内同时访问链家、贝壳、安居客,平台的风控系统很容易判定这是"批量采集行为"而非"正常用户比价"。

站大爷隧道代理 是解决这一问题的关键。它支持按请求自动切换IP,让每个平台的请求都从不同的IP发出,从根本上规避跨平台关联识别。

4.2 站大爷隧道代理的核心优势

以站大爷隧道代理为例,其在房产数据采集场景中的关键特性包括:

  • 自动切换无感知:每次请求自动更换出口IP,无需手动干预

  • 地域覆盖广泛:覆盖全国99%地域,支持300+城市IP精准定位,方便采集不同城市的房价数据

  • 高可用性:连续7天24小时压力测试,连接成功率稳定在99.3%

  • 三种鉴权模式:白名单、用户名密码/白名单、用户名密码+白名单

  • 高并发支持:分布式爬虫架构下稳定运行

4.3 在Playwright中集成站大爷隧道代理

复制代码
# 站大爷隧道代理配置
PROXY_CONFIG = {
    "server": "http://用户名:密码@tps.zdaye.com:8080"
}

browser = p.chromium.launch(
    headless=False,
    proxy=PROXY_CONFIG
)

4.4 在requests中集成站大爷隧道代理

复制代码
import requests

PROXIES = {
    "http": "http://用户名:密码@tps.zdaye.com:8080",
    "https": "http://用户名:密码@tps.zdaye.com:8080"
}

def fetch_with_tunnel(url, headers, retry=3):
    for attempt in range(retry):
        try:
            resp = requests.get(
                url,
                headers=headers,
                proxies=PROXIES,
                timeout=15
            )
            if resp.status_code == 200:
                return resp.text
            if resp.status_code in [403, 429]:
                print(f"请求被拒绝,隧道代理自动切换IP重试...")
                time.sleep(3 * (attempt + 1))
        except Exception as e:
            time.sleep(3)
    return None

对于需要精细控制IP更换周期的场景,站大爷隧道代理支持通过period参数自定义:

复制代码
# 每300秒更换一次IP
PROXIES = {
    "http": "http://用户名-period-300:密码@tps.zdaye.com:8080",
    "https": "http://用户名-period-300:密码@tps.zdaye.com:8080"
}

五、房价横向对比分析

5.1 同小区价格差异分析

复制代码
import pandas as pd
import matplotlib.pyplot as plt

# 加载匹配后的数据
matched = pd.read_csv("matched_houses.csv")

# 价格差异分布
plt.figure(figsize=(10, 6))
matched["price_diff"].hist(bins=30, color="skyblue", edgecolor="black")
plt.title("跨平台同一房源价格差异分布")
plt.xlabel("价格差异(万元)")
plt.ylabel("房源数量")
plt.savefig("price_diff_distribution.png")

# 统计
print(f"平均价格差异: {matched['price_diff'].abs().mean():.2f}万元")
print(f"最大价格差异: {matched['price_diff'].abs().max():.2f}万元")
print(f"价格差异超过10万的房源占比: {(matched['price_diff'].abs() > 10).mean()*100:.1f}%")

5.2 区域均价对比

复制代码
# 各平台在不同区域的平均单价对比
df = pd.read_csv("house_compare.csv")
df["district"] = df["position"].str.extract(r'([^\s]+区)')

district_price = df.groupby(["district", "platform"])["unit_price"].mean().unstack()
print("各区域不同平台平均单价对比:")
print(district_price.round(0))

# 可视化
district_price.plot(kind="bar", figsize=(14, 6))
plt.title("各区域不同平台二手房平均单价对比")
plt.ylabel("单价(元/平米)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("district_price_comparison.png")

5.3 从对比数据到洞察

通过跨平台房价横向对比,可以获得以下有价值的洞察:

识别价格虚高:如果某房源在链家/贝壳上的挂牌价显著高于安居客同小区同类房源,可能存在"虚高挂牌"的情况。

发现低价房源:部分房源可能只在某一个平台挂出,且价格明显低于市场均价,可能是"捡漏"机会。

平台数据差异:如果某平台的整体均价持续高于其他平台,可能反映该平台的房源结构偏向高端,或者存在系统性的"价格美化"。

六、常见问题与避坑指南

6.1 链家/贝壳字体反爬怎么破?

链家/贝壳的字体加密需要下载字体文件进行映射。建议使用Playwright等待页面完全渲染后提取,或者直接调用平台的价格接口获取明文数据。

6.2 安居客Token过期怎么办?

安居客的动态Token有效期较短。建议每次运行前从浏览器重新获取Cookie,或使用Playwright自动登录并获取最新Token。

6.3 IP被封后如何快速恢复?

  • 使用站大爷隧道代理自动切换IP

  • 控制请求频率,建议单平台请求间隔不低于2秒

  • 不同平台使用不同的IP出口,避免跨平台关联

6.4 跨平台房源匹配准确率低怎么办?

  • 优化小区名称提取规则,结合区域信息进行匹配

  • 使用面积 + 户型 + 楼层的组合进行多维度匹配

  • 对于匹配不上的房源,可以手动审核部分样本

6.5 法律与合规提醒

  • 爬取前阅读各平台的robots.txt及用户协议

  • 控制请求频率,避免对目标服务器造成过大压力

  • 仅将数据用于学习和研究目的,不得用于商业用途

  • 遵守《网络安全法》《数据安全法》《个人信息保护法》等相关法律法规

七、总结

本文从三大房产平台的反爬特征差异入手,系统介绍了跨平台房价横向对比的完整方案。核心要点可以概括为:

挑战 解决方案
链家/贝壳JS渲染 + 字体反爬 Playwright有头模式 + 等待渲染
安居客动态Token + 请求头校验 Cookie复用 + 完整请求头
跨平台IP关联识别 站大爷隧道代理,每个平台独立IP出口
数据字段不统一 统一数据模型 + 字段映射
房源跨平台匹配 小区名称 + 户型 + 面积模糊匹配
房价对比分析 价格差异分布 + 区域均价对比

技术选型速览 :推荐"Playwright(链家/贝壳)+ requests(安居客)+ 站大爷隧道代理"的组合方案。各平台适配器隔离,隧道代理统一管理IP,数据层做标准化和匹配。

三大平台的房价数据各有侧重,单独看任何一家都可能"管中窥豹"。通过跨平台横向对比,可以看清真实的价格区间,识别"虚高挂牌"和"低价引流"的套路,为购房决策提供更全面的数据支撑。

最后需要提醒的是:数据采集行为应当遵循平台规则,控制请求频率以避免对目标服务器造成过载。请遵守相关法律法规,仅将爬虫技术用于学习和研究目的。希望本文能帮助你在房产数据分析的道路上迈出坚实的一步。

相关推荐
5G微创业2 小时前
去水印API如何实现技术变现
python·sdk·短视频
for_ever_love__3 小时前
模块与包——import 机制与 requirements 管理依赖
python·模块·大模型开发·依赖管理
郝学胜-神的一滴3 小时前
AI 编程智能体 05:拆解智能体分级体系、类型与全行业落地场景
开发语言·人工智能·python·程序人生·pycharm
小小张说故事3 小时前
Python 装饰器把函数"搞丢"了?5 个必踩的坑与完整避坑对照表
后端·python
for_ever_love__3 小时前
容器类型全家桶——list、tuple、dict、set 与推导式
python·容器·数据类型·大模型开发
外收内放3 小时前
Python基础语法练习题(43-44)
开发语言·python
10年前端老司机3 小时前
LangChain 五种工具定义方式踩坑总结
python·langchain·llm
坤坤子吖3 小时前
Python基础语法学习:列表和元组
开发语言·笔记·python·学习
zwd20053 小时前
Manim arrange 和 arrange_in_grid 用法详解:buff、aligned_edge、rows/cols(0.21.0 实测)
前端·python·edge
外收内放3 小时前
Python基础语法练习题(拓展2)
python·pygame