Python 3 网络爬虫 个人笔记 (未完待续)

Python 3 网络爬虫 笔记 (未完待续)



Chap 2 爬虫基础

看书吧就不详述了。



Chap 3 基本库的使用

  • Python 内置的 HTTP 请求库 —— urllib,它包含四个模块:

    1. request最基本的 HTTP 请求模块,可用于模拟发送请求。只需传入 url 等参数即可。
    2. error异常处理模块,出现请求错误时,可以捕获异常,进行重试等操作。
    3. parse工具模块,提供了许多 url 处理方法。
    4. robotparser用于识别网站的 robots.txt 文件,以判断哪些网站可以爬取,哪些不能。
  • 1. request.urlopen()

    # >>>>>>>>>>>> 通过 request.urlopen() 实现简单的 get 请求
    import urllib.request
    
    response = urllib.request.urlopen('https://www.python.org')    # 请求 Python 官网
    print(response.read().decode('utf-8'))    # 打印网页源代码
    print(type(response))    # 这里可见 urlopen 返回的是一个 HTTPResposne 类型的对象
    

    HTTPResposne 类型的对象主要包含 read(), getheader(name), getheaders() 等方法以及 msg, version, status, reason 等属性。

    print(response.status)    # 响应状态码
    print(response.getheaders())    # 响应头信息
    print(response.getheader('Server'))    # 响应头中的 Server 值 —— 服务器用什么搭建的。
    
    urlopen() 的主要参数说明
    data传递了此参数,请求方式自动变为 POST
    timeout用于设置超时时间,单位为秒
    其他context 参数用来指定 SSL 设置;
    cafile 和 capath 这两个参数分别指定 CA 证书和它的路径
    # >>>>>>>>>>>> data 参数
    import urllib.parse
    import urllib.request
    
    # 1. 构造 data 参数: 首先用 urlencode() 方法将参数字典转化为字符串,然后通过 bytes() 方法将该字符串转化为 bytes 类型。
    data = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf-8')
    # 2. 传递 bytes 类型的 data 参数, 它是值为 hello 的 word 参数。
    response = urllib.request.urlopen('http://httpbin.org/post', data=data)
    # 3. 可以看到传递的参数出现在了 form 字段中,这就说明模拟了表单提交的方式,以 POST 方式传输数据。
    print(response.read())
    
    # >>>>>>>>>>>> timeout 参数
    import socket
    import urllib.request
    import urllib.error
    
    try:    # 请求测试链接, 设置超时时间为 0.1 秒
        response = urllib.request.urlopen('http://httpbin.org/get', timeout=0.1)
    except urllib.error.URLError as e:    # 捕获 URLError 异常
        if isinstance(e.reason, socket.timeout):    # 判断异常是 socket.timeout 类型
            print('timeout')
    
  • 2. request.Request

    # >>>>>>>>>>>> 通过 Request 对象来构建请求
    import urllib.request
    
    request = urllib.request.Request('https://python.org')    # 实例化 Request 对象
    response = urllib.request.urlopen(request)    # urlopen 的参数变成了 Request 对象的实例
    print(response.read().decode('utf-8'))
    
    Request 类的属性说明
    url要请求的 url,是必传参数。
    data必须以 bytes 类型传递,参考 urlopen() 的 data 参数。
    headers请求头,字典形式,可以通过修改 User-Agent 来伪装浏览器。
    origin_req_host请求方的 host 名称或 IP。
    method请求方法,GET,POST等。
    # >>>>>>>>>>>> 传递多个参数构建请求
    from urllib import request, parse
    
    url = 'http://httpbin.org/post'    # 请求的 URL
    para_dict = {'name': 'Gozen Sanji'}    # 参数字典
    headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Host': 'httpbin.org'}    # 在 headers 中指定了 UA 和 Host
    
    # 1. 将参数字典转化为字符串并转为 bytes 类型
    data = bytes(parse.urlencode(para_dict), encoding='utf-8')
    # 2. 实例化 Request 时传递了4个参数 (传递了 headers 和 data 并指点请求方式为 POST)
    req = request.Request(url=url, data=data, headers=headers, method='POST')
    # 3. 通过 urlopen() 发送请求
    response = request.urlopen(req)
    print(response.read().decode('utf-8'))
    
  • 3. request 高级用法:利用 Handler 构建 Opener

    # >>>>>>>>>>>> 处理验证的 Handler
    from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener
    from urllib.error import URLError
    
    username = 'username'
    password = 'password'
    url = 'http://localhost:5000/'
    
    # 1. 实例化 HTTPPasswordMgrWithDefaultRealm 对象
    p = HTTPPasswordMgrWithDefaultRealm()
    # 2. 为该实例添加用户名和密码
    p.add_password(None, url, username, password)
    # 3. 通过 1 中的对象实例化 HTTPBasicAuthHandler 对象 ———— 得到一个处理验证的 Handler
    auth_handler = HTTPBasicAuthHandler(p)
    # 4. 通过上述 Handler 构建 Opener
    opener = build_opener(auth_handler)
    
    try:    
        # 5. 通过 open() 方法发送请求, 这里会自动完成验证
        result = opener.open(url)
        html = result.read().decode('utf-8')
        print(html)
    except URLError as e:
        print(e.reason)
    
    # >>>>>>>>>>>> 设置代理的 Handler
    from urllib.request import ProxyHandler, build_opener
    from urllib.error import URLError
    
    # 1. 实例化一个 ProxyHandler 对象, 其参数是一个字典, 键名为协议类型, 值为代理链接。
    proxy_handler = ProxyHandler({
        'http': "http://127.0.0.1:9743",
        'https': "https://127.0.0.1:9743"
    })
    # 2. 通过上面的 Handler 构建 Opener。
    opener = build_opener(proxy_handler)
    try:
        # 3. 通过 Opener 的 open() 方法发送请求。
        response = opener.open('https://www.baidu.com')
        print(response.read().decode('utf-8'))
    except URLError as e:
        print(e.reason)
    
    import http.cookiejar, urllib.request
    
    # >>>>>>>>>>>> 1. 生成 Cookies 文件
    filename = 'cookies.txt'
    
    # 1. 实例化 LWPCookieJar 对象
    cookie = http.cookiejar.LWPCookieJar(filename)
    # 2. 利用上述 cookie 实例构建 Handler
    handler = urllib.request.HTTPCookieProcessor(cookie)
    # 3. 利用上述 Handler 构建 Opener
    opener = urllib.request.build_opener(handler)
    # 4. 发送请求, 打印响应状态码
    response = opener.open('http://www.baidu.com')
    print(response.status)
    # 5. 调用 save() 方法保存 Cookies 到本地文件
    cookie.save(ignore_discard=True, ignore_expires=True)
    
    
    
    # >>>>>>>>>>>> 2. 读取并使用 Cookies (以 LWPCookieJar 格式为例)
    
    # 1. 实例化 LWPCookieJar 对象
    cookie = http.cookiejar.LWPCookieJar()
    # 2. load() 方法读取本地 Cookies 文件 (之前已经生成并保存)
    cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True)
    # 3. 构建 Handler
    handler = urllib.request.HTTPCookieProcessor(cookie)
    # 4. 构建 Opener
    opener = urllib.request.build_opener(handler)
    # 5. 发送请求, 此时就能得到百度的源代码了
    response = opener.open('http://www.baidu.com')
    print(response.read().decode('utf-8'))
    
  • 4. error.URLError

    由 request 模块产生的异常都可通过捕获 URLError 这个类来处理

    from urllib import error, request
    
    try:
        # 1. 视图打开一个不存在的页面
        response = request.urlopen('https://www.cuiqingcai.com/index.htm')
    except error.URLError as e:
        # 2. URLError 的属性 reason 用于返回错误的原因
        print(e.reason)
    
  • 5. error.HTTPError

    是 URLError 的子类,专门用于处理 HTTP 请求错误,有三个属性:code,reason 和 headers

    from urllib import error, request
    
    try:
        response = request.urlopen('https://www.cuiqingcai.com/index.htm')
    # 1. 先尝试捕获子类错误
    except error.HTTPError as e:
        # 输出 错误原因, HTTP状态码, 请求头
        print(e.reason, e.code, e.headers, sep="\n\n")
    # 2. 再尝试捕获父类错误
    except error.URLError as e:
        print(e.reason)
    else:
        print("Requested Successfully.")
    

    有时 reason 属性返回的未必是字符串,而是一个对象,见下例:

    import socket
    import urllib.request
    import urllib.error
    
    try:
        # 1. 设置超时时间以强制抛出 timeout 异常
        response = urllib.request.urlopen('https://www.baidu.com', timeout=0.01)
    except urllib.error.URLError as e:
        print(type(e.reason))    # 打印结果: <class 'socket.timeout'>
        if isinstance(e.reason, socket.timeout):    # 判断 reason 的类型
            print('TIME OUT')
    
  • 6. parse 模块:用于实现 URL 各部分的抽取、合并及链接转换等。

    方法介绍
    urlparse()识别 url 并将其分成 6 个部分。返回结果是命名元组,支持属性名或index访问
    urlunparse()接受一个长度必须为 6 的可迭代对象,以构造 url
    urlsplit()与 urlparse() 相似,它将一个 url 拆分为 5 个部分(params 被合并到了 path 中)
    urlunsplit()与 urlunparse() 类似,它接受一个长度必须为 5 的可迭代对象,以构造 url
    urljoin()通过解析 base_url 对新链接进行补充
    urlencode()将字典序列化为 GET 请求的参数
    parse_qs()将 GET 请求参数 “反序列化” 为字典 (其中字典的值是列表形式)
    parse_qsl()类似 parse_qs(),但它返回以元组为元素的列表
    quote()可将 url 中的 “中文字符” 转化为 url 编码
    unquote()用于 url 解码以还原 url 中的中文字符
    # 1. urlparse() 能识别 url 并将其分解为 6 个部分: scheme, netloc, path, params, query, fragment
    # -------------------------------------------------------------------------------------------
    from urllib.parse import urlparse
    
    result = urlparse("http://www.baidu.com/index.html;user?id=5#comment")
    print(result)
    # 返回结果是 ParseResult 类型的对象: ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
    
    
    # 待解析的 url 中不含协议部分, 但在 scheme 参数中指定协议类型为 https
    result = urlparse("www.baidu.com/index.html;user?id=5#comment", scheme='https')
    print(result)
    # 返回结果中协议类型会采用 scheme 参数中指定的 https
    # Remark: 若待解析 url 中包含协议类型, 则 scheme 参数怎么指定都不影响解析结果
    
    
    # allow_fragments 参数指定为 False, 即忽略 fragment
    result = urlparse("http://www.baidu.com/index.html;user?id=5#comment", allow_fragments=False)
    print(result)
    # 此时返回结果中 fragment 部分为空, 而 #comment 被合并进了 query (前一个非空的组成部分)中
    
    
    # 返回结果 ParseResult 实际上是一个命名元组, 可以用 属性 & index 来访问:
    print(result.scheme, result[0], result.netloc, result[1], sep="\n")
    """
    运行结果为:
    http
    http
    www.baidu.com
    www.baidu.com
    """
    
    
    
    # 2. urlunparse() 方法接受一个长度(必须)为 6 的可迭代对象来构造 url
    # -------------------------------------------------------------------------------------------
    from urllib.parse import urlunparse
    
    data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']
    print(urlunparse(data))
    """
    运行结果为: http://www.baidu.com/index.html;user?a=6#comment
    """
    
    # 3. urlsplit() 方法与 urlparse() 相似, 它将一个 url 拆分为 5 个部分(params 被合并到了 path 中)
    # -------------------------------------------------------------------------------------------
    from urllib.parse import urlsplit
    
    result = urlsplit("http://www.baidu.com/index.html;user?id=5#comment")
    print(result, sep="\n")
    # 返回结果为 SplitResult 类型的对象: SplitResult(scheme='http', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment'), 这其实也是一个命名元组, 可以通过 index & 属性 来访问:
    print(result.path, result[2], sep="\n")
    """
    运行结果为:
    /index.html;user
    /index.html;user
    """
    
    
    
    # 4. urlunsplit() 方法与 urlunparse() 类似, 它接受一个长度(必须)为 5 的可迭代对象, 以构造 url
    # -------------------------------------------------------------------------------------------
    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
    """
    
    # 5. urljoin() 会解析第一参数 base_url 以对新链接的缺失部分进行补充
    # -------------------------------------------------------------------------------------------
    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"))
    """
    Remark: 通过上述例子可见 base_url 提供了三项内容 scheme, netloc 和 path
    			若这 3 项在新链接中不存在, 则予以补充;
             	 若在新链接中存在, 则使用新链接的部分
            而 base_url 中的 params, query, fragment 是不起作用的。
    """ 
    
    # 6. urlencode() 可将字典序列化为 GET 请求的参数
    # -------------------------------------------------------------------------------------------
    from urllib.parse import urlencode
    
    params = {'name': 'GozenSanji', 'age': 17}
    base_url = "http://www.baidu.com?"
    url = base_url + urlencode(params)
    print(url)
    """
    运行结果为: http://www.baidu.com?name=GozenSanji&age=17
    """
    
    # 7. parse_qs() 将 GET 请求参数 “反序列化” 为字典 (其中字典的值是列表形式)
    # -------------------------------------------------------------------------------------------
    from urllib.parse import parse_qs
    
    query = "name=GozenSanji&age=17"
    print(parse_qs(query))
    """
    运行结果为: {'name': ['GozenSanji'], 'age': ['17']}
    """
    
    
    
    # 8. parse_qsl() 类似 parse_qs(), 但它返回以元组为元素的列表
    # -------------------------------------------------------------------------------------------
    from urllib.parse import parse_qsl
    
    query = "name=GozenSanji&age=17"
    print(parse_qsl(query))
    """
    运行结果为: [('name', 'GozenSanji'), ('age', '17')]
    """
    
    # 9. quote() 可将 url 中的 "中文字符" 转化为 url 编码
    # -------------------------------------------------------------------------------------------
    from urllib.parse import quote
    
    kw = "城堡"
    url = "https://www.baidu.com/s?wd=" + quote(kw)    # 通过 quote() 对中文字符进行 url 编码
    print(url)
    """
    运行结果为: https://www.baidu.com/s?wd=%E5%9F%8E%E5%A0%A1
    """
    
    
    
    # 10. unquote() 用于 url 解码以还原 url 中的中文字符
    # -------------------------------------------------------------------------------------------
    from urllib.parse import unquote
    
    url = "https://www.baidu.com/s?wd=%E5%9F%8E%E5%A0%A1"
    print(unquote(url))
    """
    运行结果为: https://www.baidu.com/s?wd=城堡
    """
    
  • 7. robotparser 模块可以实现网站 Robots 协议的分析

    用到再说

  • 更方便的 Requests 库:

  • 1. 一个简单的例子

    # 1. 调用 get() 方法实现 GET 请求
    # -------------------------------------------------------------------------------------------
    import requests
    
    r = requests.get("https://www.baidu.com")
    print(type(r))		    # 返回一个 Response 类型的对象
    print(r.status_code)	# 状态码
    print(type(r.text))		# 响应体类型为 str
    print(r.text)   	    # 响应体内容
    print(r.cookies)	    # cookies (类型为 RequestsCookieJar)
    

    除了 GET 请求,还有下面的:

    # 2. 一行代码实现其他类型的请求
    # -------------------------------------------------------------------------------------------
    import requests
    
    r1 = requests.post("http://httpbin.org/post")
    r2 = requests.put("http://httpbin.org/put")
    r3 = requests.delete("http://httpbin.org/delete")
    r4 = requests.head("http://httpbin.org/get")
    r5 = requests.options("http://httpbin.org/get")
    
  • 2. Get 请求

    # 1. 在 get 请求中添加参数
    # -------------------------------------------------------------------------------------------
    import requests
    
    # 1. 以字典形式构造参数
    data = {"name": "victorique", "age": 12}
    # 2. 将字典传递给 params 参数
    r = requests.get('http://httpbin.org/get', params=data)
    # 3. 此时请求信息中就可以看到传递的参数了
    print(r.text)
    

    进阶の例子:

    # 1. 抓取网页(知乎页面里“热门收藏夹”下的一些title)
    # -------------------------------------------------------------------------------------------
    import requests
    import re
    
    # 1. 指定 headers 中的 UA
    headers = {
        'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"
    }
    # 2. 发送带 headers 参数的 Get 请求
    r = requests.get("https://www.zhihu.com/explore", headers=headers)
    # 3. 创建正则表达式对象(带修饰符)
    pattern = re.compile('<a class="ExploreCollectionCard-contentTitle".*?data-za-detail-view-id.*?>(.*?)</a>', re.S)
    # 4. 通过 findall 方法进行正则匹配
    titles = re.findall(pattern, r.text)
    print(titles)
    
    
    
    # 2. 抓取二进制数据(图片,音频,视频等)
    # -------------------------------------------------------------------------------------------
    
    # 1. 请求 github 网站图标
    r = requests.get("https://github.com/favicon.ico")
    print(r.text)    # 以 str 类型打印, 显示乱码
    print(r.content)    # 以 bytes 类型打印
    # 2. 以二进制写的形式打开
    with open('favicon.ico', 'wb') as f:
        # 3. 写入二进制数据, 得到网站图标文件
        f.write(r.content)
    
  • 3. POST 请求

    import requests
    
    # 1. 构造字典形式的参数
    data = {'name': 'GozenSanji', 'age': 17}
    # 2. 发送带有参数的 POST 请求
    r = requests.post("http://httpbin.org/post", data=data)
    # 3. 传递的参数在 “form” 当中
    print(r.text)
    
  • 4. 响应

    # 发送请求后, 得到响应, 下面查看其各种属性
    import requests
    
    headers = {
        'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"
    }
    r = requests.get("http://jianshu.com", headers=headers)
    # 1. 状态码
    print(type(r.status_code), r.status_code, sep=" ~~~ ")
    # 2. 响应头
    print(type(r.headers), r.headers, sep=" ~~~ ")
    # 3. Cookies
    print(type(r.cookies), r.cookies, sep=" ~~~ ")
    # 4. url
    print(type(r.url), r.url, sep=" ~~~ ")
    # 5. 请求历史
    print(type(r.history), r.history, sep=" ~~~ ")
    
  • 5. 高级用法:

    import requests
    # 1. 文件上传
    # -------------------------------------------------------------------------------------------
    
    # 0. 注意这个 files 的写法
    files = {'file': open('favicon.ico', 'rb')}
    # 1. 发送 POST 请求时, 指定 files 参数以上传文件
    r = requests.post("http://httpbin.org/post", files=files)
    # 2. 在 files 字段下的内容证明成功上传了文件
    print(r.text)
    
    
    
    # 2. 获取和设置 Cookies
    # -------------------------------------------------------------------------------------------
    r = requests.get("https://www.baidu.com")
    # 1. 直接通过 cookies 属性获取 Cookies
    print(r.cookies)
    # 2. 可对 Cookies 遍历解析
    for k, v in r.cookies.items():
        print(f"{k} = {v}")
    
    # 3. 在 headers 中设置好 Cookie (以维持登录状态)
    headers = {
        'Cookie': 'SESSIONID=yGSmUltPNjOrmaNJ36mdO9M5ZugNuhex2zE2ceZqqL2; JOID=UlgQBU4M0Np7HR0XNwo1DfeUepkjL_b6Wzs-MRcq8_xbPTs0EXy43yIYGBE3EjupzXIINfypdNYHdDZvaOFdvUM=; osd=W1AdAUgF2Nd_GxQfOg4zBP-Zfp8qJ_v-XTI2PBMs-vRWOT09GXG82SsQFRUxGzOkyXQBPfGtct8PeTJpYelQuUU=; _zap=21f4fd90-7f8b-4394-8cec-cdf3218d2566; d_c0="AABdnthhWRGPTsDP-usAf5MCAuo-_GX0fmc=|1590836114"; _ga=GA1.2.1794552939.1590836116; _gid=GA1.2.646135605.1594178659; _xsrf=db959aea-df00-4fe2-8565-23dde1c4cfa7; Hm_lvt_98beee57fd2ef70ccdd5ca52b9740c49=1593754262,1593754407,1594178659,1594191473; capsion_ticket="2|1:0|10:1594191476|14:capsion_ticket|44:MzBlMDVmOWE3MjhjNDhjNjljMDIxZDJkOTRmZGIwOWY=|8f1100ba056a2e7b86d74471116f04e330f42f0c2a07159f5e100f7db3426dea"; z_c0="2|1:0|10:1594191483|4:z_c0|92:Mi4xeEt2WUJBQUFBQUFBQUYyZTJHRlpFU1lBQUFCZ0FsVk5lN3p5WHdDM0lLWDdDVjZPZ24xaHVVME9HQWJzMmpvLTJ3|18a961f2133a663da09762645cc9a931c128431774c01a347c919f1e10b73531"; Hm_lpvt_98beee57fd2ef70ccdd5ca52b9740c49=1594191483; q_c1=3e59bd711414449b965e3cc4b7201261|1594191484000|1594191484000; KLBRSID=d017ffedd50a8c265f0e648afe355952|1594191512|1594191473',
        'Host': 'www.zhihu.com',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
               }
    # 4. 发送 get 请求时传递 headers 参数
    r = requests.get("https://www.zhihu.com", headers=headers)
    # 5. 响应结果中包含登录后的内容, 说明登录成功
    print(r.text)
    
    # 3. 利用 Session 对象维持会话
    # -------------------------------------------------------------------------------------------
    import requests
    
    # 1. 首次请求测试网址时, 设置了 cookie, 其名称为 number, 内容为 123456789
    requests.get("http://httpbin.org/cookies/set/number/123456789")
    # 2. 再请求测试网址, 此网址可以获取当前 Cookies
    r = requests.get('http://httpbin.org/cookies')
    # 3. 但这样并不能获取到设置的 Cookies
    print(r.text)
    
    # 接下来利用 Session 进行尝试
    import requests
    # 1. 实例化一个 Session 对象
    s = requests.Session()
    # 2. 请求测试网址并设置 cookie
    s.get("http://httpbin.org/cookies/set/number/123456789")
    # 3. 通过 Session 再次请求测试网址获取当前 Cookies
    r = s.get('http://httpbin.org/cookies')
    # 4. 这里成功获取到了前一次请求时设置的 cookie
    print(r.text)
    
    # Remark: 1. 使用 requests.post() 方法登录某网站后, 再使用 requests.get() 方法获取登录后的信息, 这相当于打开了两个浏览器, 是两个无关的会话, 因此这样无法得到登陆后的信息。	2. 利用 Session, 可以做到模拟同一个会话而不用担心 Cookies 的问题, 它通常用于模拟登录成功后再进行下一步操作。 
    

    在发送 HTTP 请求时,会自动检查 SSL 证书,因而会有出现证书验证错误的结果,此时可通过设置 verify 参数不进行证书验证。

    # 4. verify 参数控制是否验证证书
    # -------------------------------------------------------------------------------------------
    import requests
    from requests.packages import urllib3
    
    # 1. 设置忽略警告 (注释掉我试试看)
    urllib3.disable_warnings() 
    # 2. 发送 get 请求时将 verify 设置为 False (默认为 True)
    response = requests.get("https://www.12306.cn", verify=False)
    print(response.status_code)
    
    
    
    # 5. 通过 proxies 参数设置代理
    # -------------------------------------------------------------------------------------------
    # 1. 构造代理参数字典
    proxies = {
        'http': 'http://host:port',
        'https': 'https://host:port'
    }    # 这只是个模板哦
    """
    若代理需要使用 HTTP Basic Auth, 可以像下面这样设置代理:
    proxies = {'http': 'http://user:password@host:port'}
    """
    # 2. 发送 get 请求时通过 proxies 参数传入代理 
    requests.get("https://taobao.com", proxies=proxies)
    
    
    
    # 6. 通过 timeout 参数设置超时时间
    # -------------------------------------------------------------------------------------------
    # 1. 通过指定 timeout 参数, 将超时时间设置为 1s, 若 1s 内无响应则抛出异常
    r = requests.get("https://www.taobao.com", timeout=1)
    # 2. 将 timeout 改为 0.01 看看会怎么样?
    print(r.status_code)
    # Remark: 想要永久等待, 可以设置 timeout=None 或者干脆不设置该参数。
    
    
    
    # 7. 通过 auth 参数实现身份认证
    # -------------------------------------------------------------------------------------------
    # 1. 这里只是一个测试 url, auth 参数接受一个用户名和密码的元组
    r = requests.get('http://localhost:5000', auth=('username', 'password'))
    # 2. 发送请求时将自动认证, 认证成功则返回状态码为 200
    print(r.status_code)
    
    # 8. 将请求当作独立的对象来看待: Prepared Request
    # -------------------------------------------------------------------------------------------
    from requests import Request, Session
    
    url = "http://httpbin.org/post"
    data = {'name': 'victorique'}
    headers = {"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'}
    
    # 1. 实例化一个 Session 对象
    s = Session()
    # 2. 用 url, data, headers 构造一个 Request 对象
    req = Request('POST', url, data=data, headers=headers)
    # 3. 通过 Session 的 prepare_request() 方法将 Request 对象转换为 Prepared Request 对象
    prepared = s.prepare_request(req)
    # 4. 通过 Session 的 send() 方法发送 Prepared Request 实例
    r = s.send(prepared)
    # 5. 从结果可以看到这种方式同样达到了 POST 请求的效果
    print(r.text)
    
  • 有用的 正则表达式

    正则表达式是处理字符串的强大工具,它能实现字符串的 检索、替换、匹配

    书中 表3-2(on Page 140)列出了常用的匹配规则供随时查阅。

  • 1. match() 方法

    match() 会尝试从字符串的起始位置匹配正则表达式,若匹配则返回匹配成功的结果;若不匹配则返回 None。

    下面看一个简单的例子:

    import re
    
    # 1. 声明一个 String 并打印其长度
    content = "Hello 123 4567 World_This is a Regex Demo"
    print(len(content))
    # 2. ^匹配字符串开头, \s匹配空白字符, \d匹配数字, \d\d\d\d = \d{4}, \w{10}匹配10个字母及下划线
    regex = '^Hello\s\d\d\d\s\d{4}\s\w{10}'
    # 3. match() 的第一个参数为正则表达式, 第二个参数为目标字符串
    result = re.match(regex, content)
    # 4. 返回结果为 re.Match 对象
    print(result)
    # 5. group() 方法输出匹配到的内容, span() 方法输出匹配的下标范围
    print(result.group(), result.span(), sep="\n")
    

    以下为更实用的示例:

    # 1. 使用 () 进行目标匹配
    # -------------------------------------------------------------------------------------------
    import re
    
    content = "Hello 1234567 World_This is a Regex Demo"
    regex = "^Hello\s(\d+)\sWorld"    # 目标是7个数字
    result = re.match(regex, content)
    # group 将输出完整的匹配结果, 而 group(i) 将输出第 i 个被 () 包围的匹配结果
    print(result.group(), result.group(1), result.span(), sep="\n")
    
    
    
    # 2. 通用匹配 .*
    # -------------------------------------------------------------------------------------------
    content = "Hello 1234567 World_This is a Regex Demo"
    regex = "^Hello.*Demo$"    # 以 Hello 开始, 以 Demo 结束, 中间匹配任意字符
    result = re.match(regex, content)
    print(result.group(), result.span(), sep="\n")
    
    
    
    # 3. 贪婪与非贪婪
    # -------------------------------------------------------------------------------------------
    content = "Hello 1234567 World_This is a Regex Demo"
    regex = "^He.*(\d+).*Demo$"    # 贪婪匹配 .* 将匹配尽可能多的字符
    result = re.match(regex, content)
    print(result.group(1), sep="\n")    # 这里只能匹配到一个数字7,想想为什么?
    
    regex = "^He.*?(\d+).*Demo$"    # 非贪婪匹配 .*? 将匹配尽可能少的字符, 推荐使用!
    result = re.match(regex, content)
    print(result.group(1), sep="\n")    # 这里可以匹配到全部7个数字
    
    # 再看一个例子:
    content = 'http://weibo.com/comment/kEraCN'
    result1 = re.match('^http.*?comment/(.*?)', content)    # 目标匹配为空
    result2 = re.match('^http.*?comment/(.*)', content)    # 目标匹配到 kEraCN 
    print(f"result1: {result1.group(1)}", f"result2: {result2.group(1)}", sep="\n")
    
    
    
    # 4. 修饰符
    # -------------------------------------------------------------------------------------------
    content = '''Hello 1234567 World_This 
    is a Regex Demo'''    # 这是一个带有换行符的字符串
    result = re.match("^He.*(\d+).*?Demo$", content)
    # 匹配失败返回 None, 这是因为 . 匹配的是“除换行符之外的任意字符”, 当遇到换行符时 .*? 就不能匹配了
    print(result)
    
    # 使用修饰符 re.S 使 . 匹配 “包括换行符在内的所有字符”
    result = re.match("^He.*?(\d+).*?Demo$", content, re.S)
    # 此时就可以成功匹配到整个字符串了
    print(result)
    # Remark: 表 3-3 on Page 145 给出了其他常用修饰符以供参考
    
    
    
    # 5. 转义匹配
    # -------------------------------------------------------------------------------------------
    content = "(百度)www.baidu.com"
    # 当遇到用于正则匹配模式的特殊字符时, 在前面加反斜线转义一下即可
    result = re.match('\(百度\)www\.baidu\.com', content)
    # 经反斜线实现转义后, 这里成功匹配了上述字符串
    print(result)
    
  • 2. search() 方法

    match 方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了,而<span style='color:orange;background: ;font-size: ;font-family: ;'> search 方法则会扫描整个字符串,并返回第一个成功匹配的结果。</span>
    
    # search() 可以匹配子字符串
    # ------------------------------------------------------------------------------------------
    import re
    
    content = "EXTRA STRING Hello 1234567 World_This is a Regex Demo EXTRA STRING"
    result = re.search('Hello.*?(\d+).*?Demo', content)
    # 这里成功匹配到了除去开头和结尾的 EXTRA STRING 部分的字串
    print(result.group())
    

    下面是一个匹配网页源代码的例子:

    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>
    '''
    # 这里尝试匹配 class="active" 的节点下的子节点中的歌手与歌曲名
    regex = '<li.*?active.*?singer="(.*?)">(.*?)</a>'
    result = re.search(regex, html, re.S)    # 注意加上修饰符以应对换行符的匹配
    # 成功输出歌手名与歌曲名
    print(result.group(1), result.group(2), sep="\n")
    
  • 3. findall() 方法

    相对于 search 方法返回第一个成功匹配的内容,findall 方法会 搜索整个字符串,然后返回匹配正则表达式的所有内容。

    import re
    
    # 对同样的 html 这里尝试获取所有 a 节点的超链接, 歌手和歌名
    regex = '<a href="(.*?)".*?singer="(.*?)".*?>(.*?)</a>'
    result = re.findall(regex, html, re.S)
    # 若匹配成功, 则结果为列表类型
    print(type(result))
    
    for each in result:
        print(f"超链接: {each[0]};  歌手: {each[1]};  歌曲名: {each[2]}")
    
  • 4. sub() 方法

    sub 方法可以将字符串中与正则表达式匹配的部分替换为其他内容:
    
    import re
    
    # 1. 替换字符串中的数字为🐬
    content = 'Go233zen666 222San768ji'
    # sub() 方法的第一个参数 \d+ 匹配所有数字, 第二个参数为用于替换的内容
    content = re.sub('\d+', '🐬', content)
    print(content)
    
    # 2. 假设现在想要提取先前 html 中的所有歌名, 直接通过正则表达式实现会比较复杂, 看看下面的做法:
    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>
    '''
    
    # 首先通过 sub() 方法将 a 节点替换为空, 只保留节点的文本
    regex = '<a .*?>|</a>'
    html = re.sub(regex, '', html)
    print(html)
    
    # 在利用 findall() 匹配歌名即可
    regex = '<li .*?>(.*?)</li>'
    results = re.findall(regex, html, re.S)
    for result in results:
        print(result.strip())
    # Remark: 在适当的时候利用 sub() 简化字符串后再通过 findall() 进行提取效率更高
    
  • 5. compile() 方法

    compile() 方法可将正则字符串编译成正则表达式对象,以便重复使用:

    import re
    
    content1 = '2016-12-15 12:00'
    content2 = '2016-12-17 12:55'
    content3 = '2016-12-22 13:21'
    
    # 通过 compile 创建正则表达式对象
    pattern = re.compile('\s\d{2}:\d{2}')
    
    # 通过 sub 方法和正则表达式对象将时间部分去除
    result1 = re.sub(pattern, '', content1)
    result2 = re.sub(pattern, '', content2)
    result3 = re.sub(pattern, '', content3)
    
    print(result1, result2, result3, sep="🐬🐬🐬")
    # Remark: compile() 方法中还可以传入修饰符, 这样在 search, findall 中就不需要再传了
    
  • 实战:抓取猫眼电影排行

    代码参考:https://github.com/Python3WebSpider/MaoYan

    总体不困难,需要注意的主要是在获取到网页源代码后如何通过正则表达式提取出所需信息。



Chap 4 解析库的使用

  • 通过上一章的爬虫实战发现,通过正则表达式提取页面信息不仅繁琐而且不易于维护,接下来介绍一些常用解析库,利用它们可以大幅提高提取页面信息的效率。

  • 使用 XPath

    XPath 提供了简洁明了的路径选择表达式以及大量内建函数,用于字符串、数值、时间的匹配以及节点、序列的处理等。表 4-1 on Page 159 展示了 XPath 的常用规则。看下面的例子:

    //title[@lang='eng']    # 选择所有名称为 title, 同时属性 lang 的值为 eng 的节点
    
  • **1. 使用 XPath 的实例 **

    from lxml import etree
    
    # 1. 声明一段结构不完整的 HTML 文本
    text = '''
    <div>
    <ul>
    <li class="item-0"><a href="link1.html">first item</a></li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-inactive"><a href="link3.html">third item</a></li>
    <li class="item-1"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a>
    </ul>
    </div>
    '''
    
    # 2. 创建一个 HTML 类的实例, 得到一个 XPath 解析对象
    html = etree.HTML(text)
    # 3. 通过 tostring() 方法修正(补全)了 HTML 代码, 结果为 bytes 类型
    result = etree.tostring(html)
    # 4. 利用 decode() 方法转换为 string 类型, 这里可以看到节点标签被补全了
    print(result.decode('utf-8'))
    

    下面是一些具体用法的例子:

    from lxml import etree
    
    text = '''
    <div>
    <ul>
    <li class="item-0"><a href="link1.html">first item</a></li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-inactive"><a href="link3.html">third item</a></li>
    <li class="item-1"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a>
    </ul>
    </div>
    '''
    
    html = etree.HTML(text)
    
    # 1. 所有节点
    # ----------------------------------------------------------------------------
    # 1.1 选取所有节点
    result = html.xpath("//*")    # 使用 * 代表匹配所有节点
    print(result)    # 返回一个列表, 其中元素都是 Element 类型
    
    # 1.2 选取所有 li 节点
    result = html.xpath("//li")    # // + 节点名
    print(result)
    
    
    
    # 2. 子节点 ( / + 节点名 用于获取直接子节点; // + 节点名 用于获取子节点 & 孙节点)
    # ----------------------------------------------------------------------------
    # 2.1 选取 li 节点的所有直接 a 子节点
    result = html.xpath("//li/a")    # / 用于获取直接子节点
    print(result)
    
    # 2.2 选取 ul 节点所有的子&孙节点中的 a 节点
    result = html.xpath("//ul//a")    # 这里改成 //ul/a 获取不到任何东西了, 因为 ul 没有直接 a 子节点
    print(result)
    
    
    
    # 3. 父节点 (从当前节点找父节点可通过 /.. or /parent::* 实现)
    # ----------------------------------------------------------------------------
    # 3.1 假设现在选中 href="link4.html" 的 a 节点, 想要获取其父节点的 class 属性
    result1 = html.xpath('//a[@href="link4.html"]/../@class')
    result2 = html.xpath('//a[@href="link4.html"]/parent::*/@class')
    print(result1, result2, sep="\t")
    
    
    
    # 4. 属性匹配 (使用 @ 进行属性过滤)
    # ----------------------------------------------------------------------------
    # 4.1 要选取 class 为 item-1 的 li 节点
    result = html.xpath('//li[@class="item-1"]')
    print(result)
    
    
    
    # 5. 文本获取 (使用 text() 方法获取节点中的文本)
    # ----------------------------------------------------------------------------
    # 5.1 要获取特定 li 节点中的文本(要注意文本是在 a 节点内部而非 li 节点内部, 因此需要先选取到 a 节点, 再获取其中文本)
    result = html.xpath('//li[@class="item-1"]/a/text()')
    print(result)
    
    
    
    # 6. 属性获取 (使用 @ + 属性名 获取属性值)
    # ----------------------------------------------------------------------------
    # 6.1 要获取所有 li 节点下的所有 a 节点的 href 属性
    result = html.xpath('//li/a/@href')
    print(result)
    
    
    
    # 7. 属性多值匹配
    # ----------------------------------------------------------------------------
    # 下面 li 节点的 class 属性有两个值: li & li-first
    text = '''<li class="li li-first"><a href="link.html">first item</a></li>'''
    html = etree.HTML(text)
    
    # 7.1 要选取 class 属性中含有 "li" 的 li 节点中的文本
    result1 = html.xpath('//li[@class="li"]/a/text()')
    print(result1)    # 这样匹配结果为空, 因为 class 属性不能匹配上
    
    # 7.2 上述需求可通过 contains(@属性名, 属性值) 进行匹配, 只要该属性包含传入的属性值, 就可以完成匹配
    result2 = html.xpath('//li[contains(@class, "li")]/a/text()')
    print(result2)
    
    
    
    # 8. 多属性匹配 (根据多个属性确定一个节点, 需要用到 and 运算符)
    # ----------------------------------------------------------------------------
    # 下面的 li 节点有 class 和 name 两个属性
    text = '''<li class="li li-first" name="item"><a href="link.html">1st item</a></li>'''
    html = etree.HTML(text)
    
    # 8.1 为了选取上面的 li 节点, 需要使用 and 连接两个过滤条件
    result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')
    print(result)
    # Remark: 这里的 and 实际上是 XPath 中的运算符, 表 4-2 on Page 165 介绍了更多运算符
    
    
    
    # 9. 按序选择 (在 [] 内传入索引以选择特定次序的节点)
    # ----------------------------------------------------------------------------
    text = '''
    <div>
    <ul>
    <li class="item-0"><a href="link1.html">first item</a></li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-inactive"><a href="link3.html">third item</a></li>
    <li class="item-1"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a>
    </ul>
    </div>
    '''
    html = etree.HTML(text)
    
    # 9.1 第一个 li 节点的子节点 a 内的文本
    result = html.xpath('//li[1]/a/text()')
    print(result)
    
    # 9.2 最后一个 li 节点的子节点 a 内的文本
    result = html.xpath('//li[last()]/a/text()')
    print(result)
    
    # 9.3 前两个 li 节点的子节点 a 中的文本
    result = html.xpath('//li[position() < 3]/a/text()')
    print(result)
    
    # 9.4 倒数第三个 li 节点的子节点 a 中的文本
    result = html.xpath('//li[last() - 2]/a/text()')
    print(result)
    
    
    
    # 10. 节点轴选择
    # ----------------------------------------------------------------------------
    # 10.1 第一个 li 节点的所有祖先节点
    result = html.xpath('//li[1]/ancestor::*')    
    # ancestor 轴可获取所有祖先节点 (:: 后跟节点选择器, * 表示匹配所有节点)
    print(result)
    
    # 10.2 第一个 li 节点的祖先节点 div
    result = html.xpath('//li[1]/ancestor::div')
    print(result)
    
    # 10.3 第一个 li 节点的所有属性值
    result = html.xpath('//li[1]/attribute::*')    
    # attribute 轴可获取所有属性值
    print(result)
    
    # 10.4 第一个 li 节点的 href 属性为 link1.html 的直接子节点 a 中的文本
    result = html.xpath('//li[1]/child::a[@href="link1.html"]/text()')    
    # child 轴可获取所有直接子节点
    print(result)
    
    # 10.5 ul 节点的子孙节点中的第三个 a 节点的文本
    result = html.xpath('//ul/descendant::a[3]/text()')    
    # descendant 轴可获取所有子节点 & 孙节点
    print(result)
    
    # 10.6 第一个 li 节点后的所有节点当中的第二个节点 (也即第二个 li 节点的子节点 a) 的文本
    result = html.xpath('//li[1]/following::*[2]/text()')    
    # following 轴可获取当前节点后的所有节点(包括子, 孙节点)
    print(result)
    
    # 10.7 第一个 li 节点后的所有同级节点 (也即第 2~4 个 li 节点)
    result = html.xpath('//li[1]/following-sibling::*')    
    # following-sibling 轴可获取当前节点后的所有同级节点
    print(result)
    

    XPath 的更多用法可以参考:https://www.w3school.com.cn/xpath/index.asp

  • 使用Beautiful Soup

    Beautiful Soup 可借助网页的结构和属性等特性来解析网页,它是 Python 的一个高效的 HTML 或 XML 的解析库。Beautiful Soup 在解析时实际上依赖解析器,表 4-3 on Page 169 给出了其支持的解析器,这里推荐使用 lxml 解析器。

    from bs4 import BeautifulSoup
    
    soup = BeautifulSoup('<p>Hello</p>', 'lxml')    # 参数1: 待解析的 HTML 文档; 参数2: 指定解析器
    print(soup.p.string)    # p 节点中的字符串
    

    下面提供了更多实例:

    from bs4 import BeautifulSoup
    
    # 1. 实例基础
    # ----------------------------------------------------------------------------
    
    # 1.1 首先声明了一个不完整的 HTML 字符串 (节点没有闭合)
    html = '''
    <html><head><title>The Dormouse's story</title></head>
    <body>
    <p class="title" name="dromouse"><b>The Dormouse's story</b></p>
    <p class="story">Once upon a time there were 3 little sisters; and their names were
    <a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>
    <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and 
    <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
    and they lived at the bottom of a well.</p>
    <p class="story">...</p>
    '''
    
    # 1.2 实例化一个 BeautifulSoup 对象, 参数分别是 HTML 字符串和解析器类型 
    # (实例化时 HTML 字符串被自动补齐)
    soup = BeautifulSoup(html, 'lxml')
    # 1.3 调用 prettify() 方法可将要解析的字符串以标准的缩进格式输出
    print(soup.prettify())
    # 1.4 输出 HTML 中 title 节点的文本内容
    print(soup.title.string)
    
    
    # 2. 节点选择器
    # ----------------------------------------------------------------------------
    
    # 2.1 选择元素
    print(soup.title)    # 选择 title 节点
    print(type(soup.title))    # title 节点为 Tag 类型
    print(soup.title.string)    # title 节点中的文本
    print(soup.head)    # 选择 head 节点
    print(soup.p)    # 选择 p 节点 (只匹配了首个 p 节点)
    
    # 2.2 提取信息
    print(soup.title.name)    # 通过 name 获取节点名称
    print(soup.p.attrs)    # 通过 attrs 获取节点所有属性 (字典形式)
    print(soup.p.attrs['name'])    # 指定属性名以获取属性值
    print(soup.p.string)    # 通过 string 获取节点中包含的文本 (这里匹配到的只有首个 p 节点)
    
    # 2.3 嵌套选择
    print(soup.head.title)    # 先调用 head 后再调用 title 以选择 title 节点元素
    print(type(soup.head.title))    # 在 Tag 类型的基础上再次选择得到的依然是 Tag 类型
    print(soup.head.title.string)
    
    # 2.4 关联选择 --- 这一部分结果比较混乱, 请直接查阅书本
    
    
    # 3. 方法选择器
    # ----------------------------------------------------------------------------
    
    # 3.1 find() 返回首个匹配的元素
    html = '''
    <div class="panel">
    <div class="panel-heading">
    <h4>Hello</h4>
    </div>
    <div class="panel-body">
    <ul class="list" id="list-1">
    <li class="element">Foo</li>
    <li class="element">Bar</li>
    <li class="element">Jay</li>
    </ul>
    <ul class="list list-small" id="list-2">
    <li class="element">Foo</li>
    <li class="element">Bar</li>
    </ul>
    </div>
    </div>
    '''
    
    soup = BeautifulSoup(html, 'lxml')
    print(soup.find(name="ul"))    # 查询首个 name=ul の节点
    print(type(soup.find(name="ul")))    # 匹配到的节点依然是 Tag 类型
    print(soup.find(attrs={'class': 'list'}))    # 返回首个 class = list 的节点
    
    
    # 3.2 find_all() 返回所有匹配的元素组成的列表
    # ---------------------------------------------------------------------------------
    
    # 3.2.1 通过 name 参数查询所有名为 ul の节点, 返回结果为列表
    print(soup.find_all(name='ul'))    
    print(type(soup.find_all(name="ul")[0]))    # 列表中每个元素依然是 Tag 类型
    
    # 3.2.2 通过 attrs 参数查询所有 id=list-1 の节点, 返回结果为列表
    print(soup.find_all(attrs={'id': 'list-1'}))    
    
    # 3.2.3 嵌套查询并通过 string 获取文本
    for ul in soup.find_all(name="ul"):
        print(ul.find_all(name="li"))
        for li in ul.find_all(name="li"):
            print(li.string)
    
    import re
    html = '''
    <div class="panel">
    <div class="panel-body">
    <a>Hello, this is a link</a>
    <a>Hello, this is a link, 2</a>
    <a>link</a>
    </div>
    </div>
    '''
    
    soup = BeautifulSoup(html, 'lxml')
    # 3.2.4 通过 text 参数查询节点文本 (可传入 string 或者 Regex 对象)
    print(soup.find_all(text='link'))    # 只能匹配到第三个 a 节点中的文本
    print(soup.find_all(text=re.compile('link')))    # 可以匹配到全部 a 节点中的文本
    
    """ ======================== Remark: ========================
        还有许多查询方法, 其用法与 find() & find_all() 完全相同, 只是查询范围不同:
        1. find_parents() 和 find_parent(): 前者返回所有祖先节点, 后者返回直接父节点。 
        2. find_next_siblings() 和 find_next_sibling(): 前者返回后面所有的兄弟节点,后者返回后面第一个兄弟节点。
        3. find_previous_siblings() 和 find_previous_sibling(): 前者返回前面所有的兄弟节点, 后者返回前面第一个兄弟节点。
        4. find_all_next() 和 find_next(): 前者返回 节点后 所有符合条件的节点, 后者返回第一个符合条件的节点。 
        5. find_all_previous() 和 find_previous(): 前者返回 节点前 所有符合条件的节点, 后者返回第一个符合条件的节点。
    """
    
    
    # 4. CSS选择器 (参考链接 http://www.w3school.com.cn/cssref/css_selectors.asp)
    # ----------------------------------------------------------------------------
    
    html = '''
    <div class="panel">
    <div class="panel-heading">
    <h4>Hello</h4>
    </div>
    <div class="panel-body">
    <ul class="list" id="list-1">
    <li class="element">Foo</li>
    <li class="element">Bar</li>
    <li class="element">Jay</li>
    </ul>
    <ul class="list list-small" id="list-2">
    <li class="element">Foo</li>
    <li class="element">Bar</li>
    </ul>
    </div>
    </div>
    '''
    
    soup = BeautifulSoup(html, 'lxml')
    # 4.1 使用 css 选择器时, 只需要调用 select() 方法, 传人相应的 css 选择器即可
    print(soup.select('.panel .panel-heading'))
    print(soup.select('ul li'))    # 选择所有 ul 节点下所有の li 节点
    print(soup.select('#list-2 .element'))
    print(type(soup.select('ul')[0]))    # 依然是 Tag 类型
    
    # 4.2 嵌套选择
    for ul in soup.select('ul'):
        print(ul.select('li'))
    
    # 4.3 获取属性
    for ul in soup.select('ul'):
        print(ul.attrs['id'])
    
    # 4.4 获取文本
    for li in soup.select('li'):
        print(li.string)
        print(li.get_text())    # get_text() 同样可以获取节点内文本
    
  • 使用 pyquery

    如果你比较喜欢用 css 选择器,对 jQuery 有所了解,那么这里有一个更适合你的解析库 —— pyquery。

    from pyquery import PyQuery as pq
    
    # 1. 初始化
    # ----------------------------------------------------------------------------
    
    # 1.1 字符串初始化 (最常用)
    html = '''
    <div>
    <ul>
    <li class="item-0">first item</li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1 active"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    </div>
    '''
    
    doc = pq(html)    # 将 HTML 字符串初始化为 PyQuery 对象
    print(doc('li'))    # 传入 CSS 选择器以选择节点
    
    # 1.2 URL 初始化
    # 初始化的参数不仅可以以字符串的形式传递, 还可以传入网页的 URL: PyQuery 对象会首先请求此 URL, 然后用得到的 HTML 内容完成初始化, 这相当于用网页源代码以字符串的形式传递给 PyQuery 类来初始化
    doc = pq(url="https://cuiqingcai.com")
    print(doc('title'))
    
    # 1.3 文件初始化
    # 这里首先需要有一个本地 HTML 文件 demo.html, 其内容是待解析的 HTML 字符串。这样它会首先读取本地的文件内容, 然后用文件内容以字符串形式传递给 PyQuery 类来初始化。
    doc = pq(filename='demo.html')
    print(doc('li'))
    
    
    
    # 2. 基本 CSS 选择器
    # ----------------------------------------------------------------------------
    
    html = '''
    <div id="container">
    <ul class="list">
    <li class="item-0">first item</li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1 active"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    </div>
    '''
    doc = pq(html)
    # 2.1 选取 id 为 container 的节点, 再选取其内部的 class 为 list 的节点内的所有 li 节点
    print(doc('#container .list li'))
    print(type(doc('#container .list li')))    # 返回结果为 PyQuery 类型
    
    
    
    # 3. 节点查找
    # ----------------------------------------------------------------------------
    
    # 3.1 子节点: find & children
    items = doc('.list')    # 先选取 class 为 list 的节点
    print(type(items))
    print(items)
    lis = items.find('li')    # 然后调用 find() 方法, 传入 CSS 选择器获取其内部的 li 节点
    print(type(lis))    # 结果均为 PyQuery 类型
    print(lis)
    """
    Remark: 其实 find() 的查找范围是节点的所有子孙节点, 若只想查找子节点, 那么可以用 children() 方法
    """
    
    lis = items.children()    # 调用 children() 方法获取子节点
    print(type(lis))
    print(lis)
    
    lis = items.children('.active')    # 向 children() 中传入 CSS 选择器以实现筛选
    print(lis)
    
    # 3.2 父节点: parent & parents
    html = '''
    <div class="wrap">
    <div id="container">
    <ul class="list">
    <li class="item-0">first item</li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1 active"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    </div>
    </div>
    '''
    doc = pq(html)
    items = doc('.list')    # 首先选取 class 为 list 的节点
    container = items.parent()    # 然后调用 parent() 方法获取其 “直接父节点”
    print(type(container))    # 依然是 PyQuery 类型
    print(container)
    
    parents = items.parents()    # parents() 方法可获取 所有 祖先节点
    print(type(parents))
    print(parents)
    
    parent = items.parents('.wrap')    # 向 parents() 中传入 CSS 选择器以实现筛选
    print(parent)
    
    # 3.3 兄弟节点: siblings
    # 选择 class 为 list 的节点内 class 为 item-0 和 active 的节点
    li = doc('.list .item-0.active')
    print(li.siblings())    # siblings() 方法可获取所有兄弟节点
    print(li.siblings('.active'))    # 向 siblings() 中传入 CSS 选择器以实现筛选
    
    
    
    # 4. 遍历
    # ----------------------------------------------------------------------------
    
    # 4.1 沿用上面的 doc, 当选择结果为单个节点时, 可直接打印 or 转为 String
    li = doc('.item-0.active')
    print(li, str(li))
    
    # 4.2 当选择结果为多个节点时, 对选择结果调用 items() 方法后得到一个生成器
    lis = doc('li').items()    
    print(type(lis))
    
    # 4.3 遍历得到每个 li 节点对象, 其类型均为 PyQuery
    for li in lis:
        print(li, type(li))
    
    
    
    # 5. 获取信息
    # ----------------------------------------------------------------------------
    
    # 5.1 获取属性: attr
    html = '''
    <div class="wrap">
    <div id="container">
    <ul class="list">
    <li class="item-0">first item</li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1 active"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    </div>
    </div>
    '''
    doc = pq(html)
    
    # 5.1.1 首先选中 class 为 item-0 & active 的 li 节点内的 a 节点
    a = doc('.item-0.active a')
    print(a, type(a))
    
    # 5.1.2 然后调用 attr() 方法, 传入相应属性名以获取属性值
    print(a.attr('href'))
    
    # 5.1.3 选中了 4 个 a 节点
    a = doc('a')
    print(a, type(a), sep="\n")
    
    # 5.1.4 当返回结果包含多个节点时, 调用 attr() 方法只能得到 “首个” 节点的属性!
    print(a.attr('href'))
    
    # 5.1.5 若想获取所有 a 节点の属性, 可通过遍历每个节点来实现
    for item in a.items():
        print(item.attr('href'))
    
    # 5.2 获取文本: text & html
    # 5.2.1 首先选中 class 为 item-0 & active 的 li 节点内的 a 节点
    a = doc('.item-0.active a')
    print(a)
    
    # 5.2.2 然后调用 text() 方法获取节点内的文本 (不含 HTML 代码)
    print(a.text())
    
    # 5.2.3 首先选中第三个 li 节点
    li = doc('.item-0.active')
    print(li)
    
    # 5.2.4 然后调用 html() 方法获取 “节点内” 所有 html 代码
    print(li.html())
    
    # 5.2.5 当选择结果为多个节点时:
    li = doc('li')
    print(li.html())    # html() 返回 “首个” 节点内的 html 代码
    print(li.text())    # text() 以 String 形式返回所有节点内的文本
    
    
    
    """ 以下内容用到再查 """
    # 6. 节点操作 (主要用于修改 html 文本)
    # ----------------------------------------------------------------------------
    # 6.1 addClass 和 removeClass: 改变节点 class 属性 
    # 6.2 attr, text 和 html: 改变属性, 文本和 html 代码
    # 6.3 remove(): 删除节点
    # 还有很多节点操作方法, 可自行查阅
    
    
    
    # 7. 伪类选择器 (提供了许多其他灵活地选取节点的方法)
    # ----------------------------------------------------------------------------
    


Chap 5 数据存储



  • 文件存储

    1. TXT 文本存储

    将数据保存到 TXT 文本的操作非常简单,且 TXT 文本几乎兼容任何平台;缺点是不利于检索;
    若对检索和数据结构要求不高,追求方便的话,可采用 TXT 文本存储

    # 就是 with open 的那些内容, 基础, 不赘述
    # open 方法的参数: r, w, wb, a 等的差别可查阅书 Page 198
    

    2. Json 文件存储

    Json 通过对象和数组的组合来表示数据,构造简洁但结构化程度非常高,是一种轻量级的数据交换格式。

    import json
    
    # 2.1 读取 Json
    # ------------------------------------------------------------------
    str = '''
    [
        {
            "name": "Bob",
            "gender": "male",
            "birthday": "1992-10-18"
        },
        {
            "name": "Selina",
            "gender": "female",
            "birthday": "1995-10-18"
        }
    ]
    '''    # Remark: Json 中字符串的表示必须用双引号, 否则 loads() 方法会解析失败。
    
    # 2.1.1 使用 loads() 方法将 str 转为 JSON 对象
    data = json.loads(str)    
    print(data)
    print(type(data))    # 由于最外层是 [] 故最终类型是 list
    
    # 2.1.2 下面是两种获取键值的方式, 推荐 get() 方法, 因为若键名不存在则会返回 None 而不报错
    print(data[0]['name'], data[0].get('name'))
    
    # 2.1.3 get() 方法还可为 key 传入默认 value
    print(data[0].get('age'), data[0].get('age', 25))
    
    # 2.2 输出 Json
    # ------------------------------------------------------------------
    data = [
                {
                    "name": "Bob",
                    "gender": "male",
                    "birthday": "1992-10-18",
                    "height": "两米八"
                }
            ]
    
    # 2.2.1 调用 dumps() 方法将 Json 对象转化为 str 
    # 其中可选参数 indent 用于设置缩进, encoding & ensure_ascii 目的在于正常输出中文
    with open('data.json', 'w', encoding='utf-8') as f:
        f.write(json.dumps(data, indent=2, ensure_ascii=False))
    

    3. CSV 文件存储

    import csv
    
    # 3.1 写入 csv
    # ------------------------------------------------------------------
    with open('data.csv', 'w') as csvfile:
        # 3.1.1 初始化写入对象, delimiter 指定分隔符
        writer = csv.writer(csvfile, delimiter='>')
        # 3.1.2 调用 writerow() 方法写入每行数据
        writer.writerow(['id', 'name', 'age'])
        writer.writerow(['1001', 'Mike', 20])
        writer.writerow(['1002', 'Bob', 22])
        writer.writerow(['1003', 'Jake', 21])
        # 3.1.3 调用 writerows() 方法写入多行数据 (参数应是二维列表)
        writer.writerows([['1004', 'Jay', 21], ['1005', 'Jason', 24]])
        
    # 下面是 csv 中提供的字典的写入方式:
    with open('data_dict.csv', 'w', encoding='utf-8') as csvfile:
        # 3.1.4 定义字段
        fieldnames = ['id', 'name', 'age']
        # 3.1.5 初始化字典写入对象
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        # 3.1.6 写入头信息 (就是字段名那一行)
        writer.writeheader()
        # 3.1.7 调用 writerow() 写入字典即可
        writer.writerow({'id': 1001, 'name': 'Mike', 'age': 20})
        writer.writerow({'id': 1002, 'name': 'Bob', 'age': 22})
        writer.writerow({'id': 1003, 'name': '乔丹', 'age': 21})
    
    # 3.2 读取 csv
    # ------------------------------------------------------------------
    with open('data.csv', 'r', encoding='utf-8') as csvfile:
        # 3.2.1 初始化 reader 对象
        reader = csv.reader(csvfile)
        for row in reader:
            print(row)    # 为什么 csv 文件里会有多余的空行呢?
    
  • 关系型数据库存储(MySQL)

    用到再查

  • 非关系型数据库存储(MongoDB & Redis)

    用到再查



Chap 6 Ajax 数据爬取

  • 有时在浏览器中可以看到正常显示的页面数据,使用 requests 得到的结果中却没有。这是因为 requests 获取的 都是原始 HTML 文档,而浏览器中的页面则是经过 JavaScript 处理数据后生成的结果,这些数据的来源有多种,可能是通过 Ajax 加载的,可能是包含在 HTML 文档中的,也可能是经过 JavaScript 和特定算法计算后生成的。

    对于第一种情况,数据加载是一种异步加载方式,原始的页面最初不会包含某些数据,原始页面加载完后,会再向服务器请求某个接口获取数据,然后数据才被处理从而呈现到网页上,这其实就是发送了一个 Ajax 请求。

    如果遇到这样的页面,直接利用 requests 等库来抓取原始页面,是无法获取有效数据的,这时需要分析网页后台向接口发送的 Ajax 请求,如果可以用 requests 来模拟 Ajax 请求,那么就可以成功抓取了。

  • Ajax,即 异步的 JavaScript 和 XML 是利用 JavaScript 在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。比如微博页面 https://m.weibo.cn/u/2830678474,就采用了这样的技术实现不断加载新的微博的功能。

  • 基本原理:

    发送 Ajax 请求到网页更新的这个过程可简单分为以下 3 步:
    ① 发送请求;② 解析内容;③ 渲染网页 (详见书 Page 233)

  • Ajax 分析方法

    以微博页面为例,用 Chrome 打开 https://m.weibo.cn/u/2830678474 。F12 进入开发者工具,切换到 Network 选项卡后 Ctrl + R 刷新页面,筛选出 XHR 类型的请求,用鼠标点击这样的一个请求,即可在 Headers 标签下查看该请求的详细信息,其中 Request Headers 中有一个信息为 X-Requested-With: XMLHttpRequest,这就标记了此请求是 Ajax 请求。

    在这里插入图片描述

    随后点击一下 Preview 标签即可看到响应的内容预览,它是 Json 格式的,这里 Chrome 自动做好了解析。

    另外,也可以切换到 Response 标签,从中观察到真实的返回数据。

    随着微博页面不断下拉,开发者工具下也会出现一个个 Ajax 请求。随意点开一个条目,都可以看到其 Request URL、Request Headers、Response Headers、Response Body 等内容,接下来就可以模拟请求和提取数据了。

    在这里插入图片描述

  • Ajax 结果提取

    仍以上面的微博页面为例,下面尝试爬取这些微博。

    ① 首先选定第一个 Ajax 请求,分析其参数。显然这是一个 GET 请求,链接为 https://m.weibo.cn/api/container/getIndex?type=uid&value=2830678474&containerid=1005052830678474 ,其中包含了 type,value,containerid 这三个参数。

    ② 再看第二个请求 https://m.weibo.cn/api/container/getIndex?type=uid&value=2830678474&containerid=1076032830678474&since_id=4527282414685304 ,发现多了一个参数 since_id,而其余参数值不变。又注意到前一个 Ajax 请求的响应中包含了 since_id: 4527282414685304,这正是第二个 Ajax 请求中的新参数,借助这些信息我们可以构造出这类 Ajax 请求的 url。

    ③ 进一步观察这些请求的响应内容,发现其中就包含了想要爬取的微博信息,比如点赞数,转发数,评论数,发布时间和微博正文等。

    ④ 下面就通过 requests 来模拟 Ajax 请求以爬取这些微博信息:

    import time
    import requests
    from pyquery import PyQuery as pq
    from urllib.parse import urlencode
    
    base_url = "https://m.weibo.cn/api/container/getIndex?"
    
    headers = {
        "Host": "m.weibo.cn",
        "Referer": "https://m.weibo.cn/u/2830678474",
        "User-Agent": 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36',
        "X-Requested-With": 'XMLHttpRequest',
    }    # 怎么确定 headers 中应该包含那些信息呢?
    
    
    def get_page(since_id):
        ''' 第一次的 ajax 请求中没有 since_id, 但其返回的 json 中含有 since_id, 且该 since_id 正是下一个 ajax 请求中的参数, 之后的每个 ajax 中都含有 since_id, 并且是下个 ajax 请求中的参数 '''
        params = {
            'type': 'uid',
            'value': '2830678474',
            'containerid': '1076032830678474',
        }
            if since_id != 0:
            params['since_id'] = since_id
    
        # 1. 通过 urlencode() 函数从 params 字典构造完整的 ajax 请求の url
        url = base_url + urlencode(params)
        
        try: 
            # 2. 模拟 ajax 请求
            response = requests.get(url=url, headers=headers)
            if response.status_code == 200:
                # 3. 将内容解析为 json 后返回
                return response.json()
        except requests.ConnectionError as e:
            print('ERROR', e.args)
    
    
    def parse_page(json):
        if json:
            # 4. 从返回的 json 中提取微博信息并写入字典
            items = json.get('data').get('cards')
            for item in items:
                item = item.get('mblog')
                weibo = {}
                weibo['id'] = item.get('id')
                # 借助 pyquery 取微博正文并去掉 html 标签
                weibo['text'] = pq(item.get('text')).text()
                weibo['attitudes'] = item.get('attitudes_count')
                weibo['comments'] = item.get('comments_count')
                weibo['reposts'] = item.get('reposts_count')
                yield weibo
    
    
    if __name__ == '__main__':
        # 初始化 since_id 以使首个 ajax 请求不带该参数
        since_id = 0
    
        # 仅遍历部分页面做展示
        for i in range(0, 10):
            # 1. 发送请求
            json = get_page(since_id)
            # 2. 更新 since_id
            since_id = json.get('data').get('cardlistInfo').get('since_id')
            # 3. 解析 json 获取数据
            results = parse_page(json)
            # 4. 遍历展示数据
            for res in results:
                print(res)
            # 5. 等待
            time.sleep(2)
    
  • 6.4 节实战内容由于网页已经有了更新,代码也已经过时就不再展示了,可以用于自己练手,参考:https://github.com/Python3WebSpider/Jiepai

    唯一一个想说明的要点:今日头条当中搜索 “城堡” 得到的页面中,查看其 Ajax 请求の url 具有下面的形式:https://www.toutiao.com/api/search/content/?aid=24&app_name=web_search&offset=40&format=json&keyword=%E5%9F%8E%E5%A0%A1&autoload=true&count=20&en_qc=1&cur_tab=1&from=search_tab&pd=synthesis&timestamp=1595924946127&_signature=tiK1SAAgEBBd9MDzb0G-2bYj9FAAOlLdVntowpzBe3gl3P-xVS-AzOF.l04QkLYsVHmSZGE7D-OhvrqnEH11dNBEDaQTkc.ZLysXMTgw0.iSFHsXJXjEda2guPBjacpK9xL

    其中有很多参数,并且除了 offset,timestamp 和 _signature 之外都是不变的。乍一看这个 url 很复杂不知如何模拟,但事实上,最后两个参数 timestamp 和 _signature 是不起作用的,也就是说删掉它们后请求同样得到相同的响应,这两个参数更像是某种 “标记”,因此在模拟的时候只要堪虑其他参数来构造 url 即可。



Chap 7 动态渲染页面爬取

  • JavaScript 动态渲染的页面不止 Ajax 这一种,有时网页中不含 Ajax 请求,有时 Ajax 接口含有很多加密参数,难以直接找出其规律。为解决这些问题,可直接使用 模拟浏览器运行 的方式来爬取数据,这样就可做到在浏览器 中看到是什么样,抓取的源码就是什么样,也就是 可见即可爬

  • Selenium 的使用

    Selenium 是一个自动化测试工具,利用它可以驱动浏览器执行特定的动作(如点击、下拉等), 同时还可获取浏览器当前呈现的页面的源代码,对于一些 JavaScript 动态渲染的页面,此种抓取方式非常有效。

    7.1 先看一个简单的例子:

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.support.wait import WebDriverWait
    
    browser = webdriver.Chrome()
    try:
        browser.get('https://www.baidu.com')    # 打开百度
        input = browser.find_element_by_id('kw')
        input.send_keys('齐木楠雄')    # 搜索"齐木楠雄"
        input.send_keys(Keys.ENTER)
        wait = WebDriverWait(browser, 10)
        wait.until(EC.presence_of_element_located((By.ID, 'content_left')))
        print(browser.current_url)      # 获取当前 url
        print(browser.get_cookies())    # 获取 cookies
        print(browser.page_source)      # 获取 (浏览器中真实的) 网页源代码
    finally:
        browser.close()
    

    可以看到,这里得到的当前 URL,Cookies 和源代码都是浏览器中的真实内容。 可见用 Selenium 来驱动浏览器加载网页,可以直接拿到 JavaScript 渲染的结果,不用担心使用的是什么加密系统。

  • 接下来详细介绍一下 Selenium 的用法,以 Chrome 为例。

    7.2 访问页面:

    from selenium import webdriver
    
    # 1. 实例化一个 Chrome 浏览器对象
    browser = webdriver.Chrome()
    # 2. 调用 browser 对象的 get() 方法请求网页
    browser.get('https://www.taobao.com')
    # 3. 获取页面源代码并打印
    html = browser.page_source
    print(html)
    # 4. 关闭浏览器
    browser.close()
    

    7.3 查找节点:

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    
    browser = webdriver.Chrome()
    browser.get('https://www.taobao.com')
    
    # 1. 查找单个节点
    # -----------------------------------------------------------------------------------------
    
    # 1.1 按照 id 查找节点
    input_first = browser.find_element_by_id('q')
    # 1.2 通过 css 选择器查找 id = 'q' 的节点
    input_second = browser.find_element_by_css_selector('#q')
    # 1.3 通过 XPath 查找节点
    input_third = browser.find_element_by_xpath('//input[@id="q"]')
    
    # 1.4 可见 3 个节点都是 WebElement 类型且完全一致
    print(input_first, input_second, input_third, sep="\n\n")
    
    # 1.5 另外还有通用方法 find_element(By.查找方式, 值)
    input_fourth = browser.find_element(By.ID, 'q')
    print(input_fourth)
    
    # 下面列出所有获取单个节点的方法:
    # find_element_by_id
    # find_element_by_name
    # find_element_by_xpath
    # find_element_by_link_text
    # find_element_by_partial_link_text
    # find_element_by_tag_name
    # find_element_by_class_name
    # find_element_by_css_selector
    
    
    
    # 2. 查找多个节点
    # -----------------------------------------------------------------------------------------
    # 2.1 若要查找的节点有多个的话, 上面的方法只能返回第一个匹配的结果, 想获得所有满足条件的节点, 应使用 find_elements() 这样的方法
    lis = browser.find_elements_by_css_selector('.service-bd li')
    print(lis)     # 返回了一个元素类型为 WebElement 的列表
    
    # 2.2 另外 find_elements(By.查找方式, 值) 方法也用于获取多个节点
    lis = browser.find_elements(By.CSS_SELECTOR, '.service-bd li')
    print(lis)
    
    # 下面列出所有获取多个节点的方法:
    # find_elements_by_id
    # find_elements_by_name
    # find_elements_by_xpath
    # find_elements_by_link_text
    # find_elements_by_partial_link_text
    # find_elements_by_tag_name
    # find_elements_by_class_name
    # find_elements_by_css_selector
    
    browser.close()
    

    也就是说,如果我们用 find_element() 方法,只能获取匹配的第一个节点,结果是 WebElement 类型。如果用 find_elements() 方法,则结果是列表类型,列表中的每个节点是 WebElement 类型。

    7.4 节点交互:

    Selenium 可以驱动浏览器来执行一些操作,即可以让浏览器模拟执行一些动作。常见的用法有:输入文字时用 send_keys 方法,清空文字时用 clear 方法,点击按钮时用 click 方法等。

    import time
    from selenium import webdriver
    
    browser = webdriver.Chrome()
    # 1. 浏览器窗口最大化
    browser.maximize_window()
    browser.get('https://www.bing.com')
    # 2. 通过 find_element_by_id() 获取输入框
    input = browser.find_element_by_id('sb_form_q')
    # 3. 通过 send_keys() 输入文字
    input.send_keys('深绘里')
    time.sleep(1)
    # 4. 通过 clear() 清空输入框
    input.clear()
    input.send_keys('深田绘里子')
    # 5. 通过 find_element_by_class_name() 获取搜索按钮
    button = browser.find_element_by_class_name('b_searchboxSubmit')
    # 6. 通过 click() 点击按钮, 这样就完成了搜索动作。
    button.click()
    

    更多节点交互动作参考:http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.remote.webelement

    7.5 动作链:

    有一些操作,它们没有特定的执行对象(如上面的输入框和搜索按钮),比如鼠标拖曳,键盘按键等,这些动作用另一种方式来执行,那就是动作链。

    from selenium import webdriver
    from selenium.webdriver import ActionChains
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    url = 'http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
    browser.get(url)
    
    # 下面两行代码效果相同, 但前一行不再推荐使用 (Deprecated)
    # browser.switch_to_frame('iframeResult')
    browser.switch_to.frame('iframeResult')
    
    # 1. 获取要拖拽的节点
    source = browser.find_element_by_css_selector('#draggable')
    # 2. 获取拖拽到的目标节点
    target = browser.find_element_by_css_selector('#droppable')
    # 3. 实例化 ActionChains 对象
    actions = ActionChains(browser)
    # 4. 调用 drag_and_drop() 方法指定拖拽的目标和终点
    actions.drag_and_drop(source, target)
    # 5. 调用 perform() 执行操作
    actions.perform()
    

    更多动作链的内容参考:http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.action_chains

    7.6 执行 JavaScript:

    在 Selenium 中可通过调用 execute_script() 方法执行 JavaScript

    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    browser.get('https://www.zhihu.com/explore')
    # 1. 将网页下拉到最底部
    browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
    # 2. 弹出提示框
    browser.execute_script('alert("To Bottom")')
    

    7.7 获取节点信息:

    Selenium 提供了选择节点的方法,返回的是 WebElement 类型,它也有相关的方法和属性来直接提取节点信息,如属性、文本等。这样就可以不用通过解析源代码来提取信息了。

    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    url = 'https://www.bilibili.com/'
    browser.get(url)
    
    # 1. 获取属性
    # ------------------------------------------------------------------
    logo = browser.find_element_by_css_selector('.head-logo img')
    
    # 1.1 通过 get_attribute('属性名') 方法获取节点の属性值
    print(logo.get_attribute('class'))
    
    
    # 2. 获取文本
    # ------------------------------------------------------------------
    front = browser.find_element_by_css_selector('.text-info-link')
    
    # 2.1 调用节点的 text 属性获取节点内部文本信息
    print(front.text)
    
    
    # 3. 获取其他常用属性
    # ------------------------------------------------------------------
    slide = browser.find_element_by_css_selector('.space-between.report-wrap-module.report-scroll-module')
    
    # 3.1 id 属性可获取节点 id
    print(slide.id)
    
    # 3.2 location 属性可获取节点在页面中的相对位置
    print(slide.location)
    
    # 3.3 tag_name 属性可获取标签名称
    print(slide.tag_name)
    
    # 3.4 size 属性可获取节点大小(即宽高)
    print(slide.size)
    
    browser.close()
    

    7.8 切换 Frame:

    网页中有一种节点叫作 iframe,即 子Frame,相当于页面的子页面,其结构与外部网页的结构完全一致。 Selenium 打开页面后,默认在父级 Frame 里操作,而此时若页面中还有 子Frame,它是不能获取到 子Frame 里的节点的。这时就需要使用 switch_to.frame() 方法来切换 Frame。

    from selenium import webdriver
    from selenium.common.exceptions import NoSuchElementException
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    url = 'http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
    browser.get(url)
    
    # 1. 首先通过 switch_to.frame() 切换到 子Frame 中
    browser.switch_to.frame('iframeResult')    # 这里是什么选择器呢?为什么用 id 指定?
    
    try:
        # 2. 尝试获取 父级Frame 中的 logo 节点 (which can not be done)
        browser.find_element_by_class_name('logo')
    except NoSuchElementException as e:
        print('No Logo')
    
    # 3. 重新切换回 父级Frame
    browser.switch_to.parent_frame()
    # 4. 再次获取 logo 节点
    logo = browser.find_element_by_class_name('logo')
    # 5. 可见这里获取成功了
    print(logo, logo.text)
    
    browser.close()
    

    所以,当页面中包含 子Frame 时,若想获取子 Frame 中的节点,需要先调用 switch_to.frame() 方法切换到对应的 Frame,然后再进行操作。

    7.9 延时等待:

    在 Selenium 中,get() 方法会在 网页框架加载结束后 结束执行,此时若获取 page_source,可能并不是浏览器完全加载完成的页面,如果某些页面有额外的 Ajax 请求,在网页源代码中也不一定能成功获取到。所以,这里需要延时等待一定的时间,确保节点已经加载出来。

    等待的方式有两种:1. 隐式等待 2. 显式等待

    from selenium import webdriver
    
    # 1. 隐式等待
    # ----------------------------------------------------------------
    browser = webdriver.Chrome()
    browser.maximize_window()
    
    # 1.1 隐式等待: 当查找的节点没有立即出现时, 等待 10s 后再查找, 超出 10s 仍找不到则抛出异常
    browser.implicitly_wait(10)
    
    url = 'https://www.bilibili.com/'
    browser.get(url)
    target = browser.find_element_by_css_selector('.name.no-link')
    print(target, target.text)
    browser.close()
    
    # Remark: 隐式等待的效果其实不算好, 因为只规定了一个固定时间, 而页面加载时间会受到网络条件的影响。
    
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    
    # 2. 显式等待
    # ----------------------------------------------------------------
    """
    还有一种更合适的显式等待方法, 它指定要查找的节点, 然后指定一个最长等待时间。
        如果在规定时间内加载出了这个节点, 就返回查找的节点;
        如果到了规定时间依然没有加载出该节点, 则抛出超时异常。
    """
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    browser.get('https://www.taobao.com/')
    
    # 1. 实例化 WebDriverWait() 对象, 指定最长等待时间为 10s
    wait = WebDriverWait(browser, 10)
    
    # 2. 调用 until() 方法传入要等待的条件
    # 这里条件是 presence_of_element_located, 代表节点出现, 其参数为节点の定位元组, 这里是 ID = 'q' 的节点; 其效果是: 在 10s 内如果 ID = 'q' 的节点成功加载出来, 就返回该节点; 如果超过 10s 还没加载出来, 则抛出 TimeoutException 异常.
    input = wait.until(EC.presence_of_element_located((By.ID, 'q')))
    
    # 3. 对于按钮采用等待条件 element_to_be_clickable, 代表按钮可以点击; 其效果是: 如果 10s 内查找的按钮可点击, 就返回该按钮; 否则抛出 TimeoutException 异常.
    button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.btn-search')))
    
    print(input, button)
    browser.close()
    

    其他等待条件可参考 表 7-1 on Page 259;关于等待条件更详细的内容可参考:https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.support.expected_conditions

    7.10 前进 & 后退:

    import time
    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    urls = ['https://www.taobao.com/', 'https://www.bing.com/', 'https://www.bilibili.com/']
    
    # 1. 依次打开列表中网页
    for url in urls:
        browser.get(url)
    
    # 2. 调用 back() 方法退回前一个页面
    browser.back()
    time.sleep(1)
    
    # 3. 调用 forward() 方法前进到下一个页面
    browser.forward()
    time.sleep(1)
    browser.close()
    

    7.11 Cookies:

    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    browser.get('https://www.bing.com')
    
    # 1. 获取所有 Cookies
    print(browser.get_cookies())
    
    # 2. 添加一个 Cookie (传入字典)
    browser.add_cookie({'name': 'name', 'domain': 'www.bing.com', 'value': '🐇🐇🐇'})
    print(browser.get_cookies())    # 为什么这里看不到添加的 Cookie 呢?
    
    # 3. 删除所有 Cookies
    browser.delete_all_cookies()
    print(browser.get_cookies())
    
    browser.close()
    

    7.12 选项卡管理:

    打开新选项卡,切换选项卡等操作。

    import time
    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    browser.get('https://www.bing.com')
    
    # 1. 执行 JS 语句开启新选项卡
    browser.execute_script('window.open()')
    
    # 2. 调用 window_handles 属性获取当前所有选项卡的代号
    print(browser.window_handles)
    
    # 3. 调用 switch_to.window() 方法卡换选项卡 (参数为选项卡的代号)
    browser.switch_to.window(browser.window_handles[1])
    browser.get('https://www.baidu.com')
    time.sleep(1)
    
    # 4. 切换回第一个选项卡
    browser.switch_to.window(browser.window_handles[0])
    browser.get('https://www.python.org')
    
    browser.close()
    

    7.13 异常处理:

    利用 try except 语句捕获在使用 Selenium 的过程中可能出现的异常:

    from selenium import webdriver
    from selenium.common.exceptions import TimeoutException, NoSuchElementException
    
    browser = webdriver.Chrome()
    browser.maximize_window()
    
    try:
        browser.get('https://www.bing.com')
    except TimeoutException:
        print("Time Out!")
    try:
        # 尝试获取一个不存在的节点
        browser.find_element_by_id('Fa Q~')
    except NoSuchElementException:
        print("No Such Element Found!")
    finally:
        browser.close()
    

    There’s more info that might be useful:https://selenium-python.readthedocs.io/api.html#module-selenium.common.exceptions

  • Splash 的使用

  • Splash 是一个 JavaScript 渲染服务,是一个带有 HTTPAPI 的轻量级浏览器,同时它对接了 Python 的 Twisted 和 QT 库。利用它同样可以实现动态谊染页面的抓取。

  • 这一节的主要内容如下,推荐用到再看。

    Splash Lua 脚本,对象属性,对象方法

    Splash API 调用(6个主要接口 & 相应的 Python 实现)

  • Splash 负载均衡配置

    用 Splash 抓取大量数据时,任务非常多,单个 Splash 服务来处理压力太大,此时可以考虑搭建一个 负载均衡器 将压力分散到各个服务器上。这相当于多台机器多个服务共同参与任务处理,可以减小单个 Splash 服务的压力。

    具体配置,认证和测试用到再说。

  • 使用 Selenium 爬取淘宝商品

  • 代码稍有过时,但整体思路仍然可用,稍微修改即可实现爬取,推荐自己尝试~

  • Chrome 的 Headless 模式 on Page 296



Chap 8 验证码识别

  • 稍后再看
  • 2
  • 3


Chap 9 代理的使用

  • 稍后再看
  • 2
  • 3


Chap 10 模拟登录

  • 稍后再看
  • 2
  • 3


Chap 11 App 的爬取

  • 稍后再看
  • 2
  • 3


Chap 12 Pyspider 框架

  • 稍后再看
  • 2
  • 3


Chap 13 Scrapy 框架

  • 这个有点难,还需要再抄几个项目学习一下。
  • 2
  • 3


Chap 14 分布式爬虫

  • 用到再看
  • 2
  • 3


Chap 15 分布式爬虫的部署

  • 用到再看
  • 2
  • 3


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值