
去年年底帮人做楼盘市场调研的时候发现人工一个个去翻网页记录楼盘信息实在太费时间了。一天下来光复制粘贴楼盘名称、均价、位置这些字段就要花掉大半天。后来我干脆写了一个Python抓取安居客新房数据的简易版脚本把重复劳动交给程序去处理。今天把整个思路、实现流程和踩过的坑整理出来分享给同样有类似需求的朋友。这段代码不复杂用到的库也就是requests加BeautifulSoup不需要什么高深的技术。但麻雀虽小五脏俱全——一个完整的简易爬虫该有的东西它都有请求头的伪装、翻页循环、字段解析、数据保存、异常处理。跑完之后把数据落成CSV文件后续用Excel或者pandas做汇总分析都很方便。适合刚入门Python爬虫的朋友以及虽然写过一些代码但从来没碰过网页数据采集的同学。我写这篇东西的时候脑子里反复想的是如果半年前有人能给我一份这样的实战笔记我至少能省两到三天的摸索时间。今天我就把这些经验原原本本摊开来讲包括代码怎么写、为什么这么写、最容易在哪儿翻车全都交代清楚。1. 项目到底要做什么先把需求和目标说清楚1.1 我为什么想抓安居客的新房数据在写这篇博文之前可能有朋友会问你直接去安居客手动搜索不就行了为什么非要写一段代码来抓数据这个问题问得很实际。一句话解释人工浏览适合看几套房子但完全不适合看几百上千套房子。我当时的场景是这样的需要收集某个城市一定范围内所有在售新盘的楼盘名称、参考均价、所在区域、楼盘地址、户型面积、开盘时间等信息用来做区域供应分析。如果全部手动操作每打开一个楼盘详情页都要记录好几个字段几百个楼盘就是上千次复制粘贴。而且复制粘贴过程中非常容易出错——手一抖把均价写错、格式不统一、字段漏填后面做分析的时候会浪费更多时间返工。写个脚本抓数据程序运行期间我可以去干别的事情回来直接拿一份结构化表格用。而且脚本是可复用的——下次换个城市、换一批关键词改改URL参数就能重新跑。这才是这类小工具真正的价值所在一次编写多次复用把人力从重复劳动中解放出来。这个项目虽然叫简易版但恰恰因为简它才更容易被理解和修改这是复杂框架给不了的灵活性。1.2 简易版的定义与技术选型思路既然说了是简易版那就得先明确一下边界。我在这套方案里做的技术取舍是这样的只抓列表页数据不做详情页的深度爬取只需要requests和BeautifulSoup两个核心库不依赖Scrapy、Selenium等重量级框架数据存成CSV文件不接数据库用同步方式逐页抓取不引入多线程、协程等并发机制异常处理以跳过并记录为主不做复杂的重试和断点续传有人可能会说不用Scrapy、不用Selenium遇到动态渲染的页面怎么办这里就涉及一个很重要的判断目标网页的数据到底是怎么加载出来的。我检查过安居客的新房列表页楼盘名称、均价这些核心字段是在HTML源码里直接存在的属于服务端渲染的内容。这就意味着只需要发一个普通的HTTP请求就能在返回的HTML里找到我需要的数据完全没必要杀鸡用牛刀去上Selenium。反过来如果目标页面是纯前端渲染比如数据通过Ajax异步加载那这套方案就不适用了。判断方法也简单在浏览器里按CtrlU查看源代码如果能在源码里搜到目标字段说明数据就在HTML里如果搜不到那才需要另想办法。这个判断思路对任何爬虫项目都适用建议新手先养成这个习惯再动手写代码。技术选型上requests的Session配合自定义Headers是我最喜欢的方式既轻量又能有效模拟浏览器行为。BeautifulSoup则是老牌解析库了文档丰富遇到标签结构问题也好排查。这套组合虽然看起来朴素但对付大批量静态页面数据采集完全够用。2. 环境准备与前期分析动手前必须做好的三件事2.1 Python环境与依赖库安装先说环境。我本机用的是Python 3.10但其实3.8以上的版本跑这套代码都没什么问题。如果你还没装Python建议去官网下载对应操作系统的安装包安装的时候记得勾选Add Python to PATH这个选项否则后面在命令行里敲python会提示找不到命令。然后是安装依赖。只需要两个库pip install requests beautifulsoup4如果网络不太给力可以换国内源提升下载速度pip install requests beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成之后可以顺手验证一下import requests import bs4 print(requests.__version__) print(bs4.__version__)能输出版本号就说明环境没问题了。这一步虽然简单但一定不要跳过。我见过不少朋友跳过验证直接跑后面的代码结果因为Python路径指向了旧版本解释器import时报ModuleNotFoundError来回排查半天才发现是环境配错了。2.2 摸清网页结构动手前先看明白目标这一步是整个项目里最容易被忽略、但最容易翻车的地方。写爬虫本质上是在和网页结构打交道。如果你连目标网页长什么样、数据在哪个标签里都没搞清楚后面写出来的解析代码一定是盲人摸象。所以我强烈建议在写任何代码之前先在浏览器里完成下面三个动作打开安居客新房列表页面右键选择检查切换到Network面板刷新页面找到名为list开头的文档请求点进去看Response内容在Response里搜索一个你肉眼可见的楼盘名称确认它确实在HTML源码里实测下来安居客新房列表页的每个楼盘卡片是一个li标签class属性里包含item字样。楼盘名称在这个li内部的h3标签里均价在em标签里区域在span标签里。不同城市、不同推广位置的卡片结构会有一点差异有的带置业顾问信息有的是纯房源卡片但核心字段的位置相对稳定。一个实用的方法是复制一小段包含目标楼盘名称的HTML片段粘贴到本地笔记里然后逐行标注哪些是你需要的字段哪些是广告位需要过滤的。这个标注步骤看起来笨但能在后续写解析逻辑时省下大量试错时间。2.3 请求头的伪装别让服务器一眼识破一个容易踩的坑就是直接用默认的requests请求头去访问。requests的默认User-Agent是python-requests/x.x.x服务器一看就知道这不是正常浏览器轻则返回异常页面重则直接封IP。解决方式是在请求时带上一个看起来像正常浏览器的User-Agent。我这里用的方式是构造一个完整的Headers字典把User-Agent、Referer、Accept-Language都补上headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36, Referer: https://shanghai.anjuke.com/, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }User-Agent从哪里来最简单的办法是打开你浏览器的开发者工具在Network面板里随便点一个请求看它的Request Headers把User-Agent复制过来。千万别手打或者瞎编网上那些乱七八糟的UA字符串有时候连手机模拟器都不兼容反而更容易触发风控。注意Headers字典是一个很灵活的载体除了这三个字段有些场景可能还需要带上Cookie或者其它自定义请求头。核心思路只有一个——让服务端觉得这是一个正常用户在访问。3. 核心代码实现与关键细节从单页到翻页的完整攻略3.1 第一版单页抓取跑通流程写爬虫的正确节奏一定是从小到大、从简到繁。我写这个项目的第一个版本时只做了两件事请求一个页面然后把页面里的楼盘信息打印出来。这个版本不处理翻页、不存文件、不搞异常处理但它是整个项目的地基。核心代码大致是这样import requests from bs4 import BeautifulSoup import re headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36, Referer: https://shanghai.anjuke.com/, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } url https://shanghai.anjuke.com/community/p1 resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) items soup.find_all(li, class_re.compile(item)) print(当前页楼盘数量:, len(items)) for item in items[:5]: name_tag item.find(h3) price_tag item.find(em) area_tag item.find(span, class_re.compile(area)) if name_tag: print(名称:, name_tag.get_text().strip()) if price_tag: print(均价:, price_tag.get_text().strip()) if area_tag: print(区域:, area_tag.get_text().strip()) print(---)这里有几个细节值得说一下。resp.encoding resp.apparent_encoding这一行是为了解决中文乱码问题。requests从response headers里拿到的charset有时候和页面实际的编码不一致直接用resp.text会出现楼盘名称变成乱码的尴尬。apparent_encoding会基于页面内容本身做推断大多数情况下比headers里写的可靠。class_re.compile(item)这个技巧是因为class属性往往有多个值而且不同页面的class名称略有差异。直接用class_item精确匹配容易漏数据用正则去做模糊匹配反而更稳。这是我在多个爬虫项目里总结出来的一个通用写法。不建议把打印全部数据当成终点。这个阶段的核心目标只有一个确认请求能通、解析逻辑能拿到正确的字段。一旦这一步跑通了后面的事情都是水到渠成的。3.2 第二版翻页循环与请求间隔单页跑通之后接下来就是翻页。翻页是爬虫里最基础但最容易翻车的环节因为不同网站的翻页机制不一样。我见过有的网站通过JavaScript动态生成下一页按钮的URL有的则是改URL参数里的页码数字。实测下来安居客新房列表页是典型的URL参数翻页模式第一页通常是xxx/p1第二页是xxx/p2以此类推。所以翻页只需要在循环里构造URL即可import time import random import requests from bs4 import BeautifulSoup import re headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36, Referer: https://shanghai.anjuke.com/, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } all_data [] for page in range(1, 6): # 抓前5页 url fhttps://shanghai.anjuke.com/community/p{page} try: resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) items soup.find_all(li, class_re.compile(item)) for item in items: record parse_item(item) if record: all_data.append(record) print(f第{page}页完成本页{len(items)}条数据) except Exception as e: print(f第{page}页出错: {e}) time.sleep(random.uniform(2, 4))注意我在每页请求之间加了一个time.sleep间隔时间是2到4秒的随机值。这个操作非常重要它既不是为了装模作样也不是为了慢而慢。它有两个实际作用第一模拟人的浏览节奏降低被服务器风控系统识别为机器人的概率。第二给目标服务器留出处理请求的余量减少因为请求过密导致的连接中断。random.uniform(2, 4)是我个人比较喜欢的间隔方式。固定间隔不如随机间隔自然而且固定间隔在时间序列分析上有一个非常明显的规律性特征虽然在这个项目里可能体现不出来但养成随机间隔的习惯对以后做更大规模采集只有好处。这里我再强调一下f-string的用法要熟Py3.6以后这就是字符串拼接的首选方案了。在URL里传参数最忌讳手写号拼字符串漏一个斜杠或者漏一个等号排查半天都找不到问题。3.3 字段提取的边界情况与清洗逻辑解析HTML标签本身不复杂复杂的是处理各种边界情况。听起来像套话但实操中你一定会遇到下面这些情况第一个是字段缺失。同一页里有的楼盘均价是待定有的是具体数字有的区域直接叫浦东有的还带子区域。用find拿到的是Tag对象但有的Tag是None。所以解析函数里必须先判断Tag是否为None再做文本提取。第二个是文本清洗。HTML里提取出来的字符串经常带着换行符、空格、制表符。get_text()之后一定要做strip()最好再配合正则把连续空白符替换成单个空格。第三个是数据格式统一。均价字段提取出来可能是58000元/㎡如果要做数值分析就得把元/㎡去掉再转成int。这部分清洗逻辑我单独抽成了一个函数def parse_item(item): name_tag item.find(h3) price_tag item.find(em) area_tag item.find(span, class_re.compile(area)) address_tag item.find(span, class_re.compile(address)) if not name_tag: return None return { name: name_tag.get_text().strip(), price: parse_price(price_tag), area: area_tag.get_text().strip() if area_tag else None, address: address_tag.get_text().strip() if address_tag else None, } def parse_price(raw_tag): if not raw_tag: return None text raw_tag.get_text().strip() match re.search(r(\d), text) if match: return int(match.group(1)) return None我一直强调解析函数要独立成模块。原因很简单网站改版太常见了今天能跑通的标签选择器明天可能就失效了。数据解析逻辑集中在一个地方出了问题只需要改这一个函数不用在整个脚本里东找西找。这个习惯在项目规模变大之后会越来越值钱。4. 数据存储与结果验证抓到数据只是完成了一半4.1 用CSV保存数据抓下来的数据最终要落到文件里才有意义。我选择CSV是因为它足够通用Excel能打开、pandas能读取、写起来也简单。JSON当然也可以但如果你想拿数据做表格透视或者报表CSV显然顺手得多。写入CSV用Python内置的csv模块就够了不需要额外装库import csv def save_to_csv(data_list, filenameanjuke_data.csv): with open(filename, modew, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([楼盘名称, 均价, 区域, 地址]) for row in data_list: writer.writerow([row[name], row[price], row[area], row[address]]) print(f数据已保存到 {filename})这里有两个细节容易忽略。第一个是encodingutf-8-sig。如果你用普通的utf-8编码写CSV在Windows上用Excel打开时中文会变成乱码。用utf-8-sig会在文件开头写入一个BOM标记Excel识别起来就正常了。这个问题坑过无数新手我一开始也没注意直到有同事拿着我给他的CSV打开一看全是乱码才意识到。第二个是newline。如果不加这个参数在Windows平台上csv.writer在写每一行的时候可能会多出一个空行导致最后生成的表格看起来很松散。加上newline就能避免这个坑。4.2 数据去重与抽样核验数据存好之后别急着高兴得先做一轮质量检查。我用过一个笨但有效的方法把CSV里的楼盘名称列去重跟网站上随机抽取几个楼盘对比一下。抽样方式很简单在网站列表里随机找3个不同城市的楼盘名在CSV里搜索看是否存在。同时反向操作在CSV里随机挑3行数据去网站上核对均价和区域是否一致。如果这两轮抽样都通过了基本可以认为数据质量是可靠的。如果发现某个楼盘名称重复了那就是翻页时URL构造有误导致同一页被重复抓取。如果发现均价对不上有可能是因为页面渲染规则发生了变化需要回头检查选择器。另外再多说一句我习惯在每次抓完之后用pandas快速做一个汇总看看总共抓了多少条数据、有几个城市、均价分布大概是什么范围。这一步不是为了写论文而是为了在数据层面快速发现问题。比如正常应该有500条数据结果只有50条那肯定是有页面被翻过或解析失败早发现早处理比等到做分析时才发现要省心得多。4.3 CSV文件的扩展从Excel到pandas如果你只是想看看数据长什么样直接用Excel打开CSV就行。但如果要做进一步分析比如按区域分组求平均均价把CSV读进pandas是最省事的路径import pandas as pd df pd.read_csv(anjuke_data.csv) print(df.head()) print(df.groupby(区域)[均价].mean())pandas读CSV会自动识别文件编码和表头几乎不需要额外配置。这里提醒一句如果你前面用了utf-8-sig写入pandas读取时也需要指定encodingutf-8-sig否则第一列的表头可能会带一个不可见的BOM字符。这个细节容易让人误以为是pandas的问题其实是编码没有对上。5. 常见问题与排查技巧实录我在这个项目里踩过的五个坑5.1 请求被服务器识别为爬虫怎么办这是所有爬虫新手都会遇到的第一道坎。症状通常是代码跑着跑着返回的页面不再是正常的楼盘列表而是一张验证码页面或者访问过于频繁的提示。排查思路按优先级来检查Headers是否带上了完整的浏览器信息尤其是User-Agent和Referer检查请求频率是不是爬得太快了适当加大time.sleep的间隔检查是否触发了反爬升级如果是先用浏览器手动访问目标页面确认是否正常如果目标页面本身要求登录考虑使用Session维持登录态通过requests的get方法先访问一次首页再带着Cookie去抓列表页我用这套排查顺序解决过很多次类似问题。大多数情况下调整User-Agent和降低请求频率就能解决。如果还不行说明目标网站的风控规则比较复杂那就需要考虑更高级的方案了——但那就是另外一个话题了简易版先不涉及。提示千万别用代理池、分布式、并发刷这些手段去硬刚。且不说可能违反网站服务条款单说稳定性10个爬虫里有9个挂在这种硬刚上的。爬虫的目标是低成本拿到数据不是和服务器管理员斗智斗勇。5.2 字段提取不到或者解析出空列表另一个高频问题find_all返回空列表或者某个字段一直是None。出现这个情况90%的可能是指定页面的HTML结构和预期不一致。我通常会这样做第一步把当前页面的HTML保存下来用text resp.text然后open的方式写到本地或者直接用浏览器手动打开页面源码文件肉眼检查目标数据是否在HTML里。这一步的目的是区分问题在请求层面还是解析层面。第二步如果HTML里确实有数据但解析不到检查选择器的class名称是否写错了。HTML里的class可能存在多个值比如classitem-content clearfix单单匹配class_item-content是没问题的但如果写成了class_item-content clearfix就绝对匹配不到。用正则匹配class_re.compile(item)更有弹性。第三步如果HTML里根本没有数据那就可能是服务器做了动态渲染或者页面结构大改版。这种情况下只能回到Network面板重新分析数据接口看看有没有JSON接口可以替代HTML解析。有一个习惯值得养成写解析代码时先对拿到的soup做一次soup.prettify()输出到本地文件肉眼确认层级关系。很多新手一上来就对着语法文档写选择器结果写出的选择器和实际HTML结构牛头不对马嘴。先把结构看明白了再动手写代码效率会高很多。5.3 常用问题排查速查表我把这个项目里遇到过的问题整理成一个速查表方便大家遇到问题的时候对着查症状可能原因解决方案中文乱码编码推断错误设置resp.encoding resp.apparent_encoding返回验证码页面请求频率过高或请求头不像浏览器完善Headers加大请求间隔解析出来全是空列表页面的选择器失效保存HTML并检查结构调整正则/class匹配CSV在Excel里乱码编码方式不是utf-8-sig改用encodingutf-8-sig写入数据重复翻页URL有误或循环逻辑重复打印页码和URL检查循环边界某页请求超时服务器响应慢或IP被限制提高timeout、增加重试逻辑、适度降速项目突然无法访问网站改版或反爬策略升级重新分析页面更新选择器和请求策略这张表是我踩坑经验的浓缩。做爬虫项目过程中遇到的大部分问题根源都逃不出请求没做好和解析没做对这两个大方向按照这张表逐项排查基本上能解决80%的报错。5.4 一个容易被忽视的细节HTML结构变化导致的代码健壮性问题有一次我跑完整个脚本发现数据总量比预期少了将近四分之一。排查了很久才发现原来那天安居客在部分楼盘卡片上加了特价房标签的额外节点导致我原来用find(h3)取名称的时候有些卡片返回的却是这个特价标签里的文本。虽然不至于让程序崩掉但数据明显不对。这个经历提醒我一件事解析要尽量选稳定的祖先节点而不是绝对精确的位置。比如先定位到整个li.item卡片再在这个范围内查找h3、em等字段标签。这样即使卡片内部新增了一些干扰节点只要外层结构没变解析依然成立。这也是我在前面反复强调以li为解析单元的原因。6. 边界意识与后续扩展思路简易版的下一步在哪里6.1 数据抓取的自我约束说到这里有一件更重要的事必须正面讲清楚做数据抓取要有边界意识。这不是废话而是决定这个项目能不能长期放心的关键。我的原则很简单就三条。第一只抓公开信息不碰需要登录才能看的私有数据第二控制请求频率不给目标服务器增加明显负担第三抓下来的数据仅用于个人学习和研究不做商业转售或大规模对外分发。这三条原则听起来朴素但能守住它们就已经比很多把爬虫玩成攻击工具的人高出一个层次了。在实际做这个项目时我甚至刻意把并发控制在单线程、把每页间隔设置在2到4秒之间就是希望自己在技术上取得数据的同时也能在道德上站得住脚。网站上通常都公布了服务条款和使用协议动手写爬虫之前先花几分钟了解一下对方的合法边界是很划算的时间投资。以后再有人说爬虫就是随便爬你可以清楚明白地告诉他技术是工具合规使用是关键这才是成熟工程师的态度。6.2 从简易版到进阶版的扩展方向最后聊聊这个项目的成长路径。简易版的价值在于让你快速跑通全流程但如果你想把它变成一个真正顺手、能应对更多场景的工具有几个扩展方向可以参考第一加入重试和断点续传机制。当某页请求失败时不要整体中断而是把这个页号记录下来等主体循环跑完之后再单独重试。这个改进能显著提升长任务的鲁棒性。第二把爬虫脚本化、参数化。把城市、页码范围、保存文件名都变成命令行参数这样一来你想抓上海就抓上海想抓广州就抓广州脚本完全不用改。第三引入数据库存储。如果数据量大了CSV不够用可以把结果存入SQLite或者MySQL这样查询和去重都更方便。第四如果遇到动态渲染页面可以用Selenium或Playwright结合requests来完成更复杂的抓取。不过那是另一个量级的工程简易版先不用考虑。我认为最小可完成、能出结果的版本永远比一个设计完美但永远写不完的版本重要。先把这篇文章里的简易版跑通数据拿到手你已经有了一份能用的成果。后续想怎么扩展再按需求一步步来。最后的经验之谈我在这类小工具项目上的最大体会是成就感不是来自代码写得多优雅而是来自跑完一个脚本拿到一份可用的数据表格这种实实在在的结果。第一次全流程跑通、看到CSV里整整齐齐的楼盘信息时那种感觉比看十篇教程都来得踏实。如果你想练手不要看那么多教程了直接从这篇文章的代码开始改改到你真正能用为止。动手是学会爬虫唯一的路。