尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Scrapy分布式爬虫实战:二手房数据采集与可视化系统

Scrapy分布式爬虫实战:二手房数据采集与可视化系统 简介本资源是一份面向计算机专业本科生及初级开发者的毕业设计论文聚焦基于Scrapy框架的二手房数据分布式爬取系统实践解决链家网房源数据采集质量低、匹配度差等现实问题。全文逾1万字、共40页PDF完整覆盖需求分析、Scrapy-Redis分布式架构设计、XPath精准解析、MongoDB结构化存储、Django可视化界面开发等核心模块并附有反爬策略User-Agent池、动态代理、增量爬取与去重机制等关键技术实现细节。资源为单个1.89MB PDF文件内容组织清晰含摘要、中英文关键词、目录、系统架构图、代码片段截图及测试结果分析便于理论学习与代码复现。目前已有70人学习下载适合Python基础扎实、希望深入理解Web爬虫工程化落地的学生与数据分析师可直接用于毕业设计参考、房地产数据分析项目启动或Scrapy高阶技术进阶学习。1. 为什么用 Scrapy 做二手房数据爬取不是写个 requests BeautifulSoup 就完事了在实际房产数据采集场景中一个典型的城市链家/贝壳页面包含 200 套房源每套页含 15 字段总价、单价、面积、朝向、楼层、装修、挂牌时间、带看次数、经纪人信息且存在分页跳转、AJAX 加载、反爬验证如滑块、验证码前置、动态 iframe 嵌套楼盘详情、以及高频请求触发的 IP 限频。此时若用 requests bs4 手动管理 session、重试、代理轮换、并发控制和去重逻辑3 天能跑通单城市单站点已属高效——但毕业设计答辩要展示的是「系统性工程能力」可扩展、可监控、可复用、可部署。Scrapy 正是为此而生它内置异步调度器、中间件管道、去重指纹生成、自动节流、状态持久化通过 scrapy-redis 实现分布式且天然支持与 Pandas、Plotly、Dash、Redis 可视化工具链对接。本方案面向高校毕业设计场景聚焦「从零搭起一套能过答辩、能讲清架构、能现场演示数据流」的二手房爬取系统不追求黑产级绕过能力而强调结构清晰、参数可控、日志可查、结果可验。2. 搭建 Scrapy 项目骨架从scrapy startproject到可运行的二手房爬虫2.1 初始化项目并确认环境依赖Scrapy 对 Python 版本有明确要求必须使用 Python 3.8–3.11Scrapy 2.10 已弃用 3.7。建议使用虚拟环境隔离依赖python -m venv scrapy-house-env scrapy-house-env\Scripts\activate # Windows # 或 source scrapy-house-env/bin/activate # macOS/Linux pip install --upgrade pip pip install scrapy scrapy-redis pandas plotly dash redis提示不要用pip install scrapy后再单独装scrapy-redis——二者版本需匹配。当前2024 年主流组合推荐scrapy2.10.2scrapy-redis0.7.3该组合已适配 Redis 7.x 的 RESP3 协议避免连接失败。2.2 创建核心爬虫模块spiders/house_spider.py以链家网北京二手房列表页为例URL 示例https://bj.lianjia.com/ershoufang/pg1/定义 Spider 类需覆盖三个关键方法start_requests()生成初始请求支持从 Redis 队列读取起始 URL为分布式铺路parse()解析列表页提取每条房源链接并构造下一页请求parse_detail()解析详情页抽取结构化字段并 yield Item# spiders/house_spider.py import scrapy from scrapy import signals from scrapy_redis.spiders import RedisSpider from scrapy.utils.project import get_project_settings from ..items import HouseItem class LianjiaHouseSpider(RedisSpider): name lianjia_house allowed_domains [lianjia.com] # 分布式模式下start_urls 被忽略由 Redis key: lianjia:start_urls 提供 redis_key lianjia:start_urls classmethod def from_crawler(cls, crawler, *args, **kwargs): spider super().from_crawler(crawler, *args, **kwargs) crawler.signals.connect(spider.spider_idle, signalsignals.spider_idle) return spider def parse(self, response): # 提取当前页所有房源链接 detail_urls response.css(div.info div.title a::attr(href)).getall() for url in detail_urls: yield scrapy.Request( urlresponse.urljoin(url), callbackself.parse_detail, meta{source_url: response.url} # 记录来源页用于溯源分析 ) # 构造下一页链接仅本地调试时启用分布式下由外部任务调度 next_page response.css(div.page-box a.next::attr(href)).get() if next_page and pg in next_page: yield response.follow(next_page, self.parse) def parse_detail(self, response): item HouseItem() item[url] response.url item[title] response.css(h1.main-title::text).get().strip() item[price_total] response.css(span.total::text).re_first(r(\d\.?\d*)) or 0 item[price_unit] response.css(span.unit::text).re_first(r(\d\.?\d*)) or 0 item[area] response.css(div.area div.mainInfo::text).re_first(r(\d\.?\d*)) or 0 item[direction] response.css(div.orientation div.mainInfo::text).get().strip() item[floor] response.css(div.floor div.mainInfo::text).get().strip() item[renovation] response.css(div.renovation div.mainInfo::text).get().strip() item[publish_time] response.css(div.transaction div.content ul li:nth-child(1) span.label::text).get() item[broker_name] response.css(div.brokerName a.name::text).get().strip() item[broker_company] response.css(div.brokerCompany a::text).get().strip() item[crawl_time] response.meta.get(download_time, ) yield item关键参数说明redis_key lianjia:start_urlsScrapy-Redis 会监听该 Redis List外部程序如脚本或调度器向其中LPUSHURL 即可触发爬取实现「解耦式任务分发」。meta{source_url: ...}保留列表页来源后续可用于统计「某页平均抓取成功率」或「链接失效率」是答辩中体现「数据质量监控意识」的重要细节。response.follow(...)比scrapy.Request(url...)更安全自动处理相对路径和重定向。2.3 定义数据结构items.py中声明字段类型与约束Item 不是简单字典而是具备字段校验能力的数据容器。为适配后续 Pandas 清洗与可视化需预设类型转换逻辑# items.py import scrapy from scrapy.loader.processors import TakeFirst, MapCompose, Join from w3lib.html import remove_tags def clean_price(value): 清洗价格字符串转为 float if not value: return 0.0 try: return float(.join(filter(str.isdigit, value)) or 0) / 10000 # 万元单位 except: return 0.0 def clean_area(value): 清洗面积单位统一为平方米 if not value: return 0.0 try: return float(.join(filter(lambda x: x.isdigit() or x ., value))) except: return 0.0 class HouseItem(scrapy.Item): url scrapy.Field( output_processorTakeFirst() ) title scrapy.Field( input_processorMapCompose(remove_tags, str.strip), output_processorTakeFirst() ) price_total scrapy.Field( input_processorMapCompose(clean_price), output_processorTakeFirst() ) price_unit scrapy.Field( input_processorMapCompose(clean_price), output_processorTakeFirst() ) area scrapy.Field( input_processorMapCompose(clean_area), output_processorTakeFirst() ) direction scrapy.Field( input_processorMapCompose(remove_tags, str.strip), output_processorTakeFirst() ) floor scrapy.Field( input_processorMapCompose(remove_tags, str.strip), output_processorTakeFirst() ) renovation scrapy.Field( input_processorMapCompose(remove_tags, str.strip), output_processorTakeFirst() ) publish_time scrapy.Field( input_processorMapCompose(remove_tags, str.strip), output_processorTakeFirst() ) broker_name scrapy.Field( input_processorMapCompose(remove_tags, str.strip), output_processorTakeFirst() ) broker_company scrapy.Field( input_processorMapCompose(remove_tags, str.strip), output_processorTakeFirst() ) crawl_time scrapy.Field( output_processorTakeFirst() )注意MapCompose是链式处理器按顺序执行每个函数TakeFirst()确保只取第一个非空值避免因 HTML 结构微调导致字段为空。这些设计直接支撑答辩时「如何保证数据一致性」的问题应答。3. 实现分布式爬取Scrapy-Redis Redis 配置与任务分发机制3.1 配置settings.py启用 Redis 调度与去重Scrapy 默认使用内存队列无法跨进程共享。启用分布式需三处关键配置# settings.py # —— 启用 Redis 调度器 —— SCHEDULER scrapy_redis.scheduler.Scheduler DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter SCHEDULER_PERSIST True # 爬虫停止后请求队列不被清空 SCHEDULER_FLUSH_ON_START False # 启动时不自动清空队列便于断点续爬 # —— Redis 连接配置 —— REDIS_URL redis://127.0.0.1:6379/0 # 支持密码redis://:password127.0.0.1:6379/0 # 或细粒度配置 # REDIS_HOST 127.0.0.1 # REDIS_PORT 6379 # REDIS_PARAMS {password: yourpass} # —— 并发与节流 —— CONCURRENT_REQUESTS 16 # 单机并发数分布式下各节点独立计数 DOWNLOAD_DELAY 1.5 # 请求间隔秒防触发限频 RANDOMIZE_DOWNLOAD_DELAY True # 在 ±0.5*DELAY 范围内随机抖动 AUTOTHROTTLE_ENABLED True # 自适应节流根据响应延迟动态调整并发 AUTOTHROTTLE_START_DELAY 1.0 AUTOTHROTTLE_MAX_DELAY 3.0 # —— 中间件启用 —— DOWNLOADER_MIDDLEWARES { scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: None, scrapy_user_agents.middlewares.RandomUserAgentMiddleware: 400, # 需 pip install scrapy-user-agents scrapy.downloadermiddlewares.retry.RetryMiddleware: 500, }参数含义与答辩话术SCHEDULER_PERSIST True答辩时可演示「关掉爬虫10 分钟后再启动继续从上次中断处抓取」体现工程鲁棒性AUTOTHROTTLE_*不是简单设个 delay而是让 Scrapy 根据服务器响应时间自动调节并发比固定 delay 更符合真实业务场景RandomUserAgentMiddleware避免 User-Agent 单一被识别为爬虫比硬编码 UA 字符串更专业。3.2 启动 Redis 服务并注入初始任务Windows 下可直接下载 Redis for Windows 解压后运行redis-server.exe redis.windows.conf。macOS/Linux 推荐用 Homebrew 或 apt 安装# macOS brew install redis brew services start redis # Linux (Ubuntu) sudo apt update sudo apt install redis-server sudo systemctl enable redis-server sudo systemctl start redis-server验证 Redis 是否就绪redis-cli ping # 应返回 PONG redis-cli llen lianjia:start_urls # 查看任务队列长度向 Redis 注入起始 URL模拟分布式任务分发# 方式一命令行直接推送 redis-cli lpush lianjia:start_urls https://bj.lianjia.com/ershoufang/pg1/ # 方式二Python 脚本批量注入答辩演示用 # utils/seed_urls.py import redis r redis.from_url(redis://127.0.0.1:6379/0) urls [fhttps://bj.lianjia.com/ershoufang/pg{i}/ for i in range(1, 6)] for url in urls: r.lpush(lianjia:start_urls, url) print(f已推送 {len(urls)} 条起始 URL)提示答辩现场可打开 Redis Desktop Manager或redis-cli monitor实时观察lianjia:start_urls队列被消费、lianjia:requests和lianjia:dupefilter键动态变化直观展示「分布式协同」过程。3.3 多节点部署同一项目在不同机器上运行假设你有两台测试机A 和 B均安装好 Python 环境与依赖且能访问同一台 Redis 服务器机器操作作用A 机器scrapy crawl lianjia_house启动主爬虫实例监听lianjia:start_urlsB 机器scrapy crawl lianjia_house启动第二实例自动从同一队列取任务无需修改代码Scrapy-Redis 会自动协调两个实例Redis List 是 FIFO 队列BRPOP命令保证任务被唯一消费RFPDupeFilter使用 Redis Set 存储请求指纹scrapy.utils.request.request_fingerprint()生成全局去重所有 Item 统一存入 Redis 的lianjia:itemsList默认或配置ITEM_PIPELINES写入 MySQL/CSV。验证多节点效果在 A 机器启动爬虫观察redis-cli llen lianjia:start_urls从 5 降为 0在 B 机器启动爬虫观察redis-cli llen lianjia:items持续增长且 A/B 日志中均有Crawled记录 —— 证明负载已被分摊。4. 数据落地与可视化从 Redis Items 到可交互大屏4.1 定义 Pipeline将爬取结果写入 CSV Redis SQLite三选一Pipeline 是 Scrapy 的数据出口必须在settings.py中启用# settings.py ITEM_PIPELINES { house.pipelines.HouseCsvPipeline: 300, house.pipelines.HouseRedisPipeline: 310, house.pipelines.HouseSqlitePipeline: 320, }CSV Pipeline适合答辩快速出数据# pipelines.py import csv import os from scrapy.exporters import CsvItemExporter class HouseCsvPipeline: def __init__(self): self.files {} def open_spider(self, spider): # 每个 Spider 对应一个文件 filename f{spider.name}_data.csv self.file open(filename, wb) self.exporter CsvItemExporter(self.file, encodingutf-8-sig) self.exporter.start_exporting() def close_spider(self, spider): self.exporter.finish_exporting() self.file.close() def process_item(self, item, spider): self.exporter.export_item(item) return item注意encodingutf-8-sig解决 Excel 打开中文乱码问题start_exporting()必须在open_spider中调用否则首行无表头。SQLite Pipeline轻量持久化支持后续 SQL 查询import sqlite3 from scrapy import signals class HouseSqlitePipeline: def __init__(self): self.conn None self.cursor None def open_spider(self, spider): self.conn sqlite3.connect(house.db) self.cursor self.conn.cursor() self.cursor.execute( CREATE TABLE IF NOT EXISTS house_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT, title TEXT, price_total REAL, price_unit REAL, area REAL, direction TEXT, floor TEXT, renovation TEXT, publish_time TEXT, broker_name TEXT, broker_company TEXT, crawl_time TEXT ) ) self.conn.commit() def close_spider(self, spider): self.conn.close() def process_item(self, item, spider): self.cursor.execute( INSERT INTO house_items (url, title, price_total, price_unit, area, direction, floor, renovation, publish_time, broker_name, broker_company, crawl_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , ( item.get(url), item.get(title), item.get(price_total), item.get(price_unit), item.get(area), item.get(direction), item.get(floor), item.get(renovation), item.get(publish_time), item.get(broker_name), item.get(broker_company), item.get(crawl_time) )) self.conn.commit() return item4.2 用 Pandas 清洗数据并生成基础统计图表爬取完成后house.db中已有结构化数据。用 Jupyter Notebook 快速生成答辩所需图表# analysis/eda.ipynb import pandas as pd import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots # 读取 SQLite 数据 conn sqlite3.connect(house.db) df pd.read_sql_query(SELECT * FROM house_items WHERE price_total 0 AND area 0, conn) conn.close() # 基础统计 print(f共采集 {len(df)} 条有效房源) print(df[[price_total, area, price_unit]].describe()) # 图表 1总价分布直方图带箱线图 fig1 px.histogram(df, xprice_total, nbins50, title北京二手房总价分布万元) fig1.update_layout(xaxis_title总价万元) # 图表 2单价 vs 面积散点图颜色映射装修情况 fig2 px.scatter(df, xarea, yprice_unit, colorrenovation, title单价与面积关系按装修分类, labels{area: 面积㎡, price_unit: 单价元/㎡}) fig2.update_traces(markerdict(size6)) # 图表 3各行政区房源数量需从 title 中提取示例用正则 import re df[district] df[title].str.extract(r(.?)区) district_count df[district].value_counts().head(10) fig3 px.bar(district_count, titleTOP 10 行政区房源数量, labels{value: 数量, index: 行政区}) # 导出为 HTML答辩可直接打开 fig1.write_html(report/price_hist.html) fig2.write_html(report/price_area_scatter.html) fig3.write_html(report/district_bar.html)提示答辩 PPT 中可嵌入这三张图重点讲解「为什么单价与面积呈负相关」「装修等级对价格的影响是否显著」「哪些区域是供应主力」——体现数据分析思维而非单纯爬虫技术。4.3 构建简易可视化大屏Dash 框架快速搭建交互界面Dash 是 Python 生态中最易上手的 Web 可视化框架无需前端知识即可部署# dashboard/app.py import dash from dash import dcc, html, Input, Output, State, callback import plotly.express as px import pandas as pd import sqlite3 app dash.Dash(__name__) # 读取数据实际部署应加缓存或定时刷新 conn sqlite3.connect(house.db) df pd.read_sql_query(SELECT * FROM house_items WHERE price_total 0, conn) conn.close() app.layout html.Div([ html.H1(北京二手房数据可视化大屏, style{textAlign: center}), html.Div([ html.Label(选择分析维度), dcc.RadioItems( idanalysis-type, options[ {label: 总价分布, value: price_total}, {label: 单价分布, value: price_unit}, {label: 面积分布, value: area} ], valueprice_total, inlineTrue ), ], style{textAlign: center, margin: 20px}), dcc.Graph(idmain-graph), html.Div(idstats-summary, style{margin: 20px, fontSize: 18px}), ]) callback( Output(main-graph, figure), Output(stats-summary, children), Input(analysis-type, value) ) def update_graph(column): fig px.histogram(df, xcolumn, nbins30, titlef{column} 分布) fig.update_layout(xaxis_titlecolumn.replace(_, ).title()) stats df[column].describe() summary f均值{stats[mean]:.2f} | 中位数{stats[50%]:.2f} | 最大值{stats[max]:.2f} return fig, summary if __name__ __main__: app.run_server(debugTrue, host0.0.0.0, port8050)运行python dashboard/app.py浏览器访问http://localhost:8050即可看到交互式大屏。答辩时可现场切换维度、拖动直方图缩放展示「数据驱动决策」能力。5. 答辩高频问题应对与关键参数调优技巧5.1 如何解释「为什么不用 Playwright/Selenium 处理动态 iframe」这是一个必问点。标准回答应体现技术权衡意识“链家/贝壳的房源列表页是静态 HTML详情页主体内容也是服务端渲染仅部分组件如地图、VR 看房使用 iframe 加载。我们实测发现response.css()可稳定提取标题、价格、面积等核心字段而 iframe 内容如经纬度、小区实景非毕业设计核心指标且加载慢、稳定性差。若真需 iframe 数据Scrapy 可配合scrapy-splash或scrapy-playwright但会显著增加部署复杂度与资源消耗。本方案坚持‘够用、可控、可讲清’原则优先保障主干流程健壮性。”附实操验证命令答辩前自测# 抓取详情页源码检查关键字段是否存在 curl -s https://bj.lianjia.com/ershoufang/101102738054.html | grep -E (total|main-title|unit) | head -5 # 输出应含 span classtotal850/span、h1 classmain-title...、span classunit85000/span5.2 分布式爬虫的瓶颈在哪如何横向扩展瓶颈不在 Scrapy 本身而在三处瓶颈环节表现优化手段Redis 单点llen lianjia:start_urls持续 10000消费延迟升高升级 Redis 为集群模式Redis Cluster或改用 Kafka 替代队列目标站限频403/503响应增多retry_times达上限在settings.py中启用ROTATING_PROXY如 scrapy-rotating-proxies或接入商业代理池Item 处理 IOHouseSqlitePipeline写入变慢CPU 占用低、IO Wait 高改用pymysql写入 MySQL或启用sqlite3WAL 模式PRAGMA journal_modeWAL;关键参数调优对照表答辩问答速查参数名默认值推荐值二手房场景调整依据CONCURRENT_REQUESTS168单机/ 4云服务器防止目标站 TCP 连接耗尽DOWNLOAD_DELAY01.5–2.5 秒匹配链家平均响应时间实测 1.2s留出缓冲RETRY_TIMES23二手房页面偶发 CDN 缓存失效需重试REDIRECT_MAX_TIMES205防止重定向链过长如登录跳转拖慢流程FEED_EXPORT_ENCODINGutf-8utf-8-sig确保 CSV 被 Excel 正确识别中文5.3 如何证明爬取结果真实有效提供可验证的校验方法答辩时最有力的证据是「可复现的抽样验证」URL 抽样回访从house.db中随机取 5 条url用浏览器手动打开对比页面显示价格与数据库记录是否一致字段完整性检查运行以下 SQL确认关键字段缺失率 5%SELECT 100.0 * COUNT(*) / (SELECT COUNT(*) FROM house_items) AS missing_rate FROM house_items WHERE price_total 0 OR area 0 OR title ;时间戳合理性验证检查publish_time是否符合常识如不出现 2030 年挂牌SELECT publish_time, COUNT(*) c FROM house_items WHERE publish_time GLOB 20[2-3][0-9]% GROUP BY publish_time ORDER BY c DESC LIMIT 5;这些操作均可在答辩现场用 SQLite Browser 工具 30 秒内完成演示比口头承诺更有说服力。最后一步把scrapy crawl lianjia_house -s LOG_FILEscrapy.log的日志截取关键段含Crawled,Scraped,Filtered行连同house.db文件、report/下的 HTML 图表、dashboard/的运行截图打包为house-system-demo.zip—— 这就是你答辩时最扎实的「可运行证据包」。本文还有配套的精品资源点击获取
返回列表