《Python 3网络爬虫开发实战 》崔庆才著 第二章笔记

使用urllib

它是 Python内置的HTTP请求库,也就是说不需要额外安装即可使用。

包含以下4个模块:

名字说明
request它是最基本的 HTTP请求模块,可以用来模拟发送请求。就像在浏览器里输入网址然后回车一样,只需要给库方法传入URL以及额外的参数,就可以模拟实现这个过程了。
error异常处理模块,如果出现请求错误,我们可以捕获这些异常,然后进行重试或其他操作以保证程序不会意外终止。
parse一个工具模块,提供了许多URL处理方法,比如拆分、解析、合并等。
robotparser主要是用来识别网站的 robots.txt文件,然后判断哪些网站可以爬,哪些网站不可以爬,它其实用得比较少。

重点讲解下前3个模块。

发送请求

使用urllib 的request模块,我们可以方便地实现请求的发送并得到响应。

1.urlopen()

urllib.request模块提供了最基本的构造HTTP请求的方法,利用它可以模拟浏览器的一个请求发起过程,同时它还带有处理授权验证(authenticaton)、重定向( redirection)、浏览器Cookies以及其他内容。

import urllib.request
response=urllib.request.urlopen('https://www.python.org')
print(response.read().decode('utf-8'))

image-20220424111146543

查看返回的类型

import urllib.request
response=urllib.request.urlopen('https://www.python.org')
print(type(response))

image-20220424111320714

发现是HTTPResponse类型

  • 该对象包含了以下方法:

    • read():返回网页的内容。
    • readinto()
    • getheader(name)
    • getheaders()
    • fileno()
  • 以及以下属性:

    • msg
    • version
    • status:请求页面的状态码。
    • reason
    • debuglevel
    • closed

得到这个对象之后,我们把它赋值为response变量,然后就可以调用这些方法和属性,得到返回结果的一系列信息了。

urlopen API:

urllib.request.urlopen(url, data=None[timeout, ]* , cafile=None , capath=None, cadefault=False, context=None)

data参数

data参数是可选的。如果要添加该参数,并且如果它是字节流编码格式的内容,即 bytes类型,则需要通过bytes()方法转化。另外,如果传递了这个参数,则它的请求方式就不再是GET方式,而是POST方式。

import urllib.parse
import urllib.request
data=bytes(urllib.parse.urlencode({'word':'hello'}),encoding='utf-8')
response = urllib.request.urlopen('http://httpbin.org/post', data=data)
print(response.read())

这里我们传递了一个参数 word,值是 hello。它需要被转码成 bytes(字节流)类型。其中转字节流采用了bytes()方法,该方法的第一个参数需要是str(字符串)类型,需要用urllib.parse模块里的urlencode()方法来将参数字典转化为字符串;第二个参数指定编码格式,这里指定为utf8。

image-20220424125447712

timeout参数

timeout参数用于设置超时时间,单位为秒,意思就是如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,就会使用全局默认时间。它支持 HTTP、HTTPS、FTP请求。

import urllib.request
response = urllib.request.urlopen('http://httpbin.org/get', timeout=1)
print(response.read())

这里我们设置超时时间是1秒。程序1秒过后,服务器依然没有响应,于是抛出了URLError 异常该异常属于urllib.error模块,错误原因是超时。

因此,可以通过设置这个超时时间来控制一个网页如果长时间未响应,就跳过它的抓取。这可以利用try except语句来实现,相关代码如下:

import socket
import urllib.request
import urllib.error
try:
    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。

image-20220424135148652

其他参数

除了data参数和timeout参数外,还有context参数,它必须是ssl.SsLContext类型,用来指定SSL设置。

此外,cafile和 capath这两个参数分别指定CA证书和它的路径,这个在请求HTTPS链接时会有用。

cadefault参数现在已经弃用了,其默认值为False。

2.Request

我们知道利用urlopen()方法可以实现最基本请求的发起,但这几个简单的参数并不足以构建一个完整的请求。如果请求中需要加入Headers等信息,就可以利用更强大的Request类来构建。

import urllib.request
request = urllib.request.Request( 'https://python.org')
response = urllib.request.urlopen(request)
print(response.read().decode( 'utf-8 '))

