爬虫

目录

1. 爬虫初识

1. 什么是爬虫

通过编写程序,模拟浏览器上网,然后让其去互联网上抓取数据的过程。

2. 爬虫的价值

  1. 实际应用
  2. 就业

3. 爬虫究竟是合法还是违法的?

  • 在法律中是不被禁止
  • 具有违法风险
  • 善意爬虫 恶意爬虫

4. 爬虫带来的风险

  1. 爬虫干扰了被访问网站的正常运营
  2. 爬虫抓取了受到法律保护的特定类型的数据或者信息

5. 如何正确编写和使用爬虫

  1. 严格遵守网站设置的robots协议;
  2. 在规避反爬虫措施的同时,需要优化自己的代码,避免干扰被访问网站的正常运行;
  3. 在使用、传播抓取到的信息时,应审查所抓取的内容,如发现属于用户的个人信息、隐私或者他人的商业秘密的,应及时停止并删除。

2.爬虫初识深入

1. 爬虫在使用场景中的分类

  • 通用爬虫:通用爬虫是搜索引擎(Baidu、Google、Yahoo等)“抓取系统”的重要组成部分。主要目的是将互联网上的网页下载到本地,形成一个互联网内容的镜像备份。 简单来讲就是尽可能的;把互联网上的所有的网页下载下来,放到本地服务器里形成备分,在对这些网页做相关处理(提取关键字、去掉广告),最后提供一个用户检索接口。
  • 聚焦爬虫:聚焦爬虫是根据指定的需求抓取网络上指定的数据。例如:获取豆瓣上电影的名称和影评,而不是获取整张页面中所有的数据值。
  • 增量式爬虫:增量式是用来检测网站数据更新的情况,且可以将网站更新的数据进行爬取(后期会有章节单独对其展开详细的讲解)。
  • 分布式爬虫:提高爬取效率

2. 反爬机制

  • 门户网站通过制定相应的策略和技术手段,防止爬虫程序进行网站数据的爬取。

3. 反反爬策略

  • 爬虫程序通过相应的策略和技术手段,破解门户网站的反爬虫手段,从而爬取到相应的数据。

4. robots协议

  • robots.txt协议(君子协议)。即网站有权规定网站中哪些内容可以被爬虫抓取,哪些内容不可以被爬虫抓取。这样既可以保护隐私和敏感信息,又可以被搜索引擎收录、增加流量。
  • 可以通过网站域名 + /robots.txt的形式访问该网站的协议详情,例如:www.taobao.com/robots.txt

3. requests模块的使用

3.1 初识

1. 引入

  • 在python实现的网络爬虫中,用于网络请求发送的模块有两种,第一种为urllib模块,第二种为requests模块。urllib模块是一种比较古老的模块,在使用的过程中较为繁琐和不便。当requests模块出现后,就快速的代替了urllib模块,因此,在我们课程中,推荐大家使用requests模块。
  • Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。

警告:非专业使用其他 HTTP 库会导致危险的副作用,包括:安全缺陷症、冗余代码症、重新发明轮子症、啃文档症、抑郁、头疼、甚至死亡。

2. what is requests

  • requests模块是python中原生的基于网络请求的模块,其主要作用是用来模拟浏览器发起请求。功能强大,用法简洁高效。在爬虫领域中占据着半壁江山的地位。

3.为什么要使用requests模块

  • 在使用urllib模块的时候,会有诸多不便之处,总结如下:
    • 手动处理url编码
    • 手动处理post请求参数
    • 处理cookie和代理操作繁琐
    • ……
  • 使用requests模块:
    • 自动处理url编码
    • 自动处理post请求参数
    • 简化cookie和代理操作
    • ……

4. 如何使用requests模块

  • 环境安装
    • pip install requests
  • 使用流程/编码流程
    • 指定url
    • 基于requests模块发起请求
    • 获取响应对象中的数据值
    • 持久化存储

5. 第一个爬虫程序

import requests

# 指定url
url = 'https://www.sogou.com/'

# 使用get方法发起请求
response = requests.get(url=url)
# print(response)
# 获取响应数据(text方法)
page_text = response.text
print(page_text)

with open('sougou.html', 'w', encoding='utf-8') as f1:
    f1.write(page_text)

3.2 基于requests模块经典案例实战

1. 基于requests模块的get请求

  • 需求:爬取搜狗指定词条对应的搜索结果页面(简易网页采集器)

  • 反爬机制

    • User-Agent:请求载体的身份标识,使用浏览器发起的请求,请求载体的身份标识为浏览器,使用爬虫程序发起的请求,请求载体为爬虫程序。
    • UA检测:相关的门户网站通过检测请求该网站的载体身份来辨别该请求是否为爬虫程序,如果是,则网站数据请求失败。因为正常用户对网站发起的请求的载体一定是基于某一款浏览器,如果网站检测到某一请求载体身份标识不是基于浏览器的,则让其请求失败。因此,UA检测是我们整个课程中遇到的第二种反爬机制,第一种是robots协议
    • UA伪装:通过修改/伪装爬虫请求的User-Agent来破解UA检测这种反爬机制,具体实现见下属代码:
    import requests
    
    url = 'https://www.baidu.com/s'
    word = input('>>>').strip()
    param = {
        'wd': word
    }
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    # params 自动将字典的键值对拼接到url后
    # url = https://www.baidu.com/s?wd=百度
    response = requests.get(url=url, params=param, headers=headers)
    # print(response, type(response))
    page_text = response.text
    print(page_text)
    file_path = word + '.html'
    with open(file_path, mode='w', encoding='utf-8') as f1:
        f1.write(page_text)
        
    # 如果发生乱码可以指定response.encoding = 'utf-8'
    

2. 基于requests模块的post请求

  • 需求:百度翻译

    import requests
    import json
    # 指定url
    url = 'https://fanyi.baidu.com/sug'
    # post参数处理
    word = input('>>>').strip()
    data = {
        'kw': word
    }
    # 进行UA伪装
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    # 发送请求
    response = requests.post(url=url, data=data, headers=headers)
    # 获取响应数据 (如果响应回来的数据为json,则可以直接调用响应对象的json方法获取json对象数据)
    dic_obj = response.json()
    print(dic_obj)
    file_name = word+'.json'
    with open(file_name, mode='w', encoding='utf-8') as f1:
        # ensure_ascii = False 不指定使用ASCII码
        json.dump(dic_obj, f1, ensure_ascii=False)
    

3.基于requests模块ajax的get请求

  • 需求:爬取豆瓣电影分类排行榜 https://movie.douban.com/中的电影详情数据

    import requests
    import json
    # 指定url
    url = 'https://movie.douban.com/j/chart/top_list'
    # UA伪装
    header = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    # get参数处理
    start = input('请输入开始数目:').strip()
    limit = input('请输入所需数量:').strip()
    param = {
        'type': '5',
        'interval_id': '100:90',
        'action': '',
        'start': start,
        'limit': limit
    }
    # 发送get请求
    response = requests.get(url=url, params=param, headers=header)
    # 获取响应数据
    result = response.json()
    print(result)
    # 持久化存储
    with open('douban.json', mode='w', encoding='utf-8') as f1:
        json.dump(result, f1, ensure_ascii=0)
        
    # json.dumps 序列化时对中文默认使用的ascii编码.想输出真正的中文需要指定ensure_ascii=False
    

4. 基于requests模块ajax的post请求

  • 需求:爬取肯德基餐厅查询http://www.kfc.com.cn/kfccda/index.aspx中指定地点的餐厅数据

    import requests
    
    url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword'
    
    header = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    word = input('请输入城市:').strip()
    num = 1
    while 1:
        data = {
            'cname': '',
            'pid': '',
            'keyword': word,
            'pageIndex': str(num),
            'pageSize': '10'
        }
    
        response = requests.post(url=url, data=data, headers=header).json()
        if not response['Table1']:
            break
        # print(response['Table1'])
        for info in response['Table1']:
            print(info['storeName'], info['addressDetail'])
        num += 1
    

5. 综合练习

  • 需求:爬取国家药品监督管理总局中基于中华人民共和国化妆品生产许可证相关数据http://scxk.nmpa.gov.cn:81/xk/

    import requests
    import json
    url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList'
    
    header = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    page = input('请输入要获取几页数据:').strip()
    for i in range(1, int(page)+1):
        data = {
            'on': 'true',
            'page': i,
            'pageSize': '15',
            'productName': '',
            'conditionType': '1',
            'applyname': '',
            'applysn': ''
        }
        response = requests.post(url=url, data=data, headers=header)
        result = response.json()
    
        for j in result['list']:
            url1 = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById'
            data = {
                'id': j['ID']
            }
            result_dic = requests.post(url=url1, data=data, headers=header).json()
            print(result_dic)
            with open('化妆品.json', 'a', encoding='utf-8') as f1:
                json.dump(result_dic, f1, ensure_ascii=False)
    

6. 作业

3.3 响应内容的获取

  1. response.text # 通过文本的形式获取响应内容
  2. response.content # 通过content获取的内容便是二进制类型
  3. response.json() # 获取json格式的内容

4. 数据解析

1. python如何实现数据解析

  • 正则表达式
  • bs4解析
  • xpath解析

2. 数据解析原理

  1. 进行指定标签的定位
  2. 对标签或者标签对应的属性中存储的数据值进行提取(解析)

3. 使用正则爬取糗事百科

1. 正则用法

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

2. 练习:糗事百科

