简介面向Python爬虫初学者的完整实践项目围绕北京房价信息采集与可视化展开覆盖requests发送请求、BeautifulSoup解析页面、正则提取数据、pandas清洗分析以及matplotlib绘图等核心环节。资源包共8个文件以3个Python脚本爬虫、解析、分析为主附带2个已清洗的房价Excel数据表、2个可视化HTML图表和1个城市URL配置整体仅483KB轻量易上手。目前已有4220人学习下载。通过运行脚本可直接复现从网页请求到最终图表输出的完整流程同时代码中融入了异常处理及代理池、反爬策略的应对思路适合希望以真实项目快速打通爬虫全链路的数据爱好者。1. 项目思路与整体设计1.1 爬取房价信息到底解决什么问题我一直觉得买房子或者租房这件事最大的信息差不在房源本身而在价格分布。同一个城市、同一个板块挂牌价能差出百分之二三十这中间的水分到底在哪没有一个量化的认知只能被中介带着节奏走。爬取房价信息本质上是把零散的挂牌数据汇总成一个结构化的数据集然后从里面看出门道。比如某片区近期的均价、不同户型的价差、楼层朝向对价格的影响、挂牌价随时间的波动趋势。这些数据如果是手动去采集一套房子一套房子地翻工作量大到不现实但用脚本自动跑半小时就能把整个城市的挂牌信息拉下来。这个项目最典型的应用场景有三个第一自住刚需的人了解目标片区的真实价格区间不被单套房源带偏第二做市场研究的人用数据辅助判断板块热度第三想练手爬虫和数据清洗的开发者这个项目规模适中既有页面解析、又有反爬策略、还有数据处理和可视化是一个非常完整的综合性练习。1.2 技术选型背后的思考说实话爬房价信息的技术路线不算复杂核心就是三步请求页面、解析HTML、结构化存储。但每一步都有值得琢磨的地方。请求库方面我选择requests而不是scrapy。原因很简单这个项目的数据量在几千到几万条的级别并发要求不高requests配合简单的重试机制就足够了scrapy的框架复杂度在这个场景下反而显得笨重。解析层面BeautifulSoup配合lxml解析器是我最常用的组合它的CSS选择器语法足够直观用chrome开发者工具定位元素结构也很顺手。存储层面直接上pandas把数据存成CSV和DataFrame后期做统计分析几乎是无缝衔接。整个项目我是按模块来组织的爬虫脚本、清洗脚本、分析脚本分开写这样后期改任何一个环节都不会牵连到其他部分。而且这个结构可以套用到任意同类项目上换个数据源改改选择器和字段映射就能复用到其他垂直信息站的采集。2. 环境准备与工具选型2.1 Python环境安装与库依赖这个话题虽然基础但确实值得单独说一说。二线城市的房价数据站点页面结构老旧技术栈的占了相当比例Python 3.8以上就完全兼容了不建议用太新的版本部分解析库对Python 3.12以上版本的支持还不算稳定。环境安装上有两条路。一条是直接从python官网下载安装包装的时候记得勾选Add Python to PATH然后打开命令行输入python --version验证是否安装成功。另一条是我比较推荐的方式安装Anaconda。Anaconda本身自带了一个Python环境还预装了两百多个常用库最方便的是它的base环境里直接含pandas、numpy、matplotlib这些我们马上要用到的东西省了逐个装的功夫。Windows下安装Anaconda基本就是一路Next但需要注意安装到最后一个界面时不要勾选Register Anaconda as my default Python否则后期在多环境管理时会产生路径冲突。装完环境之后还需要手动装几个库。先把命令行切到工作目录然后执行pip install requests beautifulsoup4 lxml pandas matplotlib如果因为网络原因下载速度很慢可以临时换成国内镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests beautifulsoup4 lxml pandas matplotlib2.2 代码编辑器的选择编辑器这块VSCode是我的主力。它免费、插件生态完善对Python的支持在装完Python扩展之后体验相当流畅。配置方面只需要在设置里指定Python解释器路径如果是Anaconda环境直接关联到安装目录下的python.exe即可。有一个实用的配置建议在VSCode的settings.json里加上{ python.linting.enabled: true, python.linting.pylintEnabled: true, editor.mouseWheelZoom: true, files.encoding: utf8 }第一项开启代码检查写代码过程中就能实时发现语法问题这对爬虫脚本调试特别有帮助。编码设置为utf8也很关键这个项目要处理大量中文数据如果文件编码不对读出来全是乱码后面所有的分析都无从谈起。3. 爬虫核心实现与细节3.1 目标站点分析与请求策略房价信息的公开数据源不少普通房产中介平台的挂牌公开信息是最常见的。这类站点的页面结构相对规整新房和二手房分开列表页每个列表页展示一定数量的房源卡片下面有分页。以链家这类典型平台为例它的URL规律很清晰base_url https://{city}.lianjia.com/ershoufang/pg{page}/其中city是城市拼音page是页码。这意味着只要循环改变page参数就能遍历全城的二手房挂牌列表。但实际请求过程中服务器会对高频请求做限流。一个完全没有伪装的requests请求头服务器端很容易识别为脚本并拒绝访问。我的做法是设置一个完整真实的浏览器请求头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,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://{city}.lianjia.com/ershoufang/ }注意User-Agent的版本号要跟真实浏览器版本保持一致不要用一个过时的UA。虽然部分平台有cookie校验首轮请求可以先用requests.Session()自动管理cookie有些情况下不添加cookie也能访问到正常数据。3.2 页面解析与数据字段提取拿到页面HTML之后解析的重点是找到房源卡片的位置。用Chrome打开一个列表页按F12进入开发者工具选择元素面板然后用左上角的箭头工具点中一张房源卡片就能看到DOM结构。这类平台的列表页通常长这样外层是ul classsellListContent每个房源卡片是li classclear LOGCLICKDATA。卡片内部包含标题、小区名称、位置、面积、朝向、装修、挂牌价和单价。用BeautifulSoup提取的代码如下from bs4 import BeautifulSoup def parse_list_page(html): soup BeautifulSoup(html, lxml) house_list [] items soup.select(ul.sellListContent li) for item in items: title item.select_one(.title a) if title is None: continue house_name title.get_text(stripTrue) house_url title.get(href, ) # 位置信息 position_info item.select_one(.positionInfo) if position_info: pos_text position_info.get_text(stripTrue) else: pos_text # 总价和单价 total_price item.select_one(.totalPrice span) unit_price item.select_one(.unitPrice) house_list.append({ title: house_name, url: house_url, position: pos_text, total_price: total_price.get_text(stripTrue) if total_price else None, unit_price: unit_price.get_text(stripTrue) if unit_price else None, }) return house_list这里有个细节值得说道一定不要用find_all一层一层去遍历直接用CSS选择器效率更高而且代码更简洁。定位元素时优先看class属性如果目标控件的class名称里含有动态生成的随机字符串就退回用父级容器的class配合层级选择器。3.3 请求频率控制和异常处理初学爬虫的人最容易犯的错误就是循环不加延迟脚本像机关枪一样连发请求结果就是触发反爬被封IP。我的经验是每次请求之间随机sleep 1到3秒毕竟是一个长期运行的任务稳定性优先于速度。import time import random def fetch_page(session, url, retries3): for attempt in range(retries): try: resp session.get(url, headersheaders, timeout10) resp.raise_for_status() # 随机延迟避免触发反爬 time.sleep(random.uniform(1, 3)) return resp.text except Exception as e: print(f第{attempt1}次请求失败: {e}) time.sleep(5) return None这个重试机制我加了一个3次的上限不是越多越好。如果连续3次都被拒说明大概率是IP被限制了再多的重试只会浪费时间。这时候最合理的做法是停一段时间再继续或者换个思路缩小采集范围。4. 数据处理与房价分析4.1 数据清洗的知其所以然爬下来的原始数据是不能直接用的。比如总价字段页面上显示的是850万但pandas要算平均值必须先把它转成纯数字。我的清洗思路是先统一字段格式再处理缺失值最后做类型转换。import pandas as pd df pd.read_csv(house_data.csv) # 去掉万字并转为浮点数 df[total_price] df[total_price].str.replace(万, ).astype(float) # 单价可能是61875元/平这种格式 df[unit_price] df[unit_price].str.replace(元/平, ).str.replace(,, ).astype(float)截止到这步数据就开始变得可分析了。但清洗过程中还会遇到很多特殊值比如面积字段可能出现暂无数据朝向可能是南北、东南、北等等。对于有明确业务含义的字段我的建议是总价、单价这类数值型字段出现异常值比如总价为0直接剔除朝向这种分类字段如果缺失比例小于5%可以填充为未知如果缺失比例高就得考虑是不是页面解析失败了地址和小区名称出现重复或拼写不一致用字符串的lstrip/rstrip处理空白字符再用简单的模糊匹配合并4.2 核心指标的计算逻辑清洗完之后就可以计算一些有意义的统计口径了。最基础的是片区均价# 按片区分组计算均值和样本量 district_stats df.groupby(district)[unit_price].agg([mean, count]) district_stats.columns [平均单价, 挂牌数量] district_stats district_stats.sort_values(平均单价, ascendingFalse)这里有两个容易踩的坑。第一mean值非常容易被个别天价楼盘带偏比如某个片区有一套总价过亿的豪宅会把该片区的均价拉高一截。为了更稳健我会同时计算中位数。district_stats[中位单价] df.groupby(district)[unit_price].median()中位数对极端值不敏感能更真实地反映片区的主流价格水平。第二挂牌数量少于5套的片区统计上没有代表性直接过滤掉。这个项目的核心价值在于揭示数据背后的规律平均值和中位数互为印证才能得出真正有用的结论。4.3 可视化呈现把数据变成判断依据数据算出来是一张表格但人对表格不敏感对图表敏感。用matplotlib画几个图整个市场的状况就一目了然了。首先是片区均价的横向柱状图import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] # 解决中文乱码 matplotlib.rcParams[axes.unicode_minus] False plt.figure(figsize(12, 8)) district_stats.head(15).plot(kindbarh, y平均单价, legendFalse) plt.title(各片区二手房平均挂牌单价) plt.xlabel(单价元/平米) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(district_price.png, dpi150)中文字体显示是个经典坑matplotlib默认字体不包含中文不设置的话图上的中文全是方块。在Windows上设SimHei黑体就可以解决Linux上可以换成文泉驿或者Noto Sans CJK。其次是价格分布的直方图用来看数据覆盖的区间是否合理plt.hist(df[unit_price], bins50, edgecolorwhite) plt.title(房源单价分布) plt.xlabel(单价元/平米) plt.ylabel(房源数量) plt.show()这一步其实是在做数据质量的初步验证。如果直方图显示价格分布严重右偏或者出现了一个特别离群的长尾说明采集数据可能存在异常回头检查解析逻辑或者清洗步骤而不是直接把图丢进报告里。5. 全流程整合与常规注意事项5.1 一个完整的脚本骨架把上面所有部分串起来完整项目的核心流程可以浓缩成一个主脚本包含请求、解析、存储和统计四个阶段。import requests import pandas as pd import time import random from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36 } BASE_URL https://{city}.lianjia.com/ershoufang/pg{page}/ def parse_list_page(html): # 上文已实现的解析函数 pass def main(citybj, max_pages100): session requests.Session() all_data [] for page in range(1, max_pages 1): url BASE_URL.format(citycity, pagepage) resp session.get(url, headersheaders, timeout10) if resp.status_code ! 200: break page_data parse_list_page(resp.text) if not page_data: break all_data.extend(page_data) time.sleep(random.uniform(1, 3)) print(f已完成第{page}页累计{len(all_data)}条) df pd.DataFrame(all_data) df.to_csv(house_data.csv, indexFalse, encodingutf-8-sig) # 后续统计分析直接读取csv if __name__ __main__: main(citysh, max_pages50)分页循环里做了一个提前终止的判断如果某一页解析出来是空列表说明已经翻到了列表末尾继续循环没有意义直接跳出能省下不少时间。这个细节在真实的爬虫场景里很常用列表页返回空内容代表数据到头了。5.2 频率、慢速与稳健性爬虫脚本跑起来之后的稳定性往往比速度更重要。我见过有人追求快把每个请求之间的间隔降到0.1秒结果跑不到20页IP就被封了反而一无所获。我的经验是在一线城市的数据量级下即便每秒1次请求两三个小时也能把整个城市的数据抓完。如果项目需要长期运行建议加上一个异常处理级别的断点续爬机制。最简单的做法是每爬完20页就把当前数据存一次盘这样即便中途程序崩溃损失也控制在20页的范围内。我通常用df.iloc把数据分批追加到CSV里而不是在内存里攒到最后一并写盘。另外爬虫代码中处处都要有容错。网络请求阶段的超时、解析阶段的字段缺失都需要单独的兜底分支。宁可在代码里多写几个if判断也好过程序跑了一夜第二天早上醒来发现数据少了大半。6. 常见问题与排查技巧实录6.1 遇到反爬策略怎么办最常见的报错是请求返回401、403或者弹出一个验证码页面。处理思路分级推进第一级先从请求头下手。确认User-Agent是完整的、最新的浏览器UA加上Accept-Language。顺带检查Referer字段有些站点会校验请求来源如果Referer直接是别的域名服务器就会拒绝。第二级降低请求频率。把延迟从1秒提高到3~5秒随机化的范围拉大让请求间隔更像人的浏览节奏。第三级处理验证码。如果下拉列表页面开始出现滑块验证码说明当前IP已经被标记了。这种情况下换代理IP是最直接的办法不过要注意代理的质量和稳定性免费的代理池往往比被限速更坑。6.2 解析结果为空爬虫最常见的问题就是选择器写错了。页面改版或者内容采用动态加载都会导致选择器失效。判断方法是先打印一段HTML看看实际结构# 调试用把页面保存到本地再用浏览器打开检查 with open(debug_page.html, w, encodingutf-8) as f: f.write(html)然后把这一段HTML用浏览器打开重新定位元素的class路径。如果确认页面结构没问题但解析结果还是空那就要考虑数据是不是异步加载的。部分站点首页是框架页房源数据是通过XHR接口异步返回的这时候直接请求页面HTML是拿不到核心数据的。解决办法是打开浏览器的Network面板找到真正的数据接口URL把爬虫的目标从HTML页改成JSON接口。6.3 乱码和数据错位乱码大概率是编码问题。requests处理文本时如果页面没有明确声明charset默认按ISO-8859-1解码中文就会变成一堆乱码。解决方案是手动指定resp.encoding utf-8数据错位往往是解析顺序错了比如字段A的解析结果赋值给了字段B或者在遍历时列表索引对不上。这类问题最好的排查方式是抽样打印输出前3条记录的dict用肉眼看字段值和中文是否对应得上。我自己的习惯是在爬虫正式跑全量之前先爬一个页面做demo并打印JSON格式的结果。等到所有字段都确认正确了再放开循环跑全量。这个习惯帮我省下了非常多的返工时间。7. 从爬虫到选房决策的实践心得实际做完这个项目我最大的感触是爬虫只是手段数据分析才是目的。当整个城市的几千条房源数据铺开在面前时你能看到很多平时靠感觉根本摸不透的规律。同一个小区低楼层和高楼层的价差有多大地铁沿线和远离地铁的房源单价能差出多少同板块小户型和大户型每平米单价的走向差异。这些数据如果不主动去采集靠线下跑盘跑一个月也积累不到足够的样本量。最后分享两个小建议。第一个用总价筛选比用单价筛选更贴近真实选房的场景因为总价预算决定了你实际能买到什么样的房子。第二个跑一次完整流程之后把结果输出成excel或csv再用筛选功能手动看一眼数据里有没有异常值工具只能帮你处理规则明确的数据真正的判断力还是要落到人身上。如果后续想往深入做这个项目可以考虑引入时间维度定期采集并对比挂牌价的涨跌走势甚至结合成交数据做更贴近市场真实成交价的分析。但在那之前先把现有这套流程跑通、跑稳就已经能解决很大一部分信息不对称的问题了。本文还有配套的精品资源点击获取