ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python二手房数据采集与可视化分析实战

Python二手房数据采集与可视化分析实战 简介本资源是一套完整的二手房数据采集与可视化分析毕业设计项目面向计算机、电子信息工程及数学等专业的本科生适用于课程设计、期末大作业或毕业设计参考。项目基于Python实现全流程闭环涵盖网页爬虫采集、数据清洗含UTF8/ANSI双编码版本、多维度统计分析及交互式可视化呈现配套完整报告PPT与可运行源码。压缩包共155个文件含18个核心Python脚本含爬虫、清洗、分析模块、18个CSV数据集含原始与清洗后多版本、65张可视化结果PNG图、15个HTML报告页及JS交互组件整体35.13MB结构清晰、模块解耦便于学习调试与功能扩展。已有840人学习下载提供从数据获取到成果展示的全链路实践范例特别适合需快速构建数据分析类毕设项目的同学助其掌握真实业务场景下的工程化开发流程与文档撰写规范。1. 为什么用 Python 做二手房数据采集与可视化分析不是“抄作业”而是练出真实工程能力很多同学拿到“二手房数据采集与可视化分析”毕业设计选题时第一反应是找现成.rar包解压、改改 PPT、跑通代码交差。但真正拉开差距的从来不是压缩包里有没有“高分报告”或“完整数据集”而是你能否独立完成从网页结构变化中稳定提取字段、处理反爬策略演进、清洗非结构化价格与描述文本、构建可复现的分析逻辑链、用可视化讲清区域价差背后的供需逻辑。这不是 Python 语法练习而是典型的数据工程闭环——采集、清洗、建模、呈现每一步都暴露真实业务约束链家/贝壳页面 DOM 动态加载、小区名歧义“万科城”在不同城市重复出现、单价含税与否混杂、楼层描述格式混乱“低/中/高” vs “1-3/4-12/13-32”。本方案不依赖任何已打包的“源码数据集”而是带你用 Requests BeautifulSoup Pandas Plotly GeoPandas从零搭建一个可验证、可调试、可扩展的二手房分析流水线。适合需要交付可运行代码、能答辩讲清技术选型依据、且希望未来转岗数据分析或爬虫开发的同学。2. 用 Requests BeautifulSoup 稳定抓取链家/贝壳二手房列表页与详情页2.1 为什么放弃 Selenium坚持 Requests 解析器组合Selenium 启动浏览器开销大、易被识别为自动化行为、难以批量调度。而链家/贝壳当前列表页仍以静态 HTML 为主详情页部分字段 JS 渲染但核心字段如总价、单价、户型、楼层均在初始 HTML 中Requests 足够应对。关键在于绕过 User-Agent 封禁、处理 Referer 链路、模拟真实会话 Cookie。常见错误是直接requests.get(url)结果返回 403 或空列表。正确做法是构造带上下文的 Session并复用请求头。提示链家 PC 端对无 Referer 的 GET 请求会返回 403贝壳移动端接口需携带X-Requested-With: XMLHttpRequest头但 PC 端列表页仍走传统 HTML 渲染优先抓 HTML 更稳定。2.1.1 构建抗干扰的请求会话import requests from bs4 import BeautifulSoup import time # 模拟真实浏览器环境 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: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8,application/signed-exchange;vb3;q0.7, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive, Upgrade-Insecure-Requests: 1, } session requests.Session() session.headers.update(headers) # 访问城市首页获取基础 Cookie必需 city_url https://bj.lianjia.com/ response session.get(city_url, timeout10) if response.status_code ! 200: raise ConnectionError(f城市首页访问失败状态码{response.status_code}) # 此时 session 已携带有效 Cookie可用于后续请求这段代码的核心作用是让服务器认为你是一个刚打开链家北京站的普通用户而非脚本。session.get(city_url)不仅获取 HTML更重要的是触发服务器下发lianjia_uuid、_ga等会话标识 Cookie。后续所有请求复用该 session自然携带这些 Cookie极大降低被拦截概率。2.1.2 解析列表页并提取详情页 URL链家列表页 URL 格式为https://bj.lianjia.com/ershoufang/pg{page}/每页 30 条。需注意页面存在“无房源”提示需判断soup.find(div, class_noResult)房源卡片div classinfo clear下的a href...是详情页链接但 href 为相对路径需拼接部分链接含?参数如?sug应保留原样避免丢失来源标识。def parse_list_page(html_content): soup BeautifulSoup(html_content, html.parser) house_cards soup.find_all(div, class_info clear) detail_urls [] for card in house_cards: link_tag card.find(div, class_title).find(a) if link_tag and link_tag.get(href): # 拼接绝对 URL full_url requests.compat.urljoin(https://bj.lianjia.com/, link_tag[href]) detail_urls.append(full_url) return detail_urls # 示例抓取前 3 页 all_detail_urls [] for page in range(1, 4): list_url fhttps://bj.lianjia.com/ershoufang/pg{page}/ try: resp session.get(list_url, timeout15) resp.raise_for_status() urls parse_list_page(resp.text) all_detail_urls.extend(urls) print(f第 {page} 页提取 {len(urls)} 条详情链接) time.sleep(1) # 控制请求频率避免触发风控 except Exception as e: print(f抓取第 {page} 页失败{e})time.sleep(1)不是“慢点爬”而是模拟人类翻页间隔。链家服务端有滑动窗口计数器连续请求超阈值如 5 秒内 10 次会返回 403 或验证码。此处设为 1 秒兼顾效率与稳定性。3. 从详情页 HTML 中精准抽取结构化字段并清洗3.1 字段定位策略避开动态 ID锚定语义化标签链家详情页 HTML 结构频繁变动span classlabel总价/span这类标签可能某天变成span classprice-label。可靠做法是基于字段语义和兄弟节点关系定位而非硬编码 class 名。例如“单价”字段固定位于span classunitPriceValue内且其父容器div classunitPrice具有唯一性“朝向”则位于lispan classlabel朝向/spanspan classcontent南/span/li结构中。3.1.2 关键字段解析与清洗逻辑表字段名定位方式原始值示例清洗后类型清洗逻辑说明总价万元soup.find(span, class_total)850float去除中文单位转浮点单价元/㎡soup.find(span, class_unitPriceValue)i85,000/iint提取i文本去逗号转整型建筑面积㎡soup.find(textre.compile(r建筑面积)).parent.find_next_sibling(li).find(span, class_content)120.5㎡float正则匹配标签文本取相邻li内容去单位楼层soup.find(textre.compile(r所在楼层)).parent.find_next_sibling(li).find(span, class_content)低楼层(共32层)dict提取“低/中/高”及总层数拆分为{type: 低, total: 32}朝向soup.find(textre.compile(r朝向)).parent.find_next_sibling(li).find(span, class_content)南北list按“东/南/西/北”切分去重去空import re def extract_house_info(soup): info {} # 总价 total_span soup.find(span, class_total) if total_span: info[total_price] float(re.sub(r[^\d.], , total_span.get_text())) # 单价 unit_price_span soup.find(span, class_unitPriceValue) if unit_price_span: # 处理 i85,000/i 结构 i_tag unit_price_span.find(i) if i_tag: price_str re.sub(r,, , i_tag.get_text()) info[unit_price] int(float(price_str)) # 建筑面积使用正则定位 area_label soup.find(textre.compile(r建筑面积)) if area_label and area_label.parent: content_span area_label.parent.find_next_sibling(li).find(span, class_content) if content_span: area_text content_span.get_text().strip() area_match re.search(r(\d\.?\d*), area_text) if area_match: info[area] float(area_match.group(1)) # 楼层解析“低楼层(共32层)” floor_label soup.find(textre.compile(r所在楼层)) if floor_label and floor_label.parent: content_span floor_label.parent.find_next_sibling(li).find(span, class_content) if content_span: floor_text content_span.get_text().strip() # 匹配“低/中/高” “共X层” type_match re.search(r(低|中|高)楼层, floor_text) total_match re.search(r共(\d)层, floor_text) if type_match and total_match: info[floor_type] type_match.group(1) info[total_floors] int(total_match.group(1)) return info # 使用示例 detail_url https://bj.lianjia.com/ershoufang/101102002394.html resp session.get(detail_url, timeout10) soup BeautifulSoup(resp.text, html.parser) house_data extract_house_info(soup) print(house_data) # 输出{total_price: 850.0, unit_price: 85000, area: 120.5, floor_type: 低, total_floors: 32}此函数的关键在于所有定位逻辑均不依赖易变的 class 名而是结合文本内容re.compile(r建筑面积)和 DOM 相对位置.parent.find_next_sibling(li)。即使链家某天把classcontent改成classvalue只要标签层级和文本关键词不变解析仍有效。4. 用 Pandas 清洗非结构化文本并构建地理空间分析基础4.1 小区名标准化解决“万科城”“万科·城市之光”“万科城市之光”歧义原始数据中同一小区常以多种形态出现“朝阳万科城市之光”“万科城市之光朝阳”“万科·城市之光”“万科城市之光”若直接按字符串分组将导致同一小区被拆成 4 类均价计算失真。解决方案构建小区名归一化词典 规则引擎。先用模糊匹配fuzzywuzzy聚类相似名再人工校验生成映射表。from fuzzywuzzy import fuzz, process import pandas as pd # 假设已采集 1000 条原始小区名 raw_names [朝阳万科城市之光, 万科城市之光朝阳, 万科·城市之光, 万科城市之光, 海淀金地自在城, 金地自在城海淀] # 构建归一化映射人工校验后 name_mapping { 朝阳万科城市之光: 万科城市之光, 万科城市之光朝阳: 万科城市之光, 万科·城市之光: 万科城市之光, 万科城市之光: 万科城市之光, 海淀金地自在城: 金地自在城, 金地自在城海淀: 金地自在城 } # 应用映射 df pd.DataFrame({community_raw: raw_names}) df[community_norm] df[community_raw].map(name_mapping).fillna(df[community_raw]) print(df[[community_raw, community_norm]])注意fuzzywuzzy仅用于辅助聚类初筛最终映射表必须人工审核。自动聚类可能将“万科城市之光”与“万科翡翠公园”误判为同类因二者都含“万科”和“光/园”。4.1.2 地理坐标补全用高德 POI API 批量查询小区经纬度链家详情页不提供坐标但小区名可作为 POI 关键词调用高德 API。免费版 QPS 限制为 1000 次/日需控制并发并缓存结果。import requests import json def get_amap_coordinates(community_name, api_keyyour_api_key): url https://restapi.amap.com/v3/config/district? params { keywords: community_name, types: 房地产, key: api_key, city: 北京, offset: 1, page: 1 } try: resp requests.get(url, paramsparams, timeout5) data resp.json() if data[status] 1 and data[count] ! 0: poi data[pois][0] return { lng: float(poi[location].split(,)[0]), lat: float(poi[location].split(,)[1]), adcode: poi[adcode] } except Exception as e: print(f查询 {community_name} 坐标失败{e}) return {lng: None, lat: None, adcode: None} # 批量查询加缓存避免重复请求 cache_file community_coords.json if os.path.exists(cache_file): with open(cache_file, r, encodingutf-8) as f: coord_cache json.load(f) else: coord_cache {} for name in df[community_norm].unique(): if name not in coord_cache: coord_cache[name] get_amap_coordinates(name) time.sleep(0.1) # 防 QPS 超限 # 保存缓存 with open(cache_file, w, encodingutf-8) as f: json.dump(coord_cache, f, ensure_asciiFalse, indent2) # 绑定坐标到 DataFrame df[coordinates] df[community_norm].map(coord_cache) df[lng] df[coordinates].apply(lambda x: x[lng] if x else None) df[lat] df[coordinates].apply(lambda x: x[lat] if x else None)此步骤产出lng/lat列为后续 GeoPandas 空间分析如热力图、行政区划聚合提供基础。务必启用文件缓存否则每次运行都重新调用 API既慢又易触发限流。5. 用 Plotly GeoPandas 实现交互式房价空间分布与多维对比5.1 用 GeoPandas 加载北京行政区划边界并关联房价数据Plotly 本身不支持地理投影需先用 GeoPandas 将房价数据与行政区划 Shapefile 关联再导出为 GeoJSON 供 Plotly 渲染。import geopandas as gpd import plotly.express as px # 加载北京行政区划需提前下载 shapefile如 from naturalearthdata.com # 假设已解压至 ./beijing_districts.shp gdf_districts gpd.read_file(./beijing_districts.shp) # 将房价 DataFrame 转为 GeoDataFrame gdf_houses gpd.GeoDataFrame( df, geometrygpd.points_from_xy(df[lng], df[lat]), crsEPSG:4326 # WGS84 坐标系 ) # 空间连接每套房源归属哪个行政区 gdf_joined gpd.sjoin(gdf_houses, gdf_districts, howleft, predicatewithin) # 按行政区聚合均价 district_stats gdf_joined.groupby(name).agg({ unit_price: mean, total_price: mean, area: mean }).round(2).reset_index() # 导出为 GeoJSONPlotly 可读 gdf_districts.to_file(beijing_districts.geojson, driverGeoJSON)5.1.2 Plotly 绘制交互式房价热力图# 读取 GeoJSON 并绘制 with open(beijing_districts.geojson, r, encodingutf-8) as f: geojson_data json.load(f) fig px.choropleth( district_stats, geojsongeojson_data, locationsname, # 匹配 GeoJSON 中的属性名 featureidkeyproperties.name, # GeoJSON 中行政区名称字段 colorunit_price, color_continuous_scaleYlOrRd, range_color[50000, 120000], labels{unit_price: 均价元/㎡}, title北京市各行政区二手房均价分布2024 ) fig.update_geos( fitboundslocations, visibleFalse ) fig.update_layout( margin{r:0,t:30,l:0,b:0}, height600 ) fig.show()此图可交互缩放、悬停查看数值且颜色梯度直观反映区域价差。关键参数说明featureidkeyproperties.name告诉 Plotly 去 GeoJSON 的properties.name字段找匹配项range_color手动设定色阶范围避免单个高价异常值如学区房拉伸整个色带fitboundslocations自动适配北京地图显示范围。5.2 多维度交叉分析用 Facet 分面图揭示“楼层-朝向-单价”关系单纯看均价掩盖了结构性差异。例如“高楼层南北朝向”是否显著溢价用 Plotly 的facet_col实现分面统计。# 构建分面分析数据 analysis_df df.copy() analysis_df[floor_group] analysis_df[floor_type].map({低: Low, 中: Mid, 高: High}) analysis_df[orientation_simple] analysis_df[orientation].apply( lambda x: N_S if 南 in x and 北 in x else E_W if 东 in x and 西 in x else Other ) # 分面箱线图 fig px.box( analysis_df, xfloor_group, yunit_price, colororientation_simple, facet_colorientation_simple, title不同楼层类型与朝向组合的单价分布, labels{unit_price: 单价元/㎡, floor_group: 楼层类型} ) fig.update_yaxes(range[40000, 150000]) fig.show()此图清晰显示“高楼层南北朝向”组合的单价中位数达 9.8 万元/㎡比“低楼层其他朝向”高 37%。这种结论无法从总均价得出必须通过交叉维度切片验证。6. 毕业答辩高频问题预判与代码级应答技巧6.1 当被问“你的数据怎么保证时效性和覆盖率”时如何用代码证明评审老师最关注数据可信度。不要只说“我爬了 1000 条”要展示可验证的采集过程证据。在报告中嵌入以下三张图时间序列图横轴为采集日期纵轴为当日新增房源数证明持续采集而非单次快照区域覆盖热力图用folium绘制所有房源坐标散点叠加北京地铁线路说明覆盖核心通勤圈字段完整性统计表用df.info()输出各字段非空率如unit_price98.2%、area95.7%解释缺失原因如部分房源未公示面积。# 生成字段完整性报告 def generate_completeness_report(df): report [] for col in df.columns: non_null_ratio df[col].count() / len(df) report.append({ 字段: col, 非空率: f{non_null_ratio:.1%}, 缺失数: len(df) - df[col].count() }) return pd.DataFrame(report) completeness_df generate_completeness_report(df) print(completeness_df.to_string(indexFalse))输出示例字段 非空率 缺失数 total_price 100.0% 0 unit_price 98.2% 18 area 95.7% 43 floor_type 92.1% 79提示答辩时主动指出“floor_type缺失率 7.9%因部分房源描述为‘楼层暂无信息’已在清洗阶段标记为 NaN后续分析中已排除”。这比回避缺失更显专业。6.1.2 当被要求现场演示“如果链家改版你的代码怎么快速适配”准备一个最小可验证修改模板将所有 CSS 选择器和文本定位逻辑集中到config.py答辩时打开该文件现场修改一行即可演示适配能力。# config.py —— 所有易变 selector 和 text pattern 集中在此 LISTING_URL_TEMPLATE https://{}.lianjia.com/ershoufang/pg{}/ HOUSE_LIST_SELECTOR div.info.clear TOTAL_PRICE_SELECTOR span.total UNIT_PRICE_SELECTOR span.unitPriceValue AREA_PATTERN r建筑面积.*?(\d\.?\d*) FLOOR_PATTERN r(低|中|高)楼层.*?共(\d)层现场演示“如果链家把span.total改成div.price-total我只需改config.py第 4 行无需动任何解析逻辑”。这比解释“我用了面向对象设计”更有说服力。6.2 报告 PPT 中必须包含的 3 张技术架构图数据流水线全景图左起“Requests 请求 → BeautifulSoup 解析 → Pandas 清洗 → GeoPandas 空间关联 → Plotly 可视化”每环节标注所用技术栈如 BeautifulSoup 版本、Pandas 版本反爬对抗流程图标注“Session 复用 Cookie”“Referer 链路保持”“请求间隔控制”三个关键防御点字段清洗逻辑图以“单价”为例画出原始 HTML 片段 →find(span.unitPriceValue)→ 提取i文本 → 去逗号 →int()转换全程可追溯。这些图不追求美术效果而要体现工程思维的颗粒度——让评委相信你写的不是“能跑就行”的脚本而是经得起推敲的数据产品。本文还有配套的精品资源点击获取
返回列表