ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python爬虫入门实战:用requests和正则抓取安居客新房信息

Python爬虫入门实战:用requests和正则抓取安居客新房信息 先交代一句这篇博文写的是个人练手项目代码和思路仅用于学习交流抓取前请先确认目标网站的服务条款和robots协议控制频率、别给服务器添堵。下面进入正题。年初帮家里人看房发现手动翻安居客的新房列表特别费劲——今天看过的盘明天又要重新翻一遍价格、户型、面积这些信息散落在各个页面里想做个横向对比得靠复制粘贴效率低到让人怀疑人生。于是花了一个周末用Python写了个“能跑就行”的简易版数据抓取脚本目标只有一个把指定城市的新房列表页里的楼盘名、均价、户型、地址、标签这几项关键信息自动抓下来存成CSV方便用Excel做筛选和对比。整个项目没有用Selenium没有用Scrapy核心工具就是requests加正则表达式加上标准库里的csv和time总共一百多行代码。它解决的是“轻量、快速、够用”的问题——你不是要做大规模数据平台你只是想把某个页面的表格信息变成一张自己能用的表。这篇文章会完整拆解我的设计思路、页面分析过程、代码实现和调试时踩过的坑适合刚学完Python基础、想拿真实网站练手的人直接参考。1. 整体设计思路与技术选型1.1 为什么不用Selenium全家桶很多人一提到爬虫第一反应就是上Selenium觉得能模拟浏览器就万事大吉。但在这个项目里Selenium是明显杀鸡用牛刀——它的启动速度慢、内存占用高还得额外维护浏览器驱动版本为了抓一个静态列表页完全不值得。我实测过Selenium启动Chrome差不多要两三秒加上页面渲染等待一个页面跑下来五六秒很正常而用requests直接请求同样的页面基本在一秒内完成十页数据也就是十几秒的事。当然Selenium有它不可替代的场景比如页面内容靠JavaScript动态渲染、需要模拟点击翻页、要绕过复杂的交互验证。但安居客的新房列表页恰好是服务端渲染的HTML里直接带着完整的数据这就给轻量方案留足了空间。先打开浏览器开发者工具看一眼页面源码确认数据在不在HTML里再决定用什么方案这是爬虫项目选型的第一步。1.2 requests加正则的轻量组合requests负责把网页源码拿回来正则表达式负责从源码里把目标数据抠出来。很多人觉得正则难写、难调试但在处理结构比较规整的HTML时它反而是最快的工具——不需要解析DOM树不需要关心标签嵌套只要目标字段附近的HTML特征足够稳定一行正则就能解决问题。在这个项目里每个楼盘的基本信息都被包在div classitem-mod这个容器里楼层、户型、面积、地址这些字段各有固定的class名。比如楼盘名在span classitems-name里均价在span classprice-num里。这种结构下直接用正则从整个页面源码里圈出每一个楼盘块再从每个块里提取具体字段逻辑非常清晰。选择正则还有一个现实原因目标站点的页面结构偶尔会调整但大部分字段的class命名规律都保留着出问题时定位代码、改正则的成本比重新调试一套XPath解析逻辑要低得多。后面我会详细说正则和XPath的取舍。2. 页面分析与数据点定位2.1 找到目标页面与关键参数开始写代码之前先在浏览器里手动打开目标城市的安居客新房频道按F12进入开发者工具切到Network面板刷新页面找到第一个文档请求看它的响应内容。这一步的目的是确认html里是否直接包含楼盘数据。我以北京为例新房频道首页地址是https://beijing.anjuke.com/fangjia/之类但实际列表页通常长这样https://beijing.anjuke.com/loupan/?fromnavigation。打开后往下滚动能看到一整个楼盘列表每个楼盘一个卡片块。我随机右键一个楼盘名选择“检查”定位到对应的HTML元素发现结构大概是div classitem-mod a classlp-name hrefhttps://beijing.anjuke.com/loupan/xxx.html span classitems-name某某楼盘/span /a p classaddress北京市大兴区XX路与XX街交汇处/p span classprice-num58000/span span classunit-price元/㎡/span div classtags span近地铁/span span品牌房企/span /div p classarea3室/89㎡/p /div这个结构就是整个项目的地基。你不需要理解每一行HTML只需要确认两件事第一数据确实在HTML源码里第二目标字段的class名称是稳定且有规律的。这两点确认后后面的正则提取就是水到渠成的事。还有一个关键参数在这个页面里藏着翻页时URL会多出p参数比如https://beijing.anjuke.com/loupan/p2/。把这个规律找到后循环页码就只需要改一个数字。2.2 用正则把字段从一个HTML块里抠出来拿到页面结构后第一版提取方案是先整页匹配所有楼盘块再逐块提取字段。这里有一个很关键的经验不要试图用一个超级复杂的正则把整页所有字段一次性提出来那样出错时你根本不知道是哪里匹配失败了。正确的做法是分两步走——先用一个范围正则切出每个楼盘的HTML片段再在小片段里做细粒度提取。切分楼盘块的正则长这样pattern_block re.compile(rdiv classitem-mod(.*?)/div, re.S) blocks pattern_block.findall(html)re.S这个flag必须加它让.能匹配换行符。因为一个楼盘块的HTML可能跨多行不加这个flag匹配就会失败。提取出来之后每个block就是一段独立的楼盘HTML接下来在block里找字段就简单多了name re.search(rspan classitems-name(.*?)/span, block) price re.search(rspan classprice-num(.*?)/span, block) address re.search(rp classaddress(.*?)/p, block)如果某个字段提取不到最可能的原因是页面结构调整或者该楼盘信息不完整。我的处理方式是写一个小函数匹配不到就返回空字符串而不是让整个程序报错退出——爬虫项目里单个数据缺失是常态程序要能容忍“脏数据”。2.3 去重与后续扩展列表页存在重复数据的情况不少比如同一楼盘出现在不同的推荐位或者多页列表之间存在交叉。因此我在代码里加了一个简单的去重逻辑维护一个已经见过的楼盘名集合新抓到的楼盘名如果已在集合里就跳过。这个逻辑简单、有效而且不依赖任何第三方库。至于后续扩展比如想抓详情页的更多信息思路也很直接从列表页拿到每个楼盘的详情链接lp-name里那个href然后对详情页重复同样的“请求——解析——提取”流程。这个项目先不做详情页但链接字段我有专门提取并保存下来方便以后扩展。3. 代码实现与运行流程3.1 完整代码下面是这个简易版爬虫的完整代码。我刻意把逻辑写得线性、直接没有封装成类就是为了让刚入门的人能一眼看懂主流程。所有关键步骤都加了注释直接复制到本地把city_url换成你想抓的城市列表页地址就能跑起来。import requests import re import csv import time 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, Referer: https://www.anjuke.com/, Accept-Language: zh-CN,zh;q0.9, } # 北京新房列表页翻页通过 p 参数控制 base_url https://beijing.anjuke.com/loupan/p{page}/ pattern_block re.compile(rdiv classitem-mod(.*?)/div, re.S) pattern_name re.compile(rspan classitems-name(.*?)/span) pattern_price re.compile(rspan classprice-num(.*?)/span) pattern_address re.compile(rp classaddress(.*?)/p) pattern_rooms re.compile(rp classarea(.*?)/p) pattern_tags re.compile(rdiv classtags(.*?)/div, re.S) pattern_tag re.compile(rspan(.*?)/span) pattern_link re.compile(ra classlp-name href(.*?)) def get_html(url): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text except Exception as e: print(f[请求失败] {url} - {e}) return def extract_field(block, pattern): m pattern.search(block) return m.group(1).strip() if m else def clean_tag_text(raw): # 把标签块里的HTML标签去掉只留文本 s re.sub(r.*?, , raw) s s.replace(\n, ).replace( , ) return s def crawl_page(page): url base_url.format(pagepage) html get_html(url) if not html: return [] blocks pattern_block.findall(html) result [] for block in blocks: name extract_field(block, pattern_name) if not name: continue price extract_field(block, pattern_price) address extract_field(block, pattern_address) rooms extract_field(block, pattern_rooms) tags_raw extract_field(block, pattern_tags) tags clean_tag_text(tags_raw) if tags_raw else link extract_field(block, pattern_link) result.append({ 楼盘名: name, 均价: price, 户型面积: rooms, 地址: address, 标签: tags, 链接: link, }) return result def main(): all_data [] seen set() # 抓第1到第5页演示用想抓更多就改 range for page in range(1, 6): print(f正在抓取第 {page} 页...) page_data crawl_page(page) for item in page_data: if item[楼盘名] in seen: continue seen.add(item[楼盘名]) all_data.append(item) print(f第 {page} 页抓到 {len(page_data)} 条累计 {len(all_data)} 条) time.sleep(2) # 礼貌延时别给服务器添堵 if not all_data: print(没有抓到任何数据请检查页面结构或请求头) return with open(anjuke_newhouse.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[楼盘名, 均价, 户型面积, 地址, 标签, 链接]) writer.writeheader() writer.writerows(all_data) print(f完成共保存 {len(all_data)} 条数据到 anjuke_newhouse.csv) if __name__ __main__: main()3.2 代码关键点解析有几个地方值得单独拿出来讲一讲。第一是请求头的Referer字段。很多网站会校验请求来源如果直接请求列表页而Referer为空或者来自搜索引擎服务器可能会返回异常页面。我虽然不确定安居客对Referer的校验有多严格但把浏览器的常规请求头完整带上是每个爬虫项目都应该养成的习惯成本几乎为零却能减少很多不必要的麻烦。第二是resp.encoding utf-8。有些库会根据响应头猜编码猜错时中文会变成乱码。强制指定utf-8能避免大部分乱码问题。如果你抓的页面实际是gbk编码这里就要改成gbk怎么判断打印一小段源码出来看中文是否正常即可。第三是保存CSV时用了encodingutf-8-sig。这个细节很关键用普通的utf-8保存Excel直接打开会乱码而utf-8-sig会在文件开头写入BOM标记Excel就能正确识别编码。这是我在实际使用中被坑过一次才记住的。第四是time.sleep(2)。每抓一页歇两秒这个延时不是为了装样子而是对目标服务器最基本的尊重。频率太高容易被封IP封了之后不仅这个项目跑不了还可能影响你正常浏览网站。后面我会专门说频率控制的问题。3.3 运行流程与结果代码的运行流程很直接main函数从第1页循环到第5页每页调用crawl_page抓取并解析解析结果先查重再存进总列表最后统一写入CSV。实际运行效果大概是这样的输出正在抓取第 1 页... 第 1 页抓到 20 条累计 20 条 正在抓取第 2 页... 第 2 页抓到 20 条累计 40 条 ... 完成共保存 100 条数据到 anjuke_newhouse.csv生成的CSV在Excel里打开后就是一列楼盘名、一列均价、一列户型面积清清楚楚。我当时拿这个表做了一个简单的筛选均价在5万以下、有“近地铁”标签的楼盘。整个过程不到一分钟比手动翻网页舒服太多了。4. 常见问题与排查实录4.1 请求被拒或返回验证页面我把代码跑通之后第二天再运行突然发现抓不到任何数据打印出来的HTML源码里全是登录框和验证码。这种情况大概率是IP被临时限制或者触发了网站的反爬策略。排查思路是先在浏览器里手动访问目标页面看能否正常打开。如果能打开说明内容没问题问题在请求环节——试着更换User-Agent、降低抓取频率、增加随机延时。如果换UA还不行就考虑用代理IP但这就超出“简易版”的范畴了我建议先休息一段时间再继续抓。爬虫项目里反爬对抗的本质是“别让对方觉得你是机器”频率控制永远是最重要的手段。4.2 字段提取为空或格式错乱如果CSV里某些行的“均价”是空的先别急着改代码。用浏览器打开对应的楼盘详情页检查列表页里这个楼盘是不是本身就没展示价格。新房列表页里“价格待定”的楼盘很常见这种楼盘在HTML里根本没有price-num这个节点所以正则匹配不到是正常的返回空字符串符合预期。真正需要警惕的是另一种情况某一类字段大面积为空。这说明页面结构可能变了比如class名改了或者整个列表的DOM结构换了。这种时候要做的是回到浏览器里重新检查元素对比之前记录的结构差异针对性更新正则即可。没有一劳永逸的爬虫页面改版是家常便饭做好定期维护的心理准备。4.3 保存CSV后乱码CSV用Excel打开乱码几乎都是编码问题。我在前面已经提过用utf-8-sig这里再补充一个细节如果你是用记事本或者VS Code打开CSVutf-8-sig显示也完全正常不会出现奇怪的字符。所以这个编码可以无脑用。还有一个容易忽略的问题如果某个字段内容里本身包含逗号或者换行直接用csv.DictWriter写入是没问题的库会自动处理引号转义。但如果你是自己用字符串拼CSV就一定要小心逗号否则列会对不齐。这也是我建议用csv库而不是手动拼字符串的原因。4.4 抓取频率与合规性这个项目只是个人学习练手抓的是公开的列表信息量也很小但我还是想多说两句合规性的事。爬虫不是不能写关键要记住三条底线第一遵守robots协议和网站服务条款如果站方明确禁止抓取就别碰第二控制频率和总量不要对目标服务器造成压力第三抓到的数据不要用于商业用途不要批量转售尤其是涉及个人隐私的数据碰都不要碰。把这些原则放在心里写爬虫会踏实很多。我见过有人为了跑数据把人家网站打到宕机的那已经不是技术问题是法律问题了没必要。写在最后这个项目最让我受益的一点是它把Python基础语法里的requests请求、正则表达式、文件读写、循环和异常处理全部串起来用在一个真实场景里。你可以在课本里学一万遍正则语法不如亲手写一个re.search从一个HTML块里抠出楼盘名来得印象深刻。学习爬虫的最好方式就是找一个结构规整的静态页面像我这样反复练习。还有一个小技巧送给你写这类解析代码时不要一上来就写完整版。先把HTML保存到本地文件用脚本读本地文件做解析调试通过后再换成requests在线抓取。这样既能节省请求次数、降低被封风险又能大幅提高调试效率。我后来做所有解析类脚本都沿用这个习惯可以说屡试不爽。
返回列表