下面我们看一下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/2.0.0.11
    
  • 第四个参数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.o (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'))

image-20220424141221873

headers也可以用add_header()方法来添加:

req = request.Request(url=url, data=data,method='POST')
req.add_header( 'User-Agent''Mozilla/4.o (compatible; MSIE 5.5; Windows NT)')

3.高级用法

在上面的过程中,我们虽然可以构造请求,但是对于一些更高级的操作(比如 Cookies处理、代理设置等),就需要更强大的工具 Handler 登场了。简而言之,我们可以把它理解为各种处理器,有专门处理登录验证的,有处理Cookies的,有处理代理设置的。利用它们,我们几乎可以做到 HTTP请求中所有的事情。

首先,介绍一下urllib.request模块里的BaseHandler类,它是所有其他Handler的父类,它提供了最基本的方法,例如 default_open()、 protocol_request()等。有各种 Handler子类继承这个BaseHandler类,举例如下。

说明
HTTPDefaultErrorHandler用于处理HTTP响应错误,错误都会抛出 HTTPError类型的异常。
HTTPRedirectHandler用于处理重定向。
HTTPCookieProcessor用于处理Cookies。
ProxyHandler用于设置代理,默认代理为空。
HTTPPasswordMgr用于管理密码,它维护了用户名和密码的表。
HTTPBasicAuthHandler用于管理认证,如果一个链接打开时需要认证,那么可以用它来解决认证问题。

另一个比较重要的类就是OpenerDirector,我们可以称为Opener。Opener可以使用open()方法,返回的类型和urlopen()如出一辙。它就是利用Handler来构建Opener。下面用几个实例来看看它们的用法。

  • 验证

    有些网站在打开时就会弹出提示框,直接提示你输入用户名和密码,验证成功后才能查看页面。

    image-20220424143330346

    借助HTTPBasicAuthHandler就可以完成

    from urllib.request import HTTPPasswordMgrWithDefaultRealm,HTTPBasicAuthHandler,build_opener
    from 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,这个Opener在发送请求时就相当于已经验证成功了。

    接下来,利用Opener的open()方法打开链接,就可以完成验证了。这里获取到的结果就是验证后的页面源码内容。

  • 代理

    在做爬虫的时候,免不了要使用代理,如果要添加代理,可以这样做:

    from urllib.error import URLError
    from 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 了。我们先用实例来看看怎样将网站的Cookies获取下来,相关代码如下:

    import http.cookiejar,urllib.request
    
    cookie = http.cookiejar.CookieJar()
    handler = urllib.request.HTTPCookieProcessor(cookie)
    opener = urllib.request.build_opener(handler)
    response = opener.open( ' http://wwwl.baidu.com')
    
    for item in cookie:
        print(item.name+"="+item.value)
    

    首先,我们必须声明一个CookieJar对象。接下来,就需要利用HTTPCookieProcessor来构建一个Handler,最后利用build_opener()方法构建出Opener,执行open()函数即可。运行结果如下:

    image-20220424145459174

    我们还可以输出为文件格式:

    import http.cookiejar,urllib.request
    
    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格式。运行之后,可以发现生成了一个cookies.txt文件.其内容如下:

    image-20220424150139946

    另外,LWPCookieJar同样可以读取和保存Cookies,但是保存的格式和MozillaCookieJar不一样,它会保存成libwww-perl(LWP)格式的Cookies文件。要保存成LWP格式的Cookies文件,可以在声明时就改为:

    cookie = http.cookiejar.LWPCookieJar(filename)
    

    image-20220424150328085

    可以发现他们之间的差别有点大。

    下面我们以LWPCookieJar格式来看看如何使用cookie文件

    import http.cookiejar,urllib.request
    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的error模块定义了由request模块产生的异常。如果出现了问题,request模块便会抛出error模块中定义的异常。

1.URLError

URLError类来自urllib库的error模块,它继承自OSError类,是error异常模块的基类,由request模块生的异常都可以通过捕获这个类来处理。

它具有一个属性reason,即返回错误的原因。

from urllib import request,error
try:
    response=request.urlopen('https://cuiqingcai.com/index.htm')
except error.URLError as e:
    print(e.reason)

image-20220425141100653

程序没有直接报错,而是输出了如上内容,这样通过如上操作,我们就可以避免程序异常终止,同时异常得到了有效处理。

2.HTTPError

它是URLError的子类,专门用来处理IITTP请求错误,比如认证请求失败等。它有如下3个属性。

名字说明
code返回HTTP状态码,比如404表示网页不存在,500表示服务器内部错误等。
reason同父类一样,用于返回错误的原因。
headers返回请求头。
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')

image-20220425141814974

依然是同样的网址,这里捕获了HTTPError异常,输出了reason、code和 headers属性。

因为URLError是 HTTPError的父类,所以可以先选择捕获子类的错误,再去捕获父类的错误

有时候,reason属性返回的不一定是字符串,也可能是一个对象。

import socket
import urllib.request
import 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')

image-20220425142459543

可以发现,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。

1.urlparse()

该方法可以实现 URL 的识别和分段

from urllib.parse import urlparse

result = urlparse('https://www.baidu.com/index.html;user?id=5#comment')
print(type(result))
print(result)

这里我们利用urlparse()方法进行了一个URL 的解析。首先,输出了解析结果的类型,然后将结果也输出出来。

image-20220425143756982

可以看到,返回结果是一个ParseResult类型的对象,它包含6个部分,分别是scheme,netloc,path,params,query和 fragment。一般的URL标准格式如下:

scheme://netloc/path;params?query#fragment
名字说明输出
scheme😕/前面的就是scheme,代表协议https
netloc第一个/符号前面就是netloc,即域名www.baidu.com
pathnetloc后面就是path,即访问路径/index.html
params分号前面是params,代表参数user
query参数后面是查询条件query,一般作GET类型的URIid=5
fragment井号#后面是锚点,用于直接定位页面内部的下拉位置comment

urlparse参数如下:

urlparse(url=,scheme=,allow_fragments=)
  • url:这是必填项,即待解析的URL。
  • schema:这是默认的协议(HTTP、HTTPS等),默认为HTTPS。
  • allow_fragments:是否忽略fragment,默认为True,即不忽视fragment。当URL中不包含params和 query时,fragment便会被解析为path的一部分。

urlparse的返回结果是一个元组,我们可以用索引顺序来获取,也可以用属性名获取。

from urllib.parse import urlparse

result = urlparse('https://www.baidu.com/index.html;user?id=5#comment')
print(result[0])
print(result.scheme)

image-20220425145442398

2.urlunparse()

有了urlparse(),相应地就有了它的对立方法urlunparse()。它接受的参数是一个可迭代对象,但是它的长度必须是6,否则会抛出参数数量不足或者过多的问题。先用一个实例看一下:

from urllib.parse import urlunparse

data=['http','www.baidu.com','index.html','user','a=6','comment']
print(urlunparse(data))

这里参数data用了列表类型。当然,你也可以用其他类型,比如元组或者特定的数据结构。

image-20220425145829112

观察结果可以发现,我们成功构造了url。

3.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)

