写爬虫这件事,爬虫很多人以为门槛在代码。爬虫其实不是爬虫。真正卡住新手的爬虫是目标网站的反爬策略,以及你自己对HTTP协议的爬虫理解程度。我见过太多人照着教程敲完requests.get(),爬虫一跑就报403,爬虫然后卡在那里不知道下一步该干嘛。爬虫
先说说最基础的爬虫场景。假设你要抓一个静态博客的爬虫文章列表,用requests加BeautifulSoup就够了。爬虫代码大概长这样:
import requests from bs4 import BeautifulSoup url = "https://example.com/blog" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} resp = requests.get(url,爬虫 headers=headers) soup = BeautifulSoup(resp.text, "html.parser") for item in soup.select(".post-title"): print(item.get_text(strip=True)) 这段代码能跑通的前提是:网站没有做任何反爬。但现实是爬虫,稍微有点规模的爬虫站点都会检查User-Agent,甚至校验Referer和Cookie。爬虫我上个月想抓某技术论坛的帖子标题,不带UA直接请求,返回的是空列表。加上UA之后,前20条出来了,第21条开始全是重复内容——人家做了分页懒加载,数据根本不在初始HTML里。
这时候就得换思路。打开浏览器的开发者工具,切到Network面板,刷新页面,找XHR或Fetch请求。你会发现真正的数据接口往往返回JSON,比解析HTML省事得多。比如那个论坛,实际请求的是/api/posts?page=2&limit=20,带上正确的Referer就能拿到干净的数据。用requests直接请求这个接口,连BeautifulSoup都省了。
但别高兴太早。有些接口带签名参数,比如sign=md5(timestamp+secret)。你翻遍前端JS也找不到secret,因为它可能藏在某个.js文件里,被混淆过。我试过用js2py执行那段混淆代码,结果卡在环境检测上——代码里判断了window对象是否存在。最后用playwright开无头浏览器,让页面自己跑完JS,再拦截网络请求拿数据。慢是慢了点,但稳定。
说到playwright,它比selenium轻量,API也更现代。启动一个浏览器实例大概两秒,之后每个页面可以复用上下文。有个坑要注意:默认的headless=True模式会被某些网站识别出来。加个--disable-blink-features=AutomationControlled参数,再配合context.add_init_script()覆盖navigator.webdriver属性,基本能绕过大部分检测。不过道高一尺魔高一丈,Cloudflare的Turnstile验证就不是靠改几个属性就能过的。
频率控制是另一个容易翻车的地方。我写过一个抓取电商价格的脚本,一开始没加延迟,跑了三百多个请求后IP被封了24小时。后来改成每请求间隔random.uniform(1.5, 3.5)秒,再挂上retry装饰器处理超时,连续跑了一周没出问题。别迷信代理池,免费代理十个里九个不能用,付费的也得挑,有些机房IP段早被拉黑了。
数据存哪儿也值得琢磨。小规模用csv或json文件就行,超过几万条建议上sqlite。我习惯用sqlalchemy建表,字段类型明确,后面做去重和增量更新都方便。比如抓新闻列表,用URL的MD5当主键,INSERT OR IGNORE一执行,重复数据自动跳过。
最后说个真实教训。有次我抓一个政府公开数据网站,页面结构规整,请求也没难度,半小时就写完了。跑完发现数据对不上——网页上显示的是“截至2024年3月”,接口返回的却是2023年的旧数据。原来前端做了缓存,接口有?v=参数控制版本。这种问题跟技术无关,纯粹是没仔细看请求参数。爬虫写多了你会明白,代码只占三成,剩下七成是观察、试错和跟网站斗智斗勇。