spider 基本库
学习爬虫,最初的操作便是模拟浏览器向服务器发出请求,那么我们需要从哪个地方做起呢?请求需要我们自己来构造吗?需要关心请求这个数据结构的实现吗?需要了解 HTTP、TCP、IP 层的网络传输通信吗?需要知道服务器的响应和应答原理吗?
可能你不知道无从下手,不过不用担心,Python 的强大之处就是提供了功能齐全的类库来帮助我们完成这些请求。最基础的 HTTP 库有 urllib、httplib2、requests、treq 等。
urllib
在 Python2 中,有 urllib 和 urllib2 两个库来实现请求的发送。而在 Python3 中,已经不存在 urllib2 这个库了,统一为 urllib,其官方文档链接为:https://docs.python.org/3/library/urllib.html
urllib库是Python内置的HTTP请求库,也就是说不需要额外安装即可使用它包含如下4个模块。
- request 它是最基本的HTTP请求模块,可以用来模拟发送请求。就像在浏览器里输入网挝然后回车一样,只需要给库方法传入URL以及额外的参数,就可以模拟实现这个过程了。
- error 异常处理模块,如果出现请求错误,我们可以捕获这些异常,然后进行重试或 其他 操作以保证程序不会意外终止。
- parse 一个工具模块,提供了许多URL处理方法,比如拆分、解析、合并等。
- robot parser 主要是用来识别网站的robots.txt文件,然后判断哪些网站可以爬,哪些网站不可以爬,它其实用得比较少
本节代码可以参考这个库
发送请求
使用 urllib 的 request 模块,我们可以方便地实现请求的发送并得到响应。
urlopen
urllib.request模块提供了最基本的构造HTTP请求的方法,利用它可以模拟浏览器的一个请求发起过程,同时它还带有处理授权验证(authentication)、重定向(redirection)、浏览器Cookies以及其他内容。
以 Python 官网为例,我们来把这个网页抓下来:
import urllib.request
response = urllib.request.urlopen('<https://www.python.org>')print(response.read().decode('utf-8'))
print(type(response))一个 HTTPResposne 类型的对象,主要包含 read、readinto、getheader、getheaders、fileno 等方法,以及 msg、version、status、reason、debuglevel、closed等属性。
得到这个对象之后,我们把它赋值为 response 变量,然后就可以调用这些方法和属性,得到返回结果的一系列信息了。
import urllib.request
response = urllib.request.urlopen('<https://www.python.org>')print(response.status)print(response.getheaders())print(response.getheader('Server'))
print(type(response))# <class 'http.client.HTTPResponse'>可以发现,返回的是一个 HTTPResposne 类型的对象,主要包含 read、readinto、getheader、getheaders、fileno 等方法,以及 msg、version、status、reason、debuglevel、closed 等属性。
得到这个对象之后,我们把它赋值为 response 变量,然后就可以调用这些方法和属性,得到返回结果的一系列信息了。
例如,调用 read 方法可以得到返回的网页内容,调用 status 属性可以得到返回结果的状态码,如 200 代表请求成功,404 代表网页未找到等。
利用最基本的 urlopen 方法,可以完成最基本的简单网页的 GET 请求抓取。
如果想给链接传递一些参数,该怎么实现呢?首先看一下 urlopen 方法的 API:
urllib.request.urlopen(url, data=None, [timeout,]*, cafile=None, capath=None, cadefault=False, context=None)
可以发现,除了第一个参数可以传递 URL 之外,我们还可以传递其他内容,比如 data(附加数据)、timeout(超时时间)等。
-
data 参数 data 参数是可选的。如果要添加该参数,需要使用 bytes 方法将参数转化为字节流编码格式的内容,即 bytes 类型。另外,如果传递了这个参数,则它的请求方式就不再是 GET 方式,而是 POST 方式。
import urllib.parseimport urllib.requestdata = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf8')response = urllib.request.urlopen('<http://httpbin.org/post>', data=data)print(response.read())这里我们传递了一个参数 word,值是 hello。它需要被转码成 bytes(字节流)类型。其中转字节流采用了 bytes 方法,该方法的第一个参数需要是 str(字符串)类型,需要用 urllib.parse 模块里的 urlencode 方法来将参数字典转化为字符串;第二个参数指定编码格式,这里指定为 utf8。
在这里请求的站点是 httpbin.org,它可以提供 HTTP 请求测试,本次我们请求的 URL 为:http://httpbin.org/post,这个链接可以用来测试 POST 请求,它可以输出 Request 的一些信息,其中就包含我们传递的 data 参数。
运行结果如下:
{"args": {},"data": "","files": {},"form": {"word": "hello"},"headers": {"Accept-Encoding": "identity","Content-Length": "10","Content-Type": "application/x-www-form-urlencoded","Host": "httpbin.org","User-Agent": "Python-urllib/3.5"},"json": null,"origin": "123.124.23.253","url":"<http://httpbin.org/post>"}可以看到,我们传递的参数出现在了 form 字段中,这表明是模拟了表单提交的方式,以 POST 方式传输数据。
-
timeout 参数
timeout 参数用于设置超时时间,单位为秒,意思就是如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,就会使用全局默认时间。它支持 HTTP、HTTPS、FTP 请求。
import urllib.requestresponse = urllib.request.urlopen('<http://httpbin.org/get>', timeout=1)print(response.read())这里我们设置超时时间是 1 秒。程序 1 秒过后,服务器依然没有响应,于是抛出了 URLError 异常。该异常属于 urllib.error 模块,错误原因是超时。
因此,可以通过设置这个超时时间来控制一个网页如果长时间未响应,就跳过它的抓取。这可以利用 try except 语句来实现,相关代码如下:
import socketimport urllib.requestimport urllib.errortry:response = urllib.request.urlopen('<http://httpbin.org/get>', timeout=0.1)except urllib.error.URLError as e:if isinstance(e.reason, socket.timeout):print('TIME OUT')这里我们请求了 http://httpbin.org/get 这个测试链接,设置了超时时间是 0.1 秒,然后捕获了 URLError 这个异常,然后判断异常原因是 socket.timeout 类型,意思就是超时异常,就得出它确实是因为超时而报错,打印输出了 TIME OUT。
通过设置 timeout 这个参数来实现超时处理,有时还是很有用的。
-
其他参数
除了 data 参数和 timeout 参数外,还有 context 参数,它必须是 ssl.SSLContext 类型,用来指定 SSL 设置。此外,cafile 和 capath 这两个参数分别指定 CA 证书和它的路径,这个在请求 HTTPS 链接时会有用。cadefault 参数现在已经弃用了,其默认值为 False。
前面讲解了 urlopen 方法的用法,通过这个最基本的方法,我们可以完成简单的请求和网页抓取。若需更加详细的信息,可以参见官方文档:https://docs.python.org/3/library/urllib.request.html。
Request
我们知道利用 urlopen 方法可以实现最基本请求的发起,但这几个简单的参数并不足以构建一个完整的请求。如果请求中需要加入 Headers 等信息,就可以利用更强大的 Request 类来构建。
首先,我们用实例来感受一下 Request 的用法:
import urllib.request
request = urllib.request.Request('<https://python.org>')response = urllib.request.urlopen(request)print(response.read().decode('utf-8'))可以发现,我们依然是用 urlopen 方法来发送这个请求,只不过这次该方法的参数不再是 URL,而是一个 Request 类型的对象。通过构造这个数据结构,一方面我们可以将请求独立成一个对象,另一方面可更加丰富和灵活地配置参数。
它的构造方法如下:
class urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
- 第一个参数 url 用于请求 URL,这是必传参数,其他都是可选参数。
- 第二个参数 data 如果要传,必须传 bytes(字节流)类型的。如果它是字典,可以先用 urllib.parse 模块里的 urlencode() 编码。
- 第三个参数 headers 是一个字典,它就是请求头,我们可以在构造请求时通过 headers 参数直接构造,也可以通过调用请求实例的 add_header() 方法添加。 添加请求头最常用的用法就是通过修改 User-Agent 来伪装浏览器,默认的 User-Agent 是 Python-urllib,我们可以通过修改它来伪装浏览器。比如要伪装火狐浏览器,你可以把它设置为:
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/- 第四个参数 origin_req_host 指的是请求方的 host 名称或者 IP 地址。
- 第五个参数 unverifiable 表示这个请求是否是无法验证的,默认是 False,意思就是说用户没有足够权限来选择接收这个请求的结果。例如,我们请求一个 HTML 文档中的图片,但是我们没有自动抓取图像的权限,这时 unverifiable 的值就是 True。
- 第六个参数 method 是一个字符串,用来指示请求使用的方法,比如 GET、POST 和 PUT 等。
from urllib import request, parse
url = '<http://httpbin.org/post>'headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Host': 'httpbin.org'}dict = {'name': 'Germey'}data = bytes(parse.urlencode(dict), encoding='utf8')req = request.Request(url=url, data=data, headers=headers, method='POST')response = request.urlopen(req)print(response.read().decode('utf-8'))这里我们通过 4 个参数构造了一个请求,其中 url 即请求 URL,headers 中指定了 User-Agent 和 Host,参数 data 用 urlencode 和 bytes 方法转成字节流。另外,指定了请求方式为 POST。
运行结果如下:
{ "args": {}, "data": "", "files": {}, "form": { "name": "Germey" }, "headers": { "Accept-Encoding": "identity", "Content-Length": "11", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)" }, "json": null, "origin": "219.224.169.11", "url":"<http://httpbin.org/post>"}观察结果可以发现,我们成功设置了 data、headers 和 method。
另外,headers 也可以用 add_header 方法来添加:
req = request.Request(url=url, data=data, method='POST')req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')如此一来,可以更加方便地构造请求,实现请求的发送。
高级用法
在上面的过程中,我们虽然可以构造请求,但是对于一些更高级的操作(比如 Cookies 处理、代理设置等),我们该怎么办呢?
接下来,就需要更强大的工具 Handler 登场了。简而言之,我们可以把它理解为各种处理器,有专门处理登录验证的,有处理 Cookies 的,有处理代理设置的。利用它们,我们几乎可以做到 HTTP 请求中所有的事情。
首先,介绍一下 urllib.request 模块里的 BaseHandler 类,它是所有其他 Handler 的父类,它提供了最基本的方法,例如 default_open、protocol_request 等。
接下来,就有各种 Handler 子类继承这个 BaseHandler 类,举例如下
- HTTPDefaultErrorHandler:用于处理 HTTP 响应错误,错误都会抛出 HTTPError 类型的异常。
- HTTPRedirectHandler:用于处理重定向。
- HTTPCookieProcessor:用于处理 Cookies。
- ProxyHandler:用于设置代理,默认代理为空。
- HTTPPasswordMgr:用于管理密码,它维护了用户名密码的表。
- HTTPBasicAuthHandler:用于管理认证,如果一个链接打开时需要认证,那么可以用它来解决认证问题。
另外还有其他的 Handler 类,在这不一一列举了,详情可以参考官方文档: https://docs.python.org/3/library/urllib.request.html#urllib.request.BaseHandler
另一个比较重要的类就是 OpenerDirector,我们可以称为 Opener。我们之前用过 urlopen 这个方法,实际上它就是 urllib 为我们提供的一个 Opener。
那么,为什么要引入 Opener 呢?因为需要实现更高级的功能。
Opener 可以使用 open 方法,返回的类型和 urlopen 如出一辙,同时可以利用 Handler 来构建 Opener。
-
验证
有些网站在打开时就会弹出提示框,直接提示你输入用户名和密码,验证成功后才能查看页面。
那么,如果要请求这样的页面,该怎么办呢?借助 HTTPBasicAuthHandler 就可以完成,相关代码如下:
from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_openerfrom urllib.error import URLErrorusername = 'username'password = 'password'url = '<http://localhost:5000/>'p = HTTPPasswordMgrWithDefaultRealm()p.add_password(None, url, username, password)auth_handler = HTTPBasicAuthHandler(p)opener = build_opener(auth_handler)try:result = opener.open(url)html = result.read().decode('utf-8')print(html)except URLError as e:print(e.reason)这里首先实例化 HTTPBasicAuthHandler 对象,其参数是 HTTPPasswordMgrWithDefaultRealm 对象,它利用 add_password 方法添加进去用户名和密码,这样就建立了一个处理验证的 Handler。
接下来,利用这个 Handler 并使用 build_opener 方法构建一个 Opener,这个 Opener 在发送请求时就相当于已经验证成功了。然后,利用 Opener 的 open 方法打开链接,就可以完成验证了。这里获取到的结果就是验证后的页面源码内容。
-
代理
有些网站会检测某一段时间某个 IP 的访问次数,如果访问次数过多,它会禁止你的访问。所以我们可以设置一些代理服务器,每隔一段时间换一个代理,就算 IP 被禁止,依然可以换个 IP 继续爬取。
代理的设置也是通过 Handler 来实现的,这里介绍下 ProxyHandler 的用法:
from urllib.error import URLErrorfrom urllib.request import ProxyHandler, build_openerproxy_handler = ProxyHandler({'http': '<http://127.0.0.1>:9743','https': '<https://127.0.0.1:9743>'})opener = build_opener(proxy_handler)try:response = opener.open('<https://www.baidu.com>')print(response.read().decode('utf-8'))except URLError as e:print(e.reason)这里我们在本地搭建了一个代理,它运行在 9743 端口上。以及使用了 ProxyHandler,其参数是一个字典,键名是协议类型(比如 HTTP 或者 HTTPS 等),键值是代理链接,可以添加多个代理。
然后,利用这个 Handler 及 build_opener 方法构造一个 Opener,之后发送请求即可。
-
Cookies
Cookies 的处理就需要相关的 Handler 了。
import http.cookiejar, urllib.requestcookie = http.cookiejar.CookieJar()handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')for item in cookie:print(item.name+"="+item.value)首先,我们必须声明一个 CookieJar 对象。接下来,就需要利用 HTTPCookieProcessor 来构建一个 Handler,最后利用 build_opener 方法构建出 Opener,执行 open 函数即可。
可以看到,这里输出了每条 Cookie 的名称和值。
也可以输出成文件格式,我们知道 Cookies 实际上也是以文本形式保存的。
filename = 'cookies.txt'cookie = http.cookiejar.MozillaCookieJar(filename)handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')cookie.save(ignore_discard=True, ignore_expires=True)这时 CookieJar 就需要换成 MozillaCookieJar,它在生成文件时会用到,是 CookieJar 的子类,可以用来处理 Cookies 和文件相关的事件,比如读取和保存 Cookies,可以将 Cookies 保存成 Mozilla 型浏览器的 Cookies 格式。
另外,LWPCookieJar 同样可以读取和保存 Cookies,但是保存的格式和 MozillaCookieJar 不一样,它会保存成 libwww-perl(LWP) 格式的 Cookies 文件。
要保存成 LWP 格式的 Cookies 文件,可以在声明时就改为:
cookie = http.cookiejar.LWPCookieJar(filename)那么,生成了 Cookies 文件后,怎样从文件中读取并利用?
cookie = http.cookiejar.LWPCookieJar()cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True)handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')print(response.read().decode('utf-8'))可以看到,这里调用 load 方法来读取本地的 Cookies 文件,获取到了 Cookies 的内容。不过前提是我们首先生成了 LWPCookieJar 格式的 Cookies,并保存成文件,然后读取 Cookies 之后使用同样的方法构建 Handler 和 Opener 即可完成操作。
运行结果正常的话,会输出百度网页的源代码。
通过上面的方法,我们可以实现绝大多数请求功能的设置了。
这便是 urllib 库中 request 模块的基本用法,如果想实现更多的功能,可以参考官方文档的说明: https://docs.python.org/3/library/urllib.request.html#basehandler-objects
处理异常
前一节我们了解了请求的发送过程,但是在网络不好的情况下,如果出现了异常,该怎么办呢?这时如果不处理这些异常,程序很可能因报错而终止运行,所以异常处理还是十分有必要的。 urllib 的 error 模块定义了由 request 模块产生的异常。如果出现了问题,request 模块便会抛出 error 模块中定义的异常。
URLError
URLError 类来自 urllib 库的 error 模块,它继承自 OSError 类,是 error 异常模块的基类,由 request 模块产生的异常都可以通过捕获这个类来处理。 它具有一个属性 reason,即返回错误的原因。
from urllib import request, errortry: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.URLError as e: print(e.reason)我们打开一个不存在的页面,照理来说应该会报错,但是这时我们捕获了 URLError 这个异常,运行结果如下:
Not Found
程序没有直接报错,而是输出了如上内容,这样通过如上操作,我们就可以避免程序异常终止,同时异常得到了有效处理。
HTTPError
它是 URLError 的子类,专门用来处理 HTTP 请求错误,比如认证请求失败等。它有如下 3 个属性。
- code:返回 HTTP 状态码,比如 404 表示网页不存在,500 表示服务器内部错误等。
- reason:同父类一样,用于返回错误的原因。
- headers:返回请求头。
from urllib import request,errortry: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.HTTPError as e: print(e.reason, e.code, e.headers, sep='\\n')运行结果如下:
Not Found404Server: nginx/1.4.6 (Ubuntu)Date: Wed, 03 Aug 2016 08:54:22 GMTContent-Type: text/html; charset=UTF-8Transfer-Encoding: chunkedConnection: closeX-Powered-By: PHP/5.5.9-1ubuntu4.14Vary: CookieExpires: Wed, 11 Jan 1984 05:00:00 GMTCache-Control: no-cache, must-revalidate, max-age=0Pragma: no-cacheLink: <https://cuiqingcai.com/wp-json/>; rel="<https://api.w.org/>"依然是同样的网址,这里捕获了 HTTPError 异常,输出了 reason、code 和 headers 属性。 因为 URLError 是 HTTPError 的父类,所以可以先选择捕获子类的错误,再去捕获父类的错误,所以上述代码更好的写法如下:
from urllib import request, error
try: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.HTTPError as e: print(e.reason, e.code, e.headers, sep='\\n')except error.URLError as e: print(e.reason)else: print('Request Successfully')这样就可以做到先捕获 HTTPError,获取它的错误状态码、原因、headers 等信息。如果不是 HTTPError 异常,就会捕获 URLError 异常,输出错误原因。最后,用 else 来处理正常的逻辑。这是一个较好的异常处理写法。 有时候,reason 属性返回的不一定是字符串,也可能是一个对象。
import socketimport urllib.requestimport urllib.error
try: response = urllib.request.urlopen('<https://www.baidu.com>', timeout=0.01)except urllib.error.URLError as e: print(type(e.reason)) if isinstance(e.reason, socket.timeout): print('TIME OUT')这里我们直接设置超时时间来强制抛出 timeout 异常。
运行结果如下:
<class'socket.timeout'> TIME OUT
可以发现,reason 属性的结果是 socket.timeout 类。所以,这里我们可以用 isinstance 方法来判断它的类型,作出更详细的异常判断。
解析链接
前面说过,urllib 库里还提供了 parse 模块,它定义了处理 URL 的标准接口,例如实现 URL 各部分的抽取、合并以及链接转换。它支持如下协议的 URL 处理:file、ftp、gopher、hdl、http、https、imap、mailto、 mms、news、nntp、prospero、rsync、rtsp、rtspu、sftp、 sip、sips、snews、svn、svn+ssh、telnet 和 wais。本节中,我们介绍一下该模块中常用的方法来看一下它的便捷之处。
urlparse
该方法可以实现 URL 的识别和分段
from urllib.parse import urlparse
result = urlparse('<http://www.baidu.com/index.html;user?id=5#comment>')print(type(result), result)<class 'urllib.parse.ParseResult'> ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
可以看到,返回结果是一个 ParseResult 类型的对象,它包含 6 个部分,分别是 scheme、netloc、path、params、query 和 fragment。
可以发现,urlparse 方法将其拆分成了 6 个部分。大体观察可以发现,解析时有特定的分隔符。比如,:// 前面的就是 scheme,代表协议;第一个 / 符号前面便是 netloc,即域名,后面是 path,即访问路径;分号;后面是 params,代表参数;问号?后面是查询条件 query,一般用作 GET 类型的 URL;井号 #后面是锚点,用于直接定位页面内部的下拉位置。
所以,可以得出一个标准的链接格式,具体如下:
scheme://netloc/path;params?query#fragment
一个标准的 URL 都会符合这个规则,利用 urlparse 方法可以将它拆分开来。
# API配置urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)可以看到,它有 3 个参数。
- urlstring:这是必填项,即待解析的 URL。
- scheme:它是默认的协议(比如 http 或 https 等)。 scheme 参数只有在 URL 中不包含 scheme 信息时才生效。如果 URL 中有 scheme 信息,就会返回解析出的 scheme。
- allow_fragments:即是否忽略 fragment。如果它被设置为 False,fragment 部分就会被忽略,它会被解析为 path、parameters 或者 query 的一部分,而 fragment 部分为空。
urlunparse
有了 urlparse 方法,相应地就有了它的对立方法 urlunparse。它接受的参数是一个可迭代对象,但是它的长度必须是 6,否则会抛出参数数量不足或者过多的问题。
from urllib.parse import urlunparse
data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']print(urlunparse(data))这里参数 data 用了列表类型。当然,你也可以用其他类型,比如元组或者特定的数据结构。
http://www.baidu.com/index.html;user?a=6#comment
urlsplit
这个方法和 urlparse 方法非常相似,只不过它不再单独解析 params 这一部分,只返回 5 个结果。上面例子中的 params 会合并到 path 中。
from urllib.parse import urlsplit
result = urlsplit('<http://www.baidu.com/index.html;user?id=5#comment>')print(result)返回结果是 SplitResult,它其实也是一个元组类型,既可以用属性获取值,也可以用索引来获取。
urlunsplit
与 urlunparse 方法类似,它也是将链接各个部分组合成完整链接的方法,传入的参数也是一个可迭代对象,例如列表、元组等,唯一的区别是长度必须为 5。
from urllib.parse import urlunsplit
data = ['http', 'www.baidu.com', 'index.html', 'a=6', 'comment']print(urlunsplit(data))http://www.baidu.com/index.html?a=6#comment
urljoin
有了 urlunparse 和 urlunsplit 方法,我们可以完成链接的合并,不过前提必须要有特定长度的对象,链接的每一部分都要清晰分开。 此外,生成链接还有另一个方法,那就是 urljoin 方法。我们可以提供一个 base_url(基础链接)作为第一个参数,将新的链接作为第二个参数,该方法会分析 base_url 的 scheme、netloc 和 path 这 3 个内容并对新链接缺失的部分进行补充,最后返回结果。
from urllib.parse import urljoin
print(urljoin('<http://www.baidu.com>', 'FAQ.html'))print(urljoin('<http://www.baidu.com>', '<https://cuiqingcai.com/FAQ.html>'))print(urljoin('<http://www.baidu.com/about.html>', '<https://cuiqingcai.com/FAQ.html>'))print(urljoin('<http://www.baidu.com/about.html>', '<https://cuiqingcai.com/FAQ.html?question=2>'))print(urljoin('<http://www.baidu.com?wd=abc>', '<https://cuiqingcai.com/index.php>'))print(urljoin('<http://www.baidu.com>', '?category=2#comment'))print(urljoin('www.baidu.com', '?category=2#comment'))print(urljoin('www.baidu.com#comment', '?category=2'))运行结果如下:
<http://www.baidu.com/FAQ.html><https://cuiqingcai.com/FAQ.html><https://cuiqingcai.com/FAQ.html><https://cuiqingcai.com/FAQ.html?question=2><https://cuiqingcai.com/index.php><http://www.baidu.com?category=2#comment>www.baidu.com?category=2#commentwww.baidu.com?category=2可以发现,base_url 提供了三项内容 scheme、netloc 和 path。如果这 3 项在新的链接里不存在,就予以补充;如果新的链接存在,就使用新的链接的部分。而 base_url 中的 params、query 和 fragment 是不起作用的。 通过 urljoin 方法,我们可以轻松实现链接的解析、拼合与生成。
urlencode
这里我们再介绍一个常用的方法 ——urlencode,它在构造 GET 请求参数的时候非常有用
from urllib.parse import urlencode
params = { 'name': 'germey', 'age': 22}base_url = '<http://www.baidu.com>?'url = base_url + urlencode(params)print(url)这里首先声明了一个字典来将参数表示出来,然后调用 urlencode 方法将其序列化为 GET 请求参数。
http://www.baidu.com?name=germey&age=22
可以看到,参数就成功地由字典类型转化为 GET 请求参数了。 这个方法非常常用。有时为了更加方便地构造参数,我们会事先用字典来表示。要转化为 URL 的参数时,只需要调用该方法即可。
parse_qs
有了序列化,必然就有反序列化。如果我们有一串 GET 请求参数,利用 parse_qs 方法,就可以将它转回字典,示例如下:
from urllib.parse import parse_qs
query = 'name=germey&age=22'print(parse_qs(query))parse_qsl
另外,还有一个 parse_qsl 方法,它用于将参数转化为元组组成的列表
from urllib.parse import parse_qsl
query = 'name=germey&age=22'print(parse_qsl(query))quote
该方法可以将内容转化为 URL 编码的格式。URL 中带有中文参数时,有时可能会导致乱码的问题,此时用这个方法可以将中文字符转化为 URL 编码
from urllib.parse import quote
keyword = ' 壁纸 'url = '<https://www.baidu.com/s?wd=>' + quote(keyword)print(url)这里我们声明了一个中文的搜索文字,然后用 quote 方法对其进行 URL 编码,最后得到的结果如下:
https://www.baidu.com/s?wd=% E5% A3%81% E7% BA% B8
unquote
有了 quote 方法,当然还有 unquote 方法,它可以进行 URL 解码,示例如下:
from urllib.parse import unquote
url = '<https://www.baidu.com/s?wd=%> E5% A3%81% E7% BA% B8'print(unquote(url))这是上面得到的 URL 编码后的结果,这里利用 unquote 方法还原,结果如下:
https://www.baidu.com/s?wd = 壁纸
可以看到,利用 unquote 方法可以方便地实现解码。
分析 Robots 协议
利用 urllib 的 robotparser 模块,我们可以实现网站 Robots 协议的分析。本节中,我们来简单了解一下该模块的用法。
Robots 协议
Robots 协议也称作爬虫协议、机器人协议,它的全名叫作网络爬虫排除标准(Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以抓取,哪些不可以抓取。它通常是一个叫作 robots.txt 的文本文件,一般放在网站的根目录下。 当搜索爬虫访问一个站点时,它首先会检查这个站点根目录下是否存在 robots.txt 文件,如果存在,搜索爬虫会根据其中定义的爬取范围来爬取。如果没有找到这个文件,搜索爬虫便会访问所有可直接访问的页面。 下面我们看一个 robots.txt 的样例:
User-agent: *Disallow: /Allow: /public/3这实现了对所有搜索爬虫只允许爬取 public 目录的功能,将上述内容保存成 robots.txt 文件,放在网站的根目录下,和网站的入口文件(比如 index.php、index.html 和 index.jsp 等)放在一起。
上面的 User-agent 描述了搜索爬虫的名称,这里将其设置为* 则代表该协议对任何爬取爬虫有效。比如,我们可以设置:
User-agent: Baiduspider
这就代表我们设置的规则对百度爬虫是有效的。如果有多条 User-agent 记录,则就会有多个爬虫会受到爬取限制,但至少需要指定一条。
Disallow 指定了不允许抓取的目录,比如上例子中设置为 / 则代表不允许抓取所有页面。
Allow 一般和 Disallow 一起使用,一般不会单独使用,用来排除某些限制。现在我们设置为 /public/,则表示所有页面不允许抓取,但可以抓取 public 目录。
下面我们再来看几个例子。
<!-- 禁止所有爬虫访问任何目录的代码如下: -->User-agent: *Disallow: /<!-- 允许所有爬虫访问任何目录的代码如下: -->User-agent: *Disallow:<!-- 另外,直接把 robots.txt 文件留空也是可以的。 --><!-- 禁止所有爬虫访问网站某些目录的代码如下: -->User-agent: *Disallow: /private/Disallow: /tmp/<!-- 只允许某一个爬虫访问的代码如下: -->User-agent: WebCrawlerDisallow:User-agent: *Disallow: /这些是 robots.txt 的一些常见写法。
爬虫名称
大家可能会疑惑,爬虫名是哪儿来的?为什么就叫这个名?其实它是有固定名字的了,比如百度的就叫作 BaiduSpider。
| 爬虫名称 | 名 称 | 网 站 |
|---|---|---|
| BaiduSpider | 百度 | <www.baidu.com> |
| Googlebot | 谷歌 | <www.google.com> |
| 360Spider | 360 搜索 | <www.so.com> |
| YodaoBot | 有道 | <www.youdao.com> |
| ia_archiver | Alexa | <www.alexa.cn> |
| Scooter | altavista | <www.altavista.com> |
robotparser
了解 Robots 协议之后,我们就可以使用 robotparser 模块来解析 robots.txt 了。该模块提供了一个类 RobotFileParser,它可以根据某网站的 robots.txt 文件来判断一个爬取爬虫是否有权限来爬取这个网页。
该类用起来非常简单,只需要在构造方法里传入 robots.txt 的链接即可。首先看一下它的声明:
urllib.robotparser.RobotFileParser(url='')
当然,也可以在声明时不传入,默认为空,最后再使用 set_url() 方法设置一下也可。
下面列出了这个类常用的几个方法。
- set_url :用来设置 robots.txt 文件的链接。如果在创建 RobotFileParser 对象时传入了链接,那么就不需要再使用这个方法设置了。
- read:读取 robots.txt 文件并进行分析。注意,这个方法执行一个读取和分析操作,如果不调用这个方法,接下来的判断都会为 False,所以一定记得调用这个方法。这个方法不会返回任何内容,但是执行了读取操作。
- parse:用来解析 robots.txt 文件,传入的参数是 robots.txt 某些行的内容,它会按照 robots.txt 的语法规则来分析这些内容。 can_fetch:该方法传入两个参数,第一个是 User-agent,第二个是要抓取的 URL。返回的内容是该搜索引擎是否可以抓取这个 URL,返回结果是 True 或 False。
- mtime:返回的是上次抓取和分析 robots.txt 的时间,这对于长时间分析和抓取的搜索爬虫是很有必要的,你可能需要定期检查来抓取最新的 robots.txt。
- modified:它同样对长时间分析和抓取的搜索爬虫很有帮助,将当前时间设置为上次抓取和分析 robots.txt 的时间。
下面我们用实例来看一下:
from urllib.robotparser import RobotFileParserrp = RobotFileParser()rp.set_url('<http://www.jianshu.com/robots.txt>')rp.read()print(rp.can_fetch('*', '<http://www.jianshu.com/p/b67554025d7d>'))print(rp.can_fetch('*', "<http://www.jianshu.com/search?q=python&page=1&type=collections>"))这里以简书为例,首先创建 RobotFileParser 对象,然后通过 set_url 方法设置了 robots.txt 的链接。当然,不用这个方法的话,可以在声明时直接用如下方法设置:
rp = RobotFileParser('<http://www.jianshu.com/robots.txt>')
接着利用 can_fetch 方法判断了网页是否可以被抓取。
运行结果如下:
TrueFalse这里同样可以使用 parse 方法执行读取和分析,示例如下:
from urllib.robotparser import RobotFileParserfrom urllib.request import urlopenrp = RobotFileParser()rp.parse(urlopen('<http://www.jianshu.com/robots.txt').read().decode('utf-8').split('\\n>'))print(rp.can_fetch('*', '<http://www.jianshu.com/p/b67554025d7d>'))print(rp.can_fetch('*', "<http://www.jianshu.com/search?q=python&page=1&type=collections>"))requests
上一节中,我们了解了 urllib 的基本用法,但是其中确实有不方便的地方,比如处理网页验证和 Cookies 时,需要写 Opener 和 Handler 来处理。为了更加方便地实现这些操作,就有了更为强大的库 requests,有了它,Cookies、登录验证、代理设置等操作都不是事儿。 接下来,让我们领略一下它的强大之处吧。
基本用法
-
准备工作 在开始之前,请确保已经正确安装好了 requests 库。
-
实例引入 urllib 库中的 urlopen 方法实际上是以 GET 方式请求网页,而 requests 中相应的方法就是 get 方法。下面通过实例来看一下:
import requestsr = requests.get('<https://www.baidu.com/>')print(type(r))print(r.status_code)print(type(r.text))print(r.text)print(r.cookies)运行结果如下:
<class 'requests.models.Response'>200<class'str'><html><head><script>location.replace(location.href.replace("https://","http://"));</script></head><body><noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript></body></html><RequestsCookieJar[<Cookie BIDUPSID=992C3B26F4C4D09505C5E959D5FBC005 for .baidu.com/>, <CookiePSTM=1472227535 for .baidu.com/>, <Cookie __bsi=15304754498609545148_00_40_N_N_2_0303_C02F_N_N_N_0for .www.baidu.com/>, <Cookie BD_NOT_HTTPS=1 for www.baidu.com/>]>这里我们调用 get 方法实现与 urlopen 相同的操作,得到一个 Response 对象,然后分别输出了 Response 的类型、状态码、响应体的类型、内容以及 Cookies。
通过运行结果可以发现,它的返回类型是 requests.models.Response,响应体的类型是字符串 str,Cookies 的类型是 RequestsCookieJar。
使用 get 方法成功实现一个 GET 请求,这倒不算什么,更方便之处在于其他的请求类型依然可以用一句话来完成,示例如下:
r = requests.post('<http://httpbin.org/post>')r = requests.put('<http://httpbin.org/put>')r = requests.delete('<http://httpbin.org/delete>')r = requests.head('<http://httpbin.org/get>')r = requests.options('<http://httpbin.org/get>')这里分别用 post、put、delete 等方法实现了 POST、PUT、DELETE 等请求。
-
GET 请求
HTTP 中最常见的请求之一就是 GET 请求,下面首先来详细了解一下利用 requests 构建 GET 请求的方法。 基本实例 首先,构建一个最简单的 GET 请求,请求的链接为 http://httpbin.org/get,该网站会判断如果客户端发起的是 GET 请求的话,它返回相应的请求信息:
import requestsr = requests.get('<http://httpbin.org/get>')print(r.text)运行结果如下:
{"args": {},"headers": {"Accept": "*/*","Accept-Encoding": "gzip, deflate","Host": "httpbin.org","User-Agent": "python-requests/2.10.0"},"origin": "122.4.215.33","url": "<http://httpbin.org/get>"}可以发现,我们成功发起了 GET 请求,返回结果中包含请求头、URL、IP 等信息。 那么,对于 GET 请求,如果要附加额外的信息,利用 params 这个参数进行存储
import requestsdata = {'name': 'germey','age': 22}r = requests.get("<http://httpbin.org/get>", params=data)print(r.text)请求的链接会被自动被构造成:http://httpbin.org/get?age=22&name=germey。
另外,网页的返回类型实际上是 str 类型,但是它很特殊,是 JSON 格式的。所以,如果想直接解析返回结果,得到一个字典格式的话,可以直接调用 json 方法。示例如下:
import requestsr = requests.get("<http://httpbin.org/get>")print(type(r.text))print(r.json())print(type(r.json()))调用 json 方法,就可以将返回结果是 JSON 格式的字符串转化为字典。
但需要注意的是,如果返回结果不是 JSON 格式,便会出现解析错误,抛出 json.decoder.JSONDecodeError 异常。
-
抓取网页 上面的请求链接返回的是 JSON 形式的字符串,那么如果请求普通的网页,则肯定能获得相应的内容了。下面以 “知乎”→“发现” 页面为例来看一下:
import requestsimport reheaders = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}r = requests.get("<https://www.zhihu.com/explore>", headers=headers)pattern = re.compile('explore-feed.*?question_link.*?>(.*?)</a>', re.S)titles = re.findall(pattern, r.text)print(titles)这里我们加入了 headers 信息,其中包含了 User-Agent 字段信息,也就是浏览器标识信息。如果不加这个,知乎会禁止抓取。
-
抓取二进制数据
在上面的例子中,我们抓取的是知乎的一个页面,实际上它返回的是一个 HTML 文档。如果想抓取图片、音频、视频等文件,应该怎么办呢? 图片、音频、视频这些文件本质上都是由二进制码组成的,由于有特定的保存格式和对应的解析方式,我们才可以看到这些形形色色的多媒体。所以,想要抓取它们,就要拿到它们的二进制码。
下面以 GitHub 的站点图标为例来看一下:
import requestsr = requests.get("<https://github.com/favicon.ico>")print(r.text)print(r.content)这里抓取的内容是站点图标,也就是在浏览器每一个标签上显示的小图标
接着,我们将刚才提取到的图片保存下来:
import requestsr = requests.get("<https://github.com/favicon.ico>")with open('favicon.ico', 'wb') as f:f.write(r.content)这里用了 open 方法,它的第一个参数是文件名称,第二个参数代表以二进制写的形式打开,可以向文件里写入二进制数据。
运行结束之后,可以发现在文件夹中出现了名为 favicon.ico 的图标。
同样地,音频和视频文件也可以用这种方法获取。
-
添加 headers
与 urllib.request 一样,我们也可以通过 headers 参数来传递头信息。
import requestsheaders = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}r = requests.get("<https://www.zhihu.com/explore>", headers=headers)print(r.text)当然,我们可以在 headers 这个参数中任意添加其他的字段信息。
-
-
POST 请求
前面我们了解了最基本的 GET 请求,另外一种比较常见的请求方式是 POST
import requestsdata = {'name': 'germey', 'age': '22'}r = requests.post("<http://httpbin.org/post>", data=data)print(r.text) -
响应
发送请求后,得到的自然就是响应。在上面的实例中,我们使用 text 和 content 获取了响应的内容。此外,还有很多属性和方法可以用来获取其他信息,比如状态码、响应头、Cookies 等。
import requestsr = requests.get('<http://www.jianshu.com>')print(type(r.status_code), r.status_code)print(type(r.headers), r.headers)print(type(r.cookies), r.cookies)print(type(r.url), r.url)print(type(r.history), r.history)这里分别打印输出 status_code 属性得到状态码,输出 headers 属性得到响应头,输出 cookies 属性得到 Cookies,输出 url 属性得到 URL,输出 history 属性得到请求历史。
状态码常用来判断请求是否成功,而 requests 还提供了一个内置的状态码查询对象 requests.codes
import requestsr = requests.get('<http://www.jianshu.com>')exit() if not r.status_code == requests.codes.ok else print('Request Successfully')这里通过比较返回码和内置的成功的返回码,来保证请求得到了正常响应,输出成功请求的消息,否则程序终止,这里我们用 requests.codes.ok 得到的是成功的状态码 200。.
下面列出了返回码和相应的查询条件:
# 信息性状态码100: ('continue',),101: ('switching_protocols',),102: ('processing',),103: ('checkpoint',),122: ('uri_too_long', 'request_uri_too_long'),# 成功状态码200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\\\o/', '✓'),201: ('created',),202: ('accepted',),203: ('non_authoritative_info', 'non_authoritative_information'),204: ('no_content',),205: ('reset_content', 'reset'),206: ('partial_content', 'partial'),207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'),208: ('already_reported',),226: ('im_used',),# 重定向状态码300: ('multiple_choices',),301: ('moved_permanently', 'moved', '\\\\o-'),302: ('found',),303: ('see_other', 'other'),304: ('not_modified',),305: ('use_proxy',),306: ('switch_proxy',),307: ('temporary_redirect', 'temporary_moved', 'temporary'),308: ('permanent_redirect','resume_incomplete', 'resume',), # These 2 to be removed in 3.0# 客户端错误状态码400: ('bad_request', 'bad'),401: ('unauthorized',),402: ('payment_required', 'payment'),403: ('forbidden',),404: ('not_found', '-o-'),405: ('method_not_allowed', 'not_allowed'),406: ('not_acceptable',),407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'),408: ('request_timeout', 'timeout'),409: ('conflict',),410: ('gone',),411: ('length_required',),412: ('precondition_failed', 'precondition'),413: ('request_entity_too_large',),414: ('request_uri_too_large',),415: ('unsupported_media_type', 'unsupported_media', 'media_type'),416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'),417: ('expectation_failed',),418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'),421: ('misdirected_request',),422: ('unprocessable_entity', 'unprocessable'),423: ('locked',),424: ('failed_dependency', 'dependency'),425: ('unordered_collection', 'unordered'),426: ('upgrade_required', 'upgrade'),428: ('precondition_required', 'precondition'),429: ('too_many_requests', 'too_many'),431: ('header_fields_too_large', 'fields_too_large'),444: ('no_response', 'none'),449: ('retry_with', 'retry'),450: ('blocked_by_windows_parental_controls', 'parental_controls'),451: ('unavailable_for_legal_reasons', 'legal_reasons'),499: ('client_closed_request',),# 服务端错误状态码500: ('internal_server_error', 'server_error', '/o\\\\', '✗'),501: ('not_implemented',),502: ('bad_gateway',),503: ('service_unavailable', 'unavailable'),504: ('gateway_timeout',),505: ('http_version_not_supported', 'http_version'),506: ('variant_also_negotiates',),507: ('insufficient_storage',),509: ('bandwidth_limit_exceeded', 'bandwidth'),510: ('not_extended',),511: ('network_authentication_required', 'network_auth', 'network_authentication')比如,如果想判断结果是不是 404 状态,可以用 requests.codes.not_found 来比对。
requests高级用法
在前一节中,我们了解了 requests 的基本用法,如基本的 GET、POST 请求以及 Response 对象。本节中,我们再来了解下 requests 的一些高级用法,如文件上传、Cookies 设置、代理设置等。
-
文件上传
我们知道 requests 可以模拟提交一些数据。假如有的网站需要上传文件,我们也可以用它来实现
import requestsfiles = {'file': open('favicon.ico', 'rb')}r = requests.post('<http://httpbin.org/post>', files=files)print(r.text)这个网站会返回响应,里面包含 files 这个字段,而 form 字段是空的,这证明文件上传部分会单独有一个 files 字段来标识。
-
Cookies
前面我们使用 urllib 处理过 Cookies,写法比较复杂,而有了 requests,获取和设置 Cookies 只需一步即可完成。
import requestsr = requests.get('<https://www.baidu.com>')print(r.cookies)for key, value in r.cookies.items():print(key + '=' + value)这里我们首先调用 cookies 属性即可成功得到 Cookies,可以发现它是 RequestCookieJar 类型。然后用 items 方法将其转化为元组组成的列表,遍历输出每一个 Cookie 的名称和值,实现 Cookie 的遍历解析。
当然,我们也可以直接用 Cookie 来维持登录状态,下面以知乎为例来说明。
首先登录知乎,将 Headers 中的 Cookie 内容复制下来,将其设置到 Headers 里面,然后发送请求
import requestsheaders = {'Cookie': '','Host': 'www.zhihu.com','User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36',}r = requests.get('<https://www.zhihu.com>', headers=headers)print(r.text)当然,你也可以通过 cookies 参数来设置,不过这样就需要构造 RequestsCookieJar 对象,而且需要分割一下 cookies。这相对烦琐,不过效果是相同的
import requestscookies = ''jar = requests.cookies.RequestsCookieJar()headers = {'Host': 'www.zhihu.com','User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'}for cookie in cookies.split(';'):key, value = cookie.split('=', 1)jar.set(key, value)r = requests.get('<http://www.zhihu.com>', cookies=jar, headers=headers)print(r.text)这里我们首先新建了一个 RequestCookieJar 对象,然后将复制下来的 cookies 利用 split 方法分割,接着利用 set 方法设置好每个 Cookie 的 key 和 value,然后通过调用 requests 的 get() 方法并传递给 cookies 参数即可。当然,由于知乎本身的限制,headers 参数也不能少,只不过不需要在原来的 headers 参数里面设置 cookie 字段了。
测试后,发现同样可以正常登录知乎。
-
会话维持
在 requests 中,如果直接利用 get 或 post 等方法的确可以做到模拟网页的请求,但是这实际上是相当于不同的会话,也就是说相当于你用了两个浏览器打开了不同的页面。
设想这样一个场景,第一个请求利用 post 方法登录了某个网站,第二次想获取成功登录后的自己的个人信息,你又用了一次 get 方法去请求个人信息页面。实际上,这相当于打开了两个浏览器,是两个完全不相关的会话,能成功获取个人信息吗?那当然不能。
其实解决这个问题的主要方法就是维持同一个会话,也就是相当于打开一个新的浏览器选项卡而不是新开一个浏览器。但是我又不想每次设置 cookies,那该怎么办呢?这时候就有了新的利器 ——Session 对象。
利用它,我们可以方便地维护一个会话,而且不用担心 cookies 的问题,它会帮我们自动处理好。
import requestss = requests.Session()s.get('<http://httpbin.org/cookies/set/number/123456789>')r = s.get('<http://httpbin.org/cookies>')print(r.text)所以,利用 Session,可以做到模拟同一个会话而不用担心 Cookies 的问题。它通常用于模拟登录成功之后再进行下一步的操作。 Session 在平常用得非常广泛,可以用于模拟在一个浏览器中打开同一站点的不同页面
-
SSL 证书验证
此外,requests 还提供了证书验证的功能。当发送 HTTP 请求的时候,它会检查 SSL 证书,我们可以使用 verify 参数控制是否检查此证书。其实如果不加 verify 参数的话,默认是 True,会自动验证。
如果请求一个 HTTPS 站点,但是证书验证错误的页面时,就会提示错误 SSLError,表示证书验证错误。那么如何避免这个错误呢?把 verify 参数设置为 False 即可。
import requestsresponse = requests.get('<https://www.12306.cn>', verify=False)print(response.status_code)当然,我们也可以指定一个本地证书用作客户端证书,这可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组:
import requestsresponse = requests.get('<https://www.12306.cn>', cert=('/path/server.crt', '/path/key'))print(response.status_code)注意,本地私有证书的 key 必须是解密状态,加密状态的 key 是不支持的。
-
代理设置
对于某些网站,在测试的时候请求几次,能正常获取内容。但是一旦开始大规模爬取,对于大规模且频繁的请求,网站可能会弹出验证码,或者跳转到登录认证页面,更甚者可能会直接封禁客户端的 IP,导致一定时间段内无法访问。 那么,为了防止这种情况发生,我们需要设置代理来解决这个问题,这就需要用到 proxies 参数。可以用这样的方式设置:
import requestsproxies = {'http': '<http://10.10.1.10:3128>','https': '<http://10.10.1.10:1080>',}requests.get('<https://www.taobao.com>', proxies=proxies)当然,直接运行这个实例可能不行,因为这个代理可能是无效的,请换成自己的有效代理试验一下。
若代理需要使用 HTTP Basic Auth,可以使用类似 http://user
@host 这样的语法来设置代理,示例如下: import requestsrequests.get('<https://www.taobao.com>', proxies=proxies)除了基本的 HTTP 代理外,requests 还支持 SOCKS 协议的代理。 首先,需要安装 socks 这个库:
pip3 install"requests[socks]"然后就可以使用 SOCKS 协议代理了,示例如下:import requestsproxies = {'http': 'socks5://user:password@host:port','https': 'socks5://user:password@host:port'}requests.get('<https://www.taobao.com>', proxies=proxies) -
超时设置
在本机网络状况不好或者服务器网络响应太慢甚至无响应时,我们可能会等待特别久的时间才可能收到响应,甚至到最后收不到响应而报错。为了防止服务器不能及时响应,应该设置一个超时时间,即超过了这个时间还没有得到响应,那就报错。这需要用到 timeout 参数。这个时间的计算是发出请求到服务器返回响应的时间。
import requestsr = requests.get('<https://www.taobao.com>', timeout=1)print(r.status_code)通过这样的方式,我们可以将超时时间设置为 1 秒,如果 1 秒内没有响应,那就抛出异常。 实际上,请求分为两个阶段,即连接(connect)和读取(read)。 上面设置的 timeout 将用作连接和读取这二者的 timeout 总和。 如果要分别指定,就可以传入一个元组:
r = requests.get('<https://www.taobao.com>', timeout=(5, 30))如果想永久等待,可以直接将 timeout 设置为 None,或者不设置直接留空,因为默认是 None。这样的话,如果服务器还在运行,但是响应特别慢,那就慢慢等吧,它永远不会返回超时错误的。其用法如下:r = requests.get('<https://www.taobao.com>', timeout=None)或直接不加参数:r = requests.get('<https://www.taobao.com>') -
身份认证
在访问网站时,我们可能会遇到登录认证的页面。
此时可以使用 requests 自带的身份认证功能
import requestsfrom requests.auth import HTTPBasicAuthr = requests.get('<http://localhost:5000>', auth=HTTPBasicAuth('username', 'password'))print(r.status_code)如果用户名和密码正确的话,请求时就会自动认证成功,会返回 200 状态码;如果认证失败,则返回 401 状态码。 当然,如果参数都传一个 HTTPBasicAuth 类,就显得有点烦琐了,所以 requests 提供了一个更简单的写法,可以直接传一个元组,它会默认使用 HTTPBasicAuth 这个类来认证。
所以上面的代码可以直接简写如下:
import requestsr = requests.get('<http://localhost:5000>', auth=('username', 'password'))print(r.status_code)此外,requests 还提供了其他认证方式,如 OAuth 认证,不过此时需要安装 oauth 包,安装命令如下:
pip3 install requests_oauthlib使用 OAuth1 认证的方法如下:import requestsfrom requests_oauthlib import OAuth1url = '<https://api.twitter.com/1.1/account/verify_credentials.json>'auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET','USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')requests.get(url, auth=auth)更多详细的功能就可以参考 requests_oauthlib 的官方文档:https://requests-oauthlib.readthedocs.org/
-
Prepared Request
前面介绍 urllib 时,我们可以将请求表示为数据结构,其中各个参数都可以通过一个 Request 对象来表示。这在 requests 里同样可以做到,这个数据结构就叫 Prepared Request
from requests import Request, Sessionurl = '<http://httpbin.org/post>'data = {'name': 'germey'}headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'}s = Session()req = Request('POST', url, data=data, headers=headers)prepped = s.prepare_request(req)r = s.send(prepped)print(r.text)这里我们引入了 Request,然后用 url、data 和 headers 参数构造了一个 Request 对象,这时需要再调用 Session 的 prepare_request 方法将其转换为一个 Prepared Request 对象,然后调用 send 方法发送即可
有了 Request 这个对象,就可以将请求当作独立的对象来看待,这样在进行队列调度时会非常方便。后面我们会用它来构造一个 Request 队列。
更多的用法可以参考 Requests 的官方文档:http://docs.python-requests.org/
正则表达式
本节中,我们看一下正则表达式的相关用法。正则表达式是处理字符串的强大工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证都不在话下。
当然,对于爬虫来说,有了它,从 HTML 里提取想要的信息就非常方便了。
实例引入
说了这么多,可能我们对它到底是个什么还是比较模糊,下面就用几个实例来看一下正则表达式的用法。
打开开源中国提供的正则表达式测试工具 http://tool.oschina.net/regex/,输入待匹配的文本,然后选择常用的正则表达式,就可以得出相应的匹配结果了。例如,这里输入待匹配的文本如下:
Hello, my phone number is 010-86432100 and email is <[email protected]>, and my website is <https://cuiqingcai.com>.这段字符串中包含了一个电话号码和一个电子邮件,接下来就尝试用正则表达式提取出来。
电子邮件开头是一段字符串,然后是一个 @符号,最后是某个域名,这是有特定的组成格式的。另外,对于 URL,开头是协议类型,然后是冒号加双斜线,最后是域名加路径。
对于 URL 来说,可以用下面的正则表达式匹配:
[a-zA-z]+://[^\\s]*
用这个正则表达式去匹配一个字符串,如果这个字符串中包含类似 URL 的文本,那就会被提取出来。
这个正则表达式看上去是乱糟糟的一团,其实不然,这里面都是有特定的语法规则的。比如,a-z 代表匹配任意的小写字母,\s 表示匹配任意的空白字符,* 就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。
写好正则表达式后,就可以拿它去一个长字符串里匹配查找了。不论这个字符串里面有什么,只要符合我们写的规则,统统可以找出来。对于网页来说,如果想找出网页源代码里有多少 URL,用匹配 URL 的正则表达式去匹配即可。
常用的匹配规则
| 模 式 | 描 述 |
|---|---|
| \w | 匹配字母、数字及下划线 |
| \W | 匹配不是字母、数字及下划线的字符 |
| \s | 匹配任意空白字符,等价于 [\t\n\r\f] |
| \S | 匹配任意非空字符 |
| \d | 匹配任意数字,等价于 [0-9] |
| \D | 匹配任意非数字的字符 |
| \A | 匹配字符串开头 |
| \Z | 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串 |
| \z | 匹配字符串结尾,如果存在换行,同时还会匹配换行符 |
| \G | 匹配最后匹配完成的位置 |
| \n | 匹配一个换行符 |
| \t | 匹配一个制表符 |
| ^ | 匹配一行字符串的开头 |
| $ | 匹配一行字符串的结尾 |
| . | 匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符 |
| […] | 用来表示一组字符,单独列出,比如 [amk] 匹配 a、m 或 k |
| ^ | 不在 [] 中的字符,比如 匹配除了 a、b、c 之外的字符 |
| * | 匹配 0 个或多个表达式 |
| + | 匹配 1 个或多个表达式 |
| ? | 匹配 0 个或 1 个前面的正则表达式定义的片段,非贪婪方式 |
| {n} | 精确匹配 n 个前面的表达式 |
| {n, m} | 匹配 n 到 m 次由前面正则表达式定义的片段,贪婪方式 |
| a | b |
| ( ) | 匹配括号内的表达式,也表示一个组 |
其实正则表达式不是 Python 独有的,它也可以用在其他编程语言中。但是 Python 的 re 库提供了整个正则表达式的实现,利用这个库,可以在 Python 中使用正则表达式。在 Python 中写正则表达式几乎都用这个库,下面就来了解它的一些常用方法。
match
这里首先介绍第一个常用的匹配方法 —— match,向它传入要匹配的字符串以及正则表达式,就可以检测这个正则表达式是否匹配字符串。 match 方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果;如果不匹配,就返回 None。示例如下:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'print(len(content))result = re.match('^Hello\\s\\d\\d\\d\\s\\d{4}\\s\\w{10}', content)print(result)print(result.group())print(result.span())这里首先声明了一个字符串,其中包含英文字母、空白字符、数字等。接下来,我们写一个正则表达式^Hello\\s\\d\\d\\d\\s\\d{4}\\s\\w{10}用它来匹配这个长字符串。
开头的 ^ 是匹配字符串的开头,也就是以 Hello 开头;然后 \s 匹配空白字符,用来匹配目标字符串的空格;\d 匹配数字,3 个 \d 匹配 123;然后再写 1 个 \s 匹配空格;后面还有 4567,我们其实可以依然用 4 个 \d 来匹配,但是这么写比较烦琐,所以后面可以跟 {4} 以代表匹配前面的规则 4 次,也就是匹配 4 个数字;然后后面再紧接 1 个空白字符,最后 \w{10} 匹配 10 个字母及下划线。我们注意到,这里其实并没有把目标字符串匹配完,不过这样依然可以进行匹配,只不过匹配结果短一点而已。
而在 match 方法中,第一个参数传入了正则表达式,第二个参数传入了要匹配的字符串。
-
匹配目标
刚才我们用 match 方法可以得到匹配到的字符串内容,但是如果想从字符串中提取一部分内容,该怎么办呢?就像最前面的实例一样,从一段文本中提取出邮件或电话号码等内容。
这里可以使用 () 括号将想提取的子字符串括起来。() 实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用 group 方法传入分组的索引即可获取提取的结果。
import recontent = 'Hello 1234567 World_This is a Regex Demo'result = re.match('^Hello\\s(\\d+)\\sWorld', content)print(result)print(result.group())print(result.group(1))print(result.span())这里我们想把字符串中的 1234567 提取出来,此时可以将数字部分的正则表达式用 () 括起来,然后调用了 group(1) 获取匹配结果。
这里用的是 group(1),它与 group() 有所不同,后者会输出完整的匹配结果,而前者会输出第一个被 () 包围的匹配结果。假如正则表达式后面还有 () 包括的内容,那么可以依次用 group(2)、group(3) 等来获取。
-
通用匹配
刚才我们写的正则表达式其实比较复杂,出现空白字符我们就写 \s 匹配,出现数字我们就用 \d 匹配,这样的工作量非常大。其实完全没必要这么做,因为还有一个万能匹配可以用,那就是.(点星)。其中.(点)可以匹配任意字符(除换行符),(*星)代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意字符了。有了它,我们就不用挨个字符地匹配了。
接着上面的例子,我们可以改写一下正则表达式:
import recontent = 'Hello 123 4567 World_This is a Regex Demo'result = re.match('^Hello.*Demo$', content)print(result)print(result.group())print(result.span())这里我们将中间部分直接省略,全部用 .* 来代替,最后加一个结尾字符串就好了。
group 方法输出了匹配的全部字符串,也就是说我们写的正则表达式匹配到了目标字符串的全部内容;span 方法输出 (0, 41),这是整个字符串的长度。
因此,我们可以使用 .*简化正则表达式的书写。
-
修饰符
正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。
result = re.match('^He.*?(\\d+).*?Demo$', content)当字符串中存在换行符时,运行直接报错,也就是说正则表达式没有匹配到这个字符串,返回结果为 None,而我们又调用了 group 方法导致 AttributeError。
那么,为什么加了一个换行符,就匹配不到了呢?这是因为。匹配的是除换行符之外的任意字符,当遇到换行符时,.*? 就不能匹配了,所以导致匹配失败。这里只需加一个修饰符 re.S,即可修正这个错误:
import recontent = '''Hello 1234567 World_Thisis a Regex Demo'''result = re.match('^He.*?(\\d+).*?Demo$', content, re.S)print(result.group(1))这个修饰符的作用是使。匹配包括换行符在内的所有字符。
这个 re.S 在网页匹配中经常用到。因为 HTML 节点经常会有换行,加上它,就可以匹配节点与节点之间的换行了。
另外,还有一些修饰符,在必要的情况下也可以使用
修饰符 描 述 re.I 使匹配对大小写不敏感 re.L 做本地化识别(locale-aware)匹配 re.M 多行匹配,影响 ^ 和 $ re.S 使。匹配包括换行在内的所有字符 re.U 根据 Unicode 字符集解析字符。这个标志影响 \w、\W、\b 和 \B re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解 在网页匹配中,较为常用的有 re.S 和 re.I。
search
前面提到过,match 方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了。
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'result = re.match('Hello.*?(\\d+).*?Demo', content)print(result)这里的字符串以 Extra 开头,但是正则表达式以 Hello 开头,整个正则表达式是字符串的一部分,但是这样匹配是失败的。
因为 match 方法在使用时需要考虑到开头的内容,这在做匹配时并不方便。它更适合用来检测某个字符串是否符合某个正则表达式的规则。 这里就有另外一个方法 search,它在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果。也就是说,正则表达式可以是字符串的一部分,在匹配时,search 方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,就返回 None。
我们把上面代码中的 match 方法修改成 search
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'result = re.search('Hello.*?(\\d+).*?Demo', content)print(result)findall
前面我们介绍了 search 方法的用法,它可以返回匹配正则表达式的第一个内容,但是如果想要获取匹配正则表达式的所有内容,那该怎么办呢?这时就要借助 findall 方法了。该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容。
还是上面的 HTML 文本,如果想获取所有 a 节点的超链接、歌手和歌名,就可以将 search 方法换成 findall 方法。如果有返回结果的话,就是列表类型,所以需要遍历一下来依次获取每组内容。代码如下:
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)print(results)print(type(results))for result in results: print(result) print(result[0], result[1], result[2])返回的列表中的每个元素都是元组类型,我们用对应的索引依次取出即可。
如果只是获取第一个内容,可以用 search 方法。当需要提取多个内容时,可以用 findall 方法。
sub
除了使用正则表达式提取信息外,有时候还需要借助它来修改文本。比如,想要把一串文本中的所有数字都去掉,如果只用字符串的 replace 方法,那就太烦琐了,这时可以借助 sub 方法。
import re
content = '54aK54yr5oiR54ix5L2g'content = re.sub('\\d+', '', content)print(content)这里只需要给第一个参数传入 \d+ 来匹配所有的数字,第二个参数为替换成的字符串(如果去掉该参数的话,可以赋值为空),第三个参数是原字符串。
compile
前面所讲的方法都是用来处理字符串的方法,最后再介绍一下 compile 方法,这个方法可以将正则字符串编译成正则表达式对象,以便在后面的匹配中复用。示例代码如下:
import re
content1 = '2016-12-15 12:00'content2 = '2016-12-17 12:55'content3 = '2016-12-22 13:21'pattern = re.compile('\\d{2}:\\d{2}')result1 = re.sub(pattern, '', content1)result2 = re.sub(pattern, '', content2)result3 = re.sub(pattern, '', content3)print(result1, result2, result3)例如,这里有 3 个日期,我们想分别将 3 个日期中的时间去掉,这时可以借助 sub 方法。该方法的第一个参数是正则表达式,但是这里没有必要重复写 3 个同样的正则表达式,此时可以借助 compile 方法将正则表达式编译成一个正则表达式对象,以便复用。
另外,compile 还可以传入修饰符,例如 re.S 等修饰符,这样在 search、findall 等方法中就不需要额外传了。所以,compile 方法可以说是给正则表达式做了一层封装,以便我们更好地复用。
爬虫基本库使用-猫眼电影排行
使用request来提取出猫眼电影 TOP100 的电影名称、时间、评分、图片等信息,提取的站点URLhttp://maoyan.com/board/4,提取的结果会以文件形式保存下来。
页面分析
排名第一的电影是霸王别姬,页面中显示的有效信息有影片名称、主演、上映时间、上映地区、评分、图片等信息。
将网页滚动到最下方,可以发现有分页的列表,直接点击第 2 页,观察页面的 URL 变为 http://maoyan.com/board/4?offset=10
offset 代表偏移量值,如果偏移量为 n,则显示的电影序号就是 n+1 到 n+10,每页显示 10 个电影信息。
如果想获取 TOP100 电影,只需要分开请求 10 次,而 10 次的 offset 参数分别设置为 0、10、20…90 即可,这样获取不同的页面之后,再用正则表达式提取出相关信息,就可以得到 TOP100 的所有电影信息了。
抓取首页
import requests
def get_one_page(url): headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36', 'Cookie': '' }
response = requests.get(url, headers=headers) if response.status_code == 200: return response.text return None
def main(): url = '<http://maoyan.com/board/4>' html = get_one_page(url) print(html)
main()注意: 由于猫眼电影的反爬虫机制,需要在请求头中添加 Cookie 信息,否则会返回418错误。
正则表达式提取信息
可以看到,一部电影信息对应的源代码是一个 dd 节点,我们用正则表达式来提取这里面的一些电影信息。
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) print(items)这样就可以成功地将一页的 10 个电影信息都提取出来。
但这样还不够,数据比较杂乱,我们再将匹配结果处理一下,遍历提取结果并生成字典,此时方法改写如下:
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) for item in items: yield {'index': item[0], 'image': item[1], 'title': item[2].strip(), 'actor': item[3].strip()[3:] if len(item[3]) > 3 else '', 'time': item[4].strip()[5:] if len(item[4]) > 5 else '', 'score': item[5].strip() + item[6].strip()}写入文件
随后,我们将提取的结果写入文件,这里直接写入到一个文本文件中。这里通过 JSON 库的 dumps 方法实现字典的序列化,并指定 ensure_ascii 参数为 False,这样可以保证输出结果是中文形式而不是 Unicode 编码
def write_to_file(content): with open('result.txt', 'a', encoding='utf-8') as f: print(type(json.dumps(content))) f.write(json.dumps(content, ensure_ascii=False)+'\\n')完整代码
import jsonimport requestsfrom requests.exceptions import RequestExceptionimport reimport time
def get_one_page(url): headers = { 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Cookie':'' } try: response = requests.get(url,headers=headers) if response.status_code == 200: return response.text return None except RequestException: return None
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(\\d+)</i>.*?data-src="(.*?)".*?name"><a' + '.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>' + '.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) for item in items: yield { 'index': item[0], 'image': item[1], 'title': item[2], 'actor': item[3].strip()[3:], 'time': item[4].strip()[5:], 'score': item[5] + item[6] }
def write_to_file(content): with open('result.txt', 'a', encoding='utf-8') as f: f.write(json.dumps(content, ensure_ascii=False) + '\\n')
def main(offset): url = '<http://maoyan.com/board/4?offset=>' + str(offset) html = get_one_page(url) for item in parse_one_page(html): print(item) write_to_file(item)
if __name__ == '__main__': for i in range(10): main(offset=i * 10) time.sleep(1)
Spider Basic Library
Learning about web crawlers, the initial operation is to simulate a browser sending requests to a server. So where should we start? Do we need to construct requests ourselves? Do we need to care about the implementation of the request data structure? Do we need to understand network transmission at the HTTP, TCP, and IP layers? Do we need to know how servers respond and reply?
You might feel unsure where to start, but don’t worry—the power of Python lies in providing feature-complete libraries to help us accomplish these requests. The most basic HTTP libraries include urllib, httplib2, requests, treq, etc.
urllib
In Python 2, there were two libraries, urllib and urllib2, to implement sending requests. In Python 3, urllib2 no longer exists; it was unified into urllib, with the official documentation at: https://docs.python.org/3/library/urllib.html
The urllib library is Python’s built-in HTTP request library, which means no extra installation is required. It contains the four modules below.
- request It is the most basic HTTP request module and can be used to simulate sending requests. Just like typing a URL in a browser and pressing Enter, you only need to pass the URL and any extra parameters to the library method to simulate this process.
- error The exception handling module. If a request error occurs, we can catch these exceptions and then retry or perform other actions to ensure the program doesn’t terminate unexpectedly.
- parse A utility module that provides many URL-handling methods, such as splitting, parsing, and joining.
- robot parser Mainly used to identify a site’s robots.txt file and determine which sites can be crawled and which cannot. It’s used less frequently.
This section’s code can refer to this library
Sending Requests
Using urllib’s request module, we can conveniently implement sending requests and obtaining responses.
urlopen
The urllib.request module provides the most basic method to construct HTTP requests. It can simulate a browser’s request initiation process, and it also handles authentication, redirection, browser cookies, and other content.
Take the Python official site as an example; we’ll fetch this page:
import urllib.request
response = urllib.request.urlopen('<https://www.python.org>')print(response.read().decode('utf-8'))
print(type(response))An HTTPResponse object mainly contains methods such as read, readinto, getheader, getheaders, fileno, and attributes like msg, version, status, reason, debuglevel, and closed.
After obtaining this object, assign it to a variable named response, and you can call these methods and attributes to obtain a return information series.
import urllib.request
response = urllib.request.urlopen('<https://www.python.org>')print(response.status)print(response.getheaders())print(response.getheader('Server'))
print(type(response))# <class 'http.client.HTTPResponse'>It can be seen that the return is an HTTPResponse type object, mainly containing methods such as read, readinto, getheader, getheaders, fileno, and attributes like msg, version, status, reason, debuglevel, and closed.
After obtaining this object, assign it to a variable named response, and you can call these methods and attributes to obtain a series of information about the return result.
For example, calling read can obtain the web page content, and accessing the status attribute gives the status code of the response, where 200 indicates success and 404 indicates not found, etc.
Using the most basic urlopen method, you can complete the simplest GET request for a web page.
If you want to pass parameters in the link, how can you implement that? First, look at the urlopen method API:
urllib.request.urlopen(url, data=None, [timeout,]*, cafile=None, capath=None, cadefault=False, context=None)
You can see that besides the first argument which can pass a URL, we can also pass other contents, such as data (payload), timeout (timeout) and so on.
-
data parameter The data parameter is optional. If you want to add this parameter, you must convert the parameter to a bytes type using the bytes method. Also, if this parameter is passed, the request method is no longer GET but POST.
import urllib.parseimport urllib.requestdata = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf8')response = urllib.request.urlopen('<http://httpbin.org/post>', data=data)print(response.read())Here, we pass a parameter word with the value hello. It needs to be encoded into bytes. The bytes method requires the first argument to be a string, and you should use urllib.parse.urlencode to convert the parameter dictionary into a string; the second argument specifies the encoding, here utf8.
The target site here is httpbin.org, which provides HTTP request testing. Our request URL is http://httpbin.org/post, a URL that can be used to test POST requests. It outputs some information about the Request, including the data parameter we passed.
The result is as follows:
{"args": {},"data": "","files": {},"form": {"word": "hello"},"headers": {"Accept-Encoding": "identity","Content-Length": "10","Content-Type": "application/x-www-form-urlencoded","Host": "httpbin.org","User-Agent": "Python-urllib/3.5"},"json": null,"origin": "123.124.23.253","url":"<http://httpbin.org/post>"}As you can see, the parameter we passed appears in the form field, indicating that we simulated a form submission using the POST method to transmit data.
-
timeout parameter
The timeout parameter is used to set a timeout in seconds. It means that if the request exceeds this time without a response, an exception will be raised. If this parameter is not specified, a global default time will be used. It supports HTTP, HTTPS, and FTP requests.
import urllib.requestresponse = urllib.request.urlopen('<http://httpbin.org/get>', timeout=1)print(response.read())Here the timeout is set to 1 second. After 1 second, the server still hasn’t responded, so an URLError exception is raised. This exception comes from the urllib.error module, and the reason is a timeout.
Therefore, you can control a webpage’s crawl by setting this timeout. This can be implemented with try-except as follows:
import socketimport urllib.requestimport urllib.errortry:response = urllib.request.urlopen('<http://httpbin.org/get>', timeout=0.1)except urllib.error.URLError as e:if isinstance(e.reason, socket.timeout):print('TIME OUT')Here we request the test URL http://httpbin.org/get with a timeout of 0.1 seconds, and then catch the URLError exception, checking that the reason is socket.timeout, meaning a timeout error occurred.
Timeout handling via the timeout parameter can be very useful.
-
Other parameters
In addition to data and timeout, there are also context, cafile, capath, cadefault, etc. The context must be an ssl.SSLContext object to specify SSL settings. Besides, cafile and capath specify CA certificates and their paths, which are useful when requesting HTTPS links. The cadefault parameter has been deprecated and its default is False.
We have explained how to use the urlopen method above. With this fundamental method, we can complete simple requests and webpage crawling. For more detailed information, refer to the official documentation: https://docs.python.org/3/library/urllib.request.html.
Request
We know that using the urlopen method can initiate the most basic request, but these simple parameters are not enough to build a complete request. If you need to add information such as headers in the request, you can use the more powerful Request class to construct it.
First, let’s feel the usage of Request with an example:
import urllib.request
request = urllib.request.Request('<https://python.org>')response = urllib.request.urlopen(request)print(response.read().decode('utf-8'))We can see that we still use the urlopen method to send this request; the only difference is that this time the argument is not a URL but a Request object. By constructing this data structure, we can separate the request into an object and configure parameters more richly and flexibly.
Its constructor is as follows:
class urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
- The first parameter url is the request URL and is required; the others are optional.
- The second parameter data, if provided, must be of type bytes. If it is a dictionary, you can first use urllib.parse.urlencode() to encode it.
- The third parameter headers is a dictionary representing the request headers. You can construct it directly via the headers parameter when creating the request, or you can add headers later by calling add_header() on the request instance. Adding request headers most commonly via modifying the User-Agent to masquerade as a browser. The default User-Agent is Python-urllib; you can modify it to masquerade as a browser. For example, to masquerade as Firefox, you can set it to:
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/- The fourth parameter origin_req_host refers to the host name or IP address of the requester.
- The fifth parameter unverifiable indicates whether this request is verifiable; the default is False, meaning the user does not have sufficient permission to choose the outcome of receiving this request. For example, if we request an image within an HTML document but we do not have permission to automatically fetch images, the unverifiable value would be True.
- The sixth parameter method is a string used to indicate the method used by the request, such as GET, POST, PUT, etc.
from urllib import request, parse
url = '<http://httpbin.org/post>'headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Host': 'httpbin.org'}dict = {'name': 'Germey'}data = bytes(parse.urlencode(dict), encoding='utf8')req = request.Request(url=url, data=data, headers=headers, method='POST')response = request.urlopen(req)print(response.read().decode('utf-8'))Here we construct a request with four parameters, where url is the request URL, headers specify User-Agent and Host, data is converted to bytes via urlencode and bytes, and the request method is set to POST.
The following shows the result:
{ "args": {}, "data": "", "files": {}, "form": { "name": "Germey" }, "headers": { "Accept-Encoding": "identity", "Content-Length": "11", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)" }, "json": null, "origin": "219.224.169.11", "url":"<http://httpbin.org/post>"}From the result, we can see that we successfully set data, headers, and method.
Headers can also be added using the add_header method:
req = request.Request(url=url, data=data, method='POST')req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')This allows for easier request construction and sending.
Advanced Usage
In the previous process, although we could construct requests, for more advanced operations (such as cookies handling, proxy settings, etc.), what should we do?
Next, we need more powerful tools: Handlers. In short, you can think of them as various processors—some for login validation, some for handling cookies, some for managing proxies. With them, you can almost do everything in HTTP requests.
First, let’s introduce the BaseHandler class inside the urllib.request module. It is the parent class of all other Handlers and provides the most basic methods, such as default_open, protocol_request, etc.
Next, there are various Handler subclasses that inherit from BaseHandler, for example:
- HTTPDefaultErrorHandler: handles HTTP response errors; errors will raise HTTPError exceptions.
- HTTPRedirectHandler: handles redirection.
- HTTPCookieProcessor: handles Cookies.
- ProxyHandler: configures proxies; the default proxy is none.
- HTTPPasswordMgr: manages passwords; maintains a table of username/password.
- HTTPBasicAuthHandler: handles authentication; if a link requires authentication, this can resolve it.
There are other Handler classes as well; for details, refer to the official documentation: https://docs.python.org/3/library/urllib.request.html#urllib.request.BaseHandler
Another important class is OpenerDirector, which we can call Opener. We have used urlopen before; in fact, it is an Opener provided by urllib for us.
Why introduce Opener? Because it enables more advanced functionality.
An Opener can use the open method; the return type is the same as urlopen, and it can be built using Handlers.
-
Authentication
Some sites present a login prompt on open, requiring you to enter a username and password; you can view the page only after successful authentication.
To request such a page, you can complete it with HTTPBasicAuthHandler; the relevant code is:
from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_openerfrom urllib.error import URLErrorusername = 'username'password = 'password'url = '<http://localhost:5000/>'p = HTTPPasswordMgrWithDefaultRealm()p.add_password(None, url, username, password)auth_handler = HTTPBasicAuthHandler(p)opener = build_opener(auth_handler)try:result = opener.open(url)html = result.read().decode('utf-8')print(html)except URLError as e:print(e.reason)Here we first instantiate an HTTPBasicAuthHandler object, whose parameter is an HTTPPasswordMgrWithDefaultRealm object. It uses the add_password method to add the username and password, thereby creating a Handler to perform the authentication.
Next, use this Handler and build_opener to construct an Opener; this Opener acts as if authentication has already succeeded when sending requests. Then, use the Opener’s open method to open the link, completing the authentication. The result is the page source after authentication.
-
Proxies
Some sites monitor access counts from a given IP. If the count is too high, access may be blocked. We can configure proxies and switch IPs periodically to continue crawling even if one IP is blocked.
Proxy settings are also implemented via Handlers. Here is how to use ProxyHandler:
from urllib.error import URLErrorfrom urllib.request import ProxyHandler, build_openerproxy_handler = ProxyHandler({'http': '<http://127.0.0.1>:9743','https': '<https://127.0.0.1:9743>'})opener = build_opener(proxy_handler)try:response = opener.open('<https://www.baidu.com>')print(response.read().decode('utf-8'))except URLError as e:print(e.reason)Here we set up a local proxy running on port 9743. ProxyHandler’s argument is a dictionary where the keys are protocol types (e.g., HTTP or HTTPS) and the values are proxy URLs; you can add multiple proxies.
Then, using this Handler and build_opener, construct an Opener and send requests.
-
Cookies
Handling cookies requires the corresponding Handler.
import http.cookiejar, urllib.requestcookie = http.cookiejar.CookieJar()handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')for item in cookie:print(item.name+"="+item.value)First, declare a CookieJar object. Next, use HTTPCookieProcessor to build a Handler, and finally use build_opener to construct an Opener and call open.
You can see that it prints the name and value of each cookie.
Cookies can also be saved to a file format. Cookies are stored as text as well.
filename = 'cookies.txt'cookie = http.cookiejar.MozillaCookieJar(filename)handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')cookie.save(ignore_discard=True, ignore_expires=True)Here, CookieJar needs to be MozillaCookieJar, which is used when generating files. It handles cookies and file-related events, such as reading and saving cookies, and can save cookies in Mozilla-style browser format.
LWPCookieJar can also read and save cookies, but its format differs from MozillaCookieJar and saves cookies in the libwww-perl (LWP) format.
To save as an LWP cookies file, declare it as:
cookie = http.cookiejar.LWPCookieJar(filename)After generating the cookies file, how do you read it from the file and use it?
cookie = http.cookiejar.LWPCookieJar()cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True)handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')print(response.read().decode('utf-8'))You can see that this uses load to read the local Cookies file and obtains its contents. The prerequisite is that you first generate a LWPCookieJar-formatted cookie and save it to a file, then read the cookies and use the same method to build a Handler and an Opener to complete the operation.
If the page returns normally, you will see the source of the Baidu page.
The above methods allow you to implement most request feature configurations.
This is the basic usage of the request module in urllib. For more features, refer to the official documentation: https://docs.python.org/3/library/urllib.request.html#basehandler-objects
Handling Exceptions
In the previous section we learned about sending requests, but in poor network conditions, what should we do when an exception occurs? If these exceptions aren’t handled, the program may terminate due to errors, so exception handling is quite necessary. urllib’s error module defines exceptions raised by the request module. If issues arise, the request module raises exceptions defined in the error module.
URLError
URLError is a class from urllib’s error module; it inherits from OSError and is the base class for error exceptions produced by the request module. It has an attribute reason that returns the cause of the error.
from urllib import request, errortry: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.URLError as e: print(e.reason)If we open a non-existent page, an error would normally be raised, but in this case we catch URLError and print the reason, which would be:
Not Found
The program does not crash, and the error is handled gracefully.
HTTPError
It is a subclass of URLError, specifically for HTTP request errors, such as authentication failures. It has three attributes:
- code: the HTTP status code, e.g., 404 for not found, 500 for server error.
- reason: like the parent, the error reason.
- headers: the response headers.
from urllib import request,errortry: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.HTTPError as e: print(e.reason, e.code, e.headers, sep='\n')The result is:
Not Found 404 Server: nginx/1.4.6 (Ubuntu) Date: Wed, 03 Aug 2016 08:54:22 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: close X-Powered-By: PHP/5.5.9-1ubuntu4.14 Vary: Cookie Expires: Wed, 11 Jan 1984 05:00:00 GMT Cache-Control: no-cache, must-revalidate, max-age=0 Pragma: no-cache Link: https://cuiqingcai.com/wp-json/; rel=“https://api.w.org/”
Here we catch HTTPError and print reason, code, and headers.
Since URLError is the parent of HTTPError, a better approach is to catch the subclass first, then the parent:
from urllib import request, error
try: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.HTTPError as e: print(e.reason, e.code, e.headers, sep='\n')except error.URLError as e: print(e.reason)else: print('Request Successfully')This way, you can first catch HTTPError and obtain its status code, reason, and headers. If it’s not an HTTPError, you’ll catch URLError and print the reason. Finally, use an else to handle normal logic. This is a good exception-handling pattern.
Sometimes, the reason attribute may not be a string; it can be an object as well.
import socketimport urllib.requestimport urllib.error
try: response = urllib.request.urlopen('<https://www.baidu.com>', timeout=0.01)except urllib.error.URLError as e: print(type(e.reason)) if isinstance(e.reason, socket.timeout): print('TIME OUT')Here we directly set a timeout to force a timeout exception. The result is:
<class ‘socket.timeout’> TIME OUT
As you can see, the reason attribute is the socket.timeout class. So you can use isinstance to check its type for more precise error handling.
Parsing Links
Earlier, the urllib library also provides the parse module, which defines standard interfaces for URL handling, such as extracting, merging, and converting link components. It supports URL processing for protocols such as file, ftp, gopher, hdl, http, https, imap, mailto, mms, news, nntp, prospero, rsync, rtsp, rtspu, sftp, sip, sips, snews, svn, svn+ssh, telnet, and wais. In this section, we’ll look at commonly used methods to see how convenient they are.
urlparse
This method can identify and split a URL
from urllib.parse import urlparse
result = urlparse('<http://www.baidu.com/index.html;user?id=5#comment>')print(type(result), result)<class 'urllib.parse.ParseResult'> ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
You’ll see that the result is a ParseResult object containing six parts: scheme, netloc, path, params, query, and fragment.
It shows that urlparse splits into six parts. Generally, there are specific delimiters in parsing. For example, the part before :// is the scheme (the protocol); the first / is the netloc (domain); the part after is the path; the semicolon leads to params; the question mark indicates query; the hash denotes the fragment.
Thus, the standard URL format is:
scheme://netloc/path;params?query#fragment
A standard URL adheres to this rule, and urlparse can split it.
# API configurationurllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)It has three parameters:
- urlstring: required; the URL to be parsed.
- scheme: the default protocol (e.g., http or https). The scheme parameter only takes effect if the URL does not contain a scheme. If the URL contains a scheme, the parsed scheme is returned.
- allow_fragments: whether to ignore the fragment. If set to False, the fragment portion will be ignored and parsed as part of path, parameters, or query, and fragment becomes empty.
urlunparse
With urlparse, there is the opposite method urlunparse. It accepts an iterable object, but its length must be 6; otherwise you’ll get an error about insufficient or excessive arguments.
from urllib.parse import urlunparse
data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']print(urlunparse(data))Here, data is a list. Of course, you can also use other types, such as a tuple or a specific data structure.
http://www.baidu.com/index.html;user?a=6#comment
urlsplit
This method is very similar to urlparse, except it does not parse the params section separately; it returns five results. In the above example, params are merged into path.
from urllib.parse import urlsplit
result = urlsplit('<http://www.baidu.com/index.html;user?id=5#comment>')print(result)The result is SplitResult, which is a tuple-like object; you can access its values via attributes or indices.
urlunsplit
Similar to urlunparse, it also combines URL parts into a complete URL. The parameter is an iterable object, for example a list or tuple; the only difference is the length must be 5.
from urllib.parse import urlunsplit
data = ['http', 'www.baidu.com', 'index.html', 'a=6', 'comment']print(urlunsplit(data))http://www.baidu.com/index.html?a=6#comment
urljoin
With urlunparse and urlunsplit, we can merge links, but a base_url must be provided with scheme, netloc, and path. Another method to generate links is urljoin. You can provide a base_url as the first argument and the new link as the second; the method analyzes base_url’s scheme, netloc, and path, fills in the missing parts of the new link, and returns the result.
from urllib.parse import urljoin
print(urljoin('<http://www.baidu.com>', 'FAQ.html'))print(urljoin('<http://www.baidu.com>', '<https://cuiqingcai.com/FAQ.html>'))print(urljoin('<http://www.baidu.com/about.html>', '<https://cuiqingcai.com/FAQ.html>'))print(urljoin('<http://www.baidu.com/about.html>', '<https://cuiqingcai.com/FAQ.html?question=2>'))print(urljoin('<http://www.baidu.com?wd=abc>', '<https://cuiqingcai.com/index.php>'))print(urljoin('<http://www.baidu.com>', '?category=2#comment'))print(urljoin('www.baidu.com', '?category=2#comment'))print(urljoin('www.baidu.com#comment', '?category=2'))The results are:
<http://www.baidu.com/FAQ.html><https://cuiqingcai.com/FAQ.html><https://cuiqingcai.com/FAQ.html><https://cuiqingcai.com/FAQ.html?question=2><https://cuiqingcai.com/index.php><http://www.baidu.com?category=2#comment>www.baidu.com?category=2#commentwww.baidu.com?category=2You can see base_url provides three parts: scheme, netloc, and path. If these three parts do not exist in the new link, they will be filled in; if the new link already has them, those parts from the new link are used. The params, query, and fragment in base_url do not apply.
With urljoin, you can easily perform link parsing, concatenation, and generation.
urlencode
Here we introduce another commonly used method—urlencode—which is very useful for constructing GET request parameters.
from urllib.parse import urlencode
params = { 'name': 'germey', 'age': 22}base_url = '<http://www.baidu.com>?'url = base_url + urlencode(params)print(url)Here we first declare a dictionary to represent the parameters, then call urlencode to serialize them into a GET parameter string.
http://www.baidu.com?name=germey&age=22
As you can see, the parameters have been converted from a dictionary to GET parameters. This method is very common. For convenience, you may represent parameters as a dictionary beforehand; to convert to URL parameters, simply call this method.
parse_qs
Once serialized, there is necessarily deserialization. If we have a string of GET request parameters, using parse_qs we can convert it back to a dictionary:
from urllib.parse import parse_qs
query = 'name=germey&age=22'print(parse_qs(query))parse_qsl
There is also a parse_qsl method, which converts parameters into a list of tuples.
from urllib.parse import parse_qsl
query = 'name=germey&age=22'print(parse_qsl(query))quote
This method can convert content into URL-encoded format. When a URL contains non-ASCII characters, this can cause garbling; this method converts Chinese characters into URL encoding.
from urllib.parse import quote
keyword = ' 壁纸 'url = '<https://www.baidu.com/s?wd=>' + quote(keyword)print(url)Here we declare a Chinese search term and URL-encode it with quote, yielding:
https://www.baidu.com/s?wd=% E5% A3% 81% E7% BA% B8
unquote
With quote comes unquote, which decodes URL-encoded content:
from urllib.parse import unquote
url = '<https://www.baidu.com/s?wd=%> E5% A3% E7% BA% B8'print(unquote(url))This is the URL-encoded result obtained above; unquote decodes it to:
https://www.baidu.com/s?wd = 壁纸
As you can see, unquote makes decoding easy.
Analyzing the Robots Protocol
Using urllib’s robotparser module, we can analyze a website’s Robots protocol. In this section, we’ll briefly understand how to use this module.
Robots Protocol
The Robots Protocol, also called the crawler protocol or robot protocol, is short for the Robots Exclusion Protocol. It tells crawlers and search engines which pages may be crawled and which should not be crawled. It is typically a text file named robots.txt, usually placed in the site’s root directory.
When a search crawler visits a site, it first checks whether a robots.txt file exists in the site’s root directory. If it exists, the crawler crawls according to the scope defined there. If not, the crawler will access all directly accessible pages.
Here is a sample robots.txt:
User-agent: *Disallow: /Allow: /public/3This restricts all crawlers to crawl only the public directory. Save the above content as robots.txt in the site’s root directory, alongside the site’s entry files (such as index.php, index.html, index.jsp, etc.). The User-agent describes the crawler’s name; setting it to * means the protocol applies to any crawler. For example:
User-agent: Baiduspider
This means the rules apply to Baidu’s crawler. If there are multiple User-agent lines, multiple crawlers will be affected, but at least one should be specified.
Disallow specifies the directories not allowed to crawl (in the above example, / means disallow crawling all pages). Allow is usually used together with Disallow to carve out exceptions. Now we set Allow to /public/, meaning all pages are not crawlable except the public directory.
Let’s look at a few more examples.
<!-- Code below forbids all crawlers from accessing any directory: -->User-agent: *Disallow: /<!-- Code below allows all crawlers to access any directory: -->User-agent: *Disallow:<!-- Also, leaving robots.txt empty is allowed. --><!-- Code below forbids all crawlers from accessing certain website directories: -->User-agent: *Disallow: /private/Disallow: /tmp/<!-- Code below allows only one crawler to access: -->User-agent: WebCrawlerDisallow:User-agent: *Disallow: /These are common patterns for robots.txt.
Crawler Names
You might wonder where crawler names come from; they have fixed names, for example, Baidu’s crawler is called BaiduSpider.
| Crawler Name | Name | Website |
|---|---|---|
| BaiduSpider | Baidu | <www.baidu.com> |
| Googlebot | <www.google.com> | |
| 360Spider | 360 Search | <www.so.com> |
| YodaoBot | Youdao | <www.youdao.com> |
| ia_archiver | Alexa | <www.alexa.cn> |
| Scooter | Altavista | <www.altavista.com> |
robotparser
After understanding the Robots protocol, we can use robotparser to parse robots.txt. This module provides the RobotFileParser class, which can determine whether a given crawler has permission to crawl a website’s pages based on the site’s robots.txt.
The class is simple to use: simply pass the robots.txt URL to the constructor. First, here is its declaration:
urllib.robotparser.RobotFileParser(url='')
Of course, you can also instantiate it without passing a URL, defaulting to empty, and then use set_url() to set it.
Here are the commonly used methods of this class:
- set_url: sets the robots.txt URL. If the link is provided when creating the RobotFileParser, you don’t need to call this method.
- read: reads robots.txt and analyzes it. Note that this method performs a read and analyze operation. If you don’t call this method, subsequent checks will return False, so be sure to call it. This method does not return content, but performs the read operation.
- parse: parses the contents of robots.txt. The argument is some lines from robots.txt; it will analyze these contents according to robots.txt syntax.
- can_fetch: this method takes two parameters: the first is User-agent, the second is the URL to fetch. It returns True if the search engine is allowed to crawl the URL, otherwise False.
- mtime: returns the last time robots.txt was fetched and analyzed; useful for long-running crawlers.
- modified: similarly updates the time to now for long-running crawlers.
Let’s see an example:
from urllib.robotparser import RobotFileParserrp = RobotFileParser()rp.set_url('<http://www.jianshu.com/robots.txt>')rp.read()print(rp.can_fetch('*', '<http://www.jianshu.com/p/b67554025d7d>'))print(rp.can_fetch('*', "<http://www.jianshu.com/search?q=python&page=1&type=collections>"))Here we use JianShu (jianshu) as an example. First we create a RobotFileParser object, set the robots.txt link with set_url. Of course, without this method you can also set it directly at declaration: rp = RobotFileParser('<http://www.jianshu.com/robots.txt>'). Then we use can_fetch to determine if a page can be crawled. The output is:
TrueFalseWe can also use the parse method to read and analyze, as follows:
from urllib.robotparser import RobotFileParserfrom urllib.request import urlopenrp = RobotFileParser()rp.parse(urlopen('<http://www.jianshu.com/robots.txt').read().decode('utf-8').split('\\n>'))print(rp.can_fetch('*', '<http://www.jianshu.com/p/b67554025d7d>'))print(rp.can_fetch('*', "<http://www.jianshu.com/search?q=python&page=1&type=collections>"))Requests
In the previous section, we learned the basic usage of urllib, but there are indeed some inconveniences, such as dealing with website authentication and cookies—these require writing Openers and Handlers. To make these operations easier, there is a more powerful library: requests. With it, operations like cookies, login authentication, and proxies are no longer a hassle. Next, let’s explore its power.
Basic Usage
- Prerequisites Make sure you have the requests library installed correctly.
- Getting Started with an Example The urlopen method in urllib is effectively a GET request, while the corresponding method in requests is get. Here is an example:
import requests
r = requests.get('<https://www.baidu.com/>')print(type(r))print(r.status_code)print(type(r.text))print(r.text)print(r.cookies)The result is:
<class 'requests.models.Response'>200<class 'str'><html> <head> <script> location.replace(location.href.replace("https://","http://")); </script> </head> <body> <noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript> </body></html><RequestsCookieJar[<Cookie BIDUPSID=992C3B26F4C4D09505C5E959D5FBC005 for .baidu.com/>, <CookiePSTM=1472227535 for .baidu.com/>, <Cookie __bsi=15304754498609545148_00_40_N_N_2_0303_C02F_N_N_N_0for .www.baidu.com/>, <Cookie BD_NOT_HTTPS=1 for www.baidu.com/>]>Here we call the get method to perform the same operation as urlopen, obtaining a Response object, and then output the Response’s type, status code, the type of the response body, the content, and Cookies.
From the results, we can see that the return type is requests.models.Response, the response body is a string, and Cookies are of type RequestsCookieJar.
Using get successfully performs a GET request, which is not remarkable in itself, but it’s more convenient because other request types can also be done with a single line, for example:
r = requests.post('<http://httpbin.org/post>')r = requests.put('<http://httpbin.org/put>')r = requests.delete('<http://httpbin.org/delete>')r = requests.head('<http://httpbin.org/get>')r = requests.options('<http://httpbin.org/get>')Here we used post, put, delete, etc. to perform POST, PUT, DELETE, and so on.
GET Requests
One of the most common HTTP requests is GET. Here we’ll detail how to construct GET requests with requests.
Basic example
First, build a simple GET request to http://httpbin.org/get. That site will return information about the request if the client uses GET:
import requests
r = requests.get('<http://httpbin.org/get>')print(r.text)The result:
{"args": {},"headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Host": "httpbin.org", "User-Agent": "python-requests/2.10.0"},"origin": "122.4.215.33","url": "<http://httpbin.org/get>"}We can see that the GET request succeeded and the response includes the request headers, URL, IP, etc.
If you want to attach extra information to GET requests, you can use the params parameter to store them:
import requests
data = { 'name': 'germey', 'age': 22}r = requests.get("<http://httpbin.org/get>", params=data)print(r.text)The request URL will be automatically constructed as: http://httpbin.org/get?age=22&name=germey.
Additionally, the response is a string that is actually in JSON format. If you want to parse it directly into a dictionary, you can call the json method. For example:
import requests
r = requests.get("<http://httpbin.org/get>")print(type(r.text))print(r.json())print(type(r.json()))Calling json converts the JSON-formatted string into a dictionary.
Note that if the response isn’t JSON, parsing will raise json.decoder.JSONDecodeError.
- Fetching a webpage
The above URL returns JSON-formatted content. If you fetch a regular webpage, you’ll get the page content. For example, here is how to fetch Zhihu’s Explore page:
import requestsimport re
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}r = requests.get("<https://www.zhihu.com/explore>", headers=headers)pattern = re.compile('explore-feed.*?question_link.*?>(.*?)</a>', re.S)titles = re.findall(pattern, r.text)print(titles)Here we include headers, which contain the User-Agent header to masquerade as a browser. Without this, Zhihu may block Crawling.
- Fetching binary data
In the above example, Zhihu’s page is HTML. If you want to fetch images, audio, video, etc., what to do? Images, audio, and video are binary data and require saving in binary format with appropriate encoding.
For example, GitHub’s site favicon:
import requests
r = requests.get("<https://github.com/favicon.ico>")print(r.text)print(r.content)This fetches the site icon displayed in a browser tab.
Then save the image:
import requests
r = requests.get("<https://github.com/favicon.ico>")with open('favicon.ico', 'wb') as f: f.write(r.content)Here we used the open method to write in binary mode to a file, which creates a favicon.ico file when done.
Similarly, audio and video can be obtained this way.
- Adding headers
As with urllib.request, you can also pass header information via the headers parameter.
import requests
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}r = requests.get("<https://www.zhihu.com/explore>", headers=headers)print(r.text)You can add other fields in the headers parameter as well.
- POST Requests
Earlier we learned the basics of GET requests; another common method is POST.
import requests
data = {'name': 'germey', 'age': '22'}r = requests.post("<http://httpbin.org/post>", data=data)print(r.text)- Response
After sending a request, you naturally receive a response. In the examples above, we used text and content to obtain the response, and there are many other attributes and methods for obtaining additional information such as status code, headers, cookies, etc.
import requests
r = requests.get('<http://www.jianshu.com>')print(type(r.status_code), r.status_code)print(type(r.headers), r.headers)print(type(r.cookies), r.cookies)print(type(r.url), r.url)print(type(r.history), r.history)Here we print the status_code (status), headers, cookies, url, and history to show their types.
The status code is commonly used to determine if the request was successful, and requests also provides a built-in status code lookup object, requests.codes.
import requests
r = requests.get('<http://www.jianshu.com>')exit() if not r.status_code == requests.codes.ok else print('Request Successfully')This compares the status code to the built-in success code to ensure a proper response; here, requests.codes.ok gives the success status code 200.
The following is a list of status codes and their query values:
# Informational status codes100: ('continue',),101: ('switching_protocols',),102: ('processing',),103: ('checkpoint',),122: ('uri_too_long', 'request_uri_too_long'),
# Successful status codes200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\\\o/', '✓'),201: ('created',),202: ('accepted',),203: ('non_authoritative_info', 'non_authoritative_information'),204: ('no_content',),205: ('reset_content', 'reset'),206: ('partial_content', 'partial'),207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'),208: ('already_reported',),226: ('im_used',),
# Redirection status codes300: ('multiple_choices',),301: ('moved_permanently', 'moved', '\\\\o-'),302: ('found',),303: ('see_other', 'other'),304: ('not_modified',),305: ('use_proxy',),306: ('switch_proxy',),307: ('temporary_redirect', 'temporary_moved', 'temporary'),308: ('permanent_redirect', 'resume_incomplete', 'resume',), # These 2 to be removed in 3.0
# Client error status codes400: ('bad_request', 'bad'),401: ('unauthorized',),402: ('payment_required', 'payment'),403: ('forbidden',),404: ('not_found', '-o-'),405: ('method_not_allowed', 'not_allowed'),406: ('not_acceptable',),407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'),408: ('request_timeout', 'timeout'),409: ('conflict',),410: ('gone',),411: ('length_required',),412: ('precondition_failed', 'precondition'),413: ('request_entity_too_large',),414: ('request_uri_too_large',),415: ('unsupported_media_type', 'unsupported_media', 'media_type'),416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'),417: ('expectation_failed',),418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'),421: ('misdirected_request',),422: ('unprocessable_entity', 'unprocessable'),423: ('locked',),424: ('failed_dependency', 'dependency'),425: ('unordered_collection', 'unordered'),426: ('upgrade_required', 'upgrade'),428: ('precondition_required', 'precondition'),429: ('too_many_requests', 'too_many'),431: ('header_fields_too_large', 'fields_too_large'),444: ('no_response', 'none'),449: ('retry_with', 'retry'),450: ('blocked_by_windows_parental_controls', 'parental_controls'),451: ('unavailable_for_legal_reasons', 'legal_reasons'),499: ('client_closed_request',),
# Server error status codes500: ('internal_server_error', 'server_error', '/o\\\\', '✗'),501: ('not_implemented',),502: ('bad_gateway',),503: ('service_unavailable', 'unavailable'),504: ('gateway_timeout',),505: ('http_version_not_supported', 'http_version'),506: ('variant_also_negotiates',),507: ('insufficient_storage',),509: ('bandwidth_limit_exceeded', 'bandwidth'),510: ('not_extended',),511: ('network_authentication_required', 'network_auth', 'network_authentication')For example, if you want to check whether the result is 404, you can compare with requests.codes.not_found.
Advanced Usage of Requests
In the previous section, we covered the basic usage of requests, such as GET and POST, and the Response object. This section covers some advanced usage, such as file upload, cookies settings, proxies, etc.
- File Upload
We know that requests can simulate submitting data. If a site requires uploading files, we can use it too.
import requests
files = {'file': open('favicon.ico', 'rb')}r = requests.post('<http://httpbin.org/post>', files=files)print(r.text)This site will return a response containing a files field, while the form field is empty, indicating that the file upload is handled separately under files.
- Cookies
Earlier we used urllib to handle cookies, which was quite verbose. With requests, obtaining and setting cookies is as simple as a single step.
import requests
r = requests.get('<https://www.baidu.com>')print(r.cookies)for key, value in r.cookies.items(): print(key + '=' + value)Here we can obtain cookies via the cookies attribute, which is a RequestsCookieJar. Using items yields a list of (key, value) pairs to iterate over each cookie.
Of course, you can also maintain login state with cookies directly in some cases, as Zhihu illustrates:
- First, log into Zhihu. Copy the Cookie content from Headers, place it into Headers, then send the request.
import requests
headers = { 'Cookie': '', 'Host': 'www.zhihu.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36',}r = requests.get('<https://www.zhihu.com>', headers=headers)print(r.text)Of course, you can also use the cookies parameter to manage cookies, but this requires constructing a RequestsCookieJar object and splitting cookies, which is a bit tedious; the result is the same.
import requests
cookies = ''jar = requests.cookies.RequestsCookieJar()headers = { 'Host': 'www.zhihu.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'}for cookie in cookies.split(';'): key, value = cookie.split('=', 1) jar.set(key, value)r = requests.get('<http://www.zhihu.com>', cookies=jar, headers=headers)print(r.text)Here we first create a RequestsCookieJar object, then split the copied cookies and set each cookie’s key and value, and finally call requests.get() with the cookies parameter. Of course, Zhihu’s own restrictions mean you still need to include the headers parameter with cookies as before, but you don’t need to include the cookie field in the headers.
Tests show that this approach also allows successful login to Zhihu.
- Session Persistence
In requests, using get or post can simulate web requests, but this is effectively different sessions—two separate browser windows opening different pages.
Imagine a scenario where the first request uses post to log in to a site, and a second request then fetches your personal data after login using get. This is like opening two different browsers with two separate sessions. It would not be possible to fetch your profile in a single session.
The main solution is to maintain the same session, which is like opening a single browser tab rather than a new browser window. But we don’t want to repeatedly set cookies. This is where a new tool comes in—Session objects.
Using a Session, you can easily maintain a single session and cookies will be handled automatically.
import requests
s = requests.Session()s.get('<http://httpbin.org/cookies/set/number/123456789>')r = s.get('<http://httpbin.org/cookies>')print(r.text)Thus, using Session allows you to simulate a single session without worrying about cookies, usually used after a successful login for subsequent actions.
- SSL Certificate Verification
Additionally, requests provides certificate verification. When sending HTTP requests, it checks SSL certificates, and you can control whether to verify the certificate using the verify parameter. By default, verify is True and certificates are automatically verified.
If you request an HTTPS site and encounter a certificate verification error, you’ll see SSLError. To avoid this, set verify to False.
import requests
response = requests.get('<https://www.12306.cn>', verify=False)print(response.status_code)You can also specify a local certificate to use as a client certificate. This can be a single file (containing the key and certificate) or a tuple containing two file paths:
import requests
response = requests.get('<https://www.12306.cn>', cert=('/path/server.crt', '/path/key'))print(response.status_code)Note: The private key for a local certificate must be unencrypted; encrypted keys are not supported.
- Proxy Settings
For some sites, during testing you might get content fine a few times, but during large-scale crawling, a site might present a captcha or redirect to a login page, or even block your IP. To prevent this, you can set proxies via the proxies parameter as follows:
import requests
proxies = {'http': '<http://10.10.1.10:3128>','https': '<http://10.10.1.10:1080>',}
requests.get('<https://www.taobao.com>', proxies=proxies)Of course, this example may not work if the proxy is invalid—replace with a valid proxy for testing.
If a proxy requires HTTP Basic Auth, you can use a syntax like http://user:password@host:port, for example:
import requests
requests.get('<https://www.taobao.com>', proxies=proxies)In addition to HTTP proxies, requests also supports SOCKS proxies. First, install the socks library:
pip3 install "requests[socks]"
Then you can use SOCKS proxy:
import requests
proxies = { 'http': 'socks5://user:password@host:port', 'https': 'socks5://user:password@host:port'}requests.get('<https://www.taobao.com>', proxies=proxies)- Timeout Settings
If your local network is poor or the server is slow to respond, you might wait a long time for a response or not receive one at all. To prevent this, set a timeout. This is the time between sending the request and receiving the response.
import requests
r = requests.get('<https://www.taobao.com>', timeout=1)print(r.status_code)This sets the timeout to 1 second. If there is no response within 1 second, an exception will be raised. In fact, requests uses two phases: connect and read. The timeout applies to the total time for both connect and read. If you want to specify them separately, you can pass a tuple:
r = requests.get('<https://www.taobao.com>', timeout=(5, 30))
If you want to wait indefinitely, you can set timeout to None or omit it entirely (default is None):
r = requests.get('<https://www.taobao.com>', timeout=None)
Or simply omit the parameter:
r = requests.get('<https://www.taobao.com>')
- Authentication
When visiting a site you may encounter a login authentication page.
You can use requests’ built-in authentication features:
import requestsfrom requests.auth import HTTPBasicAuth
r = requests.get('<http://localhost:5000>', auth=HTTPBasicAuth('username', 'password'))print(r.status_code)If the username and password are correct, authentication succeeds automatically and returns 200; if authentication fails, it returns 401. Of course, if you pass all parameters as an HTTPBasicAuth object, it can be cumbersome, so requests offers a simpler form: you can pass a tuple, and it will default to HTTPBasicAuth authentication.
import requests
r = requests.get('<http://localhost:5000>', auth=('username', 'password'))print(r.status_code)Additionally, requests provides other authentication methods, such as OAuth authentication, which requires installing the oauth package. Install with:
pip3 install requests_oauthlib
Here is a method using OAuth1 authentication:
import requestsfrom requests_oauthlib import OAuth1
url = '<https://api.twitter.com/1.1/account/verify_credentials.json>'auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET', 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')requests.get(url, auth=auth)For more details, refer to the official document of requests_oauthlib: https://requests-oauthlib.readthedocs.org/
- Prepared Request
Earlier, when introducing urllib, we showed requests as a data structure, where parameters can be represented by a Request object. This is also possible in requests, and the data structure is called Prepared Request.
from requests import Request, Session
url = '<http://httpbin.org/post>'data = {'name': 'germey'}headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'}s = Session()req = Request('POST', url, data=data, headers=headers)prepped = s.prepare_request(req)r = s.send(prepped)print(r.text)Here we introduced a Request, then used the url, data, and headers to construct the Request object, then called Session’s prepare_request to convert it into a Prepared Request object, and finally used send to dispatch it.
With the Request object, you can treat the request as an independent object, which is convenient for queue scheduling. Later we will use it to construct a Request queue.
More usage can be found in Requests’ official documentation: http://docs.python-requests.org/
Regular Expressions
In this section, we look at the usage of regular expressions. Regular expressions are a powerful tool for text processing with their own syntax. They enable search, replace, and validation on strings.
Of course, for crawlers, they are very convenient for extracting information from HTML.
Example Introduction
Having said so much, you might still be fuzzy about what regular expressions actually are. Let’s look at a few examples to understand how to use them.
Open the regex tester provided by Open Source China: http://tool.oschina.net/regex/; paste the text to be matched, then choose common regular expressions to obtain the corresponding match results. For example, here is some text to be matched:
Hello, my phone number is 010-86432100 and email is <[email protected]>, and my website is <https://cuiqingcai.com>.This string contains a phone number and an email. Next, we’ll try to extract them with regular expressions.
An email starts with a string, followed by an @ symbol, and ends with a domain. It has a particular structure. For URLs, the beginning is the protocol, followed by ://, and then the domain and path.
For URLs, you can match with the following regular expression:
[a-zA-z]+://[^\\s]*
Using this regex to match a string will extract text that resembles a URL.
This regex looks chaotic, but it actually follows specific rules. For example, a-z matches lowercase letters, \s matches any whitespace, and * means repeating the preceding pattern any number of times. This long regex is a combination of many matching rules.
Once the regex is written, you can apply it to a long string to search and find matches. For a webpage, if you want to find how many URLs exist in the page source, you can match URLs with the URL-matching regex.
Common matching rules
| Pattern | Description |
|---|---|
| \w | Matches letters, digits, and underscores |
| \W | Matches characters that are not letters, digits, or underscores |
| \s | Matches any whitespace character, equivalent to [\t\n\r\f] |
| \S | Matches any non-whitespace character |
| \d | Matches any digit, equivalent to [0-9] |
| \D | Matches any non-digit character |
| \A | Matches the start of a string |
| \Z | Matches the end of the string; if a newline exists, only matches up to the newline |
| \z | Matches the end of the string; if a newline exists, also matches the newline |
| \G | Matches the position of the last match |
| \n | Matches a newline |
| \t | Matches a tab |
| ^ | Matches the start of a line |
| $ | Matches the end of a line |
| . | Matches any character except a newline; when re.DOTALL is set, it matches any character including newline |
| […] | Denotes a set of characters; for example, [amk] matches a or m or k |
| ^ | Characters not in [] (negated character set) |
| * | Repeats the preceding expression 0 or more times |
| + | Repeats the preceding expression 1 or more times |
| ? | Repeats the preceding expression 0 or 1 times (non-greedy) |
| {n} | Exactly n repetitions of the preceding expression |
| {n, m} | Repeats of the preceding expression from n to m times (greedy) |
| a | b |
| ( ) | Groups the expressions inside parentheses |
Regular expressions aren’t Python-specific; they can be used in other languages too. But Python’s re module provides the full implementation of regular expressions, enabling regex use in Python. In Python, you almost always use this library for regular expressions. Let’s now learn some common methods.
match
First, we introduce the most common matching method—match. Pass in the string to be matched and the regular expression; it checks whether the regex matches the string from the start.
The match method attempts to match the regex at the beginning of the string. If matched, it returns the matched result; if not, it returns None.
import re
content = 'Hello 123 4567 World_This is a Regex Demo'print(len(content))result = re.match('^Hello\\s\\d\\d\\d\\s\\d{4}\\s\\w{10}', content)print(result)print(result.group())print(result.span())Here we first declare a string containing letters, whitespace, and digits. We then write a regex ^Hello\\s\\d\\d\\d\\s\\d{4}\\s\\w{10} to match this long string.
The leading ^ matches the start of the string, i.e., Hello. Then \s matches whitespace to capture spaces. \d matches digits; three \d match 123. Then we again use one \s to match a space; after that there are 4 digits matched by \d{4}. Finally, there is a single whitespace character, and \w{10} matches 10 letters or underscores. Note that this does not fully match the target string, but it still succeeds; the match result simply ends earlier.
-
Capturing groups
To extract a portion of the string, you can enclose the desired substrings in parentheses. Each captured group corresponds to a group index, and you can obtain the extracted result by calling group with the index.
import recontent = 'Hello 1234567 World_This is a Regex Demo'result = re.match('^Hello\\s(\\d+)\\sWorld', content)print(result)print(result.group())print(result.group(1))print(result.span())Here, we want to extract 1234567 from the string by wrapping the numeric part in parentheses and then using group(1) to obtain the match.
group(1) differs from group(); group() returns the full match, while group(1) returns the first captured group. If there are additional groups, you can use group(2), group(3), etc.
-
General matching
The previous regex can be quite complex. For whitespace we use \s; for numbers we use \d. This can be tedious. There is a universal matcher: . (dot). The dot matches any character (except a newline); * repeats the preceding character zero or more times. They combine to match any character, so you don’t need to match character by character.
Continuing the example, we can rewrite the regex as:
import recontent = 'Hello 123 4567 World_This is a Regex Demo'result = re.match('^Hello.*Demo$', content)print(result)print(result.group())print(result.span())Here we omit the middle portion entirely, replacing it with .* and ending with the final string.
group returns the entire matched string; span returns (0, 41), indicating the entire string length.
Therefore, you can use .* to simplify regex writing.
-
Modifiers
Regular expressions can include optional flag modifiers to control the matching behavior. Modifiers are specified as optional flags.
result = re.match('^He.*?(\\d+).*?Demo$', content)If the string contains a newline, the run will fail because the pattern does not account for newlines; the result would be None, and calling group would raise an AttributeError.
Why does adding a newline cause a mismatch? Because the expression matches any character except a newline; when a newline is encountered, .*? cannot match, causing failure. Simply add the modifier re.S to fix this:
import recontent = '''Hello 1234567 World_Thisis a Regex Demo'''result = re.match('^He.*?(\\d+).*?Demo$', content, re.S)print(result.group(1))The re.S modifier makes the dot match across newlines as well. This is frequently used in web scraping because HTML nodes often contain line breaks.
There are other modifiers that can be used when necessary:
Modifier Description re.I Case-insensitive matching re.L Locale-aware matching re.M Multiline matching; affects ^ and $ re.S Dot matches all characters, including newline re.U Unicode character properties for parsing; affects \w, \W, \b, and \B re.X Allows for more readable regex with verbose mode In web matching, re.S and re.I are the most commonly used.
search
Earlier we mentioned that match starts from the beginning of the string, and if the start doesn’t match, the entire match fails.
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'result = re.match('Hello.*?(\\d+).*?Demo', content)print(result)Here the string starts with Extra, but the regex starts with Hello, so the entire match fails because match requires the start to align with the pattern.
Because match requires the start to align, it’s less convenient for general searches. It’s better to use search, which scans the entire string and returns the first successful match. In other words, the regex can be a substring; search will scan the string and return the first matching content, or None if no match is found.
We replace match with search in the above code:
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'result = re.search('Hello.*?(\\d+).*?Demo', content)print(result)findall
Earlier we introduced search, which returns the first match. But if you want all matches, use findall. This method searches the entire string and returns all matches of the regex.
If you want all the links, singers, and song titles from the HTML text’s a nodes, you can replace search with findall. If there are results, it will be a list, and you can iterate to extract each group.
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)print(results)print(type(results))for result in results: print(result) print(result[0], result[1], result[2])Each element in the returned list is a tuple; use the corresponding index to retrieve.
If you only want the first content, you can use search. When you need multiple contents, use findall.
sub
Besides extracting information, sometimes you need to modify text using regex. For example, removing all digits from a string is cumbersome with replace; you can use sub:
import re
content = '54aK54yr5oiR54ix5L2g'content = re.sub('\\d+', '', content)print(content)Here the first argument is \d+ to match all digits; the second argument is the replacement string (you can leave it empty to remove), and the third argument is the original string.
compile
All the methods above operate on strings. Finally, we introduce compile, which compiles a regex string into a regex object for reuse in later matches. Example:
import re
content1 = '2016-12-15 12:00'content2 = '2016-12-17 12:55'content3 = '2016-12-22 13:21'pattern = re.compile('\\d{2}:\\d{2}')result1 = re.sub(pattern, '', content1)result2 = re.sub(pattern, '', content2)result3 = re.sub(pattern, '', content3)print(result1, result2, result3)For example, there are three dates and we want to remove the time from all three. The sub method can be helped by compiling the regular expression into a regex object for reuse.
Moreover, compile can also take modifiers like re.S, so you don’t need to pass them every time to methods like search or findall. Therefore, compile acts as a wrapper around the regex to facilitate reuse.
Spider Basic Library Usage - Maoyan Movie Ranking
Using requests to extract the top 100 Maoyan movies’ titles, times, ratings, images, and so on from the site http://maoyan.com/board/4. The extracted results will be saved to a file.
Page Analysis
The top-ranked movie is Farewell My Concubine, and the page shows information such as the film title, cast, release time, release region, rating, and image.
Scroll to the bottom; you’ll see a paginated list. Click page 2, and observe the URL changes to: http://maoyan.com/board/4?offset=10
offset represents the offset value. If the offset is n, the displayed movie numbers are n+1 to n+10; each page shows 10 movies.
If you want to fetch the TOP 100, you can request 10 pages separately with offset values 0, 10, 20, …, 90. After getting each page, use a regex to extract the relevant information to obtain all TOP 100 movies.
Fetching the Homepage
import requests
def get_one_page(url): headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36', 'Cookie': '' }
response = requests.get(url, headers=headers) if response.status_code == 200: return response.text return None
def main(): url = '<http://maoyan.com/board/4>' html = get_one_page(url) print(html)
main()Note: Because Maoyan’s anti-crawling mechanism detects requests without cookies, add Cookie information in the request headers, otherwise you will get 418 errors.
Regular Expression Extraction of Information
As you can see, a single movie’s information in the source code is represented by a dd node. We use a regex to extract some of this information.
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) print(items)This allows us to successfully extract all 10 movies on one page.
But that’s not enough—the data is a bit messy. We’ll process the matching results and generate dictionaries; the rewritten method is:
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) for item in items: yield {'index': item[0], 'image': item[1], 'title': item[2].strip(), 'actor': item[3].strip()[3:] if len(item[3]) > 3 else '', 'time': item[4].strip()[5:] if len(item[4]) > 5 else '', 'score': item[5].strip() + item[6].strip()}Writing to a File
Next, we write the extracted results to a text file. Here we serialize the dictionaries using the JSON library’s dumps method, and set ensure_ascii to False to ensure the output is in Chinese characters rather than Unicode escapes.
def write_to_file(content): with open('result.txt', 'a', encoding='utf-8') as f: print(type(json.dumps(content))) f.write(json.dumps(content, ensure_ascii=False)+'\\n')Complete Code
import jsonimport requestsfrom requests.exceptions import RequestExceptionimport reimport time
def get_one_page(url): headers = { 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Cookie':'' } try: response = requests.get(url,headers=headers) if response.status_code == 200: return response.text return None except RequestException: return None
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(\\d+)</i>.*?data-src="(.*?)".*?name"><a' + '.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>' + '.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) for item in items: yield { 'index': item[0], 'image': item[1], 'title': item[2], 'actor': item[3].strip()[3:], 'time': item[4].strip()[5:], 'score': item[5] + item[6] }
def write_to_file(content): with open('result.txt', 'a', encoding='utf-8') as f: f.write(json.dumps(content, ensure_ascii=False) + '\\n')
def main(offset): url = '<http://maoyan.com/board/4?offset=>' + str(offset) html = get_one_page(url) for item in parse_one_page(html): print(item) write_to_file(item)
if __name__ == '__main__': for i in range(10): main(offset=i * 10) time.sleep(1)
スパイダー基本ライブラリ
クローリングを学ぶ際の最初の操作は、ブラウザを模倣してサーバへリクエストを送ることです。では、どこから始めればよいのでしょうか?リクエストは自分で構築する必要がありますか?このデータ構造の実装を気にする必要がありますか?HTTP、TCP、IP 層のネットワーク伝送通信について知っておくべきですか?サーバの応答・応答原理を知る必要がありますか?
もしかしたら手がかりがなく手をつけられないかもしれませんが、心配はいりません。Python の強みは、これらのリクエストを支援する機能豊富なライブラリを提供している点です。最も基本的な HTTP ライブラリには urllib、httplib2、requests、treq などがあります。
urllib
Python2 では urllib と urllib2 の 2 つのライブラリでリクエストの送信を実現していましたが、Python3 では urllib2 は廃止され、統一して urllib が用いられます。公式ドキュメントのリンクは:https://docs.python.org/3/library/urllib.html
urllib ライブラリは Python に組み込まれている HTTP リクエストライブラリで、追加のインストールは不要です。次の4つのモジュールを含みます。
- request 最も基本的な HTTP リクエストモジュールで、リクエストを模擬送信するのに用います。ブラウザでURLを入力して Enter を押すのと同じように、ライブラリのメソッドに URL と追加パラメータを渡すだけでこの過程を模倣できます。
- error 例外処理モジュール。リクエストエラーが発生した場合、これらの例外を捕捉してリトライやその他の処理を行い、プログラムが予期せず終了しないようにします。
- parse ツールモジュール。URL の処理に関する多くのメソッドを提供します。例えば分割、解析、結合など。
- robot parser 主に robots.txt ファイルを識別し、どのサイトをクローリングしてよく、どのサイトをクローリングしてはいけないかを判断します。実際にはあまり使われません。
本節のコードはこのライブラリ を参照してください。
送信リクエスト
urllib の request モジュールを使うと、リクエストの送信を簡単に実現し、応答を得ることができます。
urlopen
urllib.request モジュールは、最も基本的な HTTP リクエストの構築方法を提供します。これを利用して、ブラウザのリクエスト発行プロセスを模倣できます。さらに、認証認可(authentication)、リダイレクション(redirection)、ブラウザ Cookies などの処理も備えています。
例として Python の公式サイトを取得してみましょう:
import urllib.request
response = urllib.request.urlopen('<https://www.python.org>')print(response.read().decode('utf-8'))
print(type(response))HTTPResposne 型のオブジェクトは、主に read、readinto、getheader、getheaders、fileno などのメソッド、および msg、version、status、reason、debuglevel、closed などの属性を含みます。
このオブジェクトを取得したら、response 変数に代入して、これらのメソッドと属性を呼び出し、返される結果の一連の情報を取得できます。
import urllib.request
response = urllib.request.urlopen('<https://www.python.org>')print(response.status)print(response.getheaders())print(response.getheader('Server'))
print(type(response))# <class 'http.client.HTTPResponse'>戻り値は HTTPResposne 型のオブジェクトで、主に read、readinto、getheader、getheaders、fileno などのメソッド、および msg、version、status、reason、debuglevel、closed などの属性を含みます。
このオブジェクトを response 変数に代入して、これらのメソッドと属性を呼び出し、返される結果の一連の情報を取得します。
例えば、read メソッドを呼び出すと返されるウェブページの内容を得られ、status 属性を呼び出すと返される結果のステータスコードを取得できます。200 はリクエスト成功、404 はページが見つからない、などを意味します。
最も基本的な urlopen メソッドを利用すれば、最も基本的でシンプルなウェブページの GET リクエストの取得を完了できます。
リンクにパラメータを渡したい場合は、まず urlopen メソッドの API を見てみましょう:
urllib.request.urlopen(url, data=None, [timeout,]*, cafile=None, capath=None, cadefault=False, context=None)
第1引数が URL のみ渡せる点に加え、data(追加データ)、timeout(タイムアウト時間)など、他の内容を渡すこともできます。
-
data パラメータ data パラメータは任意です。追加する場合には bytes 型のデータを渡します。したがって、dict の場合は urllib.parse モジュールの urlencode() を使って文字列に変換した後、bytes() でバイト列にします。さらにこのパラメータを渡すと GET ではなく POST で送信されます。
import urllib.parseimport urllib.requestdata = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf8')response = urllib.request.urlopen('<http://httpbin.org/post>', data=data)print(response.read())ここでは word=hello を渡しています。これは bytes(バイト列)にエンコードする必要があります。bytes() の第一引数は str でなければならず、第二引数には utf8 を指定します。url のエンコードには urllib.parse モジュールの urlencode() を使います。
送信先は httpbin.org です。ここは HTTP リクエストのテストを提供します。今回の URL は:http://httpbin.org/post です。このリンクは POST リクエストのテストに使え、Request のいくつかの情報を出力します。この中には私たちが渡した data パラメータも含まれます。
実行結果の例は以下のとおりです:
{"args": {},"data": "","files": {},"form": {"word": "hello"},"headers": {"Accept-Encoding": "identity","Content-Length": "10","Content-Type": "application/x-www-form-urlencoded","Host": "httpbin.org","User-Agent": "Python-urllib/3.5"},"json": null,"origin": "123.124.23.253","url":"<http://httpbin.org/post>"}渡したパラメータは form フィールドに現れており、これは POST によるフォーム送信を模倣してデータを伝送していることを意味します。
-
timeout パラメータ
timeout パラメータはタイムアウト時間を設定します。単位は秒で、設定した時間を超えても応答が無い場合は例外が発生します。指定しなければグローバルのデフォルト時間が使われます。HTTP、HTTPS、FTP リクエストすべてに対応しています。
import urllib.requestresponse = urllib.request.urlopen('<http://httpbin.org/get>', timeout=1)print(response.read())ここではタイムアウトを 1 秒に設定しています。1 秒経ってもサーバーが応答しない場合、URLError 例外が発生します。この例外は urllib.error モジュールに属し、原因はタイムアウトです。
このように timeout を設定して、長時間応答がない場合には取得をスキップするタイムアウト処理を実装できます。これには try except 文を用います。関連コードは次のとおりです:
import socketimport urllib.requestimport urllib.errortry:response = urllib.request.urlopen('<http://httpbin.org/get>', timeout=0.1)except urllib.error.URLError as e:if isinstance(e.reason, socket.timeout):print('TIME OUT')ここでは http://httpbin.org/get のテストリンクをリクエストし、タイムアウト時間を 0.1 秒に設定しています。URLError 例外を捕捉し、原因が socket.timeout 型かを判定して、タイムアウトでエラーになったことを示す TIME OUT を出力します。
timeout パラメータを使ってタイムアウト処理を実現するのは有用なこともあります。
-
その他のパラメータ
data パラメータと timeout パラメータ以外にも、context パラメータがあります。これは ssl.SSLContext の型で、SSL 設定を指定します。cafile と capath はそれぞれ CA 証明書とそのパスを指定します。HTTPS のリンクをリクエストする際に役立ちます。cadefault パラメータは現在は廃止され、デフォルトは False です。
urlopen メソッドの使い方は前述のとおりで、この最も基本的な方法を用いれば、単純なリクエストとウェブページの取得を実現できます。より詳しい情報は公式ドキュメントを参照してください:https://docs.python.org/3/library/urllib.request.html。
Request
urlopen メソッドを用いて最も基本的なリクエスト発行は実現できますが、これらの単純なパラメータだけでは完全なリクエストを構築するには不十分です。リクエストに Headers などの情報を加える必要がある場合には、より強力な Request クラスを用いて構築します。
まず、実例を使って Request の使い方を体感してみましょう:
import urllib.request
request = urllib.request.Request('<https://python.org>')response = urllib.request.urlopen(request)print(response.read().decode('utf-8'))分かるように、urlopen メソッドを使ってこのリクエストを送信していますが、今回はこのメソッドの引数が URL ではなく、Request 型のオブジェクトになっています。このデータ構造を構築することで、リクエストを独立したオブジェクトとして扱えるだけでなく、パラメータをより豊富で柔軟に設定できます。
その構築方法は次のとおりです:
class urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
- 第1引数の url はリクエスト URL で、必須です。その他は任意のパラメータです。
- 第2引数の data は渡す場合、bytes(バイト列)型で渡します。辞書の場合は urllib.parse モジュールの urlencode() を使ってエンコードします。
- 第3引数の headers は辞書で、これはリクエストヘッダです。headers パラメータを使って直接構築することも、リクエスト实例の add_header() メソッドを用いて追加することもできます。 リクエストヘッダを最もよく使う用途は User-Agent を変更してブラウザを偽装することです。デフォルトの User-Agent は Python-urllib です。例えば Firefox を偽装したい場合は次のように設定します:「
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/」- 第4引数の origin_req_host はリクエスト元のホスト名または IP アドレスを指します。
- 第5引数の unverifiable はこのリクエストが検証不可能かどうかを表します。デフォルトは False で、つまりユーザがこのリクエストの結果を受信する十分な権限を持っていないことを意味します。例として HTML ドキュメント内の画像をリクエストしますが、画像を自動取得する権限がない場合、unverifiable の値は TRUE になります。
- 第6引数の method は、GET、POST、PUT など、リクエストで使用するメソッドを示す文字列です。
from urllib import request, parse
url = '<http://httpbin.org/post>'headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Host': 'httpbin.org'}dict = {'name': 'Germey'}data = bytes(parse.urlencode(dict), encoding='utf8')req = request.Request(url=url, data=data, headers=headers, method='POST')response = request.urlopen(req)print(response.read().decode('utf-8'))ここでは 4 つの引数でリクエストを構築しています。url はリクエスト URL、headers には User-Agent と Host を指定、data には urlencode と bytes の方法でバイト列に変換したデータを使用しています。加えてリクエスト方法として POST を指定しています。
実行結果は以下のとおりです:
{ "args": {}, "data": "", "files": {}, "form": { "name": "Germey" }, "headers": { "Accept-Encoding": "identity", "Content-Length": "11", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)" }, "json": null, "origin": "219.224.169.11", "url":"<http://httpbin.org/post>"}観察すると、私たちが渡した data、headers、method が設定されていることが分かります。
さらに headers は add_header メソッドを使って追加することも可能です:
req = request.Request(url=url, data=data, method='POST')req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')このように、リクエストをより便利に構築し、リクエストの送信を実現できます。
高度な使い方
上記の過程ではリクエストを構築することはできますが、Cookies の処理、プロキシ設定など、より高度な操作にはどう対応すればよいのでしょうか?
そのためには Handler というより強力なツールが登場します。簡単に言えば、各種処理を担当する「ハンドラ」として理解できます。認証処理、Cookies の処理、代理設定の処理など、HTTP リクエストのあらゆることをほぼ実現できます。
まず、urllib.request モジュールの BaseHandler クラスを紹介します。これは他のハンドラの親クラスで、default_open、protocol_request などの基本的なメソッドを提供します。
次に、BaseHandler を継承したさまざまな Handler クラスが現れます。例として以下のものがあります。
- HTTPDefaultErrorHandler:HTTP 応答エラーを処理します。エラーはすべて HTTPError 型の例外としてスローされます。
- HTTPRedirectHandler:リダイレクションを処理します。
- HTTPCookieProcessor:Cookies を処理します。
- ProxyHandler:プロキシを設定します。デフォルトのプロキシは空です。
- HTTPPasswordMgr:パスワードを管理します。ユーザー名とパスワードの表を保持します。
- HTTPBasicAuthHandler:認証を管理します。リンクを開く際に認証が必要な場合にこれを使って認証問題を解決できます。
他にもさまざまな Handler があり、詳細は公式ドキュメントを参照してください:https://docs.python.org/3/library/urllib.request.html#urllib.request.BaseHandler
もう一つ重要なクラスは OpenerDirector、別名 Opener です。これまで urlopen というメソッドを使ってきましたが、実際には urllib が提供してくれる Opener に相当します。
では、なぜ Opener を導入するのでしょうか。より高度な機能を実現するためです。
Opener は open メソッドを使用でき、返される型は urlopen と同じで、また Handler を使って Opener を構築することができます。
-
検証
一部のサイトでは開くときにダイアログが表示され、ユーザー名とパスワードの入力を求められ、認証に成功して初めてページが閲覧できることがあります。
このようなページをリクエストするには HTTPBasicAuthHandler を用います。関連コードは以下のとおりです:
from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_openerfrom urllib.error import URLError
username = 'username'password = 'password'url = '<http://localhost:5000/>'
p = HTTPPasswordMgrWithDefaultRealm()p.add_password(None, url, username, password)auth_handler = HTTPBasicAuthHandler(p)opener = build_opener(auth_handler)
try: result = opener.open(url) html = result.read().decode('utf-8') print(html)except URLError as e: print(e.reason)ここではまず HTTPBasicAuthHandler オブジェクトを作成します。引数は HTTPPasswordMgrWithDefaultRealm オブジェクトで、add_password メソッドを使ってユーザー名とパスワードを追加します。これで認証処理用の Handler が作成されました。
次に、この Handler を使い build_opener で Opener を構築します。これによりリクエストを送信するときにはすでに認証が完了している状態になります。Open の結果として得られるのは認証後のページのソースコードです。
-
プロキシ
一部のサイトは一定期間にある IP のアクセス回数を検出します。アクセス回数が多すぎるとアクセスを禁止する場合があります。その場合、プロキシサーバを設定して一定期間ごとに別のプロキシを使用することで、IP が禁止されても別の IP に切り替えてクロールを続行できます。
プロキシの設定も Handler を使って実現します。以下に ProxyHandler の使い方を示します:
from urllib.error import URLErrorfrom urllib.request import ProxyHandler, build_opener
proxy_handler = ProxyHandler({ 'http': '<http://127.0.0.1>:9743', 'https': '<https://127.0.0.1:9743>'})opener = build_opener(proxy_handler)try: response = opener.open('<https://www.baidu.com>') print(response.read().decode('utf-8'))except URLError as e: print(e.reason)ここではローカルにプロキシを構築し、ポート 9743 で動作しているとします。ProxyHandler の引数はディクショナリで、キーはプロトコルの種類(HTTP、HTTPS など)、値はプロキシのリンクです。複数のプロキシを追加することもできます。
その後、この Handler を用いて build_opener で Opener を構築し、リクエストを送信します。
-
Cookies
Cookies の処理には対応する Handler が必要です。
import http.cookiejar, urllib.request
cookie = http.cookiejar.CookieJar()handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')for item in cookie: print(item.name+"="+item.value)まず CookieJar オブジェクトを宣言します。次に HTTPCookieProcessor を用いて Handler を構築し、build_opener で Opener を作成します。open 関数を実行します。
このように、ここでは各クローラ出力として Cookie の名前と値が表示されます。
また、ファイル形式へ出力することもできます。Cookies は実際にはテキスト形式で保存されるためです。
filename = 'cookies.txt'cookie = http.cookiejar.MozillaCookieJar(filename)handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')cookie.save(ignore_discard=True, ignore_expires=True)この場合 CookieJar は MozillaCookieJar に変更します。ファイル作成時には Mozilla 形式の Cookies ファイルが使用され、Cookies としての操作や、読み込み・保存などのイベントを処理できます。
さらに LWPCookieJar も Cookies を読み書きできますが、保存形式は MozillaCookieJar とは異なり、libwww-perl(LWP)形式の Cookies ファイルとして保存されます。
LWP 形式の Cookies ファイルとして保存するには、宣言時に次のようにします: cookie = http.cookiejar.LWPCookieJar(filename)
Cookies ファイルを生成した後、ファイルから読み込み、それを使って処理するには次のようにします:
cookie = http.cookiejar.LWPCookieJar()cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True)handler = urllib.request.HTTPCookieProcessor(cookie)opener = urllib.request.build_opener(handler)response = opener.open('<http://www.baidu.com>')print(response.read().decode('utf-8'))ここでは load メソッドを呼び出してローカルの Cookies ファイルを読み取り、Cookies の内容を取得します。しかし前提として LWPCookieJar 形式の Cookies を生成してファイルとして保存してから読み込み、同じ方法で Handler と Opener を構築して操作を完了します。
実行結果が正常であれば、百度のページのソースコードが出力されます。
上記の方法を使えば、ほとんどのリクエスト機能を設定できます。
これが urllib ライブラリの request モジュールの基本的な使い方です。より多くの機能を実現したい場合は、公式ドキュメントの説明を参照してください:https://docs.python.org/3/library/urllib.request.html#basehandler-objects
处理异常
前節ではリクエストの送信プロセスを理解しましたが、ネットワークが悪い場合には例外が発生します。これを処理せずに放置すると、プログラムがエラーで終了してしまう可能性があるため、例外処理は非常に重要です。urllib の error モジュールは request モジュールが発生させる例外を定義します。問題が発生した場合、request モジュールは error モジュールで定義された例外を投げます。
URLError
URLError クラスは urllib ライブラリの error モジュールに属し、OSError の派生クラスです。request モジュールが発生させる例外の基底クラスになります。reason という属性を持ち、エラーの原因を返します。
from urllib import request, errortry: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.URLError as e: print(e.reason)存在しないページを開こうとすると当然エラーになりますが、この例では URLError を捕捉して、実行結果として Not Found が表示されます。プログラムは例外を直接報告せず、適切に処理されます。
HTTPError
HTTPError は URLError のサブクラスで、HTTP リクエストのエラー(認証に失敗した等)を処理するためのものです。以下の3つの属性を持ちます。
- code:HTTP ステータスコードを返します。例えば 404 はページが存在しない、500 はサーバ内部エラーなど。
- reason:親クラスと同様、エラーの原因を返します。
- headers:リクエストヘッダを返します。
from urllib import request,errortry: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.HTTPError as e: print(e.reason, e.code, e.headers, sep='\\n')実行結果は以下のとおりです:
Not Found404Server: nginx/1.4.6 (Ubuntu)Date: Wed, 03 Aug 2016 08:54:22 GMTContent-Type: text/html; charset=UTF-8Transfer-Encoding: chunkedConnection: closeX-Powered-By: PHP/5.5.9-1ubuntu4.14Vary: CookieExpires: Wed, 11 Jan 1984 05:00:00 GMTCache-Control: no-cache, must-revalidate, max-age=0Pragma: no-cacheLink: <https://cuiqingcai.com/wp-json/>; rel="<https://api.w.org/>"同じ URL に対しても、HTTPError 例外を捕捉して reason、code、headers の情報を取得できます。URLError は HTTPError の父クラスなので、まず子クラスのエラーを捕捉し、次に父クラスのエラーを捕捉するという書き方がよい場合があります。上述のコードのより良い書き方は次のとおりです:
from urllib import request, error
try: response = request.urlopen('<https://cuiqingcai.com/index.htm>')except error.HTTPError as e: print(e.reason, e.code, e.headers, sep='\\n')except error.URLError as e: print(e.reason)else: print('Request Successfully')このようにして、まず HTTPError を捕捉してエラー状態コード、原因、headers などの情報を取得します。HTTPError でなければ URLError が捕捉され、エラーの原因が出力されます。最後に else で正常時の処理を行います。これは比較的良い例外処理の書き方です。 時には reason 属性が文字列でないこともあり得ます。
import socketimport urllib.requestimport urllib.error
try: response = urllib.request.urlopen('<https://www.baidu.com>', timeout=0.01)except urllib.error.URLError as e: print(type(e.reason)) if isinstance(e.reason, socket.timeout): print('TIME OUT')ここではタイムアウトを強制的に発生させることで timeout 例外を確認しています。
実行結果は次のとおりです:
<class'socket.timeout'> TIME OUT
reason 属性の結果が socket.timeout クラスであることが分かります。したがって isinstance メソッドを用いて型を判定し、より詳しい例外判断を行うことができます。
リンクの解析
前述のとおり、urllib ライブラリには parse モジュールも用意されており、URL の分解・結合・変換などの標準インタフェースを提供します。ファイル、ftp、gopher、hdl、http、https、imap、mailto などの URL 処理をサポートします。本節では、このモジュールでよく使われるメソッドを見て、その利便性を確認します。
urlparse
このメソッドは URL の識別と分割を実現します
from urllib.parse import urlparse
result = urlparse('<http://www.baidu.com/index.html;user?id=5#comment>')print(type(result), result)<class 'urllib.parse.ParseResult'> ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
戻り値は ParseResult 型のオブジェクトで、6つの部分(scheme、netloc、path、params、query、fragment)を含みます。
urlparse はそれを6つの部分に分割します。大まかな規則として、特定の区切り文字で分けられます。たとえば、:// の前が scheme(プロトコルを表す)、最初の / の前が netloc(ドメイン名)、後ろが path、セミコロン ; の後が params、クエリ条件 ? の後が query、シャープ # の後がアンカー(ページ内の位置)です。
従って、標準的なリンクの形式は次の通りです:
scheme://netloc/path;params?query#fragment
標準の URL はこの規則に従います。urlparse メソッドを使って分割できます。
# API設定urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)3つの引数があることが分かります。
- urlstring:必須。解析対象の URL。
- scheme:デフォルトのプロトコル(http や https など)。URL に scheme 情報が含まれていない場合にのみ有効です。URL に scheme 情報が含まれていれば、解析された scheme が返されます。
- allow_fragments:fragment を無視するかどうか。False に設定すると、fragment 部分は無視され、path、parameters、query の一部として解析され、fragment は空になります。
urlunparse
urlparse に対する対になる関数で、urlunparse は引数として反復可能なオブジェクトを受け取りますが、長さは 6 でなければなりません。そうでないと、引数の数が不足または多すぎるというエラーになります。
from urllib.parse import urlunparse
data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']print(urlunparse(data))ここで data はリスト型を使用しています。もちろん他の型、例えばタプルや特定のデータ構造を使うことも可能です。
http://www.baidu.com/index.html;user?a=6#comment
urlsplit
このメソッドは urlparse と非常に似ていますが、params 部分を個別には解析せず、5 つの結果だけを返します。上の例の params は path に結合されます。
from urllib.parse import urlsplit
result = urlsplit('<http://www.baidu.com/index.html;user?id=5#comment>')print(result)結果は SplitResult で、これは実質的にタプル型で、属性取得もインデックス取得も可能です。
urlunsplit
urlunparse と似ており、リンクの各部を結合して完全なリンクを作成する方法です。引数は反復可能なオブジェクト(リスト、タプルなど)で、唯一の違いは長さが 5 でなければならない点です。
from urllib.parse import urlunsplit
data = ['http', 'www.baidu.com', 'index.html', 'a=6', 'comment']print(urlunsplit(data))http://www.baidu.com/index.html?a=6#comment
urljoin
urlunparse や urlunsplit でリンクを結合することは可能ですが、前提として長さが特定のサイズである必要があります。リンクの各部を明確に分けて生成するもう一つの方法として urljoin があります。base_url(基礎リンク)を第一引数として与え、新しいリンクを第二引数として渡します。この方法は base_url の scheme、netloc、path を解析して、新しいリンクに欠落している部分を補完し、最終的な結果を返します。
from urllib.parse import urljoin
print(urljoin('<http://www.baidu.com>', 'FAQ.html'))print(urljoin('<http://www.baidu.com>', '<https://cuiqingcai.com/FAQ.html>'))print(urljoin('<http://www.baidu.com/about.html>', '<https://cuiqingcai.com/FAQ.html>'))print(urljoin('<http://www.baidu.com/about.html>', '<https://cuiqingcai.com/FAQ.html?question=2>'))print(urljoin('<http://www.baidu.com?wd=abc>', '<https://cuiqingcai.com/index.php>'))print(urljoin('<http://www.baidu.com>', '?category=2#comment'))print(urljoin('www.baidu.com', '?category=2#comment'))print(urljoin('www.baidu.com#comment', '?category=2'))実行結果は以下のとおり:
<http://www.baidu.com/FAQ.html><https://cuiqingcai.com/FAQ.html><https://cuiqingcai.com/FAQ.html><https://cuiqingcai.com/FAQ.html?question=2><https://cuiqingcai.com/index.php><http://www.baidu.com?category=2#comment>www.baidu.com?category=2#commentwww.baidu.com?category=2ベース URL が提供する三つの内容は scheme、netloc、path であり、これらの値が新しいリンクに存在しなければ補完されます。base_url のパラメータ、query、fragment は作用しません。urljoin によってリンクの解析・結合・生成を簡単に実現できます。
urlencode
ここでまたよく使われるメソッドを紹介します―― urlencode。GET リクエストのパラメータを作成する際に非常に有用です
from urllib.parse import urlencode
params = { 'name': 'germey', 'age': 22}base_url = '<http://www.baidu.com>?'url = base_url + urlencode(params)print(url)ここでは最初にパラメータを辞書型として表現し、 urlencode メソッドを呼び出して GET リクエストパラメータへシリアライズします。
http://www.baidu.com?name=germey&age=22
このように、パラメータは辞書型から GET リクエストのパラメータへと変換されます。このメソッドはとても頻繁に使われます。パラメータを作成するのをさらに簡単にするために、事前に辞書を表現しておき、URL のパラメータへ変換するだけ、という方法もあります。
parse_qs
シリアライズしたものがあれば、それをデシリアライズすることもできます。GET リクエストパラメータの列を持っている場合、parse_qs メソッドを使って辞書に戻すことができます。以下は例です:
from urllib.parse import parse_qs
query = 'name=germey&age=22'print(parse_qs(query))parse_qsl
さらに、parse_qsl メソッドもあり、パラメータをタプルのリストとして変換します。
from urllib.parse import parse_qsl
query = 'name=germey&age=22'print(parse_qsl(query))quote
このメソッドは内容を URL エンコードされた形式に変換します。URL に中国語などのパラメータが含まれる場合、文字化けを避けるためにこの方法で中国語を URL エンコードします。
from urllib.parse import quote
keyword = ' 壁纸 'url = '<https://www.baidu.com/s?wd=>' + quote(keyword)print(url)ここでは中国語の検索語を宣言し、 quote メソッドで URL エンコードします。最終的な結果は次のとおりです:
https://www.baidu.com/s?wd=% E5% A3%81% E7% BA% B8
unquote
quote メソッドに続いて unquote メソッドもあります。URL のデコードを行います。以下は例です:
from urllib.parse import unquote
url = '<https://www.baidu.com/s?wd=%> E5% A3%91% E7% BA% B8'print(unquote(url))これは URL エンコード済みの結果のデコード例で、結果は次のとおりです:
https://www.baidu.com/s?wd = 壁纸
unquote メソッドを使うとデコードを簡単に実現できます。
Robots プロトコルの分析
urllib の robotparser モジュールを利用して、サイトの Robots プロトコルの分析を実現します。本節ではこのモジュールの使い方を簡単に見ていきます。
Robots プロトコル
Robots プロトコルは「クローラー規約」またはロボット規約とも呼ばれ、全名はネットワーク クローラ除外規約(Robots Exclusion Protocol)です。クローラーや検索エンジンに対して、どのページをクローリングしてよいかを知らせるためのものです。通常 robots.txt という名前のテキストファイルで、サイトのルートディレクトリに置かれます。 検索クローラがサイトにアクセスする際、まずサイトのルートディレクトリに robots.txt が存在するかを確認します。存在すれば、その規定されたクローリング範囲に従ってクローリングします。ファイルが見つからない場合、クローラーはすべての公開ページを訪問します。 以下は robots.txt のサンプルです:
User-agent: *Disallow: /Allow: /public/3これはすべての検索クローラに対して public ディレクトリのみをクローリングを許可することを実現します。上記の内容を robots.txt ファイルとしてサイトのルートに保存し、サイトのエントリーファイル(例えば index.php、index.html、index.jsp など)と同じ場所に置きます。
上の User-agent は検索クローラの名前を示します。ここで * と設定すると、この規則はすべてのクローラに有効になります。例えば次のように設定します:
User-agent: Baiduspider
これにより百度クローラにはこの規則が有効になります。複数の User-agent 記述がある場合、複数のクローラがクローリング制限を受けますが、少なくとも 1 つは指定する必要があります。
Disallow はクローリングを許可しないディレクトリを指します。上の例では / が設定されているため、すべてのページのクローリングを禁止します。
Allow は通常、Disallow と一緒に使われ、特定の制約を除外する場合に用いられます。今は /public/ に設定します。すべてのページはクローリング不可ですが、public ディレクトリは取得可能、という意味になります。
以下に他の例を見てみましょう。
<!-- 禁止すべきすべてのクローラがすべてのディレクトリをクロール不可にするコードは以下です: -->User-agent: *Disallow: /<!-- すべてのクローラが任意のディレクトリをクロール可能にするコードは以下です: -->User-agent: *Disallow:<!-- 直接 robots.txt ファイルを空にしてもよいです。 --><!-- サイトのいくつかのディレクトリを禁止するコードは以下です: -->User-agent: *Disallow: /private/Disallow: /tmp/<!-- 1つの特定のクローラだけを許可するコードは以下です: -->User-agent: WebCrawlerDisallow:User-agent: *Disallow: /これらは robots.txt のよくある書き方です。
クローラ名
クローラ名はどこから来たのでしょうか。なぜこの名前なのでしょうか。実は固定の名前があり、例えば百度のクローラ名は BaiduSpider です。
| クローラ名 | 名称 | サイト |
|---|---|---|
| BaiduSpider | 百度 | <www.baidu.com> |
| Googlebot | 谷歌 | <www.google.com> |
| 360Spider | 360 検索 | <www.so.com> |
| YodaoBot | 有道 | <www.youdao.com> |
| ia_archiver | Alexa | <www.alexa.cn> |
| Scooter | altavista | <www.altavista.com> |
robotparser
Robots プロトコルを理解した後、robotparser モジュールを使って robots.txt を解析できます。本節ではこのモジュールの使い方を簡単に紹介します。
ロボットファイルのパーサ RobotFileParser
このクラスは robots.txt の URL を渡して初期化します。宣言時にリンクを渡しても、後から set_url() を使って設定しても構いません。
urllib.robotparser.RobotFileParser(url='')
次にこのクラスのよく使うメソッドを挙げます。
- set_url :robots.txt ファイルのリンクを設定します。RobotFileParser オブジェクトを作成する時にリンクを渡した場合は、このメソッドを使う必要はありません。
- read:robots.txt ファイルを読み込み、分析します。このメソッドは読み込みと分析を行いますが、これを呼び出さないと以降の判断は全て False になります。必ずこのメソッドを呼び出してください。内容は返りませんが、読み込み操作を実行します。
- parse:robots.txt のファイルを解析します。引数は robots.txt の一部の行の内容で、robots.txt の文法規則に従って分析します。
- can_fetch:このメソッドは 2 つの引数を取ります。1 つ目は User-agent、2 つ目は取得する URL。URL をこの検索エンジンが取得できるかどうかを True または False で返します。
- mtime:最後に robots.txt を取得・分析した時刻を返します。長時間にわたる分析・取得を行う検索クローラーには必要で、最新の robots.txt を取得するため定期的にチェックする必要があります。
- modified:現在時刻を前回取得・分析 robots.txt の時刻として設定します。長時間分析・取得を行うクローラーにも有用です。
以下、例を見てみましょう:
from urllib.robotparser import RobotFileParserrp = RobotFileParser()rp.set_url('<http://www.jianshu.com/robots.txt>')rp.read()print(rp.can_fetch('*', '<http://www.jianshu.com/p/b67554025d7d>'))print(rp.can_fetch('*', "<http://www.jianshu.com/search?q=python&page=1&type=collections>"))简书を例に取り、RobotFileParser オブジェクトを作成し、set_url メソッドで robots.txt のリンクを設定します。宣言時に以下のように設定することもできます:
rp = RobotFileParser('<http://www.jianshu.com/robots.txt>')
次に can_fetch を使ってウェブページがクロール可能かを判断します。
実行結果は以下のとおりです:
TrueFalse同様に parse メソッドを使って読み込みと分析を行うこともできます。以下はその例:
from urllib.robotparser import RobotFileParserfrom urllib.request import urlopenrp = RobotFileParser()rp.parse(urlopen('<http://www.jianshu.com/robots.txt').read().decode('utf-8').split('\\n>'))print(rp.can_fetch('*', '<http://www.jianshu.com/p/b67554025d7d>'))print(rp.can_fetch('*', "<http://www.jianshu.com/search?q=python&page=1&type=collections>"))requests
前節では urllib の基本的な使い方を見てきましたが、実際には扱いづらい点も多く、例えばウェブサイトの認証や Cookies の処理には Opener や Handler を作る必要がありました。より便利にこれらを実現するための、より強力なライブラリである requests を使うと、Cookies、認証、プロキシ設定などの操作も難しくありません。これからその強力さを体感してみましょう。
基本的な使い方
-
準備 まずは requests ライブラリが正しくインストールされていることを確認してください。
-
実例の導入 urllib ライブラリの urlopen メソッドは実質 GET でウェブページをリクエストしますが、requests の対応するメソッドは get です。以下の例で見ていきましょう:
import requests
r = requests.get('<https://www.baidu.com/>')print(type(r))print(r.status_code)print(type(r.text))print(r.text)print(r.cookies)実行結果は以下のとおりです:
<class 'requests.models.Response'>200<class'str'><html> <head> <script> location.replace(location.href.replace("https://","http://")); </script> </head> <body> <noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript> </body></html>(RequestsCookieJar[<Cookie BIDUPSID=992C3B26F4C4D09505C5E959D5FBC005 for .baidu.com/>, <CookiePSTM=1472227535 for .baidu.com/>, <Cookie __bsi=15304754498609545148_00_40_N_N_2_0303_C02F_N_N_N_0for .www.baidu.com/>, <Cookie BD_NOT_HTTPS=1 for www.baidu.com/>])ここでは get メソッドを使って urlopen と同じ操作を実現し、Response オブジェクトを取得します。続いて Response の型、ステータスコード、レスポンス本文の型、内容、Cookies をそれぞれ出力します。
この結果から、戻り値の型は requests.models.Response、レスポンス本文の型は文字列 str、Cookies の型は RequestsCookieJar であることが分かります。
GET リクエストを成功させるには十分ですが、他のリクエストタイプも 1 行で実行できます。以下は例です:
r = requests.post('<http://httpbin.org/post>')r = requests.put('<http://httpbin.org/put>')r = requests.delete('<http://httpbin.org/delete>')r = requests.head('<http://httpbin.org/get>')r = requests.options('<http://httpbin.org/get>')これらはそれぞれ POST、PUT、DELETE、HEAD、OPTIONS のリクエストを実現します。
- GET リクエスト
HTTP において最も一般的なリクエストの1つが GET です。requests を使って GET リクエストを構築する方法を詳しく見ていきます。
基本例 まず、最も単純な GET リクエストを作成します。リンクは http://httpbin.org/get で、サイトは GET リクエストが発行された場合、そのリクエスト情報を返します:
import requests
r = requests.get('<http://httpbin.org/get>')print(r.text)実行結果は以下のとおりです:
{"args": {},"headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Host": "httpbin.org", "User-Agent": "python-requests/2.10.0"},"origin": "122.4.215.33","url": "<http://httpbin.org/get>"}このように、GET リクエストを正常に発行し、返却結果にはヘッダ、URL、IP などの情報が含まれています。 GET リクエストに追加情報を付与するには、params パラメータを使います。
import requests
data = { 'name': 'germey', 'age': 22}r = requests.get("<http://httpbin.org/get>", params=data)print(r.text)リクエスト URL は自動的に http://httpbin.org/get?age=22&name=germey の形に組み立てられます。
返却データの型は実際には str ですが、これは特別で、JSON 形式の文字列です。従って、返却結果を辞書形式で直接解析したい場合には json メソッドを使います。以下は例です:
import requests
r = requests.get("<http://httpbin.org/get>")print(type(r.text))print(r.json())print(type(r.json()))json メソッドを呼ぶと、JSON 形式の文字列を辞書へ変換できます。
ただし、返却結果が JSON 形式でない場合は、解析エラーが発生し、 json.decoder.JSONDecodeError 例外が投げられます。
- ウェブを取得
上記のリクエストURL は JSON 形式の文字列を返しますので、通常のウェブページを取得する場合も、当然内容を得られます。以下は「知乎」→「发现」ページの例です。
import requestsimport re
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}r = requests.get("<https://www.zhihu.com/explore>", headers=headers)pattern = re.compile('explore-feed.*?question_link.*?>(.*?)</a>', re.S)titles = re.findall(pattern, r.text)print(titles)ここでは headers 情報を追加しており、User-Agent の情報を含んでいます。これがないと知乎はクロールを制限します。
- バイナリデータの取得
上記の例では知乎のページを取得していますが、実際には HTML ドキュメントが返されます。画像、音声、動画などのファイルを取得するにはどうすればよいでしょうか?これらは本質的にバイナリデータであり、保存形式や解析方法があるため、バイナリデータを取得する必要があります。
以下は GitHub のサイトアイコンの例です:
import requests
r = requests.get("<https://github.com/favicon.ico>")print(r.text)print(r.content)ここで取得しているのは、ブラウザのタブに表示される小さなアイコン(サイトアイコン)です。
続いて、取得したアイコンを保存します:
import requests
r = requests.get("<https://github.com/favicon.ico>")with open('favicon.ico', 'wb') as f: f.write(r.content)open 関数の第1引数はファイル名、第2引数はバイナリ書き込みを指定します。これでファイルにバイナリデータを書き込みます。
実行後、フォルダ内に favicon.ico という名前のアイコンが作成されます。音声ファイルや動画ファイルでも同様の方法で取得できます。
- headers の追加
urllib.request と同様、headers パラメータを使ってヘッダー情報を渡すことができます。
import requests
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}r = requests.get("<https://www.zhihu.com/explore>", headers=headers)print(r.text)もちろん、headers パラメータには他の任意のフィールド情報を追加できます。
- POST リクエスト
前述の GET リクエストに対して、もう1つ一般的なリクエスト方法として POST があります。
import requests
data = {'name': 'germey', 'age': '22'}r = requests.post("<http://httpbin.org/post>", data=data)print(r.text)- 応答
リクエストを送信すると、当然応答が返ってきます。上述の例では text と content を使って応答の内容を取得しました。その他にも、ステータスコード、応答ヘッダ、Cookies などの情報を取得するための属性やメソッドが多数用意されています。
import requests
r = requests.get('<http://www.jianshu.com>')print(type(r.status_code), r.status_code)print(type(r.headers), r.headers)print(type(r.cookies), r.cookies)print(type(r.url), r.url)print(type(r.history), r.history)ここでは status_code、headers、cookies、url、history の各属性を出力しています。
ステータスコードは、リクエストが成功したかどうかを判断するためによく使われます。requests には組み込みのステータスコード照会オブジェクト requests.codes も用意されています。
import requests
r = requests.get('<http://www.jianshu.com>')exit() if not r.status_code == requests.codes.ok else print('Request Successfully')このコードは、返却コードを組み込みの成功コードと比較して、リクエストが正常に応答を得られたかを判断します。requests.codes.ok が返す値は 200 です。
以下は代表的な戻り値のコードと、それに対応する意味の一覧です:
# 情報性ステータスコード
100: ('continue',),101: ('switching_protocols',),102: ('processing',),103: ('checkpoint',),122: ('uri_too_long', 'request_uri_too_long'),
# 成功ステータスコード
200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\\\o/', '✓'),201: ('created',),202: ('accepted',),203: ('non_authoritative_info', 'non_authoritative_information'),204: ('no_content',),205: ('reset_content', 'reset'),206: ('partial_content', 'partial'),207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'),208: ('already_reported',),226: ('im_used',),
# リダイレクト
300: ('multiple_choices',),301: ('moved_permanently', 'moved', '\\\\o-'),302: ('found',),303: ('see_other', 'other'),304: ('not_modified',),305: ('use_proxy',),306: ('switch_proxy',),307: ('temporary_redirect', 'temporary_moved', 'temporary'),308: ('permanent_redirect', 'resume_incomplete', 'resume',), # These 2 to be removed in 3.0
# クライアントエラー
400: ('bad_request', 'bad'),401: ('unauthorized',),402: ('payment_required', 'payment'),403: ('forbidden',),404: ('not_found', '-o-'),405: ('method_not_allowed', 'not_allowed'),406: ('not_acceptable',),407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'),408: ('request_timeout', 'timeout'),409: ('conflict',),410: ('gone',),411: ('length_required',),412: ('precondition_failed', 'precondition'),413: ('request_entity_too_large',),414: ('request_uri_too_large',),415: ('unsupported_media_type', 'unsupported_media', 'media_type'),416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'),417: ('expectation_failed',),418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'),421: ('misdirected_request',),422: ('unprocessable_entity', 'unprocessable'),423: ('locked',),424: ('failed_dependency', 'dependency'),425: ('unordered_collection', 'unordered'),426: ('upgrade_required', 'upgrade'),428: ('precondition_required', 'precondition'),429: ('too_many_requests', 'too_many'),431: ('header_fields_too_large', 'fields_too_large'),444: ('no_response', 'none'),449: ('retry_with', 'retry'),450: ('blocked_by_windows_parental_controls', 'parental_controls'),451: ('unavailable_for_legal_reasons', 'legal_reasons'),499: ('client_closed_request',),
# サーバエラー
500: ('internal_server_error', 'server_error', '/o\\\\', '✗'),501: ('not_implemented',),502: ('bad_gateway',),503: ('service_unavailable', 'unavailable'),504: ('gateway_timeout',),505: ('http_version_not_supported', 'http_version'),506: ('variant_also_negotiates',),507: ('insufficient_storage',),509: ('bandwidth_limit_exceeded', 'bandwidth'),510: ('not_extended',),511: ('network_authentication_required', 'network_auth', 'network_authentication')例えば 404 のステータスかどうかを判断したい場合は、requests.codes.not_found と比較することができます。
requests の高度な使い方
前節では requests の基本的な使い方、GET・POST リクエスト、Response オブジェクトについて解説しました。ここでは、ファイルのアップロード、Cookies の設定、プロキシ設定など、requests の高度な使い方を紹介します。
- ファイルアップロード
requests はデータの送信を模倣できます。もしサイトがファイルのアップロードを要求する場合にも対応できます。
import requests
files = {'file': open('favicon.ico', 'rb')}r = requests.post('<http://httpbin.org/post>', files=files)print(r.text)このサイトはファイルアップロードの部分をファイルとして返します。response の中の files フィールドがあり、form フィールドは空であることを意味します。
- Cookies
これまで urllib で Cookies を扱ってきましたが、requests を使えば Cookies の取得・設定が一段と簡単になります。
import requests
r = requests.get('<https://www.baidu.com>')print(r.cookies)for key, value in r.cookies.items(): print(key + '=' + value)ここでは cookies プロパティを呼ぶだけで Cookies を取得できます。これは RequestsCookieJar 型で、items() でタプルのリストへ変換し、各 Cookies の名称と値を出力します。
もちろん、Cookies を使ってログイン状態を維持することもできます。以下は知乎を例に説明します。
まず知乎にログインして、Headers の Cookie 内容をコピーして Headers に設定してリクエストを送ります。
import requests
headers = { 'Cookie': '', 'Host': 'www.zhihu.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36',}r = requests.get('<https://www.zhihu.com>', headers=headers)print(r.text)もちろん、cookies パラメータを用いて設定することもできます。ただしこの場合は RequestsCookieJar オブジェクトを構築する必要があり、cookie を分割する必要があります。やや煩雑ですが、効果は同じです。
import requests
cookies = ''jar = requests.cookies.RequestsCookieJar()headers = { 'Host': 'www.zhihu.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'}for cookie in cookies.split(';'): key, value = cookie.split('=', 1) jar.set(key, value)r = requests.get('<http://www.zhihu.com>', cookies=jar, headers=headers)print(r.text)ここではまず新しく RequestCookieJar オブジェクトを作成し、コピーしてきた Cookies を split で分割して、set メソッドで各 Cookie のキーと値を設定し、requests の get() メソッドに cookies パラメータとして渡します。もちろん、知乎自体の制限のため、headers パラメータには cookie フィールドを設定する必要があります。
テスト後、知乎 へのログインが正常に機能することが確認できます。
- セッションの維持
requests では、単に get または post のような方法を使ってウェブページのリクエストを模倣するだけで、実際には異なるセッションを利用することになります。つまり、別々のブラウザで異なるページを開いている状態です。
この問題を解決する主要な方法は、同じセッションを維持すること、つまり cookies の問題を気にせず、1 つのセッションを維持することです。これを実現するには Session オブジェクトを使います。
import requests
s = requests.Session()s.get('<http://httpbin.org/cookies/set/number/123456789>')r = s.get('<http://httpbin.org/cookies>')print(r.text)このように、Session を使えば Cookies の問題を気にせず、同じセッションを模倣することができます。通常はログイン成功後の次の操作を行う場合に使われます。
- SSL 証明書の検証
requests は証明書検証の機能を提供します。HTTP リクエストを送る際に SSL 証明書を確認します。verify パラメータでこの検証を行うかどうかを制御します。デフォルトは True で、自動的に検証されます。
import requests
response = requests.get('<https://www.12306.cn>', verify=False)print(response.status_code)また、ローカル証明書をクライアント証明書として使用することもできます。これは単一ファイル(秘密鍵と証明書を含む)または二つのファイルパスを含むタプルです:
import requests
response = requests.get('<https://www.12306.cn>', cert=('/path/server.crt', '/path/key'))print(response.status_code)注意:ローカルの秘密鍵は非暗号状態である必要があります。暗号化された鍵はサポートされていません。
- 代理設定
一部のサイトには、テスト中に複数回リクエストすると、验证码が表示されたり、認証ページへリダイレクトされたり、場合によってはクライアントの IP がブロックされたりすることがあります。これを回避するには代理設定が有効です。以下のように proxies パラメータを使います:
import requests
proxies = {'http': '<http://10.10.1.10:3128>','https': '<http://10.10.1.10:1080>',}
requests.get('<https://www.taobao.com>', proxies=proxies)この例は有効な代理を使っていない場合もあるので、実際にはご自身の有効な代理を使って試してください。
HTTP Basic Auth が必要な場合には、http://user:password@host:port のような形式でプロキシを設定することもできます:
import requests
requests.get('<https://www.taobao.com>', proxies=proxies)requests は SOCKS プロキシにも対応しています。まず socks ライブラリをインストールします:
pip3 install"requests[socks]"
その後、SOCKS プロキシを使う例は次のとおりです:
import requests
proxies = { 'http': 'socks5://user:password@host:port', 'https': 'socks5://user:password@host:port'}requests.get('<https://www.taobao.com>', proxies=proxies)- タイムアウト設定
ローカルネットワークが安定していない、またはサーバの応答が遅い場合、応答を待つ時間が非常に長くなることがあります。サーバがすぐに応答しない場合には timeout を設定してタイムアウトを設け、超過するとエラーを発生させることができます。timeout はこのリクエストの総待機時間を表します。
import requests
r = requests.get('<https://www.taobao.com>', timeout=1)print(r.status_code)このようにしてタイムアウトを 1 秒に設定します。1 秒以内に応答がなければ例外が発生します。実際にはリクエストは接続(connect)と読み取り(read)の二段階に分かれており、上の timeout はこの両方の総和として計算されます。個別に指定したい場合はタプルを渡します:
r = requests.get('<https://www.taobao.com>', timeout=(5, 30))
永久に待つ必要がある場合は timeout=None に設定します。あるいは省略しても良いです。サーバが動作していて、応答が極端に遅い場合でも timeout には達しません。使い方は以下のとおりです:
r = requests.get('<https://www.taobao.com>', timeout=None)
または単にパラメータを渡さない。
- 認証
ウェブサイトにアクセスする際には、ログイン認証のページに遭遇することがあります。
この場合、requests が用意している認証機能を使用できます。
import requestsfrom requests.auth import HTTPBasicAuth
r = requests.get('<http://localhost:5000>', auth=HTTPBasicAuth('username', 'password'))print(r.status_code)ユーザー名とパスワードが正しければ、認証は自動的に成功し、200 が返ります。認証に失敗すれば 401 が返ります。もちろん、HTTPBasicAuth クラスをすべてのパラメータとして渡すのは煩雑なので、タプルを渡して自動的に HTTPBasicAuth を使用して認証することもできます。
以下のように簡略化できます:
import requests
r = requests.get('<http://localhost:5000>', auth=('username', 'password'))print(r.status_code)さらに、requests は OAuth 認証などの他の認証方式も提供していますが、その場合は oauth パッケージのインストールが必要です。インストール方法は以下:
pip3 install requests_oauthlib
OAuth1 認証の例は次のとおりです:
import requestsfrom requests_oauthlib import OAuth1
url = '<https://api.twitter.com/1.1/account/verify_credentials.json>'auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET', 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')requests.get(url, auth=auth)詳細な機能については、requests_oauthlib の公式ドキュメントを参照してください:https://requests-oauthlib.readthedocs.org/
- Prepared Request
これまで urllib で、リクエストをデータ構造として表現することができましたが、requests でも同様に可能です。このデータ構造は Prepared Request と呼ばれます。
from requests import Request, Session
url = '<http://httpbin.org/post>'data = {'name': 'germey'}headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'}s = Session()req = Request('POST', url, data=data, headers=headers)prepped = s.prepare_request(req)r = s.send(prepped)print(r.text)ここでは Request を導入し、url、data、headers のパラメータで Request オブジェクトを構築します。次に Session の prepare_request メソッドで Prepared Request オブジェクトへ変換し、send メソッドで送信します。
Request オブジェクトを使うことで、リクエストを独立したオブジェクトとして扱えるため、キューによるスケジューリングなどに非常に便利です。今後はこの Prepared Request を用いて Request のキューを構築します。
さらなる使い方は Requests の公式ドキュメントを参照してください:http://docs.python-requests.org/
正規表現
本節では正規表現の関連使用法を見ていきます。正規表現は文字列を扱う強力なツールで、独自の文法構造を持っています。これを使えば、文字列の検索・置換・検証などが容易にできます。
もちろん、クローラにとっては、正規表現を使って HTML から欲しい情報を抽出するのにも非常に有用です。
実例の導入
これだけ説明してきましたが、正規表現がいったい何なのかまだ分からないかもしれません。以下の実例を使って正規表現の使い方を見てみましょう。
オープンソース中国の正規表現テストツール http://tool.oschina.net/regex/ を開き、マッチさせたいテキストを入力して、一般的な正規表現を選択すると、対応するマッチ結果が得られます。例えば、以下のテキストを入力します:
Hello, my phone number is 010-86432100 and email is <[email protected]>, and my website is <https://cuiqingcai.com>.この文字列には電話番号とメールアドレスが含まれており、正規表現を使って抽出してみましょう。
メールアドレスの構成は、先頭が文字列、次に @、最後にドメイン名という特定の形を持っています。また、URL に関しては、先頭はプロトコル、次にコロン+スラッシュスラッシュ、最後にドメイン名とパスが続きます。
URL のマッチには次の正規表現を使用します:
[a-zA-z]+://[^\\s]*
この正規表現を文字列に適用すると、URL のようなテキストが含まれていれば、それを抽出します。
この正規表現は一見複雑に見えますが、実際には特定の文法規則の組み合わせです。例えば、a-z は任意の小文字文字を、\s は任意の空白文字を、* は前の文字を0回以上繰り返すことを意味します。この長い正規表現は、たくさんのマッチ規則の組み合わせです。
正規表現を作成したら、それを長い文字列に適用して検索を行えます。文字列の内容に関係なく、書いた規則に合致すればすべて見つけられます。ウェブページの場合、ウェブページのソースコード中に含まれる URL の数を見つけたい場合は、URL をマッチさせる正規表現を使用してマッチさせればよいです。
よく使われるマッチ規則
| パターン | 説明 |
|---|---|
| \w | 文字、数字、アンダースコアをマッチ |
| \W | 文字、数字、アンダースコア以外の文字をマッチ |
| \s | 任意の空白文字をマッチ、等価は [\t\n\r\f] |
| \S | 任意の非空白文字をマッチ |
| \d | 任意の数字をマッチ、等価は [0-9] |
| \D | 非数字の文字をマッチ |
| \A | 文字列の先頭にマッチ |
| \Z | 文字列の末尾にマッチ、改行がある場合は改行前のみをマッチ |
| \z | 文字列の末尾にマッチ、改行がある場合は改行もマッチ |
| \G | 最後にマッチした場所にマッチ |
| \n | 改行をマッチ |
| \t | タブをマッチ |
| ^ | 行の先頭にマッチ |
| $ | 行の末尾にマッチ |
| . | 任意の文字(改行を除く。re.DOTALL を指定すると改行を含む任意文字をマッチ) |
| […] | 文字集合を表す。例として [amk] は a、m、または k にマッチ |
| ^ | [] の外にある文字、例えば「a、b、c 以外の文字」にマッチ |
| * | 0 回以上の繰り返しをマッチ |
| + | 1 回以上の繰り返しをマッチ |
| ? | 前の正規表現の断片が 0 回または 1 回、非貪欲モード |
| {n} | 前の式をちょうど n 回マッチ |
| {n, m} | 前の正規表現の断片を n 回以上 m 回以下、貪欲にマッチ |
| a | b |
| ( ) | 括弧内の式をグループとしてマッチ |
正規表現は Python 独自のものではなく、他のプログラミング言語でも使用できます。しかし、Python の re ライブラリは正規表現の実装を提供し、Python ではこのライブラリを使って正規表現を扱います。Python で正規表現を書く際にはほぼこのライブラリを使用します。以下に、このライブラリのよく使われるメソッドをいくつか紹介します。
match
まずはよく使われるマッチングメソッドの1つ、match を紹介します。文字列と正規表現を渡すと、この正規表現が文字列にマッチするかどうかを検証します。 match は文字列の先頭からマッチを試み、成功すればマッチした結果を返します。マッチしなければ None を返します。以下は例です:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'print(len(content))result = re.match('^Hello\\s\\d\\d\\d\\s\\d{4}\\s\\w{10}', content)print(result)print(result.group())print(result.span())ここでは、英字、空白、数字などが含まれる文字列を宣言しています。次に正規表現 ^Hello\\s\\d\\d\\d\\s\\d{4}\\s\\w{10} を用いてこの長い文字列に対してマッチさせます。
先頭の ^ は文字列の先頭にマッチします。次に \s は空白文字にマッチします。\d は数字にマッチします。3 つの \d は 123 に対応します。その後 1 個の \s の空白文字にマッチします。続いて 4567 は、より煩雑ですが、4 個の \d で表現することも可能です。ここでは {4} を使用して、前の規則を4回繰り返します。最後に 1 個の空白文字を挟み、\w{10} は 10 個の文字とアンダースコアにマッチします。ここではターゲット文字列全体をマッチさせるわけではなく、マッチの一部だけを得ることになります。
-
マッチ対象
先ほどの例では、マッチした文字列の一部を取り出したい場合があります。Text の中からメールアドレスや電話番号などを取り出す場合です。括弧 () を使って抽出したい部分をグループ化します。グループのインデックスを group() に渡すと、抽出結果を得られます。
import re
content = 'Hello 1234567 World_This is a Regex Demo'result = re.match('^Hello\\s(\\d+)\\sWorld', content)print(result)print(result.group())print(result.group(1))print(result.span())この場合、文字列の中の 1234567 を取り出すために、数字部分の正規表現を () で囲んでいます。group(1) でマッチ結果を取得します。group(1) は group() と異なり、最初の括弧で囲まれた部分のみを返します。後続に () があれば group(2)、group(3) などを順次使って取得できます。
-
一般的なマッチ
先に書いた正規表現はやや複雑で、空白文字には \s、数字には \d を使うなど、作業量が多いです。正規表現を単純化する万能なマッチとして .(ドット)を使う方法があります。.(ドット)は改行を除く任意の文字を、* は直前の文字の0回以上の繰り返しを表します。これらを組み合わせると、任意の文字列をマッチさせることができます。これで、個々の文字を順番にマッチさせる必要がなくなります。
上の例を元に正規表現を次のように書き換えることができます:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'result = re.match('^Hello.*Demo$', content)print(result)print(result.group())print(result.span())この場合、中間の部分をすべて省略して、.* を使って置き換え、末尾の文字列だけを追加しました。
group() はマッチしたすべての文字列を出力します。つまり、正規表現がターゲット文字列の全体に一致していることを意味します。span() は (0, 41) となり、全体の長さを表します。したがって、.* を使って正規表現の記述を簡略化できます。
-
修飾子
正規表現には、マッチの動作を制御するオプションのフラグが含まれます。修飾子は任意のフラグとして指定されます。
result = re.match('^He.*?(\\d+).*?Demo$', content)文字列に改行が含まれる場合、実行時にエラーが発生します。つまり、正規表現はこの文字列にマッチしない、結果は None となり、続く group() の呼び出しで AttributeError が発生します。
改行を含む場合にマッチできない理由は、改行文字を除く任意の文字をマッチさせる際、. は改行を含みません。これを修正するには修飾子 re.S(DOTALL)を追加します:
import re
content = '''Hello 1234567 World_Thisis a Regex Demo'''result = re.match('^He.*?(\\d+).*?Demo$', content, re.S)print(result.group(1))この修飾子の役割は、改行を含むすべての文字をマッチさせることです。
この re.S はウェブページのマッチングにもよく使われます。HTML ノードは改行を含むことがあるため、ノード間の改行をマッチさせるのに有効です。
その他、必要に応じて使われる修飾子があります。
| 修飾子 | 説 明 |
|---|---|
| re.I | 大文字と小文字を区別しないようにします |
| re.L | locale に基づく識別を行います |
| re.M | 行単位でマッチ、^ と $ に影響 |
| re.S | ドットが改行を含むすべての文字をマッチ |
| re.U | Unicode の文字セットに従って文字を解釈します。これは \w、\W、\b、\B に影響します |
| re.X | 正規表現を見やすくするための拡張モード。より柔軟な書き方が可能 |
ウェブのマッチングでは、re.S と re.I が特に頻繁に使われます。
search
前述の match は文字列の先頭からのマッチを行います。先頭がマッチしないと、マッチは失敗します。
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'result = re.match('Hello.*?(\\d+).*?Demo', content)print(result)この文字列は Extra から始まっており、正規表現は Hello で始まるべきなので、マッチは失敗します。match は先頭の内容を考慮する必要があるため、正規表現の使用には適さない場合があります。代わって、文字列全体をスキャンして最初に成功したマッチを返す search を使用すると良いです。正規表現は文字列の一部としてもよく、search は文字列を順次スキャンして、条件に合う最初の文字列を見つけてマッチ内容を返します。もし探索しても見つからなければ None を返します。
上記の code を match から search に変更してみます:
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'result = re.search('Hello.*?(\\d+).*?Demo', content)print(result)findall
前に紹介した search の使い方は、最初のマッチ内容のみを返します。正規表現にマッチするすべての内容を取得したい場合は、findall を使います。これにより、文字列全体を検索し、正規表現にマッチするすべての内容を返します。
上記の HTML テキストでは、すべての a ノードのリンク先、アーティスト名、曲名を取得したい場合は、search を findall に置き換えます。結果がある場合はリスト型になりますので、各グループを順に取得します。コードは以下のとおりです:
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)print(results)print(type(results))for result in results: print(result) print(result[0], result[1], result[2])返ってきたリストの各要素はタプル型です。対応するインデックスを使って順番に取り出します。
最初の内容だけを取得したい場合は search を使います。複数の内容を抽出したい場合は findall を使います。
sub
正規表現を使って情報を抽出する以外にも、テキストを変更する際にも使うことができます。例えば、テキスト中のすべての数字を削除したい場合、replace だけでは煩雑になるため、sub を使います。
import re
content = '54aK54yr5oiR54ix5L2g'content = re.sub('\\d+', '', content)print(content)最初の引数にはすべての数字をマッチさせる \d+ を渡します。2番目の引数は置換後の文字列(省略すると空文字にします)、3番目は元の文字列です。
compile
前節で紹介した方法はすべて文字列を処理するためのものです。最後に compile メソッドを紹介します。これは正規表現の文字列を正規表現オブジェクトへコンパイルし、後のマッチングで再利用できるようにします。コード例は以下のとおりです:
import re
content1 = '2016-12-15 12:00'content2 = '2016-12-17 12:55'content3 = '2016-12-22 13:21'pattern = re.compile('\\d{2}:\\d{2}')result1 = re.sub(pattern, '', content1)result2 = re.sub(pattern, '', content2)result3 = re.sub(pattern, '', content3)print(result1, result2, result3)例えば、3 つの日付があり、それぞれの時刻を削除したい場合には sub を使います。第一引数は正規表現ですが、3 回同じ正規表現を繰り返し書く必要はありません。compile を用いて正規表現をオブジェクトとして再利用可能にします。
さらに、compile に修飾子(例えば re.S など)を渡すこともできます。これにより、search、findall などのメソッドで追加の引数を渡す必要がなくなります。つまり、compile は正規表現の処理をひとまとまりの形にして、再利用を容易にするということです。
クローラ基本ライブラリの使用例 - 猫眼映画ランキング
request を使って、猫眼映画 TOP100 の映画名、公開時間、評価、画像などの情報を抽出します。対象サイトの URL は http://maoyan.com/board/4 で、抽出結果はファイルとして保存されます。
ページ分析
ランキング1位の映画は「霸王别姬(巴拜別)」で、ページに表示される有効情報は映画名、主演、上映時間、上映地域、評価、画像などです。
ページを一番下までスクロールすると、ページネーション付きのリストが現れます。第2ページを直接クリックすると、URL が http://maoyan.com/board/4?offset=10 となるのを観察できます。
offset はオフセット値を表します。オフセットが n の場合、表示される映画の番号は n+1 から n+10 までで、1 ページあたり 10 件の映画情報が表示されます。
TOP100 を取得するには、10 回に分けてリクエストを送ればよく、それぞれの offset パラメータを 0、10、20…90 に設定します。異なるページを取得した後、正規表現で関連情報を抽出すれば、TOP100 全部の映画情報を得られます。
トップ頁の取得
import requests
def get_one_page(url): headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36', 'Cookie': '' }
response = requests.get(url, headers=headers) if response.status_code == 200: return response.text return None
def main(): url = '<http://maoyan.com/board/4>' html = get_one_page(url) print(html)
main()注意: 猫眼映画の反スクレイピング機構のため、リクエストヘッダに Cookie 情報を追加する必要があります。さもないと 418 エラーが返されます。
正規表現で情報を抽出
映画情報に対応するソースコードは dd ノードです。正規表現を使ってここから映画情報を抽出します。
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) print(items)このようにして、1ページの 10 件の映画情報を抽出できます。
しかし、データは雑然としているため、マッチ結果を処理して辞書として生成するなど、以下のように改良します:
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) for item in items: yield {'index': item[0], 'image': item[1], 'title': item[2].strip(), 'actor': item[3].strip()[3:] if len(item[3]) > 3 else '', 'time': item[4].strip()[5:] if len(item[4]) > 5 else '', 'score': item[5].strip() + item[6].strip()}ファイルへの書き込み
続いて、抽出結果をファイルへ書き込みます。ここではテキストファイルへ直接書き込みます。JSON ライブラリの dumps メソッドを使用して辞書をシリアライズし、ensure_ascii パラメータを False にします。これにより、出力結果が Unicode 文字列としてではなく、中国語のまま出力されます。
def write_to_file(content): with open('result.txt', 'a', encoding='utf-8') as f: print(type(json.dumps(content))) f.write(json.dumps(content, ensure_ascii=False)+'\\n')完全なコード
import jsonimport requestsfrom requests.exceptions import RequestExceptionimport reimport time
def get_one_page(url): headers = { 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Cookie':'' } try: response = requests.get(url,headers=headers) if response.status_code == 200: return response.text return None except RequestException: return None
def parse_one_page(html): pattern = re.compile('<dd>.*?board-index.*?>(\\d+)</i>.*?data-src="(.*?)".*?name"><a' + '.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>' + '.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S) items = re.findall(pattern, html) for item in items: yield { 'index': item[0], 'image': item[1], 'title': item[2], 'actor': item[3].strip()[3:], 'time': item[4].strip()[5:], 'score': item[5] + item[6] }
def write_to_file(content): with open('result.txt', 'a', encoding='utf-8') as f: f.write(json.dumps(content, ensure_ascii=False) + '\\n')
def main(offset): url = '<http://maoyan.com/board/4?offset=>' + str(offset) html = get_one_page(url) for item in parse_one_page(html): print(item) write_to_file(item)
if __name__ == '__main__': for i in range(10): main(offset=i * 10) time.sleep(1)部分信息可能已经过时









