ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

爬虫房源数据分析系统:从数据采集到可视化的完整实践

爬虫房源数据分析系统:从数据采集到可视化的完整实践 简介本资源是一套完整的本科毕业设计级Python数据分析项目面向计算机、数据科学及相关专业学生聚焦二手房市场数据采集与智能分析场景助力完成课程设计、毕设开题及实战能力提升。项目基于RequestsBeautifulSoup实现南京链家二手房全量数据爬取经Pandas清洗后利用Matplotlib与Numpy完成多维度可视化并集成KMeans聚类算法实现房源智能分群输出可直接用于购房决策的特征规律总结。压缩包共159个文件含18个核心Python脚本爬虫、清洗、分析、绘图、18个CSV数据文件原始与清洗后多编码版本、65张分析图表PNG、15个HTML报告页及配套JS交互代码整体39.98MB结构清晰、模块解耦便于理解数据流与算法落地全过程。已有172人学习下载提供从网页抓取到聚类解读的端到端可运行方案附带完整字段说明与版本化数据文件显著降低复现门槛与调试成本。 去年做毕业设计的时候我选了基于爬虫的房源数据分析系统这个题目。说实话一开始以为就是写几个爬虫脚本、跑几个统计图表的事儿真正做进去才发现这个课题横跨了爬虫工程、数据治理、特征工程和可视化表达四个环节任何一个环节偷懒最终呈现出来的系统都会显得很玩具。我花了两周时间踩坑、重构、再封装最后把整个系统完整跑通才算是真正理解了——房源数据分析这类题目核心难点从来不在爬虫本身而在数据拿到手之后怎么处理、怎么挖掘出有说服力的结论。这篇文章我把自己从选题调研、技术选型、爬虫实现、数据清洗到分析可视化的完整过程做一个复盘把关键代码的设计思路、反爬应对策略、数据标准化的坑、可视化的选型逻辑都摊开讲清楚。如果你正在做类似的毕业设计或者想自己动手做一个垂直领域的数据分析项目这篇文章可以作为一份完整的参考路线。1. 选题调研与数据源评估为什么选房源数据以及数据从哪来1.1 为什么房源是爬虫类毕设的优质题材选这个题之前我对比过好几个方向电商商品评论、影视评分、招聘岗位信息最后还是敲定了房源数据。原因有三点数据结构相对规整房源信息天然具备结构化特征——小区名、户型、面积、朝向、楼层、总价、单价、所在区域这些字段明确、语义清晰非常适合做清洗和分析。对比之下评论类数据是非结构化文本清洗工作会占用大量精力。分析维度丰富房源数据既能做整体价格分布、区域对比这类基础统计也能做面积-价格关系、户型-总价分布、楼层/朝向对价格的影响这类交叉分析甚至还能做简单的价格预测非常容易做出层次感。可验证性强房价是大家有感知的数据分析结果是否符合常理一眼就能看出来。比如核心城区均价高于郊区小户型单价通常高于大户型这些常识通过数据挖掘出来会很有说服力出了问题也好纠偏。1.2 数据源评估与合法性边界确定方向之后我梳理了可用的数据源。数据源数据类型反爬难度数据结构建议链家/贝壳二手房/租房中高字段齐全首选数据质量高安居客二手房/租房高字段较全备选反爬较严房天下二手房中字段较全可选58同城租房为主中字段较乱不推荐做二手我爱我家二手房中字段较全可选我在实际开发中选择了链家作为主要数据源原因很直接链家的房源页面是服务端渲染的HTML结构清晰字段名称规范而且列表页有明确的翻页规则。相比之下安居客的反爬策略复杂页面结构变动频繁对毕设项目来说维护成本太高。提示爬虫必须要守边界。我全程只抓取了公开可见的房源列表信息控制请求频率没有采集任何用户个人信息也没有将数据用于任何商业用途。做毕设的同学务必注意爬虫是手段不是目的合规是第一位的。1.3 确定抓取范围与样本规模数据量也需要提前规划。我最初的计划是抓取某个二线城市全量二手房数据跑了一遍发现一个二线城市在链家上的在售房源大约在 1.5 万到 3 万套之间这个量级对于毕设分析其实不太够——区域间对比时个别区域可能只有几百套数据很容易被一两套豪宅拉高均价。所以我把范围扩展到了两个核心城区 两个近郊城区目标样本量定在 8000 套以上。这个规模既能保证每个区域都有足够样本又不至于因为数据量过大导致爬虫时间过长、被封号的风险增加。2. 架构设计与技术选型从零搭一套可扩展的爬虫分析系统2.1 模块化设计的思路很多同学做毕设爬虫习惯写一个 Python 脚本从头跑到尾requests 请求页面 - 解析数据 - 存 CSV - 换个城市改参数再跑一遍。这样当然能跑通但后期扩展和排错会非常痛苦。我做的是模块化拆分整个系统分成四个独立的部分采集层负责发送 HTTP 请求、处理 Cookie/Session、应对反爬。独立成模块方便后期加代理池、加请求重试。解析层负责从 HTML 中提取目标字段。用 XPath/CSS 选择器做字段映射页面结构变化时只需要改这一层。存储层定义一个统一的字段 Schema把所有来源的数据标准化后写入 SQLite/MySQL。分析与展示层基于 pandas 做数据清洗和聚合分析用 ECharts / pyecharts 输出可视化报告。这种分层设计还有一个好处每一层都可以独立测试。比如解析层可以先拿本地保存的 HTML 文件离线调试不用每次调试都发真实请求既快又不容易触发反爬。2.2 requests 还是 Scrapy我的选择是先用 requests 跑通再考虑 Scrapy关于爬虫框架网上争论很多。我的实际经验是毕业设计项目用 requests BeautifulSoup/lxml 完全够用Scrapy 的优势主要体现在大规模分布式采集场景对 2 万条数据体量来说它的学习成本反而会拖慢进度。我用的是requests.Session()。Session 对象可以保持 TCP 连接复用连续请求时能明显减少握手开销同时也能在同一个会话中维持 Cookie。核心请求代码大致是import requests from fake_useragent import UserAgent ua UserAgent() session requests.Session() session.headers.update({ User-Agent: ua.random, Accept-Language: zh-CN,zh;q0.9, Referer: https://www.lianjia.com/ }) def fetch_page(url, retry3): for i in range(retry): try: resp session.get(url, timeout10) if resp.status_code 200: return resp.text elif resp.status_code 403: time.sleep(5) continue except requests.exceptions.RequestException as e: time.sleep(2) continue return Nonefake_useragent库会自动随机切换 User-Agent虽然只是一个很小的细节但在实际采集中的效果非常好——如果一直用一个默认 UA基本上请求几十个页面之后就会触发风控。2.3 数据字段 Schema 设计在写第一个爬虫脚本之前我建议先设计好数据 Schema这会让后续工作顺畅很多。我的字段表是这样的字段名类型说明示例house_idSTRING房源唯一标识1021XXXXXXXXtitleSTRING标题万科城 3室2厅 南北通透regionSTRING所属行政区朝阳区biz_circleSTRING商圈望京communitySTRING小区名融科橄榄城layoutSTRING户型3室2厅areaFLOAT建筑面积(㎡)126.5orientSTRING朝向南北floorSTRING楼层信息低楼层/共 28 层decorationSTRING装修情况精装total_priceFLOAT总价(万元)1280unit_priceFLOAT单价(元/㎡)101 156crawled_atDATETIME抓取时间2026-01-15设计 Schema 时有一个多年经验提炼出来的建议所有原始字段先用字符串存储清洗时再做类型转换。不要试图在解析阶段一次性转好类型——页面里经常有暂无数据、南北通透混在数字列里的情况你会在清洗阶段统一处理而不是在解析层做异常判断否则解析代码会膨胀得很难维护。3. 爬虫核心实现翻页逻辑、数据解析与反爬应对的三层博弈3.1 翻页策略从 URL 规律到动态加载链家二手房列表页的翻页规则比较友好URL 参数非常规整第1页: https://bj.lianjia.com/ershoufang/pg1/ 第2页: https://bj.lianjia.com/ershoufang/pg2/ 第N页: https://bj.lianjia.com/ershoufang/pgN/构造翻页 URL 很简单但实际爬的时候要注意一个问题不同区域有独立的列表页。比如全城列表只有前 100 页而朝阳区有 50 页、昌平区有 18 页全城看不到的房源在区域列表页可以获取到。所以我爬虫的策略是先爬区域页base_url https://{city}.lianjia.com/ershoufang/{region}/pg{page}/用region参数替换不同的城区代码可以绕开全城列表的总页数限制。我实测下来单区域最大页数约 100 页每页 30 套单区域能拿到约 3000 套房源数据四个区域加起来完全能够满足 8000-10000 套的样本目标。3.2 解析逻辑XPath 与 CSS 选择器怎么选解析层我用了 lxml 的 XPath而不是 BeautifulSoup。原因只有一个XPath 在复杂嵌套结构中的表达能力更强。比如要提取某个div下的第三个span的文本XPath 一句//div[classinfo]/span[3]/text()就能拿到BeautifulSoup 的 find_all 嵌套要写好几行。单个房源卡片的核心解析逻辑from lxml import etree def parse_house_cards(html): tree etree.HTML(html) cards tree.xpath(//*[idcontent]/div[1]/ul/li) for card in cards: item {} title_el card.xpath(.//div[classtitle]/a/text()) item[title] title_el[0].strip() if title_el else # 户型/面积/朝向等字段通常在 div.positionInfo 里 pos_info card.xpath(.//div[classpositionInfo]/text()) if pos_info: parts pos_info[0].split(|) item[layout] parts[0].strip() item[area] parts[1].replace(平米, ).strip() item[orient] parts[2].strip() item[decoration] parts[3].strip() if len(parts) 3 else # 总价在 div.totalPrice 里 price_el card.xpath(.//div[classtotalPrice]/span/text()) item[total_price] price_el[0] if price_el else ...解析时最重要的经验是不要一次写一大段解析代码就去跑真实页面。先把整个列表页 HTML 保存到本地用本地文件调试解析逻辑等解析结果完全正确了再连线上。这样能避免陷入反爬封锁 - 换代理 - 再跑 - 又需要改解析的连环坑。3.3 反爬应对验证码、IP 封禁与频率控制链家的反爬并不是铁板一块但也不能掉以轻心。我实际遇到的场景主要有三类第一类是请求频率过高导致 IP 被临时封禁。表现是连续请求 30-50 页之后突然返回一个安全验证页面状态码是 200 但页面内容和正常页面完全不同。应对方案是说起来很简单的一个道理控制节奏。import time import random def polite_sleep(): time.sleep(random.uniform(1.5, 3.5))这个随机延时是最有效的策略没有之一。你不需要用代理池不需要复杂算法老老实实每请求一页停 1.5-3.5 秒被封的概率会低非常多。整套数据爬下来实测耗时约 35-40 分钟完全在可接受范围内。第二类是 Cookie 失效。链家部分页面需要有效的 Cookie 才能访问我的做法是先用浏览器访问一次目标页面手动过掉安全验证然后把浏览器中的 Cookie 字符串复制到爬虫配置里。这样只要 Cookie 过期之前跑完数据即可。第三类是请求头校验。有些服务器会校验 User-Agent、Referer 和 Accept 头的一致性fake_useragent配合合理的 Referer 设置基本能通过。注意如果遇到返回 403 且页面显示滑块验证最稳妥的做法是停止采集。绝对不要用第三方打码平台或者自动滑块工具这类手段已经超出了学术用途的边界对毕设来说风险远大于收益。3.4 增量与去重保证数据质量的最后一道防线房源列表页会动态更新翻页爬取时可能出现同一套房源在多个页面重复出现。我的做法是在存储层做house_id唯一键约束CREATE TABLE house ( house_id TEXT PRIMARY KEY, title TEXT, region TEXT, biz_circle TEXT, community TEXT, layout TEXT, area TEXT, orient TEXT, floor TEXT, decoration TEXT, total_price TEXT, unit_price TEXT, crawled_at TEXT );插入时用INSERT OR IGNORE重复的house_id自动跳过。另外爬虫是分批次跑的我设计了增量追加模式——每次运行前先读取已有表里的house_id集合新批次的数据过滤掉已存在的 ID只插入新增部分。这种增量式爬取的思想在毕设答辩里也是一个不错的加分点。4. 数据清洗与特征工程原始数据到分析表的必经之路4.1 字段标准化字符串里挖出结构化信息爬到的原始数据里很多字段不是干净的数字。比如area字段解析出来可能是 126.5平米 这样的字符串floor是 中楼层/共24层 这样的复合信息layout是 3室2厅。这些需要用正则表达式或字符串分割做标准化。import re def clean_area(raw): # 126.5平米 - 126.5 if isinstance(raw, str): match re.search(r([\d.]), raw) return float(match.group(1)) if match else None return raw def parse_floor(raw): # 中楼层/共24层 - (中楼层, 24) if / in raw: floor_level raw.split(/)[0].strip() total re.search(r(\d), raw.split(/)[1]) return floor_level, int(total.group(1)) if total else None return raw, None这类清洗规则不是靠脑子想出来的而是从真实数据里扫出来的。我会先打印所有不满足规则的异常样本再写正则去覆盖这些样本。比如有的房源面积带暂无数据、有的户型是5室3厅6卫这种复杂结构不先看异常样本后续统计很容易因为解析错误产生偏差。4.2 异常值与缺失值处理策略清洗环节最难的其实不是规则而是如何判断一个值是异常值。我总结了几条实用经验单价异常如果某套房源单价只有周边均价的五分之一大概率是车位或者地下仓库混进来了。这类数据直接剔除或者在分析时按价格区间过滤。面积异常面积小于 10㎡ 的房源通常不是住宅。1000㎡ 以上的可能是整栋商业楼这两种极端值都建议剔除或单独标注。总价缺失链家页面偶尔会出现 暂无数据 的总价这种记录直接删除。我实际清洗后的数据量从原始 8600 条降到了 7900 条左右约 8% 的损耗这个损耗率在正常范围内。4.3 构造衍生特征让分析维度更丰富清洗完成后我构造了三个衍生字段这些字段是后续分析的核心维度unit_price_round把单价按每 5000 元为一个分箱便于做价格区间分布统计。area_group把面积分段例如50㎡、50-80㎡、80-100㎡、100-140㎡、140㎡以上用来做面积段交叉分析。is_elevator根据floor中的总层数判断是否有电梯总楼层 6 层认为有电梯用于分析电梯房 vs 楼梯房的价格差异。def make_area_group(area): if area 50: return 50㎡ if area 80: return 50-80㎡ if area 100: return 80-100㎡ if area 140: return 100-140㎡ return ≥140㎡这些特征本身不复杂但能让分析部分有话可说——你不能只输出一个平均房价是多少的统计而应该告诉读者面积在 80-100㎡ 的房源单价平均比 50㎡ 以下的低 15% 左右这样的结论有了衍生特征才能做这种交叉分析。5. 数据分析从价格分布到交叉维度的挖掘逻辑5.1 整体价格分布先建立基础认知拿到清洗后的数据第一步不是急着画图而是跑基础统计量import pandas as pd df pd.read_sql(SELECT * FROM house, conn) print(df.describe())重点关注几个指标总价均值、总价中位数、单价均值、单价中位数。均值和中位数的差异非常有信息量——如果均值明显高于中位数说明市场上有少量极高价格的房源把均值拉高了这时候分析平均数就不如实锤中位数更有代表性。我跑出来的结果是单价均值约 52 400 元/㎡中位数约 48 600 元/㎡右偏分布明显。这说明高端盘虽然数量少但对市场平均水平的拉动作用不容忽视。这个结论也提示我在后续做区域对比时应该同时给出均值和分位数而不是只给一个数。5.2 区域价格对比用箱线图替代简单的柱状图区域价格对比是房源分析里含金量最高的一个维度但也是很多毕设做得最肤浅的地方——就画一根柱状图A 区均价 6 万B 区均价 4 万太薄了。我做区域对比时用了箱线图因为箱线图能展示每个区域的价格分布形态而不只是平均值。同一个区域内部是大家都差不多还是有大量低价老破小 少量千万豪宅的极端分化箱线图一眼就能看出来。区域样本量均价(元/㎡)25分位中位数75分位A区213062 30051 00058 20069 800B区185058 90047 50055 30066 100C区147043 20036 80042 10048 500D区245038 50031 20036 90044 300这张表格比柱状图的信息量大多了。可以看到A 区和 B 区的 75 分位都接近 7 万说明这两个区域的改善型房源和高端盘集中C 区和 D 区的价格则相对平稳。在这个基础上再结合商圈粒度做细分分析结论的深度就出来了。5.3 面积-总价关系回归拟合揭示市场的基本规律面积和总价的关系是房源分析里最有数学味道的一块。我用线性回归拟合了面积(X)对总价(Y)的影响并计算了 R² 值。实际跑出来的结果是面积与总价的皮尔逊相关系数约 0.73线性回归 R² 约 0.53。这是什么概念呢——面积能解释总价变异的 53%剩下 47% 的差异来自地段、楼层、朝向、装修等其他因素。这个结论放到答辩里非常漂亮它用数据告诉听众面积是影响房价的最核心物理变量但不是唯一变量。我还在面积-总价的散点图上叠加了回归线然后用颜色标记不同区域。这样既能看到全局的正相关趋势也能看出同样的面积在不同区域之间的总价差异。import numpy as np slope, intercept np.polyfit(df[area], df[total_price], 1) axis_x np.linspace(df[area].min(), df[area].max(), 100) axis_y slope * axis_x intercept系数解读slope 大约是 4.6意味着面积每增加 1㎡总价平均增加约 4.6 万元。这个数字本身就是一个可以直接写进论文的分析结论。5.4 朝向、楼层与装修多维度因素的价格影响除了面积我还做了三个小维度分析朝向分析把房源按朝向分为南北、南、东西、北四类对比单价。南北通透的房源单价中位数最高北向房源单价最低价差约 8%-12%。这个结论符合常识数据分析的价值就在于把常识量化。楼层分析把楼层分为低、中、高三个区间对比单价。中楼层单价最高低楼层和高楼层各有折扣呈明显的倒U型关系。装修分析精装房源的单价中位数比简装高约 6%但这里有个混淆变量——精装通常出现在面积大、总价高的改善型房型中单纯对比单价时需要注意控制面积变量。这些分析不复杂但体现了一个数据人的基本素养不能只看一个变量和结果的关系要意识到多个变量之间可能相互影响。我在做装修分析时按面积段做了分层避免精装房单价高的结论被改善型房源本来就贵这个因素混淆。6. 可视化和系统封装如何让分析结果有说服力6.1 可视化工具选型pyecharts 是我最终的选择可视化的工具选择我对比了三个方案工具优点缺点结论Matplotlib最通用、文档多交互性差、样式老旧适合做论文插图Seaborn统计图表美观交互性一般适合做探索性分析pyecharts交互式图表、支持地图依赖 echarts 环境适合做系统展示毕设系统最终选择了 pyecharts。理由很实际毕业设计的最终呈现通常是本地 Web 页面或者 Jupyter 报告pyecharts 生成的图表自带交互效果鼠标悬停显示数值、缩放、图例切换比静态 matplotlib 图表的观感好一个档次而且导出 HTML 非常方便可以直接嵌入 Flask 页面做演示。6.2 核心图表设计与数据表达我做了一个由 6 个图表组成的可视化面板区域均价柱状图 样本量标注底柱是均价柱顶标注样本量防止读者对数据可靠性产生误判。价格分布直方图分区域叠加不同区域用不同颜色画在同一坐标系中观察分布偏移。面积-总价散点图带回归线整体趋势 分区域着色。户型占比饼图看看市场主力户型是什么。朝向单价对比条形图横向条形图突出朝向差异。地图热力图商圈维度如果有商圈经纬度数据可以用 pyecharts 的Geo或Map组件展示商圈价格热力视觉冲击力很强。第 6 个图是我特意加的一个亮点图。虽然从纯统计角度来说热力图不是必需的但在毕设答辩演示时一张带地图热力的图瞬间就能提升整个系统的高级感。6.3 用 Flask 搭一个简单的展示系统为了让系统不只算一个脚本集合我用 Flask 搭了一个非常轻量的 Web 前端后端只有一个路由返回一个 HTML 模板所有图表用 pyecharts 的Page容器组合成一个长页面用户访问本地 5000 端口就能滚动浏览全部分析报告。from flask import Flask, render_template from pyecharts import options as opts from pyecharts.charts import Bar app Flask(__name__) app.route(/) def index(): bar ( Bar() .add_xaxis([A区, B区, C区, D区]) .add_yaxis(均价(元/㎡), [62300, 58900, 43200, 38500]) .set_global_opts(title_optsopts.TitleOpts(title区域房价对比)) ) return render_template(report.html, chartbar.render_embed()) if __name__ __main__: app.run(debugTrue, port5000)这个阶段不用做任何复杂的应用架构核心目标就一个把分析结果以最易于观看的方式演示出来。答辩的时候打开本地网页从上到下滚动展示图表和结论比念 PPT 有说服力得多。7. 踩坑复盘与性能优化那些文档里不会教你的细节7.1 链家页面结构的变化解析器的脆弱性教训做这个项目期间我遇到了两次页面结构调整。第一次是卡片列表的li标签里多了一个div嵌套导致我的 XPath 路径直接失效第二次是部分字段从文字变成了>from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_page(page_url): html fetch_page(page_url) if html: return parse_house_cards(html) return [] with ThreadPoolExecutor(max_workers4) as executor: futures [executor.submit(crawl_page, url) for url in page_urls] for future in as_completed(futures): cards future.result() save_to_db(cards)4 个线程并发总耗时从 1.5 小时压缩到 30 分钟以内效率提升了约 3 倍。但需要注意的是并发上来了以后被封概率也会显著增加所以并发版本必须在延时策略上更保守——我把随机间隔从 2-4 秒提高到了 3-5 秒摸索下来是安全的平衡点。7.4 关于数据可视化中的欺骗性提醒最后分享一个踩过的坑在做区域均价对比时我一度只展示均价柱状图结果发现 D 区的均价虽然最低但它 75 分位的价格其实比 C 区还高——原因是 D 区的豪宅盘被 C 区的刚需盘数量稀释了。如果只看均价会得出C 区比 D 区贵的结论但看分位数就发现 D 区的改善型市场其实并不弱。这说明了一个数据分析的基本原则永远不要把单一统计指标当作真相。在毕设报告和答辩中主动展示数据的多维分布比只挑对自己有利的指标更有说服力。个人体会是做这个项目最大的收获并不是学会用 requests 爬网页而是建立起了一套从数据采集、清洗、分析到结论输出的完整方法论。这种数据思维会在今后的学习和工作中长期受用。如果你正在做类似的项目我的建议是不要一开始就追求炫酷的技术栈先把数据链路跑通再逐层优化和丰富这样才是最稳妥的路线。本文还有配套的精品资源点击获取
返回列表