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

资讯详情

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

Python爬虫实战:抓取安居客新房数据简易版教程

Python爬虫实战:抓取安居客新房数据简易版教程 朋友上周问我说想整理一下上海各个板块的新盘报价问我有没有现成工具。我说你自己打开安居客一页一页翻不就行了他说三百多个楼盘一个个点开再把单价、户型、电话复制进表格一个晚上就没了。这个场景太熟悉了。很多人学Python爬虫入门第一件事就是想抓个真实的房源网站练手选安居客的原因很直接页面结构相对规整新房列表页不需要登录字段信息也比较全是目前最适合做教学案例的房产信息源之一。这篇内容我就围绕Python实现安居客新房数据抓取简易版这个主题把从零到能跑通的完整思路写清楚。包括环境怎么搭、页面结构怎么看、代码怎么组织、反爬怎么处理、数据怎么存以及我实际调试中踩过的坑。目的不是给你一个能无限放大抓取规模的生产级工具而是让你理解数据抓取的完整链条以后遇到其他网站也能照着这个思路自己分析。1. 抓取安居客新房数据前先想清楚目标和边界写爬虫最怕一上来就写代码。先搞清楚你要什么再去想怎么拿顺序反了就会不断返工。这一节把项目定位、数据字段和合规边界都理清楚你后面写代码会顺很多。1.1 这个项目到底要做什么简易版三个字很关键。市面上很多爬虫课程一上来就上Scrapy框架、分布式部署、代理池新手没跑通就弃坑了。我这边的定位是用requests加BeautifulSoup这两个最基础的库把安居客新房列表页的公开信息抓下来整理成结构化表格够分析房价趋势、区域分布、户型配比就够了。具体来说单次抓取的目标数据包括楼盘名称所在区域行政区和板块参考均价主力户型楼盘地址开发商或售楼处电话楼盘页面的链接这些字段覆盖了绝大多数分析场景。如果你还要户型图交房时间容积率那就得进详情页继续抓逻辑是相似的但请求量会翻好几倍属于进阶内容后面单独说。1.2 先明确边界哪些能碰哪些不能碰我要先说一个价值观上的问题。数据抓取这个技术本身是中性的但使用方式有边界。我这里强调三条原则也是我自己做这个项目时给自己立的规矩第一只抓公开页面数据不做任何需要登录才能看到的内容。安居客的新房列表页和详情页的公开字段是网站向所有访客展示的信息抓取这类数据的争议性相对较小。第二严格遵守robots协议。你可以在浏览器里访问目标网站的robots.txt看看凡是明确禁止抓取的路径一律不碰。第三控制单位时间内的请求频率不给目标服务器造成压力。爬虫写出来是给自己用的不是用来搞垮别人服务的。你抓完了数据网站照样要正常运行这是底线。1.3 robots.txt和网站访问约定怎么看在这个项目开始之前我习惯性地检查目标网站的robots.txt。打开方式很简单在浏览器地址栏输入域名加/robots.txt就能看到。安居客的robots.txt里对部分路径做了访问限制这个必须看仔细。看完robots之后还有一个重要动作确认页面的真实访问成本。如果列表页每翻一页要额外发出几十个请求说明页面是重的抓取速度就得放慢。我在实际测试中发现安居客的列表页相对轻量大部分数据直接嵌在HTML里不需要依赖额外的接口请求这对新手来说是好事对服务器的压力也小。2. 开发环境与核心依赖选型环境准备好了代码才能跑得顺畅。这个项目用到的依赖库不多但每个库的选型都有讲究。我把自己常用的一套组合和选择理由写出来你按这个装就行。2.1 Python版本与虚拟环境我建议直接用Python 3.8以上的版本。为什么是3.8而不是更老的3.6因为从3.8开始内置模块和第三方库的兼容性都进入了一个稳定期尤其是类型注解、异步编程这些特性都有显著改进。安居客这种中型网站的数据抓取用3.8到3.12之间的任何版本都不会有兼容问题但如果你装的是官方最新的稳定版那就更省心。关于环境管理我强烈建议用虚拟环境。你可以用Python自带的venv也可以用Anaconda。对于这个项目来说venv就够了几条命令的事python -m venv anjuke_env source anjuke_env/bin/activate # Windows下是 anjuke_env\Scripts\activate虚拟环境的意义在于隔离依赖。你以后做其他项目时不会因为版本冲突搞得头疼。这是所有Python开发的基本功不只是爬虫才需要。2.2 用到的核心依赖及选型理由先看一张表把依赖库和它们各自承担的任务列清楚依赖库版本建议用途为什么选它requests2.28发送HTTP请求获取页面HTML语法简洁被广泛使用遇到问题搜得到答案beautifulsoup44.12解析HTML提取目标节点和数据容错性好对新手友好调试直观lxml4.9BeautifulSoup的解析器引擎比Python内置的html.parser快很多处理大页面不卡pandas2.0数据清洗和结构化导出做数据分析绕不开它后期还能做可视化fake-useragent1.4随机生成User-Agent请求头比手动维护UA列表省事这里有人可能会问为什么不用Scrapy我的答案是Scrapy是重型框架学习曲线陡峭还要配置Item Pipeline、Selector、Middleware一大堆概念。对于抓一个网站的几个字段这种需求requests加BeautifulSoup的组合拳已经够用了逻辑更透明也更容易读懂每一步在干什么。还有人会问为什么不用Selenium或Playwright这类浏览器自动化工具适合处理JavaScript动态渲染的页面。我的判断标准是如果能通过分析接口拿到数据就不要用浏览器自动化。它对资源的消耗大一个数量级而且容易触发风控。安居客的新房列表页数据基本都在静态HTML里requests就够。2.3 安装命令与版本锁定环境准备好之后安装依赖就一行命令pip install requests beautifulsoup4 lxml pandas fake-useragent安装完成之后我建议把版本记录到一个requirements文件里方便以后复现环境pip freeze requirements.txtpandas体积比较大安装时间比较长是正常的。如果你只打算把数据存成CSV不急着做分析也可以暂时不装pandas用Python内置的csv模块顶上去。但既然项目的目标是采集数据最终大概率还是要做分析所以一步到位装好更省事。3. 页面结构与请求逻辑拆解代码写得好不好取决于你对页面结构的理解深不深。这一节不讲abstract语法而是带你从浏览器开发者工具出发拆解安居客新房列表页的真实结构。3.1 从URL看分页规则打开安居客新房频道你会看到URL大概是这样的结构域名后面跟着城市代码然后是xf新房路径再跟一个v3/sale之类的子路径最后是页码参数。我把具体的域名结构做模糊化处理因为不同城市、不同时期的URL会有差异直接给出一个死规律反而会误导你。你需要掌握的是分析思路打开一个不是第一页的列表页对比URL和第一页的差异找出页码对应的参数名和值。大多数情况下就是一个pn参数或者类似的分页参数值从1开始递增。这样你就能通过循环构造URL实现翻页。我写了一个通用函数来构造列表页URL核心是把城市代码和页码作为变量传入def build_list_url(city_code: str, page: int) - str: # 不同城市代码不同例如 sh 表示上海bj 表示北京 base_url https://{}.example.com/v3/sale/pn{}.format(city_code, page) return base_url注意这个example.com只是结构示意实际使用时要把域名替换为目标站点的真实域名。我故意没有写死完整链接就是希望你跑通逻辑之后自己去页面上核对真实URL而不是拿着一串过期的硬编码到处碰壁。3.2 开发者工具分析的三个关键步骤你用Chrome或Edge打开一个新房列表页按F12进入开发者工具切换到Network网络面板然后刷新页面。这里有三个关键信息需要确认第一步确认页面数据是不是直接在HTML里。在Network面板里点击Doc这个过滤条件找到主文档请求点开Response响应标签搜索一个楼盘名称。如果能在HTML响应里搜到说明数据是服务端渲染的requests就能直接拿。第二步找到关键CSS选择器。在Elements元素面板里鼠标悬停在楼盘卡片上右键选择检查观察它对应的DOM结构。通常每个楼盘卡片会有一个公共的class例如item或者property-band之类。你用soup.select传入这个class就能拿到全部的楼盘卡片。我在下面代码里写的选择器都是示范性的因为网站在不同时期会改版类名会变。你要学会的是自己找到这个类名的方法而不是抄我的。第三步确认列表中总共有多少页。翻到最后一页看一下页码数字或者看页面底部的分页控件。这个数字将决定你的循环范围。你还可以从页面上一并提取总页数就不用硬编码了。3.3 请求头的构成与UA伪装直接拿requests去GET页面返回的概率比较低因为服务器会检查请求头。经验是模拟浏览器最基本的请求头字段至少包括User-Agent和Referer。User-Agent告诉服务器你是什么样的客户端Referer告诉服务器你是从哪个页面跳转过来的。用fake-useragent随机生成UA比手动维护一个列表更省心from fake_useragent import UserAgent ua UserAgent() headers { User-Agent: ua.random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }有些教程会建议把Cookie也放进去理由是某些页面需要Cookie标识会话。我在实际测试中发现安居客的新房列表页在未登录状态下也能正常访问所以不需要预先从浏览器复制Cookie。如果你发现某个页面总会跳到验证码或登录页才需要考虑Cookie的补充。3.4 超时和重试机制必须一开始就写进去新手最容易忽略的两个东西就是超时设置和异常重试。网络是不稳定的你的IP访问目标站点时可能出现响应缓慢、连接被重置、超时等情况。如果脚本没有超时限制它就会傻傻地等在那里一个请求卡住了整个程序就卡住了。requests的get方法支持timeout参数设一个合理的值就好。至于重试可以用一个简单的装饰器实现逻辑是请求失败后等待一段时间再试连续失败达到最大次数就放弃。基础的请求函数我建议这样写import requests import time from fake_useragent import UserAgent ua UserAgent() session requests.Session() def fetch_html(url: str, max_retries: int 3): for attempt in range(max_retries): try: headers { User-Agent: ua.random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } resp session.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except (requests.RequestException, Exception) as e: if attempt max_retries - 1: raise wait_time (attempt 1) * 2 time.sleep(wait_time)有几个细节值得展开session对象复用TCP连接比反复重建连接效率高resp.apparent_encoding是根据页面内容自动检测编码能避免中文乱码raise_for_status会主动抛出4xx或5xx的错误避免你拿着一个错误页面继续解析。四、核心代码逐段拆解从HTML到结构化数据这一节是全文的重头戏。我把代码拆成四个模块请求模块、解析模块、存储模块、主流程模块。每个模块的功能单一职责清晰。你要是在某个环节卡住了单独看那一段就能定位问题。4.1 解析模块提取楼盘卡片与字段解析模块的输入是HTML文本输出是一个列表列表里的每个元素是一个字典。字典的键就是前面规划好的字段名楼盘名、区域、均价、户型、地址、电话、链接。用BeautifulSoup解析HTML三板斧先创建BeautifulSoup对象再select找到所有卡片最后对每个卡片extract字段。from bs4 import BeautifulSoup def parse_list_html(html: str): soup BeautifulSoup(html, lxml) items soup.select(.item) # 实际选择器以页面结构为准 results [] for item in items: try: name item.select_one(.name a).get_text(stripTrue) price item.select_one(.price).get_text(stripTrue) area item.select_one(.address a).get_text(stripTrue) house_type item.select_one(.house_type).get_text(stripTrue) link item.select_one(.name a).get(href) results.append({ 楼盘名称: name, 均价: price, 区域: area, 主力户型: house_type, 链接: link, }) except AttributeError: # 某个字段缺失时跳过这条记录不中断整个解析 continue return results这段代码里最关键的是select_one和select的区别。select_one返回第一个匹配的节点适合提取唯一字段select返回所有匹配节点适合提取列表。get_text(stripTrue)可以拿到纯文本并去掉首尾空格比get_text()再手动strip省一步。很多人写解析代码时习惯抓住一个字段就写出完整的选择器其实更高效的做法是先摸清楚卡片的DOM树层级把公共类名找出来然后逐层下钻。这样即使个别字段没有匹配到其他字段仍然能正常提取。关于如果字段不存在的问题我的处理是用try except包住整条记录。解析过程中很容易出现某个楼盘没有价格或者没有户型的情况如果不加保护一个缺失字段就会让整个脚本崩溃。捕获AttributeError并continue是简易版里最实用的容错策略。4.2 存储模块直接生成CSV数据解析完之后建议第一时间写入CSV文件。CSV的好处是格式通用Excel能打开pandas能读入几乎任何数据分析工具都认它。用Python内置的csv模块就可以不需要额外依赖import csv def save_to_csv(data: list, file_path: str): if not data: return fieldnames [楼盘名称, 均价, 区域, 主力户型, 链接] with open(file_path, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(data)这里有一个Windows用户容易踩的坑如果文件以utf-8编码保存Excel打开后中文会乱码。解决方案是使用utf-8-sig编码它会自动加一个BOM头Excel就能正确识别。Mac或Linux用户用utf-8也行但反正utf-8-sig也兼容所以干脆统一用它。4.3 主流程模块翻页、解析、保存一气呵成主模块把前面的函数串起来import time import random def main(city_code: str, start_page: int, end_page: int): all_data [] for page in range(start_page, end_page 1): print(正在抓取第{}页.format(page)) url build_list_url(city_code, page) html fetch_html(url) page_data parse_list_html(html) if not page_data: print(第{}页没有解析到数据可能页面结构有变或已被限制.format(page)) break all_data.extend(page_data) time.sleep(random.uniform(1, 3)) # 关键随机睡眠降低请求频率 save_to_csv(all_data, f{city_code}_new_house.csv) print(完成共{}条数据.format(len(all_data)))这个主流程很简单但有一个必须强调的点random.uniform(1, 3)这行看起来不起眼实际上是最重要的反爬策略之一。连续快速请求哪怕每次间隔0.5秒也很容易触发服务器的访问频率限制而1到3秒的随机间隔在行为上更接近真实用户同时也给服务器留了足够的处理时间。4.4 把代码跑起来必须先确认的一件事第一次实际运行之前请先用浏览器打开一个列表页手动数一下页面底部的分页控件。确认你配置的start_page和end_page没有超出真实范围。因为当页码超出范围时网站通常会返回一个空白列表页解析不到任何数据脚本就会break退出。这个行为本身是安全的但如果你需要完整数据就白跑了一段进度。我在真实抓取中就遇到过一次页码范围判断错误抓了前10页之后第11页返回了空列表脚本直接停了。前后花了大概两分钟数据只有300多条。后来重新核对了分页信息把范围修正后重新跑才拿到全部数据。5. 反爬应对与请求频率控制的实战经验爬虫写出来最常遇到的问题不是代码报错而是跑着跑着突然没数据了。这一节我把遇到过的反爬现象和应对办法梳理一下全是实战总结。5.1 最常见的几种被限制信号页面返回200但HTML里没有楼盘数据空壳页面页面跳转到验证码页或安全验证页返回503 Service Unavailable连续请求几页之后突然出现一段长时间的空白响应这些信号出现时不要再硬着头皮重试。先停下来检查自己的请求频率是不是太高UA是不是太单一或者是否因为网络出口IP被临时限制。简易版项目里不需要搞复杂的代理池先把频率降下来大多数问题都能解决。5.2 频率控制的正确姿势很多教程会告诉你time.sleep(1)就够了但我的经验是固定间隔恰恰是机器人行为的典型特征。真实用户的浏览节奏是有波动的有时候连续点开两三个页面有时候停下来看很久。所以随机睡眠比固定睡眠更有效。代码层面就是这样import time import random # 随机睡眠1到4秒模拟用户浏览间隔 time.sleep(random.uniform(1, 4))如果你要抓的数据量比较大总共几十页那么每页之间加这个随机睡眠整体耗时大概两三分钟完全可以接受。如果只抓一页试试水那间隔都不重要。5.3 User-Agent轮换与Session复用前文代码里用了fake-useragent每次生成一个随机UA。这里有一个细节容易忽略同一个session在不同请求间不要频繁更换UA因为真实浏览器在短时间内不会经常变UA。如果你的每个请求都换一个UA反而可能被识别为异常。我的做法是每一个翻页循环开始前设置一次UA这一整轮翻页都使用它。如果你需要切换UA可以随机决定翻几页后再换更贴近真实行为。Session复用还有一个好处它会自动管理Cookie。如果服务器在第一次响应时种下了某些Cookie后续请求会带上这样就少了很多为什么第二次请求就被封了的问题。5.4 把被限制当成正常的业务分支来处理简易版爬虫的另一个关键设计是把被限制当成一个正常的业务分支来处理。不要觉得只要代码正确就不会被限制网络环境千变万化服务器策略也在动态调整。我在前面的fetch_html函数里加了一个指数退避的重试策略连续失败时等待2秒、4秒、8秒递增。这样做的好处是服务器暂时限流时脚本不会疯狂重试加剧问题而是冷静地等一等再尝试给服务器恢复的时间。如果重试了三次还是失败就放弃这一页记录日志继续下一页。等到全部跑完再回头补抓失败的那几页比一次性卡死强得多。真正做过爬虫的人都会明白一个能优雅处理失败的爬虫比一个永远不失败的爬虫更实用。因为后者根本不存在。5.5 关于验证码和登录态我的态度是绕开简易版项目里我明确不建议碰验证码识别、滑块轨迹模拟、登录态维持这些操作。原因有三点复杂度高加入这些逻辑后项目就不再是简易版了。法律风险大破解验证码和绕过登录机制属于明显的规避手段超出合理抓取范围。性价比低你真的需要那些登录后才能看到的数据时不如联系官方渠道或者找其他公开数据源。抓取技术是开源技术合理的应用场景很广。我见过有人拿爬虫做舆情分析、市场调研、学术研究这些都有正当价值。不要因为贪图某块封闭数据就把项目推到灰色地带。6. 数据清洗、落盘与后续还能怎么玩代码跑通之后你会得到一个CSV文件。这个文件可以直接用Excel打开但如果要进行更系统的分析最好用pandas再处理一下。这一节讲清洗步骤和几个很实用的扩展方向。6.1 用pandas做基础清洗安居客抓下来的均价字段一般是30000元/㎡这种格式含单位还可能有价格待定之类的非数值内容。做分析前需要清洗掉单位转成数值型把价格待定统一处理成空值。import pandas as pd df pd.read_csv(sh_new_house.csv) df[价格数值] df[均价].str.replace(元/㎡, , regexFalse) df[价格数值] pd.to_numeric(df[价格数值], errorscoerce) df df.dropna(subset[价格数值])这一步做完就已经能做很多事情了。比如按区域分组看各板块的平均价格统计每个区域在售楼盘数量了解供应分布对户型字段做词频统计看看主流供应是两居还是三居。区域字段通常包含行政区和板块两级信息比如浦东-张江可以用str.split拆成两列df[[行政区, 板块]] df[区域].str.split(-, expandTrue)6.2 简单可视化把抓到的数据画出来清洗完之后用pandas自带的绘图功能甚至不需要额外装matplotlib就可以快速画一张均价分布图df.groupby(行政区)[价格数值].mean().plot.bar()这张图能直观看出哪个区均价最高哪个区供应量最大。如果再结合板块维度的数据你可以进一步锁定价格洼地板块对购房决策或者市场分析都有参考价值。6.3 进阶扩展方向但别急着做详情页抓取从列表页拿到每个楼盘的详情页链接再进详情页抓取交房时间、户型图、容积率、物业费等信息。请求量会大幅增加务必把频率再调低。历史数据对比把每次抓取的结果存到不同日期的CSV里通过对比不同时间点的数据观察价格变化趋势。定时增量更新用系统自带的定时任务每周跑一次看看数据变化。这个属于运维范畴但在一台普通电脑上也能做。多城市扩展把城市代码做成参数循环多个城市横向对比不同城市的供应和价格。这些方向里我认为最有价值的是详情页抓取和定时更新。前者拓展数据维度后者建立数据的时间序列。但简易版项目还没有必要全部上先跑通列表页做出一次完整分析再按需扩展。最后说一个实际体会数据抓取这个技能的真正价值不在于你能从某个网站上拿到多少数据而在于你建立起了一套从需求到数据再到结论的完整链路。以后不管目标是房产信息、招聘数据还是公开统计你都可以用同样的思路快速实现。这个项目只是一个开始。
返回列表