import requests
import re
import os
url = 'https://www.qiushibaike.com/imgrank/page/%s/'

header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
}

start_page = input('请输入起始页码:').strip()
end_page = input('请输入结束页码:').strip()
if not os.path.exists('./img'):
    os.mkdir('./img')
for i in range(int(start_page), int(end_page) + 1):
    new_url = url % i
    # print(new_url)
    response = requests.get(new_url, headers=header).text
    # 提取图片路径
    img_src_list = re.findall(r'<div class="thumb">.*?<img src="(.*?)" .*?="">.*?</div>', response, re.S)
    print(img_src_list)
    for j in img_src_list:
        img_src = 'https:' + j
        print(img_src)
        ret = requests.get(url=img_src, headers=header).content
        file_name = img_src.split('/')[-1]
        file_path = os.path.join('./img', file_name)

        with open(file_path, 'wb') as f1:
            f1.write(ret)

4. 使用bs4进行数据解析

  • 环境安装

    - 需要将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一模一样
    - 需要安装:pip install bs4
         bs4在使用时候需要一个第三方库,把这个库也安装一下
         pip install lxml
    
  • bs4的基础使用

    使用流程:       
        - 导包:from bs4 import BeautifulSoup
        - 使用方式:可以将一个html文档,转化为BeautifulSoup对象,然后通过对象的方法或者属性去查找指定的节点内容
            (1)转化本地文件:
                 - soup = BeautifulSoup(open('本地文件'), 'lxml')
            (2)转化网络文件:
                 - soup = BeautifulSoup('字符串类型或者字节类型', 'lxml')
            (3)打印soup对象显示内容为html文件中的内容
    基础巩固:
        (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")  找到第一个a标签,并且title=‘xxx'
            - soup.find('a', alt="xxx")
            - soup.find('a', class_="xxx")
            - soup.find('a', id="xxx")
        (5)find_all:找到所有符合要求的标签
            - 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选择器返回永远是列表,需要通过下标提取指定的对象
    
  • 练习:使用bs4实现将诗词名句网站中三国演义小说的每一章的内容爬去到本地磁盘进行存储

    http://www.shicimingju.com/book/sanguoyanyi.html

    import requests
    from bs4 import BeautifulSoup
    
    url = 'http://www.mingzhuxiaoshuo.com/mingqing/4/'
    
    header = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    
    response = requests.get(url=url, headers=header)
    response.encoding = 'gbk'  # 需要制定编码,不然会乱码
    page_text = response.text
    # print(page_text)
    page_soup = BeautifulSoup(page_text, 'lxml')
    a_list = page_soup.select('.list > ul > li > a')
    # print(a_list)
    with open('text/红楼梦.txt', 'a', encoding='utf-8') as f1:
        for i in a_list:
            # 获取章节标题
            title = i.text
            # 获取章节链接
            content_link = 'http://www.mingzhuxiaoshuo.com' + i['href']
            # print(content_link)
            # http://www.mingzhuxiaoshuo.com/mingqing/4/23.Html
            # http://www.mingzhuxiaoshuo.com/mingqing/4/mingqing/4/23.Html
            response = requests.get(url=content_link, headers=header)
            response.encoding = 'gb2312'
            page_text = response.text
            #
            content_soup = BeautifulSoup(page_text, 'lxml')
            # (content_soup)
            content = content_soup.find('div', class_='width')
            f1.write(title + '\n' + content.text + '\n')
            print(title + '爬取完成')
    

5. xpath解析

  • 引入

    xpath解析是我们在爬虫中最常用也是最通用的一种数据解析方式,由于其高效且简介的解析方式受到了广大程序员的喜爱。在后期学习scrapy框架期间,也会再次使用到xpath解析。

  • 环境安装

    pip install lxml
    
  • 解析原理

    1. 使用通用爬虫爬取网页数据
    
    2. 实例化etree对象,且将页面数据加载到该对象中
    from lxml import etree
    
    - 本地文件:tree = etree.parse(文件名)
                    tree.xpath("xpath表达式")
    - 网络数据:tree = etree.HTML(网页内容字符串)
                    tree.xpath("xpath表达式")
    
    3. 使用xpath函数结合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
    
  • 练习:xpath爬取城市名称

    import requests
    from lxml import etree
    
    header = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    
    url = 'https://www.aqistudy.cn/historydata/'
    
    response = requests.get(url=url, headers=header).text
    tree = etree.HTML(response)
    all_city = []
    city = tree.xpath('//div[@class="hot"]/div[@class="bottom"]/ul/li | //div[@class="all"]/div[@class="bottom"]/ul//li')
    for i in city:
        # hot_city = i.xpath('./a/text()')[0]
        all_city_name = i.xpath('./a/text()')[0]
        all_city.append(all_city_name)
    print(all_city, len(all_city))
    

5. requests模块高级使用

1. 模拟登录

  • 模拟登录:爬取基于某些用户的用户信息

    构建data表单,并向对应的url发起post请求即可

2. requests模块的cookie处理

  • 在对网站进行模拟登录后,由于http协议具有无状态保存的特点,所以在发起第二次基于个人页面的请求时,服务器端并不知道该请求是基于登录状态下的请求

  • cookie:用来让服务器端记录客户端的相关状态信息。

  • requests模块处理cookie的两种方式

    • 将cookie手动从抓包工具中获取,然后封装到requests请求的headers中,将headers作用到请求方法中。(不建议)

      headers = {
          'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',
          'Cookie':'xxxxxxxxx'
      }
      
    • 创建session会话对象(和requests用法相同),使用会话对象进行请求发送。因为会话中会自动携带且处理cookie。(推荐)

      • session对象的作用
        • 该对象可以像requests一样调用get和post发起指定的请求。只不过如果在使用session发请求的过程中如果产生了cookie,则cookie会被自动存储到该session对象中,那么就意味着下次再次使用session对象发起请求,则该次请求就是携带cookie进行的请求发送。
      • 在爬虫中使用session的时候,session对象至少会被使用几次?
        • 两次。第一次使用session是为了将cookie捕获且存储到session对象中。下次的时候就是携带cookie进行的请求发送。
      #  #   #   #
      在使用会话对象发起登陆请求时,需要额外注意该请求参数:
      ’authenticity_token':'lGnVdPaEf/jfBd6dzkQFpd4idQZxO96HSvRzWIPo5099dvmZTqEx+Q13zLj'
      该参数的value值看起来像是一组密文数据,则该数据很有可能是动态变化的,因此需要动态捕获,动态赋值。如果该参数不处理的话,也是会登陆失败的。那么该值应该从那哪里动态捕获呢?这种值我们可以统一称为动态taken参数,该值一般都会动态存在与该请求对应的前台页面中(html),如果没有,就用抓包工具全局搜索。
      #  #  #  #  #  #  #
      
      import requests
      from lxml import etree
      from chaojiying import Chaojiying_Client
      
      header = {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                        'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
      }
      
      url = 'https://so.gushiwen.cn/user/login.aspx?from=http://so.gushiwen.cn/user/collect.aspx'
      
      session = requests.Session()
      
      img_response = session.get(url=url, headers=header).text
      tree = etree.HTML(img_response)
      # 获取验证码图片
      img_src = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
      print(img_src)
      img_data = session.get(url=img_src, headers=header).content
      with open('code.jpg', 'wb') as f1:
          f1.write(img_data)
      
      # 解析验证码
      chaojiying = Chaojiying_Client('AI2021', 'AI2021', '912999')  # 用户中心>>软件ID 生成一个替换 96001
      im = open('code.jpg', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
      result = chaojiying.PostPic(im, 1902)['pic_str']
      print(result)  # 1902 验证码类型
      
      # 构建表单
      data = {
          '__VIEWSTATE': 'ftxM+fR3ZCAtrfDkZR8e8lA0kwVYEV4gr36qDMM00n/gBoRHu0VvV1yWUNl4uyExy9Nea67tKNFbBafHbf8lKLU0UHxeNVumTcZ5IQvn+vGH0MlWDce1739u1Pg=',
          '__VIEWSTATEGENERATOR': 'C93BE1AE',
          'from': 'http://so.gushiwen.cn/user/collect.aspx',
          'email': '246888078@qq.com',
          'pwd': 'zjh123456',
          'code': result,
          'denglu': '登录',
      }
      
      url_post = 'https://so.gushiwen.cn/user/login.aspx?from=http%3a%2f%2fso.gushiwen.cn%2fuser%2fcollect.aspx'
      # 发起post请求后,cookie值会被自动保存/携带在session中,再次用session对个人主页发起get请求即可
      response = session.post(url=url_post, data=data, headers=header)  
      print(response.status_code)
      print(session)
      url_get = 'https://so.gushiwen.cn/user/collect.aspx'
      page_text = session.get(url=url_get, headers=header).text
      with open('html/gushiwen.html', 'w', encoding='utf-8') as f2:
          f2.write(page_text)
      

3. requests模块的IP代理

  • 代理:破解封IP这种反爬机制。
  • 什么是代理:
    • 代理服务器。
  • 代理的作用:
    • 突破自身IP访问的限制。
    • 隐藏自身真实IP
  • 在爬虫中为什么需要使用代理服务器?
    • 如果我们的爬虫在短时间内对服务器发起了高频的请求,那么服务器会检测到这样的一个异常的行为请求,就会将该请求对应设备的ip禁掉,就以为这client设备无法对服务器端再次进行请求发送。(ip被禁掉了)
    • 如果ip被禁,我们就可以使用代理服务器进行请求转发,破解ip被禁的反爬机制。因为使用代理后,服务器端接受到的请求对应的ip地址就是代理服务器而不是我们真正的客户端的
  • 代理ip的类型

    • http:应用到http协议对应的url中
    • https:应用到https协议对应的url中
  • 代理ip的匿名度:

    • 透明:服务器知道该次请求使用了代理,也知道请求对应的真实ip
    • 匿名:知道使用了代理,不知道真实ip
    • 高匿:不知道使用了代理,更不知道真实的ip
import requests
import random

header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
}

proxy_list = [
    # {'http': '183.166.20.149:9999'},
    # {'http': '113.121.71.36:9999'},
    # {'http': '123.169.125.173:9999'},
    {'http': '180.97.34.35:80'},
    # {}
]

url = 'http://www.baidu.com/s?wd=ip'
# url1 = 'http://www.ip138.com/'
proxy = random.choice(proxy_list)
print(proxy)

response = requests.get(url=url, headers=header, proxies=proxy).text
with open('./html/代理IP.html', 'w', encoding='utf-8') as f1:
    f1.write(response)

4. 验证码识别平台

  • 验证码的识别

    • 基于线上的打码平台识别验证码

    • 打码平台

      • 1.超级鹰(使用):

        http://www.chaojiying.com/about.html

        • 1.注册【用户中心的身份】
        • 2.登录(用户中心的身份)
          • 1.查询余额,请充值
          • 2.创建一个软件ID(899370)
          • 3.下载示例代码
      • 2.打码兔

6. 异步爬虫

  • 目的:在爬虫中使用异步实现高性能的数据爬取操作。

1. 实现异步爬虫的方式

1.1 多线程,多进程(不建议)

优点:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步执行

弊端:无法无限制的开启多线程或者多进程

import time
from threading import Thread
# from multiprocessing import Process
def get(s):
    print(f'正在下载: {s}')
    time.sleep(2)
    print(f'下载完成:{s}')

if __name__ == '__main__':
    data_list = ['xiazai', 'ss', 'aa', 'bb', 'cc']
    t_list = []
    start_time = time.time()
    for i in data_list:
        t1 = Thread(target=get, args=(i, ))
        t_list.append(t1)
        t1.start()
    for j in t_list:
        j.join()
    end_time = time.time()
    print('主线程', end_time-start_time)

1.2 线程池、进程池(适当的使用)

优点:我们可以降低系统对进程或者线程创建和销毁的一个频率,从而很好的降低系统的开销

弊端:池中线程或进程的数量是有上限

需求:梨视频爬取

import requests
import random
import re
import time
from lxml import etree
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
}
url = 'https://www.pearvideo.com/category_59'
page_text = requests.get(url=url, headers=header).text
tree = etree.HTML(page_text)
li_list = tree.xpath('//*[@id="listvideoListUl"]/li')
# print(detail_url_list)
details_url = []
for li in li_list:
    # 详情页url
    detail_url = 'https://www.pearvideo.com/' + li.xpath('./div/a/@href')[0]
    video_name = li.xpath('./div/a/div[2]/text()')[0] + '.mp4'
    # print(detail_url, video_name)

    # 对详情页发起请求
    cont_id = detail_url.split('_')[1]
    detail_text = requests.get(url=detail_url, headers=header).text

    detail_header = {
        'Referer': 'https://www.pearvideo.com/video_' + cont_id,
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
    }
    # get请求参数
    param = {
        'contId': cont_id,
        'mrd': str(random.random())
    }

    # url_get = 'https://www.pearvideo.com/videoStatus.jsp?contId=1720457&mrd=0.025606628697027567'
    url_get = 'https://www.pearvideo.com/videoStatus.jsp'
    json_dic = requests.get(url=url_get, params=param, headers=detail_header).json()
    # print(json_dic['videoInfo']['videos']['srcUrl'])
    video_url = json_dic['videoInfo']['videos']['srcUrl']
    # 请求到的video_url
    # https://video.pearvideo.com/mp4/third/20210217/1613800921969-12719568-205600-hd.mp4
    # 实际视频url
    # https://video.pearvideo.com/mp4/third/20210217/cont-1720457-12719568-205600-hd.mp4
    # 进行一个替换
    true_video_url = re.sub(r'\d{13}-', f'cont-{cont_id}-', video_url)

    video_info = {
        'name': video_name,
        'url': true_video_url
    }

    details_url.append(video_info)


