• 爬虫基础


    一、爬虫介绍

    (一)爬虫的分类

      1、通用爬虫:通用爬虫是搜索引擎(Baidu、Google、Yahoo等)“抓取系统”的重要组成部分。主要目的是将互联网上的网页下载到本地,形成一个互联网内容的镜像备份。再对这些网页做相关处理(提取关键字、去掉广告),最后提供一个用户检索接口。 

    搜索引擎如何抓取互联网上的网站数据?

    • 门户网站主动向搜索引擎公司提供其网站的url
    • 搜索引擎公司与DNS服务商合作,获取网站的url
    • 门户网站主动挂靠在一些知名网站的友情链接中

     2、聚焦爬虫:聚焦爬虫是根据指定的需求抓取网络上指定的数据。例如:获取豆瓣上电影的名称和影评,而不是获取整张页面中所有的数据值。

    (二)Jupyter Notebook安装

    1、Jupyter Notebook介绍

    Jupyter Notebook是基于网页的用于交互计算的应用程序,可以在网页中直接编写代码、运行代码等。

    官网:https://jupyter.org/

    2、安装

    注:安装Python3.3及以上版本或2.7版本

    需要使用Anaconda安装:

    (1)去官网下载anaconda:https://www.anaconda.com/distribution/  根据不同操作系统下载对应版本进行安装即可

    配置环境变量后,可以通过命令启动:

    jupyter notebook

    进入如下界面:

    你从哪个目录输入命令进到页面的,这个页面的主目录就是你输入命令的目录。

    (2)使用介绍

    目录下新建文件夹

    勾选刚刚新建的文件夹,可以对其进行重命名:

    点击进入这个文件夹,可以新建文件,如新建一个文本文件:

    然后编写代码即可。

    创建Python3源文件:

    重命名后到目录中查看文件:

    之后就可以在这里面进行爬虫项目编写。

    (3)编写代码

    执行标记类型的cell后将生成一个不可编辑的文档,如果想要继续编辑,就选中当前cell进行的双击即可:

    效果:

    标记类型的cell主要用于编写文档、HTML文件等 。

     (4)常用快捷键

    向上插入一个cell:a

    向下插入一个cell:b

    将cell类型切换为Markdown类型:m

    将cell类型切换为code类型:y 

    执行cell:shift+enter

    查看函数或模块的帮助文档:shift+tab

    自动补全:tab

    二、模块

    (一)urllib模块

    1、介绍

    urllib是Python自带的一个用于爬虫的库,主要作用就是可以通过代码模拟浏览器发送请求。

    常被用到的子模块在Python3中为 urllib.request 和 urllib.parse,在Python2中是 urllib 和 urllib2 。

    使用流程:

    • 指定url
    • 基于urllib的request子模块发起请求
    • 获取响应中的数据值
    • 持久化存储

    2、简单的爬取示例

    • 爬取搜狗首页数据:

    import urllib
    
    # 1、指定URL
    url = "https://www.sogou.com/"
    
    # 2、发送请求,获取响应对象
    response = urllib.request.urlopen(url=url)
    
    # 3、获取页面数据
    page_data = response.read() # read()返回的是byte类型的数据
    # print(page_data)
    
    # 4、持久化数据
    with open("./sougou.html", "wb") as f:
        f.write(page_data)
        print("successfully!")
    View Code
    • 爬取指定词条对应的页面数据
    import urllib
    
    # 注意:url中不可以存在非ascii码的字符数据,在发送请求之前要对URL中的非ascii码的字符数据进行转码
    query_data = urllib.parse.quote("爬虫")
    
    url = "https://www.sogou.com/web?query=%s" % query_data
    response = urllib.request.urlopen(url=url)
    page_data = response.read()
    
    with open("爬虫.html", "wb") as f:
        f.write(page_data)
    View Code

    3、反爬机制--UA身份伪装

    User-Agent(UA):请求载体的身份标识

    网站会检查UA,如果发现这个UA不是正常的浏览器发起的,则可能是个爬虫程序,那么网站可能就会拒绝提供数据。

    相应的反反爬机制:伪装爬虫程序请求的UA

    import urllib
    
    url = "https://www.baidu.com/"
    
    # 构造请求头相关信息
    headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.4325.121 Safari/694.36"}  
    
    # 自定制请求对象
    request = urllib.request.Request(url=url, headers=headers)
    
    # 针对自定制的请求对象发起请求
    response = urllib.request.urlopen(request)
    print(response.read())
    View Code

    4、post请求

    如何发起post请求爬取数据?如:在百度翻译中输入内容,获取翻译结果

    先在浏览器中看一下,整个翻译过程的所有ajax请求中,哪个的请求头中携带了你所要翻译的文字:

    可以在这个ajax请求里面看到有关请求信息:

    接下来就可以模拟浏览器发起请求:

    import urllib
    import json
    
    url = "https://fanyi.baidu.com/sug"
    
    # 将post参数封装成字典
    data = {"kw": "爬虫"}
    
    # 编码请求数据进行编码处理
    data = urllib.parse.urlencode(data)  # 得到一个str类型
    
    # 将str类型转换为byte类型
    data = data.encode()
    
    # 发起post请求
    response = urllib.request.urlopen(url=url, data=data)
    
    json_data = response.read()
    res = json.loads(json_data)
    print(res)  # {'errno': 0, 'data': [{'k': '爬虫', 'v': '[pá chóng] reptile;'}]}
    View Code

    5、基于urllib模块的代理操作

    代理,就是通过第三方代替本体处理相关事务,类似于代购,中介。

    应用场景:一些网站会有相应的反爬虫措施,例如很多网站会检测某一段时间某个IP的访问次数,如果访问频率太快以至于看起来不像正常访客,它可能就会会禁止这个IP的访问。所以我们需要设置一些代理IP,每隔一段时间换一个代理IP,就算IP被禁止,依然可以换个IP继续爬取。

    代理的分类:

    • 正向代理:代理客户端获取数据。正向代理是为了保护客户端防止被追究责任。
    • 反向代理:代理服务器提供数据。反向代理是为了保护服务器或负责负载均衡。
    import urllib
    
    # 创建处理器对象,在其内部封装代理IP和port
    handler = urllib.request.ProxyHandler(proxies={"http": "192.168.23.233:8080"})
    # 创建opener对象,使用该对象发起请求
    opener = urllib.request.build_opener(handler)
    
    url = "https://www.baidu.com/s?ie=UTF-8&wd=python"
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36',
    }
    request = urllib.request.Request(url=url, headers=headers)
    
    response = opener.open(request)
    with open("proxy_data.html", "wb")as f:
        f.write(response.read())
    View Code

    6、基于urllib模块的cookie

    有些网站需要有服务器返回给浏览器的cookie信息才能够获取数据,否则服务器返回的可能就是个登录页面。

    所以我在爬取某些网站数据时,往往通过以下步骤来完成:

    (1)使用爬虫程序登录一次网站,获取响应数据中的cookie

    (2)再使用url进行请求时,携带 (1)中的cookie,进而获取到想要的数据

    可以使用cookiejar来获取cookie:

    cookiejar对象:
        - 作用:自动保存请求中的cookie数据信息
        - 注意:必须和handler和opener一起使用
    cookiejar使用流程:
        - 创建一个cookiejar对象
          import http.cookiejar
          cj = http.cookiejar.CookieJar()
        - 通过cookiejar创建一个handler
          handler = urllib.request.HTTPCookieProcessor(cj)
        - 根据handler创建一个opener
          opener = urllib.request.build_opener(handler)
        - 使用opener.open方法去发送请求,且将响应中的cookie存储到openner对象中,后续的请求如果使用openner发起,则请求中就会携带了cookie

    示例:爬取人人网的个人信息页面

    import urllib
    from http import cookiejar
    
    cj = cookiejar.CookieJar()  # 请求中的cookie会自动存储到cj对象中
    handler = urllib.request.HTTPCookieProcessor(cj)
    opener = urllib.request.build_opener(handler)
    
    url = "http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=201873958471"
    # 自定义一个请求对象,让该对象作为opener的open函数中的参数
    data={
        "email":"www.zhangbowudi@qq.com",
        "icode":"",
        "origURL":"http://www.renren.com/home",
        "domain":"renren.com",
        "key_id":"1",
        "captcha_type":"web_login",
        "password":"40dc65b82edd06d064b54a0fc6d202d8a58c4cb3d2942062f0f7dd128511fb9b",
        "rkey":"41b44b0d062d3ca23119bc8b58983104",
      
        "f":"https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DpPKf2680yRLbbZMVdntJpyPGwrSk2BtpKlEaAuKFTsW%26wd%3D%26eqid%3Deee20f380002988c000000025b7cbb80"
    }
    data=urllib.parse.urlencode(data).encode()
    request=urllib.request.Request(url,data=data)
    opener.open(request)
    
    #获取当前用户的二级子页面
    s_url='http://www.renren.com/289676607/profile'
    #该次请求中就携带了cookie
    response=opener.open(s_url)
    with open("renren.html", "wb")as f:
        f.write(response.read())
    View Code

    (二)requests模块

    1、requests模块介绍

    requests是Python中原生的基于网络请求的模块,用于模拟浏览器发起请求。

    为什么使用requests模块?

    • 使用urllib模块时,需要我们手动处理编码问题
    • 使用urllib模块时,需要手动处理post请求参数
    • 使用urllib模块处理cookie和代理操作的时候步骤比较繁琐

    requests模块安装与使用:

    # 安装
    pip install requests
    
    #使用:
    -指定URL
    -使用requests模块发起请求
    -获取响应数据
    -持久化存储

    2、基于requests模块的get请求

    示例:

    import requests
    
    url = "https://www.sogou.com/"
    response = requests.get(url=url)  # 返回请求成功后的响应对象
    # text属性:获取响应对象中的字符串形式的页面数据
    page_data = response.text
    
    with open("sougou.html", "w", encoding="utf-8")as f:
        f.write(page_data)

    response对象的其他属性:

    import requests
    
    url = "https://www.sogou.com/"
    response = requests.get(url=url)  # 返回请求成功后的响应对象
    # content属性:获取响应对象中的byte类型的页面数据
    page_data = response.content
    print(page_data)
    import requests
    
    url = "https://www.sogou.com/"
    response = requests.get(url=url)  # 返回请求成功后的响应对象
    # status_code属性:获取响应对象中的状态码
    page_data = response.status_code
    print(page_data)
    import requests
    
    url = "https://www.sogou.com/"
    response = requests.get(url=url)  # 返回请求成功后的响应对象
    # headers属性:获取响应对象中的响应头信息 一个字典
    page_data = response.headers
    print(page_data)
    import requests
    
    url = "https://www.sogou.com/"
    response = requests.get(url=url)  # 返回请求成功后的响应对象
    # url属性:获取请求的URL
    page_data = response.url
    print(page_data)

    携带参数的get请求:

    # 方式一:

    import
    requests url = "https://www.sogou.com/web?query=爬虫&ie=utf8" response = requests.get(url=url) # 自动处理URL中编码 page_data = response.text print(page_data)
    # 方式二:将请求参数封装到字典中
    
    import requests
    
    url = "https://www.sogou.com/web"
    params = {"query": "爬虫"}
    response = requests.get(url, params = params)
    print(response.text)

    自定义请求头信息:

    import requests
    
    url = "https://www.sogou.com/web"
    params = {"query": "爬虫"}
    headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3040.121 Safari/432.36"}
    response = requests.get(url, params = params, headers=headers)
    print(response.status_code)

    3、基于requests模块的post请求

    需求:只有登录成功后才能获取页面数据

    import requests
    
    url = "https://accounts.douban.com/login"  # 找到post请求所对应的URL
    data = {
        "source": "movie",
        "redir": "https://movie.douban.com",
        "form_email": "15124567889",
        "form_password": "123456789",
        "login": "登录"
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/323.36 (KHTML, like Gecko) Chrome/72.0.4232.121 Safari/537.36"
    }
    response = requests.post(url=url, data=data, headers=headers)
    with open("douban.html", "wb")as f:
        f.write(response.content)
    View Code

    4、基于requests模块的ajax的get请求

    # 获取豆瓣电影的分类信息
    import requests
    
    url = "https://movie.douban.com/j/new_search_subjects"
    params = {
        "sort": "U",
        "range": [0,10],
        "tags": "",
        "start": "0",
        "countries": "香港"
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"
    }
    
    response = requests.get(url=url, params=params, headers=headers)
    page_data = response.text
    print(page_data)
    View Code

    返回的结果是json字符串。

    5、基于requests模块的ajax的post请求

    # 查询肯德基的餐厅信息
    import requests
    
    url = "http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword"
    data = {
        "cname": "",
        "pid": "",
        "keyword": "上海",
        "pageIndex": "2",
        "pageSize": "10"
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"
    }
    response = requests.post(url, headers=headers, data=data)
    
    page_data = response.text  # 一组json字符串
    print(page_data)
    View Code

    6、示例

    # 爬取搜狗知乎某个词条对应一定页码范围的数据
    import requests
    import os
    
    if not os.path.exists("./sougou_pages"):
        os.mkdir("./sougou_pages")
    
    url = "https://zhihu.sogou.com/zhihu"
    
    # 检索字段
    words = input("输入要检索的词条:").strip()
    
    # 动态指定页码范围
    start_num = int(input("输入起始页码:"))
    end_num = int(input("输入结束页码:"))
    
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"
    }
    
    # 通过循环获取指定页码范围的数据
    for i in range(start_num, end_num+1):
        params = {
            "query":words,
            "page": i,
            "ie": "utf8"
        }
        response = requests.get(url, params=params, headers=headers)
        page_text = response.text
        file_name = "sougou_page_data_%s.html" % str(i)  # 文件名
        file_path = os.path.join("./sougou_pages", file_name)  # 文件路径
        with open(file_path, "w", encoding="utf-8")as f:
            f.write(page_text)
            print("第%d页写入成功" % i)
    View Code

    爬取百度贴吧指定页码的数据

    爬取维基百科指定页码的数据

    7、requests模块的cookie操作

    当我们点击“登录”按钮,登录成功之后,服务端会为该次请求创建一个cookie,并且将这个cookie发送给客户端,客户端将其进行存储

    import requests
    
    # 发起登录请求,将获取到的cookie存入session对象中
    post_url = "https://accounts.douban.com/j/mobile/login/basic"
    data = {
        "ck": "",
        "name": "15523455432",
        "password": "123456789",
        "remember": "false",
        "ticket": "5uFnV1iD7YieacEl8MnYS67-sMYiRMAw3qK42WWk29CgL0Ll2bNrfFRtR7N7of985bVPu17aUU4*"
    }
    headers = {
        "Referer": "https://accounts.douban.com/passport/login?source=movie",
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/342.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
        "X-Requested-With": "XMLHttpRequest"
    }
    # 创建session对象
    session = requests.session()  
    
    # 登录请求:获取cookie
    login_response = session.post(url=post_url, data=data, headers=headers)
    
    # 二次请求携带cookie:获取数据
    url = "https://www.douban.com/"
    response = session.get(url, headers=headers)
    page_text = response.text
    with open("douban_personal.html", "w", encoding="utf-8")as f:
        f.write(page_text)
        print("successfully")
    View Code

    8、requests模块的代理操作

    import requests
    
    url = "https://www.baidu.com/s?ie=utf-8&wd=IP"
    
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"
    }
    
    # 将代理IP封装到字字典中  代理IP要与实际IP的协议类型相同,比如都使用http,或都使用https
    proxy_ip = {"https": "114.88.53.19:53281"}
    
    # 更换网络IP
    response = requests.get(url=url, headers=headers, proxies=proxy_ip)
    page_text = response.text
    with open("proxy_page.html", "w", encoding="utf-8")as f:
        f.write(page_text)
        print("successfully")
    View Code

    9、验证码处理

    云打码平台自动识别验证码流程:

    (1)对携带验证码的页面数据进行抓取

    (2)解析页面数据中的验证码,将验证码图片下载到本地

    (3)将验证码图片发送给第三方平台处理,然后返回图片上的数据

    云打码平台http://www.yundama.com/demo.html 

    注册:用户注册和开发者注册都要

    登录:登录开发者平台

    • 示例代码下载(在开发者首页->开发文档->调用示例即最新DLL->PythonHTTP示例下载),将下载的压缩包解压,里面有三个文件
    • 新建一个软件(在开发者首页->我的软件->添加新软件->输入软件名称,点击提交即可

    使用刚刚下载的示例代码中源码文件中的代码进行修改(YDMHTTPDemo3.x.py),让其识别验证码图片的数据值:

    • 拷贝YDMHTTPDemo3.x.py文件中的类到你的cell中,运行一下cell将类加载到当前源文件中
    • 拷贝YDMHTTPDemo3.x.py文件中余下的代码,新建一个cell,在里面新建一个函数,将拷贝的代码写到函数中,修改里面的用户名、密码等配置信息(记得以普通用户身份登录打码平台充值后才能进行验证码识别)
    import http.client, mimetypes, urllib, json, time, requests
    
    
    class YDMHttp:
    
        apiurl = 'http://api.yundama.com/api.php'
        username = ''
        password = ''
        appid = ''
        appkey = ''
    
        def __init__(self, username, password, appid, appkey):
            self.username = username  
            self.password = password
            self.appid = str(appid)
            self.appkey = appkey
    
        def request(self, fields, files=[]):
            response = self.post_url(self.apiurl, fields, files)
            response = json.loads(response)
            return response
        
        def balance(self):
            data = {'method': 'balance', 'username': self.username, 'password': self.password, 'appid': self.appid, 'appkey': self.appkey}
            response = self.request(data)
            if (response):
                if (response['ret'] and response['ret'] < 0):
                    return response['ret']
                else:
                    return response['balance']
            else:
                return -9001
        
        def login(self):
            data = {'method': 'login', 'username': self.username, 'password': self.password, 'appid': self.appid, 'appkey': self.appkey}
            response = self.request(data)
            if (response):
                if (response['ret'] and response['ret'] < 0):
                    return response['ret']
                else:
                    return response['uid']
            else:
                return -9001
    
        def upload(self, filename, codetype, timeout):
            data = {'method': 'upload', 'username': self.username, 'password': self.password, 'appid': self.appid, 'appkey': self.appkey, 'codetype': str(codetype), 'timeout': str(timeout)}
            file = {'file': filename}
            response = self.request(data, file)
            if (response):
                if (response['ret'] and response['ret'] < 0):
                    return response['ret']
                else:
                    return response['cid']
            else:
                return -9001
    
        def result(self, cid):
            data = {'method': 'result', 'username': self.username, 'password': self.password, 'appid': self.appid, 'appkey': self.appkey, 'cid': str(cid)}
            response = self.request(data)
            return response and response['text'] or ''
    
        def decode(self, filename, codetype, timeout):
            cid = self.upload(filename, codetype, timeout)
            if (cid > 0):
                for i in range(0, timeout):
                    result = self.result(cid)
                    if (result != ''):
                        return cid, result
                    else:
                        time.sleep(1)
                return -3003, ''
            else:
                return cid, ''
    
        def report(self, cid):
            data = {'method': 'report', 'username': self.username, 'password': self.password, 'appid': self.appid, 'appkey': self.appkey, 'cid': str(cid), 'flag': '0'}
            response = self.request(data)
            if (response):
                return response['ret']
            else:
                return -9001
    
        def post_url(self, url, fields, files=[]):
            for key in files:
                files[key] = open(files[key], 'rb');
            res = requests.post(url, files=files, data=fields)
            return res.text
    
    
    def get_code(code_img_path):
        """该函数调用了打码平台的相关接口,对指定验证码图片进行识别,返回图片上的数据"""
        # 云打码平台普通用户的用户名
        username    = '用户名'
    
        # 密码
        password    = '密码'                            
    
        # 软件ID(软件代码),开发者分成必要参数。登录开发者后台【我的软件】获得!
        appid       = ID                                     
    
        # 软件密钥,开发者分成必要参数。登录开发者后台【我的软件】获得!
        appkey      = '秘钥'    
    
        # 图片文件
        filename    = code_img_path                        
    
        # 验证码类型,# 例:1004表示4位字母数字,不同类型收费不同。请准确填写,否则影响识别率。在此查询所有类型 http://www.yundama.com/price.html
        codetype    = 1004
    
        # 超时时间,秒
        timeout     = 20                                    
    
        # 检查
        if (username == 'username'):
            print('请设置好相关参数再测试')
        else:
            # 初始化
            yundama = YDMHttp(username, password, appid, appkey)
    
            # 登陆云打码
            uid = yundama.login();
            print('uid: %s' % uid)
    
            # 查询余额
            balance = yundama.balance();
            print('balance: %s' % balance)
    
            # 开始识别,图片路径,验证码类型ID,超时时间(秒),识别结果
            cid, result = yundama.decode(filename, codetype, timeout);
            print('cid: %s, result: %s' % (cid, result))
        return result
    
    
    import requests
    from lxml import etree
    
    # 获取带有验证码图片的页面
    url = "http://www.cdpta.gov.cn/frt/student/login.do"
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"
    }
    session = requests.session()
    page_text = session.get(url, headers=headers).text
    
    # 解析页面的验证码
    tree = etree.HTML(page_text)
    
    # 去网页中复制验证码图片的xpath 如果需要的话再通过相关处理获取到图片的URL 
    code_img_url = tree.xpath('//*[@id="yzmimg"]/@src')[0]  
    code_img_url = "http://www.cdpta.gov.cn" + code_img_url.split(";")[0]
    
    # 获取图片的二进制数据
    code_img = session.get(url=code_img_url, headers=headers).content  
    # 保存图片到本地
    with open("code_img.png", "wb")as f:
        f.write(code_img)
        print("successfully")
    
    # 调用函数识别图片验证码
    res = get_code("./code_img.png")
    print("验证码字符:", res)
    
    # 进行登录操作
    login_url = "http://www.cdpta.gov.cn/frt/student/login.do"
    data = {
        "name": "xxx",
        "idcard": "xxx",
        "password": "xxx",
        "yzm": res,
        "newpassword": "",
        "newpassword2": "",
        "photofile": "(binary)",
        "findpassword_mo": "将密码直接发送到注册的手机",
        "loginction": "登录"
    }
    response = session.post(url=login_url, data=data, headers=headers)
    page_content = response.content
    with open("cdrs.html", "wb")as f:
        f.write(page_content)
        print("successfully")
    View Code

    三、数据解析

    一般在持久化存储爬取的数据之前会进行数据解析,留下有用的部分。

    (一)正则解析

    1、正则匹配规则:

    单字符:
            . : 除换行以外所有字符
            [] :[aoe] [a-w] 匹配集合中任意一个字符
            d :数字  [0-9]
            D : 非数字
            w :数字、字母、下划线、中文
            W : 非w
            s :所有的空白字符包,括空格、制表符、换页符等等。等价于 [ f
    
    	v]。
            S : 非空白
        数量修饰:
            * : 任意多次  >=0
            + : 至少1次   >=1
            ? : 可有可无  0次或者1次
            {m} :固定m次 hello{3,}
            {m,} :至少m次
            {m,n} :m-n次
        边界:
            $ : 以某某结尾 
            ^ : 以某某开头
        分组:
            (ab)  
        贪婪模式: .*
        非贪婪(惰性)模式: .*?
    
        re.I : 忽略大小写
        re.M :多行匹配
        re.S :单行匹配
    
        re.sub(正则表达式, 替换内容, 字符串)
    View Code

    2、使用:

    import re
    
    # 提取python
    s = "javapythongo+c#"
    re.findall("python", s)  # 返回一个列表 ['python']
    
    # 提取hello world
    s = "<html><h3>hello world</h3><html>"
    re.findall("<h3>(hello world)</h3>", s)[0]  # 利用分组
    
    # 提取123
    s = "哈哈哈123嘻嘻嘻"
    re.findall("d+", s)[0]
    
    # 提取http:// 和 https://
    s = "http://www.baidu.com and https://www.baidu.com and httpss://"
    # re.findall("https?://", s)  # ?表示前面的字符出现0次或1次
    re.findall("https{0,1}://", s)  # {}表示前面的字符出现0次或1次
    
    # 提取qq.
    s = "erhfh@qq.cn.com"
    # re.findall("q.*.", s)  # 贪婪模式 ['qq.cn.']
    re.findall("q.*?.", s)  # 非贪婪模式
    
    # 匹配abc 和 abccc
    s = "abc abcc abccc abcd"
    re.findall("abc{1,3}", s)
    
    # 匹配a开头的行  re.S(基于单行匹配) re.M(基于多行匹配)
    s = """
    this is 
    a book which is on the desk.
    application
    plant trees
    """
    re.findall("^a.*", s, re.M)
    
    # 匹配所有行
    s = """<div>
    轻轻原上草,
    一岁一枯荣。
    野火烧不尽,
    春风吹又生。
    </div>
    """
    re.findall("<div>.*</div>", s, re.S)
    View Code

    3、示例:使用正则对糗事百科中数据进行解析并下载

    import re
    import requests
    import os
    
    # 1、指定url
    url = "https://www.qiushibaike.com/pic/"
    # 2、发起请求 
    response = requests.get(url)
    # 3、获取页面数据
    page_text = response.text
    # 4、解析数据
    """
    在浏览器中查看源码找到图片对应的标签:
    <div class="thumb">
        <a href="/article/121627410" target="_blank">
            <img src="//pic.qiushibaike.com/system/pictures/12162/121627410/medium/D17IKNXRXJLDYTJ3.jpg" alt="image">
        </a>
    </div>
    通过解析img标签的src属性即可获得图片链接
    """
    img_link_list = re.findall('<div class="thumb">.*?<img src="(.*?)".*?>.*?</div>', page_text, re.S)
    # 完整的图片链接地址 https://pic.qiushibaike.com/system/pictures/12162/121627410/medium/D17IKNXRXJLDYTJ3.jpg
    # 创建一个文件夹用于存放获取的图片
    if not os.path.exists("./choushibaike_imgs"):
        os.mkdir("choushibaike_imgs")
        
    for url in img_link_list:
        # 给获取的图片链接拼接完整的链接地址
        img_url = "https:" + url
        # 发起请求获取图片二进制数据
        img_data = requests.get(url=img_url).content
        img_name = url.split("/")[-1]  # 或 re.findall("//.*?medium/(.*)", url)
        # 5、持久化存储数据
        with open(os.path.join("./choushibaike_imgs", img_name), "wb")as f:
            f.write(img_data)
    print("successfully!")
    View Code

    4、爬取指定范围页码的图片:

    import re
    import requests
    import os
    
    # 1、指定url
    url = "https://www.qiushibaike.com/pic/page/"  # https://www.qiushibaike.com/pic/page/3/
    # 2、发起请求 
    start_page = int(input("输入起始页码:"))
    end_page = int(input("输入结束页码:"))
    page_text = ""
    for i in range(start_page, end_page + 1):
        response = requests.get(url=url + str(i) + "/")
        # 3、获取页面数据
        page_text = page_text + response.text
    # 4、解析数据
    img_link_list = re.findall('<div class="thumb">.*?<img src="(.*?)".*?>.*?</div>', page_text, re.S)
    # 完整的图片链接地址 https://pic.qiushibaike.com/system/pictures/12162/121627410/medium/D17IKNXRXJLDYTJ3.jpg
    # 创建一个文件夹用于存放获取的图片
    if not os.path.exists("./choushibaike_imgs"):
        os.mkdir("choushibaike_imgs")
        
    for url in img_link_list:
        # 给获取的图片链接拼接完整的链接地址
        img_url = "https:" + url
        # 发起请求获取图片二进制数据
        img_data = requests.get(url=img_url).content
        img_name = url.split("/")[-1]  # 或 re.findall("//.*?medium/(.*)", url)
        # 5、持久化存储数据
        with open(os.path.join("./choushibaike_imgs", img_name), "wb")as f:
            f.write(img_data)
    print("successfully!")
    View Code

    (二)xpath解析

    1、如何使用

    (1)下载lxml:pip install lxml

    (2)导入包:form lxml import etree

    (3)创建etree对象进行指定数据的解析,此时有两种情况:

    • 如果解析的数据是来源于本地:
    etree = etree.parse("本地文件路径")
    etree.xpath("xpath表达式")
    • 如果数据来源于网络:
    etree = etree.HTML("网络请求到的页面数据")
    etree.xpath("xpath表达式")

    2、常用xpath解析式:

    xpath函数返回的结果是一个列表。

    # 属性定位:
        找到class属性值为song的div标签
        //div[@class="song"] 
    # 层级&索引定位:
        找到class属性值为tang的div的直系子标签ul下的第二个子标签li下的# # 直系子标签a
        //div[@class="tang"]/ul/li[2]/a
    # 逻辑运算:
        找到href属性值为空且class属性值为du的a标签
        //a[@href="" and @class="du"]
    # 模糊匹配:
        //div[contains(@class, "ng")]
        //div[starts-with(@class, "ta")]
    # 取文本:
         /表示获取某个标签下的文本内容
         //表示获取某个标签下的文本内容和所有子标签下的文本内容
        //div[@class="song"]/p[1]/text()
        //div[@class="tang"]//text()
    # 取属性:
        //div[@class="tang"]//li[2]/a/@href

    3、本地文件解析

    建立一个本地测试文件test_file.html:

    <html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>本地测试文件</title>
    </head>
    <body>
        <div>
            <p>百里守约</p>
        </div>
        <div class="song">
            <p>李清照</p>
            <p>王安石</p>
            <p>苏轼</p>
            <p>柳宗元</p>
            <a href="http://www.song.com/" title="赵匡胤" target="_self">
                <span>this is span</span>
            宋朝是最强大的王朝,不是军队的强大,而是经济很强大,国民都很有钱</a>
            <a href="" class="du">总为浮云能蔽日,长安不见使人愁</a>
            <img src="http://www.baidu.com/meinv.jpg" alt="" />
        </div>
        <div class="tang">
            <ul>
                <li><a href="http://www.baidu.com" title="qing">清明时节雨纷纷,路上行人欲断魂,借问酒家何处有,牧童遥指杏花村</a></li>
                <li><a href="http://www.163.com" title="qin">秦时明月汉时关,万里长征人未还,但使龙城飞将在,不教胡马度阴山</a></li>
                <li><a href="http://www.126.com" alt="qi">岐王宅里寻常见,崔九堂前几度闻,正是江南好风景,落花时节又逢君</a></li>
                <li><a href="http://www.sina.com" class="du">杜甫</a></li>
                <li><a href="http://www.dudu.com" class="du">杜牧</a></li>
                <li><b>杜小月</b></li>
                <li><i>度蜜月</i></li>
                <li><a href="http://www.haha.com" id="feng">凤凰台上凤凰游,凤去台空江自流,吴宫花草埋幽径,晋代衣冠成古丘</a></li>
            </ul>
        </div>
    </body>
    </html>
    View Code

    解析:

    from lxml import etree
    
    # 创建etree对象
    etr = etree.parse("./test_file.html")
    
    # 找到class属性值为song的div标签
    etr.xpath('//div[@class="song"] ')  
    
    # 找到class属性值为tang的div的直系子标签ul下的第二个子标签li下的直系子标签a
    etr.xpath('//div[@class="tang"]/ul/li[2]/a')
    
    # 找到href属性值为空且class属性值为du的a标签
    etr.xpath('//a[@href="" and @class="du"]')
    
    # 模糊匹配 class属性值包含“ng”的div标签
    etr.xpath('//div[contains(@class, "ng")]')
    
    # 模糊匹配 class属性值以“ta”开头的div标签
    etr.xpath('//div[starts-with(@class, "ta")]')
    
    # /表示获取某个标签下的文本内容
    etr.xpath('//div[@class="song"]/p[1]/text()')
    
    # //表示获取某个标签下的文本内容和所有子标签下的文本内容
    etr.xpath('//div[@class="tang"]//text()')
    
    # 获取class属性为tang的div标签下的第二个li标签下的a标签的href属性值
    etr.xpath('//div[@class="tang"]//li[2]/a/@href')
    View Code

    4、xpath插件 xpath.crx

    安装xpath插件在浏览器中对xpath表达式进行验证:可以在插件中直接执行xpath表达式

    以谷歌浏览器为例:

    打开浏览器->更多工具->扩展程序->开启开发者模式->将xpath插件拖到这个页面中

    启动和关闭插件的快捷键: ctrl + shift + x

    5、示例:对段子网中的段子标题和内容进行解析并存储

    import requests
    from lxml import etree
    
    url = "https://ishuo.cn/joke"
    response = requests.get(url)
    page_text = response.text
    
    etr = etree.HTML(page_text)
    # 获取页面所有的li标签
    li_list = etr.xpath('//div[@id="list"]/ul/li')
    
    # Element类型的对象可以继续调用xpath函数,对该对象表示的局部内容进行指定内容的解析
    f = open("duanzi.txt", "w", encoding="utf-8")
    for li in li_list:
        title = li.xpath('./div[@class="info"]/a/text()')[0]
        content = li.xpath('./div[@class="content"]/text()')[0]
        f.write(title + ":" + content + "
    
    ")
    f.close()
    print("ok")
    View Code

    (三)BeautifulSoup解析

    BeautifulSoup是Python独有的解析方式,简单、高效。

    1、安装

    - 需要将pip源设置为国内源,阿里源、豆瓣源、网易源等
       - windows
        (1)打开文件资源管理器(文件夹地址栏中)
        (2)地址栏上面输入 %appdata%3)在这里面新建一个文件夹  pip
        (4)在pip文件夹里面新建一个文件叫做  pip.ini ,内容写如下即可:
            [global]
            timeout = 6000
            index-url = https://mirrors.aliyun.com/pypi/simple/
            trusted-host = mirrors.aliyun.com
       
        - linux
        (1)cd ~2)mkdir ~/.pip
        (3)vi ~/.pip/pip.conf
        (4)编辑内容,和windows一模一样
    
    - 需要安装:
         bs4在使用时候需要一个第三方库,把这个库也安装一下
             pip install lxml
         安装bs4
             pip install bs4

    2、使用流程

    BeautifulSoup可以将一个html文档转换为一个BeautifulSoup对象,调用该对象中属性和方法进行html文档指定内容的定位查找。

    (1)导包:from bs4 import BeautifulSoup

    (2)将一个html文档,转化为BeautifulSoup对象,然后通过对象的方法或者属性去查找指定的节点内容,存在两种情况:

    • 转化本地文件:soup = BeautifulSoup(open('本地文件'), 'lxml')
    • 转化网络文件:soup = BeautifulSoup('网络请求到的字符串类型或者字节类型的数据', 'lxml')

    常用属性和方法:

        (1)根据标签名查找
            - soup.a   只能找到第一个符合要求的标签
        (2)获取属性
            - soup.a.attrs  获取a所有的属性和属性值,返回一个字典
            - soup.a.attrs['href']   获取href属性
            - soup.a['href']   也可简写为这种形式
        (3)获取内容
            - soup.a.string
            - soup.a.text
            - soup.a.get_text()
           【注意】如果标签还有标签,那么string获取到的结果为None,而其它两个,可以获取文本内容
        (4)find:找到第一个符合要求的标签
            - soup.find('a')  找到第一个符合要求的
            - soup.find('a', title="xxx")
            - soup.find('a', alt="xxx")
            - soup.find('a', class_="xxx")
            - soup.find('a', id="xxx")
        (5)find_all 或者 findAll:找到所有符合要求的标签
            - soup.find_all('a')
            - soup.find_all(['a','b']) 找到所有的a和b标签
            - soup.find_all('a', limit=2)  限制前两个
        (6)根据选择器选择指定的内容
                   select:soup.select('#feng')
            - 常见的选择器:标签选择器(a)、类选择器(.)、id选择器(#)、层级选择器
                - 层级选择器:
                    div .dudu #lala .meme .xixi  下面好多级
                    div > p > a > .lala          只能是下面一级
            【注意】select选择器返回永远是列表,需要通过下标提取指定的对象
    View Code
    # 对一个本地文档进行解析
    from bs4 import BeautifulSoup
    
    f = open('./test_file.html', 'r')
    soup = BeautifulSoup(f, 'lxml')
    
    # 查找p标签 只能找到第一个p标签
    print(soup.p)
    
    # 获取a标签的属性值,返回一个字典
    print(soup.a.attrs)
    
    # 获取内容
    print(soup.p.string)
    print(soup.p.text)
    
    # find:查找第一个符合要求的标签
    print(soup.find('a', class_="du"))
    
    # find_all:找到所有符合要求的标签
    print(soup.find_all("p"))
    # print(soup.findAll("p"))
    
    # select函数:选择器  返回的是一个列表
    print(soup.select("div > img"))
    print(soup.select(".tang li"))
    
    f.close()
    View Code

    3、示例:爬取古诗文网中三国小说里的标题和内容

    import requests
    from bs4 import BeautifulSoup
    
    url = "http://www.shicimingju.com/book/sanguoyanyi.html"
    
    
    def get_content(url):
        # 发起请求,获取数据
        page_text = requests.get(url).text
        soup = BeautifulSoup(page_text, "lxml")
        return soup
    
    
    soup = get_content(url)
    a_list = soup.select(".book-mulu > ul > li > a")  # 所有a标签对象
    
    # print(type(a))  # <class 'bs4.element.Tag'>
    # Tag类型的对象可以继续调用相应的属性和方法进行数据解析  
    f = open("sanguoyanyi.txt", "w", encoding="utf-8")
    for a in a_list:
        title = a.text
        # 章节对应的完整地址:http://www.shicimingju.com/book/sanguoyanyi/1.html
        a_link = "http://www.shicimingju.com" + a["href"]
        soup = get_content(a_link)
        contents = soup.find("div", class_ = "chapter_content")
        content = contents.text
        f.write(title + content + "
    
    ")
    f.close()
    print("ok")
    View Code

    四、selenium和phantomJs处理网页动态加载数据的爬取

    有些网站的页面数据是随着鼠标往下滑动才动态的加载出来的,即图片懒加载,这种情况下,我们可以借助一些工具进行处理。

    • 图片懒加载:图片懒加载是一种网页优化技术。图片作为一种网络资源,在被请求时也与普通静态资源一样,将占用网络资源,而一次性将整个页面的所有图片加载完,将大大增加页面的首屏加载时间。为了解决这种问题,通过前后端配合,使图片仅在浏览器当前视窗内出现时才加载该图片,达到减少首屏图片请求数的技术就被称为“图片懒加载”。

    • 网站一般如何实现图片懒加载技术呢? 在网页源码中,在img标签中首先会使用一个“伪属性”(通常使用src2,original......)去存放真正的图片链接而并非是直接存放在src属性中。当图片出现到页面的可视化区域中,会动态将伪属性替换成src属性,完成图片的加载。

    (一)selenium

    1、介绍

    selenium是一个第三方库,可以实现让浏览器自动化的操作

    2、环境搭建

    (1)安装:pip install selenium

    (2)获取浏览器的驱动程序

    3、使用

    # 1、导包
        from selenium import webdriver
    # 2、创建一个浏览器对象
        browser = webdriver.Chrome(executable_path='驱动路径')
    # 3、使用浏览器发起指定请求
        browser.get(url)
    
    # 使用下面的方法,查找指定的元素进行操作即可
        find_element_by_id            根据id找节点
        find_elements_by_name         根据name找
        find_elements_by_xpath        根据xpath查找
        find_elements_by_tag_name     根据标签名找
        find_elements_by_class_name   根据class名字查找

    4、示例:在百度进行指定词条的搜索

    from selenium import webdriver
    import time
    
    # 创建一个浏览器对象
    browser = webdriver.Chrome(executable_path='./chromedriver')
    
    # 使用浏览器发起指定请求
    browser.get("https://www.baidu.com")
    
    time.sleep(1)  # 停留1秒(模拟人工操作)
    text = browser.find_element_by_id("kw")  # 通过百度输入框的id进行定位
    text.send_keys("爬虫")  # 输入检索字段:爬虫
    
    time.sleep(1)
    button = browser.find_element_by_id("su")  # 定位提交按钮
    button.click()  # 点击按钮
    
    time.sleep(6)
    browser.quit()  # 关闭浏览器
    View Code

    (二)phantomJs

    phantomJs是一款无界面的浏览器,使用方法与谷歌驱动差不多,先下载驱动,拷贝到当前项目目录以备用

    import time
    from selenium import webdriver
    
    browser = webdriver.PhantomJS(executable_path="./phantomjs-2.1.1-macosx/bin/phantomjs")  # 导入驱动路径
    
    # 打开浏览器
    browser.get("https://www.baidu.com")
    
    # 截屏保存
    browser.save_screenshot("./001打开浏览器.png")
    
    time.sleep(1)
    # 定位文本框 输入检索词条
    text = browser.find_element_by_id("kw")
    text.send_keys("人工智能")
    browser.save_screenshot("./002输入词条.png")
    
    time.sleep(1)
    # 定位提交按钮
    button = browser.find_element_by_id("su")
    button.click()
    browser.save_screenshot("./003检索结果.png")
    
    # 关闭浏览器
    time.sleep(4)
    browser.quit()
    
    # 注意:selenium 3.x版本已经不支持PhantomJS,可以下载selenium 2.x版本   pip install selenium==2.48.0
    View Code

    对于PhantomJs停止了更新和维护的问题,除了使用低版本的selenium外,推荐使用谷歌的无头浏览器,是一款无界面的谷歌浏览器:

    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    import time
    
    # 创建一个参数对象,用来控制chrome以无界面模式打开
    chrome_options = Options()
    chrome_options.add_argument('--headless')
    chrome_options.add_argument('--disable-gpu')
    # 驱动路径
    path = r'./chromedriver'
    
    # 创建浏览器对象
    browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
    
    # 上网
    url = 'http://www.baidu.com/'
    browser.get(url)
    time.sleep(3)
    
    browser.save_screenshot('baidu.png')
    
    browser.quit()
    View Code

    动态获取豆瓣电影中的电影电影链接:

    import time
    from selenium import webdriver
    from lxml import etree
    
    browser = webdriver.PhantomJS(executable_path="./phantomjs-2.1.1-macosx/bin/phantomjs")
    url = "https://movie.douban.com/typerank?type_name=%E6%81%90%E6%80%96&type=20&interval_id=100:90&action="
    browser.get(url)
    
    time.sleep(1)  # 等待页面加载
    browser.save_screenshot("./1.png")  # 页面截屏
    # 编写js代码,让页面中的滚轮向下滑动到底部加载出更多的内容
    js = "window.scrollTo(0, document.body.scrollHeight)"
    # 让浏览器执行js代码
    browser.execute_script(js)
    time.sleep(2)
    
    browser.execute_script(js)  # 再滚动一次滚动条
    
    time.sleep(2)
    browser.save_screenshot("./2.png")
    
    # 获取加载数据后的页面
    page_text = browser.page_source
    
    # 解析数据
    etr = etree.HTML(page_text)
    div_list = etr.xpath('//*[@id="content"]/div/div[1]/div[6]/div')
    f = open('./source.txt', 'w', encoding='utf-8')
    for div in div_list:
        # 持久化存储
        f.write(div.xpath('./div/div/div/span/a/text()')[0] + ":" +
                div.xpath('./div/a/@href')[0] + "
    ")
    
    f.close()
    browser.quit()
    View Code

     五、scrapy框架

    (一)scrapy安装

    Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。集成了高性能异步下载,队列,分布式,解析,持久化等功能。

    安装:

    # Linux/mac OS :
        pip install scrapy
    
    # Windows:
        1、pip install wheel
        2、根据Python的版本下载对应版本的twisted框架:
            下载好后执行安装:pip install 下载的twisted框架.whl文件
        3、pip install pywin32
        4、 pip install scrapy

    (二)scrapy使用

    1、流程:

    • 创建一个工程
    • 在工程目录下创建一个爬虫文件
    • 对应的爬虫文件中编写爬虫程序
    • 配置文件的编写
    • 执行

    (1)新建一个文件夹,cd到这个文件夹目录下,输入命令创建工程first_scrapy:

    scrapy startproject first_scrapy

    此时便会自动生成如下文件夹及文件:

    scrapy.cfg   项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)
    items.py     设置数据存储模板,用于结构化数据,类似于Django的Model
    pipelines    数据持久化处理
    settings.py  配置文件,如:递归的层数、并发数,延迟下载等
    spiders      爬虫目录,如:创建文件,编写爬虫解析规则

    (2)进入工程目录中,创建爬虫文件:

    # cd 工程名称:
        cd first_scrapy
    # scrapy genspider 爬虫名称 起始URL:
        scrapy genspider qiushibk www.qiushibaike.com

    此时在spiders文件夹下可以看到一个名为qiushibk.py的文件

    (3)在爬虫文件中编写代码

    # -*- coding: utf-8 -*-
    import scrapy
    
    
    class QiushibkSpider(scrapy.Spider):
        name = 'qiushibk'  # 爬虫文件名
        allowed_domains = ['www.qiushibaike.com']  # 允许的域名,只能爬取该域名下的页面数据
        start_urls = ['http://www.qiushibaike.com/']  # 起始URL
    
        def parse(self, response):
            """
            对获取的页面数据对指定内容进行解析
            :param response: 请求成功后的响应数据
            :return:返回值必须为迭代器或者为空
            """
            print(response.text)  # 答应页面数据
    View Code

    (4)配置文件编写:

    (5)执行

    # scrapy crawl 爬虫名称:
        scrapy crawl qiushibk  # 会输出日志信息
        scrapy crawl qiushibk --nolog  # 不会输出日志信息

    2、爬取糗事百科中段子的内容和作者进行存储

    """
            持久化存储:
            1.存到磁盘文件
                1.1. 基于终端指令的数据存储
                    1.1.1. 保证parse方法返回一个可迭代类型的对象(存储解析到的页面内容)
                    1.1.2. 使用终端指令完成数据存储到指定磁盘文件的操作
                1.2. 基于管道的数据存储(items:存储解析到的页面数据,pipelines:处理持久化存储)
                    1.2.1. 将解析到的数据存储到items对象
                    1.2.2. 使用yield关键字将items对象提交给pipelines处理
                    1.2.3. 在pipelines中编写代码,完成数据存储操作
                    1.2.4. 在配置文件中开启管道操作
            2.存到数据库(如MySQL、Redis)
                流程:
                    2.1. 将解析到的数据存储到items对象
                    2.2. 使用yield关键字将items对象提交给pipelines处理
                    2.3. 在pipelines中编写代码,连接数据库完成存储操作
                    2.4. 在配置文件中开启管道操作
            """

    (1)基于终端指令的存储

    终端指令:

    执行输出指定格式进行存储:将爬取到的数据写入不同格式的文件中进行存储
        scrapy crawl 爬虫名称 -o xxx.json
        scrapy crawl 爬虫名称 -o xxx.xml
        scrapy crawl 爬虫名称 -o xxx.csv

    爬虫文件代码:

    # -*- coding: utf-8 -*-
    import scrapy
    
    
    class QiusbkSpider(scrapy.Spider):
        name = 'qiusbk'
        # allowed_domains = ['www.qiushibaike.com/text']  # 一般不需要指定域名,因为如果抓取图片的话,有些图片的域名不是这个
        start_urls = ['https://www.qiushibaike.com/text/']
    
        def parse(self, response):
            # 解析数据
            div_list = response.xpath('//div[@id="content-left"]/div')
            data_list = []  # 存储解析到的数据
            for div in div_list:
                # xpath解析到的内容会被存储到Selector对象中并以列表形式返回,可以通过extract()方法提取内容列表
                title = div.xpath('./div[1]/a[2]/h2/text()').extract_first()  # extract_first()获取列表第一个元素
                content = div.xpath('//div[@class="content"]/span/text()').extract()[0]
                data_list.append({"title": title, "content": content})
    
            return data_list
    View Code

    在终端执行代码存储数据:

    scrapy crawl qiusbk -o qiushibaike01.csv --nolog

    (2)基于管道的存储

    爬虫程序:

    import scrapy
    from ..items import QsbkItem
    
    
    class QiusbkSpider(scrapy.Spider):
        name = 'qiusbk'
        # allowed_domains = ['www.qiushibaike.com/text']  # 一般不需要指定域名,因为如果抓取图片的话,有些图片的域名不是这个
        start_urls = ['https://www.qiushibaike.com/text/']
    
        def parse(self, response):
            # 解析数据
            div_list = response.xpath('//div[@id="content-left"]/div')
            i = 0
            for div in div_list:
                # xpath解析到的内容会被存储到Selector对象中并以列表形式返回,可以通过extract()方法提取内容列表
                title = div.xpath('./div[1]/a[2]/h2/text()').extract_first()  # extract_first()获取列表第一个元素
                content = div.xpath('./a[1]/div/span/text()').extract()[0]
                # 1、创建item对象,将数据存储到item对象中
                item = QsbkItem()
                item["title"] = title
                item["content"] = content
                # 2、将item对象提交给管道
                yield item
    qiusbk.py

    items.py:

    import scrapy
    
    
    class QsbkItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
    
        # 声明属性:
        title = scrapy.Field()
        content = scrapy.Field()
    items.py

    pipelines.py:

    class QsbkPipeline(object):
        def __init__(self):
            self.fp = None
    
        def open_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫开始时被调用一次"""
            print("爬虫开始")
            self.fp = open("./qiushibaike02.txt", "w", encoding="utf-8")
    
        def process_item(self, item, spider):
            """每当爬虫文件向管道yield一次item,这个process_item方法就会被执行一次"""
            # 获取item对象中的数据
            title = item["title"]
            content = item["content"]
            # 存储
            self.fp.write(title + ":" + content + "
    ")
            return item
    
        def close_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫结束时被调用一次"""
            print("爬虫结束")
            self.fp.close()
    pipelines.py

    settings.py:

    执行爬虫:

    scrapy crawl qiusbk

    (3)基于MySQL的存储

    import scrapy
    from ..items import QsbkItem
    
    
    class QiusbkSpider(scrapy.Spider):
        name = 'qiusbk'
        # allowed_domains = ['www.qiushibaike.com/text']  # 一般不需要指定域名,因为如果抓取图片的话,有些图片的域名不是这个
        start_urls = ['https://www.qiushibaike.com/text/']
    
        def parse(self, response):
            # 解析数据
            div_list = response.xpath('//div[@id="content-left"]/div')
            i = 0
            for div in div_list:
                # xpath解析到的内容会被存储到Selector对象中并以列表形式返回,可以通过extract()方法提取内容列表
                title = div.xpath('./div[1]/a[2]/h2/text()').extract_first()  # extract_first()获取列表第一个元素
                content = div.xpath('./a[1]/div/span/text()').extract()[0]
                # 1、创建item对象,将数据存储到item对象中
                item = QsbkItem()
                if title:
                    title.strip("
    ")
                if content:
                    content.strip("
    ")
                item["title"] = title
                item["content"] = content
                # 2、将item对象提交给管道
                yield item
    qiusbk.py
    import scrapy
    
    
    class QsbkItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
    
        # 声明属性:
        title = scrapy.Field()
        content = scrapy.Field()
    items.py
    import pymysql
    
    
    class QsbkPipeline(object):
        def __init__(self):
            self.fp = None
            self.conn = None
            self.cursor = None
    
        def open_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫开始时被调用一次"""
            print("爬虫开始")
            self.conn = pymysql.Connect(host="127.0.0.1", port=3306, user="root", password="123", db="qiubai")
    
        def process_item(self, item, spider):
            """每当爬虫文件向管道yield一次item,这个process_item方法就会被执行一次"""
            # 获取item对象中的数据
            title = item["title"]
            content = item["content"]
            # 存储
            """
            连接数据库
            执行SQL语句
            提交事务
            """
            sql = 'insert into qiubai_db values("%s", "%s")' % (title, content)
            self.cursor = self.conn.cursor()
            try:
                self.cursor.execute(sql)
                self.conn.commit()
            except Exception as e:
                # print(e)
                self.conn.rollback()
            return item
    
        def close_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫结束时被调用一次"""
            print("爬虫结束")
            self.cursor.close()
            self.conn.close()
    pipelines.py

    (4)基于Redis的存储

    • liniux下安装

    官网下载安装包 http://www.redis.cn/download.html 

    解压安装包-> cd到安装包 ->make(编译)-> cd src -> ./redis-serve ../redis.conf 启动服务->重新打开一个终端-> cd到redis的src目录下-> ./redis-cli 启动客户端

    wget http://download.redis.io/releases/redis-5.0.3.tar.gz
    tar xzf redis-5.0.3.tar.gz
    cd redis-5.0.3
    make
    cd src
    ./redis-server ../redis.conf
    
    
    cd redis-5.0.3/src
    ./redis-cli
    • 使用

     通过 pip install redis 来下载Redis模块,在程序中 import redis 即可使用

    import scrapy
    from ..items import QsbkItem
    
    
    class QiusbkSpider(scrapy.Spider):
        name = 'qiusbk'
        # allowed_domains = ['www.qiushibaike.com/text']  # 一般不需要指定域名,因为如果抓取图片的话,有些图片的域名不是这个
        start_urls = ['https://www.qiushibaike.com/text/']
    
        def parse(self, response):
            # 解析数据
            div_list = response.xpath('//div[@id="content-left"]/div')
            i = 0
            for div in div_list:
                # xpath解析到的内容会被存储到Selector对象中并以列表形式返回,可以通过extract()方法提取内容列表
                title = div.xpath('./div[1]/a[2]/h2/text()').extract_first()  # extract_first()获取列表第一个元素
                content = div.xpath('./a[1]/div/span/text()').extract()[0]
                # 1、创建item对象,将数据存储到item对象中
                item = QsbkItem()
                if title:
                    title.strip("
    ")
                if content:
                    content.strip("
    ")
                item["title"] = title
                item["content"] = content
                # 2、将item对象提交给管道
                yield item
    qiusbk.py
    import scrapy
    
    
    class QsbkItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
    
        # 声明属性:
        title = scrapy.Field()
        content = scrapy.Field()
    items.py
    import redis
    
    
    class QsbkPipeline(object):
        """将数据存储到数据库"""
        def __init__(self):
            self.fp = None
            self.conn = None
            self.cursor = None
    
        def open_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫开始时被调用一次"""
            print("爬虫开始")
            self.conn = redis.Redis(host="244.68.34.23", port=6379, password="xxx")
    
        def process_item(self, item, spider):
            """每当爬虫文件向管道yield一次item,这个process_item方法就会被执行一次"""
            # 获取item对象中的数据
            dic = {
                "title": item["title"],
                "content": item["content"]
            }
            # 存储
            """
            连接数据库
            将数据写入Redis数据库
            """
            self.conn.lpush('qsbk', dic)
    
        def close_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫结束时被调用一次"""
            print("爬虫结束")
    pipelines.py

    3、在pipelines.py中编写多个类,实现将爬取的数据分别存储到MySQL数据库、磁盘文件、Redis中:

    import redis
    
    
    class QsbkPipeline(object):
        """将数据存储到redis中"""
        def __init__(self):
            self.fp = None
            self.conn = None
            self.cursor = None
    
        def open_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫开始时被调用一次"""
            print("爬虫开始")
            self.conn = redis.Redis(host="192.168.1.30", port=6379, password="123456")
    
        def process_item(self, item, spider):
            """每当爬虫文件向管道yield一次item,这个process_item方法就会被执行一次"""
            # 获取item对象中的数据
            dic = {
                "title": item["title"],
                "content": item["content"]
            }
            # 存储
            self.conn.lpush('qsbk', dic)
    
        def close_spider(self, spider):
            """在整个爬虫过程中 该方法只会在爬虫结束时被调用一次"""
            print("爬虫结束")
    
    
    class QsbkPipelineByFiles(object):
        """将数据存储到本地磁盘"""
        def process_item(self, item, spider):
            print("将数据存入磁盘文件")
            return item
    
    
    class QsbkPipelineByMysql(object):
        """将数据存储到MySQL数据库"""
        def process_item(self, item, spider):
            print("将数据存入MySQL数据库")
            return item
    pipelines.py

    再去settings中配置这个几个类的路径和优先级:

    (三)批量爬取多个页码范围的数据

    import scrapy
    from ..items import Qsbk01Item
    
    
    class QsbkSpider(scrapy.Spider):
        name = 'qsbk'
        # allowed_domains = ['www.qiushibaike.com/text']
        start_urls = ['https://www.qiushibaike.com/text/']
        page_num = 1
    
        def parse(self, response):
            div_list = response.xpath('//div[@id="content-left"]/div')
            for div in div_list:
                author = div.xpath('./div/a[2]/h2/text()').extract_first()
                content = div.xpath('./a/div/span/text()').extract_first()
    
                # 创建item对象,将解析数据进行存储
                item = Qsbk01Item()
                item["author"] = author
                item["content"] = content
                yield item
    
            # 请求的手动发送
            if self.page_num <= 5:  # 爬取2~6页的数据
                self.page_num += 1
                print("爬取第%d页数据" % self.page_num)
                url = "https://www.qiushibaike.com/text/page/%d/" % self.page_num
                yield scrapy.Request(url=url, callback=self.parse)  # callback:回调函数,将请求获取到的数据进行解析
    qsbk.py
    import scrapy
    
    
    class Qsbk01Item(scrapy.Item):
        # define the fields for your item here like:
        author = scrapy.Field()
        content = scrapy.Field()
    items.py
    class Qsbk01Pipeline(object):
        def __init__(self):
            self.fp = None
    
        def open_spider(self, spider):
            print("爬虫开始")
            self.fp = open("./qsbk01.txt", "w", encoding="utf-8")
    
        def process_item(self, item, spider):
            author = item["author"]
            content = item["content"]
            self.fp.write(author + ":" + content + "
    ")
            return item
    
        def close_spider(self, spider):
            print("爬虫结束")
            self.fp.close()
    pipelines.py

    settings.py:

    (四)scrapy核心组件介绍

    引擎:负责整个系统的数据流处理,触发事务(框架的核心)。

    调度器:接收引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。由它来决定下一个抓取的网址是什么,同时去除重复的网址。

    下载器:用于下载网页内容,并将网页内容返回给spider。下载器是建立在twisted这个高效的异步模型上的。

    爬虫文件:负责从特定的网页中提取想要的数据,即实体(item)

    管道:负责处理爬虫从页面中抽取的实体,持久化实体、验证实体的有效性、清除不需要的信息。

    执行流程:引擎首先会获取爬虫文件中的起始url,并且提交到调度器中。如果需要从url中下载数据,则调度器会将url通
    过引擎提交给下载器,下载器根据url去下载指定内容(响应体)。下载好的数据会通过引擎移交给爬虫文件,爬虫文件可以将下载
    的数据进行指定格式的解析。如果解析出的数据需要进行持久化存储,则爬虫文件会将解析好的数据通过引擎移交给管道进行持久化存储。

    (五)post请求的发送

    方法1:在Request方法中设置method

    import scrapy
    
    
    class DoubanSpider(scrapy.Spider):
        name = 'douban'
        # allowed_domains = ['www.baidu.com']
        start_urls = ['https://www.baidu.com/']
    
        def start_requests(self):
            """该方法是父类中的一个方法:实现对start_urls列表中的元素进行get请求的发送"""
            for url in self.start_urls:
                # yield scrapy.Request(url=url, callback=self.parse)
                #  Request默认是发起get请求,如果要发送post请求,则要设置method参数:
                yield scrapy.Request(url=url, callback=self.parse, method="post")
    
        def parse(self, response):
            pass

    方法2:使用FormRequest()来发起post请求(推荐用此法)

    import scrapy
    
    
    class DoubanSpider(scrapy.Spider):
        name = 'douban'
        # allowed_domains = ['www.baidu.com']
        start_urls = ['https://fanyi.baidu.com/sug']
    
        def start_requests(self):
            print("请求开始")
            for url in self.start_urls:
                # post请求参数进行封装
                data = {
                    'kw': '爬虫',
                }
                yield scrapy.FormRequest(url=url, formdata=data, callback=self.parse)
    
        def parse(self, response):
            print(response.text)

    (六)cookie

    scrapy框架会自动的去携带cookie进行请求操作。

    登录豆瓣网,然后获取个人信息页面的数据:

    import scrapy
    
    
    class DoubanSpider(scrapy.Spider):
        name = 'douban'
        # allowed_domains = ['www.baidu.com']
        start_urls = ['https://accounts.douban.com/j/mobile/login/basic']
    
        def start_requests(self):
            print("请求开始")
            for url in self.start_urls:
                # post请求参数进行封装
                data = {
                    'ck': '',
                    'name': 'xxx',
                    'password': 'xxx',
                    'remember': 'false',
                    'ticket': ''
                }
                yield scrapy.FormRequest(url=url, formdata=data, callback=self.parse)
    
        def personal_page(self, response):
            # 针对个人页面数据进行解析操作
            with open("personal.html", "w", encoding="utf-8") as f:
                f.write(response.text)
    
        def parse(self, response):
            # 获取登录成功后的页面
            with open("douban01.html", "w", encoding="utf-8") as f:
                f.write(response.text)
            # 登录成功后获取当前用户的个人信息页面
            personal_url = "https://www.douban.com/people/193570027/"
            yield scrapy.Request(url=personal_url, callback=self.personal_page)
    View Code

    (七)代理

    使用下载中间件。拦截请求,将请求的IP进行更换。

    在我们的爬虫项目中已经有自带的中间件:

    自定义一个下载中间件,在process_request()方法中处理拦截到的请求:

    class MyProxyMiddleware(object):
        def process_request(self, request, spider):
            # 更换请求的ip
            request.meta["proxy"] = "https://39.134.66.18:8080"
    View Code

    在配置文件中开启自定义的下载中间件:

    爬虫:

    import scrapy
    
    
    class ProxydemoSpider(scrapy.Spider):
        name = 'proxydemo'
        # allowed_domains = ['www.baidu.com/s?wd=ip']
        start_urls = ['https://www.baidu.com/s?wd=ip/']
    
        def parse(self, response):
            with open("proxy_ip.html", "w", encoding="utf-8") as f:
                f.write(response.text)
    View Code

    (八)日志等级

    ERROR:错误
    WARNING:警告
    INFO:一般信息
    DEBUG:调试信息

    如何让终端输出指定的日志类型?

    1、在settings中进行设置:

    2、在settings设置让日志信息不在终端中输出,而是保存到指定的文件:

    (九)请求传参

     使用场景:当我们爬取的数据不在同一个页面中,却要对这些不同的页面数据存储在一起时,我们使用meta参数进行传参。

    例:爬取97电影网站的电影详情页面的数据,如电影名、类型、导演、编剧、主演等

    import scrapy
    from ..items import Movie97Item
    
    
    class Movie9797Spider(scrapy.Spider):
        name = 'movie9797'
        # allowed_domains = ['www.55xia.com/movie']
        start_urls = ['https://www.55xia.com/movie']
    
        def movie_detail(self, response):
            # 2、解析电影详情页面数据
            tr_list = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody')
            for tr in tr_list:
                director = tr.xpath('./tr[1]/td[2]/a/text()').extract_first()
                screenwriter = tr.xpath('./tr[2]/td[2]/a/text()').extract_first()
                actors = tr.xpath('./tr[3]/td[2]//text()').extract()
    
                # 获取meta参数传递过来的item对象
                item = response.meta["item"]
                item["director"] = director
                item["screenwriter"] = screenwriter
                item["actors"] = actors
                yield item
    
        def parse(self, response):
            # 1、解析电影首页数据
            div_list = response.xpath('/html/body/div[1]/div[1]/div[2]/div')
            for div in div_list:
                name = div.xpath('./div/a/@title').extract_first()
                movie_link = "https:" + div.xpath('./div/a/@href').extract_first()
                movie_type = div.xpath('./div/div/div/a/text()').extract()
    
                item = Movie97Item()
                item["name"] = name
                item["movie_type"] = movie_type
    
                # 对获取的电影链接发起请求,获取电影详情页面信息
                yield scrapy.Request(url=movie_link, callback=self.movie_detail, meta={"item": item})  # 将item对象传递给回调函数
    movie9797.py
    import scrapy
    
    
    class Movie97Item(scrapy.Item):
        # define the fields for your item here like:
        name = scrapy.Field()
        movie_type = scrapy.Field()
        director = scrapy.Field()
        screenwriter = scrapy.Field()
        actors = scrapy.Field()
    items.py
    class Movie97Pipeline(object):
        def __init__(self):
            self.fp = None
    
        def open_spider(self, spider):
            print("开始")
            self.fp = open("movie_info.txt", "w", encoding="utf-8")
    
        def process_item(self, item, spider):
            info = item["name"] + ":" + "".join(item["movie_type"]) + ":" + item["director"] + ":" 
                   + item["screenwriter"] + ":" + "".join(item["actors"])
            self.fp.write(info + "
    
    ")
            return item
    
        def close_spider(self, spider):
            print("结束")
            self.fp.close()
    pipelines.py

    (十)CrawlSpider

    如果想要通过爬虫程序去爬取糗事百科全站的新闻数据的话,有几种实现方法?

      方法一:基于Scrapy框架中的Spider的递归爬取进行实现(Request模块递归回调parse方法)。

      方法二:基于CrawlSpider的自动爬取进行实现(更加简洁和高效)。

    1、CrawlSpider介绍

    CrawlSpider其实是Spider的一个子类,除了继承到Spider的特性和功能外,还派生除了其自己独有的更加强大的特性和功能。其中最显著的功能就是”LinkExtractors链接提取器“。Spider是所有爬虫的基类,其设计原则只是为了爬取start_url列表中网页,而从爬取到的网页中提取出的url进行继续的爬取工作使用CrawlSpider更合适。

    2、使用

    (1)创建scrapy工程:scrapy startproject projectName

    (2)创建爬虫文件:scrapy genspider -t crawl spiderName www.xxx.com( "-t crawl" 表示创建的爬虫文件是基于CrawlSpider这个类的)

    例:提取抽屉新热榜页面中的页码链接,并获取所有页码的内容

    # -*- coding: utf-8 -*-
    import scrapy
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    
    
    class Crawlspiderproj01Spider(CrawlSpider):
        name = 'crawlspiderproj01'
        # allowed_domains = ['dig.chouti.com']
        start_urls = ['https://dig.chouti.com/']
    
        rules = (
            Rule(LinkExtractor(allow=r'/all/hot/recent/d+'), callback='parse_item', follow=True),
        )  # 实例化了一个LinkExtractor()对象(链接提取器对象) 作为Rule()(规则解析器对象)的参数
        # allow参数:赋值一个正则表达式,这样,链接提取器就可以根据正则表达式在页面中提取指定的链接
        # 提取到的链接会全部交给规则解析器,规则解析器会对这些链接发起请求,获取页面内容
        # callback参数:解析页面内容的回调函数
        # follow参数:是否将链接提取器作用到链接提取器提取出的链接所表示的页面数据中,为True将会提取全站所有页码的数据
    
        def parse_item(self, response):
            print(response)
            # 在对response进行解析进而获取到想要的数据,通过管道进行存储......
    View Code

    (十一)分布式爬虫

    1、概念

    分布式爬虫:在多台机器上可以执行同一个爬虫程序,实现网站数据分布爬取。

    原生的scrapy框架是不支持分布式爬虫的,因为:调度器无法在多台机器上共享,管道无法在多台机器上共享

    如何实现分布式爬虫:通过scrapy-redis组件来实现

    2、使用

    (1)下载:

    pip install scrapy-redis

    (2)流程:

    • 安装redis数据库
    • redis配置文件的配置redis.conf:

    • 基于配置文件开启redis服务:
    # cd 到redis的src目录下
    ./redis-server ../redis.conf
    • 新建爬虫工程,新建基于CrawlSpider的爬虫文件:
    scrapy startproject distributedspider
    
    cd distributedspider
    scrapy genspider -t crawl qsbk www.qiushibaike.com/pic
    • 导入RedisCrawlSpider类,将爬虫文件修改为基于该类的源文件:

    • 设置调度器队列名称:将爬虫文件中的start_urls修改为redis_key 

    • 编写爬虫代码
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    from scrapy_redis.spiders import RedisCrawlSpider  # 导入RedisCrawlSpider类
    from ..items import DistributedspiderItem
    
    
    class QsbkSpider(RedisCrawlSpider):  # 继承RedisCrawlSpider
        name = 'qsbk'
        # allowed_domains = ['www.qiushibaike.com/pic']
        # start_urls = ['https://www.qiushibaike.com/pic/']  # 在分布式爬虫中不需要start_urls来设置起始URL
    
        # 用redis_key来设置调度器中队列的名称,作用与start_urls一样
        redis_key = "qsbkspider"
    
        rules = (
            Rule(LinkExtractor(allow=r'/pic/page/d+'), callback='parse_item', follow=False),
        )
    
        def parse_item(self, response):
            div_list = response.xpath('//*[@id="content-left"]/div')
            for div in div_list:
                link = "https:" + div.xpath('./div[2]/a/img/@src').extract_first()
                item = DistributedspiderItem()
                item["link"] = link
                yield item
    qsbk.py
    import scrapy
    
    
    class DistributedspiderItem(scrapy.Item):
        # define the fields for your item here like:
        link = scrapy.Field()
    items.py
    • 在settings中将管道和调度器设置为scrapy-redis的管道和调度器,设置Redis数据库的链接信息:
    ITEM_PIPELINES = {
        'scrapy_redis.pipelines.RedisPipeline': 400,  # 使用scrapy_redis提供的管道
    }
    
    # 调度器-----------------------------------------------------
    # 使用scrapy-redis组件的去重队列
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    # 使用scrapy-redis组件自己的调度器
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    # 是否允许暂停
    SCHEDULER_PERSIST = True
    
    # redis 数据库配置------------------------------------------
    REDIS_HOST = "244.123.45.233"  # redis服务的ip地址
    REDIS_PORT = 6379
    REDIS_ENCODING = "utf-8"
    REDIS_PARAMS = {"password": "123456"}
    • 打开终端cd到爬虫项目的spiders目录下,执行命令:
    scrapy runspider qsbk.py  # qsbk.py是spiders目录下的爬虫文件
    • 打开Redis客户端,将起始URL放到队列中:
    ./redis-cli  # 打开Redis客户端
    
    lpush qsbkspider https://www.qiushibaike.com/pic/  # 队列名,起始URL
  • 相关阅读:
    Antd下拉多选带勾选框
    POJ
    HDU 4281(01 背包+ 多旅行商问题)
    Codeforces Round #460 (Div. 2) D. Substring
    HDU
    POJ 2184 Cow Exhibition
    Codechef FRBSUM 解题报告
    UVA11982题解
    Suffix Array 后缀数组算法心得
    51nod1158 单调栈 个人的想法以及分析
  • 原文地址:https://www.cnblogs.com/yanlin-10/p/10549809.html
Copyright © 2020-2023  润新知