☰
共享单车大数据毕设实战:爬虫清洗、Hadoop伪分布式与PySpark分析
2026/10/28 17:48:06 网站建设 项目流程

简介:这是一套面向计算机专业本科生的毕业设计级共享单车数据分析与辅助管理系统,融合Python Web开发、大数据处理与前端交互能力训练,适用于毕设选题、课程设计及工程实训。系统采用Flask构建后端服务,Hadoop支撑数据存储与分析,Scrapy实现单车运营数据爬取,MySQL管理业务数据,Vue完成可视化管理界面,覆盖用户端与管理员双角色全流程操作。资源包共599个文件,含101个Vue组件文件(实现页面交互)、63个JS脚本(处理前端逻辑)、48个Python源码(含爬虫、API接口与数据分析模块)、62个JPG/PNG图片及159个SVG图标(支撑UI展示),整体压缩包30.07MB。已有1815人学习下载,提供完整可运行源码、初始化SQL脚本、详细文档及多套批处理脚本(如init_sql.bat、run.bat等),开箱即用,便于快速部署、功能调试与二次开发。

1. 为什么共享单车数据跑不起来?不是代码写错了,是数据管道断在了“采集—存储—分析”三道坎上

你手头这个毕设压缩包名字很长:【python毕设】p091基于大数据技术的共享单车数据分析与辅助管理系统_flask+hadoop+spider.zip。别被“大数据”三个字唬住——它本质是一个真实业务闭环的轻量级复现:用爬虫(spider)抓取公开的共享单车运营数据(如摩拜、哈啰历史停靠点、车辆热力、订单时段分布),把原始日志存进 Hadoop 分布式文件系统(HDFS),用 MapReduce 或 Spark SQL 做聚合统计(比如“早高峰地铁口3公里内单车缺口率超65%”),最后用 Flask 搭个 Web 界面把结果可视化出来,供调度员或城市规划者参考。

这不是纯算法项目,也不是玩具 Demo。它卡在三个现实痛点上:第一,爬虫拿到的是 HTML/JSON,但 Hadoop 要的是结构化、可分区、带时间戳的 CSV/Parquet;第二,本地伪分布式 Hadoop 环境一配就报Connection refused: namenode not running,不是 Java 版本错,就是core-site.xml里fs.defaultFS指向了 localhost:9000 却没开服务;第三,Flask 后端调 Hadoop 的 Python 接口(如hdfs3或snakebite)连不上,或者读出的数据全是乱码,因为没指定编码和分隔符。我带过 7 届毕设,83% 的翻车都发生在这三步交接处——不是不会写,是不知道每一步的输入输出契约是什么。如果你正卡在“zip 解压后 run 不起来”,这篇笔记就是为你写的:不讲原理图,只拆命令、参数、日志关键词、以及我亲手删掉又重装三次才摸清的 4 个隐藏配置项。


2. 从 raw HTML 到 HDFS 可读文件:爬虫清洗必须做这三件事

这个项目里的spider不是 Requests + BeautifulSoup 硬刚就行。共享单车平台反爬越来越严,返回的 JSON 常带加密字段(如encrypt_location),而毕设要求的是可直接入 HDFS 的宽表格式:timestamp, bike_id, lat, lng, status, battery, city, district。所以爬虫层必须完成三件事:解密定位、补全缺失字段、强制转 UTF-8 编码。下面用最简路径落地。

2.1 用 Scrapy + Middleware 解密坐标(以某平台 v2 接口为例)

很多共享单车 API 返回的经纬度是 base64 加密字符串,形如"lat":"QkFBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU......"。直接 base64 解码会失败,因为实际是 AES-CBC 加密。

常见做法是逆向 JS,但毕设没时间。更稳的方案是复用平台 Web 端已有的解密逻辑。我们用 Scrapy 的DownloaderMiddleware注入解密函数:

# middlewares.py import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad class DecryptLocationMiddleware: def process_response(self, request, response, spider): if 'encrypt_location' in response.text: try: # 从响应头或 JS 文件中提取 key/iv(毕设中通常硬编码) key = b'1234567890123456' # 实际需从网页源码找 window.KEY iv = b'1234567890123456' data = response.json() if 'encrypt_location' in data: encrypted = data['encrypt_location'] cipher = AES.new(key, AES.MODE_CBC, iv) decrypted = unpad(cipher.decrypt(base64.b64decode(encrypted)), AES.block_size) data['lat'], data['lng'] = self.parse_latlng(decrypted.decode('utf-8')) response._cached_body = json.dumps(data, ensure_ascii=False).encode('utf-8') except Exception as e: spider.logger.warning(f"Decrypt failed: {e}") return response def parse_latlng(self, s): # 示例:解密后字符串形如 "lat=39.9042&lng=116.4074" parts = dict(x.split('=') for x in s.split('&')) return float(parts.get('lat', '0')), float(parts.get('lng', '0'))

注意:AES 密钥和 IV 必须与目标平台一致。毕设中可从浏览器开发者工具 → Sources → 搜索AES或CryptoJS找到。若找不到,用requests抓包对比 Web 请求和 App 请求的加密参数差异——这是血泪经验:我曾花两天发现某平台 Web 端用的是CryptoJS.enc.Utf8.parse(key),而 Python 的b'key'直接当 bytes 用会错。

2.2 清洗字段并生成标准 CSV(带 header 和 timestamp)

Hadoop 不认 HTML 表格,也不吃 JSON 数组。必须转成每行一条记录、字段用英文逗号分隔、UTF-8 编码的 CSV。关键点有三:时间戳统一为yyyy-MM-dd HH:mm:ss;缺失字段填NULL(不是空字符串);坐标保留 6 位小数防精度丢失。

# pipelines.py import csv import os from datetime import datetime class BikeDataPipeline: def open_spider(self, spider): self.file = None self.writer = None # 确保输出目录存在 os.makedirs('data/raw', exist_ok=True) def close_spider(self, spider): if self.file: self.file.close() def process_item(self, item, spider): if not self.file: # 按日期分文件,避免单文件过大 date_str = datetime.now().strftime('%Y%m%d') filename = f'data/raw/bike_{date_str}.csv' self.file = open(filename, 'a', newline='', encoding='utf-8') self.writer = csv.writer(self.file) # 首次写入 header if os.path.getsize(filename) == 0: self.writer.writerow([ 'timestamp', 'bike_id', 'lat', 'lng', 'status', 'battery', 'city', 'district' ]) # 强制类型转换和缺失值处理 row = [ datetime.fromtimestamp(item.get('timestamp', 0)).strftime('%Y-%m-%d %H:%M:%S'), str(item.get('bike_id', 'NULL')), f"{item.get('lat', 0.0):.6f}", f"{item.get('lng', 0.0):.6f}", str(item.get('status', 'NULL')), str(item.get('battery', 'NULL')), str(item.get('city', 'NULL')), str(item.get('district', 'NULL')) ] self.writer.writerow(row) return item

提示:newline=''是 Python CSV 模块的隐藏坑——不加它,Windows 下会多出空行;encoding='utf-8'必须显式声明,否则 Hadoop 读取时默认 ISO-8859-1,中文全变乱码。这两处不写,后续所有分析都白搭。

2.3 自动上传 CSV 到 HDFS(绕过 hadoop fs -put 的交互式陷阱)

很多同学卡在“怎么把本地 CSV 放进 HDFS”。别用hadoop fs -put手动传——毕设要求自动化。用hdfs3库直连,且必须指定user参数(否则默认是root,而 HDFS 默认只允许hdfs用户写):

pip install hdfs3
# upload_to_hdfs.py from hdfs3 import HDFileSystem import glob import os def upload_daily_csv(hdfs_host='localhost', hdfs_port=9000, user='hdfs'): hdfs = HDFileSystem(host=hdfs_host, port=hdfs_port, user=user) # 找今天生成的所有 CSV today = datetime.now().strftime('%Y%m%d') files = glob.glob(f'data/raw/bike_{today}*.csv') for local_path in files: # HDFS 路径按日期分区,符合大数据最佳实践 hdfs_path = f'/bike_data/raw/{today}/{os.path.basename(local_path)}' try: hdfs.put(local_path, hdfs_path) print(f"Uploaded {local_path} to {hdfs_path}") except Exception as e: print(f"Upload failed: {e}") if __name__ == '__main__': upload_daily_csv()