image-20220425150145993

可以发现,返回结果是SplitResult,它其实也是一个元组类型,既可以用属性获取值,也可以用索引来获取。

4.urlunsplit()

与urlunparse()类似,它也是将链接各个部分组合成完整链接的方法,传入的参数也是一个可迭代对象,例如列表、元组等,唯一的区别是长度必须为5。示例如下:

from urllib.parse import urlunparse

data=['http','www.baidu.com','index.html','user','a=6','comment']
print(urlunparse(data))

image-20220425150430210

5.urljoin()

生成链接还有另一个方法,那就是urljoin()方法。我们可以提供一个base_url(基础链接)作为第一个参数,将新的链接作为第二个参数,该方法会分析base_url的 scheme、netloc和path这3个内容并对新链接缺失的部分进行补充,最后返回结果。

from urllib.parse import urljoin

print(urljoin('https://www.baidu.com', 'FAQ.html'))
print(urljoin('https://www.baidu.com', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html?question=2'))
print(urljoin('https://www.baidu.com?wd=abc', 'https://cuiqingcai.com/index.php'))
print(urljoin('https://www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com#comment', '?category=2'))

image-20220425151041314

可以发现,base_url 提供了三项内容 scheme、netloc 和 path。如果这 3 项在新的链接里不存在,就予以补充;如果新的链接存在,就使用新的链接的部分。而 base_url 中的 params、query 和 fragment 是不起作用的。

通过 urljoin 方法,我们可以轻松实现链接的解析、拼合与生成。

6.urlencode()

这个方法非常常用。有时为了更加方便地构造参数,我们会事先用字典来表示。要转化为URL的参数时,只需要调用该方法即可。

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请求参数。

image-20220425151458844

可以看到,参数就成功地由字典类型转化为GET请求参数了。

7.parse_qs()

有了序列化,必然就有反序列化。如果我们有一串 GET 请求参数,利用 parse_qs()方法,就可以将它转回字典,示例如下:

from urllib.parse import parse_qs

query = 'name=germey&age=25'
print(parse_qs(query))

image-20220425151644111

可以看到,这样就成功转回为字典类型了。

8.parse_sql()

还有一个 parse_qsl 方法,它用于将参数转化为元组组成的列表,示例如下:

from urllib.parse import parse_qsl

query = 'name=germey&age=25'
print(parse_qsl(query))

image-20220425151827414

可以看到,运行结果是一个列表,而列表中的每一个元素都是一个元组,元组的第一个内容是参数名,第二个内容是参数值。

9.quote()

该方法可以将内容转化为 URL 编码的格式。URL 中带有中文参数时,有时可能会导致乱码的问题,此时可以用这个方法可以将中文字符转化为 URL 编码,示例如下:

from urllib.parse import quote

keyword = '壁纸'
url = 'https://www.baidu.com/s?wd=' + quote(keyword)
print(url)

image-20220425152101357

10.unquote()

它可以进行 URL 解码,示例如下:

from urllib.parse import unquote

url = 'https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8'
print(unquote(url))

image-20220425152219546

本节中,我们介绍了 parse 模块的一些常用 URL 处理方法。有了这些方法,我们可以方便地实现 URL 的解析和构造,建议熟练掌握。

分析Robots协议

利用 urllib 的 robotparser 模块,我们可以实现网站 Robots 协议的分析。本节中,我们来简单了解一下该模块的用法。

1.Robots协议

Robots 协议也称作爬虫协议、机器人协议,它的全名叫作网络爬虫排除标准(Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以抓取,哪些不可以抓取。它通常是一个叫作 robots.txt 的文本文件,一般放在网站的根目录下。

当搜索爬虫访问一个站点时,它首先会检查这个站点根目录下是否存在 robots.txt 文件,如果存在,搜索爬虫会根据其中定义的爬取范围来爬取。如果没有找到这个文件,搜索爬虫便会访问所有可直接访问的页面。

下面我们看一个 robots.txt 的样例:

User-agent: *	<!-- 该协议对任何爬取爬虫有效 -->
Disallow: /		<!-- 不允许抓取所有页面。 -->
Allow: /public/	<!-- 可以抓取 public 目录 -->

这实现了对所有搜索爬虫只允许爬取 public 目录的功能,将上述内容保存成 robots.txt 文件,放在网站的根目录下,和网站的入口文件(比如 index.php、index.html 和 index.jsp 等)放在一起。

2.爬虫名称

一些常见搜索爬虫的名称及其对应的网站。

爬虫名称名称网站
BaiduSpider百度www.baidu.com
Googlebot谷歌www.google.com
360Spider360 搜索www.so.com
YodaoBot有道www.youdao.com
ia_archiverAlexawww.alexa.cn
Scooteraltavistawww.altavista.com
Bingbot必应www.bing.com

3.robotparser

我们就可以使用 robotparser 模块来解析 robots.txt 了。该模块提供了一个类 RobotFileParser,它可以根据某网站的 robots.txt 文件来判断一个爬虫是否有权限来爬取这个网页。

该类用起来非常简单,只需要在构造方法里传入 robots.txt 的链接即可。也可以在声明时不传入,默认为空,最后再使用 set_url 方法设置一下即可。

urllib.robotparser.RobotFileParser(url='')

这个类常用的几个方法:

  • set_url():用来设置 robots.txt 文件的链接。如果在创建 RobotFileParser 对象时传入了链接,那么就不需要再使用这个方法设置了。
  • read():读取 robots.txt 文件并进行分析。注意,这个方法执行一个读取和分析操作,如果不调用这个方法,接下来的判断都会为 False,所以一定记得调用这个方法。这个方法不会返回任何内容,但是执行了读取操作。
  • parse():用来解析 robots.txt 文件,传入的参数是 robots.txt 某些行的内容,它会按照 robots.txt 的语法规则来分析这些内容。
  • can_fetch():该方法用两个参数,第一个是 User-Agent,第二个是要抓取的 URL。返回的内容是该搜索引擎是否可以抓取这个 URL,返回结果是 TrueFalse
  • mtime():返回的是上次抓取和分析 robots.txt 的时间,这对于长时间分析和抓取的搜索爬虫是很有必要的,你可能需要定期检查来抓取最新的 robots.txt。
  • modified():它同样对长时间分析和抓取的搜索爬虫很有帮助,将当前时间设置为上次抓取和分析 robots.txt 的时间。

接下来我们查看例子:

from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
# rp = RobotFileParser('https://www.baidu.com/robots.txt')
rp.set_url('https://www.baidu.com/robots.txt')

rp.read()
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/'))
print(rp.can_fetch('Googlebot', 'https://www.baidu.com/homepage/'))

image-20220425194628421

使用requests

使用 urllib 处理网页验证和 Cookie 时,需要写 Opener 和 Handler 来处理。另外我们要实现 POST、PUT 等请求时写法也不方便。

为了更加方便地实现这些操作,就有了更为强大的库 requests,有了它,Cookie、登录验证、代理设置等操作都不是事儿。

基本用法

1.示例

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)

image-20220426141347307

除了get方法外,还有其他的请求方法:

import requests

r = requests.get('https://httpbin.org/get')
r = requests.post('https://httpbin.org/post')
r = requests.put('https://httpbin.org/put')
r = requests.delete('https://httpbin.org/delete')
r = requests.patch('https://httpbin.org/patch')
2.GET请求
GET请求如何携带参数?

一种方法是,我们可以直接在url中附加即可,如:

r=requests.get('http://httpbin.org/get?name=germey&age=20')

还可以使用requests的params这个参数,如:

import requests

data = {
    'name': 'germey',
    'age': 25
}
r = requests.get('https://httpbin.org/get', params=data)
print(r.text)

网页的返回类型实际上是 str 类型,但是它很特殊,是 JSON 格式的。所以,如果想直接解析返回结果,得到一个 JSON 格式的数据的话,可以直接调用 json 方法。如:

import requests

r = requests.get('https://httpbin.org/get')
print(type(r.text))
print(r.json())
print(type(r.json()))

image-20220426142515627

可以发现,调用 json 方法,就可以将返回结果是 JSON 格式的字符串转化为字典。

但需要注意的是,如果返回结果不是 JSON 格式,便会出现解析错误,抛出 json.decoder.JSONDecodeError 异常。

抓取网页
import requests
import re

r = requests.get('https://ssr1.scrape.center/')
pattern = re.compile('<h2.*?>(.*?)</h2>', re.S)
titles = re.findall(pattern, r.text)
print(titles)

image-20220426143043274

抓取二进制数据

在上面的例子中,我们抓取的是网站的一个页面,实际上它返回的是一个 HTML 文档。

图片、音频、视频这些文件本质上都是由二进制码组成的,由于有特定的保存格式和对应的解析方式,我们才可以看到这些形形色色的多媒体。所以,想要抓取它们,就要拿到它们的二进制数据。

如果直接使用抓取页面的方法抓取二进制文件,会出现乱码的情况。

import requests

r=requests.get("https://www.baidu.com/favicon.ico")
with open('favicon.ico','wb')as f:
    f.write(r.content)

这里用了open()方法,它的第一个参数是文件名称,第二个参数代表以二进制写的形式打开,可以向文件里写入二进制数据。

运行结束之后,可以发现在文件夹中出现了名为favicon.ico 的图标。

image-20220426143741107

同样,其他二进制文件也可以这样子来进行爬取。

添加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.baidu.com/', headers=headers)
print(r.text)
3.POST请求

实例如下:

import requests

data={'name': 'germey','age': '22'}
r=requests.post("http://httpbin.org/post",data=data)
print(r.text)

image-20220426144511144

4.响应

发送请求后,得到的自然就是响应。除了前面使用到的text和content之外,还有状态码、响应头、Cookies等。

import requests

r = requests.get('https://ssr1.scrape.center/')
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 属性得到 Cookie,输出 url 属性得到 URL,输出 history 属性得到请求历史。

image-20220426144831882

高级用法

1.文件上传
import requests
 
files={'file': open('favicon.ico','rb')}
r=requests.post("http://httpbin.org/post",files=files)
print(r.text)

image-20220426145722748

2.Cookies
import requests

r=requests.get("https://www.baidu.com")
print(r.cookies)
for key,value in r.cookies.items():
    print(key+'='+value)

image-20220426145937075

这里我们首先调用 cookies 属性即可成功得到 Cookie,可以发现它是 RequestCookieJar 类型。然后用 items 方法将其转化为元组组成的列表,遍历输出每一个 Cookie 条目的名称和值,实现 Cookie 的遍历解析。

我们还可以往headers里面添加Cookie,实现登录。

import requests

headers = {
    'Cookie': '自己的Cookie值',
    '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://github.com/', headers=headers)
print(r.text)
3.会话维持

在 requests 中,如果直接利用 get 或 post 等方法的确可以做到模拟网页的请求,但是这实际上是相当于不同的 Session,也就是说相当于你用了两个浏览器打开了不同的页面。

其实解决这个问题的主要方法就是维持同一个 Session,也就是相当于打开一个新的浏览器选项卡而不是新开一个浏览器。但是我又不想每次设置 Cookies,那该怎么办呢?这时候就有了新的利器 Session 对象。Session 在平常用得非常广泛,可以用于模拟在一个浏览器中打开同一站点的不同页面。

import requests

s = requests.Session()
s.get('https://httpbin.org/cookies/set/number/123456789')
r = s.get('https://httpbin.org/cookies')
print(r.text)

image-20220426155959376

利用 Session,可以做到模拟同一个会话而不用担心 Cookie 的问题。它通常用于模拟登录成功之后再进行下一步的操作。

4.SSL证书验证

现在很多网站都要求使用 HTTPS 协议,但是有些网站可能并没有设置好 HTTPS 证书,或者网站的 HTTPS 证书可能并不被 CA 机构认可,这时候,这些网站可能就会出现 SSL 证书错误的提示。

如果我们一定要爬取这个网站怎么办呢?我们可以使用 verify 参数控制是否验证证书,如果将其设置为 False,在请求时就不会再验证证书是否有效。如果不加 verify 参数的话,默认值是 True,会自动验证。因此我们将verify参数设为False即可。

import requests

response = requests.get('https://ssr2.scrape.center/', verify=False)
print(response.status_code)

image-20220426160553518

但是报了一个警告,它建议我们给它指定证书。我们可以对警告进行忽略。

import requests
from requests.packages import urllib3

urllib3.disable_warnings()
response = requests.get('https://ssr2.scrape.center/', verify=False)
print(response.status_code)

image-20220426160817590

我们还可以导入证书进行访问,注意,私有证书的key必须是解密状态,加密状态的key是不支持的。

import requests

response=requests.get('https://www.12306.cn',cert=('/path/server.crt','/path/key'))
print(response.status_code)
5.代理设置

一旦开始大规模爬取,对于大规模且频繁的请求,网站可能会弹出验证码,或者跳转到登录认证页面,更甚者可能会直接封禁客户端的 IP,导致一定时间段内无法访问。为了防止这种情况,我们需要设置代理来解决这个问题,这就需要用到 proxies 参数。

import requests

proxies={
    "http": "http://10.10.1.10.:3128",
    "https": "https://10.10.1.10:1080",
}

requests.get("https://www.taobao.com",proxies=proxies)

除了基本的HTTP代理意外,还有SOCKS协议代理。

import requests

proxies = {
    'http': 'socks5://user:password@host:port',
    'https': 'socks5://user:password@host:port'
}
requests.get('https://httpbin.org/get', proxies=proxies)
6.超时设置

为了防止服务器不能及时响应,应该设置一个超时时间,即超过了这个时间还没有得到响应,那就报错。这需要用到 timeout 参数。这个时间的计算是发出请求到服务器返回响应的时间。

import requests

r = requests.get('https://httpbin.org/get', timeout=1)
print(r.status_code)
7.身份认证

image-20210704202140395

这个网站就是启用了基本身份认证,在上一节中我们可以利用 urllib 来实现身份的校验,但实现起来相对繁琐。

我们可以使用 requests 自带的身份认证功能,通过 auth 参数即可设置。这个示例网站的用户名和密码都是 admin,在这里我们可以直接设置。

import requests
from requests.auth import HTTPBasicAuth

r = requests.get('https://ssr3.scrape.center/', auth=HTTPBasicAuth('admin', 'admin'))
print(r.status_code)

image-20220426163823335

如果参数都传一个 HTTPBasicAuth 类,就显得有点烦琐了,所以 requests 提供了一个更简单的写法,可以直接传一个元组,它会默认使用 HTTPBasicAuth 这个类来认证。

import requests

r = requests.get('https://ssr3.scrape.center/', auth=('admin', 'admin'))
print(r.status_code)

image-20220426164012097

8.Prepared Request

实际上,requests 在发送请求的时候,是在内部构造了一个 Request 对象,并给这个对象赋予了各种参数,包括 url、headers、data 等等,然后直接把这个 Request 对象发送出去,请求成功后会再得到一个 Response 对象,再解析即可。Response 对象实际上就是 Prepared Request。

from requests import Request, Session

url = 'https://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 方法发送。

image-20220426165008075

正则表达式

基础

对于 URL 来说,可以用下面的正则表达式匹配:

[a-zA-z]+://[^\s]*

这个正则表达式去匹配一个字符串,如果这个字符串中包含类似 URL 的文本,就会被提取出来。

比如,a-z 代表匹配任意的小写字母,\s 表示匹配任意的空白字符,* 就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。

常见的匹配规则

模  式描  述
\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匹配a或b
()匹配括号内的表达式,也表示一个组

match()

match 方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果;如果不匹配,就返回 None。示例如下:

import re

content = 'Hello 123 4567 World_This is a Regex Demo'
print(content)
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())

image-20220430140655754

开头的 ^ 是匹配字符串的开头,也就是以 Hello 开头;然后 \s 匹配空白字符,用来匹配目标字符串的空格;\d 匹配数字,3 个 \d 匹配 123;然后再写 1 个 \s 匹配空格;后面还有 4567,我们其实可以依然用 4 个 \d 来匹配,但是这么写比较烦琐,所以后面可以跟 {4} 以代表匹配前面的规则 4 次,也就是匹配 4 个数字;后面再紧接 1 个空白字符,最后的 \w{10} 匹配 10 个字母及下划线。

match 方法中,第一个参数传入了正则表达式,第二个参数传入了要匹配的字符串。

打印输出结果,可以看到结果是 SRE_Match 对象,这证明成功匹配。该对象有两个方法:

  • group 方法可以输出匹配到的内容,结果是 Hello 123 4567 World_This,这恰好是正则表达式规则所匹配的内容。
  • span 方法可以输出匹配的范围,结果是 (0, 25),这就是匹配到的结果字符串在原字符串中的位置范围。
1.匹配目标

如果想从字符串中提取一部分内容,该怎么办呢?就像最前面的实例一样,从一段文本中提取出邮件或电话号码等内容。

可以使用括号 () 将想提取的子字符串括起来。() 实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用 group 方法传入分组的索引即可获取提取的结果。示例如下:

import re

content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\sWorld', content)
print(content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())

这里我们想把字符串中的 1234567 提取出来,此时可以将数字部分的正则表达式用 () 括起来,然后调用了 group(1) 获取匹配结果。

image-20220430143236743

我们成功得到了 1234567。这里用的是 group(1),它与 group() 有所不同,后者会输出完整的匹配结果,而前者会输出第一个被 () 包围的匹配结果。假如正则表达式后面还有 () 包括的内容,那么可以依次用 group(2)group(3) 等来获取。

2.通用匹配

前面我们写的正则表达式其实比较复杂,出现空白字符我们就写 \s 匹配,出现数字我们就用 \d 匹配,这样的工作量非常大。其实完全没必要这么做,因为还有一个万能匹配可以用,那就是 .*。其中 . 可以匹配任意字符(除换行符),* 代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意字符了。有了它,我们就不用挨个字符匹配了。

import re

content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello.*Demo$', content)
print(content)
print(result)
print(result.group())
print(result.span())

image-20220430143805808

因此,我们可以使用 .* 简化正则表达式的书写。

3.贪婪与非贪婪

使用上面的通用匹配 .* 时,可能有时候匹配到的并不是我们想要的结果。看下面的例子:

import re

content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content)
print(content)
print(result)
print(result.group(1))

