爬虫学习笔记这个系列第一篇我主要写的是urllib的安装、urlopen基本用法和read数据的姿势当时还留了个尾巴说请求传参、UA伪装、代理这些内容单独开一篇。现在这篇笔记就是把这三个躲不开的问题一次性讲透请求传参到底应该怎么传、User-Agent为什么一定要设置、IP代理又是怎么挂上去的。对于正在用Python写爬虫、又不太想一上来就上requests库的朋友这篇应该能帮你把底层逻辑理清楚看完之后你再回过头看requests会发现它内部玩的花样其实全是urllib这套东西。1. 笔记背景与调试环境准备1.1 为什么先学urllib网上大多数爬虫教程一上来就推荐requests说它比urllib好用不知道多少倍。这话没错但我个人的学习路径是先把urllib啃下来。原因很简单requests内部就是对urllib的封装你把urllib这套Handler、Request、Response的机制搞明白了requests里那些让人摸不着头脑的session、proxies、headers参数本质上都是在对相同的底层组件做批量设置。用urllib写爬虫代码会稍微啰嗦一点但每一步都有迹可循。比如你设置一个代理urllib要求你显式创建ProxyHandler再通过build_opener构造一个opener最后用这个opener去open。整个过程非常直白地告诉你HTTP请求在发出之前会经过一层层Handler的处理。这种“暴露过程”的学习方式对建立脑子里的请求模型特别有帮助。另外一点比较现实在服务器端看到urllib的默认User-Agent时几乎所有反爬策略都会直接拉黑。学着设置UA其实就是在学着理解HTTP请求头的作用这是后面做任何伪装、破解反爬、模拟浏览器操作的起点。所以这篇笔记写的都是最基础但最根上的东西。1.2 环境与调试小工具先说环境。我用的是Python 3.8urllib是标准库不需要额外安装直接import就能用。建议你也在虚拟环境里操作别把依赖搞乱了。命令行建环境这步略过不是这篇的重点。调试工具方面我强烈建议你准备两个一个能显示HTTP请求和响应详情的命令行工具比如curl方便做对照实验看看直接发请求和用Python发请求的差异在哪儿。一个能回显请求信息的公共HTTP测试服务。这类服务收到你的请求后会把请求头、请求参数、来源IP原封不动地返回给你是验证传参、UA、代理是否生效的神器。我在整篇笔记里都会用这类回显服务作为观察窗口因为爬虫最怕的就是“代码没报错但抓回来的数据不对”。有了回显服务你能直接看到服务器视角里的你长什么样非常直观。1.3 贯穿全文的模拟场景为了让内容不那么枯燥我虚构了一个场景来串联三种技术点假设有一个提供天气查询的开放接口模拟项目X它接受城市名和日期两个参数返回当日气温。再假设有一个需要提交表单才能登录的留言板模拟项目Y用来演示POST传参。整个笔记就是围绕这两个非常简单的模拟目标展开的。你先学会怎么把参数正确地交给服务器然后学会怎么让服务器觉得你是个正常访客最后学会怎么让请求从代理IP发出去。三个环节走下来一个勉强能用的基础爬虫就出来了。2. 请求传参的完整实现2.1 GET传参urlencode的编码原理GET传参的本质就是把参数拼在URL的问号后面。第一个念头当然是字符串拼接city 北京 url fhttp://example.com/weather?city{city}date20200503如果你真这么干大概率会得到一个乱七八糟的URL或者直接被服务器拒绝。我当时第一次跑就懵了URL看起来没什么问题为什么请求失败后来用回显服务一看才发现“北京”这两个字在URL里被原样塞进去了而URL规范里只允许ASCII字符中文必须先做百分号编码。手动百分号编码太蠢urllib里现成的工具就是urlencode。它接受一个字典自动把键值对拼接好同时把所有需要编码的字符做转换from urllib.parse import urlencode params { city: 北京, date: 20200503 } query_string urlencode(params) print(query_string) # 输出city%E5%8C%97%E4%BA%ACdate20200503city参数变成了一串%E5%8C%97%E4%BA%AC这就是“北京”两个字的UTF-8编码结果。服务器拿到后会按照同样的规范把它解码回中文。这个过程不用背知道原理就行反正urlencode会帮你处理好。完整请求代码如下import urllib.request from urllib.parse import urlencode base_url http://example.com/weather? params {city: 北京, date: 20200503} url base_url urlencode(params) req urllib.request.Request(url) resp urllib.request.urlopen(req, timeout5) print(resp.read().decode(utf-8))注意我构造的是Request对象再交给urlopen。你也可以直接把url字符串传给urlopen两者都行但前者是后面设置UA、Cookie的基础所以建议你从现在就习惯用Request。2.2 POST传参bytes与请求头GET传参适合查询类操作但登录、提交留言这种改变服务器状态的请求必须用POST。POST的参数不是放在URL里而是放在请求体body里。urllib的Request构造方法里有一个data参数只要给它传了值请求就会自动变成POST。但这个data必须是bytes类型不能直接传字符串或字典。所以参数要经历两步转换先urlencode变成标准查询字符串再encode成UTF-8编码的bytes。import urllib.request from urllib.parse import urlencode post_data urlencode({ username: test_user, message: hello world }).encode(utf-8) req urllib.request.Request( http://example.com/message, datapost_data ) resp urllib.request.urlopen(req, timeout5) print(resp.read().decode(utf-8))我为什么一开始总在这步翻车因为urlencode返回的是str直接塞给data参数会立刻报TypeError。后来养成了习惯任何发给data的参数先检查是不是str是str就先encode。这个习惯帮我省了很多排查时间。POST请求还有一个容易被忽略的地方Content-Type请求头。默认情况下如果不手动指定urllib不会帮你声明Content-Type。大部分表单处理程序都默认按application/x-www-form-urlencoded来解析请求体不声明有时候也能工作但某些严格的框架会认为请求体格式不合法直接返回400。所以我一般在构造Request时顺便把这块也补上req urllib.request.Request( http://example.com/message, datapost_data, headers{ Content-Type: application/x-www-form-urlencoded } )2.3 传参中的常见坑这部分是血泪经验按出现频率排的。第一个坑使用urlencode之后参数的顺序会按照字典的键名排序。如果你对顺序有严格要求那就别用字典直接用列表嵌套元组传进去。绝大多数接口对参数顺序不敏感所以这个坑踩到的人才少。第二个坑重复参数的情况。有些接口允许同一个参数名传递多次比如tagsatagsb。字典结构天然不支持这种场景list of tuples也支持。遇到这种需求用列表套元组的写法准没错。第三个坑请求体编码不一致。前端页面是GBK编码你强行用UTF-8的encode去传POST中文可能变成乱码服务器拿到后一脸懵。判断编码的方式是看目标页面HTML的charset声明页面上写GBK就用gb2312或gbk编码去encode最稳妥的办法是直接用cp936这个别名。这里多说一句GET和POST不是互相替代的关系它们有各自的语义。爬虫里你只需要记住查询类、无副作用请求用GET提交类、改变数据状态的请求用POST。这也是HTTP协议本身的设计意图按着这个规矩来能避开很多服务器端的校验。3. 请求头伪装User-Agent的细节3.1 服务器怎么判断你是“人”还是“程序”服务器识别爬虫的方法有很多但第一个也是最廉价的一个就是看User-Agent。这个请求头用来标识客户端的类型浏览器会带上完整的浏览器版本、操作系统、内核信息而Python的urllib默认会发送这样一串Python-urllib/3.8任何一个正常的反爬系统看到这串都会直接拒绝请求或者返回一个验证页面让你去点选图片。服务器端的逻辑很朴素真实用户不可能用“Python-urllib”这串字符串去浏览网页所以见到它就直接当成爬虫处理。想验证这个说法用我前面说的回显服务测试一下就知道了。不设置任何请求头直接请求回显服务显示你User-Agent是Python-urllib设置了Chrome的UA之后回显服务看到的就完全是浏览器访客的样子。UA为什么在一个小地方包含了这么多信息因为设备型号、操作系统版本、内核版本、浏览器版本都在这一串字符串里编码了。服务器可以用这串信息去适配不同的页面渲染方案也可以用来做最基本的客户端识别。所以做爬虫的时候UA是不是真实存在有没有明显的拼接痕迹非常容易被识别出来。3.2 UA的构造与轮换策略最简单的破解方法就是从浏览器里复制一份完整的UA字符串然后用headers参数传进Request对象import urllib.request headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 } req urllib.request.Request( http://example.com/weather?city北京, headersheaders ) resp urllib.request.urlopen(req, timeout5) print(resp.read().decode(utf-8))这里我直接用字符串拼接URL没走urlencode是因为中文在代码里手动拼进去也可以只是不够严谨正式应用请回到第二部分的正确姿势。UA不能一直固定用同一个。当你用同一个UA、同一个IP高频率去抓数据服务器很快会分析出“这个UA的访问行为不像人类”。人类的特征是浏览几个页面停留一阵再看别的爬虫的特征是短时间内获取几十上百个页面间隔固定甚至没有间隔。轮换UA的标准做法是准备一个UA列表每次请求随机选一个import random UA_LIST [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36, Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/75.0 ] ua random.choice(UA_LIST)千万别用随机乱拼的字符串比如“HackBrowser 9.9.9”这种编造的东西。某些WAF会维护已知爬虫工具的UA名单也会把明显不存在的UA当成恶意特征。用真实的浏览器UA字符串才是最好的伪装。3.3 不只是UA一份完整的请求头清单设置完UA你以为能高枕无忧了但服务器还可以看其他请求头来猜你。最典型的是Accept-Language。一个正常的浏览器访客大概率会带简体中文和英语的语言偏好设置一个Python脚本默认可能什么都不带或者用开发者的系统语言。我在调研中发现很多反爬系统重点看这几个头是否存在、是否合理Referer当前请求从哪个页面跳转过来的缺少Referer的请求像“凭空出现”容易触发怀疑。Accept浏览器能接受的内容类型一般是text/html、application/xhtmlxml这些。Connection是否保持连接浏览器一般带keep-alive。Accept-Language浏览器界面使用的语言中文浏览器一般是zh-CN,zh;q0.9,en;q0.8。把这几项统统补上之后请求头的完整度才像一个真正的浏览器。我通常准备一个默认的headers字典每次发起请求直接拿来用不用每次从头手写DEFAULT_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive }有一点要提醒Accept-Encoding这里有个暗坑。如果你声明了gzip压缩但urllib默认不会自动解压响应体你拿到的可能是压缩后的乱码。urllib不像requests那样自带解压功能。所以对新手阶段我建议先别加Accept-Encoding或者接受之后手动用gzip模块去解。不然又多了新的东西要处理。还有一点是Cookie。Cookie是维持登录状态的核心但写起来内容比较长而且跟登录流程强相关我这里不展开后续笔记会专门写Cookie和Session。这里先提一句如果你已经通过浏览器登录过某个网站可以把浏览器里的Cookie字符串直接复制到headers里伪装成已登录用户去发起请求。很多返回数据的变化根源就是缺了这步。4. IP代理的实现与代理池搭建4.1 代理的工作方式与适用场景IP代理在爬虫领域通常指的是正向代理。客户端不直接连目标服务器而是先把请求发给代理服务器由代理服务器转发给目标服务器目标服务器拿到的来源IP是代理IP不是你本机的IP。它的作用在爬虫里很明确当你用同一个IP高频抓取数据时服务器会从IP维度做频率统计超了就拒绝响应甚至封IP。代理把请求分散到多个IP上每个IP的访问频率都降下来了被限制的概率也就低了。另外一个作用是把本机真实IP藏起来减少个人信息暴露。需要说明一下我这里说的代理解决的是高频请求下的身份与频率问题是常规爬虫开发里的正当需求。使用代理去触犯他人服务条款或做违反法律法规的访问行为不在这篇笔记的讨论范围内。4.2 ProxyHandler与opener的配合urllib设置代理的方式比较独特它引入了Handler和Opener的概念。前面的基础用法中urlopen是一个快捷函数它背后其实是使用默认的OpenerDirector去执行了一次open。而build_opener允许你替换或追加Handler然后把自定义逻辑打包成一个opener对象。设置代理的代码很简单import urllib.request proxy { http: http://127.0.0.1:8080, https: http://127.0.0.1:8080 } handler urllib.request.ProxyHandler(proxy) opener urllib.request.build_opener(handler) resp opener.open(http://example.com/weather?city北京, timeout5)这里的代理格式是协议: 代理服务器地址。字典里的http和https两个key分别对应不同协议的目标请求走哪个代理。如果代理不需要用户名密码上面这种写法就够了。需要认证的代理把地址改成这样http://username:password127.0.0.1:8080看到build_opener是不是有点懵我当时也懵。换个角度理解Opener就是你的“请求执行器”Handler列表就是执行器的零件包。你往零件包里塞了代理处理器执行器干活的时候就会先把请求转交给代理处理器处理再发出去。想去掉代理就用不带ProxyHandler的opener或者用一个空ProxyHandler替换掉本来设置的那个。实际上urllib的urlopen本身是支持一个空代理配置来屏蔽环境变量的这里不深究知道opener是更灵活的用法就够用了。4.3 免费代理池的搭建与质量检测代理的坑比UA深多了尤其是免费代理。免费代理的来源我没法放具体站点你可以自己去搜“免费代理列表”这种关键词找到一些会实时更新代理IP的列表页。但免费代理的质量参差不齐十个里面有八个活不过一下午剩下两个还可能速度极慢。所以爬虫里常用套路是抓一批代理列表存下来然后自己写个探测器一个个测试可用性把能用的留下来放进代理池。探测器的核心思路特别简单拿这个代理去请求回显服务如果请求成功并且返回数据里的来源IP恰好是这个代理IP说明代理生效了。再加上一个超时时间超过3秒或者5秒就判定不可用import urllib.request import socket def test_proxy(ip, port, protocolhttp): proxy_url f{protocol}://{ip}:{port} handler urllib.request.ProxyHandler({ http: proxy_url, https: proxy_url }) opener urllib.request.build_opener(handler) try: # 这里用一个短超时太慢的直接淘汰 resp opener.open(http://example.com/ip, timeout5) if resp.status 200: text resp.read().decode(utf-8) print(f{proxy_url} 可用回显IP{text}) return True except Exception as e: print(f{proxy_url} 不可用{e}) return False这个探测器写的代码大概长这样。实际测的时候你会发现返回状态码200不代表一切正常因为有些代理会偷偷篡改响应或者返回一个验证页面让你填验证码。所以更可靠的判定方式是看回显服务返回的IP是不是你测试的那个代理IP是才是真正生效。免费代理池的维护是个体力活我建议你在爬虫脚本里加一个循环每隔几十秒重新扫描一批代理把失效的踢出去把新的加进来。这样至少能保证代理池不彻底断供。4.4 代理踩坑与策略代理这块的坑我列几个特别典型的。第一个是代理类型不匹配。HTTP代理只能代理HTTP协议的请求你拿HTTP代理去请求HTTPS的站点经常会遇到SSL握手失败或者连接被重置的报错。严谨点的做法是爬虫脚本里把http和https的代理分开配置测代理的时候也分协议测。第二个是代理返回验证页。好一点的代理商会直接转发原始内容烂一点的代理会在你请求的页面上插入自己的广告。这种“污染”很恶心你是看不出来的。解决办法是在解析阶段做内容校验如果发现页面结构和预期不符就换下一个代理重试。第三个是免费代理几乎都被用烂了。同一个免费代理IP可能同时被几十个人用访问量一高目标服务器早就把这个IP拉黑了你拿来用也访问不到目标。这就导致免费代理的成功率极低。应对策略就是加大样本量批量采集几百个代理轮流试总能遇到几个还能用的。要是经济允许直接买商业代理服务稳定性完全不是免费代理能比的。另外还有个小技巧代理不要全局使用。有些请求走公共API或者回显服务根本不需要代理套上代理反而慢一大截。我通常用opener来区分需要代理的请求走代理opener不需要的走带UA的默认opener两者互不干扰。5. 高频问题与排查方法速查5.1 抓包对比与代码定位写爬虫的时候代码报错反而不是最难排查的因为堆栈信息会直接告诉你哪一行出了问题。真正难的是代码不报错但返回结果不对比如返回了一个意料之外的HTML页面、一段JSON、一个验证码弹窗。我排这种问题的固定流程是三步先在浏览器里手动打开目标页面确认正常情况下应该长什么样。然后打开开发者工具的网络面板看浏览器发出的请求头、请求体和参数长什么样。最后把代码里的请求方式和浏览器里的做对比找差异。这里强烈建议用回显服务来做对照实验它会把你发的请求完全展开给你看。如果你在回显服务里发的请求跟浏览器请求基本一致说明代码没问题问题出在目标服务器的反爬策略上。如果差异很大那就继续调代码。开发者工具里面“复制为cURL”的功能也很常用能把浏览器当前请求完整转成一条cURL命令你拿到命令行里执行一下再看它跟代码版差在什么地方。这种对比做多了对HTTP请求的理解会突飞猛进。5.2 常见报错速查表把我自己实战中撞见过的报错整理成检阅用的速查表现象可能原因解决方向HTTP Error 403缺少UA或UA被识别为爬虫设置完整浏览器请求头检查IP是否被限制HTTP Error 429请求频率太高触发限流降低抓取频率换代理加随机延时HTTP Error 503服务器过载或主动拒绝等一段时间再试检查IP是否被封URLError: urlopen error [WinError 10054]对方主动断开连接重试换代理检查是否触发反爬参数传了但服务器不识别GET参数没编码 / POST没转bytes用urlencode处理data参数记得encode中文变乱码编码格式不匹配检查目标页面charset统一编码返回内容与浏览器不一致缺少Referer或者Cookie补齐请求头带上已登录CookieSSL certificate verify failedHTTPS证书校验失败临时用context忽略校验但生产环境不建议最后一行提到SSL证书校验问题简单解释一句urllib默认会校验证书碰到某些自签名证书或者公司内部系统会直接中断请求。测试阶段可以用SSLContext临时跳过校验但正式爬虫别这么干风险很大。这个属于进阶用法知道有这回事就行。5.3 爬虫的边界意识技术能力归技术能力对待目标网站的态度决定了你能走多远。我自己的习惯是开工之前先看一眼对方的robots.txt尽量爬那些明确允许抓取的内容也严格遵守对方公开接口的使用频率限制。抓取公开数据不算什么见不得人的事但任何事情一旦把对方的服务器拖垮性质就变了。控制频率的方法很简单每次请求之间加一个随机延时比如time.sleep(random.uniform(1, 3))。这既降低了服务器压力也让你的访问模式更接近人类。高并发抓取并不是爬虫能力的体现能稳定、克制地拿回数据才是真正考验架构水平的地方。把这些边界想清楚再回头去看IP代理、UA轮换这些技能你就知道它们存在的意义是“让请求更规范地分散负载”而不是为了搞破坏。这个认知摆正了后面学任何爬虫进阶技术都不会跑偏。整理这篇笔记时我最大的感受是urllib虽然写起来啰嗦但它把HTTP客户端的那套流程完全暴露在你面前请求头、参数处理、代理配置这些概念在urllib里都有清清楚楚的对应步骤。等你把这篇笔记里的例子全部跑通再去看requests库的文档你会发现大量概念瞬间就对上了号。我实操中还养成了一个比较受用的习惯笔记本里专门有一个“报错速查”分区每次撞见新报错就把它连同上下文抄进去。现在很多问题我都不用去搜索直接翻自己的笔记就能找到答案。如果这篇笔记对你有用下一步我准备把Cookie、Session和登录态保持这块写完这也是爬虫进阶最绕不开的下一道坎。