ARTICLE DETAIL

资讯详情

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

全国二手房数据分析系统实战:Requests爬虫+Django+ECharts可视化

全国二手房数据分析系统实战:Requests爬虫+Django+ECharts可视化 毕业设计年年有人做二手房数据系统但真正把爬虫、Web框架、可视化这些技术点讲明白、能直接跑通的资料其实不多。大多数同学要么卡在数据采集环节被反爬拦住要么做出来的可视化就是几个静态表格答辩时被老师一问数据怎么更新就露怯。这篇我把整个全国二手房数据分析系统的完整实现路径拆开讲从Requests爬虫到Django后端再到ECharts可视化大屏每一步都给出实际可用的代码和踩坑记录适合正在做毕设的本科生也适合想快速搭一个数据分析Demo的开发者参考。这个项目最终交付的是一套能够自动采集房源数据、清洗入库、按城市和板块聚合统计、通过可视化大屏展示价格分布和市场趋势的完整Web系统。核心价值不在于某个单一技术多高深而在于把数据采集、存储、分析、展示这条链路真正打通让你在答辩时有底气说“整个流程是我自己实现的”。1. 项目整体设计与技术选型思路1.1 技术栈组合为什么选 Django Requests ECharts先说技术选型。市面上能做数据分析可视化的方案很多Flask、FastAPI、Spring Boot都行但Django在这类毕业设计里几乎是最稳妥的选择。原因有三个。第一Django自带Admin后台和ORM数据管理不需要额外写前端页面。二手房数据涉及房源表、城市表、区域表如果全部手写增删改查界面工作量会多出三分之一。Django的Admin直接注册模型就能用测评老师看到后台可以管理数据印象分会上去。第二Django的模板系统渲染服务端页面非常顺手配合ECharts只需要在模板里挂一个容器div通过Ajax或者直接传JSON数据就能完成图表渲染。相比前后端分离的方案少写一堆跨域和接口文档。第三Django的项目结构规范化程度高models、views、urls、templates各司其职答辩时你按照这个结构讲逻辑清晰不限。爬虫这边选Requests而不是Scrapy主要考虑毕设场景的代码可读性。Scrapy是框架级的爬虫写起来要理解Spider、Pipeline、Middleware一堆组件调试周期长。Requests是直白的HTTP客户端配合BeautifulSoup解析几十行代码就能把一套房源数据抓下来源码也容易讲清楚。对于二手房这种量级的数据Requests加多线程完全够用不需要分布式爬虫这种杀鸡用牛刀的东西。可视化选ECharts没有悬念。百度开源的ECharts图表覆盖全面地图、柱状图、折线图、散点图、仪表盘都有现成案例改改配置就能用。更重要的是ECharts对中文文档支持好答辩时现场调图表样式也快。1.2 系统架构与模块划分整个系统我按数据流方向分成四个模块数据采集模块、数据管理模块、数据分析模块、可视化展示模块。模块之间解耦任何一个模块出问题都不影响其他模块运行。数据采集模块Requests BeautifulSoup ↓ 清洗、去重、标准化 数据管理模块Django Models SQLite/MySQL ↓ ORM查询聚合统计 数据分析模块Django View 聚合函数 ↓ JSON序列化 可视化展示模块ECharts HTML模板数据采集模块独立成一个Python脚本不放在Django项目内部。这样做的好处是爬虫报错不会影响Web服务而且你可以随时在命令行单独跑爬虫更新数据。爬虫爬取结果直接写入数据库通过ORM操作不需要中间文件。数据管理模块就是Django的models定义把房源、城市、区域、价格字段结构化。数据分析模块写在views.py里用aggregate和annotate做聚合运算。可视化展示模块是模板页面加ECharts前端渲染。四个模块实际对应了四层职责答辩时按这个架构图讲老师能很清楚地看到你对系统设计的理解。1.3 数据库模型设计从字段到表结构二手房数据最核心的表是房源信息表字段设计会直接影响后续所有分析逻辑。我最终的表结构是这样设计的# models.py from django.db import models class City(models.Model): name models.CharField(max_length50, uniqueTrue) # 城市名 code models.CharField(max_length20, uniqueTrue) # 城市代码 class House(models.Model): title models.CharField(max_length200) # 房源标题 city models.ForeignKey(City, on_deletemodels.CASCADE) # 所属城市 district models.CharField(max_length50, blankTrue) # 行政区 area models.CharField(max_length50, blankTrue) # 商圈/板块 layout models.CharField(max_length50, blankTrue) # 户型如 2室1厅 floor_area models.FloatField(default0) # 建筑面积(㎡) total_price models.FloatField(default0) # 总价(万元) unit_price models.FloatField(default0) # 单价(元/㎡) direction models.CharField(max_length10, blankTrue) # 朝向 decoration models.CharField(max_length20, blankTrue) # 装修情况 publish_time models.CharField(max_length30, blankTrue) # 发布时间 url models.URLField(uniqueTrue) # 房源详情页链接 crawl_time models.DateTimeField(auto_now_addTrue) # 抓取时间为什么要单独建City表而不是直接存城市名字段因为后续可视化要做全国地图需要按城市聚合外键关联的查询效率更高而且如果后面要扩展城市基础信息经纬度、常住人口不用改动房源表结构。房源表的url字段设了唯一约束这是去重逻辑的基础同一套房源就不会重复入库。关于总价和单价我建议都存FloatField而不是IntegerField。虽然房价看起来是整数但数据分析阶段会有均价计算、分组统计浮点类型更灵活。还有一点发布时间的字段我用的是CharField而不是DateField因为网站上的时间格式不统一有的写“2024-05-10”有的写“5天前发布”直接转日期类型容易报错先存字符串清洗后再处理。2. 数据采集模块Requests 爬虫从零到落地2.1 目标网站分析与爬虫策略二手房房源公开数据链家是绕不开的站点。链家二手房页面结构规范、数据完整而且有按城市划分的入口适合做全国范围的数据采集。但注意链家反爬机制在主流房产网站里算比较严的什么都不配置直接跑IP很快会被封。我的策略是先限定频率单次请求间隔1到2秒随机延迟每个城市采集前100页每页30条大约3000套总量控制在可控范围。对于毕设来说数据量不是重点你有一两万条优质房源数据已经足够撑起所有图表和分析结论贪多必失。页面分析这一步很关键。打开链家二手房列表页F12看网络请求房子的标题、总价、单价、户型、面积、楼层、朝向、装修信息都嵌在HTML里BeautifulSoup就能解析不需要分析额外的XHR接口这大大降低了爬虫难度。2.2 爬虫核心代码实现与反爬应对爬虫脚本我放在项目根目录的spider/文件夹下和Django项目分离。核心逻辑分三步构造URL列表、请求页面、解析入库。# spider/run_spider.py import random import time import requests from bs4 import BeautifulSoup from django_orm_setup import setup_django # 初始化Django环境 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://xx.ke.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } def parse_house_list(html): soup BeautifulSoup(html, html.parser) items soup.select(.sellListContent li) # 列表页房源卡片 for item in items: try: title item.select_one(.title a).text.strip() house_info item.select_one(.houseInfo).text.strip().split( | ) total_price float(item.select_one(.totalPrice span).text.strip().replace(,, )) unit_price item.select_one(.unitPrice span).text.strip().replace(元/平, ).replace(,, ) url item.select_one(.title a)[href] # 数据入库逻辑 save_house(title, house_info, total_price, unit_price, url, city) except Exception as e: print(f解析异常: {e}) continue反爬是第一道关卡。链家高频请求触发验证后最典型的特征是页面返回的不是房源列表而是安全验证页。实测下来下面几个设置能最大程度降低封禁概率User-Agent必须真实不要用简单的requests默认UA搜索一个Chrome真实UA字符串最好爬之前用浏览器访问一次页面复制。Referer伪装链家会校验请求来源Referer要设置成目标页面的域名否则返回403。请求间隔必须随机固定间隔最容易被识别为脚本用random.uniform(1, 2)随机延迟。控制单IP并发不要开几十个线程同时打一个站点我用线程数控制4个以内实测稳定。如果IP还是被封可以考虑在requests会话里加Cookie把浏览器登录后的Cookie复制过来或者降低采集量、换城市分组执行。不要一上来就上代理池那个复杂度对毕设没必要。2.3 数据清洗、去重与入库爬虫解析出来的数据不能直接入库因为原始字段太脏。房价字符串里带了“万”、“元/平”面积字符串里带了“平米”户型信息里混合了楼层、朝向这些都需要拆分清洗。# 数据清洗示例 def clean_house_info(house_info_list): # 原始格式类似: [2室1厅, 79.33平米, 南 北, 简装, 高楼层/6层, 板塔结合] layout house_info_list[0] if len(house_info_list) 0 else floor_area house_info_list[1].replace(平米, ) if len(house_info_list) 1 else 0 direction house_info_list[2] if len(house_info_list) 2 else decoration house_info_list[3] if len(house_info_list) 3 else return layout, float(floor_area), direction, decoration清洗逻辑的核心是容错。二手房列表页偶尔有数据缺失比如某个字段没有值索引越界直接崩掉整个爬虫是最不应该的。我用try-except包住每条房源的解析与入库单条失败就打印日志跳过不影响整体流程。去重有两种方式。第一种是入库前检查url是否已存在用Django的get_or_create。第二种是数据库层面唯一约束兜底。实测get_or_create在数据量几千条时性能没问题两万条以上后会慢一些但毕设场景完全够用。3. 数据分析与可视化大屏实现3.1 可视化方案选型与页面布局可视化大屏在毕业设计里的作用是“一眼看出你做了数据分析”。单看数字表格老师根本不会有体感但是一个配色专业的地图加动态图表展示效果完全不一样。我最终选择用ECharts搭建四个核心视图模块全国城市房价分布地图用地图展示不同城市均价颜色深浅代表价格高低。重点城市价格趋势折线图展示目标城市近半年价格走势。户型面积单价散点图展示户型、面积与单价的关系。城市房价Top10柱状图直观对比城市间价格差异。页面布局参考百度可视化大屏的栅格风格头部是系统标题下方左右信息区对称中间主视图放全国地图这样整体结构清晰饱满。3.2 核心图表实现细节与配置ECharts图表直接写配置对象关键是后端把数据组织成前端要的结构。以全国地图为例需要把Django聚合出来的数据转成ECharts map需要的JSON格式# views.py - 全国均价地图数据接口 def map_data(request): city_avg_prices ( House.objects .values(city__name) .annotate(avg_priceAvg(unit_price)) ) data [ {name: item[city__name], value: round(item[avg_price], 2)} for item in city_avg_prices ] return JsonResponse({data: data})前端拿到这个JSON后用ECharts的map类型渲染。中国地图的geoJSON需要额外引入ECharts 5之后官方不再内置地图数据需要下载china.json放在static目录里。// 前端ECharts地图渲染 $.getJSON(/static/map/china.json, function (chinaJson) { echarts.registerMap(china, chinaJson); var chart echarts.init(document.getElementById(mapChart)); chart.setOption({ tooltip: { trigger: item }, visualMap: { min: 0, max: 60000, text: [高, 低] }, series: [{ type: map, map: china, roam: true, label: { show: true }, data: jsonData.data }] }); });地图的visualMap颜色区间要结合数据实际范围调整不能写死。我这边全国均价在几千到六万不等所以max设成60000太高的城市会被压成同一色阶。这个细节答辩时老师问起来你能说得清楚为什么这么设置就是加分项。折线图和柱状图相对简单核心是后端按月份或城市分组聚合。这里有一个坑Django的DateTimeField按月份聚合需要用到TruncMonth不处理的话时间分组全是精确到秒的前端没法按月份对齐。from django.db.models.functions import TruncMonth monthly_avg ( House.objects .annotate(monthTruncMonth(crawl_time)) .values(month) .annotate(avg_priceAvg(unit_price)) .order_by(month) )3.3 Django 视图与 JSON 接口设计可视化页面需要多个数据接口我建议把所有接口统一放在一个视图文件里按功能区分。接口列表如下/api/map/全国城市均价地图数据/api/trend/?city北京指定城市月度均价趋势/api/bar_top10/均价Top10城市柱状图数据/api/scatter/户型与单价散点图数据为什么单独分接口而不是直接在HTML模板里渲染数据因为ECharts是通过JavaScript异步拿数据的模板渲染只能解决页面加载时的数据刷新图表、切换城市这些操作都需要重新请求接口。分了接口之后前端做交互就很灵活。# views.py 接口统一返回JsonResponse def trend_data(request): city_name request.GET.get(city, 北京) city City.objects.filter(namecity_name).first() if not city: return JsonResponse({code: 404, msg: 城市不存在}) data ( House.objects .filter(citycity) .annotate(monthTruncMonth(crawl_time)) .values(month) .annotate(avg_priceAvg(unit_price)) .order_by(month) ) result [ {month: item[month].strftime(%Y-%m), avg_price: round(item[avg_price], 2)} for item in data ] return JsonResponse({code: 0, data: result})接口返回格式统一用{code: 0, data: ...}code等于0表示成功。这样做最大的好处是前端可以通过统一逻辑判断接口是否正常后续扩展新接口时前端代码几乎没有重复劳动。4. 踩坑记录与常见问题排查4.1 爬虫阶段的典型问题问题一请求返回验证码页面这是爬虫阶段遇到最多的坑。现象是解析出来的房源数量为0或者标题全是“验证中心”。排查方法很简单第一步打印返回的HTML前500个字符看到verify、captcha之类的关键词就说明被识别了。解决思路是按“降低频率、增加延迟、换UA”的顺序调整。我先降到单线程间隔加到3秒再换了浏览器复制的UA基本能恢复正常。如果还不行就主动放慢采集节奏每天只爬一个城市。问题二字段解析出来乱码链家的页面是UTF-8编码但个别城市的子页面声明可能不同。requests的response.text属性会根据HTTP头自动解码偶尔会猜错。稳妥的做法是用response.content手动解码html response.content.decode(utf-8, errorsignore)。errors参数一定要加某些页面里特殊字符会导致UnicodeDecodeErrorignore忽略异常字符不会影响房价信息。问题三入库后数据重复量巨大爬虫跑的时间长了或者中途重启多次房源表里会出现重复记录。单纯靠爬虫端每次get_or_create检查数据库记录多之后效率很低。我的解决方案是用uniqueTrue的url字段做数据库约束然后用INSERT OR IGNORE语义的ORM写法批量去重。结合Django的bulk_create加ignore_conflictsTrue参数上万条数据一次性插入不仅去重而且效率极高from django.db import IntegrityError # 批量入库url重复自动忽略 def save_houses_bulk(house_objs): House.objects.bulk_create(house_objs, ignore_conflictsTrue, batch_size500)4.2 Django 开发与部署的坑问题一static文件加载不出来ECharts的JS、china.json放在static目录后页面能打开但图表空白控制台报404。这是因为Django开发服务器对static文件的处理需要配置。Django 3.2之后settings里要明确设置STATIC_URL /static/并且在项目的urls.py里加一段开发用static处理from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.STATIC_URL, document_rootsettings.STATIC_ROOT)china.json是一个大文件放在static目录下时浏览器加载会比较慢。我实际测试1.5MB的JSON文件本地加载还好放到云服务器上首次访问会卡一两秒。建议在nginx层面对静态文件做缓存或者把地图文件改成异步加载先渲染其他图表地图加载完成后再show。问题二聚合查询报FieldError用values(city__name)跨表聚合时Django会严格检查字段名。常见的报错是Cannot resolve keyword city__name这通常是models里外键字段名和实际关联表名不匹配。排查方法打印一下模型的_meta.fields看字段名确认外键字段是city而不是city_id。Django在数据库层面确实生成了city_id列但ORM查询必须用city__name这种关系写法直接写city_id__name会报错。问题三可视化数据量太大图表卡顿如果采集了全国几万套房源前端散点图直接绑定全量数据ECharts渲染会有明显卡顿。我的做法是后端做抽样返回散点图接口随机取2000条记录返回地图接口只返回城市级的聚合结果。这样既保留整体分布形态又保证交互流畅。散点图本来就只是看趋势的不需要把每个点都画出来。4.3 答辩现场的高概率问题与应对思路做过完整的项目之后答辩时老师的问题其实比较集中提前准备好就行。“你的数据量是多少数据来源合法性如何”数据量如实回答比如某城市3000套、总计两万条左右。数据来源方面要说清楚爬的是公开页面展示信息不涉及用户隐私仅用于学习研究采集频率做了控制。不要说你采集了全站所有数据那反而容易被追问风险。“为什么用Django不用Flask”从项目规模角度说二手房分析系统包含数据模型、后台管理、多页面视图Django自带Admin和ORM减少重复开发而且Django的项目结构后期扩展爬虫脚本、API接口都清晰。如果你用Flask这些问题就会被反问“你怎么处理数据库迁移和维护”。“可视化图表的数据刷新机制是什么”说清楚是“爬虫脚本手动或定时执行写入数据库前端页面通过接口实时查询最新数据”。这里如果你做了一个定时刷新按钮就顺便展示给老师看比干讲有力得多。5. 项目扩展思路与个人经验总结这个系统做到现在这个程度已经能顺利答辩了但如果你想冲刺优秀毕设或者后续作品集有更好的展示效果有几个扩展方向可以参考。第一个是引入定时任务调度。把爬虫脚本用APScheduler或者系统crontab挂起来每周自动爬一次数据系统就从一个静态Demo变成动态更新的数据平台。这个改动不大但质的区别非常明显。第二个是增加更深入的维度分析。当前系统主要是价格维度的聚合展示你可以往面积、楼层、装修、朝向维度深入分析比如不同装修档次的单价差异、高层与低层的价差、户型单价分布等。分析维度越多答辩时的谈资越充足。第三个是预测模型。如果你学了机器学习可以用房源历史价格数据训练一个简单的房价预测模型线性回归或者随机森林放一个“预测价格”的功能模块。这绝对能让你的系统从“数据分析平台”升级成“数据智能平台”。最后分享一点个人体会。我刚开始做这个项目的时候觉得最难的不是代码而是面对一个空项目时不知道从哪下手。后来给自己定了一个规则先跑通最小闭环再逐步扩展。第一版只要能把一个城市的100条房源从爬虫跑到Dashboard链路已经通了一半剩下就是填充细节、加功能、踩坑修复。这个方法在毕设里特别管用你一旦有了能跑的东西后面所有改进都有了明确的验证依据而不是对着设计和文档空转。做毕设不要追求一步到位先把主链路跑通剩下的都是时间问题。
返回列表