ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python爬虫入门:urllib与requests核心用法详解

Python爬虫入门:urllib与requests核心用法详解 我做了五年爬虫开发带过不少新人发现大多数人入门时都卡在同一个问题上想爬的东西太多结果连最基本的网络请求都没搞明白。所以这次我打算写一个系列从零开始带你把爬虫这条路走通。第一篇先讲最基础也最关键的东西——urllib和requests这两个库就是爬虫世界的两条腿没有了它们后面所有花里胡哨的操作都白搭。这篇文章不会跟你扯太多虚的我会直接告诉你这两个库到底怎么用、它们之间有什么区别、以及你写第一个爬虫时会遇到哪些坑。看完这篇你就能写一个像模像样的爬虫程序了——哪怕你今天才装好Python只要跟着敲代码也能跑起来。1. 爬虫到底是什么先搞懂你每天都在干的事很多人一提爬虫就觉得高深莫测什么分布式、逆向、JS加密听起来像黑客技术。其实爬虫的本质特别朴素——就是模拟浏览器向服务器发请求然后把返回的数据解析出来。你平时用浏览器打开一个网页浏览器干了两件事先把请求发到服务器再把服务器返回的HTML代码渲染成好看的页面。爬虫做的事情跟浏览器几乎一样只不过它不渲染页面而是直接拿原始数据来用。举个最直白的例子你想查某个商品的价格人工操作是打开浏览器、输入网址、看页面爬虫的操作是发一个HTTP请求、拿到返回的HTML、用代码把价格字段提取出来。省去了人工点击的过程还能一次性处理几百上千个页面。1.1 一次HTTP请求的完整过程理解了这点之后我们再往深一点看。一次HTTP请求本质上就是客户端你的代码和服务器之间的一次对话整个过程分为四个阶段建立连接你的客户端通过DNS解析域名拿到服务器IP然后建立TCP连接如果是HTTPS还需要TLS握手加密。发送请求客户端把请求头Headers、请求方法GET/POST、请求参数等内容打包发送给服务器。服务器处理服务器收到请求后根据请求内容查数据库、读文件、调用其他服务然后生成响应内容。返回响应服务器把状态码200表示成功404表示找不到403表示禁止访问等、响应头、响应体一般是HTML或JSON返回给客户端。你的爬虫代码核心工作就是控制第二步和接收第四步。urllib和requests这两个库就是帮你完成这个过程的工具包。1.2 为什么必须先学urllib再学requests你可能会问既然有更强大的requests为什么还要学urllib这个问题我以前带新人的时候总会反复强调。urllib是Python标准库也就是说你装完Python它就自带不需要额外安装任何东西。这意味着无论你在什么环境、什么机器上只要有Python就能用urllib写爬虫。而requests是第三方库需要手动安装在某些无法联网、或者环境受限的服务器上安装第三方库可能会比较麻烦。更重要的是requests底层其实封装了urllib。它的很多设计思路都源自urllib只是把繁琐的操作简化了。先学urllib再学requests你会明白requests为什么那样设计遇到复杂需求时也能看懂错误信息——很多requests报错最终都指向urllib3的底层实现。所以顺序很重要先用urllib打基础再用requests提高效率。两者都会用才是爬虫工程师的完整技能。2. 环境准备别在起跑线上卡住2.1 Python版本的选择与安装我见过太多人在环境上浪费了三四天。这里直接给你结论装Python 3.8以上的稳定版本比如3.10或3.11不要追最新版也不要死守Python 2。为什么强调这点因为Python 2在2020年就停止维护了很多新库根本不支持而最新大版本刚发布时第三方库兼容性往往跟不上。选个稳定版本至少能用两年不用折腾。安装时有一个极其关键的勾选项——在Windows安装向导的第一个界面一定要勾选Add Python to PATH。这一步不勾你后面在命令行敲Python会提示不是内部或外部命令新手在这一步卡住的比例非常高。装完之后验证一下打开命令行CMD或PowerShell输入python --version如果显示Python 3.10.x之类的版本号说明装好了。2.2 编辑器推荐VS Code比PyCharm更适合新手编辑器方面我给个比较中肯的建议新手优先用VS Code而不是很多人推荐的PyCharm。理由有三点第一VS Code轻量打开速度快PyCharm启动要等半天第二VS Code配置Python环境更直观第三VS Code是免费的而PyCharm专业版要收费虽然社区版够用但新手面对两个版本的选项容易懵。VS Code装好后装一个Python官方扩展然后在项目文件夹里按Ctrl Shift P输入Python: Select Interpreter选中你装的那个Python版本即可。之后写代码就能有语法高亮和自动补全了。顺便说一下我在实际开发中也是用VS Code为主PyCharm反而用不惯——个人习惯问题但对新手来说VS Code的学习曲线确实更平缓。2.3 安装requests库环境准备好后先安装第一个第三方库。打开命令行执行pip install requests如果网速慢或者安装失败可以换成国内镜像源pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple这里解释一步pip是Python的包管理工具装库全靠它。当你运行python --version没问题时pip应该也自带了如果提示pip不存在就重新把Python安装一遍大概率又是PATH的问题。至此环境准备完毕我们可以开始写人生第一个爬虫了。3. 用urllib写你的第一个爬虫3.1 最简单的请求抓取一个网页现在开始动手。创建一个Python文件比如first_spider.py输入以下代码import urllib.request # 目标网址 url https://example.com # 发送请求并获取响应 response urllib.request.urlopen(url) # 读取响应内容bytes类型 html_bytes response.read() # 解码成字符串通常是utf-8但有时是其他编码 html_str html_bytes.decode(utf-8) # 打印前500个字符 print(html_str[:500])这段代码做了四件事导入库、指定网址、发请求、读内容。跑一下试试如果你能看到以!doctype html开头的HTML代码恭喜你——你的第一个爬虫诞生了。这里有个细节需要注意response.read()返回的是字节类型bytes所以要用decode(utf-8)转成字符串。如果解码报错说明网站的编码不是utf-8常见的有gbk和gb2312很多中文老网站还在用可以改成对应的编码或者更稳妥的做法是在响应头里查编码# 获取响应头里的编码信息 content_type response.headers.get(Content-Type) print(content_type) # 可能会输出text/html; charsetutf-8看到charset后面的值就是正确的编码方式。这个细节特别实用我后面处理中文网站时经常用它来避免乱码问题。3.2 带上请求头伪装成浏览器你按照上面的代码抓取https://example.com肯定没问题但如果你把网址换成百度或其他大型网站大概率会收到错误。原因很简单服务器识别出你不是真实浏览器直接拒绝了请求。不同的客户端在发起HTTP请求时会带一个User-Agent头告诉服务器我是谁。浏览器发请求时会被识别为Chrome、Firefox等而urllib默认的User-Agent是Python-urllib/3.x服务器一看就知道是程序在访问自然就拒绝了。解决办法是自己在请求头里伪装成浏览器。先来理解一下什么是User-Agent它就像你的身份证服务器通过它来区分访问者类型。很多网站对非浏览器UA一律拒绝或验证。修改代码如下import urllib.request url https://httpbin.org/get # 构造请求头伪装成Chrome浏览器 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 } # 创建Request对象把请求头挂上去 req urllib.request.Request(url, headersheaders) # 发送请求 response urllib.request.urlopen(req) # 读取并解码 print(response.read().decode(utf-8))这里引入了一个新概念——Request对象。之前我们是把URL直接传给urlopen()现在改成先创建一个Request对象把URL和请求头都塞进去再传给urlopen()。好处是请求信息更丰富也更灵活。我建议你把这个User-Agent存到一个常量里后续每个爬虫都能复用USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.363.3 设置超时避免程序卡死另一个新手容易踩的坑是超时问题。如果你请求的网站比较慢或者干脆无法访问urlopen()会一直等着你的程序就卡在那里不动了不知道是网络慢还是代码错了。解决方法是加一个timeout参数response urllib.request.urlopen(req, timeout10)这个参数的单位是秒含义是最多等待10秒。如果10秒还没收到响应就会抛出异常。你可以捕获这个异常做更友好的处理import urllib.error try: response urllib.request.urlopen(req, timeout10) print(response.read().decode(utf-8)) except urllib.error.URLError as e: print(请求失败, e) except TimeoutError: print(请求超时请检查网络或目标网站状态)这段代码的处理逻辑是先尝试请求遇到超时或URL错误就打印提示信息而不是让程序崩溃。这是爬虫工程化的第一步——学会处理异常而不是让程序一遇错就停。3.4 URL编码处理参数中的中文和特殊字符现在你已经能抓取静态网页了但爬虫经常需要带参数请求比如搜索功能。你可能会直接拼URLurl https://www.baidu.com/s?wd爬虫入门直接把中文拼在URL里大概率会失败或乱码。因为URL标准只允许ASCII字符中文和空格、特殊符号都必须经过编码。urllib提供了urlencode方法专门处理这个问题from urllib import parse # 定义查询参数 params { wd: 爬虫入门, page: 1 } # 编码成 query string query_string parse.urlencode(params) print(query_string) # 输出wd%E7%88%AC%E8%99%AB%E5%85%A5%E9%97%A8page1 # 拼接到基础URL后面 base_url https://www.baidu.com/s full_url base_url ? query_string print(full_url)urlencode的作用是把字典转成URL合法的查询字符串中文会自动变成百分号编码。这个函数你会用得非常频繁尤其是在构造搜索类爬虫时。如果你只需要编码单个值也可以直接用quotefrom urllib import parse encoded parse.quote(爬虫入门) print(encoded) # 输出%E7%88%AC%E8%99%AB%E5%85%A5%E9%97%A83.5 urllib的局限为什么还要学requests看到这里你可能会觉得urllib也挺好用的。但它其实有很多不便之处我用了一个月urllib之后才真正体会到第一每次设置请求头都要手动创建Request对象代码很繁琐。如果要做POST请求你还需要手动编码表单数据再塞到Request里写五六个API的时候你会崩溃。第二urllib处理cookies、session等会话信息特别麻烦需要手动维护CookieJar对象。第三urllib获取状态码、编码信息等不够方便都是底层操作。简而言之urllib功能全但封装不够适合做底层学习和一般性请求。真实项目中的爬虫绝大多数用的是requests。我这就好好讲讲它。4. 用requests写一个更优雅的爬虫4.1 requests库的安装与体验先说安装方式在命令行执行pip install requests装好后你就可以感受一下它的简洁了。同样的请求requests写起来是import requests url https://httpbin.org/get 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 } # 一行搞定发送get请求带请求头最长等10秒 response requests.get(url, headersheaders, timeout10) # 直接输出文本内容 print(response.text)对比一下urllib的写法import urllib.request url https://httpbin.org/get 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 } req urllib.request.Request(url, headersheaders) response urllib.request.urlopen(req, timeout10) print(response.read().decode(utf-8))两段代码的差距一目了然。requests的设计哲学就是用最少代码完成最多功能很多时候一行requests.get()就干了urllib五行的活。4.2 深入理解requests.get的每个参数requests.get()是你在爬虫生涯中最常用的函数没有之一。它的完整签名长这样requests.get(url, paramsNone, headersNone, cookiesNone, authNone, timeoutNone, proxiesNone, verifyTrue, allow_redirectsTrue)逐个来说我经常用的几个params自动拼接URL参数之前在urllib里URL参数编码要用urlencode再手动拼接。requests直接支持用字典传参import requests url https://httpbin.org/get params { wd: 爬虫入门, page: 1, sort: latest } # requests会自动把params编码并拼接到URL末尾 response requests.get(url, paramsparams, timeout10) print(response.url) # 输出https://httpbin.org/get?wd%E7%88%AC%E8%99%AB%E5%85%A5%E9%97%A8page1sortlatest我特别喜欢这个设计因为你不用关心编码细节requests全帮你处理了还能在response.url里看到最终拼接后的完整URL方便调试。timeout必须养成的习惯我再说一次因为太重要了生产环境的每个请求都要设置timeout。5秒、10秒都可以但不设timeout的代码就是一颗定时炸弹随时可能卡死整个程序。当你爬几百个页面时如果某个网站挂了不设timeout意味着你的程序停在那里后面的几百个页面全部白等。verify遇到SSL证书错误时的处理有些网站尤其是小众站的SSL证书配置有问题requests默认会验证证书然后就报错。解决办法是把证书验证关掉response requests.get(url, verifyFalse, timeout10)同时会伴随一个警告可以忽略或过滤掉。不过这里强调一句verifyFalse适合自己学习调试用如果做正式项目还是应该把证书链配好或者至少在可信环境中使用避免中间人攻击的风险。proxies代理池的入口爬虫爬多了你的IP会被目标网站封禁这时候就需要用代理。requests的proxies参数专门干这个proxies { http: http://127.0.0.1:7890, https: http://127.0.0.1:7890 } response requests.get(url, proxiesproxies, timeout10)这个参数后期对接代理池非常方便换IP只是改一个字典的事情。4.3 状态码管理不要盲目的print现在你已经能发请求了但优秀的爬虫程序必须有状态码判断。状态码是服务器给你的响应状态标志2xx表示成功4xx表示客户端错误如404找不到、403禁止访问5xx表示服务器错误如500内部错误。requests提供了response.status_code属性和response.raise_for_status()方法import requests url https://httpbin.org/status/404 response requests.get(url, timeout10) print(f状态码{response.status_code}) # 自动抛出异常遇到4xx/5xx就报错 try: response.raise_for_status() except requests.exceptions.HTTPError as e: print(HTTP请求出错, e)这些判断非常重要因为很多网站当你访问过于频繁时会返回429状态码——意思是请求太多我受不了了。如果不检查状态码你的爬虫会傻傻地继续爬结果全是无效数据还加重了对方服务器的压力。这里插入一个我实际项目中经常遇到的情况当你触发了网站的限流机制返回429时很多新手会立刻慌神盲目重试。正确的操作是检查状态码如果是429说明你的请求频率太快应当增加延时而不是继续硬闯。4.4 带数据提交POST请求和表单提交GET请求适合获取数据但当你需要模拟登录、提交表单时就得用POST了。requests处理POST同样简洁import requests url https://httpbin.org/post # 要提交的表单数据 data { username: admin, password: 123456 } # 自动把data编码成表单格式并放到请求体中 response requests.post(url, datadata, timeout10) print(response.text)如果是JSON格式的数据用json参数import requests url https://httpbin.org/post payload { name: 张三, age: 25 } # json 会自动设置Content-Type为application/json并序列化字典 response requests.post(url, jsonpayload, timeout10) print(response.text)data和json的区别在于请求体的格式前者是表单编码application/x-www-form-urlencoded后者是JSON格式application/json。现在很多API接口倾向于接收JSON所以这个参数你会经常用到。4.5 获取响应内容text、content和json()拿到响应之后怎么用里面的数据也是门学问。requests提供了三种主要方式response.text返回解码后的字符串。requests会根据响应头的charset自动解码但偶尔会猜错遇到乱码时可以手动指定编码response.encoding utf-8 # 手动指定解码方式 text response.textresponse.content返回原始的bytes类型。当你下载图片、音频、文件时必须用这个img_data response.content # 写入本地文件 with open(image.jpg, wb) as f: f.write(img_data)response.json()直接把JSON格式的响应解析成Python字典。现代API接口基本都返回JSONdata response.json() print(data[title])这里有个小坑如果响应内容不是合法的JSONresponse.json()会抛异常。稳妥的做法是先用response.text打印看一下确认是JSON再调用.json()。4.6 会话保持requests.Session的妙用爬虫爬多了你会发现很多网站需要登录后才能访问。登录成功后服务器会下发一个Cookie然后你每次请求都要带上这个Cookie服务器才认识你是谁。requests提供了一个神器——Session对象。它就像一个浏览器标签页能自动保存Cookie让你在整个会话期间保持登录状态import requests # 创建一个session对象 session requests.Session() # 先登录获取Cookie login_url https://httpbin.org/post login_data { username: admin, password: 123456 } session.post(login_url, datalogin_data, timeout10) # 后续请求自动携带Cookie不用手动管理 profile_url https://httpbin.org/cookies response session.get(profile_url, timeout10) print(response.text) # 会输出你之前设置的cookie信息Session对象还会自动管理连接池当你对同一个网站发起大量请求时性能会明显提升。我强烈建议你做爬虫时统一用Session而不是每次直接调requests.get()。这里也说一下为什么这个习惯重要当你爬取的页面有几十上百个时每次新建连接的开销会累积起来Session复用连接不仅速度快还更不容易被服务器识别为异常流量。5. 常见问题与排查技巧实录写爬虫没有不踩坑的。我把这几年最常见的几类问题整理成一张速查表你遇到问题时可以直接对号入座问题现象可能原因解决方案请求报ConnectionError网络不通或目标网站宕机检查本地网络用浏览器访问一下目标网址返回403服务器拒绝了你的请求检查请求头大概率是User-Agent没有伪装返回429请求太频繁触发限流降低请求频率加延时或使用代理中文乱码编码判断错误用response.encoding手动指定正确的charsetSSL证书错误对方证书配置有问题临时用verifyFalse跳过验证请求超时网络或服务器响应慢设置更长的timeout或考虑用重试机制5.1 触发反爬虫机制怎么办当你爬取的网站开始检测到非浏览器流量时最简单的反爬措施就是加请求头和适当延时。但有些更极端的网站会检测你访问的频率、模式甚至需要JS执行才能获取数据。新手阶段你只需要掌握两层应对第一层是基础伪装即带上浏览器的User-Agent、Referer等请求头。Referer表示你从哪个页面跳转过来的有些网站会校验这个字段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.example.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, }第二层是控制爬取频率。养成在请求之间加延时比如time.sleep(1)的好习惯。这个延时既能保护对方服务器也能避免你的IP被封。一旦你看到429 Too Many Requests这种错误就意味着你的请求频率已经超过了网站的容忍阈值。这时候最明智的做法是停一下等一会儿再继续或者用指数退避的策略第一次等10秒第二次等20秒第三次40秒逐步拉长重试间隔。5.2 检查响应的利器httpbin.org你可能会问我怎么知道自己发的请求长什么样服务器看到了什么这里推荐一个非常好用的调试工具——https://httpbin.org。这个网站的功能很简单它把所有请求信息原样返回给你。比如httpbin.org/get会返回你发请求时的请求头、参数、IP地址等信息httpbin.org/post会返回POST提交的数据内容。我调试请求头、Cookie、代理时都会先拿它练手确认无误后再换成真实的目标网站。强烈建议你每学一个新功能都先用它验证一下import requests import json response requests.get(https://httpbin.org/get, timeout10) # 打印服务器看到的请求头 data response.json() print(json.dumps(data[headers], indent2, ensure_asciiFalse))输出结果里包含了你的User-Agent、Accept等请求头信息一目了然。5.3 保存数据先学会存进文件很多新手爬取数据之后不知道干嘛——其实你可以先存成文件为后面学习数据库存储打基础。保存HTML页面是最简单的import requests url https://example.com response requests.get(url, timeout10) response.encoding utf-8 with open(example.html, w, encodingutf-8) as f: f.write(response.text) print(页面已保存)保存JSON数据import requests import json url https://httpbin.org/json response requests.get(url, timeout10) data response.json() with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(数据已保存)注意json.dump里的ensure_asciiFalse这是保证中文以原文写入文件而不是\uXXXX格式的关键。6. 综合实战写一个真正有意义的爬虫前面学了一堆散装知识点现在把它们拼起来——我带你写一个完整的爬虫程序目标是抓取一个新闻网站的标题列表。我会把整个流程走通你跟着敲就行了。6.1 准备阶段观察目标网站先确定目标网站。这里我以一个公开的新闻API为例也可以换成任意你喜欢的公开API比如url https://httpbin.org/json如果你有想爬的真实网站记住一个原则先用浏览器打开目标网站按F12进开发者工具切到Network标签页刷新页面观察浏览器发出的请求长什么样然后用你的代码去模拟它。观察要点有三个请求方法GET还是POST、请求头特别是User-Agent和Cookie、请求参数。把这个结构摸清了你就成功了一半。6.2 编写爬虫代码下面我写一个相对完整的爬虫脚本它包含前面说的各类基础用法import requests import time import json # 全局配置 REQUEST_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, Accept: application/json, text/plain, */*, } def fetch_data(url, paramsNone): 通用请求函数带异常处理和状态码检查 try: response requests.get( url, paramsparams, headersREQUEST_HEADERS, timeout10, verifyFalse # 学习阶段先关闭证书验证正式环境不建议 ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f请求失败{e}) return None def save_to_json(data, filename): 保存数据到JSON文件 with open(filename, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f数据已保存到 {filename}) def main(): # 模拟一个新闻数据源 url https://httpbin.org/json print(开始抓取数据...) # 抓取数据 data fetch_data(url) if data: # 打印部分内容确认 print(抓取成功前300字符预览) print(json.dumps(data, ensure_asciiFalse, indent2)[:300]) # 保存数据 save_to_json(data, news_data.json) else: print(抓取失败) # 模拟爬取多个页面时的节奏控制 print(继续下一轮请求...) time.sleep(2) if __name__ __main__: main()这段代码已经具备了一个工程化爬虫的基本要素请求头伪装、超时控制、状态码检查、异常处理、数据保存、频率控制。你拿它去改换上真实的目标URL和解析逻辑就能成为一个功能齐备的爬虫。6.3 关键技巧请求之间的延时控制很多新手爬取失败不是因为代码逻辑有问题而是因为请求发得太快触发了网站的反爬机制。这里给出一个比较稳妥的延时方案import time import random def polite_request(url, headers): 带礼貌延时的请求函数 # 随机延时1到3秒模拟真人操作 time.sleep(random.uniform(1, 3)) response requests.get(url, headersheaders, timeout10) return response用random.uniform(1, 3)随机延时不是固定频率更加接近真人浏览的行为模式。固定延时会被识别为机器的周期性请求随机延时能有效降低被检测的概率。6.4 下一步准备学什么这篇文章的结尾给你一个明确的下一步路标爬虫技术栈路线大致是这样第一阶段当前掌握urllib和requests能够正确发起GET/POST请求获取响应内容第二阶段学习HTML解析用正则表达式、BeautifulSoup或XPath从网页中提取你需要的数据第三阶段学习更复杂的抓取策略处理登录、Cookie、Session、代理IP第四阶段学习数据存储把爬取的数据存到MySQL、MongoDB等数据库第五阶段学习框架比如Scrapy处理大规模、分布式的爬虫任务你现在身处的第一阶段是我认为最关键的一步——因为不管后续学什么发请求获取响应始终是爬虫的地基。地基不牢后面学再多都是空中楼阁。如果你跟着这篇文章一路做到这里你已经能自豪地说一句我写过爬虫了。下一篇我会接着写如何解析HTML数据带你把抓下来的页面变成真正结构化、可使用的信息。到时候见。
返回列表