多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python爬虫之urllib基础用法教程

Python爬虫之urllib基础用法教程 前言很多人第一次写爬虫时会先去装requests其实 Python 标准库里本来就带了一套 HTTP 客户端工具叫urllib。它不需要pip install装好 Python 就能用代价是 API 比requests啰嗦一些取回来的内容永远是bytes要自己解码传参要自己编码加请求头要自己拼。还有一个常见的说法错误需要先纠正「urllib 是一个模块」并不准确。在 Python 3 里urllib是一个包package下面挂着四个子模块各管一摊子模块职责本篇文章用到的部分urllib.request打开 URL、构造请求、处理响应urlopen、Requesturllib.parse解析与编码 URLquote、urlencodeurllib.error请求相关的异常只在前言提一句urllib.robotparser读取并解释 robots.txt合规部分另外必须先点明 Python 2 的差异Python 2.7 已于2020 年 1 月 1 日停止维护PEP 3732 里的urllib2模块在 Python 3 中已经被合并进urllib.request。所以网上那些import urllib2的老代码在 Python 3 里会直接ModuleNotFoundError本文一律用 Python 3 的写法。本文只讲最基础的四个动作urlopen、Request、quote/urlencode、GET 与 POST 的最简写法。进阶的 handler 机制不在本文范围。一、urlopen一个函数就能发请求urllib.request.urlopen()是最短的入口。它的签名以官方文档为准大致是urllib.request.urlopen(url, dataNone, [timeout, ]*, contextNone)url可以是一个字符串也可以是一个Request对象。它返回一个类文件对象支持read()、readline()也支持with语句。最简用法# 适用于 Python 3.8import urllib.requestwith urllib.request.urlopen(https://www.python.org/, timeout10) as resp:print(resp.status) # 200print(resp.reason) # OKbody resp.read() # bytesprint(type(body)) # class bytesprint(len(body)) # 字节数有三个细节要记住urlopen的返回值不是strread()拿到的是bytes。官方文档说得很清楚之所以不直接返回字符串是因为库无法自动确定字节流的编码所以解码这一步必须由调用者来做。resp.status是整数状态码resp.reason是原因短语。resp.headers是一个大小写不敏感的头字典可以用.get(Content-Type)取头。timeout参数是秒。不传就用全局默认socket.getdefaulttimeout()的值默认None表示一直等。写爬虫时一定要显式传 timeout否则遇到不响应的服务器会挂死。关于编码响应头里的Content-Type常常带charsetutf-8之类的信息但服务器也可能根本不带。稳妥做法是显式解码并允许替换非法字节# 适用于 Python 3.8import urllib.requestwith urllib.request.urlopen(https://www.python.org/, timeout10) as resp:raw resp.read()charset resp.headers.get_content_charset() or utf-8text raw.decode(charset, errorsreplace)print(text[:80])headers.get_content_charset()是email.message.Message提供的方法HTTPResponse.headers就是它的实例所以可以直接用。二、Request 对象控制请求方法、请求头只传一个 URLurlopen会替你生成一个默认请求头里带着User-Agent: Python-urllib/3.x。要换方法、加头、带请求体就得自己构造Request。官方签名是urllib.request.Request(url, dataNone, headers{}, origin_req_hostNone,unverifiableFalse, methodNone)headers是一个普通字典method是字符串比如HEAD。不传method时规则是data为None就是GET否则是POST。# 适用于 Python 3.8import urllib.requestreq urllib.request.Request(https://www.python.org/,headers{# 用可识别的真实身份方便对方站长根据 UA 找到你User-Agent: my-first-crawler/1.0 (contact: meexample.com),Accept-Language: zh-CN,zh;q0.9,},methodGET,)with urllib.request.urlopen(req, timeout10) as resp:print(resp.status)print(resp.headers.get(Content-Type))请求头也可以在构造之后再加Request提供了add_header(key, val)、has_header(header)、remove_header(header)、get_header(header_name, defaultNone)这几个方法。官方文档提醒的一点是OpenerDirector会自动给每个请求补上User-Agent所以如果你想改默认值也可以改 opener 的addheaders这属于进阶内容见 handler 那篇思路。Request还有几个只读属性值得知道full_url原始 URL带 setter可重新赋值、method、data、headers以及get_method()。想调试自己拼出来的请求长什么样可以先看这些属性而不用真的发出去。三、GET 传参先编码再拼进 URLHTTP 的 GET 参数是拼在 URL 问号后面的规则是application/x-www-form-urlencoded。中文和空格不能直接出现在 URL 里必须先做百分号编码。urllib.parse提供了两个函数函数用途空格编码成默认safequote(string, safe/, encodingNone, errorsNone)编码路径片段等单个字符串%20/quote_plus(string, safe, ...)编码查询串里的键值urlencode(query, doseqFalse, safe, encodingNone, errorsNone, quote_viaquote_plus)把映射或键值对序列拼成查询串可换quote变%20—注意urlencode的默认编码函数是quote_plus所以空格会变成这在表单编码里是标准行为服务端会把它还原成空格。# 适用于 Python 3.8from urllib.parse import urlencode, quoteparams {q: a b, page: 1}query urlencode(params)print(query) # qabpage1url https://example.com/search? queryprint(url)# 路径片段用 quote安全字符集默认含 /print(quote(a b, safe)) # a%20bprint(quote(/docs/a b)) # /docs/a%20b如果参数值本身是一个列表要用doseqTrue否则整个列表会被当成一个字符串编码# 适用于 Python 3.8from urllib.parse import urlencodeprint(urlencode({tag: [py, web]}, doseqTrue)) # tagpytagwebprint(urlencode({tag: [py, web]})) # 整个列表被当成一个字符串编码上面第二行那种把列表整体编码的结果服务端基本没法用这是真会踩的坑。四、POST 提交表单POST 与 GET 的最大差别是参数放在请求体里不在 URL 里。用 urllib 发 POST 要做三件事用urlencode把参数字典编码成查询串格式的字符串把字符串用.encode()转成bytes因为Request的data参数只接受 bytes、类文件对象或 bytes 可迭代对象把data传给Request此时方法自动变成 POST或者直接传给urlopen。# 适用于 Python 3.8import urllib.requestfrom urllib.parse import urlencodeform {username: alice, action: login}data urlencode(form).encode(utf-8) # 关键编码成 bytesreq urllib.request.Request(https://example.com/login,datadata,headers{User-Agent: my-first-crawler/1.0 (contact: meexample.com),# 带 data 时官方文档说若不指定 Content-Type 会默认补上下面这个Content-Type: application/x-www-form-urlencoded,},)with urllib.request.urlopen(req, timeout10) as resp:print(resp.status)print(resp.read()[:200].decode(utf-8, errorsreplace))官方文档明确写道如果data不为None且你没有提供Content-Type头库会自动加上Content-Type: application/x-www-form-urlencoded。所以这一行写不写通常都能通写出来更清楚。本文不涉及携带 Cookie 的登录流程——那是 Session 与HTTPCookieProcessor的范畴。五、合规抓取的基本动作urllib 是工具怎么用决定了它是不是「爬虫」。下面几条是任何请求库都通用的底线请默认遵守先看 robots.txt。标准库里有urllib.robotparser.RobotFileParser它提供了set_url()、read()、can_fetch(useragent, url)、crawl_delay(useragent)等方法。对目标站先问一句「我能不能抓这个路径」比事后被拉黑划算得多。带可识别的真实 User-Agent。官方文档的示例就是urllib-example/0.1 (Contact: ...)这种格式写明用途和联系方式站长才有可能在你要抓太多时先联系你而不是直接封 IP。限速与退避。两次请求之间time.sleep()一段时间遇到 5xx 错误时等更久再试。不要为了快就并发压站。只取公开页面。不绕登录墙、付费墙、验证码不伪造身份去拿未授权数据。下面这段把 robots.txt 检查和限速串在一起是一个最小可用的合规骨架# 适用于 Python 3.8import timeimport urllib.requestimport urllib.robotparserUA my-first-crawler/1.0 (contact: meexample.com)BASE https://www.python.orgrp urllib.robotparser.RobotFileParser()rp.set_url(BASE /robots.txt)rp.read()target BASE /if not rp.can_fetch(UA, target):print(robots.txt 不允许抓取放弃)else:delay rp.crawl_delay(UA) or 1.0 # 没写 Crawl-delay 时自己定 1 秒req urllib.request.Request(target, headers{User-Agent: UA})with urllib.request.urlopen(req, timeout10) as resp:print(resp.status, len(resp.read()))time.sleep(delay)crawl_delay()在 Python 3.6 起才可用且当 robots.txt 里没有对应规则时返回None所以上面用or 1.0兜底。常见坑点1. URL 忘了写协议头。❌urllib.request.urlopen(www.python.org)—— 只有主机名会抛ValueError: unknown url type。 ✅ 补上协议名写成完整的 URL 字符串没有协议部分库根本不知道该用哪种方式去访问。2. 把read()的结果当字符串用。❌text resp.read() \n——bytes和str相加直接TypeError。 ✅text resp.read().decode(utf-8, errorsreplace) \n。3. POST 忘了把 data 编码成 bytes。❌data urlencode(form)然后urlopen(url, datadata)—— 传str给data会失败。 ✅data urlencode(form).encode(utf-8)。4. 用quote处理查询串结果空格变成%20而不是。❌ 手工quote(f{k}{v})拼查询串服务端按表单规则解析时对不上。 ✅ 直接用urlencode({k: v})它默认走quote_plus。要显式用quote就传quote_viaquote并清楚后果。5. 列表参数不用doseq。❌urlencode({tag: [py, web]})—— 列表被整体str()编码服务端收到的是[py, web]这个字符串。 ✅urlencode({tag: [py, web]}, doseqTrue)。6. 不设 timeout。❌urlopen(url)—— 服务器不响应时会长时间挂住。 ✅urlopen(url, timeout10)并在外层try/except里处理超时和网络错误。7. 不看 robots.txt 就批量抓。❌ 直接循环抓取站点的整站路径。 ✅ 先用RobotFileParser.can_fetch()判断遵守Crawl-delay并主动限速。8. 以为 Python 3 里还有urllib2。❌import urllib2——ModuleNotFoundError。 ✅import urllib.requesturllib2是 Python 2 的模块2.7 已于 2020-01-01 停止维护Python 3 里它被合并进了urllib.request。总结需求写法最简 GETurlopen(url, timeout10).read()得 bytes加请求头 / 换方法Request(url, headers{...}, methodGET)编码路径片段urllib.parse.quote(s, safe)拼查询串urllib.parse.urlencode(params)空格变列表参数urlencode(params, doseqTrue)POST 表单urlencode(form).encode(utf-8)作为data合规前置检查urllib.robotparser.RobotFileParser.can_fetch()Python 2 对照urllib2→urllib.request2.7 已 EOLurllib的价值在于它零依赖、随 Python 一起发布适合写小工具或在不方便装第三方库的环境里应急。它的基础用法就这么多打开、取回、解码、编码、发出去。真正让爬虫变得「专业」的不是库的选择而是限速、识别身份、尊重 robots.txt 这些习惯。
返回列表