
如果你正被毕业设计搞得焦头烂额看到“基于DjangoSpark的南昌房价数据分析系统”这个题目很容易本能地把它归类为“听起来就很重”的项目。但实际上这类题目的核心难度并没有想象中高真正要做的只是把 Django、Spark、数据分析 和数据库 四件套串成一条完整的数据链路爬虫拿到数据、Spark 清洗与聚合、数据库持久化、Django 展示。它既有技术层次又不需要海量数据一个人完全能独立完成。这篇文章不是把源码再贴一遍而是把那套系统从选题、技术选型、代码实现到答辩的整个思考过程讲清楚还附带一些我在做类似项目时踩过的坑。最适合正在做课程设计或毕业设计并且想用同一套思路完成一个完整系统的同学。即使你手里没有南昌的数据把城市换成成都、杭州、长沙整个系统骨架一样能复用。1. 为什么选这个题目南昌房价数据背后的开发空间1.1 房价分析天然具备“数据量合适、维度多、可视化出效果”三重优势毕业设计最怕什么最怕选题“看起来很大做起来很空”。纯搞算法模型容易被质疑工作量纯搞增删改查又显得没技术含量。房价数据分析恰好介于两者之间数据源公开、指标丰富、可视化结果直观。南昌作为具体研究对象有其独到之处。它不像北京上海那样房源数据量大到单机处理有压力也不像县城那样样本太少没有统计学意义。南昌各区之间的房价差异明显比如红谷滩区的新盘和老城区的老破小价格能差出一倍以上这就让“区域维度分析”这种常规功能有了真实的解读空间。整个数据量级控制在几千到几万条单机 Spark 跑起来毫无压力但你又确实用到了分布式计算框架写论文时有东西可讲。1.2 毕业设计评审到底在考核什么我后来复盘发现评审老师看一个毕设项目重点看三件事功能是否闭环从数据采集、数据存储、数据分析到前端展示链路完整比单个功能炫酷重要得多。技术栈是否有层次前后端分离、大数据框架、数据库设计、可视化每一条都能在答辩时展开讲。工作量是否可感知项目里的代码量、文档篇幅、图表数量都是“肉眼可见”的工作量。DjangoSpark这套组合恰好能把这三件事全部覆盖。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 分析任务更新分析结果。这个架构在产业界也常见属于离线数仓里典型的 T1 批处理模式。我强烈建议你不要在 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, headersHEADERS, timeout10) 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 清洗逻辑缺值和异常值如何处理爬下来的数据不可能直接用脏数据比你想象的多。我总结的清洗规则有四条去重同一套房源可能因为列表页和详情页重复解析而多次出现。我用“小区名称户型面积总价”作为唯一键做去重这个组合基本可以确定是同一套房源。缺失值面积缺失的整条删除总价缺失但单价和面积完整的用单价乘以面积补全。朝向和建造年份缺失的用“未知”和“0”填充而不是删除避免样本量缩水。异常值过滤单价低于每平米3000元的房源直接删除南昌正常的二手房价格范围基本在5000到30000之间低于3000元明显是录入错误。总价和面积之间的比例也要校验单价超过正常区间上限的也一并删除。类型转换从页面抓下来的“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, ascendingFalse) 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(urljdbc:mysql://localhost:3306/housing, tabledistrict_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 架构没有额外引入前端框架。整体页面分五个首页全市核心指标卡片 近三月趋势图如果有时间维度数据。区域分析页各区均价柱状图 各区房源量占比饼图。户型分析页户型分布饼图 户型均价与房源量组合图。价格区间页总价区间频数分布柱状图 面积段与总价交叉热力图。房源列表页带筛选器的表格支持区域、户型、价格区间组合筛选分页展示。视图层我不建议写复杂逻辑直接在视图里查询数据库模型然后通过模板渲染。一个典型视图是这样的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 /scriptjson_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(districtdistrict) if layout: houses houses.filter(layoutlayout) if min_price: houses houses.filter(total_price__gtemin_price) if max_price: houses houses.filter(total_price__ltemax_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引设计上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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;区域表加了(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.mdREADME.md 一定要写清楚三件事环境版本、运行步骤、默认账号。很多同学把源码交上去评审老师自己跑不起来第一印象就差了。我的 README 里写明从创建虚拟环境、安装依赖、迁移数据库、导入数据、启动 Django 到手动跑 Spark 分析脚本的全过程把每一步命令都列出来。requirements.txt也要精确锁定版本不要只写django或pyspark而是写成Django3.2.20、pyspark3.1.2这种形式避免别人装依赖时自动装到不兼容的版本。6.3 运行环境与部署的注意点本地开发 Debug 模式没问题但如果想部署到服务器上演示有几个坑必须提前处理静态文件Django 在 DebugFalse 时不会自动服务静态文件需要先执行python manage.py collectstatic把静态文件收集到指定目录再用 Nginx 或其他静态服务器提供访问。如果只是本地演示写清楚“必须开着 DebugTrue”也能接受。数据库迁移我用的 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 版本不匹配。我一开始装了 JDK17Spark 3.1.2 直接报UnsupportedClassVersionError。JDK 版本降到 1.8 之后就一切正常。记住Spark 不是越新越好而是和 JDK 的兼容性最重要。第三个坑是爬虫采集字段类型混乱。页面上的价格有的写“万”有的写“元/平”有的还带“起”字清洗时正则表达式改了好几版。最后我总结出一个原则所有清洗逻辑必须集中在一个清洗函数里每一条规则都要写清楚输入输出切不要边爬边洗后面复盘会非常痛苦。第四个坑是 JSON 文件编码。Python 默认写文件用的是 UTF-8如果爬虫页面本身是 GBK 编码而你没有正确解码写入 JSON 后再被 Spark 读取中文区域字段就会变成乱码区域展示页面全是“”。解决方案是请求时明确指定resp.encoding utf-8写文件时open(..., encodingutf-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 出一个统计结果—页面画出一张图”再考虑加功能和做美化。很多同学包括我自己最初都喜欢先搭框架、配环境、调样式结果核心计算逻辑一直没跑通时间一拖就慌了。先花两天做一个最小可运行版本后面所有的迭代都会变得很踏实。南昌房价只是这套系统的一个应用场景你换一个城市换一套数据源甚至把房价换成二手汽车、共享单车、商场客流整个 DjangoSpark 的骨架都能继续用。把链路打通了以后遇到什么数据都不怵。