简介本资源为基于Python的二手房数据采集及可视化分析毕业设计项目完整资料包面向计算机、通信、人工智能、自动化等相关专业的学生与教师可用于毕业设计、期末课程设计或课程大作业。项目为个人毕设成果答辩评审分达98分代码均经过调试测试确保可运行适合小白学习与进阶。压缩包共157个文件约40.01MB包含18个py源码文件、18个csv数据集、15个html页面、11个js脚本及65个png图表等覆盖数据采集、清洗、存储与可视化全流程并附论文资料。已有177人学习下载。读者可获得完整可运行的赛题方案、原始与清洗后的二手房数据、可视化图表与网页展示代码以及论文写作参考基础较好者还可在此基础上修改调整实现不同功能具有较高的学习借鉴价值。1. 二手房数据采集与可视化从标题到可跑通的完整链路二手房数据采集及可视化分析这个题目在计算机毕业设计里出现的频率极高但真正能跑通、能答辩、能写进论文的项目并不多。原因不复杂大部分人卡在采集环节就被反爬拦住了或者采下来一堆脏数据不知道怎么清洗最后可视化只能拿几条假数据凑数。这个项目要解决的核心问题就三件事——把真实房源数据稳定采下来、把脏数据洗成能分析的格式、用图表把价格分布和影响因素讲清楚。适合谁做Python基础一般、但愿意照着步骤一步步跑通的本科生也适合想快速搭一个数据管线原型的初级工程师。整条链路我建议用 requests BeautifulSoup 做采集pandas 做清洗pyecharts 或 matplotlib 做可视化Flask 或 Streamlit 做展示层。下面按实际落地顺序拆开讲。2. 采集层用 requests 和解析库把房源列表稳定拿下来2.1 为什么选 requests BeautifulSoup 而不是 Scrapy毕业设计场景下Scrapy 的工程化程度确实更高但它带来的学习成本对多数人来说是负收益。你需要理解 Twisted 异步模型、中间件优先级、Item Pipeline 的生命周期这些概念在答辩时被追问的概率远大于你实际用到的深度。requests BeautifulSoup 的组合足够覆盖二手房列表页和详情页的采集需求代码量少、调试直观、出问题容易定位。常见做法是列表页负责翻页拿房源 ID 和详情链接详情页负责拿具体字段。我一般会把这两步拆成两个函数中间用一个队列或列表传递 URL这样单页调试和批量跑互不影响。import requests from bs4 import BeautifulSoup import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_list_page(city, page): 抓取列表页返回详情页链接列表 url fhttps://example.com/{city}/ershoufang/pg{page}/ resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) links [] for tag in soup.select(.sellListContent .title a): href tag.get(href) if href: links.append(href) return links def fetch_detail(url): 抓取详情页返回结构化字段 resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) data { title: soup.select_one(.sellDetailHeader .main).get_text(stripTrue) if soup.select_one(.sellDetailHeader .main) else , total_price: soup.select_one(.price .total).get_text(stripTrue) if soup.select_one(.price .total) else , unit_price: soup.select_one(.unitPrice .unitPriceValue).get_text(stripTrue) if soup.select_one(.unitPrice .unitPriceValue) else , area: soup.select_one(.base .area .mainInfo).get_text(stripTrue) if soup.select_one(.base .area .mainInfo) else , } return data这段代码的逻辑很直白fetch_list_page负责翻页并提取详情链接fetch_detail负责从详情页里按 CSS 选择器抠字段。参数上最需要关注的是timeout和HEADERS——超时设 10 秒是经验值太短容易误判失败太长会拖慢整体节奏User-Agent 必须带否则很多站点直接返回 403。resp.encoding utf-8这行不能省部分站点不声明编码不手动指定会出现中文乱码。2.2 翻页策略与请求频率控制翻页本身不难难的是让请求看起来不像机器。我的做法是每抓一页 sleep 一个随机区间比如 2 到 5 秒同时在抓够一定页数后换一次 User-Agent。这不是为了对抗什么复杂机制纯粹是降低被临时限制的概率。def crawl(city, max_page50): all_data [] for page in range(1, max_page 1): links fetch_list_page(city, page) if not links: break for link in links: try: item fetch_detail(link) item[city] city all_data.append(item) except Exception as e: print(ffailed: {link}, reason: {e}) time.sleep(random.uniform(2, 5)) print(fpage {page} done, total {len(all_data)}) return all_datamax_page设 50 是一个保守值实际能抓到多少取决于站点结构。random.uniform(2, 5)控制请求间隔这个区间是我试过比较稳的——低于 2 秒容易被限高于 5 秒采集效率太低。异常捕获那层不要省单条失败不应该中断整个任务。注意采集前先确认目标站点的 robots.txt 和使用条款毕业设计建议用公开的、允许采集的数据源或者用平台提供的开放接口。3. 清洗层把脏数据变成能分析的 DataFrame3.1 字段标准化价格、面积、楼层怎么统一采下来的原始数据几乎不能直接用。价格字段带「万」字面积带「平米」楼层是「中楼层(共18层)」这种混合文本。清洗的核心思路是把所有数值字段拆成纯数字加单位再统一量纲。import pandas as pd import re def parse_price(text): 320万 - 320.0 if not text: return None m re.search(r(\d\.?\d*), text) return float(m.group(1)) if m else None def parse_area(text): 89.5平米 - 89.5 if not text: return None m re.search(r(\d\.?\d*), text) return float(m.group(1)) if m else None def parse_floor(text): 中楼层(共18层) - (中, 18) if not text: return None, None level_map {低: 1, 中: 2, 高: 3} level None for k, v in level_map.items(): if k in text: level v break total None m re.search(r共(\d)层, text) if m: total int(m.group(1)) return level, total df pd.DataFrame(raw_data) df[total_price] df[total_price].apply(parse_price) df[area] df[area].apply(parse_area) df[floor_level], df[floor_total] zip(*df[floor].apply(parse_floor)) df[unit_price_calc] (df[total_price] * 10000 / df[area]).round(0)parse_price和parse_area都用正则提取第一个数字这是最稳的做法因为不同站点的单位写法不统一。parse_floor把楼层拆成两个维度等级低/中/高映射成 1/2/3和总层数这样后续可以做「楼层等级对单价的影响」分析。最后一行用总价和面积反算单价用来和页面标注的单价做交叉验证——如果两者差距超过 10%说明这条数据大概率有问题。3.2 缺失值与异常值处理清洗完字段后下一步是处理缺失和异常。缺失值分两种关键字段缺失价格、面积直接丢弃非关键字段缺失楼层、朝向用众数或「未知」填充。异常值用 IQR 方法识别超出 1.5 倍四分位距的单价记录标记为可疑。# 关键字段缺失直接丢弃 df df.dropna(subset[total_price, area]) # 非关键字段填充 df[floor_level] df[floor_level].fillna(df[floor_level].mode()[0]) df[floor_total] df[floor_total].fillna(df[floor_total].median()) # IQR 异常值标记 q1 df[unit_price_calc].quantile(0.25) q3 df[unit_price_calc].quantile(0.75) iqr q3 - q1 lower q1 - 1.5 * iqr upper q3 1.5 * iqr df[is_outlier] (df[unit_price_calc] lower) | (df[unit_price_calc] upper) # 分析时排除异常值 df_clean df[~df[is_outlier]].copy() print(f原始 {len(df)} 条清洗后 {len(df_clean)} 条剔除 {len(df) - len(df_clean)} 条)IQR 的系数用 1.5 是标准做法不需要调。is_outlier这列保留下来论文里可以专门写一节分析异常房源的特征——比如某些单价特别高的可能是学区房或别墅这本身就是有价值的分析点不要直接删掉。4. 可视化层用 pyecharts 把价格分布和影响因素讲清楚4.1 价格分布直方图 箱线图组合可视化不是把数据随便画成图就完事每张图要回答一个具体问题。价格分布这张图要回答的是「这个城市的二手房单价集中在什么区间长尾有多长」。直方图看整体形态箱线图看分区域或分户型的中位数差异。from pyecharts import options as opts from pyecharts.charts import Histogram, Boxplot, Grid import numpy as np # 直方图 prices df_clean[unit_price_calc].tolist() hist ( Histogram() .add_xaxis([str(int(b)) for b in np.histogram_bin_edges(prices, bins30)]) .add_yaxis(房源数量, np.histogram(prices, bins30)[0].tolist()) .set_global_opts( title_optsopts.TitleOpts(title二手房单价分布), xaxis_optsopts.AxisOpts(name单价(元/平米)), yaxis_optsopts.AxisOpts(name房源数量), ) ) hist.render(price_hist.html)bins30是经验值数据量在几百到几千条之间时30 个分箱能看出分布形态又不会太碎。如果数据量超过一万条可以调到 50。np.histogram_bin_edges自动计算分箱边界比手动指定更省事。4.2 影响因素用散点图和分组柱状图对比面积、楼层、朝向对单价的影响用散点图看面积和单价的关系最直观用分组柱状图看不同楼层等级的平均单价差异。from pyecharts.charts import Scatter scatter ( Scatter() .add_xaxis(df_clean[area].round(1).tolist()) .add_yaxis(单价, df_clean[unit_price_calc].tolist()) .set_global_opts( title_optsopts.TitleOpts(title面积与单价关系), xaxis_optsopts.AxisOpts(name面积(平米), type_value), yaxis_optsopts.AxisOpts(name单价(元/平米)), ) ) scatter.render(area_price_scatter.html) # 楼层等级平均单价 floor_avg df_clean.groupby(floor_level)[unit_price_calc].mean().round(0) print(floor_avg)散点图里如果点太多可以加symbol_size5把点缩小或者用opts.ScatterOpts开启透明度。楼层等级的平均单价用groupby一行就能算出来论文里可以直接把这个结果做成表格。提示pyecharts 生成的 HTML 文件可以直接嵌入 Flask 模板或 Streamlit 页面不需要额外转换。5. 避坑与排查采集和分析中最容易翻车的 5 个点5.1 请求返回 200 但内容为空现象resp.status_code是 200但soup.select拿不到任何元素。原因通常是站点返回了验证页面或空壳页面真正的数据通过 JavaScript 动态加载。解决先打印resp.text[:500]看返回内容如果是验证页就换请求头或降低频率如果是动态加载改用 Selenium 或直接找数据接口。5.2 中文乱码导致解析失败现象抓下来的标题和地址全是乱码正则匹配不到中文。原因是resp.encoding没有正确设置requests 默认用 ISO-8859-1。解决手动设resp.encoding resp.apparent_encoding或直接指定utf-8设完再解析。5.3 清洗后数据量骤减现象采了 2000 条清洗完只剩 300 条。原因通常是dropna把大量记录干掉了而缺失的根源在采集阶段——某个字段的选择器写错了导致整列都是空。解决清洗前先df.isnull().sum()看每列缺失比例超过 50% 缺失的列回去检查采集代码。5.4 可视化图表中文显示为方块现象matplotlib 生成的图表里中文全是方框。原因是默认字体不支持中文。解决在绘图前设plt.rcParams[font.sans-serif] [SimHei]和plt.rcParams[axes.unicode_minus] False。pyecharts 不存在这个问题它用 HTML 渲染。5.5 论文里数据对不上现象论文里写的房源数量和代码跑出来的不一致。原因通常是多次运行采集脚本数据文件被覆盖或追加了重复记录。解决每次采集输出带时间戳的文件名清洗前先df.drop_duplicates(subset[title, total_price])去重。6. 进阶技巧用 Streamlit 把分析结果做成可交互的展示页答辩时如果只放几张静态图评委很容易走神。用 Streamlit 把清洗后的数据和图表串成一个可交互页面现场筛选区域、拖动价格区间图表实时更新效果完全不一样。Streamlit 的优势是不需要写前端代码一个 Python 文件就能跑起来。import streamlit as st import pandas as pd import plotly.express as px st.set_page_config(page_title二手房数据分析, layoutwide) st.title(二手房数据可视化分析) st.cache_data def load_data(): return pd.read_csv(clean_data.csv) df load_data() # 侧边栏筛选 st.sidebar.header(筛选条件) price_range st.sidebar.slider( 单价区间(元/平米), int(df[unit_price_calc].min()), int(df[unit_price_calc].max()), (int(df[unit_price_calc].quantile(0.1)), int(df[unit_price_calc].quantile(0.9))), ) area_range st.sidebar.slider( 面积区间(平米), float(df[area].min()), float(df[area].max()), (float(df[area].quantile(0.1)), float(df[area].quantile(0.9))), ) mask ( df[unit_price_calc].between(*price_range) df[area].between(*area_range) ) filtered df[mask] col1, col2 st.columns(2) with col1: fig1 px.histogram(filtered, xunit_price_calc, nbins30, title单价分布, labels{unit_price_calc: 单价(元/平米)}) st.plotly_chart(fig1, use_container_widthTrue) with col2: fig2 px.scatter(filtered, xarea, yunit_price_calc, title面积与单价关系, opacity0.5, labels{area: 面积(平米), unit_price_calc: 单价(元/平米)}) st.plotly_chart(fig2, use_container_widthTrue) st.metric(筛选后房源数量, len(filtered)) st.metric(平均单价, f{filtered[unit_price_calc].mean():.0f} 元/平米)st.cache_data装饰器让数据只加载一次筛选时不会重复读 CSV。st.sidebar.slider的默认区间用 10% 和 90% 分位数避免极端值把滑块范围拉得太大。st.columns(2)把两张图并排显示页面利用率更高。use_container_widthTrue让图表自适应容器宽度不用手动调尺寸。跑起来只需要在终端执行streamlit run app.py浏览器会自动打开页面。部署到服务器上答辩时直接给评委一个链接比放 PPT 里的截图有说服力得多。这个方案我从采集到展示完整跑过一遍最大的教训是不要等到采集全部跑完才开始清洗先拿 50 条数据把清洗和可视化链路走通确认字段解析没问题再放量跑。否则采了几千条发现某个字段解析全错返工的时间成本非常高。希望帮到你。本文还有配套的精品资源点击获取