• 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="
    ")       # 可以使用文件对象的几乎所有方法

    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())  
  • 相关阅读:
    需求分析作业01
    2016年秋季个人阅读计划
    个人总结
    梦断代码读后感03
    进度条16
    我的第一个小系统总结
    进度条15
    eclipse连接SqlServer2008(被它搞得惨兮兮)
    梦断代码读后感02
    Android ViewPager刷新解析
  • 原文地址:https://www.cnblogs.com/k5210202/p/13074161.html
Copyright © 2020-2023  润新知