# print(details_url)

def download_video(info):
    # 向视频url发起请求
    print(f'{info["name"]}开始下载。。。')
    data = requests.get(url=info['url'], headers=header).content

    with open(f'./video/{info["name"]}', 'wb') as f1:
        f1.write(data)
        print(f'{info["name"]}下载完成。')

if __name__ == '__main__':
    thread_poor = ThreadPoolExecutor(max_workers=5)
    start = time.time()
    # print(details_url)
    for i in details_url:
        thread_poor.submit(download_video, i)
    thread_poor.shutdown()

    end = time.time()
    print(f'共耗时:{end - start}秒')

1.3 异步协程(推荐)

  • asyncio模块

    import asyncio
    import time
    
    async def func1():
        print('hello1')
        await asyncio.sleep(2)
        print('hello1')
    
    async def func2():
        print('hello2')
        await asyncio.sleep(1)
        print('hello2')
    
    async def func3():
        print('hello3')
        await asyncio.sleep(4)
        print('hello3')
    
    async def main():
        tasks = [func1(), func2(), func3()]
        await asyncio.wait(tasks)  # await 也可以直接放在func1()前面
    
    if __name__ == '__main__':
        t1 = time.time()
        asyncio.run(main())
        t2 = time.time()
        print(t2 - t1)
    
  • aiohttp模块

    import asyncio
    import time
    import aiohttp
    
    urls = [
        'https://pic.netbian.com/uploads/allimg/210505/233149-162022870979b1.jpg',
        'https://pic.netbian.com/uploads/allimg/210412/235103-1618242663736c.jpg',
        'https://pic.netbian.com/uploads/allimg/210314/223339-1615732419fa74.jpg'
    ]
    
    async def download(url):
        name = url.split('/')[-1]
        # session = aiohttp.ClientSession()  # 相当于requests模块
        async with aiohttp.ClientSession() as session:
            async with session.get(url=url) as response:
                # 文件读写也是IO操作,可以使用aiofiles模块
                with open(name, mode='wb') as f:
                    f.write(await response.content.read())  # 读取内容是异步的,需要await挂起
                    # response.text() 等同于requests中response的text
                    # response.json() 等同于requests中response的json()
                    # response.content.read() 等同于requests中response的content
    
    async def main():
        tasks = []
        for url in urls:
            d = download(url)
            tasks.append(d)
        await asyncio.wait(tasks)
    
    if __name__ == '__main__':
        asyncio.run(main())
    
  • aiofiles模块

    # 异步文件操作
    # pip install aiofiles
    
    # 基本用法
    import asyncio
    import aiofiles
    
    async def wirte_demo():
        # 异步方式执行with操作,修改为 async with
        async with aiofiles.open("text.txt","w",encoding="utf-8") as fp:
            await fp.write("hello world ")
            print("数据写入成功")
    
    async def read_demo():
        async with aiofiles.open("text.txt","r",encoding="utf-8") as fp:
            content = await fp.read()
            print(content)
    
    async def read2_demo():
        async with aiofiles.open("text.txt","r",encoding="utf-8") as fp:
            # 读取每行
            async for line in fp:
                print(line)
    if __name__ == "__main__":
        asyncio.run(wirte_demo())
        asyncio.run(read_demo())
        asyncio.run(read2_demo())
    

7. selenium模块

1. 简介

selenium最初是一个自动化测试工具,而爬虫中使用它主要是为了解决requests无法直接执行JavaScript代码的问题 selenium本质是通过驱动浏览器,完全模拟浏览器的操作,比如跳转、输入、点击、下拉等,来拿到网页渲染之后的结果,可支持多种浏览器

2. 环境安装

  • selenium安装

    pip install selenium
    
  • 浏览器的驱动程序

    自行下载

3. 简单使用

  • 示例代码

    from selenium import webdriver
    from time import sleep
    # 后面是你的浏览器驱动位置,记得前面加r'','r'是防止字符转义的
    driver = webdriver.Chrome(r'./chromedriver')
    # 用get打开百度页面
    driver.get("http://www.baidu.com")
    # 查找页面的“设置”选项,并进行点击
    driver.find_elements_by_link_text('设置')[0].click()
    sleep(2)
    # # 打开设置后找到“搜索设置”选项,设置为每页显示50条
    driver.find_elements_by_link_text('搜索设置')[0].click()
    sleep(2)
    # 选中每页显示50条
    m = driver.find_element_by_id('nr')
    sleep(2)
    m.find_element_by_xpath('//*[@id="nr"]/option[3]').click()
    m.find_element_by_xpath('.//option[3]').click()
    sleep(2)
    # 点击保存设置
    driver.find_elements_by_class_name("prefpanelgo")[0].click()
    sleep(2)
    # 处理弹出的警告页面   确定accept() 和 取消dismiss()
    driver.switch_to_alert().accept()
    sleep(2)
    # 找到百度的输入框,并输入 美女
    driver.find_element_by_id('kw').send_keys('美女')
    sleep(2)
    # 点击搜索按钮
    driver.find_element_by_id('su').click()
    sleep(2)
    # 在打开的页面中找到“Selenium - 开源中国社区”,并打开这个页面
    driver.find_elements_by_link_text('美女_百度图片')[0].click()
    sleep(3)
    # 关闭浏览器
    driver.quit()
    

