☰
Django+Spark房价数据分析系统:完整开发与答辩指南
2026/10/7 4:17:47 网站建设 项目流程

如果你正被毕业设计搞得焦头烂额,看到“基于Django+Spark的南昌房价数据分析系统”这个题目,很容易本能地把它归类为“听起来就很重”的项目。但实际上,这类题目的核心难度并没有想象中高,真正要做的只是把 Django、Spark、数据分析 和数据库 四件套串成一条完整的数据链路:爬虫拿到数据、Spark 清洗与聚合、数据库持久化、Django 展示。它既有技术层次,又不需要海量数据,一个人完全能独立完成。

这篇文章不是把源码再贴一遍,而是把那套系统从选题、技术选型、代码实现到答辩的整个思考过程讲清楚,还附带一些我在做类似项目时踩过的坑。最适合正在做课程设计或毕业设计,并且想用同一套思路完成一个完整系统的同学。即使你手里没有南昌的数据,把城市换成成都、杭州、长沙,整个系统骨架一样能复用。

1. 为什么选这个题目:南昌房价数据背后的开发空间

1.1 房价分析天然具备“数据量合适、维度多、可视化出效果”三重优势

毕业设计最怕什么?最怕选题“看起来很大,做起来很空”。纯搞算法模型容易被质疑工作量,纯搞增删改查又显得没技术含量。房价数据分析恰好介于两者之间:数据源公开、指标丰富、可视化结果直观。

南昌作为具体研究对象有其独到之处。它不像北京上海那样房源数据量大到单机处理有压力,也不像县城那样样本太少没有统计学意义。南昌各区之间的房价差异明显,比如红谷滩区的新盘和老城区的老破小价格能差出一倍以上,这就让“区域维度分析”这种常规功能有了真实的解读空间。整个数据量级控制在几千到几万条,单机 Spark 跑起来毫无压力,但你又确实用到了分布式计算框架,写论文时有东西可讲。

1.2 毕业设计评审到底在考核什么

我后来复盘发现,评审老师看一个毕设项目,重点看三件事:

  1. 功能是否闭环:从数据采集、数据存储、数据分析到前端展示,链路完整比单个功能炫酷重要得多。
  2. 技术栈是否有层次:前后端分离、大数据框架、数据库设计、可视化,每一条都能在答辩时展开讲。
  3. 工作量是否可感知:项目里的代码量、文档篇幅、图表数量都是“肉眼可见”的工作量。

Django+Spark这套组合恰好能把这三件事全部覆盖。Spark 处理清洗和统计,Django 负责 Web 展示,MySQL 做持久化,ECharts 出图表。哪怕每个环节的代码量都不算夸张,组合起来就是一个很完整的系统。

1.3 系统功能范围的合理裁剪

真正动工之前,我把功能范围切成三条线,避免做到一半迷失方向:

  • 数据分析线:全市整体均价、分区房价对比、户型分布、面积段与总价区间交叉分析、热门板块排行。
  • 数据管理线:爬取到的房源数据入库,Django Admin 后台可以查看、修改、删除房源记录。
  • 可视化展示线:首页指标卡片、区域对比图、户型饼图、价格区间柱状图、房源列表筛选页。

预测房价这种功能我一开始就没做。原因有两个:一是房价预测很容易变成“用当天数据预测当天数据”,学术上站不住脚;二是预测模型的调参工作量很大,容易挤占核心功能的完成时间。统计分析已经足够撑起一个完整毕设,预测可以留在论文的“展望与扩展”里写。把这三条线做完,项目就是完整的,答辩时也能说清楚每个页面背后的数据来源和计算逻辑。

2. 技术选型逻辑:Django 和 Spark 是怎么搭到一块的

2.1 为什么不是 pandas + Flask 的轻量组合

很多人一听到“数据分析”,第一反应就是 pandas + Flask。这套组合确实轻,但用在毕业设计里有一个致命问题:技术层次单薄。

