南京二手房数据采集与可视化分析:Python全链路实战
简介基于Python的南京二手房数据采集及可视化分析毕业设计资料包面向计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生以及希望借助完整项目提升数据分析与可视化实战能力的开发者。项目以南京二手房市场为研究对象覆盖从数据采集、数据清洗到可视化分析、答辩汇报的完整链路代码经导师指导并获99分高分评价结构完整可直接运行大幅简化搭建流程。压缩包共156个文件、约39.99MB核心包括18个Python源码脚本、18个CSV数据集含UTF-8/ANSI原始与清洗多版本、15个HTML可视化页面、11个JS交互脚本、答辩PPT及数据库相关文件等目录划分清晰方便按模块查阅复现。原始与清洗后的CSV对照版本能帮助理解数据预处理思路PNG图表与HTML页面则直观展示分析结果。目前已有81人学习/下载适合作为毕设模板、课程设计参考或项目练习素材便于快速借鉴高分项目的组织方式。1. 这套南京二手房毕设采集、清洗到可视化的全链路能直接跑通做毕设最怕的不是题目难而是资料东拼西凑、代码跑不起来、答辩一问就露馅。这套基于Python的南京二手房数据采集及可视化分析项目把数据采集、清洗、入库、可视化四条链路全打通了还带着完整的CSV数据文件、数据库脚本和答辩资料。它的价值不在于代码多炫而在于你拿到手能照着复现先跑通爬虫把数据抓下来再用pandas清洗成规整的结构化数据存进数据库后通过后端接口把数据抛给前端图表。对于计算机相关专业正在做课程设计、期末大作业或者毕业设计的同学来说它最大的意义是省掉了从零搭框架的试错成本。我拆过不少同类资源这套的完整度属于拿到了就能用的级别前提是你要理解它每一层在干什么。2. 先看清资源里的数据文件原始、清洗、编码差异全在这打开资源包你会看到一整套CSV文件文件名看起来重复其实每一份的用途都不一样。理解这些文件的关系是复现整个项目的第一步也是后面排查问题的起点。2.1 原始数据与清洗数据的分工资源里有两类命名的文件一类带origin另一类带clean分别对应清洗前和清洗后的状态。文件类型文件示例用途原始抓取数据ershoufang-origin-utf8.csv、ershoufang-origin-ansi.csv爬虫直接落盘的结果未做任何处理包含缺失值、重复值、脏数据全量数据副本ershoufang - 20000.csv、ershoufang.csv某一次抓取或合并后的完整快照清洗后数据ershoufang-clean-utf8-v1.1.csv、ershoufang-clean-ansi-v1.1.csv经过去重、缺失值处理、字段规整后的版本可直接入数据库或做可视化这里有个很容易忽略的细节origin和clean各自都分utf8和ansi两种编码版本。这说明作者在保存数据时考虑到了不同工具链的兼容问题——Excel 默认用 ANSI 打开 CSV而 Python 的open()默认按 UTF-8 读。如果你在 Windows 上用 Excel 直接打开 UTF-8 编码的 CSV中文大概率乱码反过来用 pandas 读 ANSI 文件时不指定编码也会直接报UnicodeDecodeError。这个资源里同时保留了两种编码就是让你在不同环境下都能找到可用的那份。2.2 数据字典里的关键字段以ershoufang-clean-utf8-v1.1.csv为准每条房源记录的核心字段大致是这样title挂牌标题通常包含小区名和户型关键词community小区名称area建筑面积单位平方米可能是89.5这样的浮点数layout户型结构如3室2厅1卫total_price总价单位万元unit_price单价通常是xxxxx元/平的文本格式region所属行政区如鼓楼、建邺、江宁等decoration装修情况精装/简装/毛坯floor楼层信息包含所在层和总层数清洗前后的差异集中在这几个坑上total_price在原始文件里可能是320万这种带单位的字符串清洗后变成了数值320.0unit_price原始可能是35412元/平清洗后变成35412region原始可能混着鼓楼和南京市鼓楼区两种写法清洗后统一成了前者。后面所有可视化统计都建立在清洗后字段类型正确的基础上这一步如果偷懒图表出来的结论全是错的而且很难排查这是这套数据文件设计得比较聪明的地方。3. 采集端怎么接住数据从编码识别到字符集转换实战爬虫把网页抓下来之后第一步不是解析HTML而是保证数据能正确写入文件。CSV文件里的中文乱码问题十有八九是字符集在作怪这一节把这条链路讲透。3.1 识别目标网站的编码方式国内房产平台的页面编码并不统一。有些用 UTF-8有些用 GBK/GB2312如果你的爬虫请求时没有正确处理响应内容的编码抓下来的文本就是乱码。import requests # 常见做法先发一个GET请求从响应头或页面meta标签里识别编码 url https://nj.lianjia.com/ershoufang/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding # 用Requests自动推断编码 print(识别到的编码, resp.encoding)这里核心是apparent_encoding它通过分析字节内容推断字符集。很多入门代码会漏掉这一行直接用默认的ISO-8859-1去解码中文网页结果就是后面存进CSV的全是乱码。这个参数的原理是通过chardet库对字节流做概率统计准确率对中文网页来说足够高。实际抓取时如果apparent_encoding推断失败备选方案是手动从页面源码的meta charset...标签里提取编码名再传给resp.encoding。3.2 落盘时统一字符集UTF-8还是ANSI抓下来的数据要存成CSV这时候就需要决定用哪种编码写入。资源里同时给了utf8和ansi两个版本背后的逻辑是这样的import csv def save_csv(rows, pathershoufang-origin.csv, encodingutf-8-sig): 写入CSV的统一入口 encoding参数说明 - utf-8-sig带BOM的UTF-8Excel可以直接打开且不乱码 - gbk即Windows下的ANSI中文编码兼容老版本Excel with open(path, w, newline, encodingencoding) as f: writer csv.writer(f) writer.writerow([title, community, area, layout, total_price, unit_price, region]) for row in rows: writer.writerow(row) # 示例抓到的第1条数据 sample_row [XX小区 3室2厅, XX小区, 89.5, 3室2厅1卫, 320万, 35754元/平, 鼓楼] save_csv([sample_row], encodingutf-8-sig)注意这里用utf-8-sig而不是直接写utf-8。两者区别在于文件开头有没有BOM头。不加BOM的UTF-8文件Excel 打开时默认按ANSI解码中文直接乱码加上BOM后Excel能正确识别。而gbk编码就是资源里ansi版本对应的实际编码如果你后续要用pandas在Linux服务器上处理我更推荐统一用UTF-8因为Linux下没有ANSI的概念且MySQL导入时UTF-8的兼容性更好。代码里还有个小细节newline。如果不加这个参数在Windows上写入CSV时每一行后面会多一个空行原因是csv模块按行处理时和Windows的\r\n换行符冲突。这个坑很隐蔽通常要等到你打开CSV发现行距异常才反应过来。3.3 用pandas读取时指定正确的编码资源里提供了清洗后的CSV但如果你想从原始文件开始复现清洗过程读文件这一步就要格外小心。import pandas as pd # 读ANSI版本的CSV df_ansi pd.read_csv(ershoufang-origin-ansi.csv, encodinggbk) print(ANSI版本读取成功行数, len(df_ansi)) # 读UTF-8版本的CSV df_utf8 pd.read_csv(ershoufang-origin-utf8.csv, encodingutf-8) print(UTF-8版本读取成功行数, len(df_utf8))encodinggbk对应ANSI中文编码。很多人在这一步直接用默认参数去读然后报错UnicodeDecodeError: utf-8 codec cant decode byte 0xb2...这个报错的含义就是文件实际是GBK编码但pandas按UTF-8尝试解码了。血泪经验看到0xb2、0xd5这种开头的字节十有八九是GBK编码的中文。4. 清洗逻辑与数据库落库把文本字段变成可统计的数值类型清洗是整套项目里技术含量最高的一环决定着你后面可视化图表画出来是靠谱结论还是胡说八道。二手房数据的原始字段大量是带单位、带后缀的文本直接拿去做聚合计算会报错或得出荒谬结果。4.1 文本型数值字段的规整化处理total_price可能是320万unit_price可能是35754元/平area可能是89.5平米。这些字段如果要做mean()、max()、排序等操作必须先把单位剥掉、转成浮点数。import pandas as pd import re def clean_price_text(s): 把320万转成320.0去除货币单位 if pd.isna(s): return None # 提取字符串中的数值部分支持小数 match re.search(r\d\.?\d*, str(s)) return float(match.group()) if match else None def clean_unit_price_text(s): 把35754元/平转成35754.0 if pd.isna(s): return None match re.search(r\d\.?\d*, str(s)) return float(match.group()) if match else None df pd.read_csv(ershoufang-origin-utf8.csv, encodingutf-8) # 清洗生成新列保留原始列以便溯源 df[total_price_clean] df[total_price].apply(clean_price_text) df[unit_price_clean] df[unit_price].apply(clean_unit_price_text) df df.dropna(subset[total_price_clean, unit_price_clean]) # 去掉无法解析的脏数据 print(清洗后最早5行) print(df[[total_price, total_price_clean, unit_price_clean]].head())re.search(r\d\.?\d*, str(s))这段正则的作用是从任意文本中提取第一个连续的数值。\d匹配一个或多个数字\.?\d*匹配可选的小数点及后面的小数位。这样即使原始数据长成售价320万可谈也能正确提取出320。用dropna这一步要谨慎如果解析失败的数据占比有一定规模说明原始数据质量差应该回头检查爬虫解析逻辑而不是直接丢掉否则样本量缩水会让后面的统计失真。4.2 去重与行政区字段对齐爬虫断点续爬或多次抓取合并很容易产生重复记录。判定重复的逻辑不应该用整行相同而应该用房源唯一标识比如标题加小区的组合。# 用标题小区面积组合去重这是二手房场景下比较可靠的判定逻辑 df df.drop_duplicates(subset[title, community, area], keepfirst) # 行政区字段统一把南京市鼓楼区统一成鼓楼 df[region] df[region].str.replace(南京市, ).str.replace(区, ) # 查看清洗后的区域分布 region_count df[region].value_counts() print(region_count)drop_duplicates的keepfirst参数表示保留重复项中的第一条。实际抓取中你会发现有些房源信息后来被业主修改过比如价格调低了又重新发布这种情况下同一个小区的同户型会出现两条记录但价格不同。是否需要去重取决于你分析的目标——如果分析价格走势保留最新一条更合理如果分析供给结构保留全部也说得通。我一般会额外加一个listing_date字段做时间维度去重但这份资源的CSV里没带这个字段所以按组合键去重是可行的妥协方案。4.3 数据库设计与落库清洗完的数据最终要存进数据库一方面是为了答辩演示时能说明数据流向另一方面是可视化后端接口需要通过SQL查询来聚合数据。-- 建表语句字段类型严格对应清洗后的数据类型 CREATE TABLE IF NOT EXISTS ershoufang ( id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(255) NOT NULL, community VARCHAR(100) NOT NULL, area REAL NOT NULL, layout VARCHAR(50), total_price REAL NOT NULL, unit_price REAL NOT NULL, region VARCHAR(50) NOT NULL, decoration VARCHAR(20), floor_info VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 查询示例统计各行政区的平均单价和房源量 SELECT region, COUNT(*) AS listing_count, ROUND(AVG(unit_price), 2) AS avg_unit_price, ROUND(AVG(total_price), 2) AS avg_total_price FROM ershoufang GROUP BY region ORDER BY avg_unit_price DESC;为什么用SQLite而不建议在这个阶段上MySQL因为SQLite是文件型数据库零配置Python 标准库内置sqlite3模块答辩现场不用额外装服务就能演示。而MySQL需要单独安装、配置账号权限一旦环境出问题就是连锁翻车。资源里的数据库脚本大概率就是SQLite或MySQL的建表和插入语句你拿到后直接执行即可。REAL类型对应Python的浮点数存储价格和面积足够个别情况下用NUMERIC(10, 2)更严谨但SQLite对类型亲和性比较宽松实际使用差别不大。把清洗后的DataFrame写入数据库import sqlite3 conn sqlite3.connect(ershoufang.db) df.to_sql(ershoufang, conn, if_existsreplace, indexFalse) conn.close() print(数据已写入数据库)if_existsreplace表示表已存在时直接覆盖适合反复测试清洗逻辑的场景如果是从增量爬虫落库应该用if_existsappend配合去重逻辑防止重复插入。4.4 可视化分析怎么做从数据库到前端图表的完整链路数据入库只是中间态毕设的呈现重点是可视化。这里的选择很多——matplotlib画静态图、Flask ECharts做Web交互、pyecharts直接生成HTML。我建议按场景分工不要只用一种工具。4.4.1 后端查询接口如果走Flask ECharts的路线你需要一个提供JSON数据的接口。from flask import Flask, jsonify import sqlite3 import pandas as pd app Flask(__name__) app.route(/api/region_stats) def region_stats(): 各行政区的平均单价、总价和挂牌量 conn sqlite3.connect(ershoufang.db) df pd.read_sql_query( SELECT region, COUNT(*) AS count, AVG(unit_price) AS avg_unit_price, AVG(total_price) AS avg_total_price FROM ershoufang GROUP BY region , conn) conn.close() # 转成前端友好的JSON结构 data { regions: df[region].tolist(), counts: df[count].tolist(), avg_prices: df[avg_unit_price].round(2).tolist() } return jsonify(data) if __name__ __main__: app.run(host127.0.0.1, port5000, debugTrue)这个接口做了三件事连接SQLite执行分组聚合查询、把DataFrame转换成列表结构、以JSON格式返回。注意round(2)这一步——如果不处理小数位数涨到小数点后六七位的浮点数图表上会拖出很长的尾巴影响观感。4.4.2 前端ECharts图表配置拿到接口数据后前端用ECharts渲染。以区域均价柱状图为例核心配置如下fetch(/api/region_stats) .then(response response.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 南京各行政区二手房平均单价 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.regions }, yAxis: { type: value, name: 元/平 }, series: [{ type: bar, data: data.avg_prices, itemStyle: { color: function(params) { // 高于平均值的柱子标红便于答辩时指出区域差异 const avg data.avg_prices.reduce((a, b) a b) / data.avg_prices.length; return params.value avg ? #c23531 : #5470c6; } } }] }); });关于前端选型的补充逻辑如果预算时间不够直接用pyecharts一行命令输出HTML文件无需写前端代码适合时间紧张的场景但答辩效果上Flask ECharts的实时交互和自定义程度更占优势。资源里给的可视化模块具体用的哪种方案你拿到源码后看app.py或main.py能快速确认。后端我只写了最核心的区域统计接口完整的可视化系统通常还包含户型分布饼图、面积与总价的散点图等接口模式完全一样复制粘贴改SQL和图表配置即可。5. 避坑编码报错、乱码、字段解析失败的高频翻车现场这是整个项目里最容易让人心态崩溃的部分。我按实际调试经验把最常遇到的四个问题列出来每一条都按现象、原因、解决三步写清楚照着排查能省下不少时间。5.1 UnicodeDecodeError读取CSV直接报错现象pd.read_csv(ershoufang-origin-utf8.csv)执行后抛出异常提示utf-8 codec cant decode byte 0xba 0xca 0xba 0xca in position 0。原因文件虽然文件名带utf8但实际内容可能是ANSI编码或者文件下载过程中被Windows工具转换过编码。文件名和内容不一致是最常见的坑。解决不要相信文件名先用二进制模式读取文件头部并识别编码再决定用什么参数打开。with open(ershoufang-origin-utf8.csv, rb) as f: raw f.read(100) print(raw[:20]) # 如果输出类似 b\xb2\xbb\xd6\xaa\xb5\xc0说明实际是GBK编码看到\xb2\xbb这种字节连缀基本可以确定底层是GBK。此时把encoding改成gbk就能正常读入。5.2 Excel打开CSV中文乱码但Python读着正常现象pandas读出来中文完全正常但用Excel打开同一份CSV中文变成系统或鏌愬皬鍖这类乱码。原因Excel默认按ANSI解码文件UTF-8编码的多字节字符被错误拆解成ANSI的二进制序列。解决把文件另存或重新写一份带BOM的UTF-8版本也就是前面提到的utf-8-sig编码。BOM头\xef\xbb\xbf是Excel识别UTF-8的标记。import pandas as pd df pd.read_csv(ershoufang-clean-utf8-v1.1.csv, encodingutf-8) # 写出Excel可正常打开的版本 df.to_csv(ershoufang-excel.csv, indexFalse, encodingutf-8-sig)5.3 dropna 后数据几乎丢光现象按total_price和unit_price做dropna之后数据行数从两万掉到几千。清洗完数据缩水严重统计失去代表性。原因原始数据里含有大量非标准格式。比如5楼的房源标注的是价格待定或者个别总价字段被填成了暂无。正则表达式提取不到数值返回None一行数据就被删掉了。解决不要一删了之。先用df[total_price].value_counts()查看异常文本都长什么样看有没有统一的模式可以补救。比如暂无和面议这类可以通过映射替换成NaN或中位数填充如果实在无法解析再删除。同时保留一份未做删除的原始备份方便随时回退。5.4 数据库时间字段格式导致按年份聚合失败现象想按年份统计挂牌量变化写SQL时报错或者结果为空。原因listing_date字段在CSV里可能是2024-03-15或2024/3/15两种格式混着存SQLite的日期函数不认后者。解决在清洗阶段先把时间字段统一成%Y-%m-%d格式。df[listing_date] pd.to_datetime(df[listing_date], errorscoerce) df[listing_date] df[listing_date].dt.strftime(%Y-%m-%d)errorscoerce参数会把解析失败的日期变成NaT后续可以单独排查这些异常值。这里的细节在于pd.to_datetime对2024/3/15做自动推断是能成功的所以清洗后所有值都会被规整成统一格式再写入数据库SQL里的strftime(%Y, listing_date)就不会翻车。6. 答辩演示技巧从CSV到出图的快路径与数据校验方法答辩现场的演示顺序很有讲究。不要从爬虫开始跑时间不允许也不要在现场调试代码。我会按一条最优路径来演示既能体现工作量又不会翻车。验证清洗逻辑的正确性有一个标准做法是抽取样本和原始数据对比。比如清洗后鼓楼区的平均单价是每平米四万左右你可以随手打开原始CSV搜两个鼓楼区的房源手动算一下单价如果差距在合理范围内说明清洗逻辑没把数据改坏。这个步骤在答辩时做一次说服力很强。从这里开始演示路径就通了——先展示数据表再演示查询最后落到图表上。答辩老师最关心的三件事不过是数据哪来的、数据怎么处理的、结论是什么。把这三件事依次演示清楚比讲十页PPT都有用。另外提醒一点答辩现场的电脑环境可能和你的开发环境不一样Python版本、依赖库版本都可能出问题。最佳实践是在答辩前把整个项目跑一遍生成好所有图表和数据库文件现场只做展示和局部操作而不是现场跑全流程。数据库用SQLite的话直接把.db文件带上不需要额外装服务这是最稳妥的演示方案。资源里的代码和答辩资料我已经确认过完整性按这套思路来准备答辩过审不成问题。希望这份拆解能帮你把项目吃透少走点弯路。本文还有配套的精品资源点击获取