尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python爬虫实战:链家二手房数据采集到分析全流程

Python爬虫实战:链家二手房数据采集到分析全流程 简介这是一份基于Python爬虫与Jupyter的链家二手房数据分析完整项目面向正在准备Python课程大作业、毕业设计或数据实战训练的开发者。项目覆盖链家二手房信息抓取、数据清洗、特征分析与可视化全流程源码经本地编译可正常运行综合评审分达95分以上。压缩包共20个文件主要包含二手房抓取脚本、数据分析Notebook、上海市二手房数据CSV、多张可视化图表、期末报告Word及答辩PPT并附抓取说明文本整体约20.91MB目录结构便于按流程查阅。目前已有150人学习下载适合作为课程设计与大作业模板。借助词云图、聚类图、单价热力图等成果可学习价格、面积、户型与区域分布间的关联洞察配套的文档报告与PPT则能帮助快速梳理项目方法、完善报告撰写并准备演示答辩实用性与完整度较高值得参考复用。1. 从链家页面到分析结论一条值得走通的数据链路每个做过数据分析的人应该都有过这种经历想拿真实业务数据练手但公开数据集要么太旧要么字段被清洗得没脾气。链家二手房页面反而是一座还算稳定的数据源——房源标题、小区、户型、面积、朝向、装修、楼层、总价、单价这些字段都在 DOM 里规规矩矩摆着而且城市和区域的粒度够细足够支撑多种维度的分析。问题在于怎么把页面数据变成一份能反复查询、能出结论的本地数据集。这个标题里的“源码文档报告”透露出一个信号它不只是一个爬虫脚本而是一套从请求到入库再到分析产出的完整流程。用 Python 爬虫解决数据从哪来的问题用 Jupyter Notebook 解决数据怎么分析、结论怎么陈述的问题。对初学者来说这是一条能看到全貌的进阶路径对工作几年的工程师来说这套流程也值得重新审视——很多项目其实不缺数据缺的是把数据采集、清洗、分析串成一条可控流水线的方法。以下按我平时做这类项目的方式把数据链路完整铺开。2. 爬虫与反爬的边界请求策略与解析方案在写请求代码之前得先想清楚面对的是什么类型的页面。链家二手房列表页和数据详情页结构不同列表页用于建立房源索引详情页则用来补全更多字段但绝大多数分析场景其实只用到列表页就能拿到的信息就够了——总价、单价、面积、户型、朝向、楼层、装修、小区名、城区这些已经足够支撑价格分布、面积与总价关系、区域对比等核心分析。因此我的建议是先用列表页把主数据表建好只有当你需要房屋用途、产权年限这类深度字段时才去请求详情页。2.1 列表页 URL 规律与分页参数链家二手房列表页的 URL 模式比较规律https://{city}.lianjia.com/ershoufang/pg{n}/其中{city}是城市缩写pg{n}是页码。以北京为例第二页就是https://bj.lianjia.com/ershoufang/pg2/。这种确定性意味着你不必通过模拟点击来翻页直接拼 URL 即可。需要注意一个隐藏条件链家网页版默认一页显示 30 条房源但只有前 100 页可访问。也就是说单区域超过 3000 套房源时靠翻页是抓不全的。遇到这种体量北京上海这类城市必须带上区域条件URL 变成https://bj.lianjia.com/ershoufang/dongcheng/pg2/以行政区为单位分片采集才能把数据覆盖完整。2.2 requests 请求头设置与频率控制请求头是第一个反爬门槛。链家对缺少浏览器特征标识的请求大概率返回 302 跳转或直接拒绝连接。我一般会在 Session 对象上统一设置 UA、Accept、Referer 三个字段尽量模拟真实浏览器的 HTTP 上下文。import requests from fake_useragent import UserAgent session requests.Session() ua UserAgent() session.headers.update({ User-Agent: ua.random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) session.headers[Referer] https://www.lianjia.com/用fake_useragent每次请求随机更换 UA比固定写死一个 UA 的鲁棒性高很多。参数说明Accept-Language按中文优先排列因为链家会根据语言偏好返回不同编码的页面缺失时偶发出现乱码问题。Referer设置为站点根域避免因来源不明被服务端拒绝。频率控制方面每次请求间隔time.sleep(random.uniform(1, 2))是底线低于这个频率很容易触发验证码。提示如果请求返回的页面状态码是 200 但内容里没有房源列表大概率是返回了验证码页或空壳页。此时检查响应内容里是否包含verify或captcha特征串即可确认。2.3 页面解析BeautifulSoup 还是 parsel列表页的房源数据落在li.clear结构的 DOM 节点里每个房源卡片包含title、address、totalPrice、unitPrice等元素字段层级非常规律。parsel 的 XPath 语法会更紧凑我一般用 parsel 做这类结构化页面的提取因为它的选择器对缺失字段直接返回空列表不像 BeautifulSoup 那样需要额外判断。import parsel def parse_list_page(html: str): sel parsel.Selector(texthtml) items [] for li in sel.xpath(//li[contains(class, clear)]): title li.xpath(.//div[classtitle]/a/text()).get() total_price li.xpath(.//div[classtotalPrice]/span/text()).get() unit_price li.xpath(.//div[classunitPrice]/span/text()).get() position_info li.xpath(.//div[classpositionInfo]//text()).getall() house_info li.xpath(.//div[classhouseInfo]//text()).getall() items.append({ title: title, total_price: total_price, unit_price: unit_price, position_info: .join([t.strip() for t in position_info if t.strip()]), house_info: .join([t.strip() for t in house_info if t.strip()]), }) return items解析逻辑的关键点链家列表页每个房源卡片都带有clear类名但这不足以唯一定位房源节点因为页面底部推荐区也有类似结构。更稳妥的做法是限制在ul.sellListContent容器内查找用//ul[classsellListContent]/li限定范围避免混入推荐位数据。positionInfo字段里同时包含小区名和地理位置抓取后建议以空格分隔整段保存后续在清洗阶段按需拆列。3. 并发采集与异常恢复把脚本从能跑到跑得稳单线程串行爬取 100 个页面大约需要 200 秒以上这个速度在开发调试时可以接受但全量采集时就显得低效了。引入并发是必要的一步但并发会同步放大被封风险所以必须同时引入重试、退避和失败续抓机制。爬虫并发设计的好坏往往直接决定一个采集任务能不能完整跑完。3.1 使用 ThreadPoolExecutor 控制并发与退避我一般用ThreadPoolExecutor做轻量级并发最大工作线程控制在 4 到 8 之间这样既能显著提升采集速度又不至于瞬间产生大量请求。请求失败后采用指数退避重试连续失败超过阈值就跳过该页并把失败页码记录到错误日志中等待后续补抓。from concurrent.futures import ThreadPoolExecutor, as_completed import time import random def fetch_page(url: str, session: requests.Session, retries: int 3): for attempt in range(retries): try: resp session.get(url, timeout8) if resp.status_code 200: return url, resp.text elif resp.status_code 403: time.sleep(10 * (attempt 1)) except requests.RequestException: pass time.sleep(random.uniform(1, 3)) return url, None failed_urls [] urls [fhttps://bj.lianjia.com/ershoufang/dongcheng/pg{n}/ for n in range(1, 101)] with ThreadPoolExecutor(max_workers4) as executor: futures [executor.submit(fetch_page, url, session) for url in urls] for future in as_completed(futures): url, html future.result() if html is None: failed_urls.append(url)并发参数设计说明max_workers4是综合效率和风险后的经验值太高容易触发 IP 级别的访问限制。timeout8用于兜底网络异常防止某个请求长期占住线程。返回(url, html)的元组结构很关键——并发场景下只有携带 URL 才能定位解析失败的数据来源。403 状态码代表已被服务端识别为爬虫此时必须等待更长时间10 秒起跳逐次翻倍。3.2 请求失败后的数据补抓方案failed_urls里累积的页面单独做第二轮补抓。补抓时不再并发而是串行低俗重试并且使用与首轮不同的 Cookie 和 UA以尽最大可能恢复抓取。如果三轮补抓后仍失败这个页面就丢弃并在日志中标注原因——不必追求百分之百完整分析场景下 95% 以上的覆盖率已经完全够用。for url in failed_urls: session.headers[User-Agent] ua.random _, html fetch_page(url, session, retries5) if html: items parse_list_page(html) # append to storage time.sleep(random.uniform(3, 5))这里的补抓策略是换 UA、降频率、加次数。核心思路是让补抓流量的特征与首轮不同而不是简单地重复首轮请求。还可以把错误页面保存到本地文件用于事后分析具体失败原因——是超时、403 还是解析异常不同的失败类型对应不同的解决方案。3.3 数据落库CSV 还是 SQLite采集到的数据量通常在几千到几万行这个量级下 CSV 和 SQLite 都够用。我的建议是采集阶段用 CSV 做临时存储因为写入简单、编码可控进入分析阶段后再用 Pandas 一次性加载并清洗清洗后可导出为 Parquet 或直接写入 SQLite 供 Jupyter 反复查询。title,total_price,unit_price,position_info,house_info 南北通透两居室,560,56000,小区A 朝阳门,2室1厅 80平米 南 北 精装CSV 存在的已知问题有三个一是house_info字段内部包含多个空格分隔的信息片断split 时要小心二是价格字段是字符串需要转成整数三是坐标或小区名中可能包含逗号写入 CSV 时容易错列。处理方式是在写入前统一将字段中的逗号替换为中文逗号或干脆使用 Pandas 的to_csv方法来自动处理引号转义。4. 数据清洗与 Jupyter 分析从脏数据到业务结论数据抓下来了真正的分析工作从 Jupyter Notebook 里才开始。之所以选择 Jupyter是因为二手房分析不是一次查询就结束的事——你会在清洗、透视、可视化之间来回往返有太多临时的、探索性的处理步骤。Notebook 的单元格结构天然适合这种“假设—验证—修正”的循环节奏而且中间的图表和结论可以直接沉淀成文档报告不需要额外整理。4.1 价格字段清洗与特征工程原始数据里总价和单价都是带单位的字符串比如560和56000元/平米直接进 DataFrame 类型就是 object。需要先做一轮类型转换和特征提取把字符串中的数值部分与单位剥离并生成新的派生特征。import pandas as pd df pd.read_csv(lianjia_dongcheng.csv) df[total_price] pd.to_numeric( df[total_price].str.replace(万, ), errorscoerce ) df[unit_price] pd.to_numeric( df[unit_price].str.replace(元/平米, ), errorscoerce ) house_info_split df[house_info].str.split( , expandTrue) df[bedrooms] house_info_split[0].str.extract(r(\d)).astype(float) df[area] pd.to_numeric( house_info_split[1].str.replace(平米, ), errorscoerce ) df[price_per_area] df[total_price] * 10000 / df[area]errorscoerce是一个值得展开的参数它会把无法转换的值置为 NaN而不是直接抛异常终止清洗流程。这在分析真实采集数据时几乎必然用到——因为链家页面里有的房源价格字段可能是空值或异常格式。house_info_split分别拆出户型、面积、朝向、装修、楼层但要注意部分房源缺少 5 个字段拆分后的列数会不足处理时用expandTrue配合reindex固定列数。4.2 区域房源对比分析透视表与聚合计算清洗后的数据可以做区域维度的对比了。使用pivot_table可以快速得到各区域的房源数量、平均总价、平均单价、中位数总价等关键指标。中位数比平均数更能反映“普通房源”的价格水平因为链家数据里高价房源对平均值拉动明显。region_stats df.pivot_table( indexregion, values[total_price, unit_price, area], aggfunc{ total_price: [count, median, mean], unit_price: median, area: median, }, ) region_stats.columns [_.join(col).strip() for col in region_stats.columns.values] region_stats region_stats.sort_values(total_price_median, ascendingFalse)聚合逻辑说明索引列region来自position_info字段的子集在清洗阶段需要把小区名和行政区拆成独立字段。aggfunc传入字典的方式允许不同列使用不同聚合函数count统计房源量median和mean并行列示便于发现均值与中位数差距大的区域——这类区域通常是高低价房源混杂明显的板块。4.3 价格与面积相关性的可视化验证数据验证阶段最常用的图是一张散点图加一条拟合线横轴面积、纵轴总价。它可以直观回答“面积越大单价越低”这个普遍直觉是否成立。链家数据的典型结论是总价与面积正相关但单价与面积呈负相关这反映了刚需小户型单价高、大户型单价相对低的市场规律。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 2, figsize(14, 5)) sns.scatterplot(datadf, xarea, ytotal_price, axaxes[0], alpha0.4) sns.regplot(datadf, xarea, ytotal_price, axaxes[0], scatterFalse, colorred) sns.scatterplot(datadf, xarea, yunit_price, axaxes[1], alpha0.4) sns.regplot(datadf, xarea, yunit_price, axaxes[1], scatterFalse, colorgreen) axes[0].set_title(面积-总价关系) axes[1].set_title(面积-单价关系) plt.tight_layout()这段代码有两个容易踩坑的地方。第一个是中文字体问题Matplotlib 默认字体不含中文字符不设置font.sans-serif画出来的图全是方框不同操作系统可用的中文字体不同Windows 下用SimHeimacOS 下用Arial Unicode MSLinux 服务器通常需要安装相应字体包才能正确显示。第二个是axes[0]和axes[1]的索引顺序subplots返回的数组按行列排列单行两列时索引 0 是左图、1 是右图。5. 把分析链路收拢成可复用的项目模板当爬虫和分析两段都验证通过后最后一步是把它们拼成一个可以被重新执行的工程。这里有一个很关键的思路爬虫和分析不应生活在同一个 Notebook 里。爬虫是批处理任务适合脚本化、命令行运行分析是探索性任务适合留在 Notebook 里迭代。两者之间用 CSV 或 SQLite 文件作为边界互不干扰。常见的做法是维护一个三层目录结构crawler/存放请求和解析代码data/存放采集结果和清洗后的中间数据analysis/存放 Jupyter Notebook 和导出的报告。这个结构前期看起来有点重但当你换一个城市重新采集时复用价值立刻就出来了——更换请求参数和解析规则分析代码完全不需要改动。链家的页面结构偶尔会调整这就要求爬虫脚本在正式执行前先做一次页面结构探测用少量请求验证 XPath 或 CSS 选择器是否仍然匹配。探测不通过时自动中断任务而不是盲目跑完整个采集流程然后拿到一堆空数据。这个验证逻辑虽然简单但在正式项目中往往是最能节省时间的一环。爬虫与数据分析结合的价值在于它能反复执行而不需要每次重新调试。把采集频率、字段定义、清洗逻辑和图表模板固化成可用模块后以后获取任何一套链家数据从触发到产出报告都只是修改配置参数和重新运行代码的事。本文还有配套的精品资源点击获取
返回列表