3.1 元素定位

find_element_by_id()  # 通过元素id定位
find_element_by_name()  # 通过元素的name属性值定位
find_element_by_class_name()  #  通过classname进行定位
find_element_by_tag_name()  # 通过标签定位
find_element_by_link_text()  # 通过完整超链接定位
find_element_by_partial_link_text()  # 通过部分链接定位
find_element_by_xpath()  # 通过xpath表达式定位
find_element_by_css_selector()   # 通过css选择器进行定位
  • 注意
  1. find_element_by_xxx找的是第一个符合条件的标签,find_elements_by_xxx找的是所有符合条件的标签。

  2. 根据ID、CSS选择器和XPath获取,它们返回的结果完全一致。

  3. 另外,Selenium还提供了通用方法find_element(),它需要传入两个参数:查找方式By和值。实际上,它就是find_element_by_id()这种方法的通用函数版本,比如find_element_by_id(id)就等价于find_element(By.ID, id),二者得到的结果完全一致。

3.2 节点交互

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

from selenium import webdriver
import time
browser = webdriver.Chrome(r'驱动程序路径')
browser.get('https://www.taobao.com')
input = browser.find_element_by_id('q')
input.send_keys('MAC')
time.sleep(1)
input.clear()
input.send_keys('IPhone')
button = browser.find_element_by_class_name('btn-search')
button.click()
browser.quit()

3.3 执行js代码

对于某些操作,Selenium API并没有提供。比如,下拉进度条,它可以直接模拟运行JavaScript,此时使用execute_script()方法即可实现

from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.jd.com/')
browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
browser.execute_script('alert("123")')

3.4 获取页面源码数据

通过page_source属性可以获取网页的源代码(类型为str),接着就可以使用解析库(如正则表达式、Beautiful Soup、pyquery等)来提取信息了。

import time
from selenium import webdriver
from lxml import etree

# bor = webdriver.Edge(executable_path=r'./driver/msedgedriver.exe')
bor = webdriver.Chrome(executable_path=r'./driver/chromedriver.exe')

bor.get('http://scxk.nmpa.gov.cn:81/xk/')

page_text = bor.page_source
print(type(page_text))  # str

tree = etree.HTML(page_text)
li_list = tree.xpath('//*[@id="gzlist"]/li')
for li in li_list:
    name = li.xpath('./dl/@title')[0]
    print(name)

time.sleep(3)
bor.quit()

3.5 前进与后退

#模拟浏览器的前进后退
import time
from selenium import webdriver
browser=webdriver.Chrome()
browser.get('https://www.baidu.com')
browser.get('https://www.taobao.com')
browser.get('http://www.sina.com.cn/')
# back 后退
browser.back()
time.sleep(10)
# forward 前进
browser.forward()
browser.close()

3.6 iframe和动作链

from selenium import webdriver
from time import sleep
# 导入动作链对应的类
from selenium.webdriver import ActionChains

bro = webdriver.Chrome(executable_path='./chromedriver')

bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')

# 如果定位的标签是存在于iframe标签之中的则必须通过如下操作在进行标签定位
bro.switch_to.frame('iframeResult')  # 切换浏览器标签定位的作用域
div = bro.find_element_by_id('draggable')

# 动作链
action = ActionChains(bro)
# 点击长按指定的标签
action.click_and_hold(div)

for i in range(5):
    # perform()立即执行动作链操作
    # move_by_offset(x,y):x水平方向 y竖直方向
    action.move_by_offset(17, 0).perform()
    sleep(0.5)

# 释放动作链
action.release()

bro.quit()

# 注意
# 如果两个iframe是平行关系,需要先回到主框架,如果是嵌套,则直接进入即可
## 回到主框架
# broswer.switch_to.default_content()

3.7 无头浏览器和规避检测

from selenium import webdriver
from time import sleep
#实现无可视化界面(无头浏览器)
from selenium.webdriver.chrome.options import Options
#实现规避检测
from selenium.webdriver import ChromeOptions

#实现无可视化界面的操作
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')

#实现规避检测
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])

#如何实现让selenium规避被检测到的风险
bro = webdriver.Chrome(executable_path='./chromedriver',chrome_options=chrome_options,options=option)

#无可视化界面(无头浏览器) phantomJs
bro.get('https://www.baidu.com')
sleep(1)
print(bro.page_source)
browser.save_screenshot('baidu.png')
sleep(2)
bro.quit()

4. QQ空间模拟登录

import time
from selenium import webdriver
from selenium.webdriver import ActionChains

broswer = webdriver.Chrome(executable_path='./driver/chromedriver.exe')
broswer.maximize_window()
broswer.get(url='https://i.qq.com/')

broswer.switch_to.frame('login_frame')
broswer.find_element_by_id('switcher_plogin').click()
time.sleep(1)
broswer.find_element_by_id('u').send_keys('246888078')
time.sleep(1)
broswer.find_element_by_id('p').send_keys('abc123...')
time.sleep(1)
# 点击登录按钮
broswer.find_element_by_id('login_button').click()

# 回到主框架
# broswer.switch_to.default_content()
time.sleep(1)
# iframe = broswer.find_element_by_xpath('//*[@id="tcaptcha_iframe"]')
# broswer.switch_to.frame(iframe)
# 滑块验证需要再次将作用域切换,如果两个iframe是平行关系,需要先回到主框架,如果是嵌套,则直接进入即可
broswer.switch_to.frame('tcaptcha_iframe')
time.sleep(1)

btn = broswer.find_element_by_id('tcaptcha_drag_thumb')
# 动作链
action = ActionChains(broswer)
# 点击长按滑块按钮
action.click_and_hold(btn)
# 滑动
# for i in range(5):
# perform()立即执行动作链操作
# move_by_offset(x,y):x水平方向 y竖直方向
# for i in range(5):
#     action.move_by_offset(xoffset=34, yoffset=0).perform()
#     time.sleep(0.2)
action.move_by_offset(xoffset=170, yoffset=0).perform()
# 释放动作链
action.release()
time.sleep(5)

page_text = broswer.page_source
with open('./html/QQ.html', 'w', encoding='utf-8') as f1:
    f1.write(page_text)
time.sleep(1)
broswer.quit()

5. 12306模拟登录

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver import ChromeOptions
import time
from PIL import Image
from chaojiying import Chaojiying_Client

# 实现规避检测
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])

# 实现让selenium规避 被检测到的风险
bro = webdriver.Chrome(executable_path='./driver/chromedriver.exe', options=option)

bro.maximize_window()  # 浏览器最大化
bro.get(url='https://www.12306.cn/index/')
time.sleep(1)
bro.find_element_by_xpath('//*[@id="J-header-login"]/a[1]').click()
time.sleep(1)

bro.find_element_by_xpath('/html/body/div[2]/div[2]/ul/li[2]/a').click()
time.sleep(1)

# bro.execute_script("document.body.style.zoom='0.80'")  网页比例缩放
time.sleep(2)
bro.save_screenshot('./code_img/12306.png')
# 定位到验证码图片
code_img_ele = bro.find_element_by_xpath('//*[@id="J-loginImg"]')
location = code_img_ele.location  #获取验证码图片左上角的坐标 x,y
print('location:', location)

size = code_img_ele.size  # 验证码标签对应的长和宽
print('size:', size)
# 左上角和右下角坐标
rangle = (
    int(location['x']), int(location['y']), int(location['x'] + size['width']), int(location['y'] + size['height']))
# 至此验证码图片区域就确定下来了

# print(rangle)
i = Image.open('./code_img/12306.png')
code_img_name = './code_img/code.png'
# crop根据指定区域进行图片裁剪(需要将电脑缩放比例改成100%才能裁剪成功)
frame = i.crop(rangle)
frame.save(code_img_name)
# bro.execute_script("document.body.style.zoom='1.00'")
time.sleep(1)

