python爬虫 --- urllib

urllib包

urllib是python3中的一个标准库,它是一个工具包模块,使用该模块我们可以模仿浏览器向服务器发送请求,并获得返回结果数据进行处理。

urllib包分级简单,在该包下5个模块

  • urllib.request :用于打开和读写url
  • urllib.response:将获取后的内容进行封装为一个类文件对象,方便使用
  • urllib.error包含了由 urllibreques引起的异常
  • urllib.parse用于解析url
  • urllib.robotparser分析 robots. txt文件

Python2中提供了urlb和urlib2两个库,urllib提供较为底层的接口, urllib2对 urllib进行了进一步封装。

Python3中将urllib2合并到了urllib中,并更名为标准库urllib包。

urllib. request模块

模块定义了在 基本和摘要式身份验证、重定向、cookies等应用中打开url(主要是HTTP)的函数和类。通过这些内置的函数和类去完成请求消息的构建和发送请求。

urlopen函数

该函数用于访问指定的url, 并得到response结果。

import urllib
from http.client import HttpResponse

url = ""
res: HttpResponse = urllib.request.urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT)
with res:
    with res:
    print(res, type(res))
    print(res.getcode(),   # 获取状态码
          res.reason,      # OK, 状态码说明
          res.geturl(),    # 访问的真实的url,发生了重定向。
          res.headers,     # 头信息
          res.info(),
          res.read(),     # Httresponse对象使用socket.makefile方法构建为一个类文件对象
          sep="\n")       # 可以使用文件对象的几乎所有方法

data如果不指定,使用get方法进行请求,指定后使用会使用post方法请求,并携带data中的数据进行发送,还可以设置访问超时时间,ca安全文件等信息。但是直接通过urlopen的参数无法自定义头信息内容,例如指定请求的方法和user-agent信息,这就需要我们自定义的Requset对象。

Request请求类

Request类,对提供的简单url请求信息进行封装,构建请求头等信息,实例化后获得一个请求对象,并可以为该对象添加请求头信息等内容。如果直接使用urlopen 对一个url进行请求,实际上会先将请求内容(指定的url)封装为一个Request对象,添加一些必要的头信息后在进行请求,以保证服务器接受该请求。

urlopen方法的url参数可以是一个url字符串,也可以是一个Request对象。通过Request类我们自己构建请求对象,并设置该请求对象的头信息,包括user-agent等信息。

Request初始化方法如下:

class Request:
    def __init__(self, url, data=None, headers={}, origin_req_host=None, unverifiable=False,
                 method=None):
# url
# data: 携带的数据,get方法携带的查询字符串或post方法提交的数据。
# header: 头信息,为一个字典
# method: 指定请求的方法
from urllib.request import Request

headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64)"}
req = Request(url, headers=headers, method="get")
# req.add_header("user-Agent", ""Mozilla/5.0 (Windows NT 10.0; WOW64)")  也可调用该方法添加

res = urlopen(req, timeout=10)  # 使用请求对象进行请求,得到response对象

Handler的使用

在request模块中,还有一个重要的角色,就是各种handler, 在该模块中定义了10多种handler类型,分别实现不同的功能,默认情况下,程序会自动加载某些handler去做日常的处理,如果需要更多功能实现,可以为程序添加handler。

urlopen函数

urlopen是常用的请求函数,源码核心逻辑如下

opener = None
def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
            *, cafile=None, capath=None, cadefault=False, context=None):
    global _opener
        if cafile or capath or cadefault:
            # 省略若干........
            opener = build_oNonepener(https_handler)
        elif context:
            https_handler = HTTPSHandler(context=context)
            opener = build_opener(https_handler)
        elif _opener is None:
            _opener = opener = build_opener()
        else:
            opener = _opener
        return opener.open(url, data, timeout)

经过各种的参数判断,最终都选择使用了build_opener函数去创建一个opener对象,返回值是其open方法的返回值,也就是将来的response。创建opener是发送请求前的准备工作。

opener对象

继续跟踪open方法

