基于Django与爬虫的二手房数据可视化系统设计与实现
2026/9/18 4:22:00 网站建设 项目流程

1. 项目到底做什么:从毕业设计到可演示系统的需求拆解

每年到这个节点,总有不少人在后台问我毕业设计该选什么方向。我的建议一直很明确:选题别贪大,要做那种“技术栈覆盖广、代码量适中、演示效果好”的项目。今天要拆解的这套“基于Django数据可视化+网络爬虫的二手房屋信息采集系统”,就是非常典型的一类——它把后端框架、爬虫、数据库、前端可视化全部串起来,一套代码能讲出五六个技术点,答辩时老师问什么你都有东西接。

1.1 这个项目解决了哪些实际问题

先说人话:这个系统要干的事,就是自动去目标网站抓取二手房的挂牌信息,比如小区名称、户型、面积、总价、单价、朝向、楼层这些字段,抓完之后存进MySQL,再通过Web页面把数据用图表展示出来。整个链路是“数据采集 -> 数据存储 -> 数据展示”,非常完整。

你可能觉得这不就是爬虫加个网页吗?其实没那么简单。当你把爬虫和Django放在一起时,会遇到几个平时单练时碰不到的问题:

  • 爬虫的触发时机怎么控制?是手动点按钮爬,还是设置定时任务?
  • 爬取的数据如何清洗、去重、格式化?直接存入库里会出现大量脏数据。
  • 页面图表需要的数据格式,和数据库原始表结构不一致时,怎么在视图层做聚合?
  • 数据量到几万条之后,Django ORM查询速度变慢,怎么优化?

这些正是毕业设计答辩时最容易被打分老师追问的细节。哪怕你只是做到了“能跑”,只要把这些问题想清楚并在文档里写明白,就已经拉开一大截差距。

1.2 为什么是Django+爬虫+MySQL+ECharts这个组合

先别急着写代码,选型这件事值得说道说道。

Django这个东西,说它是“大而全”一点都不冤。它自带ORM、Admin后台、模板引擎、表单处理,最核心的是它有一个非常清晰的MTV架构(Model-Template-View),非常适合用来做“信息管理系统”类的毕设。你不需要像Flask那样自己拼第三方库,也不像Spring Boot那样还得先折腾Maven依赖,Django装上就能跑,对时间紧张的毕业生来说就是救命稻草。

爬虫这边,很多人纠结是学Scrapy还是用requests+BeautifulSoup。我的看法很直接:毕设项目,requests+BeautifulSoup就够了。Scrapy的性能优势在单机小规模采集场景下体现不出来,反而会带着一大堆配置和中间件、管道、爬虫类的概念,光调试就要花掉不少时间。你自己写一个简单的爬虫脚本,逻辑透明,答辩时老师问“这个字段你是怎么解析出来的”,你可以直接回答——这就是你亲手写的代码,底气完全不一样。

MySQL是存数据的主力,Django官方对MySQL的支持非常成熟,只需要在settings里配置一下连接参数,再用ORM定义模型类,建表、增删改查基本不用写SQL。ECharts做可视化不用多说,纯前端方案,配置灵活,图表交互效果也拿得出手,配合Django提供的JSON接口,前后端各干各的活,思路清晰。

一句话总结技术选型的核心逻辑:尽量用你熟悉或能快速上手的技术,同时保证每个环节都有“可讲的点”。这套组合恰好每一层都有话说。

2. 爬虫模块:把网页变成结构化数据的关键几步

爬虫是整个系统的数据源头,判断一个爬虫写得好不好,不是看抓了多少条数据,而是看三点:能不能稳定抓到目标字段、能不能应对轻微的反爬、数据入库之后干不干净。这一节我把整个流程拆开讲。

2.1 爬虫的完整工作流程

一个常规的网页爬虫,核心流程就是四步:发起请求、解析HTML、提取字段、保存数据。

先看发起请求。如果是requests库,一个最简单的GET请求长这样:

import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.example.com/" } resp = requests.get("https://www.example.com/ershoufang/", headers=headers, timeout=10) print(resp.status_code)