我们依然想获取中间的数字,所以中间依然写的是 (\d+)。而数字两侧由于内容比较杂乱,所以想省略来写,都写成 .*。最后,组成 ^He.*(\d+).*Demo$,看样子并没有什么问题。我们看下运行结果:

image-20220430144751866

可以发现最终匹配到的结果是一个数字7。这里就涉及贪婪匹配与非贪婪匹配的问题。

在贪婪匹配下,.* 会匹配尽可能多的字符。正则表达式中 .* 后面是 \d+,也就是至少一个数字,并没有指定具体多少个数字,因此,.* 就尽可能匹配多的字符,这里就把 123456 匹配了,给 \d+ 留下一个可满足条件的数字7,最后得到的内容就只有数字7了。

这里只需要使用非贪婪匹配就好了。非贪婪匹配的写法是 .*?,多了一个 ?,那么它可以达到怎样的效果?我们再用实例看一下:

import re

content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*?(\d+).*Demo$', content)
print(content)
print(result)
print(result.group(1))

image-20220430145019468

此时就可以成功获取 1234567 了。贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符。当 .*? 匹配到 Hello 后面的空白字符时,再往后的字符就是数字了,而 \d+ 恰好可以匹配,那么这里 .*? 就不再进行匹配,交给 \d+ 去匹配后面的数字。所以这样 .*? 匹配了尽可能少的字符,\d+ 的结果就是 1234567 了。

