urllib
About
urllib是Python内置的HTTP请求库。urllib 模块提供的上层接口,使访问 www 和 ftp 上的数据就像访问本地文件一样,并且它也是requests的底层库。
其中包括4个主要模块:
- urllib.request:请求模块。
- urllib.error:异常处理模块。
- urllib.parse:URL解析模块。
- urllib.robotparser:robots.txt解析模块。
这里以Python3.6为例。
urllib、urllib2、urllib3
在Python2.x中,分为urllib和urllib2,简单来说,urllib2是urllib的增强版,但urllib中的函数又比urllib2多一些,对于简单的下载之类的,urllib绰绰有余,如果涉及到实现HTTP身份验证或者cookie或者扩展编写自定义协议,urllib2更好一些。
-
urllib支持设置编码的函数urllib.urlencode,在模拟登陆的时候经常需要传递经过post编码之后的参数,如果不想使用第三方库完成模拟登录,就必须使用到标准库中的urllib。urllib提供一些比较原始基础的方法而urllib2并没有,比如urllib中的urlencode方法用来GET查询字符串的产生。
- urllib2比较有优势的地方在于urllib2.openurl中可以接受一个Request类的实例来设置Request参数,来修改/设置Header头从而达到控制HTTP Request的header部分的目的,也可以修改用户代理,设置cookie等,但urllib仅可以接受URL。这就意味着,如果访问一个网站想更改User Agent(可以伪装你的浏览器),你就需要使用urllib2。
-
urllib2模块没有加入urllib.urlretrieve函数以及urllib.quote等一系列quote和unquote功能,这个时候就需要urllib的辅助。
因此,在Python2.x中,这两个库可以搭配使用。
而urllib3则是增加了连接池等功能,两者互相都有补充的部分。
那么Python3.x中呢,urllib和urllib2都合并到了urllib中了,所以,Python3.x即将一统江湖,还是踏实儿的学习Python3.x下的urllib吧。
再来看看Python2.x和Python3.x的区别。
在Python2.x中,urlopen
方法在urllib下:
import urllib2 response = urllib2.urlopen()
在Python3.x中,urlopen
方法就移动到了urllib.request
下:
import urllib.request response = urllib.request.urlopen()
urllib.request
请求
先来看urllib怎么发送请求。
发送get请求
import urllib response = urllib.request.urlopen('http://www.baidu.com') # 获取bytes类型的数据 # print(response.read()) # 想要获取到字符串类型的数据,需要使用 decode 转码为字符串 print(response.read().decode('utf-8'))
发送post请求
import urllib.request import urllib.parse data = bytes(urllib.parse.urlencode({'world': 'hello'}), encoding='utf-8') response = urllib.request.urlopen(url='http://httpbin.org/post', data=data) print(response.read().decode('utf-8'))
首先,需要了解一个http://httpbin.org
这个网站,HTTPBin是以Python+Flask写的一款工具,它包含了各类的HTTP场景,且每个接口一定都有返回。
data参数需要字节类型的数据,所以,首先用bytes转码,然后将字典使用urllib.parse.urlencode
并指定编码方式完成编码。
另外,data参数用来控制请求方式,如果请求中有data则是post请求,否则为get请求。
timeout
import socket import urllib.request import urllib.error try: # 没有超时,正常返回json格式的内容 # response = urllib.request.urlopen(url='http://www.httpbin.org/get', timeout=2) # 超时会抛出异常,由异常语句捕获 response = urllib.request.urlopen(url='http://www.httpbin.org/get', timeout=0.1) print(response.read().decode('utf-8')) except urllib.error.URLError as e: # print(e) # <urlopen error timed out> if isinstance(e.reason, socket.timeout): print(e.reason) # timed out
e.reason
为错误原因。timeout
单位是秒。
响应
现在来研究urlopen返回对象提供的方法:
- read(),readline(),readlines(),fileno(), close():这些方法与文件对象一样。
- info():返回一个httplibHTTPMessage对象,表示远程服务器返回的头信息。
- getcode():返回HTTP状态码。
- geturl():返回请求的url。
响应类型
import urllib.request response = urllib.request.urlopen(url='http://www.httpbin.org/get') print(response) # <http.client.HTTPResponse object at 0x07B65270> print(type(response)) # <class 'http.client.HTTPResponse'>
状态码
import urllib.request response = urllib.request.urlopen(url='http://www.httpbin.org/get') print(response.status) # 200
响应头
import urllib.request response = urllib.request.urlopen(url='http://www.httpbin.org/get') print(response.headers) print(response.getheaders()) # 列表类型的响应头 print(response.getheader('Server', None)) # 获取特定的响应头
响应体
import urllib.request response = urllib.request.urlopen(url='http://www.httpbin.org') # print(response.read()) print(response.read().decode('utf-8'))
之前说过,urllib获取到的内容是字节形式,通过read读出来之后需要使用decode解码。
request对象
上面发送一些简单的请求没问题,但是复杂的请求(比如带headers)就力有不逮了。这个时候,需要学习一个Request
来定制复杂的请求参数了。
import urllib.request request = urllib.request.Request(url='http://www.httpbin.org') response = urllib.request.urlopen(request) print(response.read().decode('utf-8'))
首先,使用urllib.request.Request
来构建一个request对象,然后传给urlopen
即可。
再来看一个复杂点的带headers。
import urllib.request import urllib.parse # 指定url url='http://www.httpbin.org/post' # 自定义headers headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"} # 自定义data参数并编码为bytes类型 data_dict = {"username": "zhangkai"} data = bytes(urllib.parse.urlencode(data_dict), encoding='utf-8') # 使用urllib.request.Request构造request对象 request = urllib.request.Request(url=url, data=data, headers=headers, method='POST') # 将request对象交给urlopen发请求去吧 response = urllib.request.urlopen(request) print(response.read().decode('utf-8'))
再来看一个add_headers
方法。
import urllib.request import urllib.parse url='http://www.httpbin.org/post' # 这里跟上面的示例区别就是不在单独指定headers字典 # headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"} data_dict = {"username": "zhangkai"} data = bytes(urllib.parse.urlencode(data_dict), encoding='utf-8') request = urllib.request.Request(url=url, data=data, method='POST') # 而是使用构造好的request对象下面的add_header方法添加header,换汤不换药.... request.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36") response = urllib.request.urlopen(request) print(response.read().decode('utf-8'))
add_header
方法作为添加headers的补充方式,如果有多个header键值对,可以使用for循环一一添加进去.....
会了构造request对象,我们基本能应付大部分的网站了。
再来看看更复杂的一些用法,以应对更多的复杂环境,比如携带cookie什么的。
Cookie
获取cookie
import urllib.request import http.cookiejar # 声明 cookiejar 对象 cookie = http.cookiejar.CookieJar() # 使用handler处理cookie对象 handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) # 当opener.open发送请求 response = opener.open('http://www.baidu.com') # 当response有了结果之后,会自动将cookie信息保存到上面声明的cookie对象中去 print(cookie)
http.cookiejar
提供一个永久性的cookie对象。并且cookiejar
有3个子类:
- CookieJar:管理HTTP cookie值、存储HTTP请求生成的cookie、向传出的HTTP请求添加cookie的对象。整个cookie都存储在内存中,对CookieJar实例进行垃圾回收后cookie也将丢失。
- FileCookieJar (filename,delayload=None,policy=None):从CookieJar派生而来,用来创建FileCookieJar实例,检索cookie信息并将cookie存储到文件中。filename是存储cookie的文件名。delayload为True时支持延迟访问访问文件,即只有在需要时才读取文件或在文件中存储数据。
- MozillaCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与Mozilla浏览器 cookies.txt兼容的FileCookieJar实例。
- LWPCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与libwww-perl标准的 Set-Cookie3 文件格式兼容的FileCookieJar实例。
大多数情况下,只用CookieJar()
,如果需要和本地文件交互,就用 MozillaCookjar()
或 LWPCookieJar()
补充:libwww-perl集合是一组Perl模块,它为万维网提供简单而一致的应用程序编程接口。该库的主要重点是提供允许您编写WWW客户端的类和函数。
将cookie写入文件
cookie是保持登录会话信息的凭证,那么在获取到cookie后写入本地,在cookie失效前,后续访问再读取本地文件,并在请求中携带,以达到继续保持登录信息的状态。
import urllib.request import http.cookiejar ''' 写入cookie对象到本地 ''' url = 'http://www.baidu.com' file_name = 'cookie.txt' # 这里将cookie信息处理成火狐浏览器cookie格式 cookie = http.cookiejar.MozillaCookieJar(file_name) # LWPCookieJar格式cookie # cookie = http.cookiejar.LWPCookieJar(file_name) # 使用handler处理cookie对象 handler = urllib.request.HTTPCookieProcessor(cookie1) opener = urllib.request.build_opener(handler) response1 = opener.open(url) # 将cookie信息保存到本地文件 cookie1.save(ignore_discard=True, ignore_expires=True) ''' 读取本地cookie信息 ''' # 以什么格式保存,就以什么格式读出来,这里都用MozillaCookieJar cookie2 = http.cookiejar.MozillaCookieJar() # 使用load方法从本地读取cookie信息 cookie2.load(file_name, ignore_discard=True, ignore_expires=True) # 放到handle中 handler = urllib.request.HTTPCookieProcessor(cookie2) opener = urllib.request.build_opener(handler) response2 = opener.open(url) print(response2.read().decode('utf-8'))
在save
和load
中,有两个参数需要说明:
- ignore_discard:保存cookie,即使设置为丢弃的cookie也保存。
- ignore_expires:如果cookie已过期也要保存到文件,并且如果文件存在则覆盖。后续可以使用
load
或者revert
方法恢复保存的文件。
代理
import urllib.request # 调用ProxyHandler 代理ip的形式是字典 # 付费代理 # money_proxy = {"协议":"username:pwd@ip:port"} proxy_handler = urllib.request.ProxyHandler({'sock5': 'localhost:1080'}) # 使用build_opener构建opener对象 opener = urllib.request.build_opener(proxy_handler) # 调用opener的open方法向指定url发请求 response = opener.open('http://www.httpbin.org/get') print(response.read().decode('utf-8')) ''' { "args": {}, "headers": { "Accept-Encoding": "identity", "Host": "www.httpbin.org", "User-Agent": "Python-urllib/3.6" }, "origin": "124.64.16.181, 124.64.16.181", "url": "https://www.httpbin.org/get" } '''
{'sock5': 'localhost:1080'}
中sock5
是协议,localhost:1080
是走本地的1080端口。
其实urlopen
在内部也是调用了opener.open
方法发送请求。
另外,返回结果origin
是访问IP,而这个IP正好是我们使用的代理IP。
下载
可以使用urllib下载图片,文件等到本地。
import urllib.request url = 'http://n.sinaimg.cn/news/1_img/upload/cf3881ab/69/w1000h669/20190912/bed4-iepyyhh6925213.jpg' filename = 'a.jpg' # 使用urlretrieve进行下载操作 urllib.request.urlretrieve(url=url, filename=filename)
除此之外,还可以使用urllib.urlcleanup()
来清除urllib.urlretrieve
所产生的缓存。
urllib.error
urllib中的异常处理有3个:
urllib.error.URLError
:捕获关于url的错误,只有一个返回方法reason,错误原因。urllib.error.HTTPError
:捕获关于http的错误,有3个返回方法。code,http中定义的http状态码;reason,错误原因;headers,响应头。urllib.error.ContentTooShortError
:当urlretrieve
检测到下载数据量小于预期时,会引发此异常。
urllib.error.URLError
import urllib.request import urllib.error try: # 这里将 cnblogs 写成 cnblogsww # response = urllib.request.urlopen(url='https://www.cnblogsww.com/Neeo/p/1083589221.html') print(response.read().decode('utf-8')) except urllib.error.ContentTooShortError as e: print('ContentTooShortError: ', e.reason)
urllib.error.HTTPError
import urllib.request import urllib.error try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"} # 不存在的 103048922222.html response = urllib.request.urlopen(url='https://www.cnblogs.com/Neeo/p/103048922222.html') print(response.read()) except urllib.error.HTTPError as e: print('HTTPError: ', e.reason, e.headers, e.code)
urllib.error.ContentTooShortError
这个错误比较重要,因为在下载文件时,很可能会因为各种原因导致下载没有完成就失败了。这个时候,就可以使用ContentTooShortError
来捕捉错误从新下载。如果重新下载还是下载失败,可以考虑使用递归,下载失败就重新下,直到下载成功。
import urllib.request import urllib.error try: url = 'http://n.sinaimg.cn/news/1_img/upload/cf3881ab/69/w1000h669/20190912/bed4-iepyyhh6925213.jpg' filename = 'a.jpg' urllib.request.urlretrieve(url=url, filename=filename) except urllib.error.ContentTooShortError as e: print('ContentTooShortError: ', e.reason) # 如果下载失败,就重新下载 urllib.request.urlretrieve(url=url, filename=filename) def get_file(url, filename): """ 使用递归进行重新下载,直到下载成功为止 """ try: urllib.request.urlretrieve(url=url, filename=filename) print('download done ......') except urllib.error.ContentTooShortError: get_file(url, filename) url = "http://n.sinaimg.cn/news/1_img/upload/cf3881ab/69/w1000h669/20190912/bed4-iepyyhh6925213.jpg" filename = 'a.jpg' get_file(url, filename)
urllib.parse
urllib.parse
定义了URL的标准接口,实现对于URL的各种操作,包括解析、合并、编码、解码,使用之前需导入。
urllib.parse.urlparse
urllib.parse.urlparse
以元组的形式返回URL解析后的6个组件。对应的URL结构一般是这样的:
scheme://netloc/path;parameters?query#fragment
每个元组项都是一个字符串,也可能是空的。
返回的元组中:
- scheme:协议。
- netloc:域名。
- path:路径。
- params:参数。
- query:查询条件,一般用于个get请求的URL。
- fragment:锚点。
基本使用:
import urllib.parse urllib.parse.urlparse(url, scheme='', allow_fragments=True)
- url:待解析的url
- scheme:协议,如果URL中没有协议,则使用该参数设置协议,如果有协议,该参数不生效
- allow_fragments:是否忽略锚点,默认为True,表示表示不忽略,False表示忽略
示例:
import urllib.parse result = urllib.parse.urlparse('https://www.cnblogs.com/Neeo/p/10864123.html?k1=v1#python') print(result) ''' ParseResult(scheme='https', netloc='www.cnblogs.com', path='/Neeo/p/10864123.html', params='', query='k1=v1', fragment='python') '''
urllib.parse.urlunparse
与urlparse
相反,urllib.parse.urlunparse
用来构造URL。
import urllib.parse url_components = ['https', 'www.cnblogs.com', '/Neeo/p/10864123.html', '', '', 'python'] result = urllib.parse.urlunparse(url_components) print(result) # https://www.cnblogs.com/Neeo/p/10864123.html#python
在url_components
中,顺序必须遵循:
scheme, netloc, url, params, query, fragment
也就是6个选项都要有值,如果没有也要填充空字符串占位。
urllib.parse.urljoin
urllib.parse.urljoin
用来拼接URL。
import urllib.parse # 将两个url拼接成一个完整的url,base_url + sub_url print(urllib.parse.urljoin('https://www.cnbogs.com', '/Neeo/p/10864123.html')) # 如果每个参数中都有相应的参数的话,比如都有协议,那么将以后面的为准 print(urllib.parse.urljoin('https://www.cnbogs.com', 'http://www.baidu.com/Neeo/p/10864123.html'))
注意,urljoin
方法只能接受两个url参数:
urljoin(base, url, allow_fragments=True)
别意淫它能跟os.path.join
一样可劲儿的往后拼!
urllib.parse.urlencode
urllib.parse.urlencode
将字典类型的参数序列化为url编码后的字符串,常用来构造get/post请求的参数k1=v1&k2=v2
。
import urllib.parse params = {"k1": "v1", "k2": "v2"} encode_params = urllib.parse.urlencode(params) print('https://www.baidu.com?' + encode_params) # https://www.baidu.com?k1=v1&k2=v2 # 这里用urljoin不好使, 它在处理base url时会将 ? 干掉 print(urllib.parse.urljoin('https://www.baidu.com', encode_params)) # https://www.baidu.com/k1=v1&k2=v2 # 或者你继续使用拼接....好蛋疼 print(urllib.parse.urljoin('https://www.baidu.com', '?' + encode_params)) # https://www.baidu.com?k1=v1&k2=v2
urllib.parse.quote系列
按照标准,URL只允许一部分ASCII字符(数字字母和部分符号),其他的字符如汉字是不符合URL标准的。所以在使用URL的时候,要进行URL编码。
urllib提供了urllib.parse.quote
和urllib.parse.quote_plus
进行URL编码。
而urllib.parse.quote_plus
比urllib.parse.quote
更近一步,会对/
符号进行编码。
而urllib.parse.unquote
和urllib.parse.unquote_plus
则是将编码后的URL进行还原。
import urllib.parse url = 'https://www.baidu.com/s?&wd=张开' result = urllib.parse.quote(url) print(result) # https%3A//www.baidu.com/s%3F%26wd%3D%E5%BC%A0%E5%BC%80 result_plus = urllib.parse.quote_plus(url) print(result_plus) # https%3A%2F%2Fwww.baidu.com%2Fs%3F%26wd%3D%E5%BC%A0%E5%BC%80 un_result = urllib.parse.unquote(result) print(un_result) # https://www.baidu.com/s?&wd=张开 un_result_plus = urllib.parse.unquote_plus(result_plus) print(un_result_plus) # https://www.baidu.com/s?&wd=张开
urllib.robotparser
可以利用urllib.robotparser
对爬取网站的Robots协议进行分析。
那问题来了,什么是Robots协议?
Robots协议
Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。robots.txt文件是一个文本文件,放在站点的根目录下。
当一个搜索蜘蛛访问一个站点时,它会首先检查该站点根目录下是否存在robots.txt,如果存在,搜索机器人就会按照该文件中的内容来确定访问的范围;如果该文件不存在,所有的搜索蜘蛛将能够访问网站上所有没有被口令保护的页面。
禁止所有爬虫访问任何内容
User-Agent: *
Disallow: /
允许所有爬虫访问任何内容
User-Agent: *
Disallow:
允许某些爬虫访问某些目录
User-agent: Baiduspider Allow: /article Allow: /oshtml Disallow: /product/ Disallow: / User-Agent: Googlebot Allow: /article Allow: /oshtml Allow: /product Allow: /spu Allow: /dianpu Allow: /oversea Allow: /list Disallow: / User-Agent: * Disallow: /
关于爬虫名称
爬虫名称 | 所属公司 | 网址 |
---|---|---|
Baiduspider | 百度 | www.baidu.com |
Googlebot | 谷歌 | www.google.com |
Bingbot | 微软必应 | cn.bing.com |
360Spider | 360搜索 | www.so.com |
Yisouspider | 神马搜索 | http://m.sm.cn/ |
Sogouspider | 搜狗搜索 | https://www.sogou.com/ |
Yahoo! Slurp | 雅虎 | https://www.yahoo.com/ |
RobotFileParser
robotpaser模块提供RobotFileParser类来解析robots.txt文件,判断是否允许爬取网站的某一目录。
在实例化类时传入robots.txt。
import urllib.robotparser response = urllib.robotparser.RobotFileParser(url='https://www.zhihu.com/robots.txt') response.read()
或者通过实例化对象调用set_url
方法传入robots.txt。
import urllib.robotparser response = urllib.robotparser.RobotFileParser() response.set_url('https://www.zhihu.com/robots.txt') response.read()
whatever,都需要使用response.read()
读出来,不然后面解析不到。
常用方法:
- set_url(url):用来设置 robots.txt 文件链接,如果在初次实例化 RobotFileParser 类的时候传入了 url 参数,那么就不需要再次调用此方法设置了。
- read():读取 robots.txt 文件并将读取结果交给 parse() 解析器进行解析。
- parse(lines):用来解析 robots.txt 文件内容,分析传入的某些行的协议内容。
- can_fetch(useragent, url):需要两个参数,User-Agent、所要抓取的 URL 链接,返回此搜索引擎是否允许抓取此 URL,返回结果为 True、False。
- mtime():返回上次抓取分析 robots.txt 文件的时间,这对于需要对 robots.txt 进行定期检查更新的长时间运行的网络爬虫非常有用 。
- modified():同样的对于长时间分析和抓取的搜索爬虫很有帮助,将当前时间设置为上次抓取和分析 robots.txt 的时间。
- crawl_delay(useragent):返回抓取延迟时间的值,从相应的 User-Agent 的 robots.txt 返回 Crawl-delay 参数的值。 如果没有这样的参数,或者它不适用于指定的 User-Agent,或者此参数的 robots.txt 条目语法无效,则返回 None。
- request_rate(useragent):从robots.txt返回Request-rate参数的内容,作为命名元组RequestRate(requests,seconds)。 如果没有这样的参数,或者它不适用于指定的useragent,或者此参数的robots.txt条目语法无效,则返回None。(Python3.6新增方法)
示例:
import urllib.robotparser response = urllib.robotparser.RobotFileParser(url='https://www.zhihu.com/robots.txt') # 要有读操作 response.read() # 判断某一个网址是否能爬取 print(response.can_fetch('Googlebot', 'https://www.zhihu.com/question/268464407/answer/804631692')) # True print(response.can_fetch('*', 'https://www.zhihu.com/question/268464407/answer/804631692')) # False # 返回上一次抓取分析 robots.txt 的时间 print(response.mtime()) # 1568542822.1876643 # 将当前时间设置为上次抓取和分析 robots.txt 的时间 response.modified() # 返回 robots.txt 文件对请求频率的限制 print(response.request_rate('MSNBot').requests) # 1 print(response.request_rate('*')) # None # 返回 robots.txt 文件对抓取延迟限制 print(response.crawl_delay('*')) # None print(response.crawl_delay('MSNBot')) # 10
使用parser方法读取和分析robots.txt。
import urllib.robotparser import urllib.request # 实例化 RobotFileParser 对象 response = urllib.robotparser.RobotFileParser() # 使用parser读取 robots.txt 文件 result = urllib.request.urlopen('https://www.zhihu.com/robots.txt').read().decode('utf-8').split('\n') response.parse(result) # 判断url是否可爬取 print(response.can_fetch('Googlebot', 'https://www.zhihu.com/question/268464407/answer/804631692')) # True print(response.can_fetch('*', 'https://www.zhihu.com/question/268464407/answer/804631692')) # False
see also:http://www.httpbin.org/ | 使用HTTPBin测试HTTP库 | urllib — URL handling modules | http://www.66ip.cn/ | HTTP模块 | 使用ip付费的代理是遇到http.client.RemoteDisconnected: Remote end closed connection without response 解决方法 | http.cookiejar库之CookieJar |libwww-perl | 爬虫保存cookies时重要的两个参数(ignore_discard和ignore_expires)的作用 | http.cookiejar- HTTP客户端的Cookie处理 | [python]解决urllib.urlretrieve()下载不完全的问题 | urllib.parse模块使用 | 4. Urllib -- urllib.robotparser | urllib.robotparser- robots.txt的解析器 | urllib模块中的方法 | python中quote函数是什么意思,怎么用