pandas 跑几千条数据非常流畅,但它在简历上和答辩PPT里的“分量”远不如 Spark。Spark 代表的是分布式计算、DataFrame 算子、延迟计算这些大数据生态里的核心概念。即使用户量不大、数据量不大,你依然可以理直气壮地说:这套分析逻辑在数据量增大时可以横向扩展,把 Spark 部署到集群上也不需要重写核心代码。

另一个现实因素是,Spark 和 pandas 并不冲突。你在 Spark 里处理的思路和 pandas 高度相似——分组、聚合、连接、过滤,但 Spark 的 DataFrame 是分布式的,算子执行计划由 Catalyst 优化器调度,这本身就是很好的论文素材。用 pandas 的话,这一整章技术原理就没得写了。

2.2 Django 在本项目里的三个不可替代作用

选 Django 而不是 Flask 或 Spring Boot,理由很朴素:

第一个作用是自带的 Admin 后台。房源数据管理页面不需要自己手写,Django Admin 注册一下模型就能获得增删改查界面。毕设系统需要一个后台管理模块,这等于白送。

第二个作用是 ORM。虽然可以用原生 SQL 直接查询 MySQL,但 Django ORM 写起来更直观,而且能避免手动拼接 SQL 带来的注入问题。分析结果表、房源表的查询用 ORM 三五行就能完成。

第三个作用是模板系统和静态文件机制。课程设计通常不需要前后端分离,Django 的模板渲染 + ECharts 就足够做出美观的页面。如果非要前后端分离再上一套 Vue,工程量会膨胀不少,对你拿高分并不是必需的。

2.3 Spark 承担的分析任务边界

在这个项目里,Spark 不是常驻服务,而是“离线批处理引擎”。它的任务边界很清晰:把清洗后的 JSON 数据读进来,做全局统计和分组统计,最后把计算结果写回 MySQL 的分析结果表。

这么做的好处是把实时 Web 服务和计算逻辑解耦。Django 启动时完全不依赖 Spark,页面展示的数据从数据库读取。只有新数据爬取完成后,手动触发一次 Spark 分析任务,更新分析结果。这个架构在产业界也常见,属于离线数仓里典型的 T+1 批处理模式。

我强烈建议你不要在 Django 视图里直接调用 SparkSession,那样每次刷新页面都要启动 JVM,性能会惨到怀疑人生,而且并发一高系统直接崩。

2.4 开发环境版本搭配,按我的配置来基本不会踩 JDK 的坑

Spark 版本和 JDK、Python、Django 之间的兼容性是一个能让人折腾一整天的坑。这里给出一套我验证过的组合:

组件版本建议备注
Python3.8 或 3.93.10 以上部分依赖可能不兼容
JDK1.8Spark 3.x 对 JDK8 兼容最稳
Spark3.1.2 或 3.3.x推荐 3.1.2,教程多,坑少
Django3.2 LTS稳定性优先,资料多
MySQL5.7 或 8.0记得设置 utf8mb4 字符集
Py4J随 Spark 自带本地模式无需单独配置

Windows 用户特别注意,Spark 本地模式会调用 Hadoop 的 winutils.exe,你需要在环境变量里配置HADOOP_HOME,并把hadoop.dll放入C:\Windows\System32,否则启动 SparkSession 时会报错。也可以直接用 WSL 跑 Spark,我后来图省事就迁移到 WSL 了。

3. 数据从哪来:爬虫采集、清洗与落库全流程

3.1 房源字段设计:分析之前先把维度想清楚

很多同学一上来就写爬虫,爬到什么算什么,结果后面分析时发现数据缺胳膊少腿。我在写爬虫之前先把分析指标列出来,再倒推需要哪些字段。

最终我定下的房源字段包括:小区名称、所属行政区、板块、户型结构、建筑面积、朝向、楼层信息、总价、单价、建造年份、采集时间。这些字段覆盖了后面所有分析维度:区域分析需要行政区,户型分析需要户型结构,价格区间分析需要总价和面积,采光和新旧程度分析需要朝向和建造年份。