chaojiying = Chaojiying_Client('AI2021', 'AI2021', '912999')  # 用户中心>>软件ID 生成一个替换 96001
im = open('./code_img/code.png', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
result = chaojiying.PostPic(im, 9004)['pic_str']
# print(chaojiying.PostPic(im, 9004))
print(result)  # 1902 验证码类型  官方网站>>价格体系 3.4+版 print 后要加()

coord = []
if '|' in result:
    li1 = result.split('|')
    for i in li1:
        coord.append(i.split(','))
    # print(coord)
else:
    coord.append(result.split(','))
print(coord)
# 动作链
# action = ActionChains(bro)
for i in coord:
    x, y = int(i[0]), int(i[1])
    # 此处可能是每次循环都需要一个动作链,所以直接写在循环内,不能在循环外先定义
    ActionChains(bro).move_to_element_with_offset(code_img_ele, x, y).click().perform()
    # action.move_to_element_with_offset(code_img_ele, x, y).click().perform()
    time.sleep(0.5)
# action.release()

bro.find_element_by_id('J-userName').send_keys('用户名')
time.sleep(1)
bro.find_element_by_id('J-password').send_keys('密码')
time.sleep(1)
bro.find_element_by_id('J-login').click()
time.sleep(3)

# btn = bro.find_element_by_id('nc_2_n1z')
# 定位到滑动按钮,实现长按并拖动
action = ActionChains(bro)
btn = bro.find_element_by_xpath('//*[@id="nc_1_n1z"]')
action.click_and_hold(btn)  # 长按
action.move_by_offset(400, 0).perform()  # 滑动
action.release()

time.sleep(3)
bro.quit()

8. scrapy框架

1. 什么是scrapy框架

  • 什么是框架?

    • 就是一个集成了很多功能并且具有很强通用性的一个项目模板。
  • 如何学习框架?

    • 专门学习框架封装的各种功能的详细用法。
  • 什么是scrapy?

    • 爬虫中封装好的一个明星框架。功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式

2. scrapy框架的基本使用

  • 环境的安装:
    • mac or linux:pip install scrapy
    • windows:
      • pip install wheel
      • 下载twisted,下载地址为http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
      • 安装twisted:pip install Twisted‑17.1.0‑cp36‑cp36m‑win_amd64.whl
      • pip install pywin32
      • pip install scrapy
        测试:在终端里录入scrapy指令,没有报错即表示安装成功!
  • 创建工程:
    • scrapy startproject ProName
  • 进入工程目录:
    • cd ProName
  • 创建爬虫文件:
    • scrapy genspider spiderName www.xxx.com
  • 编写相关操作代码
  • 执行工程:
    • scrapy crawl spiderName

爬虫文件解析:

  import scrapy
  class QiubaiSpider(scrapy.Spider):
      name = 'qiubai' #应用名称
      #允许爬取的域名(如果遇到非该域名的url则爬取不到数据)
      allowed_domains = ['https://www.qiushibaike.com/']
      #起始爬取的url
      start_urls = ['https://www.qiushibaike.com/']
      #访问起始URL并获取结果后的回调函数,该函数的response参数就是向起始的url发送请求后,获取的响应对象.该函数返回值必须为可迭代对象或者NUll 
      def parse(self, response):
          print(response.text) #获取字符串类型的响应内容
          print(response.body)#获取字节类型的相应内容

配置文件setting.py的修改:

 22行:
 ROBOTSTXT_OBEY = False  #可以忽略或者不遵守robots协议
 LOG_LEVEL = 'ERROR'  # 只打印error及以上的日志

3. scrapy基于xpath数据解析操作

  • 糗事百科段子数据的爬取

    import scrapy
    
    class QsbkSpider(scrapy.Spider):
        name = 'qsbk'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['https://www.qiushibaike.com/text/']
    
        def parse(self, response, **kwargs):
            div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
            for i in div_list:
                # xpath返回的是列表,但是列表元素一定是Selector类型的对象
        		# extract可以将Selector对象中data参数存储的字符串提取出来
                # name = i.xpath('./div[1]/a[2]/h2/text()')[0].extract()
                name = i.xpath('./div[1]/a[2]/h2/text()').extract_first() #在保证列表中只有一个对象时,可以使用这个
                #列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
                content = i.xpath('./a[1]/div/span//text()').extract()
                print(name, content)
                break
    

4. scrapy的数据持久化存储

1. 基于终端指令的持久化存储

  • 保证爬虫文件的parse方法中有可迭代类型对象(通常为列表or字典)的返回,该返回值可以通过终端指令的形式写入指定格式的文件中进行持久化操作。

    import scrapy
    
    
    class QsbkSpider(scrapy.Spider):
        name = 'qsbk'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['https://www.qiushibaike.com/text/']
    
        def parse(self, response, **kwargs):
            div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
            data_list = []
            for i in div_list:
                name = i.xpath('./div[1]/a[2]/h2/text()').extract_first()
                content = i.xpath('./a[1]/div/span//text()').extract()
                content = ''.join(content)
                # print(name, content)
                data = {
                    'name': name,
                    'content': content
                }
                data_list.append(data)
            return data_list
    
  • 执行指令:

    • 执行输出指定格式进行存储:将爬取到的数据写入不同格式的文件中进行存储

      scrapy crawl 爬虫名称 -o xxx.json
      scrapy crawl 爬虫名称 -o xxx.xml
      scrapy crawl 爬虫名称 -o xxx.csv
      
      如果保存的文件数据出现乱码:
      在setting文件中添加:
      # FEED_EXPORT_ENCODING = 'utf-8' 或
      FEED_EXPORT_ENCODING = 'gb18030'
      
      注意:持久化存储对应的文本文件的类型只可以为:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle'
      

2. 基于管道的持久化存储

  • scrapy框架中已经为我们专门集成好了高效、便捷的持久化操作功能,我们直接使用即可。要想使用scrapy的持久化操作功能,我们首先来认识如下两个文件:

    • items.py:数据结构模板文件。定义数据属性。
    • pipelines.py:管道文件。接收数据(items),进行持久化操作。
  • 持久化流程:

    • 1.爬虫文件爬取到数据后,需要将数据封装到items对象中。
    • 2.使用yield关键字将items对象提交给pipelines管道进行持久化操作。
    • 3.在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码将item对象中存储的数据进行持久化存储
    • 4.settings.py配置文件中开启管道

    示例:将糗事百科首页中的段子和作者数据爬取下来,然后进行持久化存储

    爬虫文件代码:

    import scrapy
    from qsbk_pro.items import QsbkProItem
    
    class QsbkSpider(scrapy.Spider):
        name = 'qsbk'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['https://www.qiushibaike.com/text/']
    
        def parse(self, response, **kwargs):
            div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
            data_list = []
            for i in div_list:
                name = i.xpath('./div[1]/a[2]/h2/text()').extract_first()
                content = i.xpath('./a[1]/div/span//text()').extract()
                content = ''.join(content)
                # print(name, content)
                item = QsbkProItem()
                item['name'] = name
                item['content'] = content
    
                yield item  # 将item提交给了管道
    

    items文件代码:

    import scrapy
    
    class QsbkProItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        name = scrapy.Field()
        content = scrapy.Field()
    

pipelines.py文件代码:

  class QsbkProPipeline:
      def __init__(self):
          self.fp = None
  	# 重写父类的一个方法,该方法只在开始爬虫的时候调用一次
      # 注意,此方法函数名必须这么写!!!!
      def open_spider(self, spider):
          print('开始。。。')
          self.fp = open(r'D:\study\spider\text\qsbk.txt', 'w', encoding='utf-8')
  	# 专门处理item对象中的数据,就是爬虫文件提交过来的item对象
      # 该方法每接收一个item对象就会被调用一次
      def process_item(self, item, spider):
          self.fp.write(f'{item["name"].strip()}:{item["content"].strip()}\n')
          
          return item  # 会将item对象传递给下一个即将被执行的管道类
  
      def close_spider(self, spider):
          self.fp.close()
          print('结束。。')

3. 面试题:将爬取到的数据一份存储到本地一份存储到数据库,如何实现?

  • 管道文件中一个管道类对应的是将数据存储到一种平台(载体)
  • 爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受
  • process_item中的return item表示将item传递给下一个即将被执行的管道类

管道文件代码:

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


# useful for handling different item types with a single interface
from itemadapter import ItemAdapter
import pymysql

# 这个管道类写入本地
class QsbkProPipeline:
    def __init__(self):
        self.fp = None

    def open_spider(self, spider):
        print('开始。。。')
        self.fp = open(r'D:\study\spider\text\qsbk.txt', 'w', encoding='utf-8')

    def process_item(self, item, spider):
        self.fp.write(f'{item["name"].strip()}:{item["content"].strip()}\n')

        return item

    def close_spider(self, spider):
        self.fp.close()
        print('结束。。')

# 写入MySQL数据库
class MysqlPipeline:
    def __init__(self):
        self.conn = None
        self.cursor = None

    def open_spider(self, spider):
        print('MySQL开始写入。。')
        self.conn = pymysql.Connect(
            host='127.0.0.1',
            port=3306,
            user='root',
            password='123',
            db='plus',
            charset='utf8'
        )

    def process_item(self, item, spider):
        self.cursor = self.conn.cursor()
        try:
            sql = 'insert into qsbk values ("%s","%s")'
            self.cursor.execute(sql % (item['name'], item['content']))
            self.conn.commit()
        except Exception:
            self.conn.rollback()

        return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()
        print('MySQL结束')

settings.py文件配置:

# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
   'qsbk_pro.pipelines.QsbkProPipeline': 300,
   'qsbk_pro.pipelines.MysqlPipeline': 301,
    # 数字表示优先级,数字越小,优先级越高,越先执行对应的管道类。
}

