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

引言

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

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

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

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

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

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

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

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

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

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

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

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

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

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

请求头完整性要求高 :User-Agent、Referer、Accept-Language等字段缺失或不合法会被直接拒绝。部分接口要求traceparentX-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,数据层做标准化和匹配。

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

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

相关推荐
小静AI工程实验室1 小时前
JS 逆向接口 ID 变了?Python 与 Node.js 复现 JSON 大整数精度丢失
javascript·python·node.js
李航19831 小时前
用 DeepDraw 几何引擎开发建筑设计软件(五):创建选择工具
python·3d
Metaphor6921 小时前
使用 Python 设置 Excel 行列自适应 【代码示例】
python·excel
花酒锄作田10 小时前
FastAPI 使用 session 认证
python·fastapi
lsswear10 小时前
Python 并发 线程
开发语言·python
Ivanqhz11 小时前
MLIR OpBuilder
开发语言·python·mlir
威联通安全存储12 小时前
TS-h2287XU-RP 在家电制造总装与质检数据场景的部署
python·制造
泡泡鱼(敲代码中)13 小时前
Python 字符串 str 完整学习笔记
python·学习
troy12813 小时前
Python 基础语法(八):Web 后端开发、数据分析与可视化、网络爬虫、人工智能 / 大模型应用
前端·python·数据分析