今天聊一个非常典型的Django毕设项目:基于大数据的安客居二手房屋信息采集系统。一句话概括,就是写一个Python网络爬虫去安客居抓取二手房源数据,清洗整理后存入数据库,再用Django把这个数据源变成一个带后台管理、能搜索筛选、能看到各种统计分析图表的Web系统。整个过程把“爬虫采集 + 数据清洗 + Django Web开发 + 数据可视化”串成了一条完整的数据链路。
这个项目适合两类人。一类是刚学完Python基础、Django框架,想找一个能完整训练“从数据到展示”全流程的学生;另一类是正在选毕设题目、想要一个“既有技术深度又有可视化成稿”的同学。它能解决的真实痛点很明确:二手房平台上的信息庞杂、难比较、难统计,系统把数据抓下来之后,用图表直观呈现房价分布、区域均价、户型占比,比手动刷网页不知道高效多少。
作为过来人,我想先提醒一点:题目里写的是“大数据”,但毕设阶段的重点并不在于搭Hadoop/Spark集群,而在于理解并实现“多维度数据的采集、清洗、聚合、展示”这一条流水线。把这个链路打通,数据量级上来之后自然能往分布式方向扩展。下面我把完整设计和落地过程拆开讲,尽量把每一个关键决策背后的原因也讲清楚。
1. 项目整体设计与技术选型思路
1.1 毕设题目到底在考什么
很多同学看到“大数据”三个字,第一反应是“要不要上一套Hadoop生态”。我建议先冷静。本科毕设阶段,老师真正想看到的是你对“数据从哪来、怎么处理、怎么用”有清晰的认知和完整的实现。这个题目本质上是考察三层能力:
第一层是数据采集能力,你要能写爬虫,能应对反爬,能稳定地把目标站点的房源信息抓下来。第二层是数据管理能力,包括数据清洗、去重、字段规范化、入库,这一层直接决定后续统计分析的准确性。第三层是数据应用能力,也就是用Django把数据变成用户能看、能查、能交互的页面,并且用可视化图表把隐藏在数据背后的规律表达出来。
如果你认清了这层逻辑,就会发现这个题目的好处:每一个环节都有明确的产出物。爬虫对应“采集模块”,数据库对应“数据表设计”,Django对应“业务系统”,图表对应“可视化大屏”。无论是写毕业论文还是做答辩演示,结构都会很清晰,老师问起来也很有料。
1.2 技术栈选型:为什么是Django + 爬虫 + ECharts
选型这件事,说白了就是“在合适的场景里用最稳的组合”。这个项目里我最开始对比过几组技术,下面用表格直接列出选型依据:
| 模块 | 候选方案 | 最终选择 | 选择理由 |
|---|---|---|---|
| Web框架 | Django / Flask / FastAPI | Django | 自带ORM、Admin后台、用户认证、模板引擎,单体毕设系统开发效率最高 |
| 爬虫库 | requests+BeautifulSoup / Scrapy | requests+BeautifulSoup | 本项目以列表页+详情页为主,请求链路简单,BS4解析直观,Scrapy对毕设略重 |
| 数据库 | MySQL / SQLite | MySQL | 支持复杂查询和统计;如果用SQLite,数据量大了之后聚合查询会吃力 |
| 可视化 | ECharts / pyecharts / Chart.js | ECharts | 中文文档丰富、图表交互强、配置灵活,前端Ajax动态渲染效果最好 |
| 前端 | Bootstrap / Layui / 原生 | Bootstrap + jQuery | 上手快,栅格系统对可视化大屏的布局帮助很大 |
这里特别说下为什么选requests+BeautifulSoup而不是Scrapy。爬虫框架Scrapy确实很强大,但它的学习曲线陡峭,管道、中间件、Twisted异步机制对新手不友好。本项目采集量级一般,requests加简单重试和限速完全够用。如果你之后想把项目做得更有深度,可以在论文“系统扩展”章节写一句“后续可引入Scrapy框架实现分布式爬虫”,这比直接上手Scrapy稳得多。
“大数据”这顶帽子也不用慌,可以在系统里把数据量做上去、把统计分析做细,比如按区域、户型、价格区间多个维度做交叉统计,这就是“基于大数据分析”能自圆其说的落点。
1.3 系统功能模块划分
整个系统我拆成了六个模块,每个模块都有独立职责:
- 爬虫采集模块:负责从目标站点抓取列表页和详情页数据。
- 数据清洗模块:负责字段规范化、去重、异常值处理。
- 数据可视化模块:负责把数据库中的聚合结果渲染成图表。
- 房源信息管理模块:面向普通用户,提供搜索、筛选、排序、收藏功能。
- 后台管理模块:基于Django Admin,管理房源、用户和采集日志。
- 用户系统模块:注册、登录、会话管理,为收藏等功能做支撑。
模块化设计不只是代码整洁的问题,最大的收益体现在毕业论文上。每个章节对应一个模块,需求分析、系统设计、功能实现、系统测试的逻辑天然就通了。我强烈建议你在动手之前先画出功能结构图,哪怕是在纸上手画也比直接写代码强。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 爬虫模块:安客居房源数据的采集与处理
2.1 先分析目标站点,别急着写代码
写爬虫最忌讳一上来就写requests.get。我一般会先在浏览器里打开目标站点的二手房列表页,按F12打开开发者工具,一步一步看清三件事:URL结构规律、列表页数据位置、目标字段来源。
以安客居这类房产信息平台的常见结构为例,列表页URL往往会带分页参数,比如这种形式:https://www.example.com/ershoufang/pg2/。你可以多翻几页,对比URL的变化规律,找出分页参数。然后按下Ctrl+U查看网页源代码,如果房源标题、总价、单价直接出现在HTML里,说明是服务端渲染;如果页面数据是后期加载出来的,就得去Network面板里找XHR接口。
我在做这个项目时,列了一份目标字段清单:小区名称、所在区域、户型、面积、朝向、楼层、建造年份、总价、单价、关注人数。这些都是后续可视化分析的核心维度。字段清单越早确定越好,因为它直接决定了数据库表结构和清洗逻辑。
这里要特别提醒:不管抓哪个目标站,都要控制请求频率,做好合规意识。我们做的是学习和技术验证,不是搞“暴力采集”。建议在爬虫中加入请求间隔,比如每次请求后随机sleep 1到3秒,既减少对目标站的压力,也降低被反爬封禁的概率。
2.2 爬虫代码实现与反爬应对
下面是我在项目里用的核心爬虫代码骨架,基于requests和BeautifulSoup实现:
python复制import time
import random
import requests
from bs4 import BeautifulSoup
BASE_URL = "https://www.example.com/ershoufang/pg{page}/"
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://www.example.com/ershoufang/",
"Accept-Language": "zh-CN,zh;q=0.9",
}
def fetch_page(page):
url = BASE_URL.format(page=page)
try:
resp = requests.get(url, headers=HEADERS, timeout=10)
resp.encoding = resp.apparent_encoding
if resp.status_code == 200:
return resp.text
except requests.RequestException as e:
print(f"第{page}页请求失败: {e}")
return None
def parse_list(html):
soup = BeautifulSoup(html, "html.parser")
items = []
for li in soup.select("div.list-item"):
item = {
"title": li.select_one(".title a").get_text(strip=True),
"community": li.select_one(".community").get_text(strip=True),
"layout": li.select_one(".houseInfo").get_text(strip=True).split("|")[0].strip(),
"area": li.select_one(".houseInfo").get_text(strip=True).split("|")[1].replace("平米", "").strip(),
"total_price": li.select_one(".totalPrice").get_text(strip=True).replace("万", "").strip(),
"unit_price": li.select_one(".unitPrice").get_text(strip=True).replace("元/平", "").strip(),
"url": li.select_one(".title a")["href"],
}
items.append(item)
return items
def main():
all_data = []
for page in range(1, 11):
html = fetch_page(page)
if html:
items = parse_list(html)
all_data.extend(items)
print(f"第{page}页采集到{len(items)}条数据")
time.sleep(random.uniform(1, 3))
print(f"累计采集{len(all_data)}条")
这里有几个关键细节经我实测非常值得注意:
第一,User-Agent和Referer要模拟成真实浏览器。很多反爬系统会检查请求头是否完整,只带UA不带Referer很容易被识别。第二,resp.encoding = resp.apparent_encoding这行代码能解决80%的中文乱码问题,requests会根据响应头猜测编码,但有时候猜错,手动用apparent_encoding更可靠。第三,sleep一定要加随机性,固定的1秒反而比随机区间更容易被识别为爬虫。
如果目标站点是Ajax异步加载数据,那么你需要去Network面板里找到返回JSON的XHR接口,直接用requests请求那个接口。接口返回的数据通常是结构化JSON,解析起来比HTML还要简单。我建议优先看有没有这类接口,有的话优先用接口。
2.3 数据清洗与入库
爬下来的数据不能直接入库,因为在真实项目中你会遇到各种脏数据:面积字段变成了“85平米”、总价变成“暂无标价”、朝向是“南 北”、甚至整条数据为空。清洗的核心目标就是把所有字段变成统一的、可参与计算和统计的格式。
以价格和面积为例,我写了一个清洗函数:
python复制def clean_price(value):
"""把' 620万 '、'暂无标价'等字符串转为数字,无法解析时返回None"""
if not value:
return None
value = value.strip().replace("万", "").replace(",", "")
try:
if "暂无" in value:
return None
return float(value)
except ValueError:
return None
def clean_area(value):
"""把'85平米'转为数值"""
if not value:
return 0
value = value.strip().replace("平米", "").replace("㎡", "")
try:
return float(value)
except ValueError:
return 0
清洗完的数据要写到MySQL。我的做法是先用Django定义好模型,然后通过ORM直接从清洗脚本入库,这样后端查询逻辑和爬虫数据模型是同一套,不需要额外维护SQL建表语句。
python复制from apps.house.models import House
def save_to_db(data_list):
new_objs = []
for item in data_list:
house, created = House.objects.get_or_create(
url=item["url"],
defaults={
"title": item["title"],
"community": item["community"],
"layout": item["layout"],
"area": clean_area(item["area"]),
"total_price": clean_price(item["total_price"]),
"unit_price": clean_price(item["unit_price"]),
}
)
if created:
new_objs.append(house)
print(f"新增{len(new_objs)}条记录")
get_or_create配合URL唯一约束,是防止重复采集重复入库最简单有效的手段。等爬虫跑完之后,你能在后台看到一个几千条甚至上万条的房源库,这时候可视化和统计分析就有数据基础了。
3. 数据可视化模块:把采集到的数据变成图表
3.1 可视化方案选型与对比
可视化部分是整个系统最出效果的地方,也是答辩环节老师停留最久的地方。我在选型时对比过三种方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| ECharts | 功能最全,中文文档丰富,交互灵活 | 需要写前端JS逻辑 |
| pyecharts | 纯Python生成HTML,适合写报告 | 前端动态交互相对受限 |
| Chart.js | 轻量、上手快 | 图表类型和分析能力不如ECharts丰富 |
最终我选了ECharts。原因有两点:一是ECharts支持从柱状图、饼图、散点图到地图的全套图表类型,二手房分析里用得上的都有;二是它本身是纯前端的,可以在Django页面里通过Ajax从后端拉JSON数据,再动态渲染,这个“前后端分离式”的交互方式很契合毕业设计评分的“系统具有交互性”要求,也比截图式的静态图表更有说服力。
ECharts的引入方式,推荐直接下载echarts.min.js放到项目static目录下,而不是用CDN。因为答辩现场可能存在无法联网的情况,本地静态文件最稳妥。我踩过这个坑——答辩前一周用得好好的CDN,答辩当天教育网加载不出来,现场很狼狈。从那以后项目里一律改用本地静态文件。
3.2 核心图表设计与前后端对接
可视化的核心不是“画图”,而是“把数据变成正确的图表输入”。后面这句话我说的直白一点:如果后端JSON接口里给的数据形状不对,ECharts画出来就是空的。所以我在实现时设计了几个核心图表和对应接口,每个接口返回的都是标准化的结构:
第一个是价格区间分布柱状图,目的是看房源总价集中在哪个区间。我在后端用Django ORM聚合统计:
python复制from django.http import JsonResponse
from django.db.models import Count
from apps.house.models import House
def price_distribution(request):
# 定义价格区间
ranges = [(0, 100), (100, 200), (200, 300), (300, 500), (500, 1000), (1000, 99999)]
labels = ["0-100万", "100-200万", "200-300万", "300-500万", "500-1000万", "1000万以上"]
counts = []
for low, high in ranges:
count = House.objects.filter(total_price__gte=low, total_price__lt=high).count()
counts.append(count)
return JsonResponse({"labels": labels, "counts": counts})
前端用Ajax请求这个接口,然后渲染:
javascript复制$.ajax({
url: '/api/price/distribution/',
type: 'GET',
dataType: 'json',
success: function(res) {
var chart = echarts.init(document.getElementById('priceChart'));
chart.setOption({
title: { text: '房源总价区间分布' },
tooltip: {},
xAxis: { data: res.labels },
yAxis: {},
series: [{
type: 'bar',
data: res.counts,
itemStyle: { color: '#2f7ed8' }
}]
});
}
});
第二个是各区域平均单价排名,用横向条形图展示。这个图表直接反映了“哪个区更贵”,是所有人都会注意的图表。后端代码:
python复制from django.db.models import Avg
def region_avg_price(request):
rows = (House.objects
.exclude(unit_price__isnull=True)
.values("district")
.annotate(avg_price=Avg("unit_price"))
.order_by("-avg_price")[:10])
labels = [r["district"] for r in rows]
prices = [round(r["avg_price"], 2) for r in rows]
return JsonResponse({"labels": labels, "prices": prices})
第三个是户型占比饼图,统计几室几厅最多,用values("layout").annotate(Count("id"))就能实现。第四个我建议做面积-总价散点图,横轴面积、纵轴总价,能直观看出面积和价格的正相关关系。这些图表组合起来,就是一个非常完整的二手房分析面板。
你可能注意到,每个接口都要写一遍查询和返回。为了不做大量重复代码,我后来把所有统计逻辑抽到了一个analytics.py文件里,视图只做一层薄封装。这个小改动让代码可读性提升了不少,答辩讲解的时候也更容易说清楚。
3.3 大屏展示页面怎么做
首页是整个系统的门面,我把它设计成了一个轻量“数据大屏”,从上到下三层布局:
顶部是统计指标卡,一行展示总房源数量、均价、平均面积、在售户型数。这些数字通过一个统计算法一次性从数据库查询出来,指标卡用Bootstrap的卡片组件实现。中间是图表区,左侧放区域均价Top10条形图,中间放价格区间分布柱状图,右侧放户型占比饼图。底部放面积-总价散点图,占据整行。
有一个细节需要注意:ECharts容器必须设置高度,否则图表初始化后高度为0根本看不见。建议在CSS里给每个div.chart-box显式设置height: 400px,并且在外层用Bootstrap的col-md-6或col-md-4做栅格布局。
另外推荐加一个window.addEventListener("resize", function() { chart.resize(); }),这样浏览器缩放时图表不会变形。这个监听代码看起来小,但实际体验提升很大,演示时拖动窗口也不会露怯。
4. Django系统功能与数据库设计
4.1 数据库模型设计
数据库是整个系统最不该偷懒的地方。我在设计表结构时按照“核心房源表 + 辅助表”的思路来建。
房源表是最核心的表,字段设计如下:
python复制from django.db import models
class House(models.Model):
title = models.CharField(max_length=255, verbose_name="标题")
district = models.CharField(max_length=50, db_index=True, verbose_name="区域")
community = models.CharField(max_length=100, verbose_name="小区")
layout = models.CharField(max_length=50, db_index=True, verbose_name="户型")
area = models.FloatField(verbose_name="面积(平米)")
total_price = models.FloatField(db_index=True, verbose_name="总价(万)")
unit_price = models.FloatField(db_index=True, verbose_name="单价(元/平米)")
direction = models.CharField(max_length=20, verbose_name="朝向")
floor = models.CharField(max_length=50, verbose_name="楼层")
build_year = models.IntegerField(null=True, blank=True, verbose_name="建造年份")
url = models.URLField(unique=True, verbose_name="房源链接")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="采集时间")
class Meta:
db_table = "house"
verbose_name = "房源信息"
ordering = ["-created_at"]
def __str__(self):
return self.title
有几个设计要点是踩过坑之后才定下来的:url字段加unique=True,配合get_or_create能从根本上避免重复数据;district和total_price、unit_price加上db_index,因为这些字段是最常用作筛选和排序的,索引能显著提升查询效率;build_year允许为null,因为不是每个房源都标注建造年份,强制非空会导致清洗时丢掉大量数据。
辅助表我设计了两个:用户表沿用Django内置的auth.User,收藏表单独建一个Favorite模型,外键关联User和House。Django自带的用户认证系统直接能用,省去自己设计密码加密和登录会话的麻烦。
4.2 后台管理与业务功能
后台管理是Django的独门绝技,注册一下模型就能用:
python复制from django.contrib import admin
from apps.house.models import House
@admin.register(House)
class HouseAdmin(admin.ModelAdmin):
list_display = ("title", "district", "community", "layout", "area", "total_price", "unit_price")
list_filter = ("district", "layout")
search_fields = ("title", "community")
list_per_page = 20
列表页可以直接看到所有房源,还能按区和户型筛选,按标题和小区搜索。给老师演示的时候可以强调:这是Django Admin自动生成的后台管理界面,代码量很少但功能实用。
前台业务功能方面,我实现了四个核心点:
第一是注册登录。Django内置的auth.views.LoginView和LogoutView能省不少事。注册页面自己写一个RegisterForm,继承UserCreationForm,加上邮箱字段就够用了。
第二是房源列表页,支持多个条件联合筛选:
python复制from django.core.paginator import Paginator
def house_list(request):
queryset = House.objects.all()
district = request.GET.get("district")
layout = request.GET.get("layout")
min_price = request.GET.get("min_price")
max_price = request.GET.get("max_price")
if district:
queryset = queryset.filter(district=district)
if layout:
queryset = queryset.filter(layout=layout)
if min_price:
queryset = queryset.filter(total_price__gte=min_price)
if max_price:
queryset = queryset.filter(total_price__lte=max_price)
paginator = Paginator(queryset, 12)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(request, "house_list.html", {"page_obj": page_obj})
筛选逻辑本身不难,但要注意把当前筛选条件带到分页链接里,否则翻页后筛选状态就丢了。Django的Paginator很好用,前端模板里用page_obj.has_previous和page_obj.has_next就能控制上一页下一页。
第三是房源详情页。点击列表页的房源标题,进入详情页,展示全部字段。这里配合前端收藏按钮,点击后用Ajax请求收藏接口,登录用户才能收藏。这个功能用了@login_required装饰器,未登录用户会被重定向到登录页面。
第四是收藏列表页,展示当前用户收藏的房源,带取消收藏按钮。
4.3 部署上线与调试经验
调试阶段用python manage.py runserver就够了,但真正的部署我会用gunicorn加nginx的组合。这个项目是毕设,通常只需要演示到“能在本地跑起来”的程度,但如果想部署到云服务器,有几个点要提前处理好。
第一个是静态文件。Django默认不处理生产环境的静态文件,需要执行python manage.py collectstatic把所有静态文件收集到指定目录,再让nginx指向这个目录。settings里要配置:
python复制STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
第二个是环境隔离。项目依赖写进requirements.txt,数据库连接配置、密钥这类敏感信息建议放到.env文件里,用python-dotenv读取,避免把密钥暴露在源码里。这不是毕设硬性要求,但养成好习惯,面试时能加分。
第三个是DEBUG开关。生产环境务必将DEBUG = False,同时配置ALLOWED_HOSTS,否则会有安全警告。我在自己项目里曾经忘了改DEBUG,结果刚部署完就被安全扫描提示了,赶紧改掉。
5. 常见问题与排查技巧实录
5.1 爬虫采集中的典型问题
第一个是403 Forbidden。这是最典型的反爬反馈,解决办法按优先级从低到高排列:先换User-Agent,模拟真实浏览器;加上Referer、Accept-Language等完整请求头;再在代码里加Cookie(从浏览器里复制未登录的Cookie);如果还不行,就降低请求频率并引入代理池。但代理池对毕设来说成本偏高,一般操作到“加完整请求头+限频”就能解决问题。
第二个是爬取结果为空。如果你发现页面能请求到,但解析出来是空列表,大概率是页面结构变了或者选择器写错。先把抓回来的HTML存成文件,用编辑器打开,检查你写的CSS选择器是否还能匹配到。我平时排查这个问题就是:resp.text写到一个临时html文件里,然后用浏览器打开,直接右键检查元素复制选择器,比对一下。
第三个是中文乱码。这个在前面提过,核心解法就一句话:resp.encoding = resp.apparent_encoding。只要目标站是UTF-8或GBK,这个操作基本能解决。
第四个是数据重复。除了靠get_or_create防重复,另一个细节是清理历史数据后用bulk_create批量写入。再次强调url唯一约束加在数据库层面最稳,即使脚本逻辑出错,数据库也会拦下一部分重复数据。
5.2 Django运行时的典型问题
静态文件加载不出来是新手遇到最多的问题,现象是页面有HTML但完全没有CSS样式。排查顺序:第一,确认django.contrib.staticfiles在INSTALLED_APPS;第二,确认模板里用了{% load static %}标签;第三,CSS文件确实放在app的static目录下;第四,如果部署环境,确认STATIC_ROOT和nginx指向是否一致。
CSRF token错误是另一个高频问题。Django默认开启CSRF防护,凡是POST表单都必须加{% csrf_token %}。用Ajax POST时,JS里要带上请求头X-CSRFToken。可以在模板中通过{{ csrf_token }}取出token,再在Ajax里发送。
时间字段模板渲染问题也经常出现。比如created_at字段在模板里显示2019-01-01 10:30:00+00:00这种带时区的格式,很丑。可以在Django模板中用{{ obj.created_at|date:"Y-m-d H:i" }}过滤器格式化。这个问题本身不难,但答辩时如果页面上出现乱七八糟的时间格式,观感不好。
5.3 可视化图表的常见坑
图表容器显示不出来,第一步先看控制台报错。如果提示echarts is not defined,说明ECharts文件没加载成功,检查script标签路径。如果控制台没有报错但页面空白,多半是容器高度为0,这是最常见的原因。
数据格式不对是第二个高发问题。比如你想渲染饼图,ECharts希望的数据格式是[{name: "三室", value: 120}, {name: "两室", value: 80}],而后端接口返回的是{"labels": ["三室"], "values": [120]}。两者都对不上,图表自然不渲染。我的调戏方法是先console.log(res)确认数据结构,再在setOption之前按ECharts需要的格式做一次数据转换。
Ajax请求返回500也是常见问题。看到enter code here或者浏览器网络面板里的红色状态码,先看Django日志。大多数情况是后端查询写错,比如字段名写错或空值处理不当。建议在视图函数里临时加个print输出,确认数据到底有没有查出来。
6. 写代码之外的几点体会
项目做完之后回头看,真正让这个毕设“显得成熟”的,反而是一些代码之外的工作。第一是文档,需求分析里要写清楚为什么做这个系统,可行性分析里写明白技术路线,数据库设计里给ER图,系统测试里贴测试用例截图。这些内容几乎都能从系统实现里直接整理,平时注意保存截图和日志,写论文时就不会手忙脚乱。
第二是答辩演示的准备。我建议在答辩前把数据库里预置一批干净、有代表性的数据,不要临时去爬,因为生成环境和网络状态不可控。演示顺序按“首页大屏图表 → 列表筛选 → 详情收藏 → 后台管理”这个路径走,整个过程不超过10分钟,核心亮点全部覆盖。
第三是源码管理。用Git初始化项目,写清楚README,包含运行环境、安装依赖、数据库迁移、启动命令、默认账号。别小看这份README,它能帮你节省大量回答“怎么跑起来”的时间。
最后我想说一点个人体会:如果一个项目里的每个模块你都能讲清楚“为什么这么设计”,比单纯堆功能重要得多。比如为什么字段加索引、为什么清洗数据时允许某些字段为空、为什么用Django的ORM而不是原生SQL——这些细节就是你面试时的底气。这个项目做完,你不需要再纠结“我到底会什么”,因为一条数据链路你已经完整走通了。
