尧图精选

Python房产数据爬取与可视化实战:Playwright反爬+地理编码+交互热力图

🕒 发布时间:2026/9/20 1:24:23 📁 来源:尧图网络
简介本资源是一份面向专科及本科毕业生的毕业论文范例聚焦Python网络爬虫在二手房数据采集与可视化分析中的工程实践解决房地产信息分散、难比对的现实问题助力学生掌握数据挖掘全流程技术栈。资源为单文件DOCX文档共1个文件大小28KB内容完整覆盖研究背景、Python爬虫技术选型Scrapy/BeautifulSoup、反爬策略应对、数据清洗、Matplotlib/Seaborn可视化实现、Django系统架构设计及测试评估等核心章节目录结构规范含摘要、六章正文与参考文献具备直接参考或二次开发基础。已有426人学习下载文中包含西南财经大学学士学位论文标准格式、真实网站爬取逻辑、房价区域热度分析案例及可复用的代码思路与图表设计说明适合毕业设计选题、课程设计拓展与Python数据项目入门实战。1. 用 Python 爬取真实二手房源数据并画出价格热力图不是 demo是能跑通、能查房、能导出的完整链路你打开链家、贝壳或安居客看到的每一套挂牌房源背后都是一组结构化字段总价、单价、面积、朝向、楼层、装修、建成年代、地铁距离、学区标签……这些数据本就公开但人工一页页点开、复制、粘贴效率极低且无法回溯。而用 Python 构建一套稳定采集 清洗 分析 可视化的闭环不是写个“Hello World”式爬虫而是要解决反爬识别、页面动态加载、字段缺失补全、地理坐标解析、多平台数据对齐等真实工程问题。本文面向有基础 Python 能力会写函数、读过 requests 和 pandas 文档的从业者不讲 urllib 基础语法也不堆砌“requests BeautifulSoup 万能组合”空话而是从「为什么选 Selenium Playwright 混合方案」开始到「用 Folium 生成带房价气泡的交互地图」结束每一步命令可复制、每个参数有依据、每个失败点有排查路径。如果你正被业务部门催着交一份区域房价分布报告或想把爬虫能力真正嵌入数据分析工作流这篇就是你今天该花 45 分钟读完的实操指南。2. 用 Playwright 处理动态渲染 Selenium 补全 JS 执行绕过主流房产平台的前端校验主流房产平台如贝壳找房、链家 PC 端、58 同城二手房频道已普遍采用 SPA 架构与前端风控逻辑页面初始 HTML 不含房源列表需触发滚动、点击“下一页”或执行 JS 函数后才加载真实数据部分平台还会检测navigator.webdriver、window.chrome等属性直接拦截无头浏览器请求。纯 requests 正则或静态解析已失效必须引入具备真实浏览器行为的工具。Playwright 因其默认禁用自动化特征、支持多浏览器上下文隔离、API 更简洁成为首选但某些平台如部分城市安居客子站仍依赖 Selenium 的execute_script手动注入绕过脚本。因此我们采用混合策略主流程用 Playwright 启动 Chromium 实例设置--disable-blink-featuresAutomationControlled并覆盖navigator.permissions.query返回值对个别需手动触发__NEXT_DATA__注入的页面则切换至 Selenium WebDriver 并调用driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, {...})注入伪造对象。2.1 安装 Playwright 并配置抗检测启动参数# 推荐使用 pipx 隔离环境避免与系统 Python 冲突 pipx install playwright playwright install chromium # 若在 Linux 服务器无 GUI 环境需额外安装依赖 sudo apt-get update sudo apt-get install -y \ libnss3 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgcc1 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ ca-certificates \ fonts-liberation \ libappindicator1 \ libasound2 \ libatk-bridge2.0-0 \ libatspi2.0-0 \ libdrm2 \ libgbm1 \ libpango-1.0-0 \ libxkbcommon0 \ wget \ xdg-utils提示Playwright 默认启用headlessTrue但部分平台会检测headless字符串。实际部署时应显式设为headlessnewChromium 116 新模式并配合user_agent伪装成主流桌面浏览器。不要用网上流传的“随机 UA 列表”而应固定使用Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36—— 这是当前 Chrome Stable 最新版本的真实 UA平台风控白名单中已包含该字符串。2.2 编写抗检测的 Page 初始化函数from playwright.sync_api import sync_playwright import time def init_page_with_anti_detect(): with sync_playwright() as p: # 启动 Chromium禁用自动化特征 browser p.chromium.launch( headlessnew, args[ --disable-blink-featuresAutomationControlled, --no-sandbox, --disable-setuid-sandbox, --disable-dev-shm-usage, --disable-gpu, --disable-extensions, --disable-plugins, --disable-background-networking, --disable-default-apps, --disable-ipc-flooding-protection, --disable-renderer-backgrounding, --disable-background-timer-throttling, --disable-reading-from-canvas, --disable-2d-canvas-clip, --disable-webgl, --disable-web-security, --disable-featuresIsolateOrigins,site-per-process,TranslateUI,BlinkGenPropertyTrees, --proxy-serverdirect://, --proxy-bypass-list*, ] ) context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, viewport{width: 1920, height: 1080}, java_script_enabledTrue, bypass_cspTrue, ) # 注入 JS 覆盖 navigator.webdriver 等敏感属性 context.add_init_script( Object.defineProperty(navigator, webdriver, { get: () undefined }); Object.defineProperty(navigator, permissions, { get: () ({ query: (permission) Promise.resolve({ state: granted }) }) }); window.chrome { runtime: {} }; Object.defineProperty(navigator, plugins, { get: () [1, 2, 3, 4, 5] }); ) page context.new_page() return browser, context, page # 使用示例 browser, context, page init_page_with_anti_detect() page.goto(https://bj.ke.com/ershoufang/) time.sleep(3) # 等待首屏渲染完成非轮询 print(Page loaded successfully, ready for listing extraction.)2.2.1 关键参数说明与调试技巧参数作用修改建议headlessnew启用 Chromium 新版无头模式规避旧版headlesstrue检测不可改为True或old否则多数平台返回空白页--disable-blink-featuresAutomationControlled禁用 Blink 引擎中用于识别自动化行为的特性必须保留是绕过navigator.webdriver检测的核心add_init_script(...)中的navigator.permissions.query伪造权限查询结果防止因geolocation权限拒绝导致 JS 报错中断若目标页面含地图组件此行必加否则可删减以提升启动速度viewport设为1920x1080模拟主流桌面分辨率避免平台返回移动端精简版 HTML若采集移动站如 m.ke.com应改为375x667并同步更换 UA注意Playwright 的page.wait_for_timeout(3000)是硬等待不可替代page.wait_for_selector()。真实场景中应监听.listContent li.clear贝壳列表项或.sellListContent li链家等具体选择器例如page.wait_for_selector(.listContent li.clear, timeout10000)。超时时间设为 10 秒而非 3 秒是为应对 CDN 加载延迟或平台临时限速。3. 解析房源卡片并结构化存储从 HTML 标签到 Pandas DataFrame 的字段映射规则爬取页面后核心任务是将 DOM 中的文本、属性、嵌套结构准确映射为结构化字段。不同平台字段命名差异极大贝壳用>import re import pandas as pd from bs4 import BeautifulSoup def parse_ke_com_listing(html_content: str) - list[dict]: 解析贝壳找房 PC 端二手房列表页 HTML返回标准化字段字典列表 标准字段id, title, total_price, unit_price, area, direction, floor, renovation, year_built, district, subway, school, url soup BeautifulSoup(html_content, html.parser) listings [] # 定位所有房源卡片容器贝壳为 .listContent li cards soup.select(.listContent li.clear) for card in cards: try: # 房源 ID从>import requests import time import json from typing import List, Dict, Optional def batch_geocode_amap(addresses: List[str], city: str 北京, key: str YOUR_AMAP_KEY) - List[Dict]: 调用高德地图地理编码 API批量将地址转为经纬度 注意免费版 QPS 限制为 1需加 sleep 控制频率 results [] base_url https://restapi.amap.com/v3/geocode/geo for addr in addresses: # 构造查询参数地址 城市 key params { address: f{addr}{city}, city: city, key: key, output: json } try: resp requests.get(base_url, paramsparams, timeout10) data resp.json() if data.get(status) 1 and data.get(count) 1: geocode data[geocodes][0] results.append({ address: addr, longitude: float(geocode[location].split(,)[0]), latitude: float(geocode[location].split(,)[1]), formatted_address: geocode[formatted_address], level: geocode[level] }) else: # API 返回失败记录错误原因 results.append({ address: addr, longitude: None, latitude: None, formatted_address: None, level: None, error: data.get(info, Unknown error) }) except Exception as e: results.append({ address: addr, longitude: None, latitude: None, formatted_address: None, level: None, error: str(e) }) # 高德免费版限流1 次/秒 time.sleep(1.1) return results # 示例从贝壳数据中提取唯一小区名title 中前半段常为小区名 unique_estates df_ke[title].str.split( ).str[0].dropna().unique().tolist() geo_results batch_geocode_amap(unique_estates[:50]) # 先试 50 个 # 转为 DataFrame 并与原数据 merge geo_df pd.DataFrame(geo_results) df_ke_geo df_ke.merge(geo_df, left_ontitle, right_onaddress, howleft) print(fGeo-coded {df_ke_geo[longitude].count()} out of {len(df_ke_geo)} listings)4.1.1 高德 API 调用关键参数说明参数值示例说明address国贸公寓北京必须拼接城市名否则返回北京市中心坐标不要加“小区”二字高德对“国贸公寓”识别率高于“国贸公寓小区”city北京限定搜索城市范围提升精度若跨城采集需动态传入key7a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p在高德开放平台申请免费额度 1 万次/日足够中小项目outputjson固定值返回结构化 JSON便于解析注意高德 API 返回的level字段表示匹配精度“兴趣点”、“门牌号”、“道路”等若为“城市”或“省份”说明地址太模糊需人工核对。生产环境应增加level 兴趣点的过滤条件避免坐标漂移。4.2 用 FuzzyWuzzy 对齐不同平台的小区名称from fuzzywuzzy import fuzz, process import numpy as np def align_estate_names(ke_titles: pd.Series, lianjia_titles: pd.Series, threshold: int 75) - pd.DataFrame: 用 fuzzywuzzy 匹配贝壳与链家的小区名返回相似度矩阵 返回 DataFrameindexke_title, columnslianjia_title, valuessimilarity_score # 提取小区名贝壳 title 通常为 国贸公寓 3室2厅取前两个词链家为 【朝阳】国贸公寓 ke_estates ke_titles.str.extract(r^([\u4e00-\u9fa5a-zA-Z0-9·\-\.\s]{2,15})[\s\d\u4e00-\u9fa5]*$)[0].str.strip() lj_estates lianjia_titles.str.extract(r【.*?】(.))[0].str.strip() # 去重并转为列表 ke_unique ke_estates.dropna().unique().tolist() lj_unique lj_estates.dropna().unique().tolist() # 计算两两相似度 similarity_matrix [] for ke in ke_unique: row [] for lj in lj_unique: score fuzz.token_sort_ratio(ke, lj) # 忽略词序适合“国贸公寓”vs“公寓国贸” row.append(score if score threshold else 0) similarity_matrix.append(row) return pd.DataFrame(similarity_matrix, indexke_unique, columnslj_unique) # 使用示例 sim_df align_estate_names(df_ke[title], df_lianjia[title]) # 找出最高相似度的匹配对 best_matches sim_df.stack().sort_values(ascendingFalse).head(10) print(Top 10 matched estate pairs:) print(best_matches)4.2.1 FuzzyWuzzy 算法选型依据方法调用方式适用场景本例选用理由fuzz.ratio()fuzz.ratio(s1, s2)字符串完全匹配度不适用因“国贸公寓”和“国贸·公寓”字符不同但语义相同fuzz.partial_ratio()fuzz.partial_ratio(s1, s2)子串匹配防缩写不适用易将“国贸”误匹配为“国贸公寓”的子串fuzz.token_sort_ratio()fuzz.token_sort_ratio(s1, s2)分词后排序再比对✅ 本例首选将“国贸 公寓”和“公寓 国贸”排序为相同序列再计算编辑距离fuzz.WRatio()fuzz.WRatio(s1, s2)加权综合算法可作为备选但token_sort_ratio更可控、更易调试提示threshold75是经验值。低于 70 易产生噪声匹配如“望京”匹配“朝阳”高于 85 会漏掉合理变体如“华润城”vs“华润·润府”。实际项目中应先抽样 100 对人工标注用sklearn.metrics.f1_score评估不同阈值下的准确率再确定最终值。5. 用 Folium 绘制房价热力图与交互式气泡地图支持按行政区、装修、房龄筛选可视化不是简单调plt.hist()而是让业务方能自助探索数据。Folium 因其基于 Leaflet.js、支持 GeoJSON、可导出独立 HTML 文件、无需服务器部署成为房产数据可视化的事实标准。本章不展示“画一个散点图”而是构建一个可筛选、可缩放、可点击查看详情的交互地图左上角下拉菜单选择行政区右侧滑块控制房龄范围地图上气泡大小代表总价、颜色深浅代表单价点击气泡弹出房源标题、链接、核心参数。所有逻辑封装为create_interactive_map()函数输入为清洗后的 DataFrame输出为map.html文件。5.1 构建可筛选的 Folium 交互地图import folium from folium import plugins import branca.colormap as cm import pandas as pd def create_interactive_map( df: pd.DataFrame, output_path: str house_price_map.html, center_lat: float 39.9042, center_lng: float 116.4074, zoom_start: int 11 ) - folium.Map: 创建交互式二手房价格地图 支持气泡大小总价颜色单价点击弹窗房源详情右上角图例 # 初始化地图 m folium.Map( location[center_lat, center_lng], zoom_startzoom_start, tilesCartoDB positron, # 无广告、加载快的底图 attrcopy; a hrefhttps://www.openstreetmap.org/copyrightOpenStreetMap/a contributors ) # 过滤掉无坐标的记录 df_valid df.dropna(subset[latitude, longitude]).copy() # 创建单价颜色映射归一化到 0-1再映射到 Viridis 色带 min_unit df_valid[unit_price].min() max_unit df_valid[unit_price].max() colormap cm.LinearColormap( colors[blue, green, yellow, red], vminmin_unit, vmaxmax_unit, caption单价元/㎡ ) m.add_child(colormap) # 添加气泡标记 for idx, row in df_valid.iterrows(): if pd.isna(row[latitude]) or pd.isna(row[longitude]): continue # 气泡大小总价万元缩放最小 5最大 30 radius np.clip(row[total_price] * 0.03, 5, 30) if pd.notna(row[total_price]) else 10 # 颜色根据单价映射 color colormap(row[unit_price]) if pd.notna(row[unit_price]) else #999999 # 弹窗内容 popup_html f b{row[title]}/bbr 总价{row[total_price]} 万元br 单价{row[unit_price]} 元/㎡br 面积{row[area]} ㎡br 朝向{row[direction] or 未知}br a href{row[url]} target_blank查看详情 →/a folium.CircleMarker( location[row[latitude], row[longitude]], radiusradius, popupfolium.Popup(popup_html, max_width300), colorcolor, fillTrue, fill_colorcolor, fill_opacity0.7, weight1, ).add_to(m) # 添加图层控制可选未来扩展地铁线、学区边界等 GeoJSON folium.LayerControl().add_to(m) # 保存地图 m.save(output_path) print(fInteractive map saved to {output_path}) return m # 使用示例传入已 geocode 的 df_ke_geo m create_interactive_map(df_ke_geo, beijing_ershoufang_map.html)5.1.1 Folium 参数调优与性能优化参数推荐值说明tilesCartoDB positron✅ 替代默认OpenStreetMap加载更快、无商业水印、适配房产数据的浅色背景radiusnp.clip(...)min5, max30防止总价过高的豪宅如 5000 万气泡遮盖整片区域缩放系数0.03需根据数据分布调整fill_opacity0.70.5~0.8 之间透明度太高看不清太低则重叠气泡无法分辨层次0.7 是兼顾可读性与美观的平衡点popupfolium.Popup(..., max_width300)max_width300防止弹窗过宽破坏地图布局HTML 中用br换行比\n更可靠提示若数据量 5000 条直接CircleMarker会卡顿。此时应改用plugins.MarkerCluster进行聚类或预处理为 GeoJSON 后用folium.GeoJson()渲染。本例假设单城市采集量在 2000 条以内优先保证交互响应速度。5.2 导出为可离线运行的 HTML 并嵌入筛选控件# 在 create_interactive_map 函数末尾添加以下代码注入自定义 JavaScript 控件 m.get_root().html.add_child(folium.Element( script // 简单的行政区筛选实际项目中应从 df.district.unique() 动态生成 document.addEventListener(DOMContentLoaded, function() { const filterSelect document.createElement(select); filterSelect.id district-filter; filterSelect.style.cssText position:fixed;top:10px;left:10px;z-index:1000;background:white;padding:5px;border-radius:4px;; [朝阳, 海淀, 丰台, 西城, 东城].forEach(d { const opt document.createElement(option); opt.value d; opt.textContent d; filterSelect.appendChild(opt); }); document.body.appendChild(filterSelect); filterSelect.addEventListener(change, function() { alert(筛选功能需后端支持此处仅为示意。实际项目中应通过 Flask API 动态重绘地图。); }); }); /script )) # 保存时自动注入 m.save(output_path)注意Folium 本身不提供服务端筛选上述 JS 仅为示意前端交互形态。真实落地需搭配 Flask/FastAPI 提供/api/filter?district朝阳min_year2000接口返回过滤后的 GeoJSON再由前端 JS 调用map.eachLayer(...).remove()清空旧图层L.geoJSON(data).addTo(map)重绘。本节聚焦“能跑通的最小可行地图”服务端集成属于下一阶段工程化范畴。6. 用 pandas-profiling 一键生成数据质量报告快速定位字段缺失、异常值与分布偏斜采集与可视化完成后最后一道关卡是验证数据可信度。业务方不会关心你用了 Playwright 还是 Selenium他们只问“这个均价 8.5 万/㎡ 是怎么算出来的有没有把别墅和老破小混在一起” 因此必须在交付前生成一份机器可读、人可理解的数据质量快照。pandas-profiling现名ydata-profiling是目前最成熟的自动化 EDA 工具它能在 30 秒内输出 HTML 报告涵盖每个字段的缺失率、唯一值数量、数值分布直方图、类别频次、相关性热力图、甚至“疑似异常值”高亮如单价 30 万/㎡ 的记录。这不是可选项而是数据交付的准入门槛。6.1 生成专业级数据质量报告# 安装最新版 ydata-profilingpandas-profiling 已归档 pip install ydata-profiling # 若使用 conda conda install -c conda-forge ydata-profilingfrom ydata_profiling import ProfileReport import pandas as pd def generate_data_profile(df: pd.DataFrame, output_path: str data_profile.html): 为二手房数据生成交互式质量报告 自动检测缺失值、重复行、异常值、高相关性字段、文本长度分布 # 配置 ProfileReport关闭耗时的 correlations除非需分析字段关系 profile ProfileReport( df, title北京二手房数据质量报告, explorativeTrue, # 启用深度分析如异常值检测 minimalFalse, # 不启用 minimal 模式保留全部图表 samplesNone, # 显示所有样本非抽样 duplicatesNone, # 显示重复行分析 missing_diagrams{ bar: True, matrix: True, heatmap: True }, correlations{ pearson: {calculate: False}, spearman: {calculate: False}, kendall: {calculate: False}, phi_k: {calculate: False}, cramers: p a hrefhttps://download.csdn.net/download/No_Name_Cao_Ni_Mei/88488588 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
上一篇/下一篇内容由系统自动关联 返回资讯列表 →