因此,在做匹配的时候,字符串中间尽量使用非贪婪匹配,也就是用 .*? 来代替 .*,以免出现匹配结果缺失的情况。

import re

content = 'http://weibo.com/comment/kEraCN'
result1 = re.match('http.*?comment/(.*?)', content)
result2 = re.match('http.*?comment/(.*)', content)
print(content)
print('result1', result1.group(1))
print('result2', result2.group(1))

image-20220430150524081

可以观察到,.*? 没有匹配到任何结果,而 .* 则尽量匹配多的内容,成功得到了匹配结果。

4.修饰符

.*是用来匹配除换行符之外的任意符号,因此在遇到换行符的时候会报错,因此我们需要添加修饰符re.S进行修正,如:

result = re.match('^He.*?(\d+).*?Demo$', content, re.S)

这个 re.S 在网页匹配中经常用到。因为 HTML 节点经常会有换行,加上它,就可以匹配节点与节点之间的换行了。

修饰符及其描述:

修饰符描  述
re.I使匹配对大小写不敏感
re.L做本地化识别(locale-aware)匹配
re.M多行匹配,影响 ^$
re.S使。匹配包括换行符在内的所有字符
re.U根据 Unicode 字符集解析字符。这个标志影响 \w\W\b\B
re.X该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