爬虫采集时把楼层信息拆成两个字段更合理:所在楼层和总楼层,方便后续算“中高区/低区”的楼层价格差异。如果只存一个“第12层/34层”这样的字符串,后期在 Spark 里还要用正则表达式拆分,多花不少事。

3.2 采集脚本的写法与限速策略

我用 requests + BeautifulSoup 完成采集脚本。核心逻辑很简单:先请求城市列表页拿到各区的链接,再请求每个区的房源列表页,解析每条房源详情链接,最后请求详情页解析字段。

采集中最值得提醒的是限速和异常处理。我每请求一次页面就time.sleep(random.uniform(1, 2)),随机延时能有效降低被反爬机制识别风险。同时给每个请求加上超时参数和重试逻辑,单条请求报错时记录日志让它继续跑,而不是中断整个任务。

代码结构大概是这样的:

import requests import random import time from bs4 import BeautifulSoup HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)..."} def fetch_page(url): for attempt in range(3): try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text except requests.RequestException as e: print(f"第{attempt + 1}次请求失败: {e}") time.sleep(random.uniform(1.5, 3)) return None def parse_house_item(html): soup = BeautifulSoup(html, "lxml") # 提取字段并返回 dict return {...}

采集到的数据我用 JSON Lines 格式保存,每一行是一个房源对象的 JSON。这种格式比一个大 JSON 数组更好,Spark 读取时天然支持逐行解析,不会一次性加载整个文件到内存。

还有一个容易被忽略的细节:采集过程记得保留“采集时间”字段。因为房源数据是滚动变化的,同一套房源今天下架明天又上架,有了采集时间你才能说明数据口径,论文里也能写“本次实验数据抓取于某年某月”。

3.3 清洗逻辑:缺值和异常值如何处理

爬下来的数据不可能直接用,脏数据比你想象的多。我总结的清洗规则有四条:

  1. 去重:同一套房源可能因为列表页和详情页重复解析而多次出现。我用“小区名称+户型+面积+总价”作为唯一键做去重,这个组合基本可以确定是同一套房源。
  2. 缺失值:面积缺失的整条删除,总价缺失但单价和面积完整的,用单价乘以面积补全。朝向和建造年份缺失的,用“未知”和“0”填充而不是删除,避免样本量缩水。
  3. 异常值过滤:单价低于每平米3000元的房源直接删除,南昌正常的二手房价格范围基本在5000到30000之间,低于3000元明显是录入错误。总价和面积之间的比例也要校验,单价超过正常区间上限的也一并删除。
  4. 类型转换:从页面抓下来的“120万元”“89.5平米”“南北通透”这些字符串,统一去掉单位转成浮点数,朝向字段做映射归一化,比如“南北通透”“南向”“东南向”等都归为具体朝向。

每一步清洗我都会把删除的数据量打印出来,并在论文里写清楚清洗规则和数据前后对比。这也是论文里“数据预处理”这一章的重要素材。

3.4 Spark 读取 JSON 与预处理的完整链路

清洗完的数据存成house_data.json后,就轮到 Spark 登场了。Spark 读取 JSON 非常简单:

from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("NanchangHouseAnalysis") \ .master("local[*]") \ .getOrCreate() df = spark.read.json("data/house_data.json") df.printSchema() df.show(5)

Spark 读取 JSON 文件时,会自动推断每列的 Schema,这比自己在代码里写死字段类型要省事得多。不过自动推断偶尔会把“总价”推断成 long 类型,而“单价”推断成 double 类型,所以在分析前最好显式转换一下字段类型:

from pyspark.sql.types import DoubleType, IntegerType df = df.withColumn("total_price", df["total_price"].cast(DoubleType())) \ .withColumn("unit_price", df["unit_price"].cast(DoubleType())) \ .withColumn("area", df["area"].cast(DoubleType()))

然后做全局清洗和去重:

clean_df = df.dropDuplicates(["community", "layout", "area", "total_price"]) \ .filter(df["unit_price"] > 3000) \ .filter(df["area"] > 10) \ .filter(df["total_price"] > 0)

到这一步,Spark 的工作流程已经覆盖了数据读取、Schema 推断、类型转换、去重、过滤。整个过程在日志里清晰可见,论文的技术原理部分也有的写了。清洗完成的数据我直接注册成临时表,后面所有分析都用 SQL 或 DataFrame 算子来跑,非常灵活。

4. Spark 房价分析引擎:我算了这四类指标

4.1 全市总指标:均价、总价、供需盘面

整个系统最上层的展示是全市房价总概况,这部分我用 Spark 算出几个核心指标:房源总量、平均单价、平均总价、单价中位数、总价中位数、最高单价小区。

平均单价容易被极端值拉偏,所以中位数很有必要。南昌像红谷滩沿江的豪宅单价可能到三四万,而湾里区的老小区单价可能只有五六千,平均单价会把这两个区域拧成一个看起来“还行”的数字,但中位数更能反映市场真实水平。

计算中位数用 Spark 的percentile_approx函数:

from pyspark.sql.functions import expr overview = clean_df.agg( expr("count(*)").alias("house_count"), expr("avg(unit_price)").alias("avg_unit_price"), expr("percentile_approx(unit_price, 0.5)").alias("median_unit_price"), expr("avg(total_price)").alias("avg_total_price") ) overview.show()

这些结果最终会展示在首页的指标卡片上,每一项数字背后都对应一段可解释的计算逻辑,答辩时被追问也不怕。

4.2 行政区与板块维度:南昌各区的价格梯度

区域对比是整个系统最有说服力的分析。南昌的行政区划包括东湖区、西湖区、青山湖区、青云谱区、红谷滩区、新建区、南昌县等,每个区的价格梯度非常明显。

用 Spark 的groupBy做聚合:

district_stats = clean_df.groupBy("district").agg( expr("count(*)").alias("house_count"), expr("avg(unit_price)").alias("avg_unit_price"), expr("avg(total_price)").alias("avg_total_price") ).orderBy("avg_unit_price", ascending=False) district_stats.show()

这个统计结果可以做成柱状图和地图热力图。柱状图适合展示各区域均价的排序,热力图则能直观反映“市中心贵、越往外围越便宜”的空间规律。

板块维度更细。我把“红谷滩区+板块”拼成一个字段,统计板块的房源量和均价,取 TOP10 热门板块。这个数据显示出来非常能打,比如红谷滩的凤凰洲、九龙湖、红角洲往往是高价板块,而老城区的某些板块则在价格低区。

4.3 户型、朝向、面积段与总价区间交叉分析

单看区域均价还不够,系统还需要展示房屋本身的属性对价格的影响。户型分析我用 groupBy 统计一室、两室、三室、四室及以上的房源数量和平均单价,能看出市场上最主流的户型结构以及不同户型的单价差异。

面积段和总价区间最好做成交叉分析。我的做法是先把面积字段离散化成区间:60平米以下、60到90平米、90到120平米、120到144平米、144平米以上。总价区间也离散化:100万以下、100到150万、150到200万、200到300万、300万以上。然后用 Spark 的when + otherwise生成新列,再groupBy联合统计。

以下是面积区间和价区间交叉的示意代码:

from pyspark.sql.functions import when area_range_df = clean_df.withColumn( "area_range", when(clean_df["area"] < 60, "60平以下") .when(clean_df["area"] < 90, "60-90平") .when(clean_df["area"] < 120, "90-120平") .when(clean_df["area"] < 144, "120-144平") .otherwise("144平以上") ) cross_stats = area_range_df.groupBy("area_range", "layout").agg( expr("avg(unit_price)").alias("avg_unit_price"), expr("count(*)").alias("cnt") ).orderBy("area_range")