def open(self, fullurl, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
        # 如果url是字符串,构建一个Request对象,是Request对象则直接使用
        if isinstance(fullurl, str):
            req = Request(fullurl, data)
        else:
            req = fullurl
            if data is not None:
                req.data = data

        req.timeout = timeout
        protocol = req.type

        # pre-process request
        meth_name = protocol+"_request"
        for processor in self.process_request.get(protocol, []):
            meth = getattr(processor, meth_name)
            req = meth(req)

        # 这里我们看到response,这个函数发送了请求并得到了返回的数据,最后经过封装将response进行返回
        response = self._open(req, data)   # 这里我们看到response,这个函数真正的发送了请求得到了服务

        # post-process response
        meth_name = protocol+"_response"
        for processor in self.process_response.get(protocol, []):
            meth = getattr(processor, meth_name)
            response = meth(req, response)

        return response

opener对象的open方法发送了该请求,并得到了response返回。如果继续跟踪获取返回值的opener的_open方法,可以看到内部开始调用注册到这个opener上的handler,然后根据每个handle设置的优先级先后调用。

urlopen函数实际是调用opener对象的方法进行请求, opener对象通过build_opener函数创建。

def build_opener(*handlers):
    # 实例化opener对象,准备为其添加handler并返回
    opener = OpenerDirector()
    # 默认的handler,可以通过函数的参数添加新的handler, 
    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
                       FTPHandler, FileHandler, HTTPErrorProcessor,
                       DataHandler]
    if hasattr(http.client, "HTTPSConnection"):
        default_classes.append(HTTPSHandler)
    skip = set()
    
    # 遍历默认handles和函数参数中提供的handles,如果我们提供handler类型和默认列表中handler的子类
    # 或实例对象,则将默认列表中的handler剔除。使用我们提供的handler。
    for klass in default_classes:
        for check in handlers:
            if isinstance(check, type):
                if issubclass(check, klass):
                    skip.add(klass)
            elif isinstance(check, klass):
                skip.add(klass)
    for klass in skip:
        default_classes.remove(klass)  # 从默认列表中剔除

    # 遍历剔除后default_classes和给定的handler,使用opener.add_handler方法将其添加到opener对象中
    for klass in default_classes:
        opener.add_handler(klass())

    for h in handlers:
        if isinstance(h, type):  # 如果是类,实例化一个handler对象添加
            h = h()
        opener.add_handler(h)
    return openerdef build_opener(*handlers):
    # 实例化opener对象,准备为其添加handler并返回
    opener = OpenerDirector()
    # 默认的handler,可以通过函数的参数添加新的handler, 
    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
                       FTPHandler, FileHandler, HTTPErrorProcessor,
                       DataHandler]
    if hasattr(http.client, "HTTPSConnection"):
        default_classes.append(HTTPSHandler)
    skip = set()
    
    # 遍历默认handles和函数参数中提供的handles,如果我们提供handler类型和默认列表中handler的子类
    # 或实例对象,则将默认列表中的handler剔除。使用我们提供的handler。
    for klass in default_classes:
        for check in handlers:
            if isinstance(check, type):
                if issubclass(check, klass):
                    skip.add(klass)
            elif isinstance(check, klass):
                skip.add(klass)
    for klass in skip:
        default_classes.remove(klass)  # 从默认列表中剔除

    # 遍历剔除后default_classes和给定的handler,使用opener.add_handler方法将其添加到opener对象中
    for klass in default_classes:
        opener.add_handler(klass())

    for h in handlers:
        if isinstance(h, type):  # 如果是类,实例化一个handler对象添加
            h = h()
        opener.add_handler(h)
    return opener

handler

通过阅读源码,我们可以使用build_opner(handler) 传入自己的handler去构建opener,那么如何去构建一个自己handler。

urllib 定义了近20个handler类,每个handler分别实现自己的功能,而只需要将这个需要的handler添加到opener中即可。这些内置的handler基本可以满足我们的日常需求。在urllib.request模块中定义了这些handler。

proxy代理

代理是指在访问某一个站点时候,自己不直接访问,而是通过一个中间人代理去访问,代理得到结果后再把结果给我们,这样我们自己的ip地址就不会暴露给该站点,这也是常用的反反爬措施。urllib中使用Proxyhandler去实现代理。我们只需要创建一个ProxyHandler对象。

from urllib import request

url = "http://www.baidu.com"
proxy_handler = request.ProxyHandler(
    {'http': 'http://120.78.236.100:8080'}   # 配置好的代理ip地址
)
opener = request.build_opener(proxy_handler)

response = opener.open(url)

ProxyHandler的实例化参数要求必须为字典,key为指定的协议,value为完整的地址。同样的,如果已经有opener对象,想为其添加handler可以使用opener的add_handler方法

opener = build_opener()  # 先创建在添加
opener.add_handler(proxy_handler)

handler的优先级

优先级直接决定了opener上的这些handler的调用顺序,在handler类的类属性上指定了其优先级值handler_order,ProxyHandler通常经量靠前,所以其默认值为100, 而用于错误处理HTTPErrorProcessor优先级总是位于最后默认为1000。实现这个handler实例的优先级大小排序,只需要实现一个大小比较的方法即可。源码:

class BaseHandler:
    handler_order = 500

    def __lt__(self, other):
        if not hasattr(other, "handler_order"):
            # Try to preserve the old behavior of having custom classes
            # inserted after default ones (works only for custom user
            # classes which are not aware of handler_order).
            return True
        return self.handler_order < other.handler_order

在基类上实现了__lt__方法,handler实例的大小比较将会调用该方法,从而通过handler_order的值来进行判断。所以可以有 ProxyHandler() < HTTPErrorProcessor()成立(__lt__只对实例生效,而不是ProxyHandler <HTTPErrorProcessor)。如果我们需要进行优先级调整,只需要Hander.handler_order指定为合适的值即可。