在网页匹配中,较为常用的有 re.Sre.I

5.转义匹配

我们知道正则表达式定义了许多匹配模式,如 . 匹配除换行符以外的任意字符,但是如果目标字符串里面就包含 .,那该怎么办呢?

这里就需要用到转义匹配了,示例如下:

import re

content = '(百度) www.baidu.com'
result = re.match('\(百度\) www\.baidu\.com', content)
print(result)

当遇到用于正则匹配模式的特殊字符时,在前面加反斜线转义一下即可。例如可以用 \. 来匹配 .,运行结果如下:

image-20220430151353945

可以发现成功匹配到了原字符串。

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 开头,整个正则表达式是字符串的一部分,但是这样匹配是失败的。运行结果如下:

image-20220430151903347

因为 match 方法在使用时需要考虑到开头的内容,这在做匹配时并不方便。它更适合用来检测某个字符串是否符合某个正则表达式的规则。

这里就有另外一个方法 search,它在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果。也就是说,正则表达式可以是字符串的一部分,在匹配时,search 方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,就返回 None

import re

content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'
result = re.search('Hello.*?(\d+).*?Demo', content)
print(result)

image-20220430152019401

下面再用几个实例来看看 search 方法的用法。

这里有一段待匹配的 HTML 文本,接下来写几个正则表达式实例来实现相应信息的提取:

