
简介这份资源面向大数据入门与进阶学习者围绕杭州租房信息构建了一条从爬虫采集、数据清洗到分析可视化的完整实践链路帮助读者理解真实场景下数据获取与价值挖掘的全过程。压缩包共26个文件、约7.55MB以xlsx数据表、py爬虫与预处理脚本、xml配置、ipynb分析笔记为主另含csv样本、txt词表及png词云图等覆盖采集、清洗、建模到图表输出的各环节。目前已有1449人学习下载适合作为课程设计或自学练手项目。读者可参考其中的爬虫脚本、Pandas清洗流程与可视化代码掌握缺失值处理、去重、租金分布与面积关系等分析思路并借助词云与统计图表直观呈现结论快速搭建属于自己的数据分析流程。1. 杭州租房数据从哪来一个爬虫项目的真实起点做租房数据分析的人十有八九第一步就卡在数据源上。市面上现成的租房数据集要么字段残缺要么时间久远要么价格明显失真。我去年帮一个做长租公寓运营的朋友看他们手里的数据发现同一套房源在三个渠道的挂牌价能差出八百块这种数据拿来做决策就是自欺欺人。所以当有人问我「杭州租房信息爬虫数据清洗数据分析」这条路能不能走通时我的回答一直是能走但爬虫只是入口清洗才是分水岭分析只是最后一步的自然结果。这个方向适合两类人一类是想练手数据全流程的开发者另一类是手里有区域房源运营需求、需要自己拉数据做判断的从业者。整条链路的核心不是爬得多快而是字段设计得对不对、清洗规则扛不扛得住脏数据。2. 爬虫方案选型与字段设计别等跑完才后悔2.1 静态页面和动态接口的判断方法杭州租房信息主要分布在几类平台上页面结构差异很大。动手之前先花十分钟判断目标页面是服务端渲染还是前端异步加载这一步决定了你后面是写解析还是抓接口。判断方法很直接在浏览器里打开目标列表页查看页面源代码搜索房源标题里的关键词。如果源代码里能搜到说明是静态渲染用常规请求加解析库就能拿到如果搜不到说明数据是异步加载的需要打开开发者工具的网络面板筛选 XHR 或 Fetch 请求找到返回房源列表的那个接口。我一般会先抓一个列表页的接口看看返回结构。常见做法是接口返回 JSON里面分页字段、房源数组、每套房源的标题、价格、区域、面积都在。这种接口比解析 HTML 稳定得多因为页面样式改版频繁接口字段反而相对固定。但要注意接口通常带签名参数或时间戳需要把请求头里的关键字段一并带上否则会返回空数据或者被限流。2.2 房源字段表哪些必须抓哪些可以后补字段设计是爬虫项目里最容易被低估的环节。很多人跑完爬虫才发现少抓了区域字段回头补爬成本极高。下面这张表是我做租房数据项目时固定使用的字段清单分成核心字段和扩展字段两类。字段名类型是否必抓说明title字符串是房源标题含小区名和户型信息price数值是月租金单位元area数值是建筑面积单位平方米region字符串是所在行政区如西湖区、拱墅区community字符串是小区名称layout字符串是户型如两室一厅floor字符串否楼层信息可后补orientation字符串否朝向publish_time日期否发布时间用于时效过滤source_url字符串是原始链接用于去重和回溯核心字段里 region 和 community 必须抓全因为后面做区域均价分析全靠这两个字段。price 和 area 要确保是数值类型抓下来如果是「3500元/月」这种带单位的字符串清洗阶段要处理掉。source_url 看起来不起眼但去重和排查数据问题时它是唯一的后悔药。2.3 最小可运行爬虫请求、解析、落盘三步走下面这段代码是一个最小可运行的爬虫骨架以静态列表页为例。实际项目中我会把请求间隔、重试、日志都加上但骨架先跑通再补细节。import requests from bs4 import BeautifulSoup import csv import time import random # 请求头模拟浏览器缺少 User-Agent 很多站点直接返回 403 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_page(url): 请求单页失败重试两次 for attempt in range(3): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: return resp.text except requests.RequestException as e: print(f请求失败 {url}第 {attempt1} 次{e}) time.sleep(random.uniform(2, 5)) # 随机间隔降低被封风险 return None def parse_list(html): 解析列表页返回房源字典列表 soup BeautifulSoup(html, html.parser) items [] for card in soup.select(.house-list .house-item): item { title: card.select_one(.title).get_text(stripTrue), price: card.select_one(.price).get_text(stripTrue), area: card.select_one(.area).get_text(stripTrue), region: card.select_one(.region).get_text(stripTrue), community: card.select_one(.community).get_text(stripTrue), layout: card.select_one(.layout).get_text(stripTrue), source_url: card.select_one(a)[href], } items.append(item) return items def save_csv(rows, filenamehangzhou_rent.csv): 追加写入 CSV字段顺序固定 with open(filename, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesrows[0].keys()) if f.tell() 0: writer.writeheader() writer.writerows(rows) if __name__ __main__: base_url https://example.com/rent/list?page{} for page in range(1, 51): # 先跑 50 页试水 html fetch_page(base_url.format(page)) if not html: continue rows parse_list(html) if rows: save_csv(rows) print(f第 {page} 页完成累计 {len(rows)} 条) time.sleep(random.uniform(3, 7))这段代码的逻辑很直白fetch_page 负责请求并做重试parse_list 用 CSS 选择器提取字段save_csv 追加写入。几个关键参数需要根据实际情况调整。timeout 设 10 秒是经验值太短容易误判超时太长会拖慢整体进度。请求间隔用 random.uniform 生成 3 到 7 秒的随机值比固定间隔更难被识别。CSV 编码用 utf-8-sig 是为了 Excel 打开不乱码这个坑我踩过不止一次。选择器部分需要根据目标页面的实际结构改。我一般会先在浏览器控制台用 document.querySelector 验证选择器能不能选中确认后再写进代码。如果页面是异步加载的就把 fetch_page 换成请求接口parse_list 换成解析 JSON整体结构不变。3. 数据清洗从脏数据到可用表的六个动作3.1 去重、缺失值、异常值清洗的三道关爬下来的原始数据几乎不可能直接用来分析。我跑完 50 页大概能拿到两千多条记录里面重复房源、价格为空、面积写成「暂无」的情况比比皆是。清洗的第一步是去重按 source_url 或者 title 加 community 的组合去重。第二步处理缺失值price 和 area 缺失的记录直接丢弃region 缺失的尝试从 title 里提取。第三步处理异常值比如价格低于 500 或者高于 50000 的记录大概率是录入错误或者特殊房源需要单独标记。import pandas as pd import re df pd.read_csv(hangzhou_rent.csv) # 去重同一 source_url 只保留第一条 df df.drop_duplicates(subset[source_url], keepfirst) # 价格清洗去掉「元/月」等单位转数值 def clean_price(val): if pd.isna(val): return None match re.search(r(\d), str(val)) return int(match.group(1)) if match else None df[price] df[price].apply(clean_price) # 面积清洗去掉「㎡」「平米」等后缀 def clean_area(val): if pd.isna(val): return None match re.search(r(\d\.?\d*), str(val)) return float(match.group(1)) if match else None df[area] df[area].apply(clean_area) # 丢弃核心字段缺失的记录 df df.dropna(subset[price, area, region]) # 异常值标记价格低于 500 或高于 50000 df[price_outlier] (df[price] 500) | (df[price] 50000) # 区域字段标准化去掉「区」字后缀统一格式 df[region] df[region].str.replace(区, , regexFalse) df.to_csv(hangzhou_rent_clean.csv, indexFalse, encodingutf-8-sig) print(f清洗后剩余 {len(df)} 条异常价格 {df[price_outlier].sum()} 条)这段清洗脚本的核心是三个函数加两个标记。clean_price 和 clean_area 用正则提取数字处理带单位的字符串。drop_duplicates 按 source_url 去重因为同一房源可能被多次抓取。异常值不直接删除而是打标记是因为有些高价房源可能是真实存在的整租豪宅直接删掉会丢失信息。区域字段去掉「区」字是为了后面分组统计时格式统一这种细节不做的话groupby 的时候「西湖区」和「西湖」会被当成两个区域。3.2 区域和户型字段的标准化处理区域字段的标准化比想象中麻烦。原始数据里同一个区域可能有多种写法比如「西湖区」「西湖」「西湖板块」甚至有些记录把商圈写进了区域字段。我的处理方式是先建一张映射表把常见写法归一到标准区域名映射不到的记录单独输出人工检查。户型字段同理「两室一厅」「2室1厅」「两房一厅」需要统一成「2室1厅」这种格式。做法是用正则提取室和厅的数字重新拼接。这一步做完后面按户型分组统计均价才有意义。# 区域映射表左边是原始写法右边是标准名 region_map { 西湖: 西湖, 西湖区: 西湖, 西湖板块: 西湖, 拱墅: 拱墅, 拱墅区: 拱墅, 上城: 上城, 上城区: 上城, 滨江: 滨江, 滨江区: 滨江, 余杭: 余杭, 余杭区: 余杭, } df[region_std] df[region].map(region_map) # 未映射到的区域单独输出 unmapped df[df[region_std].isna()][region].unique() print(未映射区域, unmapped) # 户型标准化提取室和厅的数字 def clean_layout(val): if pd.isna(val): return None match re.search(r(\d)\s*[室房]\s*(\d)\s*厅, str(val)) if match: return f{match.group(1)}室{match.group(2)}厅 return None df[layout_std] df[layout].apply(clean_layout)映射表的方式看起来笨但比写复杂的规则可靠。未映射的区域输出后手动补几条映射规则再跑一遍就行。户型标准化用正则匹配「数字加室或房加数字加厅」的模式覆盖大部分写法。匹配不到的记录标记为 None分析时排除即可。3.3 用 pandas 做分组聚合区域均价和户型分布清洗完的数据要能回答具体问题才有价值。最常见的两个分析维度是区域均价和户型分布。区域均价用 groupby 加 mean 就能算出来但要注意样本量太小的区域均价没有参考意义我一般会过滤掉样本少于 30 条的区域。# 区域均价过滤样本量不足的区域 region_stats df.groupby(region_std).agg( avg_price(price, mean), median_price(price, median), count(price, count), avg_area(area, mean), ).reset_index() region_stats region_stats[region_stats[count] 30] region_stats region_stats.sort_values(avg_price, ascendingFalse) print(region_stats) # 户型分布按标准化后的户型统计 layout_stats df.groupby(layout_std).agg( avg_price(price, mean), count(price, count), ).reset_index().sort_values(count, ascendingFalse) print(layout_stats.head(10))聚合的时候同时算均值和中位数是因为租金分布通常右偏少数高价房源会把均值拉高中位数更能反映典型水平。样本量过滤阈值设 30 是统计上的经验值低于这个数均值波动太大。户型分布按数量排序能快速看出市场上哪种户型供应最多。4. 避坑与排查爬虫和清洗阶段的血泪经验4.1 请求被限流的三种表现和应对现象爬了十几页之后突然全部返回 403 或者空数据。原因请求频率过高触发了站点的限流机制。解决把请求间隔从固定值改成随机区间同时检查请求头是否缺少 Referer 字段。我一般会在请求头里补上 Referer 指向列表页这样看起来更像正常浏览行为。如果还是被限就降低并发或者换时间段再跑。4.2 价格字段混入「面议」导致清洗报错现象clean_price 函数遇到「面议」返回 None后续 dropna 把整条记录删掉。原因部分房源不标价格用「面议」代替。解决这类记录单独存一张表不要直接丢弃因为「面议」本身可能意味着房源特殊。我在清洗脚本里加了一个分支把价格字段无法解析的记录输出到 hangzhou_rent_unknown_price.csv后续人工判断。4.3 区域字段混入商圈名导致分组错乱现象groupby 之后出现「西湖」「西湖文三路」「文三路」三个分组实际都是同一个区域。原因爬取时区域选择器抓到了包含商圈的文本。解决在清洗阶段用映射表归一映射不到的记录输出后手动补充规则。这个问题的根源在爬虫阶段如果选择器能精确到区域字段清洗会省很多事。4.4 CSV 编码问题导致中文乱码现象用 Excel 打开清洗后的 CSV中文全部变成乱码。原因写入时用了 utf-8 编码Excel 默认按 GBK 解析。解决写入时统一用 utf-8-sig这个编码会在文件头加 BOM 标记Excel 能正确识别。这个坑几乎每个做中文数据处理的人都踩过记住就行。4.5 去重后数据量骤减的排查思路现象两千条原始数据去重后只剩八百条。原因可能是 source_url 字段抓取时带了分页参数或者时间戳导致同一房源每次抓取的 URL 都不同。解决去重前先检查 source_url 的格式如果带动态参数需要先截取到房源 ID 部分再去重。我一般会用正则提取 URL 里的数字 ID 作为去重键比直接用完整 URL 可靠。5. 从清洗后的数据到可复用的分析结论5.1 用透视表快速定位价格洼地清洗完的数据最有价值的用法是做区域和户型的交叉分析。pandas 的 pivot_table 能直接生成一张区域乘户型的均价表一眼就能看出哪个区域哪种户型性价比最高。# 区域和户型的交叉均价表 pivot df.pivot_table( valuesprice, indexregion_std, columnslayout_std, aggfuncmedian, ) # 只保留样本量足够的户型列 valid_layouts [1室1厅, 2室1厅, 3室1厅] pivot pivot[[c for c in valid_layouts if c in pivot.columns]] print(pivot.round(0))透视表用中位数而不是均值避免极端值干扰。列只保留常见户型冷门户型样本太少没有统计意义。这张表出来之后哪个区域的两室一厅中位租金最低一目了然比看一堆数字直观得多。5.2 单价指标比总价更能反映真实水平总价受面积影响很大两室一厅的总价高可能只是因为面积大。用单价元每平方米每月做对比更公平。计算方式很简单price 除以 area 就行但要先过滤掉面积异常的记录。# 计算单价过滤面积过小或过大的记录 df_valid df[(df[area] 20) (df[area] 300)].copy() df_valid[unit_price] df_valid[price] / df_valid[area] # 按区域统计单价中位数 unit_stats df_valid.groupby(region_std)[unit_price].median().sort_values() print(unit_stats.round(2))面积过滤范围设 20 到 300 平方米低于 20 可能是车位或者储藏室高于 300 可能是别墅或者录入错误。单价中位数排序后能看出哪些区域的单位面积租金最便宜这对预算有限但想住大一点的人来说是更实用的参考。5.3 数据时效性维护定期增量爬取和清洗租房数据时效性很强一个月前的价格参考价值就大打折扣。我的做法是每周跑一次增量爬取只抓最新发布的房源然后和已有数据合并去重。增量爬取的关键是记录上次爬取的时间点只请求发布时间在时间点之后的房源。清洗脚本不用改直接对新数据跑一遍再合并就行。# 增量合并新数据追加后按 source_url 去重保留最新 old pd.read_csv(hangzhou_rent_clean.csv) new pd.read_csv(hangzhou_rent_new.csv) merged pd.concat([old, new], ignore_indexTrue) merged merged.drop_duplicates(subset[source_url], keeplast) merged.to_csv(hangzhou_rent_clean.csv, indexFalse, encodingutf-8-sig)合并时 keeplast 保留最新抓取的记录因为价格可能已经变动。这个流程跑顺之后每周只需要几分钟就能完成数据更新。做这个方向最大的体会是爬虫代码写一次就够清洗规则要反复迭代。我最早那版清洗脚本只处理了价格和面积后来发现区域字段的脏数据才是分析结果失真的主因。现在我的习惯是每跑完一轮数据先随机抽二十条人工核对确认字段没问题再进入分析。这个习惯帮我省了很多返工的时间。希望帮到你。本文还有配套的精品资源点击获取