5. 全站数据爬取

  • 大部分的网站展示的数据都进行了分页操作,那么将所有页码对应的页面数据进行爬取就是爬虫中的全站数据爬取。

  • 基于scrapy如何进行全站数据爬取呢?

    • 将每一个页码对应的url存放到爬虫文件的起始url列表(start_urls)中。(不推荐)

    • 使用Request方法手动发起请求。(推荐)

      - yield scrapy.Request(url,callback):# GET请求
          - callback指定解析函数,用于解析数据
      
      - yield scrapy.FormRequest(url,callback,formdata):  # POST请求
          - formdata:字典,请求参数
          
      - 为什么start_urls列表中的url会被自动进行get请求的发送?
          - 因为列表中的url其实是被start_requests这个父类方法实现的get请求发送
          def start_requests(self):
              for u in self.start_urls:
                 yield scrapy.Request(url=u,callback=self.parse)
                 
      - 如何将start_urls中的url默认进行post请求的发送?
          - 重写start_requests方法即可
          def start_requests(self):
              for u in self.start_urls:
                 yield scrapy.FormRequest(url=u,callback=self.parse)
      
      # 爬取彼岸图网图片的名称
      import scrapy
      
      class BianSpider(scrapy.Spider):
          name = 'bian'
          # allowed_domains = ['www.xxx.com']
          start_urls = ['http://pic.netbian.com/4kqiche/']
      
          # 通用url模板
          page_num = 2
          url = 'http://pic.netbian.com/4kqiche/index_%s.html'
          # print(url)
      
          def parse(self, response, **kwargs):
              li_list = response.xpath('//*[@id="main"]/div[3]/ul/li')
              for li in li_list:
                  name = li.xpath('./a/b/text()').extract_first()
                  print(name)
      
              if self.page_num <= 20:
                  new_url = format(self.url % self.page_num)
                  self.page_num += 1
                  print(new_url)
                  # 递归爬取数据:callback参数的值为回调函数(将url请求后,
                  # 得到的相应数据继续进行parse解析),递归调用parse函数
                  yield scrapy.Request(url=new_url, callback=self.parse)
      

6. scrapy的五大核心组件

  • 引擎(Scrapy)
    • 用来处理整个系统的数据流处理, 触发事务(框架核心)
  • 调度器(Scheduler)
    • 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    • 用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    • 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    • 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。

7. 请求传参

import scrapy
from recruit_pro.items import RecruitProItem


class RecruitSpider(scrapy.Spider):
    name = 'recruit'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['http://tech.163.com/special/gd2016/']
    page_num = 2
    url = 'http://tech.163.com/special/gd2016_%s/'
	# 对详情页进行解析,并将item传递给管道,管道负责将数据保存到本地
    def parse_detail(self, response, **kwargs):
        item = response.meta['item']
        news_detail = response.xpath('//*[@id="content"]/div[2]/p[1]//text()').extract()
        news_detail = ''.join(news_detail)
        item['news_detail'] = news_detail
        # print(news_detail)
        yield item  # 将item发送给管道
	# 对首页进行解析,解析出名称和单个对应的详情页的url
    def parse(self, response, **kwargs):
        li_list = response.xpath('//*[@id="news-flow-content"]/li')
        for li in li_list:
            item = RecruitProItem()
            news_name = li.xpath('./div[1]/h3/a/text()').extract_first()
            # print(news_name)
            item['news_name'] = news_name
            detail_url = li.xpath('./div[1]/h3/a/@href').extract_first()
            # 
            # 请求传参:meta={},可以将meta字典传递给请求对应的回调函数
            yield scrapy.Request(url=detail_url, callback=self.parse_detail, meta={'item': item})
            # break
        # 分页操作
        if self.page_num <= 3:
            new_url = format(self.url % self.page_num)
            self.page_num += 1
            print(new_url)
            yield scrapy.Request(url=new_url, callback=self.parse)

8. 基于文件下载的管道类

  • 在scrapy中我们之前爬取的都是基于字符串类型的数据,那么要是基于图片数据的爬取,那又该如何呢?

    • 其实在scrapy中已经为我们封装好了一个专门基于图片请求和持久化存储的管道类ImagesPipeline,那也就是说如果想要基于scrapy实现图片数据的爬取,则可以直接使用该管道类即可。

      注意:图片懒加载

  • ImagesPipeline使用流程

    • 在配置文件中进行如下配置:
      IMAGES_STORE = ‘./imgs’:表示最终图片存储的目录 (没有此文件夹会自动创建)

      如果是下载文件,例如视频,模块导入时为from scrapy.pipelines.files import FilesPipeline

      配置文件修改为:FILES_STORE = './videos'

    • 管道类的编写:

      # 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储
      
      from itemadapter import ItemAdapter
      from scrapy.pipelines.images import ImagesPipeline
      import scrapy
      
      # class ZzscProPipeline:
      #     def process_item(self, item, spider):
      #         return item
      
      #ImagesPipeline专门用于文件下载的管道类,下载过程支持异步和多线程
      class ImgPipeline(ImagesPipeline):
          # 对item中的图片url进行请求操作
          def get_media_requests(self, item, info):
              yield scrapy.Request(url=item['img_src'])
          # 定制图片的名称
          def file_path(self, request, response=None, info=None, *, item=None):
              # file_name = item['img_name'] + '.jpg'
              file_name = request.url.split('/')[-1]
              print(file_name)
              return file_name
      
          def item_completed(self, results, item, info):
              return item  #该返回值会传递给下一个即将被执行的管道类
      

9. 中间件

  • 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件。

  • 作用:我们主要使用下载中间件处理请求,一般会对请求设置随机的User-Agent ,设置随机的代理。目的在于防止爬取网站的反爬虫策略。

    • (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系列处理。比如设置请求的 User-Agent,设置代理等

      UA池:User-Agent池

      • 作用:尽可能多的将scrapy工程中的请求伪装成不同类型的浏览器身份。
      • 操作流程:
        • 1.在下载中间件中拦截请求
        • 2.将拦截到的请求的请求头信息中的UA进行篡改伪装
        • 3.在配置文件中开启下载中间件

      代理池

      • 作用:尽可能多的将scrapy工程中的请求的IP设置成不同的。
      • 操作流程:
        • 1.在下载中间件中拦截请求
        • 2.将拦截到的请求的IP修改成某一代理IP
        • 3.在配置文件中开启下载中间件
    • (2)在下载器完成将Response传递给引擎中,下载中间件可以对响应进行一系列处理。比如进行gzip解压等。

      • 拦截响应:篡改响应数据,响应对象

需求:爬取网易新闻中的新闻数据(标题和内容)

  • 1.通过网易新闻的首页解析出五大板块对应的详情页的url(没有动态加载)
  • 2.每一个板块对应的新闻标题都是动态加载出来的(动态加载)
  • 3.通过解析出每一条新闻详情页的url获取详情页的页面源码,解析出新闻内容

爬虫文件:

import scrapy
from selenium import webdriver
import os
from news_pro.items import NewsProItem


class NeteaseSpider(scrapy.Spider):
    name = 'netease'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://news.163.com/']
    models_url = []  # 四个板块的url

    def __init__(self):
        # options = webdriver.ChromeOptions()
        # options.binary_location = r"D:\软件安装\Google\Chrome\Application\chrome.exe"
        self.bro = webdriver.Chrome(executable_path=r'D:\study\spider\driver\chromedriver.exe',
                                    # chrome_options=options,
                                    service_log_path=os.devnull)
        # self.bro = webdriver.Edge(executable_path=r'D:\study\spider\driver\msedgedriver.exe')

    def parse(self, response, **kwargs):
        li_list = response.xpath('//*[@id="js_festival_wrap"]/div[3]/div[2]/div[2]/div[2]/div/ul/li')
        li_index = [3, 4, 6, 7]
        # 将四大板块的url存放到models_url
        for index in li_index:
            model_url = li_list[index].xpath('./a/@href').extract_first()
            self.models_url.append(model_url)

        # 依次对四大板块url发送请求
        for url in self.models_url:
            yield scrapy.Request(url=url, callback=self.parse_model)

    def parse_model(self, response, **kwargs):
        # 因为每个板块的新闻是动态加载,所以需要下载中间件进行响应的篡改
        # 可使用selenium模块获取动态响应数据
        # 所有新闻对应的div
        div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')
        for div in div_list:
            news_title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
            news_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()

            item = NewsProItem()
            item['news_title'] = news_title

            if news_title is not None:
                # print(news_title, news_detail_url)
                yield scrapy.Request(url=news_detail_url,
                                     callback=self.parse_detail,
                                     meta={'item': item})

    # 解析每个新闻的详情页
    def parse_detail(self, response, **kwargs):
        content = response.xpath('//*[@id="content"]/div[2]//text()').extract_first()
        item = response.meta['item']
        item['news_content'] = content

        yield item  # 将item给管道

    # 重写父类方法,最后执行(关闭浏览器对象)
    def closed(self, spider):
        self.bro.quit()

middleware.py

from scrapy import signals
from itemadapter import is_item, ItemAdapter
import time
from scrapy.http import HtmlResponse  ## 用来篡改响应数据

class NewsProDownloaderMiddleware:
    def process_request(self, request, spider):
        # request.headers['User-Agents'] = new_user_agents
        # request.meta['proxies'] = {'http': 'IP:端口'}
        return None

    def process_response(self, request, response, spider):
        # spider对象是爬虫文件对象,可以通过spider.name方法调用爬虫文件中的属性
        # 获取爬虫文件中定义的浏览器对象
        bro = spider.bro
        # 通过判断请求的url是否在spider对象的models_url列表中,来确定是否是四大板块的url
        if request.url in spider.models_url:
            bro.get(url=request.url)
            time.sleep(2)
            page_text = bro.page_source
			# 将selenium获取到的动态数据作为新的响应
            new_response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request)
            return new_response  # 将新的响应返回给爬虫文件
        else:
            return response

        # return response
    def process_exception(self, request, exception, spider):
        pass

10. 基于CrawlSpider类爬取全站数据