<div id="songs-list">
	<h2 class="title">经典老歌</h2>
	<p class="introduction">经典老歌列表</p>
	<ul id="list" class="list-group">
		<li data-view="2">一路上有你</li>
		<li data-view="7"><a href="/2.mp3" singer="任贤齐">沧海一声笑</a></li>
		<li data-view="4" class="active"><a href="/3.mp3" singer="齐秦">往事随风</a></li>
		<li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
		<li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
		<li data-view="5"><a href="/6.mp3" singer="邓丽君">但愿人长久</a></li>
	</ul>
</div>

首先,我们尝试提取 classactiveli 节点内部的超链接包含的歌手名和歌名,此时需要提取第三个 li 节点下 a 节点的 singer 属性和文本。

此时正则表达式可以以 li 开头,然后寻找一个标志符 active,中间的部分可以用 .*? 来匹配。接下来,要提取 singer 这个属性值,所以还需要写入 singer="(.*?)",这里需要提取的部分用小括号括起来,以便用 group 方法提取出来,它的两侧边界是双引号。然后还需要匹配 a 节点的文本,其中它的左边界是 >,右边界是 </a>。然后目标内容依然用 (.*?) 来匹配,所以最后的正则表达式就变成了:

<li.*?active.*?singer="(.*?)">(.*?)</a>

然后再调用 search 方法,它会搜索整个 HTML 文本,找到符合正则表达式的第一个内容返回。另外,由于代码有换行,所以这里第三个参数需要传入 re.S。整个匹配代码如下:

import re
html = '''
<div id="songs-list">
   <h2 class="title">经典老歌</h2>
   <p class="introduction">经典老歌列表</p>
   <ul id="list" class="list-group">
      <li data-view="2">一路上有你</li>
      <li data-view="7"><a href="/2.mp3" singer="任贤齐">沧海一声笑</a></li>
      <li data-view="4" class="active"><a href="/3.mp3" singer="齐秦">往事随风</a></li>
      <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
      <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
      <li data-view="5"><a href="/6.mp3" singer="邓丽君">但愿人长久</a></li>
   </ul>
</div>
'''
result = re.search('<li.*?active.*?singer="(.*?)">(.*?)</a>', html, re.S)
if result:
    print(result.group(1), result.group(2))

由于需要获取的歌手和歌名都已经用小括号包围,所以可以用 group 方法获取。

image-20220430153102305

如果正则表达式不加 active(也就是匹配不带 classactive 的节点内容),那会怎样呢?我们将正则表达式中的 active 去掉,代码改写如下:

import re
html = '''
<div id="songs-list">
   <h2 class="title">经典老歌</h2>
   <p class="introduction">经典老歌列表</p>
   <ul id="list" class="list-group">
      <li data-view="2">一路上有你</li>
      <li data-view="7"><a href="/2.mp3" singer="任贤齐">沧海一声笑</a></li>
      <li data-view="4" class="active"><a href="/3.mp3" singer="齐秦">往事随风</a></li>
      <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
      <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
      <li data-view="5"><a href="/6.mp3" singer="邓丽君">但愿人长久</a></li>
   </ul>
</div>
'''
result = re.search('<li.*?singer="(.*?)">(.*?)</a>', html, re.S)
if result:
    print(result.group(1), result.group(2))

由于 search 方法会返回第一个符合条件的匹配目标,这里结果就变了:

image-20220430153213615

active 标签去掉后,从字符串开头开始搜索,此时符合条件的节点就变成了第二个 li 节点,后面的就不再匹配,所以运行结果就变成第二个 li 节点中的内容了。

由于绝大部分的 HTML 文本都包含了换行符,所以尽量都需要加上 re.S 修饰符,以免出现匹配不到的问题。

findall()

前面我们介绍了 search 方法的用法,它可以返回匹配正则表达式的第一个内容,但是如果想要获取匹配正则表达式的所有内容,这时就要借助 findall 方法了。该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容。

import re
html = '''
<div id="songs-list">
   <h2 class="title">经典老歌</h2>
   <p class="introduction">经典老歌列表</p>
   <ul id="list" class="list-group">
      <li data-view="2">一路上有你</li>
      <li data-view="7"><a href="/2.mp3" singer="任贤齐">沧海一声笑</a></li>
      <li data-view="4" class="active"><a href="/3.mp3" singer="齐秦">往事随风</a></li>
      <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
      <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
      <li data-view="5"><a href="/6.mp3" singer="邓丽君">但愿人长久</a></li>
   </ul>
</div>
'''
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])

image-20220430153602385

可以看到,返回的列表中的每个元素都是元组类型,我们用对应的索引依次取出即可。如果只是获取第一个内容,可以用 search 方法。当需要提取多个内容时,可以用 findall 方法。

sub()

想要把一串文本中的所有数字都去掉,如果只用字符串的 replace 方法,那就太烦琐了,这时可以借助 sub 方法。示例如下:

import re

content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+', '', content)
print(content)

这里只需要给第一个参数传入 \d+ 来匹配所有的数字,第二个参数为替换成的字符串(如果去掉该参数的话,可以赋值为空),第三个参数是原字符串。

image-20220430154054933

在上面的 HTML 文本中,如果想获取所有 li 节点的歌名,直接用正则表达式来提取可能比较烦琐。比如,可以写成这样子:

import re
html = '''
<div id="songs-list">
   <h2 class="title">经典老歌</h2>
   <p class="introduction">经典老歌列表</p>
   <ul id="list" class="list-group">
      <li data-view="2">一路上有你</li>
      <li data-view="7"><a href="/2.mp3" singer="任贤齐">沧海一声笑</a></li>
      <li data-view="4" class="active"><a href="/3.mp3" singer="齐秦">往事随风</a></li>
      <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
      <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
      <li data-view="5"><a href="/6.mp3" singer="邓丽君">但愿人长久</a></li>
   </ul>
</div>
'''
results = re.findall('<li.*?>\s*?(<a.*?>)?(\w+)(</a>)?\s*?</li>', html, re.S)
for result in results:
    print(result[1])

image-20220430154253793

此时借助 sub 方法就比较简单了。可以先用 sub 方法将 a 节点去掉,只留下文本,然后再利用 findall 提取就好了:

import re
html = '''
<div id="songs-list">
   <h2 class="title">经典老歌</h2>
   <p class="introduction">经典老歌列表</p>
   <ul id="list" class="list-group">
      <li data-view="2">一路上有你</li>
      <li data-view="7"><a href="/2.mp3" singer="任贤齐">沧海一声笑</a></li>
      <li data-view="4" class="active"><a href="/3.mp3" singer="齐秦">往事随风</a></li>
      <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
      <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
      <li data-view="5"><a href="/6.mp3" singer="邓丽君">但愿人长久</a></li>
   </ul>
</div>
'''
html = re.sub('<a.*?>|</a>', '', html)
print(html)
results = re.findall('<li.*?>(.*?)</li>', html, re.S)
for result in results:
    print(result.strip())

image-20220430154427362

可以看到,a 节点经过 sub 方法处理后就没有了,然后再通过 findall 方法直接提取即可。可以看到,在适当的时候,借助 sub 方法可以起到事半功倍的效果。

compile()

这个方法可以将正则字符串编译成正则表达式对象,以便在后面的匹配中复用。示例代码如下:

import re

content1 = '2019-12-15 12:00'
content2 = '2019-12-17 12:55'
content3 = '2019-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)

image-20220430155730098

另外,compile 还可以传入修饰符,例如 re.S 等修饰符,这样在 searchfindall 等方法中就不需要额外传了。所以,compile 方法可以说是给正则表达式做了一层封装,以便我们更好地复用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值