urllib.parse模块

该模块主要进行url的解析问题,例如拼接url和对url进行编码

urlencode

def urlencode(query,doseq=False, safe='',encoding=None, errors=None,quote_via=quote_plus)

urlencode方法用于解决url的编码问题,解码则使用unquote方法,url进行发送前都会进行编码。主要是因为url中的查询字符串以k1=v1&k2=v2的形式发送时候,k和v中包含有特殊的元字符=或者&,导致接受方将字符串分解错误,所以编码以解决字符冲突问题,同时还解决了多字节字符和元字符冲突,例如中文。

host = "http://www.baidu.com/"
query_str = parse.urlencode({"tag": "中国"})
en_url = host + "?" + query_str
print(en_url)                   # http://www.baidu.com/?tag=%E4%B8%AD%E5%9B%BD  编码
print(parse.unquote(en_url))    # http://www.baidu.com/?tag=中国                 解码

上面直接拼接目标字符串后访问该url,也可以通过urlopen的data参数自动进行拼接,但是拼接前同样需要将数据进行编码。

上面的方式通常是使用get方法以查询字符串的方式提交数据,编码后的数据包含在url中。如果使用post方法提交,提交的数据 content-type : application/x-www-form-urlencoded,同样需要编码再作为消息体传输。

res = urlopen(url, data=parse.enconde("tag":"中国").encode()) 
# data将会被编码为tag=%E4%B8%AD%E5%9B%BD,由于制定了data,将会以post方法提交数据,data数据在请求体中

urljoin

两个url拼接时候如果连续出现host的内容,以后面的url为主,前面所有内容将会被覆盖。即遇到新的host重新计算。否则直接进行普通的url拼接即可。allow_fragments参数表示是否拼接锚点的内容

# urllib.parse.urljoin(base, url, allow_fragments=True)

form urllib.parse import urljoin

print(urljoin('http://www.baidu.com', 'abc.html'))   
print(urljoin('http://www.baidu.com', 'https://bing.cn/abc.html'))

# http://www.baidu.com/abc.html
# https://pythonsite.com/FAQ.html

urlparse和urlunparse

根据一个url的组成规则,scheme://username:password@host/path?query#fragment, urlparse函数会将这些内容进行切分,返回一个切分后的结果对象

def urlparse(url, scheme='', allow_fragments=True)

# 示例
print(urlparse("https://www.baidu.com/search?tag='中国'#comment"))
# 结果,按照各部分进行切分。
# ParseResult(scheme='https', netloc='www.baidu.com', path='/search', params='', query="tag='中国'", fragment='comment')

urlunparse的作用则是按照此方式拼接。

忽略CA认证

当我们使用https协议去访问网站时,数据传输是经过加密,这样可可以防止数据再传输的过程中(包括服务器或者客户端)被他人窃取,篡改信息,而加密则使用ssl协议完成,即https = http + ssl。关于CA证书详细的书名介绍参照https://www.cnblogs.com/handsomeBoys/p/6556336.html

https连接过程

  • 客户端发送请求到服务器端
  • 服务器端返回证书和公开密钥,公开密钥作为证书的一部分而存在
  • 客户端验证证书和公开密钥的有效性,如果有效,则生成共享密钥并使用公开密钥加密发送到服务器端
  • 服务器端使用私有密钥解密数据,并使用收到的共享密钥加密数据,发送到客户端
  • 客户端使用共享密钥解密数据
  • SSL加密建立………

由于https使用中间机构去保证该证书的安全性。则该证书颁发机构必须得到服务端和客户端同时认可,如果客户端不信任该机构的颁发的证书,将会拒绝进行数据的加密CA认证。在某些安全性要求不高的情况下我们可以忽略CA认证信息,即使没有被客户端接受任然进行数据通信,urllib包中的函数提供了该功能。

 

假如我们访问一个非权威CA机构认证的网站,客户端的ssl协议会默认拒绝该CA机构的认证文件。

import urllib

url = "https://www.12306.cn/mormhweb/"   # 使用了https,就需要CA认证,但是客户端不认可该CA机构
res = urllib.request.urlopen(url)
with res:
    printt(res.read())  
# ssl.CertificateError:hostname 'www.12306.cn' don't match either of ...

上面程序将会报ssl.CertificateError,即由于客户端的拒绝而认证失败。忽略CA认证可使用以下方法。

import urllib
import ssl     # 导入ssl

context = ssl._create_unverified_context()    # 创建忽略不信任的证书的信息

url = "https://www.12306.cn/mormhweb/"   
res = urllib.request.urlopen(url, context=context)  # 访问时携带该信息,证书返回后将会忽略,可以正常访问
with res:
    printt(res.read())  

 

posted @ 2020-06-09 17:38  没有想象力  阅读(467)  评论(0编辑  收藏  举报