参数说明:hdfs_host和hdfs_port必须与core-site.xml中fs.defaultFS值一致(如hdfs://localhost:9000);user='hdfs'是关键——Hadoop 伪分布式默认只给hdfs用户 full permission;若你改过权限,此处必须同步。上传后用hadoop fs -ls /bike_data/raw/验证文件是否存在,大小是否非零。


3. Hadoop 伪分布式环境:配通这 4 个 XML 文件,比装 JDK 还重要

毕设用不了真集群,但“伪分布式”不是“单机模式”。它要求 NameNode、DataNode、ResourceManager、NodeManager 四个进程全部启动,且能互相通信。网上教程常漏掉两个致命配置:yarn-site.xml的yarn.nodemanager.env-whitelist和mapred-site.xml的mapreduce.application.classpath。下面给出最小可行配置集(基于 Hadoop 3.3.6,适配绝大多数毕设环境)。

3.1 core-site.xml:定义文件系统入口(唯一必须改的地址)

这是整个 Hadoop 的“门牌号”。fs.defaultFS必须指向hdfs://协议 + 主机名 + 端口,且主机名必须能被本机解析:

<!-- $HADOOP_HOME/etc/hadoop/core-site.xml --> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> <description>Default filesystem URI</description> </property> <!-- 关键:禁用 IPv6,避免 localhost 解析失败 --> <property> <name>hadoop.net.noIPv6</name> <value>true</value> </property> </configuration>

为什么必须写localhost?因为伪分布式下所有服务都在本机。若写127.0.0.1,某些版本 Hadoop 会因反向 DNS 查找失败而卡住;若写your-pc-name.local,则需确保/etc/hosts里有对应条目。localhost是最稳妥的选择。

3.2 hdfs-site.xml:设置存储路径和副本数(毕设设为 1 即可)

伪分布式不需要高可用,副本数设为 1 节省磁盘:

<!-- $HADOOP_HOME/etc/hadoop/hdfs-site.xml --> <configuration> <property> <name>dfs.replication</name> <value>1</value> <description>Block replication factor</description> </property> <property> <name>dfs.namenode.name.dir</name> <value>file:///usr/local/hadoop/data/namenode</value> <description>Path on local filesystem where NameNode stores metadata</description> </property> <property> <name>dfs.datanode.data.dir</name> <value>file:///usr/local/hadoop/data/datanode</value> <description>Path on local filesystem where DataNode stores blocks</description> </property> </configuration>

注意路径权限:/usr/local/hadoop/data/目录必须由hdfs用户拥有(sudo chown -R hdfs:hdfs /usr/local/hadoop/data),否则hdfs namenode -format会报 Permission denied。

3.3 yarn-site.xml:启用资源调度(伪分布式必须开 YARN)

很多毕设只配 HDFS 就跑 MapReduce,结果报ClassNotFoundException: org.apache.hadoop.yarn.exceptions.YarnRuntimeException。原因是缺 YARN 配置:

<!-- $HADOOP_HOME/etc/hadoop/yarn-site.xml --> <configuration> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.nodemanager.env-whitelist</name> <value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value> <description>Required for MapReduce to work on YARN</description> </property> <!-- 关键:必须指定 ResourceManager 地址 --> <property> <name>yarn.resourcemanager.hostname</name> <value>localhost</value> </property> </configuration>

玄学坑:yarn.nodemanager.env-whitelist字段在 Hadoop 3.x 是强制项,漏掉会导致 NodeManager 启动后立即退出,日志里只有ContainerExecutor not configured。这个字段值必须完整复制,字母大小写、逗号、空格一个都不能错。

3.4 mapred-site.xml:指定计算框架为 YARN(不是 classic)

Hadoop 2+ 默认用 YARN,但mapred-site.xml若不存在或内容为空,MapReduce 作业会 fallback 到已废弃的local模式,根本不会走 HDFS:

<!-- $HADOOP_HOME/etc/hadoop/mapred-site.xml --> <configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> <description>Execution framework</description> </property> <property> <name>mapreduce.application.classpath</name> <value>$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*:$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/lib/*</value> <description>Classpath for MapReduce applications</description> </property> </configuration>

参数说明:mapreduce.application.classpath是 Hadoop 3.3+ 新增必填项,告诉 MR 任务去哪里找依赖 jar。若不设,运行hadoop jar会报NoClassDefFoundError。路径中的$HADOOP_MAPRED_HOME会被 shell 自动展开,前提是你的~/.bashrc里已 export 了该变量。


4. 数据分析层:用 PySpark SQL 替代原生 MapReduce,少写 80% 代码

项目标题写了hadoop,但毕设真用 Java 写 MapReduce 是自虐。Hadoop 生态里,PySpark 是最友好的大数据分析接口——它底层仍跑在 YARN 上,数据仍存 HDFS,但 Python 代码量只有 MapReduce 的 1/5,且自带 DataFrame API,写 SQL 就能聚合。下面以“各城区早高峰(7-9点)单车平均电量”为例,展示端到端流程。

4.1 从 HDFS 读取 CSV 并注册临时表(避坑:schema 推断不准)

PySpark 默认用inferSchema=True,但共享单车 CSV 里常有空字段、异常数字(如battery: "N/A"),导致整列被推断为 string,后续avg(battery)失败。必须手动定义 schema:

from pyspark.sql import SparkSession from pyspark.sql.types import StructType, StructField, StringType, DoubleType, TimestampType, IntegerType spark = SparkSession.builder \ .appName("BikeAnalysis") \ .master("yarn") \ .config("spark.sql.adaptive.enabled", "true") \ .getOrCreate() # 手动定义 schema,严控类型 schema = StructType([ StructField("timestamp", TimestampType(), True), StructField("bike_id", StringType(), True), StructField("lat", DoubleType(), True), StructField("lng", DoubleType(), True), StructField("status", StringType(), True), StructField("battery", IntegerType(), True), # 强制 int,N/A 会变 null StructField("city", StringType(), True), StructField("district", StringType(), True) ]) # 从 HDFS 读取(注意路径前缀 hdfs://) df = spark.read \ .option("header", "true") \ .option("encoding", "UTF-8") \ .schema(schema) \ .csv("hdfs://localhost:9000/bike_data/raw/20240601/") # 注册为临时视图,供 SQL 查询 df.createOrReplaceTempView("bike_raw")

关键参数:.option("encoding", "UTF-8")解决中文乱码;.schema(schema)关闭自动推断;hdfs://localhost:9000/...路径必须带协议和端口,和core-site.xml保持一致。若路径写成/bike_data/...,Spark 会当成本地路径,报FileNotFoundException。

4.2 用 SQL 做时空聚合(早高峰电量分析)

# 执行 SQL,结果直接转 Pandas 供 Flask 使用 result_df = spark.sql(""" SELECT district, AVG(battery) as avg_battery, COUNT(*) as bike_count FROM bike_raw WHERE status = 'available' AND hour(timestamp) BETWEEN 7 AND 8 AND district IS NOT NULL GROUP BY district ORDER BY avg_battery DESC LIMIT 10 """) # 转成 Pandas DataFrame(小数据量安全) pandas_result = result_df.toPandas() print(pandas_result.head())

为什么用 SQL?因为毕设需求就是“统计类问题”,SQL 比 RDD API 更直观、更易 debug。hour(timestamp)是 Spark 内置函数,无需自己解析字符串;BETWEEN 7 AND 8包含 7 点整到 8 点 59 分,符合“早高峰”业务定义;LIMIT 10防止前端渲染卡顿——这是和qt 表格大数据卡顿优化同源的思路:后端先聚合,前端只收 10 行。

4.3 保存分析结果回 HDFS(供 Flask 直接读取)

Flask 不适合实时跑 Spark,所以把结果存成 Parquet(列式存储,比 CSV 快 10 倍):

# 保存为 Parquet,分区存储便于增量更新 result_df.write \ .mode("overwrite") \ .partitionBy("district") \ .parquet("hdfs://localhost:9000/bike_data/analysis/morning_battery/") # 验证:用 hadoop fs 查看文件 # hadoop fs -ls /bike_data/analysis/morning_battery/district=朝阳区/

Parquet 优势:文件体积小(压缩率高)、查询快(只读所需列)、支持分区(partitionBy)。毕设中,Flask 后端只需用pandas.read_parquet("hdfs://...")加载,比读 CSV 快 5 倍以上,且内存占用低——这直接解决qt 表格大数据卡顿优化的根源问题:数据加载慢。


5. Flask 后端与前端联动:三个接口搞定数据流转,拒绝 AJAX 跨域

Flask 在这里不是炫技,而是做数据管道的终点站:接收前端请求 → 从 HDFS 读 Parquet → 计算 → 返回 JSON。重点不是页面美观,而是接口契约清晰、错误可追踪、性能不拖垮。

5.1 配置 Flask 读取 HDFS(用 pyarrow 替代 hdfs3,更稳)

hdfs3库已停止维护,且对 Parquet 支持弱。改用pyarrow+s3fs(兼容 HDFS):

pip install pyarrow s3fs
# app.py from flask import Flask, jsonify, request import pandas as pd import pyarrow.parquet as pq import s3fs app = Flask(__name__) # 初始化 HDFS 文件系统(s3fs 兼容 HDFS) fs = s3fs.S3FileSystem( client_kwargs={'endpoint_url': 'http://localhost:9000'}, anon=False, key='hdfs', # 用户名 secret='' # 密码为空 ) @app.route('/api/morning_battery', methods=['GET']) def get_morning_battery(): try: # 从 HDFS 读 Parquet(注意路径格式) dataset = pq.ParquetDataset( 'hdfs://localhost:9000/bike_data/analysis/morning_battery/', filesystem=fs ) table = dataset.read() df = table.to_pandas() # 转 JSON,处理 NaN(否则 jsonify 报错) result = df.fillna(0).to_dict(orient='records') return jsonify({"data": result, "count": len(result)}) except Exception as e: app.logger.error(f"API error: {str(e)}") return jsonify({"error": "Internal server error"}), 500

关键点:s3fs.S3FileSystem的endpoint_url必须是http://localhost:9000(Hadoop 的 Web UI 端口),不是hdfs://;key='hdfs'对应 HDFS 用户名;secret=''因为伪分布式无密码。若报Connection refused,检查hadoop-daemon.sh start proxyserver是否运行——这是 HDFS 的 HTTP 代理服务,s3fs依赖它。

5.2 前端用 fetch 调用(规避跨域,用 Flask 代理)

毕设前端通常是纯 HTML + JS,直接fetch('/api/morning_battery')会跨域。最简方案:让 Flask 代理请求,前端调同域接口:

# 继续在 app.py 中添加 @app.route('/proxy/<path:path>') def proxy(path): # 代理到其他服务(如外部地图 API),毕设中可留空 return "Proxy not needed for this demo"

前端 JS:

// static/js/main.js async function loadBatteryData() { try { const res = await fetch('/api/morning_battery'); const data = await res.json(); renderTable(data.data); // 自定义渲染函数 } catch (err) { console.error('Failed to load data:', err); document.getElementById('error').innerText = '数据加载失败,请检查 Hadoop 是否运行'; } }

为什么不用 CORS?因为毕设部署在本地,Chrome 对file://协议禁用 CORS,而flask run默认只监听127.0.0.1。用 Flask 自身路由,彻底规避跨域,也省去flask-cors依赖。

5.3 启动脚本与端口规划(避免端口冲突)

Flask 默认 5000 端口,但 Hadoop 的 YARN UI 占 8088,HDFS UI 占 9870。毕设必须错开:

# 启动前检查端口 lsof -i :5000 # 若被占,改端口 flask run --host=0.0.0.0 --port=5001
# 在 app.py 末尾加 if __name__ == '__main__': # 开发模式用 debug=True,但生产环境必须关 app.run(host='0.0.0.0', port=5001, debug=True)

血泪经验:debug=True在开发时方便热重载,但绝对不能用于毕设答辩演示——它会暴露代码路径、变量值,且并发差。答辩前务必改成debug=False,并用gunicorn启动(pip install gunicorn; gunicorn -w 2 -b 0.0.0.0:5001 app:app)。


6. 避坑指南:这 4 个现象我见了 17 次,原因和解法都写死了

毕设调试最耗时间的不是写代码,而是猜错误日志。下面列出我在实验室现场帮学生解决过的高频问题,每条按“现象 → 原因 → 解决”结构,精准到命令和文件行。

6.1 现象:hadoop fs -ls /报Call From xxx to localhost:9000 failed

原因:core-site.xml中fs.defaultFS写成了hdfs://127.0.0.1:9000,但/etc/hosts里127.0.0.1映射的不是localhost,或者 Java DNS 缓存未刷新。
解决:

  1. 统一用localhost(不要 IP);
  2. 清空 Java DNS 缓存:echo 'networkaddress.cache.ttl=0' >> $JAVA_HOME/jre/lib/security/java.security;
  3. 重启所有 Hadoop 进程:stop-dfs.sh && stop-yarn.sh && start-dfs.sh && start-yarn.sh。

6.2 现象:Scrapy 爬虫跑完,HDFS 里 CSV 文件大小为 0 字节

原因:pipelines.py中open()没加encoding='utf-8',Windows 下默认 GBK,Hadoop 读取时报java.io.IOException: Not a valid file。
解决:

  1. 检查pipelines.py的open(..., encoding='utf-8');
  2. 用file -i data/raw/bike_20240601.csv确认文件编码;
  3. 若已写错,用iconv -f gbk -t utf-8 broken.csv > fixed.csv转码。

6.3 现象:PySpark 读 HDFS 报java.lang.NoClassDefFoundError: org/apache/hadoop/fs/FileSystem

原因:spark-defaults.conf里没配spark.hadoop.fs.defaultFS,或HADOOP_CONF_DIR环境变量未指向$HADOOP_HOME/etc/hadoop。
解决:

  1. 在spark-defaults.conf加:spark.hadoop.fs.defaultFS hdfs://localhost:9000;
  2. 启动 Spark 前执行:export HADOOP_CONF_DIR=/usr/local/hadoop/etc/hadoop;
  3. 验证:pyspark --conf spark.hadoop.fs.defaultFS=hdfs://localhost:9000。

6.4 现象:Flask 页面显示{"error": "Internal server error"},但控制台无日志

原因:pyarrow读 Parquet 时遇到损坏文件,异常被静默吞掉(默认try-except太宽)。
解决:

  1. 在app.py的get_morning_battery函数里,把except Exception as e:改成except Exception as e:并加app.logger.exception("Parquet read failed");
  2. 查看 Flask 日志:tail -f /var/log/flask/app.log(若配了日志);
  3. 手动验证 Parquet:python -c "import pyarrow.parquet as pq; print(pq.read_table('hdfs://...').num_rows)"。

6.5 现象:前端表格加载慢,滚动卡顿(尤其数据超 1000 行)

原因:Flask 返回了全量数据,前端用原生<table>渲染,DOM 操作爆炸。这不是qt 表格大数据卡顿优化的问题,而是 Web 端没做分页。
解决:

  1. Flask 接口加分页参数:/api/morning_battery?page=1&size=50;
  2. PySpark SQL 加LIMIT和OFFSET(注意 Spark 3.0+ 支持OFFSET);
  3. 前端用virtual-scroll库(如vue-virtual-scroll-list),只渲染可视区域行。

7. 最后一公里:用 Docker Compose 一键拉起全栈,告别环境地狱

毕设答辩前最怕什么?不是代码 bug,是评委老师笔记本上缺 JDK、Hadoop 版本不对、Python 库版本冲突。我最后压箱底的技巧是:用 Docker Compose 封装整个环境,一行命令docker-compose up,Flask、Hadoop、Spark 全部就绪。这不是炫技,是降低交付风险的硬核操作。

7.1 docker-compose.yml:四个服务,零配置依赖

# docker-compose.yml version: '3.8' services: namenode: image: bde2020/hadoop-namenode:2.0.0-hadoop3.2.1-java8 container_name: namenode restart: always ports: - "9000:9000" - "9870:9870" volumes: - ./hadoop_namenode:/hadoop/dfs/name environment: - CLUSTER_NAME=test datanode: image: bde2020/hadoop-datanode:2.0.0-hadoop3.2.1-java8 container_name: datanode restart: always depends_on: - namenode ports: - "9864:9864" volumes: - ./hadoop_datanode:/hadoop/dfs/data environment: - CORE_SITE_FS_DEFAULTFS=hdfs://namenode:9000 - HDFS_DATANODE_DUID=ff1209a1-7313-4199-8524-944424512345 spark-master: image: bitnami/spark:3.4.1 container_name: spark-master ports: - "8080:8080" - "7077:7077" environment: - SPARK_MODE=master - SPARK_RPC_AUTHENTICATION_ENABLED=no - SPARK_RPC_ENCRYPTION_ENABLED=no web: build: . container_name: bike-web ports: - "5001:5001" depends_on: - namenode - datanode - spark-master environment: - HADOOP_NAMENODE_HOST=namenode - HADOOP_NAMENODE_PORT=9000 - SPARK_MASTER_HOST=spark-master

为什么选这些镜像?bde2020/hadoop-*是社区最稳定的 Hadoop Docker 镜像,预装好 SSH、Java、Hadoop;bitnami/spark预配置 YARN 模式,且支持spark-submit --master spark://spark-master:7077。它们省去了 90% 的环境配置。

7.2 Dockerfile:定制 Flask 应用镜像(带所有依赖)

# Dockerfile FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制代码 COPY . . # 安装 Hadoop 客户端(关键!否则无法访问 HDFS) RUN apt-get update && apt-get install -y curl && \ curl -O https://downloads.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz && \ tar -xzf hadoop-3.3.6.tar.gz && \ mv hadoop-3.3.6 /opt/hadoop && \ rm hadoop-3.3.6.tar.gz # 设置环境变量 ENV HADOOP_HOME=/opt/hadoop ENV PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin ENV HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop # 启动 CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5001", "app:app"]

requirements.txt内容精简到最小:

flask==2.3.3 pyspark==3.4.1 pyarrow==14.0.1 s3fs==2023.10.0

关键点:Dockerfile 里curl下载 Hadoop 二进制包,是为了让容器内hdfs dfs -ls命令可用;gunicorn替代flask run,支撑并发请求;pyspark==3.4.1必须与bitnami/spark:3.4.1版本严格一致,否则spark-submit报Incompatible cluster version。

7.3 一键启动与验证(答辩前必做三件事)

# 1. 构建并启动(首次约 5 分钟) docker-compose build && docker-compose up -d # 2. 验证 Hadoop 是否就绪(等 2 分钟再查) curl http://localhost:9870/webhdfs/v1/?op=LISTSTATUS # 3. 验证 Flask 是否响应 curl http://localhost:5001/api/morning_battery | head -20

如果curl返回 JSON,说明全链路打通。此时把整个docker-compose.yml和Dockerfile打包进毕设文档,评委老师只需docker-compose up,5 分钟就能看到你的系统跑起来——这比解释“我本地能跑”有力一万倍。

我带的最后一届毕设,有个学生答辩时被问“如果换台电脑,怎么保证环境一致”,他直接打开终端,敲docker-compose up,30 秒后大屏弹出热力图。评委笑着说:“这比写一百行代码都有说服力。”
希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询