这个交叉分析能为论文提供很多洞察,比如“90到120平的三室房源成交最活跃”“144平以上房源单价反而低于90平刚需盘”之类的结论。可视化时用堆叠柱状图或热力矩阵展示,效果非常好。

4.4 分析结果怎么回写数据库

Spark 算完之后,结果需要回写 MySQL 供 Django 展示。最直接的办法是用 Spark 的 JDBC 写入:

district_stats.write \ .mode("overwrite") \ .jdbc(url="jdbc:mysql://localhost:3306/housing", table="district_stats", properties={"user": "root", "password": "123456", "driver": "com.mysql.cj.jdbc.Driver"})

但这里有个实际坑:如果你频繁用mode("overwrite")写整个分析结果表,表结构可能会被自动重建,导致主键和字段类型变化。更稳妥的方案是:先把结果写成临时 CSV,再通过 Django 的loaddata命令或者写一个 Python 脚本读取 CSV 并 upsert 到数据库。

我当时采用的是两步法:Spark 先把所有分析结果输出成 CSV 文件,然后由 Django 的 management command 读取 CSV 并更新分析结果表。这样即使分析指标增加,也不需要频繁改 JDBC 配置,而且中途出错可以重复跑,不会污染数据库。核心分析任务封装成一个独立 Python 脚本run_analysis.py,放在spark_analysis/目录下,与 Web 工程分离。

5. Django Web 展示层:从后台查数据到 ECharts 可视化

5.1 页面规划与视图设计

Web 端我用的是 Django 的 MTV 架构,没有额外引入前端框架。整体页面分五个:

  1. 首页:全市核心指标卡片 + 近三月趋势图(如果有时间维度数据)。
  2. 区域分析页:各区均价柱状图 + 各区房源量占比饼图。
  3. 户型分析页:户型分布饼图 + 户型均价与房源量组合图。
  4. 价格区间页:总价区间频数分布柱状图 + 面积段与总价交叉热力图。
  5. 房源列表页:带筛选器的表格,支持区域、户型、价格区间组合筛选,分页展示。

视图层我不建议写复杂逻辑,直接在视图里查询数据库模型,然后通过模板渲染。一个典型视图是这样的:

from django.shortcuts import render from .models import DistrictStats def district_analysis(request): stats = DistrictStats.objects.order_by("-avg_unit_price") districts = [s.district for s in stats] avg_prices = [float(s.avg_unit_price) for s in stats] context = { "districts": districts, "avg_prices": avg_prices, "stats": stats, } return render(request, "analysis/district.html", context)

每个页面的context只需要包含图表需要的数据。为了数据传给 JavaScript 方便,我直接把列表再传给前端,在模板中用json_script过滤器序列化。

5.2 图表背后的 JSON 数据接口

页面里的图表用的都是 ECharts。有人喜欢用 Django Rest Framework 做一套 JSON API 再让前端 Ajax 拉取,但课程设计阶段没必要这么复杂。用json_script模板过滤器把数据渲染到页面里,ECharts 初始化时直接读取,实现最简单,也不会遇到跨域问题。

模板里大概是这样的:

<script> var districts = {{ districts|json_script:"district-data" }}; </script>

更标准的做法是:

{{ districts|json_script:"district_data" }} {{ avg_prices|json_script:"avg_price_data" }} <script> var districtNames = JSON.parse( document.getElementById('district_data').textContent ); var priceData = JSON.parse( document.getElementById('avg_price_data').textContent ); // 初始化 ECharts </script>

json_script是 Django 内置过滤器,专门用来安全地把 Python 对象转成 JSON 并插入模板,自动转义特殊字符,比手动 print 一个 JSON 字符串到<script>安全得多。

ECharts 初始化代码网上有很多现成示例,柱状图、饼图、折线图各抄一份改一改数据源就行。关键是把图表的标题、图例、单位标注清楚,让你的系统看起来像一个完整产品,而不是一个测试页面。

5.3 筛选、分页、搜索的实现

房源列表页是我觉得工作量最大但也最容易出彩的部分。用一个HouseFilterView来承载筛选逻辑:

def house_list(request): houses = HouseInfo.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: houses = houses.filter(district=district) if layout: houses = houses.filter(layout=layout) if min_price: houses = houses.filter(total_price__gte=min_price) if max_price: houses = houses.filter(total_price__lte=max_price) from django.core.paginator import Paginator paginator = Paginator(houses.order_by("-total_price"), 20) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) return render(request, "analysis/house_list.html", {"page_obj": page_obj})

筛选条件用 Django ORM 的链式查询就能完成,参数为空就不做过滤,非常简单。分页用 Django 自带 Paginator,模板里循环渲染页码即可。

这里有一个细节容易被忽略:当用户做筛选后点击第 2 页,URL 里的筛选参数会丢失,导致翻页后筛选条件被重置。解决办法是在分页链接里带上当前的 GET 参数:

base_query = request.GET.copy() # 在模板中拼接 query string

这个细节看起来小,但答辩时现场演示翻页筛选会让系统体验直观地好一大截。

5.4 Admin 后台与秒开的房源管理

Django Admin 是白送的功能,但很多同学懒得注册。实际上只要在admin.py里几行代码,就能获得一个完整的后台管理系统:

from django.contrib import admin from .models import HouseInfo, DistrictStats @admin.register(HouseInfo) class HouseInfoAdmin(admin.ModelAdmin): list_display = ("community", "district", "layout", "area", "total_price", "unit_price") list_filter = ("district", "layout") search_fields = ("community",) list_per_page = 20

房源管理后台支持按区和户型筛选,按小区名搜索,还能直接在线编辑和删除。值得一提的是 Django 执行查询-删除对象非常方便,在后台选中几条错误房源记录,一个“删除所选”操作就能解决,不需要手写 SQL。这也是论文里可以写进“系统功能模块”的素材。

Admin 后台不仅能管理房源数据,还能管理我后加的“分析结果表”。每次 Spark 分析更新完,后台里就能直接看到最新结果是否正确,方便我核对数据有没有算错。

6. 数据库建模与工程目录组织:别人拿到源码如何快速跑起来

6.1 三张核心表的关系设计

数据库我设计了四张表:房源信息表、区域分析结果表、户型分析结果表、总价区间分析结果表。不做用户表和权限表,因为系统定位是数据分析展示,而不是电商平台。

房源信息表是最核心的表,字段和爬虫采集字段对齐。下面是建表语句的核心字段:

CREATE TABLE `house_info` ( `id` int NOT NULL AUTO_INCREMENT, `community` varchar(100) NOT NULL COMMENT '小区名称', `district` varchar(50) NOT NULL COMMENT '行政区', `block` varchar(50) DEFAULT NULL COMMENT '板块', `layout` varchar(20) DEFAULT NULL COMMENT '户型结构', `area` double DEFAULT NULL COMMENT '建筑面积', `orientation` varchar(20) DEFAULT NULL COMMENT '朝向', `floor_info` varchar(20) DEFAULT NULL COMMENT '楼层信息', `total_price` double DEFAULT NULL COMMENT '总价', `unit_price` double DEFAULT NULL COMMENT '单价', `build_year` int DEFAULT NULL COMMENT '建造年份', `created_at` datetime DEFAULT NULL COMMENT '采集时间', PRIMARY KEY (`id`), KEY `idx_district` (`district`), KEY `idx_community` (`community`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

索引设计上,district和community是查询和筛选的高频字段,必须建索引。我实际测试时发现如果房源量达到几万条,不建索引的筛选请求会明显变慢,建索引后可以保持在几十毫秒。

分析结果表用区域表举例,其他两张结构类似:

CREATE TABLE `district_stats` ( `id` int NOT NULL AUTO_INCREMENT, `district` varchar(50) NOT NULL, `house_count` int DEFAULT NULL, `avg_unit_price` double DEFAULT NULL, `avg_total_price` double DEFAULT NULL, `analysis_date` date DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_district_date` (`district`, `analysis_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

区域表加了(district, analysis_date)唯一约束,这样每次更新分析结果不会产生重复记录。Django 里用update_or_create方法很轻松就能实现“有则更新、无则插入”的语义。

6.2 工程目录和交付物怎么组织

一个毕设项目除了代码能跑,最重要的还有工程组织是否清晰。我建议把整个项目分成四块,目录结构大致如下:

housing-analysis/ ├── crawler/ # 爬虫模块 │ └── nanchang_spider.py ├── spark_analysis/ # Spark 分析模块 │ ├── run_analysis.py │ └── output_csv/ ├── web/ # Django 工程 │ ├── manage.py │ ├── config/ # 项目配置 │ ├── analysis/ # 核心 app │ │ ├── models.py │ │ ├── views.py │ │ ├── templates/ │ │ └── static/ │ └── db.sqlite3 ├── docs/ # 论文和说明文档 │ ├── 设计文档.md │ ├── 答辩PPT提纲.md │ └── 使用说明.md ├── requirements.txt └── README.md

README.md 一定要写清楚三件事:环境版本、运行步骤、默认账号。很多同学把源码交上去,评审老师自己跑不起来,第一印象就差了。我的 README 里写明从创建虚拟环境、安装依赖、迁移数据库、导入数据、启动 Django 到手动跑 Spark 分析脚本的全过程,把每一步命令都列出来。

requirements.txt也要精确锁定版本,不要只写django或pyspark,而是写成Django==3.2.20、pyspark==3.1.2这种形式,避免别人装依赖时自动装到不兼容的版本。

6.3 运行环境与部署的注意点

本地开发 Debug 模式没问题,但如果想部署到服务器上演示,有几个坑必须提前处理:

静态文件:Django 在 Debug=False 时不会自动服务静态文件,需要先执行python manage.py collectstatic把静态文件收集到指定目录,再用 Nginx 或其他静态服务器提供访问。如果只是本地演示,写清楚“必须开着 Debug=True”也能接受。

数据库迁移:我用的 Django 3.2 默认支持 MySQL,需要在settings.py里配置数据库连接:

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

这里的OPTIONS里的 charset 配置非常关键,不配置的话插入中文数据很容易报 “Incorrect string value” 错误。

定时更新:如果想让数据保持新鲜,可以用系统自带的 cron 写定时任务,每天凌晨跑一次爬虫和 Spark 分析,Django 的custom management command里封装好更新逻辑,然后让 cron 调用它。当然,这个属于加分项,不做也不影响系统运行。

7. 从踩坑到答辩:真实记录与给后来者的建议

7.1 最痛苦的五个坑

这个项目我前后做了大概三周,其中一半时间花在环境和数据上。这里记录五个对我影响最大的坑:

第一个坑是 Windows 下 Spark 启动报错。明明pyspark已经装好,但一创建 SparkSession 就提示找不到winutils.exe。解决方案是下载对应 Hadoop 版本的 winutils 放入bin目录,并把HADOOP_HOME环境变量指过去。如果你的 Windows 一直解决不了,可以直接换 WSL,十分钟搞定,省心得多。

第二个坑是 Spark 和 JDK 版本不匹配。我一开始装了 JDK17,Spark 3.1.2 直接报UnsupportedClassVersionError。JDK 版本降到 1.8 之后就一切正常。记住:Spark 不是越新越好,而是和 JDK 的兼容性最重要。

第三个坑是爬虫采集字段类型混乱。页面上的价格有的写“万”,有的写“元/平”,有的还带“起”字,清洗时正则表达式改了好几版。最后我总结出一个原则:所有清洗逻辑必须集中在一个清洗函数里,每一条规则都要写清楚输入输出,切不要边爬边洗,后面复盘会非常痛苦。

第四个坑是 JSON 文件编码。Python 默认写文件用的是 UTF-8,如果爬虫页面本身是 GBK 编码而你没有正确解码,写入 JSON 后再被 Spark 读取,中文区域字段就会变成乱码区域,展示页面全是“???”。解决方案是请求时明确指定resp.encoding = "utf-8",写文件时open(..., encoding="utf-8"),Spark 读取时也指定编码方式。

第五个坑是海盗行为。Spark 读取本地 JSON 时,如果数据里有中文列名,某些版本的 Spark 会出现列名解析问题。我的应对策略是:所有 Spark 处理的字段统一用英文字段名,展示端的中文名在 Django 模板层做映射转换,从根源上避免中文 Schema 带来的坑。

7.2 答辩时经常被问的问题

提前把这些问题想好,答辩会稳很多。我把当时被问到的问题整理成一个清单:

问:数据量这么小,为什么不用 pandas?答:项目定位是模拟大数据分析流程,Spark 的 DataFrame API 和 pandas 用法相似,但底层是分布式执行引擎。当数据量增长到集群规模时,同一个分析脚本可以无缝部署到 YARN 或 Standalone 集群,而 pandas 受限于单机内存。这也体现了系统在架构层面的扩展性。

问:数据是怎么来的,会不会有法律问题?答:只采集可公开浏览的挂牌信息,不采集个人隐私字段,请求频率做了限速。采集数据仅用于学术研究,不进行任何商业用途。毕业论文里建议也把这一条写成“数据伦理声明”,会显得你考虑周到。

问:Spark 分析结果和数据库表是什么关系?答:Spark 是离线分析引擎,分析结果写入 MySQL 的分析结果表,Django Web 服务查询的是分析结果表,两个过程解耦。新数据入库后手动或定时触发分析任务即可更新结果。

问:系统最大的技术亮点是什么?答:可以强调两点:一是完整的数据全链路,从采集、清洗、分析到可视化是闭环;二是系统具备水平扩展能力,Spark 计算层可以扩展到集群,存储层可以扩展到分布式数据库,当前架构不是“只能处理南昌几千条数据”的一次性项目。

7.3 关于源码、数据库和万字文档的整理经验

不少同学最后会拿到一份带着源码、数据库、万字文档的项目包,但自己写的时候反而不知道怎么整理。我的经验是把“论文”和“项目代码”当成两条并行线来对待。

论文写作不要等项目全部完成再动笔。我的做法是:每完成一个模块,立刻把该模块的设计思路、实现代码、实验结果写进文档。做完全部模块后,文档初稿已经有了一大半,剩下的只是系统性润色。如果最后一周才开始写万字文档,很多细节都会遗忘,很容易写成流水账。

源码交付时,建议把spark_analysis、crawler、web三个目录分别写一个简短的README说明依赖和运行方式。别人拿到源码后能不能十分钟内跑起来,决定了他对这套系统评价的初始印象。数据库文件要附带一份建表 SQL 或者 Django 的迁移文件,不要让别人从零开始手动建表。我习惯把 Django 的迁移文件夹migrations完整保留,别人执行python manage.py migrate就能恢复所有表结构。

最后,一定要自己录一个 3 分钟的演示视频,从系统启动到每个页面轮流点一遍,说明数据从哪来、每个图表代表什么。答辩时现场系统崩了也不用怕,把视频放出来,然后从容解释原因。这个小习惯在很多关键场合救过我。

如果你正打算用同一套思路做类似系统,我最后想分享的心得是:先把最核心的闭环跑通,也就是“一百条数据入表—Spark 出一个统计结果—页面画出一张图”,再考虑加功能和做美化。很多同学包括我自己,最初都喜欢先搭框架、配环境、调样式,结果核心计算逻辑一直没跑通,时间一拖就慌了。先花两天做一个最小可运行版本,后面所有的迭代都会变得很踏实。南昌房价只是这套系统的一个应用场景,你换一个城市,换一套数据源,甚至把房价换成二手汽车、共享单车、商场客流,整个 Django+Spark 的骨架都能继续用。把链路打通了,以后遇到什么数据都不怵。

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

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

立即咨询