简介这是一份基于Python的二手房数据采集与可视化分析毕业设计项目资源包含完整源码、配套论文资料与答辩展示面向计算机、通信、人工智能、自动化等相关专业的学生、教师或从业者也可作为课程设计、期末大作业或毕业设计的参考蓝本。资源包共157个文件压缩后约40MB以Python脚本、CSV数据集、HTML页面和JavaScript交互图表为主另含PNG截图、配置文件、字体以及答辩PPT等完整覆盖数据抓取、清洗、存储与可视化展示链路。目前已有179人学习浏览项目代码均经过调试测试答辩评审分达到98分运行稳定性好、可复现性强。内置多份不同编码、不同清洗阶段的二手房数据快照便于对照理解每一步处理逻辑论文与PPT可辅助梳理论文结构目录层次清晰。整体项目围绕爬虫采集、数据清洗、存储入库和可视化展示四大模块组织各阶段文件独立可查支持在此基础上替换数据源、扩展分析维度快速搭建自己的二手房数据分析系统。1. 二手房数据采集及可视化毕设选题里性价比最高的一条路答辩现场最怕听到的三个问题数据从哪来、怎么保证数据没问题、分析图表跟你的代码是不是对得上。很多同学代码能跑但数据量少得可怜图表是从别处截的一问就露馅。二手房数据采集及可视化这个题目的价值在于它天然自带闭环采集能拿到真实数据清洗能做数据治理可视化能讲出价格、区域、户型、面积之间的关系论文里每一个图表都能指到对应代码和数据行答辩时底气完全不一样。这篇文章写给两类人一类是正在选毕设题、想找一个能稳定落地又不太依赖硬件的Python项目另一类是已经定了题但被反爬、清洗、图表中文乱码反复折腾的人。我会按从业者做这类项目的通用路径来拆采集选型、字段设计、清洗入库、可视化分析以及我踩过的那几个坑。全程用可复现的代码片段参数我都写清楚为什么这么设你照着改就能用。2. 采集侧选型与结构化解析requests BeautifulSoup 的最小闭环2.1 为什么先选 requests 而不是一上来就上 Scrapy做二手房采集第一反应往往是Scrapy因为它是爬虫框架、有并发、有中间件听起来更专业。但以毕业设计为交付目标我建议先冷静一下。Scrapy的调试成本高Item Pipeline、Spider中间件、Twisted异步这些概念你论文里要解释清楚就得写好几页而且出问题时很难快速定位。相反requests BeautifulSoup是同步请求、单页解析代码顺序跟人脑思考顺序一致逻辑出错一眼就能看出来。我在做这类采集时反而刻意不用Scrapy主要原因是落地的稳定性。毕设不是做大规模爬虫数据量几千到几万条就足够支撑分析同步请求完全够用。requests负责发HTTP请求拿HTMLBeautifulSoup负责从HTML里抽取挂牌信息两个库加起来依赖少、调试直观。更重要的是论文里描述这套方案的思路非常清晰发起请求、解析页面、提取字段、翻页循环。评审老师看到的是你理解了这个过程而不是只会调框架。下面是一个最小闭环的采集骨架我一般会先拿单个列表页跑通再套翻页循环。这个脚本针对的是某头部经纪平台的挂牌列表页你实际使用时需要根据目标页面的结构调整选择器但它把关键环节都带出来了import time import random import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } def fetch_listing_page(city_code, page_num): # city_code 控制城市page_num 控制翻页具体参数名要以目标网站实际接口为准 url fhttps://example.com/{city_code}/ershoufang/pg{page_num}/ resp requests.get(url, headersHEADERS, timeout10) resp.encoding resp.apparent_encoding # 避免中文乱码 if resp.status_code ! 200: return None return resp.text def parse_listing(html): soup BeautifulSoup(html, html.parser) items [] # 每个挂牌卡片通常在一个重复的 li 或 div 结构里需按页面实际情况换选择器 for card in soup.select(li.listing-item): title card.select_one(div.title a) total_price card.select_one(div.total-price span) unit_price card.select_one(div.unit-price) if title and total_price and unit_price: items.append({ title: title.get_text(stripTrue), total_price: total_price.get_text(stripTrue), unit_price: unit_price.get_text(stripTrue), }) return items for page in range(1, 4): html fetch_listing_page(sh, page) if html: data parse_listing(html) print(fpage {page}: got {len(data)} items) time.sleep(random.uniform(1.5, 3.0))这段代码的逻辑分三层请求层负责拿HTML解析层负责抓标题和价格字段调度层控制翻页和延时。三个参数值得你重点关注。timeout10是请求超时上限防止某个页面挂死导致程序卡住不往后走apparent_encoding是根据页面内容自动推断编码二手房平台很多页面是GBK或GB2312直接默认UTF-8解码会出现中文变问号random.uniform(1.5, 3.0)是每次翻页之间的随机延时作用是让请求间隔不均匀避免固定间隔被识别出自动化特征。有同学会问为什么不像爬虫教程里那样用json.loads去解析接口数据。因为二手房挂牌页大多数情况下服务端渲染的是完整HTML接口形式不统一有的需要签名参数有的在XHR请求里加密了payload。直接解析HTML反而最稳定你只需要关心页面结构而不需要逆向JS逻辑这是毕设周期里最明智的取舍。2.2 挂牌详情解析字段设计、翻页循环与参数采集字段的设计决定了后期分析能做多深。很多同学只抓了标题、总价、单价就收工结果做可视化时发现面积、朝向、楼层都没有什么对比都做不了。我一般会在解析阶段直接抓全字段宁可多存几列也不要后期返回去补采。以下是二手房挂牌详情里比较通用的字段表你抓取时对照着这个清单去页面里找对应元素字段数据类型含义采集注意点小区名称字符串房源所在小区不同平台写法差异大需要规范化区域字符串所在行政区及商圈通常是“区-商圈”两级结构总价字符串原始值挂牌总价注意有“万”后缀需清洗单价字符串原始值每平米单价有“元/平”后缀部分平台隐藏建筑面积字符串原始值房屋面积单位是㎡需转float朝向字符串东/南/西/北或组合可能缺省需做缺失处理楼层字符串低/中/高楼层有的平台是“/共6层”结构装修字符串毛坯/简装/精装部分房源此项为空挂牌时间字符串房源发布时间直接影响时效性分析字段抓不全的根源是页面结构里标题和详情不在同一个标签层级或者某些字段只存在于详情页而不在列表页。我的做法是列表页抓全部卡片字段然后只对缺失字段的房源发起详情页请求按房源ID拼接URL。这一步看似多写了不少代码但它直接决定了你后面做分析时能不能讲出“不同区域的单价差异”“面积与总价的关系”这种结论。翻页循环需要注意边界条件。有的网站翻页到最后一页会返回空列表有的会返回重复的第一页数据还有的会在URL里把页码参数名改成pageNo而不是pg。我建议在循环里加一个终止判断如果连续三页解析结果都为空就主动停止而不是傻傻地请求到第100页。这个判断还能避免网站改版后无限请求空页面浪费时间。2.3 温和反爬策略User-Agent、延时与失败重试反爬是采集逃不开的话题但毕业设计不需要高深的对抗手段你只需要做到“不被封、够用、可解释”。我的策略是温和三件套自定义User-Agent、随机延时、失败重试。这三件事的成本极低但能避免大部分初级的封禁。import requests from requests.adapters import HTTPAdapter session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, }) adapter HTTPAdapter(max_retries3) session.mount(http://, adapter) session.mount(https://, adapter) def safe_get(url, sessionsession): try: resp session.get(url, timeout10) resp.raise_for_status() return resp except requests.RequestException as e: print(frequest failed: {url}, retrying later) return None这里用到了HTTPAdapter(max_retries3)它让每次请求在网络超时或连接被重置时自动重试最多3次省去手动写重试逻辑。重试之间的退避时间由底层urllib3的指数退避控制是默认行为。关键是不要在重试代码里再加固定延时否则请求速率会不均匀反而更容易触发反爬。有人会问要不要上代理池。以我的经验毕设场景完全没必要。代理池的维护成本很高免费代理大多不可用付费代理又要额外开销而且代理本身也可能被封。温和采集、控制单日总量对模拟项目和论文来说是完全够用的。另一个血泪经验是别在高峰期采集比如晚上八点到十一点是用户访问高峰网站的限流策略更敏感我一般安排在深夜或凌晨跑成功率明显更高。3. 数据清洗与存储把网页文本变成可分析的数据集3.1 字段规范化从“120万”“98㎡”变成数值采集下来的数据是给人看的文本不是给计算用的数值。直接拿“120万”去做均值计算会得到NaN。字段规范化是可视化分析前最枯燥但最重要的一步这一步做不好后面所有图表都会翻车。import pandas as pd import re def clean_price(value): # 输入是类似 120万 的字符串输出数值单位万 if not isinstance(value, str): return None value value.replace(万, ).replace(\u00a0, ) try: return float(value) except ValueError: return None def clean_unit_price(value): # 输入类似 58000元/平 或 5.8万/平 统一换算成元/㎡ if not isinstance(value, str): return None if 万 in value: number float(re.search(r[\d.], value).group()) * 10000 else: number float(re.search(r[\d.], value).group()) return number def clean_area(value): # 输入类似 89.5㎡ 或 89.5平 输出平方米数值 if not isinstance(value, str): return None match re.search(r[\d.], value) if match: return float(match.group()) return None这三个清洗函数解决的是挂牌文本里最常见的格式问题。clean_price把“万”字后缀去掉并转浮点顺便处理了页面里常见的\u00a0这个不换行空格clean_unit_price要处理“万/平”和“元/平”两种写法统一换算成元为单位clean_area只提取字符串中的数字不关心后面跟的是“㎡”还是“平”。正则里的[\d.]匹配连续数字和小数点已经覆盖了整数和小数面积。参数层面的关键点是清洗规则要有兜底。你预期用户输入是“120万”但实际页面里可能混入“价格待定”“暂无报价”“约120万”这类变体。所以每个函数在无法解析时都返回None而不是抛异常。这个设计直接决定了清洗流程是安稳跑完还是中途崩掉。3.2 重复数据与缺失值处理二手房的重复数据有两个来源同一个平台里同一套房被多个中介重复挂牌不同平台之间同一房源也会有交叉。去重的核心思路是找主键而不是简单地对所有字段完全匹配。df[is_dup] df.duplicated(subset[小区名称, 建筑面积, 总价, 朝向]) df df[~df[is_dup]].copy() print(fafter dedup: {len(df)} rows)我一般把“小区名称建筑面积总价朝向”四字段组合作为去重主键这个组合已经能基本覆盖同一房源重复挂牌的场景。不建议加“标题”字段因为中介写的标题文案差异太大同一个房源在不同中介手里的标题完全不同会导致漏去重。去重后看缺失率df.isnull().sum()按列统计缺失如果某一列缺失超过30%就要考虑是否在采集阶段补充详情页抓取而不是在清洗阶段硬填。缺失值处理有两种常见策略数值型字段用中位数填充类别型字段单独标记为“未知”。但这里有个玄学问题很多同学喜欢用均值填充总价结果做了个均价图发现均值被缺失值拉低整个结论都失真。我的习惯是删除关键字段缺失的行例如总价、面积、区域这三个字段缺失时直接丢弃而不是填充。3.3 存储入库SQLite 建表与循环利用清洗后的数据要落地存储。SQLite是毕设项目的最佳选择它不需要单独安装数据库服务一个文件就是整个库论文里可以写“轻量级嵌入式数据库”听起来也成立。表结构设计直接对应前面清洗后的字段。CREATE TABLE IF NOT EXISTS house_listing ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, community TEXT, district TEXT, total_price REAL, unit_price REAL, area REAL, orientation TEXT, floor_level TEXT, decoration TEXT, listing_time TEXT, crawl_source TEXT, crawled_at TEXT DEFAULT CURRENT_TIMESTAMP );这里用REAL类型存价格和面积用TEXT存文本类字段。crawl_source字段记录数据来源方便表明数据仅用于学习研究。建表后通过df.to_sql(house_listing, conn, if_existsappend, indexFalse)写入pandas的to_sql会自动做类型映射大多数情况下不用手写批量INSERT。有一个细节值得注意千万不要把清洗前和清洗后的数据存在同一个表里。我吃过这个亏第一次用if_existsreplace直接覆盖了原始表后面想回头对比清洗差异时完全没有依据。现在我会建两个表house_listing_raw存采集原始数据house_listing_clean存清洗后结果。这样论文里能写清洗步骤的统计对比比如“原始数据共12000条清洗后有效数据9800条去重率18%”这就是答辩时很好的过程性证据。4. 可视化分析从价格分布到区域对比的几个必做图表4.1 分析维度怎么定先想清楚论文要讲什么结论可视化的误区是先有图后找结论导致图表一堆但讲不出完整故事。我习惯先写两到三句结论句比如“该市二手房均价呈现中心城区向外围递减的圈层结构”再反推需要哪些图表来支撑这个结论。区域均价对比柱状图、各区域房源数量分布、总价区间占比这些图表都服务于一个主线叙事。分析维度至少要覆盖四个方面价格层面看总价和单价的分布区间区域层面看不同行政区的均价排名和挂牌量房源结构看户型、朝向、楼层的占比相关关系看建筑面积与总价是否线性相关。这四个维度已经能撑起一篇中等篇幅论文的主体分析章节而且每个维度对应图表时思路清晰。有一个分析维度经常被忽略房屋面积与总价的散点关系。这个图表做出来会直接暴露一个现象即总价不只是随面积线性变化区域和房龄的影响有时大于面积影响。答辩时拿这张图讲分析深度效果比堆十个柱状图好得多。数据处理上只需要用清洗好的area和total_price两列不需要额外特征工程。4.2 pyecharts 生成区域均价与户型占比的组合图可视化库我用pyecharts因为生成的图表是交互式HTML可以在浏览器里缩放、悬停查看数值演示时比matplotlib的静态图好看而且代码量差别不大。需要注意新版pyecharts的链式写法与旧版差异下面这段代码用新版风格如果你本地跑的是旧版需要把add方法的参数位置做调整。from pyecharts import options as opts from pyecharts.charts import Bar, Pie # 统计区域均价 district_avg df.groupby(district)[total_price].mean().sort_values(ascendingFalse) districts district_avg.index.tolist() avg_prices [round(v, 1) for v in district_avg.values] bar ( Bar() .add_xaxis(districts) .add_yaxis(区域均价(万), avg_prices) .set_global_opts( title_optsopts.TitleOpts(title各区域二手房挂牌均价对比), yaxis_optsopts.AxisOpts(name均价(万)), ) ) bar.render(district_avg.html) # 户型占比 room_type_counts df[room_type].value_counts() pie ( Pie() .add(, [list(z) for z in zip(room_type_counts.index, room_type_counts.values)]) .set_global_opts( title_optsopts.TitleOpts(title二手房户型分布占比), legend_optsopts.LegendOpts(orientvertical, pos_leftleft), ) ) pie.render(room_type_pie.html)两段图表代码的逻辑结构是一样的先用groupby或value_counts聚合数据再传入图表组件。sort_values(ascendingFalse)保证柱状图从左到右按均价从高到低排列这个排序在答辩演示时很重要能一眼看出头部区域和尾部区域的差距。饼图里的[list(z) for z in zip(...)]是把pandas的索引和值组装成pyecharts要求的二维列表格式如果你写room_type_counts.items()新版pyecharts会直接报类型错误。中文字体问题在pyecharts里相对少一些它用浏览器渲染不依赖系统字体库。但如果你用matplotlib做辅助图就必须手动指定plt.rcParams[font.sans-serif] [SimHei]否则坐标轴全是方框。很多同学在这个地方反复踩坑明明数据没错图就是不能看。4.3 让图表服务论点口径统一与数据快照可视化阶段的翻车大多数不是代码问题而是口径问题。均价的计算范围不一致比如一张图包含所有房源、另一张图只包含满两年房源柱状图高高低低根本没法对比。我要求自己同一套图表只用一个数据源版本也就是清洗后的house_listing_clean表或者直接从一个固定CSV快照读数据。现在养成的一个习惯是清洗完数据之后先导出一份clean_data.csv并拷贝一份带时间戳的快照所有后续图表都从这个快照读取。这样做的好处是图表可复现论文评审如果提出“你今天的图和论文里的图数据不一致”你可以指着快照说明所有分析基于哪个数据版本。幸运的是我第一版论文就是这么干的答辩时直接被问到图表来源我当场拿快照和脚本复现了图表这一关过了之后基本没有硬性问题。还有一个微小的可视化细节是颜色和图例顺序。value_counts()返回的Series默认按数量降序排列饼图图例顺序和占比顺序一致但如果你后面用sort_values()重排了数据图例顺序也要跟着变。这个看似无关紧要的问题在论文截图里的观感差异非常大我建议每组图生成后用浏览器打开检查一遍再截图。5. 毕设避坑采集、清洗、可视化全流程的5个常见翻车点5.1 页面结构一变整个解析器当场报废现象采集脚本昨天跑得好好的今天一运行全返回空列表打印HTML发现结构完全变了。 原因目标网站在你采集周期内做了一次前端改版标签class名称换了CSS选择器全部失效。 解决解析层和数据层分离。把选择器集中放在脚本顶部的字典里SELECTORS {card: div.item, title: h3.title}改版时只改这个字典。另外采集脚本要加页面变化检测比如解析结果为空时主动发报警日志而不是静默写进数据库。5.2 数据量看着多清洗完剩不到一半现象数据库里有一万多条记录清洗后只有四千条可用区域均价分析只剩几个区有数据。 原因采集阶段没有做字段完整性过滤大量记录缺失面积、朝向、区域等关键字段清洗时整体删除。 解决采集列表页的同时抓详情页只对缺失字段的房源补采详情。这个代价在毕设里完全可控详情页数量通常只有列表页的十分之一。清洗时先看每列缺失率再决定删除还是补采不要一上来就dropna()。5.3 价格字段里混入了非数值文本现象total_price列清洗后出现大量NaN均价图缺了好几个区域。 原因部分房源挂牌价格是“价格待定”或“业主改价”正则提取数字失败函数返回None。 解决清洗函数要分类处理。先判断字符串里是否包含数字不包含就标记为缺失而不是报错包含但格式异常的就打印告警方便回头统一看看是哪些脏格式。我在清洗脚本里专门加了一个bad_format_list把无法解析的原始值收集起来答辩时可以直接展示这个清单作为数据质量的实证。5.4 图表里的中文变成方框或乱码现象柱状图的区域名称变成一个个小方块保存成PNG后问题依旧。 原因matplotlib库的默认字体是DejaVu Sans不支持中文。pyecharts理论上没有这个问题的。 解决matplotlib设置中文字体时要看操作系统环境plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, WenQuanYi Zen Hei]把常见中文字体列一个列表按顺序匹配。还要顺带设置axes.unicode_minusFalse否则负号会显示成方块。建议直接用pyecharts输出HTML文件省去截图后字体失真的问题。5.5 论文图表和代码口径对不上现象论文里的区域均价前三名和仓库代码重新跑出来的结果不一样答辩被质疑数据造假。 原因论文写作用的是清洗后某个版本的数据代码后来又改过清洗规则两者数据不一致图表不可复现。 解决数据快照和代码版本一起锁死。把清洗后的CSV快照放进data/目录论文里写明“本文所有分析基于2025年4月采集数据清洗后样本量N9800条”。答辩前用同一脚本从快照重新生成全部图表确保图、表、代码三者完全对齐。这个习惯能直接消除掉大多数关于数据真实性的追问。6. 答辩前的最后一公里让图表和论文、代码对得上论文资料整理这件事我见过太多人拖到答辩前一晚才开始结果就是图表编号对不上、代码文件乱成一团、README里一句运行说明都没有。这里分享一个我固定使用的项目结构你直接照着排布就能省去很多麻烦project/ ├── data/ │ ├── raw/ │ ├── clean/ │ └── snapshots/ ├── scripts/ │ ├── crawl.py │ ├── clean.py │ └── visualize.py ├── output/ │ └── charts/**/*.html ├── docs/ │ ├── 开题报告.md │ ├── 论文.md │ └── 答辩PPT.md └── README.mddata/snapshots/只放带时间戳的最终数据文件scripts/三个脚本各自独立可运行output/charts/按图表用途分目录。README里只需要写三句话项目做什么、数据怎么获取、代码怎么按顺序运行。这三句话同时也是答辩现场的开场白。我给这个目录结构配套了一个固定习惯论文里每引用一个图表就在图表下方写一行简短的生成说明例如“数据来源snapshot_20250401.csv清洗脚本clean.py输出图表district_avg.html”。写起来只有一行字但评审问的时候你可以理直气壮地指出数据和代码路径而不是含糊地说“这个图之前跑出来的”。答辩前一周我会花十五分钟做一次完整回归从原始CSV快照重新生成所有图表核对论文里的每个数字和图表数值是否一致。这一轮检查基本能兜住所有低级错误。有一句话我记了很久毕设项目翻车不可怕最怕是翻车之后拿不出一个能自洽的版本。数据快照、脚本分离、图表标注这三个习惯就是你的后悔药。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?