这里有几个容易被新手忽略的细节。第一个是headers里的User-Agent,很多网站会拦截没有UA的请求,直接返回403或者跳转验证页。第二个是timeout,必须设置,不设的话请求挂起会卡死整个采集任务。第三个是编码问题,如果resp.text出现乱码,先检查resp.encoding是不是被错误识别了,手动指定为utf-8或gbk之后通常就正常了。

拿到HTML之后进入解析阶段。BeautifulSoup配合lxml解析库是常见组合:

from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "lxml") # 根据页面结构调整选择器 items = soup.select("div.house-item") for item in items: title = item.select_one("a.title").text.strip() price = item.select_one("span.price").text.strip()

选择器的写法完全取决于目标站点的HTML结构。这个没法说统一套路,我建议你打开浏览器按F12,亲自找到存放标题、价格、户型信息的标签,再去写select或select_one。有个新手常犯的错误:用id选择器去定位列表页的重复元素,id在HTML里是唯一的,列表项应该优先用class或者标签层级关系来定位。

2.2 提取字段的细节与数据清洗

假设我们要采集的页面结构大致包含这些字段:小区名称、所在区域、户型(几室几厅)、面积、总价、单价、朝向、楼层、建筑年份、房源链接。

解析之后,原始数据往往是这样的:

"金湖湾 3室2厅 128.5平 精装 南北通透 185万"

这种原始字符串没法直接存数据库,必须做拆分和清洗。清洗这一步我可太有发言权了,很多毕设项目死在这里——数据库里存了一堆"3室2厅|128.5平|185万"这种拼接字符串,后期做可视化时根本没法聚合。

推荐的清洗逻辑是用正则表达式提取关键数字和特征:

import re def parse_house_text(raw_text): result = {} # 提取户型,如 3室2厅 layout_match = re.search(r"(\d)室(\d)厅", raw_text) if layout_match: result["bedrooms"] = layout_match.group(1) result["living_rooms"] = layout_match.group(2) # 提取面积 area_match = re.search(r"([\d.]+)平", raw_text) if area_match: result["area"] = float(area_match.group(1)) # 提取总价 price_match = re.search(r"(\d+)万", raw_text) if price_match: result["total_price"] = float(price_match.group(1)) return result

清洗的核心原则是:能分字段存储的绝不合并存一个字符串,能用数值类型的绝不用文本类型。因为Django的ORM筛选用的是字段名,如果全塞在一个字段里,后面做“按价格区间筛选”“按面积排序”全都无从谈起。

再说去重。爬虫多次运行之后,同一个房源会被重复采集。去重最简单的方案是给source_url加唯一约束,入库前先查这个链接是否已经存在。用Django ORM表达就是:

if not HouseInfo.objects.filter(source_url=url).exists(): HouseInfo.objects.create(**data)

别小看这个exists判断,它能救你于数据爆炸之中。

2.3 反爬应对与采集节奏控制

这里得先声明一句:本文讲爬虫的目的仅限于毕业设计和技术学习,实操时务必遵守目标网站的robots协议和使用条款,只采集公开的展示数据,控制请求频率,不要对目标站点造成压力。尤其要注意,不要在博文或者代码注释里明说任何“针对某平台”的字眼,只以“目标站点”代称即可。

最常见的反爬手段就是限速。处理方式非常简单,在两次请求之间sleep一下:

import time import random # 随机延时,避免规律性请求 time.sleep(random.uniform(2, 5))

为什么用random.uniform而不是固定sleep?因为固定间隔的请求模式很容易被识别为脚本行为,加入随机抖动之后,请求节奏更像真人浏览。

再一种反爬是要求登录后查看,这个毕设阶段一般不用绕,直接采集无需登录即可访问的公开列表页就行。还有一种是页面数据通过Ajax动态加载,requests拿到的HTML里根本没有房源数据,这时候你需要去Network面板里找到真正的数据接口,可能是一个返回JSON的地址,直接请求那个接口反而更简单。