1. 基本使用

  • CrawlSpider类:Spider的一个子类

    • 作用:被用作于专业实现全站数据爬取。
      将一个页面下所有页码对应的数据进行爬取。
  • 基本使用

    1. 创建一个工程

    2. cd 工程

    3. 创建一个基于CrawlSpider的爬虫文件

      scrapy genspider -t crawl SpiderName www.xxx.com
      
    4. 执行工程

      scrapy crawl SpiderName
      

    注意:

    • 1.一个链接提取器对应一个规则解析器(多个链接提取器和多个规则解析器)

      链接提取器:
          作用:根据指定的规则(allow)进行指定链接的提取
      规则解析器:
          作用:将链接提取器提取到的链接进行指定规则(callback)的解析
      
    • 2.在实现深度爬取的过程中需要和scrapy.Request()结合使用

  • 面试题

    如何将一个网站中全站所有的链接都进行爬取。
    link = LinkExtractor(allow=r'')
    rules = (
    	Rule(link, callback='parse_item', follow=False),
    )
    

2. 深度爬取

  • 通用方式:CrawlSpider+Spider实现

    使用CrawlSpider提取到所有页码对应的链接,再将该页码页面中的详情页面手动发送请求,获取页面数据

    scrapy.Request(url='', callback=self.parse_detail, meta={'': ''})
    
    import scrapy
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    from wxapp_pro.items import WxappProItem
    
    
    class WxappSpider(CrawlSpider):
        name = 'wxapp'
        # allowed_domains = ['www.wxapp.com']
        start_urls = ['https://www.wxapp-union.com/portal.php?mod=list&catid=2']
    
        link = LinkExtractor(allow=r'catid=2&page=\d+')
        rules = (
            Rule(link, callback='parse_item', follow=False),
        )
    
        def parse_item(self, response):
            # print(response)
            div_list = response.xpath('//*[@id="itemContainer"]/div')
            for div in div_list:
                # 详情页url
                detail_url = div.xpath('./a/@href').extract_first()
                # 标题
                title = div.xpath('./div/h3/a/text()').extract_first()
                # print(title, detail_url)
                item = WxappProItem()
                item['title'] = title
                yield scrapy.Request(url=detail_url, callback=self.parse_detail, meta={'item': item})
                break
            # item = {}
            # return item
    
        def parse_detail(self, response):
            item = response.meta['item']
            # print(item['title'], response)
            # item = WxappProItem()
            detail = response.xpath('//*[@id="ct"]/div[1]/div/div[1]/div/div[3]/p/text()').extract_first()
            # print(item['title'], '--->', detail)
            item['detail'] = detail
            return item
    

11. settings.py文件的常用配置

这些设置可以提高scrapy的爬取效率

  • 增加并发:
    • 默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。
  • 降低日志级别:
    • 在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’
  • 禁止cookie:
    • 如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False
  • 禁止重试:
    • 对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False
  • 减少下载超时:
    • 如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s

9. 分布式爬虫

1. 概念

  • 什么是分布式爬虫

    1. 默认情况下,我们使用scrapy框架进行爬虫时使用的是单机爬虫,就是说它只能在一台电脑上运行,因为爬虫调度器当中的队列queue去重和set集合都只能在本机上创建的,其他电脑无法访问另外一台电脑上的内存和内容。
    2. 分布式爬虫实现了多台电脑使用一个共同的爬虫程序,它可以同时将爬虫任务部署到多台电脑上运行,这样可以提高爬虫速度,实现分布式爬虫。
  • 如何实现分布式

    1. 安装一个scrapy-redis的组件

    2. 原生的scarapy是不可以实现分布式爬虫,必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫。

    3. 为什么原生的scrapy不可以实现分布式?

      1. 调度器不可以被分布式机群共享
  1. 管道不可以被分布式机群共享

  2. scrapy-redis组件作用:

    可以给原生的scrapy框架提供可以被共享的管道和调度器

搭建一个分布式的机群,让其对一组资源进行分布联合爬取。
作用:提升爬取数据的效率

2. 实现流程

  1. 修改爬虫文件

    - 1.1 导包:from scrapy_redis.spiders import RedisCrawlSpider
    - 1.2 修改当前爬虫类的父类为:RedisCrawlSpider
    - 1.3 将start_url替换成redis_keys的属性,属性值为任意字符串
    	- redis_key = 'xxx':表示的是可以被共享的调度器队列的名称,最终是需要将起始的url手动放置到redis_key表示的队列中
    - 1.4 将数据解析的补充完整即可
    
  2. settings.py文件配置

    - 指定调度器
        # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
        DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
        # 使用scrapy-redis组件自己的调度器
        SCHEDULER = "scrapy_redis.scheduler.Scheduler"
        # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据
        SCHEDULER_PERSIST = True
    - 指定管道
        ITEM_PIPELINES = {
        	'scrapy_redis.pipelines.RedisPipeline': 400
        }
    	- 特点:该种管道只可以将item写入redis
    - 指定redis
        REDIS_HOST = 'redis服务的ip地址'
        REDIS_PORT = 6379
        # REDIS_ENCODING = 'utf-8'
        # REDIS_PARAMS = {'password': 12345}
    
  3. redis配置文件(redis.window.conf)

    - 解除默认绑定
    	- 56行:bind 127.0.0.1
    - 关闭保护模式
    	- 75行:protected-mode no
    
  4. 启动redis服务和客户端

  5. 执行scrapy工程(不要在配置文件中加入LOG_LEVEL

    • 程序会停留在listening位置:等待起始的url加入
  6. 向redis_key表示的队列中添加起始url

    - 需要在redis的客户端执行如下指令:(调度器队列是存在于redis中)
    	- lpush xxx http://wz.sun0769.com/political/index/politicsNewest?id=1&page=1
    

10. 增量式爬虫

增量式
- 概念:监测网站数据更新的情况,以便于爬取到最新更新出来的数据。
- 实现核心:去重
- 实战中去重的方式:记录表
    - 记录表需要记录什么?记录的一定是爬取过的相关信息。
        - 爬取过的相关信息:每一部电影详情页的url
        - 只需要使用某一组数据,该组数据如果可以作为该部电影的唯一标识即可,刚好电影详情页的url
          就可以作为电影的唯一标识。只要可以表示电影唯一标识的数据我们统称为数据指纹。
- 去重的方式对应的记录表:
    - python中的set集合(不可以)
        - set集合无法持久化存储
    - redis中的set可以的
        - 可以持久化存储

- 数据指纹一般是经过加密
    - 当前案例的数据指纹没有必要加密。
    - 什么情况数据指纹需要加密?
        - 如果数据的唯一标识标识的内容数据量比较大,可以使用hash将数据加密成32位的密文。
            - 目的是为了节省空间。

爬虫文件:

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from zlsPro.items import ZlsproItem
from redis import Redis

class ZlsSpider(CrawlSpider):
    name = 'zls'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['http://www.4567kan.com/index.php/vod/show/id/1.html']

    conn = Redis(host='123.57.56.49', port=6379, password='123')

    link = LinkExtractor(allow=r'page/\d+\.html')
    rules = (
        Rule(link, callback='parse_item', follow=False),
    )

    def parse_item(self, response):
        li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li')
        for li in li_list:
            title = li.xpath('./div/div/h4/a/text()').extract_first()
            detail_url = 'http://www.4567kan.com' + li.xpath('./div/div/h4/a/@href').extract_first()
            ret = self.conn.sadd('movies_url', detail_url)
            if ret == 1:
                item = ZlsproItem()
                item['title'] = title
                yield scrapy.Request(url=detail_url, callback=self.parse_detail, meta={'item': item})
            else:
                print('无更新!')

    def parse_detail(self, response):
        desc = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]').extract_first()
        item = response.meta['item']
        item['desc'] = desc

        yield item

管道:

from itemadapter import ItemAdapter

class ZlsproPipeline:
    count = 0
    def process_item(self, item, spider):
        self.count += 1
        print(item, self.count)
        conn = spider.conn
        conn.lpush('movieData', item)

        return item

