链家上海租房爬虫实战:数据清洗与可视化分析 简介链家出租房上海为例爬虫、数据分析与可视化是一套面向Python学习者与期末大作业场景的完整项目方案源码均在本地编译通过评审分95分以上难度适中适合作为毕业设计或课程大作业的参考模板。压缩包共20个文件约20.91MB涵盖爬虫脚本、交互式分析文件、采集得到的表格数据集、可视化图表13张图片如词云图、聚类图、散点图、热力图等并附有详细实验报告、答辩演示文稿与说明文档能够覆盖从数据采集、清洗分析到展示汇报的全流程。项目以上海链家出租房为实例涉及房源面积、总价、户型、装修类型、区域分布等维度的分析与可视化代码结构清晰图表类型丰富便于理解爬虫与数据分析的完整思路。目前已有104人学习下载对有相似选题需求的学生具有直接参考价值。1. 链家租房爬虫为什么拿上海当样本比全国数据更靠谱如果你正在做“爬虫数据分析可视化”方向的大作业多半纠结过一个问题抓全国数据还是单城市数据。我的建议是除非导师明确要求“全国维度”否则老老实实选一个城市比如上海。链家上海租房页面结构规整房源量在几万条的量级既能体现爬虫与分布式处理的价值又不至于让反爬和存储把作业变成灾难。更重要的是上海各行政区的租金差异非常明显随便跑几张图就能看出“内环贵、郊区便宜”的规律对数据分析阶段非常友好。这套从 requests 到 XPath、再到 pandas 和 ECharts 的链路适合所有想把大作业做成完整作品的人——它不要求你有分布式爬虫背景但能让你跑通从发请求到写报告的全过程。下文我会按一个可复现的路径来讲包括代码、参数和最容易踩的坑。2. 链家上海租房爬虫从请求到解析的最小闭环2.1 先看页面结构再写XPath打开开发者工具的直观步骤很多人拿到链家租房页面就直接复制网上的 XPath结果没两天选择器就失效。链家的前端结构会随着版本更新调整但核心思路不变先手动打开一个列表页按 F12 进入开发者工具用元素选择器定位一张租房卡片。你会发现每条房源在ul.log-list下的li里每一条li上有>import requests from lxml import etree 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, Referer: https://sh.lianjia.com/zufang/, } base_url https://sh.lianjia.com/zufang/pg{page}/ all_items [] for page in range(1, 11): url base_url.format(pagepage) resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 if resp.status_code ! 200: print(f第{page}页请求失败状态码: {resp.status_code}) break tree etree.HTML(resp.text) # 每条房源所在的 li 节点class 是稳定的 li_list tree.xpath(//div[contains(class,content__list--item)]) for li in li_list: title li.xpath(.//div[contains(class,content__list--item--main)]//a/text())[0].strip() rent li.xpath(.//span[contains(class,content__list--item-price)]/em/text()) rent rent[0] if rent else 0 location li.xpath(.//p[contains(class,content__list--item--des)]/a/text()) location .join([x.strip() for x in location]) if location else item { title: title, rent: float(rent), location: location, } all_items.append(item) print(f第{page}页完成累计 {len(all_items)} 条) time.sleep(2) # 重要减速避免被封 print(all_items[:5])这段代码里pg{page}是链家分页的标准格式第一页对应pg1。注意resp.encoding utf-8不设置的话中文可能乱码。li.xpath前面有.表示在当前li节点下继续查找而不是从整个文档重新找。解析时优先用contains(class, ...)而不是精确等于class因为链家的 class 后面经常会拼接一段动态的哈希值。rent字段用em标签定位因为租金数字单独放在该标签内。最后每页抓完强制time.sleep(2)这能降低请求频率虽然不能完全规避反爬但至少不会刚发出 10 个请求就被服务器拒绝。2.3 翻页减速与请求头伪装别让IP被临时禁掉初版代码跑通后你一定想提高抓取速度于是把time.sleep(2)改成了time.sleep(0.1)以为这样可以更快。结果跑了三页就收到 403甚至captcha.html。这就是爬虫里最经典的翻车现场频率过高导致临时封禁。链家对 IP 的限流策略比我见过的其他房产网站更敏感尤其是上海这种热门城市单 IP 一分钟请求超过 20 次就可能触发验证码。解决方案分三级。第一级保持 1 到 3 秒的随机延迟使用random.uniform(1.5, 3.5)而不是固定数值。第二级准备一个可用的 Cookie登录一次链家网页版把浏览器里的 Cookie 复制到代码里。不登录的匿名请求在某些时段会被加重校验带上 Cookie 能显著降低风险。第三级如果作业要求抓取几万条数据那就不能用单线程了这时可以考虑用ThreadPoolExecutor配合代理池但要注意代理质量参差不齐反而更容易被封。对大作业来说300 条数据完全够用不需要上分布式爬虫写清楚你的设计和选型理由比硬堆技术更得分。除了请求头你还得处理解析的健壮性。链家偶尔会在列表页插入广告位导致li数量对不上但我的经验是只要 XPath 写的是“包含特定 class”的容器广告节点通常不带这些 class所以即使混入也不会爆错。如果某条数据解析失败不要直接让程序崩溃而是把它记录到异常列表里最后统一补抓。3. 把爬下来的数据洗成能分析的格式数据清洗与标准化3.1 字符串拆分把“整租·3室1厅”拆成室厅与面积列表页返回的title一般是“整租·3室1厅·南北”而location是一个拼接字符串比如“中山公园/江苏路/长宁”。如果直接拿这串字符串去做聚合你得不到任何分组粒度。所以数据清洗的第一步是把这些字段拆开。我用的是最简单的 split因为链家的格式非常固定。下面这段代码演示如何把title拆出租赁方式、室、厅把location拆出小区名和行政区。import pandas as pd import re df pd.DataFrame(all_items) # 拆分title df[lease_type] df[title].apply(lambda x: x.split(·)[0]) df[rooms] df[title].apply(lambda x: x.split(·)[1]) df[orientation] df[title].apply(lambda x: x.split(·)[2] if len(x.split(·)) 2 else 未知) # 从rooms中提取数字 df[bedrooms] df[rooms].str.extract(r(\d)室).astype(float) df[livingrooms] df[rooms].str.extract(r(\d)厅).astype(float) # 拆location链家一般格式是板块/商圈/行政区 df[plate] df[location].apply(lambda x: x.split(/)[0] if / in x else x) df[district] df[location].apply(lambda x: x.split(/)[-1] if / in x else x) print(df[[title, lease_type, bedrooms, district]].head())这段代码里正则表达式r(\d)室是从字符串中提取第一个数字比如“3室1厅”得到 3 和 1。如果遇到“1室0厅”甚至“开间”没有厅的房源会提取到 NaN这就是后面要处理的问题。我建议用str.extract而不是直接匹配因为extract可以忽略干扰项。但这里有个坑浦东新区的名字叫“浦东新区”有的列表里显示为“浦东”如果你后续想要画地图必须统一行政区名称否则聚合时会出现两个“浦东”。3.2 租金与单位价格计算处理中文数字与NaN租金字段虽然已经是数字但页面里经常出现“8000元/月”这样的格式如果直接用float()会报错。比较稳的做法是提前把非数字字符全部删除再转换。另外不少房源没有填写租金页面显示“暂无”这在列表页里会变成空字符串。下面这段代码处理这些问题并计算每平方米月租金这是做区域比价的常用指标。def parse_rent(s): if isinstance(s, str): s re.sub(r[^\d.], , s) return float(s) if s else None return s df[rent] df[rent].apply(parse_rent) # 处理面积字段从title或详情中获取 # 假设之前已经抓到面积并存为df[area] df[area] pd.to_numeric(df[area], errorscoerce) # 单位租金 月租金 / 面积 df[unit_price] df.apply( lambda row: round(row[rent] / row[area], 2) if row[area] and row[area] 0 else None, axis1 ) # 删除租金或面积为空的记录 df_clean df.dropna(subset[rent, area, unit_price]) print(df_clean.shape)这里关键的参数是errorscoerce它会把无法转换的值变成 NaN比如面积里的“-”。在计算unit_price时先判断area是否大于 0避免除零错误。我建议不要直接删除所有 NaN 记录而是先看看缺失比例如果缺失规模超过 20%你需要重新定义指标。比如面积缺失但租金存在可以按卧室数分组填充该组面积中位数这样能保留更多样本。链家出租房数据里“整租”的面积通常填写完整而“合租”有时只有“5室1厅”而面积留空所以根据租赁方式填充是合理的。3.3 字段补充用高德API把小区名转经纬度如果你的作业要画地图热力图那么需要经纬度。爬虫本身抓不到经纬度只能通过小区名去地理编码接口换。这里我不推荐用百度地图因为百度对非认证用户的配额更小。高德地图免费版每天大概有 5000 次地理编码额度对 300 条数据绰绰有余。具体做法是写一个函数把小区名拼上“上海”前缀然后请求高德的geocode/regeo接口拿到经度和纬度。import requests import time amap_key 你的高德Key # 在高德开放平台创建应用获得 def get_lnglat(addr): url https://restapi.amap.com/v3/geocode/geo params { address: 上海 addr, output: json, key: amap_key, } resp requests.get(url, paramsparams, timeout5) data resp.json() if data[status] 1 and data[geocodes]: loc data[geocodes][0][location] # 格式 经度,纬度 lng, lat loc.split(,) return float(lng), float(lat) return None, None # 先做缓存避免重复请求 cache {} for idx, row in df_clean.iterrows(): name row[plate] # 用板块名而不是小区名降低定位误差 if name in cache: lng, lat cache[name] else: lng, lat get_lnglat(name) cache[name] (lng, lat) time.sleep(0.3) # 控制QPS df_clean.loc[idx, lng] lng df_clean.loc[idx, lat] lat这段代码里我故意用板块名而不是小区名因为链家的小区名可能在上百个板块名只有几十个能大幅减少 API 调用次数而且画图粒度足够。如果你要精细到小区级再换回title里的小区名。time.sleep(0.3)用来控制并发高德的 QPS 限制大约是 3 到 5虽然免费版允许你每秒 3 次但为了稳定我通常留出 0.3 秒的间隔。另外一个容易忽略的问题是地址重名比如“世纪公园”可能被解析为上海世纪公园这站但如果你使用板块名重名率会低很多。地理编码结果需要保存到 CSV避免下次运行作业再重新请求这就是缓存的价值。4. 上海租房数据分析与可视化回答“哪里租得值”4.1 Pandas聚合按行政区统计平均租金与样本量数据清洗完之后分析的第一件事一定是看整体分布。大多数房产项目都要回答“哪个区租金最贵”和“哪个区性价比最高”。Pandas 的groupby能一行搞定。但这里有个微妙之处直接把所有房源的平均租金拉出来会被豪宅区拉高。所以我会同时计算中位数和样本量优先看中位数。import pandas as pd # 读取清洗后的数据 df pd.read_csv(shanghai_rent_clean.csv) district_stats df.groupby(district).agg( rent_median (rent, median), rent_mean (rent, mean), unit_price_median (unit_price, median), count (rent, count) ).sort_values(rent_median, ascendingFalse) # 过滤样本量太少的区 district_stats district_stats[district_stats[count] 10] print(district_stats)这里agg里的rent_median是给聚合结果命名(rent, median)表示用rent列计算中位数。筛选count 10很重要否则个别只有两三条房源的小板块画出来会显得异常。如果你的作业报告写到这一步记得把结果转成表格用“中位数租金”而非“平均租金”作为主要指标因为在答辩时你很容易被问到“为什么你们的平均租金和网上查到的统计年鉴不一样”。回答思路就是“中位数更能代表普通房源”。另外可以增加一个“高性价比区”的交叉分析同时看unit_price_median和距离市中心远近的关系。这里还可以用pd.cut把面积分箱然后看箱内租金差异这属于探索性分析的加分项。4.2 ECharts可视化用地图热力图展示区域租金分布可视化是大作业的门面。很多同学直接用 Matplotlib 画柱状图虽然简单但视觉效果平平。我建议用 ECharts 的地图组件它支持 GeoJSON 区域映射。如果你不想写前端代码可以直接用 pyecharts 库它把配置都封装成 Python API。但要注意 pyecharts 的版本兼容问题我用的是 1.x 版本如果你安装的是 2.x部分参数名有改动。下面代码画出一张上海行政区租金热力图。from pyecharts.charts import Map from pyecharts import options as opts # 准备数据district - rent_median data_pair [(d, v) for d, v in district_stats[rent_median].items()] c ( Map() .add(租金中位数, data_pair, maptype上海) .set_global_opts( title_optsopts.TitleOpts(title上海各行政区租房中位数元/月), visualmap_optsopts.VisualMapOpts(max_20000, is_piecewiseTrue), ) ) c.render(shanghai_rent_map.html)这里有个细节maptype上海要求 pyecharts 内置的 geo 地图数据中包含上海。如果你的 pyecharts 版本较老可能没有上海地图这时候你需要手动注册 GeoJSON。更稳妥的做法是直接用 ECharts 的官方在线地图但大作业一般不能依赖外网所以建议在本地放置一份上海地图的 GeoJSON 文件用Map.add(..., maptype上海地图)配合registe_map。如果不想折腾地图退一步可以用Bar画行政区租金排行效果也足够清晰。但既然标题里写着“可视化”我建议你至少尝试一次地图热力图这张图在报告里非常占版面。地图上的颜色区间按照 8000、12000、15000 分三段避免极值造成整体色阶失真。4.3 面积与租金关系用Seaborn回归图发现异常房源数据分析不能只看均值还得看变量之间的关系。对于租房数据最经典的关系是面积和租金。你可以用 Seaborn 的regplot画散点加回归线这会显示出一个明显的正相关但也会暴露出离群点。比如内环的 30 平米“老破小”租金可能比郊区的 90 平米整租还贵这时你需要根据回归残差把这些点标出来。import seaborn as sns import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 解决中文乱码 plt.rcParams[axes.unicode_minus] False sns.set_theme(stylewhitegrid, palettemuted) # 过滤超大数据后再作图 df_plot df[(df[area] 10) (df[area] 200) (df[rent] 50000)] g sns.lmplot(datadf_plot, xarea, yrent, huedistrict, coldistrict, col_wrap4, height3.5, scatter_kws{alpha: 0.4}, line_kws{color: red}) plt.show()lmplot的参数col会按行政区画多个子图col_wrap控制换行。对于普通作业可能只需要画一张全量散点图再加一张按户型分箱的柱状图。但如果你要让报告显得更硬核可以计算残差并筛选出“面积小但价格高”的房源这些房源往往是靠近地铁的公寓。你可以用下面的方式提取异常点from scipy.stats import linregress slope, intercept, r, p, se linregress(df_plot[area], df_plot[rent]) df_plot[pred_rent] slope * df_plot[area] intercept df_plot[resid] df_plot[rent] - df_plot[pred_rent] abnormal df_plot[np.abs(df_plot[resid]) 8000] print(abnormal[[district, plate, area, rent, pred_rent]].head())判断“异常”的阈值设为残差绝对值大于 8000 元。这个方法可以帮你在答辩时回答“如何识别高性价比和低性价比房源”。注意这里如果用huedistrict会导致图例很乱所以可以考虑只用huelease_type区分整租和合租。合租的租金本来就低和整租混在同一个回归模型里会扭曲关系建议先拆分出整租再单独做回归。5. 大作业的5个经典翻车现场反爬、数据缺失与打包避坑5.1 列表页时好时坏403状态码的根源是Headers你信心满满地运行爬虫前两次一切正常第三次突然报“HTTP 403 Forbidden”。按 F12 打开网页正常显示说明服务器已经识别出你来自脚本。最初我以为是 IP 被封后来发现只是没有设置Referer。链家对/zufang/路径会校验来源页如果Referer不是链家域名返回 403 的概率非常高。解决办法是给headers加上Referer: https://sh.lianjia.com/zufang/同时替换成完整的浏览器用户代理。做完这两步403 基本消失。不要相信网上流传的“Base headers”模版链家已经能识别Python-requests的默认特征只要发现请求头里没有Sec-Fetch-*字段就可能拒绝。你可以直接从浏览器复制完整的请求头但注意Cookie不要过期。5.2 详情页被重定向到验证码页面当你尝试抓取详情页时会发现列表页都抓完了但详情页返回的不是房源信息而是一个包含“安全验证”的 HTML。这是因为访问详情页的路径是/zufang/xxx.html该路径有独立的风控策略触发条件通常是短时间访问超过 10 个详情页。另一种情况是你在列表页中提取了某个房源链接但链家对旧房源已经下架访问 404 页面也可能被重定向到验证码。解决办法是不要并发抓详情保持 2 到 3 秒间隔并且监控响应 URL 是否仍然是sh.lianjia.com。如果发现 URL 变成了captcha.lianjia.com立即暂停 5 分钟。还有个偏方是修改Cookie中的lianjia_id但这其实是让每次请求都模拟“新访客”千万不要这么做因为这个 Cookie 唯一标记是你用于风控识别频繁清空反而会导致封禁。5.3 面积字段缺失导致的数据类型错误当你把area列从字符串转为 float 时如果遇到“暂无”或空字符串pd.to_numeric会得到 NaN。你把 NaN 的行删掉之后发现整租的样本只剩下 60%合租房源大面积缺失。解决这个问题的方法是回溯到详情页补抓面积或者直接用房源标题中的“3室1厅”去推算一个合理范围。常见做法是写一个函数如果面积缺失查看该小区或者同楼盘的均价用租金除以均价估算。这种方法虽然不准确但比你直接删掉所有缺失行更能保住样本。另一种情况是面积单位不是平方米而是“平方”在链家页面上可能格式不同比如“45.6㎡”你需要用正则提取数字。str.extract(r(\d\.?\d*))可以同时匹配整数和小数。注意不要用float(45.6㎡)那会直接抛异常。5.4 地理编码API配额耗尽导致地图空白你在数据清洗时写好了地理编码跑了一次作业得到了完整的经纬度。然而第二天重新运行时高德接口返回“USER_DAILY_LIMIT”或者“INVALID_USER_KEY”。这是一个典型的“昨天能跑今天不能跑”的问题。原因可能是你反复运行多次把 5000 次配额用完了或者是高德的 key 还没生效。解决办法是永远优先读取本地缓存的经纬度文件只有遇到新地址才请求 API。如果作业要求多次运行你最好把第一次获得的经纬度直接 merge 回主数据表。另外一点用板块名做地理编码比小区名更省配额因为板块数量固定。如果高德抛错别忘了设置重试机制最多三次间隔两秒。5.5 源码发给同学跑不通绝对路径与Python环境依赖大作业一般需要提交源码评审或同学会在别的电脑上运行。你抱着“我机器上运行没问题”的心态把代码发出去结果对方报“ModuleNotFoundError: No module named lxml”或者“FileNotFoundError”。前者是依赖问题需要在项目内提供一个requirements.txt内容至少包括requests、lxml、pandas、matplotlib、seaborn、pyecharts。后者是你用了绝对路径比如/Users/username/Desktop/xxx.csv换台机器立刻失效。正确做法是使用os.path动态拼接路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) data_path os.path.join(BASE_DIR, data, rent.csv)另外如果你的代码有画图部分需要设置中文字体否则对方电脑显示一堆方块。在提交报告时把输出图片保存成 PNG 内嵌到文档里不要只留一份动态 HTML因为有些评审点开 HTML 时可能无法加载地图包。这五个问题是大作业被扣分最多的隐藏项提前规避胜过大晚上在群里哭诉“我明明能跑”。6. 从大作业到作品假设检验、交互大屏与验收清单6.1 用统计检验证明“地铁沿线租金更高”如果你的报告只叫“数据分析”那到柱状图为止就够用了。但要拿高分建议引入一个简单的假设检验。设想你可以在报告里提出一个可证伪的假设在其他条件相同的情况下距离地铁站越近的房源单位租金越高。你可以用数据来支持它。但链家列表页没有显示具体地铁距离你可以根据经纬度和地铁站坐标计算距离然后分两组。组 A距离最近地铁站 500 米内组 B500 米外。运行一个双样本 t 检验检验这两组单位租金的均值差是否显著。由于样本量可能不满足正态分布更稳妥的是使用mannwhitneyu非参数检验。这段分析虽然代码不多但能展示你的统计素养。from scipy.stats import mannwhitneyu group_near df_clean[df_clean[distance_subway] 500][unit_price] group_far df_clean[df_clean[distance_subway] 500][unit_price] stat, p mannwhitneyu(group_near, group_far, alternativegreater) print(fp-value: {p:.4f}) # 结论p 0.05 时拒绝原假设说明地铁沿线租金显著更高注意这里需要剔除极端值否则结论可能被特殊豪宅影响。报告里要写清楚样本量、分组方式和检验方法截图留底。6.2 把静态图升级成可视化大屏pyecharts的仪表盘布局“可视化大屏”是近年来各类作业的常见加分项。你可以用 pyecharts 的Page或者Tab把多张图组合成一个仪表盘里面放上地图、柱状图、散点图和一侧的数据表卡。由于页面自动轮播你的展示效果会明显比纯 Matplotlib 好。具体做法是先定义每张图然后Page(layoutPage.SimplePageLayout)把图表对象按顺序放入。这里不需要复杂的 Web 服务只需要生成一个静态 HTML 文件。from pyecharts.charts import Bar, Page from pyecharts import options as opts bar Bar().add_xaxis(list(district_stats.index)).add_yaxis(中位数租金, district_stats[rent_median].tolist()) page Page(layoutPage.SimplePageLayout) page.add( map_chart, bar, scatter_chart ) page.render(dashboard.html)但要提醒你可视化大屏的目的不是视觉轰炸而是让每个图表都能支撑一段结论。如果你插入的图与报告论点无关评审问起来反而减分。所以我的习惯是只放四个图地图、区域中位数柱状图、面积与租金回归图、户型占比饼图。这四个已经能完整串起整个故事。6.3 交作业前的自检清单数据、代码、报告三位一体最后在提交前我用一张自检清单重新核对。第一爬虫代码能不能在只改变城市前缀的情况下复用如果不能说明城市参数写死了。第二数据 CSV 里是否有乱码或者重复行我一般会用pd.read_csv后去重存储时加encodingutf-8-sig以便 Excel 打开不乱码。第三代码运行时间是否在五分钟内如果地理编码导致运行时间过长我会把经纬度缓存文件也提交并在 README 里说明“最快复现方式”。第四文档报告里有没有记录反爬策略和你的应对方案这是“高分大作业”的关键因为评委想看的是你如何分析和解决问题而不是单纯的技术堆叠。我曾在报告里用一整节写“反爬应对与合规说明”很多评委对此印象深刻。这一套流程做完你的作业就已经不是普通的小爬虫而是一个具备完整数据工程思维的案例。回看整个过程最大的收获不是爬了多少条数据而是知道每一步都可能翻车但你能通过观察状态码、分析 HTML、调整参数来站起来。这个处理问题的习惯比任何源码都值钱。希望这篇笔记能帮你少走几个月弯路祝你的大作业顺利拿高分。本文还有配套的精品资源点击获取