采集节奏也得控制。一次性把目标站点的几千页全部抓完,既不现实也不礼貌。建议是做一个“按页采集”的功能,每次指定抓取前N页,或者只抓取特定区域和特定条件的数据样本。对毕设来说,1000条到5000条有效数据就足够撑起整个可视化了,数据贵在质量不在数量。

3. 数据可视化:让MySQL里的数字自己“说话”

爬虫把数据存进MySQL只是第一步,真正决定答辩演示效果的是可视化页面。这块的实现核心就一句话:前端框架负责画图,后端负责提供结构化的JSON数据,两边通过Ajax异步通信。下面拆开说。

3.1 数据接口设计:视图返回JSON而不是HTML

Django的视图函数不一定非要返回render渲染的HTML页面。对可视化大屏来说,更合理的做法是先返回一个承载页面框架的HTML,然后页面里的图表再去请求独立的API接口获取JSON数据。

比如你想做一个展示区域均价柱状图,先定义一个接口视图:

from django.http import JsonResponse from django.db.models import Avg from .models import HouseInfo def district_avg_price(request): data = ( HouseInfo.objects .values("district") .annotate(avg_price=Avg("unit_price")) .order_by("-avg_price") ) result = { "districts": [item["district"] for item in data], "prices": [round(item["avg_price"], 2) for item in data], } return JsonResponse(result)

这个接口设计有两点值得说。第一,聚合计算放在数据库层,用values().annotate()组合,效率远高于把几千条记录拉进Python再手动循环求平均值。第二,JSON的key直接用前端好消费的短命名,前端拿到之后甚至不需要再做什么转换,直接用就行。

前端页面引用ECharts,再用fetch或axios请求这个接口,拿到数据后setOption,一个图表就出来了:

fetch("/api/district-avg-price/") .then(res => res.json()) .then(data => { var chart = echarts.init(document.getElementById("priceChart")); chart.setOption({ xAxis: { type: "category", data: data.districts }, yAxis: { type: "value" }, series: [{ type: "bar", data: data.prices }] }); });

这是我觉得最适合毕设演示的写法:接口逻辑简单明了,答辩时你可以直接从接口讲到前端渲染,整条链路非常清楚。

3.2 常用的几种可视化图表场景

我建议你至少上四类图表,足够撑起一个信息量丰富的看板:

  • 区域房源数量柱状图:统计每个行政区的在售房源数,让人一眼看出哪个区房源供应多。
  • 面积-总价散点图:以面积为横轴、总价为纵轴,每个点代表一套房,可以做颜色的第二维度映射来区分不同区域,这种图展示出来数据密度高,非常出效果。
  • 户型分布饼图:把2室、3室、4室的占比画出来,直观反映主流户型。
  • 均价Top10小区横向条形图:排序后取前10个小区,适合展示单价最高的热门小区。

每种图表对应后端一两个接口,接口里无非是annotate聚合、order_by排序、切片limit的组合。这不是堆代码,而是在向老师展示你对数据维度的理解。

3.3 可视化大屏的布局思路

也许你会说:毕业设计不是做企业大屏,不用那么花哨。但其实一个简单整洁的看板页面,反而比css动画满天飞的大屏更有效。布局上我推荐经典的“上下+左右”结构:

顶部放标题和整体数据摘要,比如总房源数、均价、平均面积、最高单价这几个卡片型指标。中部分两列,左侧放区域柱状图、户型饼图,右侧放面积-价格散点图。底部放小区均价排行榜。这样整个页面信息分布均衡,同时呼应了答辩时你想强调的几个核心数据分析角度。

做一个这种页面,Django模板里正常写HTML和CSS,图表区域用div占位,页面加载后按顺序请求多个接口。即使模板逻辑不复杂,也要规范地用Django模板变量传递数据,比如把统计卡片里的数字在view里算好之后render到模板,而不是在JS里再发请求,这样更符合Django的使用习惯。

4. 数据库设计与Django集成

可视化做得再华丽,底层数据库设计不合理也一样白搭。这一节讲讲表结构怎么设计、Django ORM有哪些坑、后台管理怎么配。