11. m3u8文件

  • 需求:爬取91看剧视频

  • 思路:

    1. 拿到主页面的页面源代码,找到iframe

    2. 从iframe中的页面源代码中拿到m3u8文件的地址

    3. 下载第一层的m3u8文件,通过第一层m3u8文件获取到第二层的m3u8文件(视频地址存放路径)

    4. 下载视频(异步下载)

    5. 下载秘钥,进行解密(对称加密DES/AES算法)4和5可同时进行

    6. 合并所有ts文件

      import requests
      import re
      import asyncio
      import aiohttp
      import aiofiles
      from Crypto.Cipher import AES
      
      headers = {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                        'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
      }
      '''
      1. 从视频页的页面源代码中拿到m3u8文件的地址
      2. 下载第一层的m3u8文件,通过第一层m3u8文件获取到第二层的m3u8文件(视频地址存放路径)
      3. 下载视频(异步下载)
      4. 下载秘钥,进行解密(对称加密DES/AES算法)
      5. 合并所有ts文件
      '''
      # print(first_m3u8_url)  # https://vod6.wenshibaowenbei.com/20210305/FUBXE9dy/index.m3u8
      def get_first_m3u8_url(url):
          response = requests.get(url=url, headers=headers).text
          # print(response)
          obj = re.compile(r"geturl.*?'(?P<url>.*?)'.*?;", re.S)
          first_m3u8_url = obj.search(response).group('url')
          print('first_m3u8--->', first_m3u8_url)
          return first_m3u8_url  # https://vod6.wenshibaowenbei.com/20210305/FUBXE9dy/index.m3u8
      
      
      def get_second_m3u8_url(url):
          # https://vod6.wenshibaowenbei.com/20210305/FUBXE9dy/index.m3u8
          with open('m3u8文件.m3u8', 'r', encoding='utf-8') as f:
              for line in f:
                  if line.startswith('#'):
                      continue
                  line.strip()  # /20210305/FUBXE9dy/1000kb/hls/index.m3u8
          # https://vod6.wenshibaowenbei.com/20210305/FUBXE9dy/1000kb/hls/index.m3u8
          second_m3u8_url = url.split('/20210305')[0] + line.strip()
          print('second_m3u8-->', second_m3u8_url)
          return second_m3u8_url
      
      
      def down_m3u8(url):
          response = requests.get(url=url, headers=headers).text
          with open('m3u8文件.m3u8', 'w', encoding='utf-8') as f:
              f.write(response)
      
      
      # 下载视频
      async def down_ts(url, name, session, key):
          # key 和 IV必须是bytes类型,内容应该也是
          aes = AES.new(key=key.encode('utf-8'), IV=b"0000000000000000", mode=AES.MODE_CBC)
          async with session.get(url=url, headers=headers) as response:
              async with aiofiles.open(f'../flower/{name}', 'wb') as f:
                  ts = await response.content.read()  # 先对文件内容进行解密,再写入
                  await f.write(aes.decrypt(ts))
              print(name, '下载完成')
      
      
      async def aio_down(key):
          tasks = []
          async with aiohttp.ClientSession() as session:
              async with aiofiles.open('m3u8文件.m3u8', 'r', encoding='utf-8') as f:
                  async for line in f:
                      if line.startswith('#'):
                          continue
                      line = line.strip()
                      name = line.rsplit('/', 1)[1]
                      task = asyncio.create_task(down_ts(line, name, session, key))
                      tasks.append(task)
                  await asyncio.wait(tasks)
      
      
      def get_key(url):
          print('key-->', url.rsplit('/', 1)[0] + '/key.key')
          url = url.rsplit('/', 1)[0] + '/key.key'
          response = requests.get(url=url, headers=headers).text
          return response
      
      
      def main(url):
          # 拿到第一个m3u8文件的url
          first_m3u8_url = get_first_m3u8_url(url)
          # 下载第一个m3u8文件
          down_m3u8(first_m3u8_url)
          # 获取第二个m3u8的url
          second_m3u8_url = get_second_m3u8_url(first_m3u8_url)
          # 下载第二个m3u8文件
          down_m3u8(second_m3u8_url)
          # 拿到秘钥
          key = get_key(second_m3u8_url)
          print(key)
          # 从m3u8文件中提取ts的url,并下载视频
          asyncio.run(aio_down(key))
      
      
      if __name__ == '__main__':
          url = 'https://www.pianku.li/py/lNGbwMTYsVWa_1.html'
          main(url)
      
      

12. JS 加密

需求:爬取有道翻译

分析:

  • 提取出请求的url:https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule

  • 请求参数:其中有加密的密文

    import requests
    import time
    import random
    import hashlib
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
        'Referer': 'https://fanyi.youdao.com/',
        'Cookie': 'OUTFOX_SEARCH_USER_ID=883263944@10.108.160.101; JSESSIONID=aaa7wZwfCz9fd9N8w8fLx; '
                  'OUTFOX_SEARCH_USER_ID_NCOO=416212489.1839349; ___rl__test__cookies=1620376613929 '
    }
    url = 'https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
    word = input('Enter:').strip()
    lts = str(int(time.time() * 1000))
    salt = lts + str(random.randint(0, 9))
    
    # n.md5("fanyideskweb" + e + i + "Tbh5E8=q6U3EXe+&L[4c@")
    s = "fanyideskweb" + word + salt + "Tbh5E8=q6U3EXe+&L[4c@"
    md5 = hashlib.md5()
    md5.update(s.encode(encoding='utf-8'))
    sign = md5.hexdigest()
    print(lts, salt, sign)
    
    data = {
        'i': word,
        'from': 'AUTO',
        'to': 'AUTO',
        'smartresult': 'dict',
        'client': 'fanyideskweb',
        'salt': salt,  # 1 13位的时间戳加一位随机数(0~9)
        'sign': sign,  # 1
        'lts': lts,  # 1 为13位的时间戳,
        'bv': "19413bb132e864b42a71e17c0a92015a",
        'doctype': 'json',
        'version': '2.1',
        'keyfrom': 'fanyi.web',
        'action': 'FY_BY_REALTlME'
    }
    response = requests.post(url=url, data=data, headers=headers).json()
    print(response)
    

13. JS 逆向

+ js加密算法

  • 线性散列Md5&sha1算法
  • 对称加密DES/AES算法
  • 非对称加密算法RSA
  • base64伪加密
  • https证书秘钥加密

1. 微信公众平台---js算法逆向

  • Js调试工具

    • 发条js调试工具
  • PyExecJs模块

    • 实现使用Python执行js代码
    • 环境安装
      • 1.node.js开发环境
      • pip install PyExecJs
  • js算法改写

    • 打断点
    • 代码调试时,如果发现了相关变量的缺失,一般给其定义成空字典即可。如果未定义的变量为js的内置对象,则直接赋值为this
    # MD5加密--js逆向
    import execjs
    import requests
    
    # 实例化一个node对象
    node = execjs.get()
    
    # js源文件编译
    ctx = node.compile(open('./JS文件/WeChat.js', encoding='utf-8').read())
    
    # 执行js函数
    funcName = f'getPwd("abc123...")'
    pwd = ctx.eval(funcName)
    print(pwd)
    
    url = 'https://mp.weixin.qq.com/cgi-bin/bizlogin?action=startlogin'
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
        'Cookie': 'RK=XPgdtB76bF; ptcz=ff2d6e21b8f39dcae4d50ff836174abdeb1469139507cd4027e20b4bb84d6006; '
                  'ua_id=mneJY6egMsWPzwrfAAAAAPOOrEkaXvTlbsurRxUjKps=; wxuin=20457410506931; '
                  'cert=CDFjGUSvLN3KIAY9G_9I12j3JmG6_k33; '
                  'sig=h010d5b462169c71c9ae42d00c56071af84a05a0ac038f02fc628c59deacf09b0e31f87f9c55af91fae; '
                  'uuid=4b3e648e7c48b974e894788b3094d2b6 ',
    
        # 'origin': 'https://mp.weixin.qq.com',
        'referer': 'https://mp.weixin.qq.com/?token=&lang=zh_CN'
    }
    
    data = {
        'username': '246888078',
        'pwd': pwd,
        'imgcode': '',
        'f': 'json',
        'userlang': 'zh_CN',
        'redirect_url': '',
        'token': '',
        'lang': 'zh_CN',
        'ajax': '1'
    }
    response = requests.post(url=url, data=data, headers=headers).json()
    print(response)
    

2. steam平台--Js逆向

# 非对称加密算法RSA --js逆向
import execjs
import requests
import time

url = 'https://store.steampowered.com/login/getrsakey/'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
}
data = {
    'donotcache': int(time.time()*1000),
    'username': '123'
}
response = requests.post(url=url, data=data, headers=headers).json()
# print(response)
mod = response['publickey_mod']
exp = response['publickey_exp']
print(mod, exp)

node = execjs.get()
ret = node.compile(open('./JS文件/steam.js', encoding='utf-8').read())

funcName = f'getPwd("456", "{mod}", "{exp}")'
pwd = ret.eval(funcName)
print(pwd)

login_url = 'https://store.steampowered.com/login/dologin/' # 实现密码加密后,可对登录的url发送登录请求

3. 试客联盟--js逆向

import requests
import execjs
import re
url = 'http://login.shikee.com/getkey?v=787dd6afe77f51424f449987df41e136'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
}
response = requests.get(url=url, headers=headers).text
# print(response)
obj = re.compile(r'var rsa_n = "(?P<rsa_n>.*?)";', re.S)
rsa_n = obj.search(response).group('rsa_n')
print(rsa_n)

node = execjs.get()

ret = node.compile(open('./JS文件/shike.js', encoding='utf-8').read())
funcName = f'getPwd("123456", "{rsa_n}")'
pwd = ret.eval(funcName)
print(pwd)

4. 空中网--js逆向+js混淆

  • js混淆

    • 将js核心的相关代码进行变相的加密,加密后的数据就是js混淆之后的结果
  • js反混淆

    • 使用反混淆的线上工具
    • 浏览器自带的反混淆工具设置(推荐)
      • 开发者工具的设置--> Sources --> 勾选第一项Serach in anonymous and content scripts
      • 然后进行关键字的全局搜索-->找到VMxx(就是反混淆后的代码)
    import requests
    import re
    import execjs
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
        'Referer': 'https://passport.kongzhong.com/'
    }
    
    url = 'https://sso.kongzhong.com/ajaxLogin?'
    params = {
        'j': 'j',
        'jsonp': 'j',
        'service': 'https://passport.kongzhong.com/',
        '_': '1620555504932'
    }
    response = requests.get(url=url, params=params, headers=headers).text
    # print(response)
    obj = re.compile(r'"dc":"(?P<dc>.*?)".*?', re.S)
    dc = obj.search(response).group('dc')
    print(dc)
    
    node = execjs.get()
    
    ntx = node.compile(open('./JS文件/kongzhong.js', encoding='utf-8').read())
    funcName = f'getPwd("123456", "{dc}")'
    pwd = ntx.eval(funcName)
    print(pwd)
    

</rsa_n>

posted @ 2021-10-04 14:21  Z-J-H  阅读(468)  评论(0)    收藏  举报