项目背景
很多做数据采集的同学都会遇到一个老问题:到底是一次性把网站的数据全部抓取下来,还是定期只更新新增和变化的部分?
我之前在做二手房市场监测的时候,就碰到过这个选择。当时目标是对比不同城市、不同小区的挂牌房源,看看价格走势和交易活跃度。如果抓取策略不对,不仅会浪费资源,还可能导致数据质量不高。
所以,本文就结合「链家二手房」这个实际站点,聊聊全量抓取和增量采集的取舍,并通过一个实战小项目,展示如何结合代理 IP 技术去实现定期的数据获取和统计。
数据目标
目标字段(示例)
- 基础识别:
house_id(从 URL 或页面特征提取)、title、url - 位置维度:
city、district(区)、bizcircle(商圈/板块)、community(小区名) - 核心指标:
total_price(万元)、unit_price(元/㎡)、area(㎡)、room_type(几室几厅) - 时间戳:
first_seen_at(首次发现时间)、last_seen_at(最后一次看到) - 变更检测:
content_hash(用于判断记录是否变更)
存储设计
- 使用 SQLite(轻量)/ PostgreSQL(生产可选)持久化记录;
- 以
house_id作为主键,配合content_hash实现 幂等写入 与 增量更新; - 定期产出 统计汇总,如“按区/小区的挂牌数量、均价、面积分布”。
统计示例
district维度:挂牌量、平均单价、价格分位community维度:挂牌量 Top N、均价 Top N- 趋势维度(可扩展):每日新增挂牌量、下架量(需引入“消失检测”)
技术选型
- 在数据获取方式上,常见有两种:
- 全量抓取
- 每次任务都从头到尾抓一遍。
- 优点:不会漏数据。
- 缺点:压力大,耗时耗流量,重复数据多。
- 增量采集
- 每次只采集“新增”或“变化”的部分,比如根据发布时间筛选。
- 优点:节省资源,数据更新快。
- 缺点:需要额外逻辑来判断哪些是新数据,哪些是修改过的数据。
我的经验是:
- 前期数据基线不足时,用全量抓取先把底子打好。
- 后期维护阶段,采用增量采集,避免重复抓取大量无效信息。
在网络层面,由于链家有一定的访问频率限制,所以必须结合代理池。这里我选用了爬虫代理服务,支持用户名密码认证,可以减少封禁风险。
模块实现(代码可直接运行/改造)
运行环境:Python 3.10+
安装依赖:pip install requests curl_cffi lxml beautifulsoup4 fake-useragent sqlalchemy pandas apscheduler
0)统一配置(目标入口、代理、数据库)
# -*- coding: utf-8 -*-
"""
项目:贝壳二手房抓取 - 全量 vs 增量
说明:示例代码仅作教学演示,请遵守目标站点条款与 robots.txt。
"""
import os, re, time, random, hashlib, json, datetime as dt
from typing import List, Dict, Optional, Tuple
import requests
from curl_cffi import requests as cffi_requests # 更拟真TLS栈,可在受限站点兜底
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
from sqlalchemy import create_engine, text
import pandas as pd
from apscheduler.schedulers.blocking import BlockingScheduler
# -- 代理(参考:亿牛云爬虫代理 www.16yun.cn)--------
# 请替换为你的真实配置(域名、端口、用户名、密码)
PROXY_HOST = os.getenv("YINIU_HOST", "proxy.16yun.cn") # 示例域名
PROXY_PORT = os.getenv("YINIU_PORT", "3100") # 示例端口
PROXY_USER = os.getenv("YINIU_USER", "16YUN")
PROXY_PASS = os.getenv("YINIU_PASS", "16IP")
PROXY = f"http://{
PROXY_USER}:{
PROXY_PASS}@{
PROXY_HOST}:{
PROXY_PORT}"
PROXIES = {
"http": PROXY, "https": PROXY}
# ------------------ 目标与数据库 ------------------
BASE_URL = "https://www.ke.com/ershoufang/"
CITY = "sh" # 城市简码可按需切换,如:bj、gz、sz;或直接用根入口配合筛选
DB_URL = os.getenv("DB_URL", "sqlite:///houses.db")
engine = create_engine(DB_URL, echo=False, future=True)
# ------------------ 抓取模式开关 ------------------
MODE = os.getenv("MODE", "incremental") # 可选:'full' / 'incremental'
1)建表 & 工具函数(主键、哈希、幂等)
DDL = """
CREATE TABLE IF NOT EXISTS house (
house_id TEXT PRIMARY KEY,
title TEXT,
url TEXT,
city TEXT,
district TEXT,
bizcircle TEXT,
community TEXT,
total_price REAL,
unit_price REAL,
area REAL,
room_type TEXT,
content_hash TEXT,
first_seen_at TEXT,
last_seen_at TEXT
);
CREATE TABLE IF NOT EXISTS cursor_state (
key TEXT PRIMARY KEY,
value TEXT
);
"""
with engine.begin() as conn:
for stmt in DDL.strip().split(";"):
s = stmt.strip()
if s:
conn.execute(text(s))
def sha1(obj: Dict) -> str:
"""对核心字段做内容哈希,用于变更检测。"""
payload = json.dumps(obj, sort_keys=True, ensure_ascii=False)
return hashlib.sha1(payload.encode("utf-8")).hexdigest()
def now_iso():
return dt.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
def load_state(key: str, default: str="") -> str:
with engine.begin() as conn:
r = conn.execute(text("SELECT value FROM cursor_state WHERE key=:k"), {
"k": key}).fetchone()
return r[0] if r else default
def save_state(key: str, value: str):
with engine.begin() as conn:
conn.execute(text("""
INSERT INTO cursor_state(key, value) VALUES(:k, :v)
ON CONFLICT(key) DO UPDATE SET value=excluded.value
"""), {
"k": key, "v": value})
2)请求层
ua = UserAgent()
def get_session(use_cffi: bool=False)


最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