4.1 MySQL表结构设计

说实话,二手房屋信息这种数据,表结构不需要搞多范式,核心就一张房源主表。字段设计上要注意的点是:能用数值的坚决不用文本,能拆开的字段坚决不合并。

推荐的核心字段大致如下:

字段名类型说明
idint主键自增
titlevarchar(255)房源标题
districtvarchar(50)所在区域
locationvarchar(255)小区或详细位置
layout_descvarchar(50)户型描述,如3室2厅
bedroomsint室数量
living_roomsint厅数量
areadouble建筑面积,单位平方米
total_pricedouble总价,单位万元
unit_pricedouble单价,单位元/平方米
orientationvarchar(20)朝向
floor_descvarchar(50)楼层描述
source_urlvarchar(500)房源原始链接,用于去重
created_atdatetime采集时间

在Django里定义模型,就可以用ORM建表。一个完整的例子是这样:

from django.db import models class HouseInfo(models.Model): title = models.CharField(max_length=255) district = models.CharField(max_length=50) location = models.CharField(max_length=255) layout_desc = models.CharField(max_length=50) bedrooms = models.IntegerField(default=0) living_rooms = models.IntegerField(default=0) area = models.FloatField(default=0) total_price = models.FloatField(default=0) unit_price = models.FloatField(default=0) orientation = models.CharField(max_length=20, blank=True) floor_desc = models.CharField(max_length=50, blank=True) source_url = models.CharField(max_length=500, unique=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "house_info"

source_url加unique约束,就是前面说去重的关键。有的同学图省事,直接不加唯一约束,结果采集脚本每跑一次数据库就翻倍,后面清理成本高得很。

4.2 Django访问MySQL的配置

Django连接MySQL的基本配置写在settings.py里:

DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "house_db", "USER": "root", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4", }, } }

用之前记得先安装PyMySQL,并且在项目包目录下的__init__.py里写入:

import pymysql pymysql.install_as_MySQLdb()

这个细节太容易踩坑了,不写这两行,Django会报找不到MySQLdb模块。另外一个常见问题是字符集,MySQL默认创建数据库时如果没有指定utf8mb4,中文存进去很可能变成乱码。建议在MySQL客户端里这样建数据库:

CREATE DATABASE house_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

utf8mb4和utf8的区别在于前者支持表情符号,虽然房屋数据里用不到emoji,但防一手总没错,而且utf8mb4对中文兼容性更好。

4.3 Django Admin后台管理与数据维护

Django自带的Admin后台,是毕设演示时的另一个亮点。你只需要在admin.py里注册模型:

from django.contrib import admin from .models import HouseInfo @admin.register(HouseInfo) class HouseInfoAdmin(admin.ModelAdmin): list_display = ("title", "district", "area", "total_price", "unit_price", "created_at") list_filter = ("district", "orientation") search_fields = ("title", "location") ordering = ("-created_at",)

这几十行代码写完,你就拥有一个可以直接在浏览器里管理房源数据、按区域筛选、按关键词搜索的后台。答辩时现场演示一遍“我从后台能看到刚刚采集入库的数据,能按区域过滤”,比任何截图都有说服力。

5. 常见问题与排查技巧实录

最后这部分,我把自己做这类项目时踩过的坑按“症状-原因-解法”的方式整理出来。你照着这个清单排查,大概率能省下好几个晚上的调试时间。

5.1 爬虫拿不到数据页面怎么排查

症状:请求返回200,但解析后列表为空。

排查思路,先打印response.text的前500个字符,看看返回的到底是什么。我遇到过好几种情况:一是返回的是JavaScript挑战页,内容里有一大段script;二是跳转到了验证码页面;三是页面本身的结构变了,没有之前分析的那些class名。解决方案分别是对抗JS挑战、降低请求频率或更换采集时段、重新检查页面结构。每一步都是为了靠近真实的目标——搞清楚网页到底返回了什么。

5.2 MySQL写入中文乱码

乱码问题几乎每个人都遇到过。你先对照这几个位置:数据库字符集是不是utf8mb4?settings.py里OPTIONS有没有指定charset?页面显示端有没有指定meta charset utf-8?这三个地方不一致就会出现乱码。最简单的排查顺序是从数据库最后查起,先查库里存的是什么,再往上推。

如果库里存的就是乱码,说明是写入端问题,检查字符集配置;如果库里正常而页面上乱码,那是读取或渲染端的问题。这就是很典型的“看数据在哪一步变形”的排查思路。

5.3 查询慢和数据量过大的优化

数据几千条时基本不用考虑性能,一旦到几万条,Django ORM性能问题就会出现。初级优化手段是给高频查询的字段加索引,比如district和unit_price:

class HouseInfo(models.Model): district = models.CharField(max_length=50, db_index=True) unit_price = models.FloatField(default=0, db_index=True)

另一个常见问题是N+1查询。新手在页面循环里查关联表,每个循环都发一次数据库请求,压垮性能。房屋信息这个项目只有单表,倒不严重,但如果你扩展了小区表、区域表,就一定要注意select_related和prefetch_related的使用。

页面上如果数据太多,建议后端做分页,前端用Ajax翻页,而不是一次性加载全部。Django的Paginator就够用:

from django.core.paginator import Paginator def house_list(request): all_data = HouseInfo.objects.all() paginator = Paginator(all_data, 20) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) return render(request, "house/list.html", {"page_obj": page_obj})

