简介:本资源是一份面向专科及本科毕业生的毕业论文范例,聚焦Python网络爬虫在二手房数据采集与可视化分析中的工程实践,解决房源信息分散、难以比价分析的现实问题,兼顾数据挖掘、Django后端开发与可视化技术应用。压缩包含1个28KB的Word文档(.docx),完整呈现从研究背景、技术选型(Scrapy/BeautifulSoup/Matplotlib)、系统架构设计到数据清洗、相关性分析及可视化展示的全流程,目录覆盖六章内容,含需求分析、爬虫实现、预处理、测试评估等关键模块。已有426人学习下载,读者可直接参考万字原创降重论文结构,复用爬虫反爬策略(如User-Agent模拟、登录处理)、房价区域热度图表生成代码思路,以及学位论文规范写作框架与实验结果讨论范式。
1. 为什么用 Python 爬二手房数据,不是写个脚本就完事——它本质是「结构化信息工程」
你可能试过用requests + BeautifulSoup抓链家一页房源,发现字段对不上:总价显示“285万”,单价却标“8.2万/㎡”,而面积字段里混着“建面98.5㎡(含公摊)”和“套内72㎡”两种口径;再翻贝壳,同样的小区,挂牌时间差3天,但发布时间字段却是“刚刚”“1小时前”“昨天”这种非结构化文本。这不是爬虫写得不对,而是二手房数据天然带三重混沌:来源异构、字段语义漂移、业务逻辑嵌套。这篇西南财经大学的毕业论文没停留在“能跑通”的层面,它把整个流程锚定在「可复现的数据工程闭环」上——从目标网站 DOM 结构解析规则的可验证性(比如用lxml.etree.XPath替代模糊的soup.find_all('div', class_='price')),到 Pandas 中astype(str).str.extract(r'(\d+\.?\d*)万')这类正则清洗的确定性,再到 Seaborn 绘图时用sns.histplot(data=df, x='unit_price', bins=50, stat='density')强制归一化避免样本量差异导致的视觉误导。它面向的是真实场景:一个本科毕设要经得起答辩老师问“你爬的 12 个字段里,‘装修情况’有‘精装’‘简装’‘毛坯’‘豪装’4 种写法,怎么统一?”而不是教科书里“获取网页→解析→存 CSV”的线性幻觉。适合两类人:一是需要交一份技术扎实、细节经得起推敲的毕业论文的学生;二是想快速搭建本地化房产数据看板的从业者——它不依赖云服务或商业 API,所有代码可离线运行,数据主权完全在本地。
2. 爬虫模块不是发请求那么简单:反爬策略、字段映射与结构化存储的三层校验
2.1 目标网站选择与反爬适配必须同步设计,而非事后补救
论文中明确将链家网(lianjia.com)和贝壳找房(ke.com)作为双源验证对象,这个选型背后有硬逻辑:链家 DOM 结构稳定、API 接口暴露充分,适合做主爬取通道;贝壳则因存在大量经纪人手动录入数据,字段噪声大但覆盖区域广,适合作为交叉校验源。实际编码时,不能简单用requests.get(url)发包。以链家为例,其反爬核心在于三重校验:
- User-Agent 动态轮换:固定 UA 会被限流,需维护至少 5 个主流浏览器 UA 字符串池,每次请求随机选取;
- Referer 必填校验:直接访问详情页会返回 403,必须携带列表页 URL 作为 Referer;
- Cookie 会话维持:首页请求后需提取
lianjia_uuid和_ga字段,后续请求必须携带。
# 示例:链家列表页请求头构造(摘自论文附录代码) headers = { 'User-Agent': random.choice([ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15' ]), 'Referer': 'https://cd.lianjia.com/ershoufang/', 'Cookie': f'lianjia_uuid={uuid_str}; _ga=GA1.2.{ga_id}' } response = requests.get(url, headers=headers, timeout=10)提示:论文强调,
timeout=10是硬性要求。实测中若超时未响应,链家服务器会主动断连并封禁 IP 段 5 分钟。这不是网络问题,而是其风控系统对慢请求的主动拦截。
2.2 数据解析必须建立字段映射表,拒绝“见名知义”的直觉式提取
二手房字段存在严重语义歧义。例如“楼层”字段,在链家页面中同时存在三种表达:
<span class="positionInfo">高楼层(共32层)</span>→ 需提取“高楼层”和“32”两个值;<div class="flood">中楼层(共28层)</div>→ “中楼层”是分类标签,“28”是总层数;<div class="positionInfo">地下室</div>→ 无总层数信息。
论文给出的解决方案是构建XPath 映射字典,每个字段对应一组可验证的解析规则:
| 字段名 | XPath 规则 | 提取逻辑 | 校验方式 |
|---|---|---|---|
total_floor | //div[@class="positionInfo"]/text()[contains(.,'共')] | 正则r'共(\d+)层' | 必须为整数且 >0 |
floor_level | //div[@class="positionInfo"]/text()[1] | 匹配r'(低|中|高)楼层' | 必须属于预设枚举集 |
building_age | //div[@class="area"]/div[2]/text() | 去除空格后取首字符 | 必须为数字或“未知” |
# 论文中实际使用的解析函数(简化版) def parse_floor_info(html): tree = etree.HTML(html) # 提取总层数 floor_text = tree.xpath('//div[@class="positionInfo"]/text()') total_floor = None for text in floor_text: match = re.search(r'共(\d+)层', text.strip()) if match: total_floor = int(match.group(1)) break # 提取楼层等级 level_text = tree.xpath('//div[@class="positionInfo"]/text()[1]') floor_level = "未知" if level_text: level_match = re.search(r'(低|中|高)楼层', level_text[0]) if level_match: floor_level = level_match.group(1) + "楼层" return {"total_floor": total_floor, "floor_level": floor_level}注意:该函数返回字典而非字符串,强制结构化。论文在测试章节指出,当某条数据
total_floor为None时,系统自动标记为data_quality: low并进入人工复核队列,而非丢弃——这是数据工程与脚本编程的本质区别。
2.3 存储设计采用「宽表+元数据」双轨制,规避后期分析陷阱
爬取原始数据若直接存 CSV,会埋下巨大隐患:比如“朝向”字段在链家是['东', '南', '西', '北']四选一,但在贝壳却出现'东南''西南''南北通透'等复合值。论文采用 MySQL 存储,但设计了两层表结构:
- 主数据表
house_raw:仅存原始字符串,字段如orientation_raw VARCHAR(50),不做任何清洗; - 清洗后宽表
house_clean:包含标准化字段orientation_main ENUM('东','南','西','北')、orientation_secondary ENUM('东','南','西','北','无')、is_dual_orientation BOOLEAN; - 元数据表
field_mapping:记录每个字段的清洗规则版本、生效时间、校验通过率(如orientation字段清洗成功率 92.7%)。
这种设计让数据溯源成为可能。当导师质疑“你如何证明‘南北通透’被正确拆解为‘南’和‘北’?”时,可直接查field_mapping表中orientation_main字段的清洗规则 SQL:
-- 论文中定义的清洗逻辑(MySQL 8.0+) UPDATE house_clean SET orientation_main = CASE WHEN orientation_raw REGEXP '南.*北|北.*南' THEN '南' WHEN orientation_raw LIKE '%东%' THEN '东' WHEN orientation_raw LIKE '%西%' THEN '西' ELSE '未知' END;3. 数据预处理不是删空行:缺失值类型识别、异常值业务语义过滤与地理编码标准化
3.1 缺失值必须按生成机制分类,而非统一填充
论文将缺失值分为三类,每类对应不同处理策略:
| 类型 | 特征 | 处理方式 | 论文依据 |
|---|---|---|---|
| 系统性缺失 | 某些区域(如成都天府新区)所有房源均无“楼龄”字段 | 用该区域平均楼龄填充,并标记source_missing=1 | 避免用全局均值污染区域特征 |
| 采集失败 | 单条数据unit_price为空,但total_price和area均有值 | 用total_price / area反算,标记calculated=1 | 利用业务逻辑修复 |
| 真实缺失 | “装修情况”字段明确标注“暂无数据” | 保留空值,不填充 | 尊重原始信息完整性 |
# 论文中缺失值处理核心逻辑(Pandas) df['unit_price'] = df.apply( lambda row: row['total_price'] / row['area'] if pd.isna(row['unit_price']) and not pd.isna(row['total_price']) and not pd.isna(row['area']) else row['unit_price'], axis=1 ) df['unit_price_source'] = np.where( df['unit_price'].isna() & df['total_price'].notna() & df['area'].notna(), 'calculated', 'original' )3.2 异常值检测必须绑定业务规则,拒绝纯统计阈值
单纯用IQR或Z-score剔除房价异常值会误杀真实数据。论文定义了四条业务规则:
- 价格下限:单价 < 3000 元/㎡ 的房源,若位于主城区(如成都锦江区),视为异常(低于土地成本);
- 价格上限:单价 > 15 万元/㎡ 的房源,必须满足
is_villa == True或has_garden == True,否则标记price_flag=1; - 面积合理性:
area < 20且room_count > 1,或area > 500且room_count < 4,触发人工复核; - 时间逻辑:
listing_date > today或listing_date < '2018-01-01',直接剔除。
# 论文中业务规则校验函数 def validate_price_logic(df): # 规则1:主城区低价房校验 urban_areas = ['锦江', '青羊', '金牛', '武侯', '成华'] df.loc[ (df['district'].isin(urban_areas)) & (df['unit_price'] < 3000), 'price_flag' ] = 'low_price_urban' # 规则2:高价房属性校验 df.loc[ (df['unit_price'] > 150000) & ~(df['property_type'].isin(['别墅', '花园洋房'])), 'price_flag' ] = 'high_price_no_villa' return df3.3 地理位置必须标准化为 WGS84 坐标系,支持跨平台可视化
原始数据中的“地址”字段(如“成都市武侯区人民南路四段1号”)无法直接绘图。论文采用高德地图 Web API 进行批量地理编码,但关键创新在于坐标系校验与降噪:
- 调用 API 返回
location: "103.945678,30.623456"后,必须验证该坐标是否落在成都市行政边界内(使用shapely.geometry.Point.within(Chengdu_Boundary)); - 若坐标偏离行政区划 > 5km,触发二次查询(加
city=成都市参数); - 对同一小区多条房源,计算坐标标准差,若
std > 0.001(约 100 米),则用聚类中心点替代(sklearn.cluster.KMeans)。
# 地理编码后坐标校验(论文附录代码) from shapely.geometry import Point, Polygon import geopandas as gpd # 加载成都市边界 GeoJSON chengdu_boundary = gpd.read_file('chengdu_boundary.geojson') def validate_and_correct_coord(lat, lng, district): point = Point(lng, lat) # 注意:高德返回 lng,lat 顺序 if not point.within(chengdu_boundary.unary_union): # 二次查询,限定城市 new_coord = amap_geocode(f"{district} {address}", city="成都市") return new_coord['lat'], new_coord['lng'] return lat, lng # 对小区坐标聚类降噪 def cluster_community_coords(coords_list): if len(coords_list) < 3: return np.mean(coords_list, axis=0) kmeans = KMeans(n_clusters=1, n_init=10).fit(coords_list) return kmeans.cluster_centers_[0]4. 可视化不是画图,是构建可交互的决策支持界面:Django 集成与 Plotly 动态图表实战
4.1 Django 后端必须解耦爬虫与展示,用 Celery 实现任务队列
论文的系统架构图明确将爬虫模块与 Django Web 层物理隔离。所有爬取任务通过 Celery 发起,结果存入 Redis,Django 视图只负责读取缓存数据。这样设计解决了三个现实问题:
- 爬虫阻塞 Web 请求:用户访问
/dashboard时,Django 不执行爬取,只查 Redis 中最新缓存; - 任务状态可追踪:Celery Beat 定时触发
scrape_task.delay(city='chengdu', pages=50),前端可通过/api/task-status/查询进度; - 失败自动重试:若某次爬取因网络中断失败,Celery 自动重试 3 次,超过则发邮件告警。
# Django views.py 中的可视化数据接口(论文实现) from django.http import JsonResponse from django.core.cache import cache def get_visualization_data(request): # 从 Redis 读取预计算的聚合数据,非实时计算 cached_data = cache.get('viz_data_chengdu_2024Q2') if not cached_data: # 触发预计算任务(非爬取!) from .tasks import calculate_viz_metrics calculate_viz_metrics.delay(city='chengdu') return JsonResponse({'status': 'calculating'}, status=202) return JsonResponse(cached_data)4.2 Plotly 图表必须支持业务维度联动,拒绝静态快照
论文的可视化模块核心是维度联动。例如点击地图上的“高新区”区域,右侧所有图表自动刷新:
- 房价分布直方图只显示高新区房源;
- 价格-面积散点图添加趋势线
y = 0.82x + 15.6(高新区特有拟合); - 楼龄分布饼图突出显示“5年内新盘占比 37%”。
# Plotly Dash 应用核心回调(论文附录) @app.callback( [Output('price-hist', 'figure'), Output('area-price-scatter', 'figure'), Output('age-pie', 'figure')], [Input('map', 'clickData'), Input('date-range', 'start_date')] ) def update_charts(click_data, start_date): # 获取点击区域 if click_data and 'points' in click_data: district = click_data['points'][0]['customdata'][0] # customdata 存储区域名 df_filtered = df[df['district'] == district] else: df_filtered = df # 动态生成高新区专属散点图趋势线 if district == '高新区': z = np.polyfit(df_filtered['area'], df_filtered['total_price'], 1) trend_line = go.Scatter( x=df_filtered['area'], y=z[0]*df_filtered['area'] + z[1], mode='lines', name='高新区趋势线', line=dict(color='red', width=2) ) return [ px.histogram(df_filtered, x='unit_price', nbins=30), go.Figure(data=[scatter_trace, trend_line]), px.pie(df_filtered, names='building_age_group', values='count') ]4.3 关键指标卡片必须嵌入业务解释,让图表自己说话
论文的最终可视化界面包含 6 张核心卡片,每张不仅显示数值,还附带业务解读短句。例如:
| 卡片标题 | 数值 | 解读短句 | 依据 |
|---|---|---|---|
| 当前均价 | 14,280 元/㎡ | “较上月微涨 0.3%,涨幅收窄,市场进入横盘期” | 对比历史数据计算环比 |
| 热门板块 | 高新区 | “挂牌量占全市 22%,但成交周期达 87 天,供需错配明显” | 关联挂牌量与成交数据 |
| 户型热度 | 三居室 | “占比 41.2%,但 78% 的三居室集中在 90-110 ㎡,面积同质化严重” | 房屋面积分布分析 |
这些解读不是人工撰写,而是由预设规则引擎动态生成:
# 论文中指标解读生成器 def generate_price_insight(current_price, last_month_price, city_avg): change_pct = (current_price - last_month_price) / last_month_price * 100 if abs(change_pct) < 0.5: return f"较上月微涨 {change_pct:.1f}%,市场进入横盘期" elif change_pct > 2: return f"加速上涨 {change_pct:.1f}%,需警惕泡沫风险" else: return f"回调 {abs(change_pct):.1f}%,回归理性区间"5. 毕业论文级交付物:可复现环境、防重检测报告与答辩应答话术库
5.1 一键复现环境:Docker Compose 封装全栈依赖
论文提供docker-compose.yml文件,3 条命令即可启动完整环境:
# 1. 构建镜像(含 Python 3.9、MySQL 8.0、Redis 7.0) docker-compose build # 2. 启动服务(后台运行) docker-compose up -d # 3. 进入爬虫容器执行首次采集 docker exec -it scrapy-worker bash -c "cd /app && python main.py --city chengdu --pages 20"镜像内预装所有依赖:
- 爬虫层:
Scrapy==2.11.0,lxml==4.9.3,fake-useragent==1.4.0 - 数据层:
pandas==1.5.3,numpy==1.23.5,mysql-connector-python==8.0.33 - 可视化层:
plotly==5.18.0,dash==2.14.2,gunicorn==21.2.0
提示:论文特别注明,MySQL root 密码在
docker-compose.yml中明文写为dev_password,这是为答辩演示设计的——避免现场调试数据库连接,符合本科毕设“可展示性”优先原则。
5.2 查重报告与代码原创性声明必须嵌入论文正文
论文在“致谢”之后新增附录 B《原创性说明》,包含三项硬证据:
- 代码查重:使用
CodeSimilarity工具扫描全部 Python 文件,相似度 < 12%(阈值为 15%); - 数据来源声明:明确列出所有爬取网站 Robots.txt 允许路径(如
lianjia.com/ershoufang/),并截图备案; - 算法差异化:对比主流教程,本论文的
unit_price清洗采用total_price/area反算 + 业务规则校验,而非简单丢弃缺失值。
5.3 答辩高频问题应答话术,直击评审痛点
论文附录 C 提供 8 个答辩必问题的标准回答,每条包含技术要点+业务价值+局限坦白三层结构:
Q:为什么不用 Scrapy-Redis 做分布式爬虫?
A:技术上可行,但本科毕设需控制复杂度。我们实测单机 Scrapy 每小时稳定爬取 1200 条(链家),满足成都全域 2 万条数据日更需求;分布式会引入 Redis 连接稳定性、任务去重等新问题,超出毕设范围。未来可扩展为多进程模式(已预留multiprocessing.Pool接口)。
Q:可视化图表能否导出为 PDF?
A:可以。Dash 框架通过pdfkit库支持一键导出,但需注意:地图组件(Plotly Mapbox)导出为静态 PNG,动态缩放功能丢失。我们在/export路由中实现了 PDF 生成,导出内容包含所有指标卡片和图表,符合学校论文附件格式要求。
Q:数据时效性如何保证?
A:系统设置双保险:一是 Celery Beat 每 6 小时触发全量爬取(@periodic_task(run_every=timedelta(hours=6)));二是用户在前端点击“刷新数据”时,触发增量爬取(只抓取listing_date > last_update的新房源)。实测成都数据延迟 < 8 小时。
最后一步,打开requirements.txt,确认django==4.2.7和scrapy==2.11.0版本严格匹配论文所写——这比任何炫技都更能说服答辩老师:你真的跑通了。
本文还有配套的精品资源,点击获取