5.4 爬虫采集时间过长中断怎么办

采集几百页数据,中间网络波动导致请求异常,整个脚本直接退出,这是常见到不能再常见的事。解决方式很简单:每条数据处理都包裹try-except,异常时打印日志、跳过当前条,不中断主流程。再配合前面分析的source_url唯一约束,即使中断后重跑,已入库的数据也会自动跳过,不必担心重复。

for page in range(1, total_pages + 1): try: items = fetch_page(page) except Exception as e: print(f"第{page}页抓取失败: {e}") continue for item in items: save_house(item)

写代码时留好日志习惯,方便事后定位问题,这不只是毕设技巧,也是工作习惯。

6. 项目扩展与答辩发挥建议

最后这部分不是凑字数,而是想给你几条实际可操作的后续扩展方向,顺便聊聊答辩时的表达技巧。

6.1 想加分可以怎么扩展

爬虫跑的定时化:把采集脚本做成Django管理命令,比如用manage.py crawl_houses调用,再配合系统的定时任务定期执行,这样系统就能自动更新数据。这一条非常加分,因为它让系统从“手动运行脚本”升级为“自动化定时采集”。

区域对比分析:在可视化页面增加一个区域选择器,选择不同区域后散点图和柱状图联动更新。实现上就是给接口加一个district参数,前端切换时重新请求接口。

价格预测:如果你的数学基础还行,可以用sklearn里的线性回归,拿面积、房龄、所在区域做特征,训练一个简单的房价预测模型,再在页面上增加一个“估价工具”模块。这里不需要特别复杂的调参,一个线性回归就足够撑起一道“算法结合”的展示题。

6.2 答辩时怎么讲这套系统

答辩不是讲你写了多少代码,而是讲你的设计思路和解决问题的能力。建议按“为什么这么做、遇到什么问题、怎么解决的”来讲。比如爬虫模块,不要只演示“我能抓数据”,而是主动讲“我遇到了数据重复,所以给source_url加了唯一约束;我遇到了反爬限速,所以加了随机延时”。老师听到这种回答,印象分会明显不一样。

可视化模块,强调你在接口层的聚合设计,说明你是通过数据库聚合来减少数据传输量,而不是前端拿全量数据来算——这种“会做技术取舍”的表述,是答辩中关键的加分项。

整个项目做下来,你其实掌握了Django全栈开发的完整闭环:从数据采集到数据入库,再到数据展示。这几项技能并不是孤立的——它们也是大多数数据类业务系统的通用骨架。真正重要的是吃透“数据从哪里来、存在哪里、怎么展示”这条链路,这个思路换任何领域都能复用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询