Python爬取网站数据

Python爬取网站数据

前言

什么是爬虫?

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

爬虫合法还是违法?

  • 在法律上是不被禁止的
  • 但是也有违法风险

爬虫带来的风险可以体现在如下2方面

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

如果在使用编写爬虫的过程中避免违法

  • 经常的优化自己的程序,避免干扰被访问网站的正常运行
  • 在使用、传播爬取到的数据时,审查抓取到的内容
  • 如果发现涉及到用户隐私或者商业机密等敏感内容
  • 需要及时停止爬取并及时进行删除

爬虫在使用场景中的分类

  • 通用爬虫
    • 搜索引擎抓取系统的重要组成部分
    • 抓取的是一整张页面数据
  • 聚焦爬虫
    • 是建立在通用爬虫的基础之上
    • 抓取的是页面中的特定的局部内容
  • 增量式爬虫
    • 检测网站中数据更新的情况
    • 只会爬取网站中最新更新的数据

爬虫的矛与盾

  • 爬虫的某种爬取功能对公司有利也有弊
  • 利:可以让改公司爬取同行的营业数据等
  • 弊:响应的别的公司也可以爬取你这个公司的数据

反爬机制

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

反反爬策略

  • 爬虫程序也可以通过制定相关的策略或者技术手段,
  • 破解门户网站中具备的反爬机制,
  • 从而获取门户网站的数据

robots.txt 协议

  • 该协议也称之为君子协议
  • 里面规定了哪些数据可以被爬取
  • 哪些数据不能被爬取(尽管可以爬取,但我们还是应当遵守,避免违法)

怎么查看网站的robots.txt协议

  • 网站的域名加/robots.txt 即可访问到协议内容
  • Allow 标识的路径表示允许爬取
  • Disallow 标识的路径表示禁止爬取

1.http和https协议

http协议

  • 概念

    • 该协议是服务器和客户端进行数据交互的一种形式

    • 只有服务端和客户端遵从了这个协议,它们之间就才能进行通信

  • 常用请求头信息

    • User-Agent: 请求载体的身份标识 (浏览器信息)
    • Connection:请求完毕后,是断开连接还是保持连接
  • 常用响应头信息

    • Content-Type:服务器响应会客户端的数据类型

https协议

  • 安全的超文本传输协议
    • 就是服务端对客户端请求的数据进行加密
    • 客户端提交给服务器的数据也是加密的
  • 加密方式
    • 对称密钥加密
      • 比如,由客户端制定加密方式并将密钥发送给服务端
      • 这种方式就叫对称密钥加密
      • 缺点 是容易被第三方半路截取密钥和加密数据进行破解
    • 非对称密钥加密
      • 由服务端创建密钥对
      • 先将公钥发送给客户端
      • 客户端根据这个公钥对信息进行加密
      • 再把加密信息发送给服务端
      • 服务端使用私钥进行解密
      • 缺点
      • 第一点:公钥也可能被挟持,然后给客户端发送非服务端的公钥
      • 第二点:非对称加密的方式效率比较低,它处理起来更加复杂,
      • 通信过程中使用会有效率问题,影响通信速度
    • 证书密钥加密(https使用的加密方式)
      • 由服务端创建非对称密钥,将公钥发送给证书机构
      • 证书机构进行签名,将该证书和公钥一并发送给客户端
      • 客户端看到有证书认证才能放心使用公钥进行加密
      • 这样就可以防止公钥被篡改

补充:http和https都是无状态的

2.Requests模块

概念

  • requests模块是Python中原生的一款基于网络请求的模块

  • 功能强大,使用便捷,效率极高

  • 他的作用是 ,模拟浏览器发送请求获得数据

使用步骤

  • 安装requests模块

  • pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests

  • 指定请求的地址

  • 请求头伪装

  • 发送请求

  • 获取相应数据

  • 持久化存储

  • F12 中的response数据中可以使用 ctrl+f 查找指定数据

示例

import requests

if __name__ == '__main__':
    # 指定要爬取的地址
    url = 'https://www.kugou.com/yy/html/rank.html'
    # 设置请求头伪装(伪装成浏览器)
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                      '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
    }
    # 使用requests的get方法传入url和请求头信息,获得html或者JSON数据的response对象
    # get方法还可以有一个params位置参数,这个参数需要传入一个字典类型
    # 字典中含有请求地址的请求参数键值对
    response = requests.get(url=url, headers=headers)

    # 使用响应对象的text属性查看爬取到的数据(html或者JSON)
    # print(response.text)

    # 将爬取到的html数据存入硬盘中
    with open('C:/Users/admin/Desktop/kugou.html', 'w', encoding='UTF-8') as f:
        f.write(response.text)

3.练习 爬取网易云的音乐

步骤

  • 安装requests第三方包
  • pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests
  • 导入request模块
  • import requests
  • 进入想要爬取的数据的页面,
  • 点击F12, 点击network, 点击刷新页面
  • 在search框输入你想要的目标数据
  • 点击一条数据, 点击Preview 和 Repones查看是不是自己想要的数据
  • 确认是自己想要的数据,再点击Headers查看接口类型是不是Get
  • 如果是则将这个接口链接复制下来存入 url 变量中
  • 再将headers 的请求头信息存入字典中 User-Agent 作为键
  • 使用 requests的get方法传入url 和请求头伪装
  • 获得响应数据
  • 使用 响应数据.text 查看获取到的内容
  • 如果是json数据则将它转为字典类,一步步获取想要的数据,比如id和歌曲名
  • 如果是html数据则使用 re正则模块.findall方法 匹配出所有想要的数据存入列表的元组中
  • 再根据id和歌曲名请求歌曲的播放地址获得二进制数据
  • 然后将数据存入文件中
# 导入request模块
# 导入re正则表达式模块
# 导入os文件操作模块
import requests
import re
import os

# 要爬取数据的API(请求地址)
# 如果要爬取其他的歌单只需要修改这个请求地址就可以了
url = 'https://music.163.com/artist?id=160947'
# 浏览器请求头,用于伪装成浏览器访问api接口,对服务器发送请求,从而取得数据
# 服务器接收到请求之后,会给我们相应数据(response)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 通过requests的get方法请求数据
# 需要传入连个参数
# 第一个请求地址   (字符串类型)
# 第二个请求头伪装 (字典类型)
# 该方法会返回
response = requests.get(url=url, headers=headers)

# 获取请求的页面的源代码
html_txt = response.text

# 查找所有符合规则的数据
# re模块的findall方法
# 第一个参数正则表达式
# 正则表达式的内部的每一个整体(),当有数据符合时会将一个整体存入一个元组中,多个整体,会分为多个元素存入
# 然后将每一组元素存入列表中并返回
# 第二个参数参数要匹配的数据
# 最终返回一个列表类型,里面包含多个元组,元组里的每个元素就是一个正则表达是对应的整体()
html_data = re.findall(r'<a href="/song\?id=(\d+)">(.*?)</a>', response.text)
# 通过循环取出每首歌曲的id和歌曲名
# for循环遍历可以使用两个变量接收列表中的元组的多个元素

dir_name = 'C:/Users/admin/Desktop/music网易云'
# 判断文件夹路径是否存在
if not os.path.exists(dir_name):
    # 如果不存在则创建这个文件夹
    os.mkdir(dir_name)

for num_id, title in html_data:
    # 组装每首歌曲的下载地址
    # https://music.163.com/song/media/outer/url?id={num_id}.mp3  # 网易云的歌曲播放地址
    music_url = f'https://music.163.com/song/media/outer/url?id={num_id}.mp3'
    # content是将请求到的数据转为二进制数据
    music_content = requests.get(url=music_url, headers=headers).content
    # 循环保存列表中的所有歌曲的二进制数据到硬盘中
    # wb模式表示写入二进制数据
    # 第一个参数表示要写入数据的文件名称
    # 第二个参数表示写入的模式
    # 因为这里的写入二进制数据所以不用编码
    with open('C:/Users/admin/Desktop/music网易云/' + title +'.mp3', 'wb') as file:
        file.write(music_content)
        print(num_id, title)

4.练习 获取我喜欢的音乐的数据

import requests

# 请求url
# 从浏览器的F12查看源码获得
post_url = 'https://music.163.com/weapi/v6/playlist/detail?csrf_token=50f158475f9ca8be493ee0e3472353ac'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 请求参数
# 从f12的数据的payload中获得
data = {
    'params': 'MTTizQaQ1gmShYA10WTaGftmD8evbpbjwGqsWHHhVtVcDmPD/4+vqVoOF1Qxay94Zpy8EMJ'
              'wBD8HDExBkjkvXvtWDlo61Sy6poVqe7+5oGIrRl0KnKQOdIQWs642N+0xKmr0mpJ2SJaBhG'
              'DAX1t3PUPdqk+kHXKct6xS3f8Jd2I4cysrB+2HwB8QMEo9/8COJqMEoZcmKDeVCiL4VLQD3'
              'Pyzol1RCfe32NywLFlRsxI=',
    'encSecKey': '42b5234a0dead553942ba4edcf386800e28fa7bb9922e2e9cd181136aa950c9fbd1'
                 '8f8d520d22eacd2cc387ea4308d1e1b0209108e7233edd1fff7408283886c631ddbb'
                 '8999045c0da84058b9b6f1c2888b171a12b1805dd16c5013ae6ad2aa3d28e2c4ce14'
                 '33d44e4d9324b2a38f462e20b0a1809c0ba063831521581ac1982'
}

# 使用requests的post方法
# 传入请求地址
# 传入请求头伪装
# 传入请求数据(请求参数)
dict_data = requests.post(url=post_url, headers=headers, data=data).json()

# 通过循环抓去每首歌曲的数据
for dicts in dict_data['privileges']:
    # 获取歌曲id
    song_id = str(dicts['id'])

    # 组装请求地址
    music_url = f'https://music.163.com/song/media/outer/url?id={song_id}.mp3'
    # 使用content方法获得二进制数据
    response = requests.get(url=music_url, headers=headers).content

    # 组装文件路径
    filename = f'C:/Users/admin/Desktop/music网易云/' + song_id + '.mp3'

    # 打开文件用于存储二进制数据
    with open(filename, 'wb') as f:
        # 将二进制文件写入文件中
        f.write(response)

5.练习 网页采集器

# 导入请求模块
import requests

# 指定请求地址
url = f'https://www.baidu.com/s?'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 处理url携带的参数:封装到字典中
key_word = input("请输入您要搜索的内容")
# 指定请求路径要传入的参数
param = {
    'wd': key_word
}

# 请求地址的参数可以使用params参数传入一个字典类型的值
# 也是get方法的第二个位置参数params用于给请求地址传参的
# 对指定的url发起请求对应的url是携带参数的,并且请求过程中传递了参数
response = requests.get(url=url, params=param, headers=headers)

# 查看响应数据的文本格式
# print(response.text)

# 将响应数据转为字符串类型
html_data = response.text

# 拼接文件名称
filename = './'+key_word+'.html'
# 将html数据写入当前文件夹中的当前目录下
with open(filename, 'w', encoding='UTF-8') as f:
    f.write(html_data)

6.练习 破解百度翻译(传请求参数)

import requests
import json

# 请求url
post_url = 'https://fanyi.baidu.com/sug'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 输入请求参数
# post提交的方式获得数据需要设置请求参数
key_word = input("请输入您要翻译的英文")
params = {
    'kw': key_word
}

# 通过get方法获得相应对象
response = requests.get(url=post_url, params=params, headers=headers)

# 使用response响应对象的json()方法获得JSON数据
# 如果确认响应数据是json类型才能使用json()方法
json_data = response.json()

filename = f'./{key_word}.json'
# 选择打开一个文件
file = open(filename, 'w', encoding='UTF-8')
# 使用json模块的dump()方法将json数据存入文件中
# 参数一 要存储的数据
# 参数二 目标文件对象
# 参数三
json.dump(json_data, fp=file, ensure_ascii=False)

# 关闭文件
file.close()

7. 练习 获取豆瓣电影的排行榜数据

import requests
import json

# 请求地址
url = 'https://movie.douban.com/j/chart/top_list'

# 设置请求参数
params = {
    'type': 25,
    'interval_id': '100:90',
    'action': '',
    'start': 0,                     # 从数据库中的第几条数据开始提取
    'limit': 20                     # 一次取出的个数
}

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 获得数据的相应对象
response = requests.get(url=url, params=params, headers=headers)

# 将响应数据转为json数据

list_data = response.json()

# 将json数据写入文件中
# 设置文件名称
filename = './animation_ranking.json'
# 打开一个文件用于存储json数据
file = open(filename, 'w', encoding='UTF-8')
# 将json数据存入文件中,不把中文转ascii码
# dump()方法的作用是将数据转为符合json规范的格式存入文件中
# 第一个参数是要转为json格式的python数据
# 第二个参数的要将数据存入的文件对象
# 第三个参数是不把中文转码
json.dump(list_data, fp=file, ensure_ascii=False)

# 关闭文件对象
file.close()

8. 练习 爬取肯德基餐厅的位置信息

import requests
import json

# url路径
# 通过浏览器的F12查看Headers获得
url = 'https://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}
# 打开文件用于保存数据
file = open('./kfc_location.json', 'a', encoding='UTF-8')

list_data = list()
# 循环将数据添加到
for item in range(10):
    # 请求参数
    # 通过浏览器的F12查看Payload获得
    # 注意如果页面是Ajax请求需要点击All的倒三角选择XHR
    # 才能准确获取请求的路径和参数信息
    params = {
        'cname': '',
        'pid': '',
        'keyword': '深圳',
        'pageIndex': item + 1,
        'pageSize': 10
    }

    # get方法获得响应对象
    response = requests.get(url=url, params=params, headers=headers)
    # 把json格式转为字典类型数据
    dict_data = json.loads(response.text)
    list_data.append(dict_data)


# 将数据转为json并存入文件中
json.dump(list_data, file, ensure_ascii=False)
# 关闭文件对象
file.close()

练习 爬取网站单个视频

import requests
import re
import os
from time import sleep
from lxml import etree


class CrawlVideo:

    # 构造方法,用于初始化变量值
    def __init__(self, stat_url=None, headers=None, base_url=None, first_path=None):
        if stat_url is None:
            # 视频详情页
            self.star_url = 'https://www.acfun.cn/v/ac20783544'
        else:
            self.star_url = stat_url

        if headers is None:
            # 请求头
            self.headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
        }
        else:
            self.headers = headers

        if base_url is None:
            # 片段视频前半部分链接
            self.base_url = 'https://tx-safety-video.acfun.cn/mediacloud/acfun/acfun_video/'
        else:
            self.base_url = base_url

        if first_path is None:
            # 视频存储路径 (注意后面要加 /)
            self.first_path = 'C:/Users/admin/Desktop/'
        else:
            self.first_path = first_path

        # 视频名称
        self.video_name = ""
        # 视频id用作m3u8文件名
        self.m3u8_name = ""

    # 获得页面源码数据的方法
    def get_html_data(self, url=None, head=None):
        """
        获取页面源码的方法
        :param url: 传入一个视频详情页的url
        :param head: 传入请求头
        :return: 返回一个html页面数据
        """
        # 当请求头为空时设置默认值
        if head is None:
            head = self.headers
        if url is None:
            url = self.star_url

        # 向详情页发送请求获得响应对象
        response = requests.get(url=url, headers=head)

        # 获得详情页源码
        html_data_text = response.text

        # 获得详情页的视频id,用作视频命名
        self.m3u8_name = response.url.split('/')[-1]

        # 判断视频页面源码文件是否存在,如果存在表示已经将存储过了,不在进行二次存储
        if not os.path.exists(f'{self.first_path}{self.m3u8_name}.txt'):
            # 将上面获得的视频id作为文件名,将Html源码存入
            with open(f'{self.first_path}{self.m3u8_name}.txt', 'w', encoding='utf-8') as f:
                f.write(html_data_text)

        # 读取存好页面源码数据
        with open(f'{self.first_path}{self.m3u8_name}.txt', 'r', encoding='utf-8') as f:
            # 存入变量中
            html_data_text = f.read()
        return html_data_text

    # 获取所有分段视频链接的方法
    def get_fragment_url_list(self, data):
        """
        获得所有完整的分段视频链接url
        :param data: 传入一个视频详情页源码数据
        :return: 返回一个列表,列表中含有所有分段视频的链接
        """
        # 实力化一个etree对象,用于获取视频的名称
        tree = etree.HTML(data)

        # 获得视频的名称
        self.video_name = tree.xpath("//h1[@class='title']/span/text()")[0]

        # 输出视频名称
        print(self.video_name)

        # 使用正则获得m3u8链接所在的json字符串
        m3u8_str = re.findall(r'"ksPlayJsonHevc":"(.*?)window.videoResource', data, re.S)[0].strip()[:-1]

        # 获取各种清晰度的m3u8请求地址
        m3u8_uri_list = re.findall('"url":"(https://.*?m3u8.*?)",', m3u8_str.replace('\\', ''))

        # 查看各种清晰度的m3u8链接
        # print(m3u8_uri_list)

        # 使用列表中的第一个m3u8链接(1080p或其他),获得 m3u8文件 中的所有内容
        m3u8 = requests.get(url=m3u8_uri_list[0], headers=self.headers).text

        # print(m3u8)

        # 将m3u8文件中的所有文本数据存入文件中
        with open(f'{self.first_path}{self.m3u8_name}_list.txt', 'w', encoding='utf-8') as f:
            f.write(m3u8)

        # 将m3u8文件中的每行数据存入列表中
        with open(f'{self.first_path}{self.m3u8_name}_list.txt', 'r', encoding='utf-8') as f:
            m3u8_str_list = f.readlines()

        # 获取每段视频对应的m3u8链接
        # 循环读取每行数据,排除以# 开头的行
        # 对其他符合要求的行进行去除空格,并拼接前半部分URL(前半部分可以从F12异步加载的m3u8中分析获得)
        fgt_url_list = [self.base_url + line.strip() for line in m3u8_str_list if not line.startswith("#")]

        # 查看所有的分段的视频下载链接
        print(fgt_url_list)

        return fgt_url_list

    # 将每个片段的视频循环写入文件中的函数
    def append_fragment(self, fil, lst, v_typ=''):
        """
        循环将文件写入一个文件的方法  (另一种分段链接视频的方法)
        :param v_typ: 传入一个前半部分的补充链接
        :param fil: 传入一个视频存入位置的文件对象
        :param lst: 传入分段视频链接列表
        :return: None
        """
        # 循环对视频分段链接发送请求
        for u in lst:
            # 重新对分段链接进行修改
            u = u.replace(self.base_url, (self.base_url + v_typ))
            print(u)
            # 获得每个分段的二进制响应数据
            res = requests.get(url=u, headers=self.headers).content
            # sleep(0.5)
            # 将每个分段的响应数据最佳到文件中
            fil.write(res)

    # 保存视频的方法
    def save_video(self, fra_url_list):
        """
        将所有分段视频保存到一个文件中的方法
        :param fra_url_list: 传入一个分段视频列表
        :return: None
        """
        # 如果本地中没有这个路径,表示该视频没有下载过,让他下载到本地
        if not os.path.exists(f'{self.first_path}{self.video_name}.mp4'):
            # 打开一个文件用于存放视频,模式为追加模式
            file = open(f'{self.first_path}{self.video_name}.mp4', 'ab')

            # 调用方法将所有片段存入文件中
            self.append_fragment(file, fra_url_list)

            # 判断文件中是否有内容,如果没有则对链接进行修改,再次发送请求
            if os.path.getsize(f'{self.first_path}{self.video_name}.mp4') == 0:

                # 调用方法将所有片段存入文件中
                self.append_fragment(file, fra_url_list, v_typ='hls/')

            # 刷新硬盘,将数据存入
            file.flush()
            # 关闭文件
            file.close()

            print("下载完成")

        # 将之前下载的页面源码文件和m3u8文件删除
        if os.path.exists(f"{self.first_path}{self.m3u8_name}.txt"):
            os.remove(f"{self.first_path}{self.m3u8_name}.txt")
        if os.path.exists(f"{self.first_path}{self.m3u8_name}_list.txt"):
            os.remove(f"{self.first_path}{self.m3u8_name}_list.txt")


if __name__ == '__main__':
    # 实例化一个爬取视频的对象
    crawl = CrawlVideo()
    # 获得爬取对象中的get_html方法获得页面的源码数据
    html_data = crawl.get_html_data()
    # 获得所有分段视频的下载链接
    fragment_url_list = crawl.get_fragment_url_list(html_data)
    # 将所有的分段视频下载,并保存到一个文件中
    crawl.save_video(fragment_url_list)

9. 数据解析

数据解析分类

  • 正则进行数据解析
  • bs4模块进行数据解析
  • xpath模块进行数据解析

数据解析原理

  • 解析的局部的文本内容都会在标签中或标签或标签对应的属性中存储
  • 进行指定标签的定位
  • 标签或者标签对应的属性中存储的数据值进行提取(也称为解析)

10. RE抓取图片(正则对html进行解析) re模块

import requests
import re
import os

url = 'https://item.taobao.com/item.htm?id=20493355719&pvid=2255d6ae-9f84-4a43-a880-fc8b0a2fcc08&scm=100' \
      '7.40986.369799.0&spm=a21bo.jianhua.201876.1.5af92a89ottFE4'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 请求页面数据并将它装为文本数据
html_data = requests.get(url, headers=headers).text

# 使用正则获得页面中的所有图片地址
src_list = re.findall('src="(https://img.*?)"', html_data)

# 判断如果文件夹不存在则创建该文件夹
if not os.path.exists('./img'):
    # 创建文件夹
    os.mkdir('./img')

# 定义一个外部变量用作文件名
index = 0
for item in src_list:
    # 向图片地址发送请求并将取得的数据转为二进制数据(content)
    response = requests.get(url=item, headers=headers).content
    # 将所有图片的二进制数据存入文件中
    with open(f'./img/{index}.jpg', 'wb') as file:
        file.write(response)
        
    # 图片名称变量加一,避免重名
    index += 1

re.findall() 方法

# 传入两个参数,
# 第一个是正则表达式
# 第二个是源字符串
# 将所有 括号里的 正则匹配的符合条件的字符串存入一个列表中并返回
my_list = re.findall(r'src="(.*?)"', 'src="./img/a.png" src="./img/b.png"')
print(my_list)  # 打印输出(['./img/a.png', './img/b.png'])

re.search() 方法

# 传入两个参数,
# 第一个是正则表达式
# 第二个是源字符串
# 获得第一个匹配的字符串并返回一个迭代器对象
# 可以使用 迭代器对象.group() 获得里面的值
iter_obj = re.seatch(r'src="(.*?)"', 'src="./img/a.png" src="./img/b.png"')
print(iter_obj.group())  # 使用group()方法获得迭代器里面的字符串

re.compile() 方法

# 获得一个预加载正则对象 (用于多次使用该正则表示式)
# 传入一个参数 正则表达式
# 参数 re.S 表示让 . 能匹配到换行
first_re = re.compile(r'src="(?P<name>.*?)"', re.S)
# 使用与预加载正则对象时不用传入正则对象了,
# 直接传入源数据即可获得方法对应的返回值
my_list = first.re.findall('src="./img/a.png" src="./img/b.png"')
print(my_list)

# 使用前提 必须使用 re.finditer() 方法
# 可以给每个分组命名 (?P<分组名>正则内容)  如  (?P<name>.*?)
iters = first.re.finditer('src="./img/a.png" src="./img/b.png"')
for iter in iters:
    # 获得所有迭代器中分组名位 name 的内容
    print(iter.group("name")) 

11. bs4进行数据解析(下载小说或图片)

数据解析原理

  • 实例化BeautifulSoup对象,并将页面源码数据作为参数传入该对象中
  • 通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取

使用步骤

  • 安装bs4模块 pip install bs4

  • 安装lxml模块(数据解析器)pip install lxml

  • 导入BeautifulSoup类 from bs4 import BeautifulSoup

  • 实例化BeautifulSoup对象

    • 将本地的html文档中的数据加载到该对象中

      将文件对象和解析器lxml作为构造参数实例化对象

    • 将互联网上获取的页面源码添加到该对象中

      将爬取到的响应数据和解析器作为构造参数实例化对象

  • soup对象的属性和方法

**soup.标签名称 , 获取html页面中第一次出现的该标签的所有数据 **

from bs4 import BeautifulSoup
with open('./每日销售额柱状图.html', 'r', encoding='utf-8') as file:
    soup = BeautifulSoup(file, 'lxml')

    # soup.标签名称
    # 表示获取html页面中第一次出现的该标签的所有数据
    # 返回值是一个bs4.element.TagName 对象
    print(soup.div)

soup.find(‘标签名称’,class_=‘类选择器名称’) 获得一个节点对象

# soup对象find()方法还有第二个参数 class_
# 用于获得指定的标签的指定类名的所有标签数据
# 同理也可以用 id 参数获取指定的标签内容
# 返回值是一个bs4.element.TagName 对象
print(soup.find('div', class_='last'))

获取标签之间的文本数据

# 获取标签之间的文本数据
# soup.find('div').text  # 获取该标签下的所有文本内容,包括子标签的文本内容
# soup.find('div').string  # 获取该标签下的直系的文本内容
# soup.find('div').get_text()  # 获取该标签下的所有文本内容,包括子标签的文本内容
print(soup.div.text)

获取标签中的属性的值

# 获取标签中的属性值
# soup.标签名称['属性名称']
# 获取指定标签对应的属性的值
print(soup.div['id'])

soup对象的find_all() 方法 获取所有符合条件的标签 返回列表

# soup对象的find_all() 方法 获取所有符合条件的标签
# 返回值是一个bs4.element.ResultSet,
# 就是将所有符合条件的数据都存入列表中并返回该对象
# 可以直接将它当成列表使用
print(soup.findAll('div', class_='last'))

soup对象的select() 方法 返回列表

# soup对象的select() 方法
# 获得所有调用了指定选择器的标签的所有信息
# 将每个标签存入列表中并返回
print(soup.select('.last'))

# soup对象的select() 方法
# 该方法也可以按层级筛选需要的数据
# > 前面的表示父标签,后面的表示子标签
# 表示获取指定的类选择器下的ul下的li下的所有a标签的内容
# 并将每一个a标签存入列表中
# 空格表示跨过多个层级 '.last > ul a'
print(soup.select('.last > ul > li > a'))

12. bs4练习 下载网页中的小说

使用requests和bs4获取网页中的文本数据(下载小说)

import requests
from bs4 import BeautifulSoup
import os

# 首页地址,用于获取每一章的连接地址,用于二次爬取
url = 'https://www.shicimingju.com/book/sanguoyanyi.html'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 请求首页源代码
html_data = requests.get(url=url, headers=headers).text

# 通过BeautifulSoup类实例化soup对象
# 传入一个网页数据
# 传入一个解析器lxml  (需要安装)
soup = BeautifulSoup(html_data, 'lxml')

# 将所有调用了类选择器book-mulu 下面的所有的a标签都存入列表中
a_list = soup.select('.book-mulu a')

# 如果文件夹不存在则创建文件夹
if not os.path.exists('./三国演义'):
    os.mkdir('./三国演义')

# 通过循环获得每个a标签
for sub in a_list:
    # 通过域名拼接子路径获得每个详情页的地址
    section_url = 'https://www.shicimingju.com' + sub['href']
    # 循环请求每个详情页的数据,并转为文本类型
    section_html = requests.get(url=section_url, headers=headers).text
    # 通过BeautifulSoup类实例化soup对象
    # 传入一个网页数据
    # 传入一个解析器lxml
    section_soup = BeautifulSoup(section_html, 'lxml')
    # 使用soup对象的find方法获得一个标签节点,这个标签的id为 main_left
    # 并且获得该标签下的所有标签内的内容(文字)
    section_text = section_soup.find('div', id='main_left').get_text()  # 每一次获取的就是一个章节的所有内容
    # 通过soup对象的find方法获得调用了类card bookmark-list的第一个div 里面的 h1标签 的文字 (也就是章节名称)用作文件名
    section_name = section_soup.find('div', class_='card bookmark-list').h1.string  
    # 拼接文件路径
    file_path = f'./三国演义/{section_name}.txt'
    # 循环将每章的内容存入文件中
    with open(file_path, 'w', encoding='utf-8') as file:
        # 将每章的内容写入不同的文件中
        file.write(section_text)
    # 打印输出每一章下载完成后的结果
    print(section_name + ',下载完成')

13. xpath 数据解析

解析原理

  • 实例化一个etree对象,并将页面源码数据作为参数传入该类的构造函数中
  • 调用etree对象中的xpath方法结合表达式实现标签的定位和内容的捕获

使用步骤

  • 安装解析器 pip install lxml
  • 导入lxml模块的etree类 from lxml import etree
  • 实例化以一个etree对象etree.parse(页面数据)
  • 使用etree对象的xpath方法获得想要的数据
  • etree.xpath(xpath表达式)

xpath表达式语法

获得符合条件的节点对象

# 导入lxml模块中的etree类
from lxml import etree
# 打开文件
file = open('./每日销售额柱状图.html', 'r', encoding='UTF-8')

# 获得etree的实例对象
# 这里也可以直接传入文件路径
# 如 tree = etree.parse('./每日销售额柱状图.html')

# 如果是通过request获取的页面数据需要使用HTML()方法实例化etree对象
# tree = etree.HTML(response)  # 实例化etree对象
tree = etree.parse(file)

# /html/body/div
# 一个 / 表示一个层级
# 表示获取根节点html标签里面的body标签里的所有div标签对象
# 将每个div节点对象存入一个列表中
list_els = tree.xpath('/html/body/div')

//可以跨越多个层级

# '/html//div'
# //表示多个层级
# 表示根节点下的html的下一个层级(可以是任意一个或多个层级)里面的所有div
# 将这些div节点对象存入列表中,并返回
list_els2 = tree.xpath('/html//div')

# '//div'
# 表示从任意一个节点(整个页面的div)寻找所有的div标签
# 将这些div节点对象存入列表中,并返回
list_els3 = tree.xpath('//div')

结合层级语法和属性定位获得指定的元素节点对象

# '//div[@class="last"]'
# 注意:写选择器是根据reponse中的含有的名称写的,可能浏览器中有,响应数据中没有
# 标签名@属性名="属性值"  # 属性定位
# 表示获取指定的标签中调用了指定的类型的所有div对象,并存入列表中
list_els4 = tree.xpath('//div[@class="last"]')

标签名[ 元素位置 ] ,获得指定位置的元素对象

# '//div[@class="last"]/p[2]'
# p[想要得到的标签的位置]
# 获取页面中的所有调用了last类选择的div里面的第二个p标签
# 因为可能有多个div都调用了last类选择器,
# 所有也可获得多个p标签对象
# 最终将些p标签对象存入列表中
list_els5 = tree.xpath('//div[@class="last"]/p[2]')

/text() 获得标签对象的直系文本内容

# '//div[@class="last"]/p[2]/text()'
# /text()  # 获取的是直系的文本内容
# 表示获取前面对应的标签的内容,
# 并将每个符合xpath表达式的内容存入列表中
list_els6 = tree.xpath('//div[@class="last"]/p[2]/text()')

//text() 获得标签下的所有文本内容,只有换行也被视为一个元素

# '//div[@class="last"]//text()'
# //text()
# 获取的是标签下面的所有的文本数据
# 将每个数据作为一个元素存入列表中(换行也会当成一个元素存入)
# 表示获取前面对应的标签的内容,
# 并将每个符合xpath表达式的内容存入列表中
list_els7 = tree.xpath('//div[@class="last"]//text()')

标签名/@属性名,获取某个标签的指定属性的值

# '//div[@class="last"]/a/@href'
# 标签/@href   # 表示获取指定标签的指定属性的值
# 并将符合xpath表示的这些值存入列表中
list_els8 = tree.xpath('//div[@class="last"]/a/@class')

便捷获取页面xpath的方法

  • 点击想要爬取的页面
  • 点击想要获取xpath的标签
  • 点击检查,然后点击该标签
  • 右键点击copy,再点击子选项Copy Xpath

14. xpath 抓取页面中的图片并下载(并处理中文乱码)

from lxml import etree
import requests

url = "https://pic.netbian.com/4kyouxi/"

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 请求主页的数据
response = requests.get(url=url, headers=headers)
# 使用响应对象的encoding属性将响应数据的编码改为utf-8
# 有可能无效,发生另一种格式的乱码,可以尝试换成utf-8
# 或者单独对文字进行iso编码再解码
response.encoding = 'gbk'
# 将对象转为文本格式
html_data = response.text

# 实例化一个etree对象用于对数据进行解析
tree = etree.HTML(html_data)

# 获取所有的图片标签的src属性的值
sub_path_list = tree.xpath('//img/@src')
# 获取所有图片的名称
img_names = tree.xpath('//img/@alt')
# 循环拼接图片下载地址
for sub_path, img_name in zip(sub_path_list, img_names):
    # 域名拼接子路径为图片下载地址
    img_url = f"https://pic.netbian.com{sub_path}"
    # 向图片地址发送请求并转为二进制数据
    img_data = requests.get(url=img_url, headers=headers).content
    # 通用处理中文乱码的解决方案
    # 先将乱码编码为iso格式,再将iso格式解码为gbk格式
    # img_name.encode('ISO-8859-1').decode('gbk')

    # 拼接图片存储路径
    img_path = f"./img/{img_name}.jpg"
    with open(img_path, 'wb') as file:
        file.write(img_data)

    print(sub_path, img_name)

15. xpath解析 抓取二手房标题信息

import requests
from lxml import etree
url = 'https://bj.58.com/ershoufang/'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

html_data = requests.get(url=url, headers=headers).text

# 使用HTML()方法获得etree的实例对象
tree = etree.HTML(html_data)
# 使用xpath表达式获得每个标签内的标题内容
list_els = tree.xpath('//section[@class="list"]/div/a/div[2]/div/div/h3/text()')

# 写入模式,如果文件没有刷新,它并不会覆盖内容,只有文件重新打开再写入才会覆盖
file = open("./58title.txt", 'w', encoding='UTF-8')
for title in list_els:
    # 每个标题写入文件中
    file.write(title)

# 刷新并关闭文件
file.flush()
file.close()

16. xpath解析 抓取所有城市名称

import requests
from lxml import etree

url = 'https://www.aqistudy.cn/historydata/'
# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 请求首页源码
html_data = requests.get(url=url, headers=headers).text

# 创建etree对象
tree = etree.HTML(html_data)

# 获取所有的热门城市的名称
# hot_city_names = tree.xpath("")

# 获取所有城市的名称
# | 表示匹配两个xpath表达式的其中一个,只有有符合条件的就将它们存入列表中
all_city_names = tree.xpath("//div[@class='bottom']/ul/li/a/text() | //div[@class='bottom']/ul/div[2]/li/a/text()")
# print(hot_city_names)
print(all_city_names)

17. xpath解析 爬取多个页面的简历

难点

  • 页面地址拼接
  • 中文乱码解决
import requests
from lxml import etree
import os

# 如果文件夹不存在,则创建文件夹
if not os.path.exists('./简历'):
    os.mkdir('./简历')

# 请求链接
url = 'https://sc.chinaz.com/jianli/index%d.html'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}
# 循环爬取多个页面的简历
for page in range(1, 6):
    if not page == 1:
        # 如果是第一页用以下链接
        new_url = 'https://sc.chinaz.com/jianli/index.html'
    else:
        # 如果是其他页面就用循环的page拼接请求地址
        new_url = format(url % page)

    html_data = requests.get(url=new_url, headers=headers).text

    tree = etree.HTML(html_data)

    # 注意:类选择器的多个类名,可能和浏览器看到的并不一样,以获得的响应数据为准
    resume_details = tree.xpath('//div[@class="main_list jl_main"]/div/a/@href')

    for url in resume_details:
        response = requests.get(url=url, headers=headers)
        # 对相应数据进行编码 ,解决乱问题
        response.encoding = 'utf-8'
        html_data_detail = response.text
        # 获得etree对象
        tree = etree.HTML(html_data_detail)
        download_url = tree.xpath("//ul[@class='clearfix']/li[3]/a/@href")[0]

        # 获取简历名称
        resume_name = tree.xpath("//div[@class='ppt_tit clearfix']/h1/text()")[0]

        # 向下载地址发送请求
        resume_data = requests.get(url=download_url, headers=headers).content

        # 拼接文件路径
        resume_path = f'./简历/{resume_name}.rar'
        with open(resume_path, 'wb') as file:
            file.write(resume_data)

        print(resume_name)

18. 验证码识别检测

验证码和爬虫的关联

  • 验证码是一种反爬机制

识别验证码的操作

  • 人工肉眼识别 (不推荐)
  • 第三方自动识别 (调用Api接口传入二进制验证码图片,返回里面的验证码)

​ 工具类验证码识别工具类

import json
import time

import requests
import base64


class YdmVerify(object):
    # 从云码平台https://zhuce.jfbym.com 获取的API接口,
    # 用于解析图片中的验证码
    _custom_url = "http://api.jfbym.com/api/YmServer/customApi"
    # 登录云码平台后,从个人中心获得请求令牌,需要作为请求参数
    _token = "QNHT6Hp5FhgunwlreeFbUT2Ub6vB-o0ifPo_K92Q708"
    # 设置请求头信息
    _headers = {
        'Content-Type': 'application/json'  # 指定返回值类型为json
    }
	
    # 通用的图片验证码识别方法
    # 传入一个二进制的验证码图片数据
    # 参数verify_type 为验证码的类型,下方有对应类型的代码
    def common_verify(self, image, verify_type="60000"):
        """
        通用类型的获取图片中验证码的方法
        :param image: 验证码图片的二进制数据
        :param verify_type: 图片中含有的文字类型
        :return: 返回一个json对象,里面含有图片的验证码识别结果
        """
        # 数英汉字类型
        # 通用数英1-4位 10110
        # 通用数英5-8位 10111
        # 通用数英9~11位 10112
        # 通用数英12位及以上 10113
        # 通用数英1~6位plus 10103
        # 定制-数英5位~qcs 9001
        # 定制-纯数字4位 193
        # 中文类型
        # 通用中文字符1~2位 10114
        # 通用中文字符 3~5位 10115
        # 通用中文字符6~8位 10116
        # 通用中文字符9位及以上 10117
        # 定制-XX西游苦行中文字符 10107
        # 计算类型
        # 通用数字计算题 50100
        # 通用中文计算题 50101
        # 定制-计算题 cni 452
        
        # API接口请求的参数
        payload = {
            "image": base64.b64encode(image).decode(),  # 对二进制数据进行base64编码,再将其从字节流解码为字符串形式。
            "token": self._token,  # 用户令牌
            "type": verify_type,  # 验证码类型
            "developer_tag": "10e12fba758f82ce7de2b5710e1d852b"  # 开发者密钥,每调用一次接口该账户都可获得积分
        }
        print(payload)
        # 向API接口发送请求获得响应对象
        resp = requests.post(self._custom_url, headers=self._headers, data=json.dumps(payload))
        print(resp.text)
        # 将响应对象转为python对象,并获取里面的key的里面data键对应的值
        return resp.json()['data']['data']

主函数类中对API进行测试

# 导入验证码识别类
from utils.verify_code_util import YdmVerify

# 实例化验证码识别对象
code_verify = YdmVerify()

# 打开一张图片验证码
# 模式为rb 读取二进制数据
with open('D:/Downloads/2.png', 'rb') as file:
    # 读取文件中是所有内容 (二进制的)
    img_byte = file.read()
	
    # 调用验证码识别对象里面的通用验证码识别方法
    # 传入一个二进制格式的图片数据
    # 指定验证码的类型为 10110 (4字符数英)对应类型可在官网查看
    # 识别出来的验证码
    code = code_verify.common_verify(img_byte, verify_type='10110')
	
    print(code)

19. 练习 识别登录页面中的验证码

import requests
from lxml import etree
from utils.verify_code_util import YdmVerify
import json

# 请求url
url = 'https://so.gushiwen.cn/user/login.aspx?from=http://so.gushiwen.cn/user/collect.aspx'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 请求登录页面的数据返回响应对象并转为html文本
html_data = requests.get(url=url, headers=headers).text

# 实例化一个etree对象,用于解析页面中的数据
tree = etree.HTML(html_data)

# 使用xpath表达式获取验证码的子url
sub_url = tree.xpath('//div[@class="mainreg2"]/div[@class="mainreg2"][3]/img/@src')[0]

# 拼接验证码的完整路径
code_url = 'https://so.gushiwen.cn' + sub_url

# 请求验证码的url并使用content转为二进制数据
binary_code = requests.get(url=code_url, headers=headers).content

# 实例化一个验证码识别对象
verify_code = YdmVerify()

# 通过验证码识别对象的通用图片验证码识别方法获得验证码
code = verify_code.common_verify(binary_code, verify_type='10110')

print(code)

20. 练习 模拟登录获得个人信息(session)

需求

  • 爬取基于某些用户的用户信息
  • 点击登录按钮之后会发起一个post请求
  • post请求中会携带登录之前录取的相关的登录信息 (验证码等)
  • 验证码,每次请求都会变化
  • 需要保持登录的状态(使用session对象对URL进行请求)
import requests
from lxml import etree
from utils.verify_code_util import YdmVerify
import json

# 请求url
url = 'https://so.gushiwen.cn/user/login.aspx?from=http://so.gushiwen.cn/user/collect.aspx'

# 设置请求头伪装(伪装成浏览器)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/'
                  '537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# 请求登录页面的数据返回响应对象并转为html文本
html_data = requests.get(url=url, headers=headers).text

# 实例化一个etree对象,用于解析页面中的数据
tree = etree.HTML(html_data)

# 使用xpath表达式获取验证码的子url
sub_url = tree.xpath('//div[@class="mainreg2"]/div[@class="mainreg2"][3]/img/@src')[0]

# 拼接验证码的完整路径
code_url = 'https://so.gushiwen.cn' + sub_url

# 使用requests模块中的Session()实例化一个会话对象,用于对url发起请求并携带cookie
# 使用 session对象 和直接使用 requests模块 的区别在于
# session请求到的数据会写带服务器生成的cookie值,用于记录客户端的状态
# 也就是 未登录状态 和 登录状态 等
session = requests.Session()

# 请求验证码的url并使用content转为二进制数据
binary_code = session.get(url=code_url, headers=headers).content

# 实例化一个验证码识别对象
verify_code = YdmVerify()

# 通过验证码识别对象的通用图片验证码识别方法获得验证码
code = verify_code.common_verify(binary_code, verify_type='10110')

print(code)
login_url = 'https://so.gushiwen.cn/user/login.aspx?from=http%3a%2f%2fso.gushiwen.cn%2fuser%2fcollect.aspx'
payload = {
    "__VIEWSTATE": "qshjVID8tL7CJP/pt4bhvUoXMxsa7zf+d42/rR/WFWxKwgqCfT4+g/qaUT9wI2/cK+Xkr9eET0UIZF9NnUnYrKf8bBuEeRS/om"
                   "ZejSKU6soyd1ghfCrUZmjyoyP0A8qvIptwGQqKD/meC3e8ucM1Sn5saig=",
    "__VIEWSTATEGENERATOR": "C93BE1AE",
    "from": "http://so.gushiwen.cn/user/collect.aspx",
    "email": "358449765@qq.com",
    "pwd": "0000000",
    "code": code,
    "denglu": "登录"
}

# session对象中会一直记录着cookie的值(页面的状态)
response = session.post(url=login_url, headers=headers, data=payload)
# 使用response对象的status_code属性 获得响应状态码
# 响应状态码为200 表示登录成功
print(response.status_code)
html_index = response.text
# 将页面写入文件中查看
# with open('./index.html', 'w', encoding='utf-8') as file:
#     file.write(html_index)

# 登录成功后就可以继续使用session对象爬取用户的个人数据
# 个人中心链接(每个用户的都一样)
url = 'https://so.gushiwen.cn/user/collect.aspx'
response_profile = session.get(url=url, headers=headers)
# 继续处理响应对象获得想要的数据

21. IP代理

概念

  • 代理就是代理服务器
  • 总的来说就是将我们的访问请求发送给代理服务器
  • 由代理服务器发送给web服务端
  • 代理服务器拿到数据再返回给我们
  • 代理的作用
    • 突破自身IP访问的限制
    • 隐藏自身真实的IP避免被封禁
  • 代理相关网站:快代理 https://www.kuaidaili.com/free/fps/

IP代理在python中的应用

  • 在python的爬虫程序运行过程中,访问网站的次数过多,IP可能会被封禁
  • 这也是一种反爬机制

代理ip的类型

  • http只能应用到http协议对应的URL中
  • https只能应用到https协议对应的URL中

代理IP的匿名度

  • 透明: 目标服务器知道该次请求使用了代理,并且知道你的真实ip
  • 匿名 :目标服务器知道该次请求使用了代理,但不知道你的真实ip
  • 高匿:目标服务器不知道该次请求使用了代理,也不知道你的真实ip

示例

import requests
url = 'https://www.baidu.com/s?wd=ip'

# 定义请求头
headers = {
    # 不能换行否则会报错
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}

# 请求url连接的时候传入proxies参数 这个参数的值是一个字典类型
# 键是http或https  值是代理Ip加一个端口号 103.82.157.91:8080
html_data = requests.get(url=url, headers=headers, proxies={
    "https": "103.82.157.91:8080"  # 代理服务器的ip和端口
}).text

with open('./ip.html', 'w', encoding='utf-8') as file:
    file.write(html_data)

22. 异步(并发非阻塞执行)爬虫(线程池、进程池)

异步爬虫发方式

  • 多线程、多进程 ( 不建议)
    • 好处:可以为相关阻塞操作开启多个线程或进程,进行异步执行
    • 弊端:无法无限制的开启多线程或多进程
  • 线程池、进程池 (适当的使用)
    • 好处:可以降低系统对进程或线程创建和销毁的一个频率,从而降低系统的开销
    • 弊端:池中线程或进程的数量有上限

线程池的使用

原则:线程池处理的是阻塞且耗时的操作(如下载多个文件)

import time
# 导入线程池类
from multiprocessing.dummy import Pool

# 记录开始时间戳 (自1970年1月1日以来的秒数)
start_time = time.time()


# 定义一个函数用于对列表中的元素进行操作
def get_ele(ele):
    print(f"正在请求{ele}的数据")
    time.sleep(2)  # 睡眠两妙
    print(f"{ele}请求成功")


# 所有需要请求数据的url
url_list = [
    'http://dfsfewg1.com',
    'http://dfsfewg2.com',
    'http://dfsfewg3.com',
    'http://dfsfewg4.com',
    'http://dfsfewg5.com'
]

# 实例化一个线程池
# 传入一个数字参数
# 表示线程池里面线程的个数 (不要设置太多也不要设置太少)
# 如果只传入一个数字1 ,那么和同步(按顺序阻塞)执行没区别
pool = Pool(6)
# 将列表中每个元素作为get_ele的参数,
# 再把这个方法传递给线程池里的空闲线程,
# 让他它们异步执行get_ele方法
# 线程池对象的map方法
# 参数一 传入线程要执行的函数
# 参数二 传入一个列表,列表中的元素类型要和传入的函数的参数类型一致
pool.map(get_ele, url_list)

# 传入lambda表示作为参数示例
# list_url = pool.map(lambda x: x.replace('http', 'https'), url_list)

# 执行完毕后关闭线程池
pool.close()
# 等待线程池中的所有任务执行完毕后,再执行主线程
# 注意:使用pool.join() 之前必须使用pool.close()或pool.shutdown()
# 否则可能会导致程序一直阻塞无法继续执行
pool.join()

# 获得结束时间戳
end_time = time.time()

# 查看程序运行耗费的时间
print(f"列表中的所有url发送请求一共用了{end_time-start_time}秒")

23. asyncio 模块的使用

名词和方法简介

  • async: 声明一个异步函数,该函数会返回一个协程对象

  • coroutine: 协程对象,使用async关键字来声明一个方法,这个方法在调用时不会立即执行,而是返回一个协程对象

  • task任务 它是协程对象的进一步封装,包含了任务的状态

  • 创建task任务对象的方式

    • 方式一: loop循环事件对象的create_task(协程对象) 方法将协程对象封装为任务对象
    • 方式二:asyncio模块的create_task(协程对象) 方法将协程对象封装为任务对象实际上和loop.create_task()方法没有本质区别3.7及以上的版本使用
    • 方式三:asyncio模块的ensure_future(协程对象)方法将协程对象封装为任务对象实际上和loop.create_task()方法没有本质区别 旧版本写法,python3.7以上不建议使用
  • 使用asyncio模块的get_event_loop()方法获得事件循环对象: ,相当于一个无限循环,可以把函数注册到这个事件循环中,当满足某些条件时函数会被循环执行

  • await关键字 用来挂起阻塞方法的执行

async 关键字的基本使用

import asyncio

# 使用async关键字修饰一个函数
# 当调用该函数时不会直接返回里面的计算结果
# 而是返回一个协程对象 <class 'coroutine'> 类型
async def request(url):
    print(f"请求{url}数据中。。。")
    print("请求成功")
    return url


# 调用调用async修饰的函数获得协程对象
cor_obj = request('www.baidu.com')

# 使用asyncio模块的get_event_loop方法
# 获得一个事件循环对象
loop = asyncio.get_event_loop()

# 将协程cor_obj对象注册到事件循环对象loop中
# 调用事件循环对象的 run_until_complete() 方法
# 传入一个协程对象,表示将这个协程对象注册到loop中
# 在循环事件对象中会执行协程对象里面的逻辑
# 该方法的返回值是一个协程对象 coroutine 类型
loop.run_until_complete(cor_obj)

print(type(cor_obj))

task 任务对象的使用(协程对象的二次封装)

import asyncio


# 使用async关键字修饰一个函数
# 当调用该函数时不会直接返回里面的计算结果
# 而是返回一个协程对象 <class 'coroutine'> 类型
async def request(url):
    print(f"请求{url}数据中。。。")
    print("请求成功")
    return url


# 调用调用async修饰的函数获得协程对象
cor_obj = request('www.baidu.com')

# 使用asyncio模块的get_event_loop方法
# 获得一个事件循环对象
loop = asyncio.get_event_loop()

# 基于loop创建一个任务对象task
# 使用事件循环对象loop的create_task()方法
# 传入一个协程对象,返回一个任务对象task, 类型是 <class '_asyncio.Task'>
# task 是协程对象的二次封装
task = loop.create_task(cor_obj)
# 查看任务状态为 Task pending
print(task)
print(type(task))

# 调用loop对象的run_until_complete()方法
# 传入一个task任务对象
# 执行功能函数里面的逻辑
loop.run_until_complete(task)

# 当循环对象执行完毕后
# 查看任务状态为 Task finished
print(task)
# 可以通过task任务对象的result方法获取原始函数的返回值
print(task.result())
# print(type(cor_obj))

future 的使用 (协程对象的二次封装)两种方法无区别

import asyncio


# 使用async关键字修饰一个函数
# 当调用该函数时不会直接返回里面的计算结果
# 而是返回一个协程对象 <class 'coroutine'> 类型
async def request(url):
    print(f"请求{url}数据中。。。")
    print("请求成功")
    return url


# 调用调用async修饰的函数获得协程对象
cor_obj = request('www.baidu.com')

# 基于asyncio创建一个任务对象task
# 使用asyncio模块的ensure_future() 方法对协程对象进行二次封装
# 获得一个任务对象
task = asyncio.ensure_future(cor_obj)

# 使用asyncio模块的get_event_loop方法
# 获得一个事件循环对象
loop = asyncio.get_event_loop()

# 任务执行之前
# 查看任务状态为 Task pending
print(task)

# 调用loop对象的run_until_complete()方法
# 传入一个task任务对象
# 执行功能函数里面的逻辑
loop.run_until_complete(task)

# 当循环对象执行完毕后
# 查看任务状态为 Task finished
print(task)
# 可以通过task任务对象的result方法获取原始函数的返回值
print(task.result())
# print(type(cor_obj))

任务对象绑定回调 (任务对象的result方法可以获得原函数的返回值)

import asyncio


# 使用async关键字修饰一个函数
# 当调用该函数时不会直接返回里面的计算结果
# 而是返回一个协程对象 <class 'coroutine'> 类型
async def request(url):
    print(f"请求{url}数据中。。。")
    print("请求成功")
    return url


# 调用async修饰的函数获得协程对象
cor_obj = request('www.baidu.com')


# 在任务对象被创建之前,
# 创建一个回调函数用于绑定到任务对象中
# 传入一个参数
# 这个参数默认就是绑定该回调函数的 任务对象task
def callback_fun(t):
    # 打印输出task任务执行完成后的里面的原函数的返回值
    print(t.result())


# 使用get_event_loop方法获得一个事件循环对象
loop = asyncio.get_event_loop()
# 传入一个协程对象,获得一个任务对象
task = asyncio.ensure_future(cor_obj)
# 将回调函数绑定到任务对象中
# task任务对象的add_done_callback()方法 绑定回调函数
# 当task被run执行后,
# 会将自身作为参数传给回调函数callback_fun
task.add_done_callback(callback_fun)
# 将任务对象添加到事件循环对象中
# 让其执行具体的逻辑
loop.run_until_complete(task)

await关键字 和 asyncio.wait() 方法的使用

import asyncio
import time


# 声明一个异步函数
# 用于获得协程对象
async def request(url):
    print(f"向{url}送请求中。。。")
    # 在异步协程中如果出现了同步模块相关的代码,那么就无法实现异步
    # request.get() 方法也是同步模块代码
    # time.sleep(2)  # 同步模块代码

    # 当在asyncio中出现阻塞操作时必须使用await关键字进行手动挂起
    await asyncio.sleep(2)  #
    print("请求成功")

# 开始时间戳
start = time.time()

url_list = [
    'www.dfefs.com',
    'www.dfedsfsfs.com',
    'www.dfeeefffs.com'
]

# 创建一个任务列表,用于存放多个任务对象
tasks = list()
for url in url_list:
    # 调用async声明的函数获得协程对象
    cor_obj = request(url)
    # 通过asyncio模块的ensure_future()方法对协程对象进行二次封装
    # 获得一个任务对象
    task = asyncio.ensure_future(cor_obj)
    # 将多个任务对象添加到任务列表中
    tasks.append(task)

# 获得一个循环事件对象
loop = asyncio.get_event_loop()
# 使用asyncio模块的wait()方法传入一个任务列表
# 再把wait方法的返回值传入loop对象的run_util_complete方法中
loop.run_until_complete(asyncio.wait(tasks))

# 打印程序运行时间
print(time.time()-start)

24. aiohttp模块的使用

使用步骤

  • 安装aiohttp模块 pip install aiohttp

示例代码

import aiohttp

25. 单线程加异步协程爬取数据(3.7以上版本)

示例

import asyncio
import aiohttp
import time
import random


# 定义请求头
headers = {
    # 不能换行否则会报错
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}

# 定义要抓取的URL列表
urls = [
    'http://example.com',
    'http://example.org',
    'http://example.net'
]


# 定义一个异步抓取函数
async def get_page(url):
    # 使用async 声明一个异步请求对象
    # async with 固定语法 异步代码 with 前必须加 async 修饰
    # 使用aiohttp模块中的 ClientSession()方法
    # 获得一个session会话对象,用于对url进行请求的发送
    async with aiohttp.ClientSession() as session:
        # 使用async 声明一个异步响应对象
        # async with 固定语法 异步代码 with 前必须加 async 修饰
        # 使用session对象的get方法对url发送请求
        # 可传入的参数 url 要请求的地址
        # headers 请求头伪装
        # proxy 代理IP (这个代理需要是一个地址加端口号)如 :http://192.168.12.16:6052
        async with session.get(url, headers=headers) as response:
            # 注意:使用响应对象时必须在它前面添加一个 await 进行修饰,让其手动挂起
            # response对象的text()方法 将响应数据转为文本
            # response对象的json()方法 将响应数据转为json对象
            # response对象的read()方法 将响应数据转为二进制数据
            html_data = await response.text()

    return html_data


# 定义一个回调函数
async def callback_fun(task):
    # 等待task任务对象返回结果
    result = await task
    html_data = result  # 将获得的结果赋值给新的变量
    num = random.randint(1, 20)
    # 将获得的结果存入列表中
    with open(f'./{num}.html', 'w', encoding='utf-8') as file:
        file.write(html_data)


# 定义一个异步主函数
async def main():
    # 定义一个任务列表
    tasks = list()
    for url in urls:
        cor_obj = get_page(url)
        # 对协程对象进行二次封装获得task任务对象
        task = asyncio.create_task(cor_obj)
        # 给任务对象传入一个回调函数
        # 当任务对象执行完毕后,对其返回的结果进行处理
        # task.add_done_callback(callback_fun)
        callback_task = asyncio.ensure_future(callback_fun(task))

        # 将多个任务对象存入任务列表中
        tasks.append(callback_task)

    # 等待所有任务完成
    await asyncio.gather(*tasks)

# 运行整个异步程序
if __name__ == '__main__':
    # 创建开始时间戳
    start = time.time()
    # 使用asyncio模块的run方法运行整个异步主函数
    asyncio.run(main())
    # 获得结束时间戳
    end = time.time()
    print("程序运行总耗时(秒)", end-start)

26. selenium模块的使用(获取动态加载的数据)

概念

  • 为什么要使用selenium
    • 可以便捷的获取网站中动态加载的数据
    • 可以便捷的实现模拟登录
  • 什么是selenium模块?
    • 基于浏览器自动化的模块,让代码自动操作浏览器
  • selenium使用步骤
    • 安装selenium pip install selenium
    • 下载浏览器的驱动程序
    • 下载地址1 http://npm.taobao.org/mirrors/chromedriver/
    • 下载地址2新版 https://googlechromelabs.github.io/chrome-for-testing/#stable
    • 驱动程序和浏览器版本映射要一致
    • 浏览器驱动映射表 https://blog.csdn.net/huilan_same/article/details/51896672

示例

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

# 创建一个Service实例对象
# 指定 ChromeDriver 路径
service = Service('./浏览器驱动/chromedriver.exe')
# 实例化一个浏览器对象 (传入浏览器的驱动程序路径)
# 使用webdriver模块中的Chrome方法(谷歌浏览器)
# service参数 传入该浏览器对应的驱动程序的路径
# 获得一个浏览器对象(会自动打开浏览器)
bro = webdriver.Chrome(service=service)

# 使用浏览器对象的get方法, 传入一个url
# 自身使用浏览器对象地址发送请求
bro.get('https://www.gushiwen.cn/')

# 使用浏览器对象的 page_source属性获得页面文本的数据
# 页面中含有所有的,页面中显示的数据,包括Ajax请求的数据
html_data = bro.page_source

print(html_data)
print(type(html_data))

# 关闭浏览器
bro.quit()

27. 使用selenium自动对页面进行操作

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
import time

# 创建一个Service实例对象
# 指定 ChromeDriver 路径
service = Service('./浏览器驱动/chromedriver.exe')
# 实例化一个浏览器对象 (传入浏览器的驱动程序路径)
# 使用webdriver模块中的Chrome方法(谷歌浏览器)
# service参数 传入该浏览器对应的驱动程序的路径
# 获得一个浏览器对象(会自动打开浏览器)
bro = webdriver.Chrome(service=service)

# 使用浏览器对象的get方法, 传入一个url
# 自身使用浏览器对象地址发送请求
bro.get('https://p4psearch.1688.com/')

# 标签定位
# 首先需要导入 By 类
search_input = bro.find_element(By.ID, value='alisearch-keywords')
# 标签交互,在输入框中输入要搜索的词
search_input.send_keys('iphone')

# 定位到搜索按钮标签
# 传入连个参数
# 第一个设置选择器的类型
# 第二个设置对应的名称
btn = bro.find_element(By.ID, value='alisearch-submit')
# 点击搜索按钮
btn.click()

# 执行一组js程序
# 传入一个js代码, 浏览器会自动执行
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')

bro.get('https://www.baidu.com')

# 回退到前一个页面
bro.back()
time.sleep(2)
# 前进一个页面
bro.forward()

# 关闭浏览器
bro.quit()

28. selenium的动作链对象和操作iframe标签中的内容(一系列操作)

from selenium.webdriver import Chrome
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from  time import sleep

service = Service('./浏览器驱动/chromedriver.exe')
bro = Chrome(service=service)

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

# 如果定位的标签在iframe标签中
# 需要先定位到iframe的位置
bro.switch_to.frame('iframeResult')

# 定位标签位置
# 根据ID选择器,获得标签对象
div = bro.find_element(By.ID, 'draggable')

# 实例化一个动作链对象
# 导入selenium中的 ActionChains 方法
action = ActionChains(bro)

# 动作链对象的click_and_hold()方法
# 传入一个定位好的标签对象
# 表示点击长按指定标签
action.click_and_hold(div)

# 执行5次偏移操作
for i in range(5):
    # 对标签进行像素偏移
    # perform() 方法表示立即执行
    action.move_by_offset(17, 0).perform()
    sleep(0.3)

# 释放动作链对象
action.release()

print(div)
# 关闭浏览器
bro.quit()

29. selenium自动登录练习

from selenium.webdriver import Chrome
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
import time

service = Service('./浏览器驱动/chromedriver.exe')
bro = Chrome(service=service)

# 访问qq空间页面
bro.get('https://qzone.qq.com/')

# 使用浏览器对象的 switch_to.frame()方法
# 传入iframe标签的id 选择操作里面的元素
bro.switch_to.frame('login_frame')

# 获得密码登录的 a标签对象
login_a = bro.find_element(By.ID, 'switcher_plogin')

# 点击这个标签
login_a.click()

# 定位到用户名输入框
username_input = bro.find_element(By.ID, 'u')

# 定位到密码输入框
pwd_input = bro.find_element(By.ID, 'p')

# 定位到登录按钮
btn_login = bro.find_element(By.ID, 'login_button')

# 使用send_key方法输入QQ账号
username_input.send_keys('358449765')

# 使用send_key方法输入QQ密码
pwd_input.send_keys('123454125461')

# 点击登录按钮
btn_login.click()
time.sleep(3)
btn_login.click()

time.sleep(30)

# 退出浏览器
bro.quit()

30. 实现无头浏览器和规避被检测

示例

from selenium.webdriver import Chrome  # 谷歌浏览器类
from selenium.webdriver.chrome.service import Service  # 驱动程序配置
from selenium.webdriver.chrome.options import Options  # 无头浏览器类

# 无头浏览器配置
# 不打开浏览器界面的情况下进行请求发送的配置
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
# 避免被检测到使用selenium爬取数据的类
chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])

# 配置浏览驱动路径
service = Service('./浏览器驱动/chromedriver.exe')

# 给谷歌浏览器传入 配置驱动路径对象 和 无头浏览器配置对象
# 获得一个浏览器对象
bro = Chrome(service=service, options=chrome_options)

# 访问url
bro.get('https://www.baidu.com')

# 使用浏览器对象的 page_source属性
# 获得请求的数据并打印
print(bro.page_source)

bro.quit()

31. selenium自动登录并自动点击验证码

from utils.verify_code_util import YdmVerify
from utils.chaojiying import cjy
from selenium.webdriver import Chrome
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver import ActionChains
from PIL import Image
from time import sleep
import os

if not os.path.exists('./验证码页面截图'):
    os.mkdir('./验证码页面截图')

service = Service('./浏览器驱动/chromedriver.exe')

# 创建了一个 Chrome 浏览器选项对象
chrome_options = Options()
# 设置浏览器为无头模式,即在后台运行浏览器,不会打开可见的窗口
chrome_options.add_argument('--headless')
# 禁用 GPU 加速,通常在无头模式下需要禁用 GPU 加速
chrome_options.add_argument('--disable-gpu')
# 添加实验性选项,禁用自动化扩展
# 避免被检测到使用selenium爬取数据的类
chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])

# 实例化一个浏览器对象
bro = Chrome(service=service)

# 使用浏览器对象请求url
bro.get('https://qzone.qq.com/')

# 进入 iframe 标签内部
bro.switch_to.frame('login_frame')

# 定位到密码登录a标签并点击它
pwd_login_a = bro.find_element(By.ID, 'switcher_plogin')
pwd_login_a.click()

# 获得账户输入框对象并输入账号
user_input = bro.find_element(By.ID, 'u')
user_input.send_keys('358449765')

# 获得密码输入框对象并输入密码
pwd_input = bro.find_element(By.ID, 'p')
pwd_input.send_keys('0000000')

# 等待一秒
sleep(1)

# 获得登录按钮对象并点击
login_btn = bro.find_element(By.ID, 'login_button')
login_btn.click()
# 登录后等待五秒
sleep(5)

# 定位到验证码所在的 iframe
bro.switch_to.frame('tcaptcha_iframe_dy')

# 获得验证码图片所在的div
code_img_div = bro.find_element(By.ID, 'slideBg')

# 元素点击被拦截的异常。可能是由于验证码图片所在的元素被其他元素遮挡或导致无法直接点击
# 使用 JavaScript 执行点击操作,而不是直接在元素上调用 click() 方法。这种方法通常可以绕过元素被遮挡的问题。
# bro.execute_script("arguments[0].click();", code_img_div)

# 使用该元素对象的 screenshot() 方法截取元素的截图
# 传入一个参数为图片的存入路径
code_img_div.screenshot('./验证码页面截图/code.png')


with open('./验证码页面截图/code.png', 'rb') as f:
    byt_code_img = f.read()

"""
# 使用浏览器对象的 save_screenshot() 方法截取当前页面
# 传入一个参数为图片的存入路径
bro.save_screenshot('./验证码页面截图/verify.png')

# 获得该div对象所在的左上角的坐标 x,y
# location 属性的是一个字典对象
# {x: ??, y: ??}
location = code_img_div.location


# 获得该div的长和宽
# size 属性也是一个字典对象
# {width: ??, height: ??}
size = code_img_div.size

# 将验证码所在标签的左上角坐标(x y)和左下角坐标(x+with  y+high)存入元组中
rangel = (
    int(location['x']),
    int(location['y']),
    int(location['x'] + size['width']),
    int(location['y'] + size['height'])
)

# 安装 pip install pillow 模块
# 并导入 from PIL import Image
# 使用Image类实例化一个图片处理对象

# 传入一个图片的路径
# 获得一个图片对象
img = Image.open('./验证码页面截图/verify.png')
# 使用图片对象的 crop 方法对图片进行裁剪
# 传入一个元组作为参数
# 元组中需要包含要截取的区域的左上角的x,y坐标点 和 右下角的x, y坐标点
# 获得一个截取后的验证码图片对象
crop_img = img.crop(rangel)

crop_img.save('./验证码页面截图/code.png')

# 将这个验证码图片对象转为二进制数据
byt_img = crop_img.tobytes()
"""

# 获得验证码描述信息所在的元素标签对象
code_text_span = bro.find_element(By.XPATH, '//*[@id="instructionText"]')
# 获得标签的属性值,也就验证码描述信息
code_text = code_text_span.text
print("验证码描述和二进制数据", code_text)

# # 实例化一个验证码识别对象
# ydm = YdmVerify()
#
# # 调用点击类型的验证码的识别方法
# # 传入图片的二进制数据,和描述信息,寻找验证码的类型为30009 (通用点选类型)
# result = ydm.click_verify(image=byt_code_img, extra=code_text, verify_type=30009)

result = cjy.PostPic(byt_code_img, codetype=9004)['pic_str']
all_list = []
# 判断是有一个坐标还是有多个坐标
# 然后将所有的xy坐标存入列表中,再将列表存入大列表中
if '|' in result:
    list_coordinates = result.split('|')
    for coordinates in list_coordinates:
        x = coordinates.split(',')[0]
        y = coordinates.split(',')[1]
        xy_list = [x, y]
        all_list.append(xy_list)
else:
    x = result.split(',')[0]
    y = result.split(',')[1]
    xy_list = [x, y]
    all_list.append(xy_list)

for xy_list in all_list:
    x = xy_list[0]
    y = xy_list[1]
    # 使用动作链对象循环点击多个坐标点
    # move_to_element_with_offset (选择在指定的标签作为坐标参照)
    # .click().perform() 点击立即执行
    ActionChains(bro).move_to_element_with_offset(code_img_div, x, y).click().perform()


# 获得验证码的确认标签,并点击
verify_submit = bro.find_element(By.CLASS_NAME, 'verify-btn-text')
verify_submit.click()

sleep(200)

# 退出浏览器
bro.quit()

32. scrapy框架

什么是框架?

  • 一个集成了很多功能并且具有很强通用性的一个项目模板
  • 主要学习框架封装的各种功能的详细用法
  • scrapy爬虫框架泛用性强,受到广泛使用

scrapy 框架的主要功能

  • 高性能的持久化存储
  • 异步的数据下载
  • 高性能的数据解析
  • 分布式抓取数据

scrapy 框架的安装

  • Linux :直接 pip install scrapy
  • windows:
    • pip install wheel
    • 下载twisted, 下载地址: http://www.lfd.uci.edu/~gohlke/pythonlibs/#teisted
    • 进入到安装包所在的目录,文件名要对应,安装teisted: pip install aggdraw-1.3.14-cp310-cp310-win_amd64.whl cp310表示python版本
    • pip install pywin32 pywin32提供了对Windows系统功能的访问
    • pip install scrapy

32.1 scrapy的五大核心组件

  • 引擎(Scrapy)
    • 用来处理整个系统的数据流处理,触发事务(根据接收到的不同数据类型调用不同的方法)
  • 调度器(Scheduler)
    • 调度器接收来自Spider的请求,并将它们加入到队列中,
    • 这个过程涉及到对请求的去重和优先级排序
    • 根据一定的策略(如优先级)从队列中取出请求发送给Downloader
    • 调度器还负责过滤掉重复的请求,确保同一个资源不会被多次下载
    • 这通常通过一个去重过滤器(DupeFilter)实现
  • 下载器(Downloader)
    • Downloader负责下载Scrapy Engine发送的所有请求, 并将网页内容返回给引擎, 由引擎再传递给Spider
    • Downloader 中间件 可以在请求发送到Downloader之前或从Downloader返回之后执行自定义的功能,例如设置代理、用户代理(User-Agent)等
  • 爬虫(spiders)
    • Spiders是用户编写用来从特定网站(或一组网站)提取数据的类
    • 它们接收来自Downloader的响应并解析内容,提取数据(抽取项),寻找新的URL来爬取
    • 把数据提交给Item Pipeline处理
    • 而新的URL请求将被提交给Engine,由Scheduler进一步处理
  • 管道(Item Pipeline)
    • Item Pipeline负责处理由Spider提取出来的数据
    • 它的主要任务包括清洗、验证和存储数据
    • Pipeline是由多个阶段组成的处理管道,每个阶段都是一个Python函数或对象
    • 数据在Pipeline中流经各个阶段,每个阶段都可以对数据进行处理,如去重、存储到数据库

33. scrapy框架的基本使用

新建一个scrapy项目 (运行项目一定要使用命令运行

  • cd 到指定的目录
  • 执行命令创建项目scrapy startproject 项目名
  • 执行命令后会创建一个工程目录,目录下有一个同名的包,包中有一个子包spiders
  • 我们需要再spiders包中新建一个爬虫文件
  • 直接使用命令行的方式创建 cd 到工程目录下
  • 执行命令创建主文件 scrapy genspider 文件名 网站url
  • 运行 scrapy 工程 scrapy crawl 在spiders中新建的第一个文件名称
  • 运行scrapy 工程 并且不显示日志信息 scrapy crawl xxx --nolog
  • 修改python解释器并继承全局站点
  • 步骤 : 点击设置→点击Settings→点击对应的工程名称→点击Python Interpreter→点击Add Interpreter→点击Add local Interpreter→设置所有的解释器都为同一版本→点击System Interpreter 勾选继承全局站点包Inherit global site-packages

执行命令 scrapy genspider 文件名 网站url 后在spiders包下生成的文件

import scrapy


# 该类名的定义规则为文件名首字母大写,加一个Spider作为类名
# 本类继承了scrapy框架的Spider父类
class FirstSpider(scrapy.Spider):
    # 爬虫文件的名称,爬虫源文件的唯一标识
    name = "first"
    # 允许的域名列表,用来限定start_urls列表中哪些url可以进行请求发送
    # allowed_domains = ["www.baidu.com"]  # 通常不会使用到
    # 起始的URL列表,该列表中存放的URL会被scrapy自动进行请求的发送
    start_urls = ["https://www.baidu.com"]

    # 发送请求后会自动调用该函数
    # parse函数用于对数据进行解析
    # response参数表示,当请求成功后返回的响应数据
    def parse(self, response):
        print(response)

34.scrapy框架的配置文件

# 工程名称
BOT_NAME = "ScrapyProject"

# 项目模块名称列表
SPIDER_MODULES = ["ScrapyProject.spiders"]
# 模块名称字符串(单个)
NEWSPIDER_MODULE = "ScrapyProject.spiders"

# 设置请求头伪装
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"


# 是否遵从robots.txt协议
ROBOTSTXT_OBEY = False

# 显示指定类型的日志信息
LOG_LEVEL = 'ERROR'  # 只显示错误信息

# 开启管道,使之可以在管道类中对数据进行后续处理、清洗、存储等操作
ITEM_PIPELINES = {
   "ScrapyProject.pipelines.ScrapyprojectPipeline": 300,  # 300表示的是优先级,数值越小优先级越高(key是管道类路径)
    # 有几个管道类就创建几条配置

}

# 指定图片存储的路径
IMAGES_STORE = './imgs'

# 指定媒体文件存储的路径
FILES_STORE = './medias'


# 下载中间件配置
DOWNLOADER_MIDDLEWARES = {
   "ScrapyProject.middlewares.ScrapyProjectDownloaderMiddleware": 543,
}

# 指定Scrapy使用的请求指纹生成的实现版本为 2.7
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
# TWISTED
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
# 编码
FEED_EXPORT_ENCODING = "utf-8"

35. scrapy框架 的入口文件的parse方法中使用xpath

import scrapy

class MainSpider(scrapy.Spider):
    # 主程序文件名称,用于在终端启动
    name = "main"
    # allowed_domains = ["www.gushiwen.cn"]
    start_urls = ["https://www.gushiwen.cn/"]
# 发送请求后会自动调用该函数
    # parse函数用于对数据进行解析
    # response参数表示,当请求成功后返回的响应数据
    def parse(self, response):
        # xpath返回的是列表,而scrapy会将xpath封装为一个Selector对象再返回
        # extract_first() 使用该方法可以将Selector对象转为原始xpath数据,并取出列表中的第一个元素
        # extract()  使用该方法可以将Selector对象转为原始xpath数据
        title = response.xpath('//*[@id="zhengwene3a35aaf1625"]/p[1]/a/b/text()').extract_first()
        content = response.xpath('//*[@id="contsone3a35aaf1625"]//text()').extract()
        # 打印输出获得的内容
        print(title, content[0])

36.scrapy持久化存储

实现持久化存储的方式

  • 基于终端指令

    • 该方法只能将parse方法的返回值存储到本地的文本文件中
    • 在终端输入 scrapy crawl 主程序文件名 -o ./文件名称.csv
    • 只能将数据存入指定类型的文件中
    • 运行存入的文件格式有 :json csv xml jsonlines marshal pickle
    • 使用该方法存储数据的优点简洁高效便捷
    • 使用该方法存储数据的缺点局限性比较强只能存储指定格式的数据,到指定类型的文件中

    使用示例

    def parse(self, response):
        # xpath返回的是列表,而scrapy会将xpath封装为一个Selector对象再返回
        # extract_first() 使用该方法可以将Selector对象转为原始xpath数据,并取出列表中的第一个元素
        # extract()  使用该方法可以将Selector对象转为原始xpath数据
        title = response.xpath('//*[@id="zhengwen5fa055308fb1"]/p[1]/a/b/text()').extract()
        content = response.xpath('//*[@id="contson5fa055308fb1"]//text()').extract()
    
        dic = {
            'title': title,
            'content': content
        }
    
        # 当执行了命令scrapy crawl main -o ./shi.csv 时
        # scrapy会将返回的数据存入指定的文件中
        return dic
    
  • 基于管道

    • 实现流程
      • 数据解析
      • 在item类中定义相关的属性用于封装数据
      • 将解析的数据封装到Item类型的对象中
      • 将item类型的对象提交给管道进行持久化存储操作
      • 在管道类(在pipelines.py文件中)的process_item中要将其接收到item对象中存储的数据进行持久化存储操作
      • 在配置文件中开启管道
    • 好处:通用性强

示例

主函数文件中的代码(用于发送请求和数据解析)

import scrapy
from ScrapyProject.items import ScrapyprojectItem


class MainSpider(scrapy.Spider):
    name = "main"
    # allowed_domains = ["www.gushiwen.cn"]
    start_urls = ["https://www.gushiwen.cn/"]

    # 发送请求后会自动调用该函数
    # parse函数用于对数据进行解析
    # response参数表示,当请求成功后返回的响应数据
    def parse(self, response):
        # xpath返回的是列表,而scrapy会将xpath封装为一个Selector对象再返回
        # extract_first() 使用该方法可以将Selector对象转为原始xpath数据,并取出列表中的第一个元素
        # extract()  使用该方法可以将Selector对象转为原始xpath数据
        title = response.xpath('//div[@id="zhengwen85b7fe68d321"]//b/text()').extract()
        content = response.xpath('//div[@id="contson85b7fe68d321"]//text()').extract()

        # 实例化一个item对象
        item = ScrapyprojectItem()

        # 通过中括号的形式获得Item类中自定义里面的属性
        # 将处理好的数据赋值给item对象
        item['title'] = title
        item['content'] = content

        # 使用yield关键字将item对象提交到管道类的 Pipeline 方法中
        # 让其对数据进行持久化存储
        for i in range(3):
            yield item

items.py文件中的代码 (用于存储解析后的数据)

import scrapy


class ScrapyprojectItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 定义一些属性,在主程序中的parse方法中可以是这写属性
    title = scrapy.Field()
    content = scrapy.Field()

管道类 pipelines.py 中的代码 (用于将数据持久化存储到本地或数据库中)

class ScrapyprojectPipeline:
    fp = None

    # 重新父类的方法
    # 该方法只会在开始爬取的时候调用一次
    def open_spider(self, spider):
        print('开始爬取')
        self.fp = open('shi.csv', 'w', encoding='utf-8')

    # 处理item类型的对象的方法
    # 该方法可以接收爬虫文件提交过来的item对象
    # 每接收到一次item对象,该方法就会执行一次
    def process_item(self, item, spider):
        # 获得自动传入的item对象里面的值
        title = item['title']
        content = item['content']
        self.fp.write('\n'.join(title) + '\n' + '\n'.join(content) + '\n')
        print(title, content)
        return item

    # 重新父类的方法
    # 该方法只会在结束爬取的时候调用一次
    def close_spider(self, spider):
        print("爬取结束了")
        self.fp.close()

配置类 settings 中的代码 (用于配置各种参数,实现各种功能)

# 工程名称
BOT_NAME = "ScrapyProject"

SPIDER_MODULES = ["ScrapyProject.spiders"]
NEWSPIDER_MODULE = "ScrapyProject.spiders"

# 设置请求头伪装
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"


# 是否遵从robots.txt协议
ROBOTSTXT_OBEY = False

# 显示指定类型的日志信息
LOG_LEVEL = 'ERROR'  # 只显示错误信息

# 开启管道,使之可以在管道类中对数据进行后续处理、清洗、存储等操作
ITEM_PIPELINES = {
   "ScrapyProject.pipelines.ScrapyprojectPipeline": 300,  # 300表示的是优先级,数值越小优先级越高

}

# 指定Scrapy使用的请求指纹生成的实现版本为 2.7
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

37.scrapy多管道存储数据

主要实现步骤

  • 在pipelines.py 文件中新建一个管道类
  • 在管道类中实现 三个方法
  • open_spider 打开数据或文件方法
  • process_item 存储数据方法
  • 知识点:process_item 中的return item 表示传递给下一个即将被执行的管道类
  • close_spider 关闭数据库或文件方法
  • 添加该管道类到配置文件中

38.scrapy爬取全站数据(手动发送单个请求)

概念

  • 就是将网站中某个板块下的全部页码对应的页面数据进行爬取

  • 实现方式

    • 方式一: 将所有页面的url添加到start_urls列表中 (不推荐)

    • 自行收到进行请求发送 (推荐)

    • 知识点:scrapy中手动向一个连接发送请求

    • # 当请求成功时,会将响应的数据传给自身的parse函数继续执行里面的逻辑代码
      yield scrapy.Request(url=new_page_url, callback=self.parse)
      

主文件代码

import scrapy


class MainSpider(scrapy.Spider):
    name = "main"
    # allowed_domains = ["www.xxx.com"]
    # 定义第一页的页面链接,会自动爬取
    start_urls = ["https://pic.yesky.com/c/6_25152.shtml"]

    # 定义一个页面连接模板
    page_url = 'https://pic.yesky.com/c/6_25152_%d.shtml'

    # 定义一个页码变量, 从2开始, 因为第一页已经自动发送请求了
    page_num = 2

    def parse(self, response):
        # 获得所有的li元素并将这些li存入列表中,封装为Selector对象返回
        li_list = response.xpath('//ul[@class="classification_listContent"]/li')
        # 循环获得所有li标签下的第二个a标签的文本
        for li in li_list:
            # 获得每个图片的描述
            img_name = li.xpath('./a[2]/text()').extract_first()

            print(img_name)
            # 可以将爬取到的数据存入item对象中

        # 判断要爬取的页面数量大于等于5时
        # 则不再进行爬取新的页面
        if self.page_num <= 5:
            # 拼接页面的链接
            new_page_url = format(self.page_url % self.page_num)

            # 拼接完成后页码加一,用于下一次爬取,下一个页面的数据
            self.page_num += 1

            # 使用  yield scrapy.Request(请求的url, 对返回数据进行处理的回调函数)
            # 手动向一个URL地址发送请求
            # 传入两个参数,一个请求的URL地址,一个回调函数
            # 当请求成功时,会将响应的数据传给自身的parse函数继续执行里面的逻辑代码
            yield scrapy.Request(url=new_page_url, callback=self.parse)

39. scrapy 请求传参(整合主页和详情页的数据)

使用场景

  • 如果爬取解析的数据不在同一张页面中,即可使用请求传参。(深度爬取)
  • 目的是为了让一个页面的数据和详情页面的数据可以存放进一个item对象中

主爬虫程序的代码

import scrapy
from BossCrawling.items import BosscrawlingItem
import time


class MainSpider(scrapy.Spider):
    name = "main"
    # allowed_domains = ["www.xxx.com"]
    start_urls = ["https://www.gushiwen.cn/default_1.aspx"]

    page_url = "https://www.gushiwen.cn/default_%d.aspx"

    page_num = 2

    def parse(self, response):
        # 获得页面中所有的标题
        title_list = response.xpath('//div[@class="sons"]/div[@class="cont"]/div[2]/p/a/b/text()').extract()

        # 获得所有的url
        url_list = response.xpath('//div[@class="sons"]/div[@class="cont"]/div[2]/p/a/@href').extract()

        print(title_list)
        print(url_list)
        for title, detail_url in zip(title_list, url_list):
            print(title, detail_url)
            # time.sleep(5)
            # 实例化一个Item对象
            item = BosscrawlingItem()
            # 将标题存入Item对象中
            item['title'] = title
            # 对详情页发送请求
            # 参数 meta 传入一个字典,字典中存入一个Item对象
            # 字典中的key会作为参数名传递给回调函数parse_detail 并且将对应的值 实体Item对象也传递过去
            yield scrapy.Request(url=detail_url, callback=self.parse_detail, meta={'item': item})

        # 爬取多页的数据
        if self.page_num <= 3:
            full_page_url = format(self.page_url % self.page_num)
            # 页面拼接完成后页码加1,以便于下一次请求下一个页面的数据
            self.page_num += 1
            # 向新的页面发送请求再把响应数据回调给自身,继续爬取详情数据
            yield scrapy.Request(url=full_page_url, callback=self.parse)

    # 响应页面的数据解析方法
    # 用于给手动发送的请求传入的回调函数
    def parse_detail(self, response):
        # 通过响应对象的meta数据获得传过来的Item对象
        item = response.meta['item']
        # 获得岗位描述,并封装为Selector对象后,使用extract_first() 方法转为列表并取出第一个元素
        detail_text = response.xpath('//div[@class="contyishang"]//text()').extract()

        # 并对Item里面的属性进行赋值
        item['detail_text'] = detail_text

        print(detail_text)
        # time.sleep(5)
        # 将封装好的一条数据提交给管道处理
        yield item

items.py 中的代码

import scrapy


class BosscrawlingItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    detail_text = scrapy.Field()

pipelines.py 中的代码

# 主爬虫文件中每返回一个item,就会调用一次该管道方法
class BosscrawlingPipeline:
    def process_item(self, item, spider):
        print(item)
        return item

settings.py 中的代码

# 工程名称
BOT_NAME = "BossCrawling"

# 项目模块名称列表
SPIDER_MODULES = ["BossCrawling.spiders"]
# 模块名称字符串(单个)
NEWSPIDER_MODULE = "BossCrawling.spiders"

# 设置请求头伪装
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"


# 是否遵从robots.txt协议
ROBOTSTXT_OBEY = False

# 显示指定类型的日志信息
LOG_LEVEL = 'ERROR'  # 只显示错误信息

# 开启管道,使之可以在管道类中对数据进行后续处理、清洗、存储等操作
ITEM_PIPELINES = {
   "BossCrawling.pipelines.BosscrawlingPipeline": 300,  # 300表示的是优先级,数值越小优先级越高(key是管道类路径)
    # 有几个管道类就创建几条配置

}

# 指定Scrapy使用的请求指纹生成的实现版本为 2.7
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
# TWISTED
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
# 编码
FEED_EXPORT_ENCODING = "utf-8"

40. scrapy 爬取图片数据

爬取图片步骤

  • 数据解析
  • 将单个图片的地址存入item对象中,并将这个对象提交给图片管道类 yield item
  • 在管道类中导入图片管道父类 ImagesPipeline
  • 自定义一个图片管道类,并继承父类 ImagesPipeline
  • 重写里面的方法
    • get_media_requests(self, item, info) 图片数据请求方法
      • item参数为 主程序传过来的 item对象
    • file_path(self, request, response=None, info=None, *, item=None) 该方法会自动将数据存入 返回的图片名称中,图片的路径可以在配置文件中设置
      • request 为发送请求后的请求对象
      • response 会自动接收到请求的数据
      • item 为主程序中传过来的item对象
    • item_completed(self, results, item, info) 该方法用于将item 返回给下一个即将执行的管道类
  • 在配置文件中配置文件的存储路径 IMAGES_STORE = './imgs'

示例

主爬虫程序代码

import scrapy
from ImgCrawling.items import ImgcrawlingItem


class MainSpider(scrapy.Spider):
    name = "main"
    # allowed_domains = ["www.xxx.com"]
    # 定义第一页的页面链接,会自动爬取
    start_urls = ["https://pic.yesky.com/c/6_25152.shtml"]

    # 定义一个页面连接模板
    page_url = 'https://pic.yesky.com/c/6_25152_%d.shtml'

    # 定义一个页码变量, 从2开始, 因为第一页已经自动发送请求了
    page_num = 2

    def parse(self, response):
        # 获得所有的li元素并将这些li存入列表中,封装为Selector对象返回
        li_list = response.xpath('//ul[@class="classification_listContent"]/li')

        # 循环获得所有li标签下的第二个a标签的文本
        for li in li_list:
            # 获得每个图片的描述
            img_name = li.xpath('./a[2]/text()').extract_first()
            # 获取每个图片的链接, 注意查看图片是否是懒加载
            # 如果是懒加载需要获得伪属性的值
            img_url = li.xpath('./a/img/@src').extract_first()
            print(img_name, img_url)
            # 实例化一个item对象
            item = ImgcrawlingItem()
            # 可以将爬取到的数据存入item对象中
            item['img_url'] = img_url
            # item['img_name'] = img_name

            yield item

        # 判断要爬取的页面数量大于等于5时
        # 则不再进行爬取新的页面
        if self.page_num <= 2:
            # 拼接页面的链接
            new_page_url = format(self.page_url % self.page_num)

            # 拼接完成后页码加一,用于下一次爬取,下一个页面的数据
            self.page_num += 1

            # 使用  yield scrapy.Request(请求的url, 对返回数据进行处理的回调函数)
            # 手动向一个URL地址发送请求
            # 传入两个参数,一个请求的URL地址,一个回调函数
            # 当请求成功时,会将响应的数据传给自身的parse函数继续执行里面的逻辑代码
            yield scrapy.Request(url=new_page_url, callback=self.parse)

管道文件代码

import scrapy
from itemadapter import ItemAdapter

# 导入图片处理管道父类
from scrapy.pipelines.images import ImagesPipeline


# 图片处理管道类,继承父类 ImagesPipeline
class imgsPileLine(ImagesPipeline):

    # 实现父类的 get_media_requests() 方法
    # 根据图片地址进行数据请求的方法
    def get_media_requests(self, item, info):
        # 对图片地址发送请求
        yield scrapy.Request(item['img_url'])

    # 指定图片存储的路径
    def file_path(self, request, response=None, info=None, *, item=None):
        # 请求的地址url中截取图片的名称
        # 对路径进行分割获得最后一个元素
        img_name = request.url.split('/')[-1]
        print(item['img_url'])
        # 将图片文件名返回
        return img_name

    # 图片处理方法
    def item_completed(self, results, item, info):
        # 将item返回给下一个即将被执行的管道类
        return item

item文件代码

import scrapy


class ImgcrawlingItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    img_url = scrapy.Field()
    img_name = scrapy.Field()
    pass

配置文件代码

BOT_NAME = "ImgCrawling"

SPIDER_MODULES = ["ImgCrawling.spiders"]
NEWSPIDER_MODULE = "ImgCrawling.spiders"


# Crawl responsibly by identifying yourself (and your website) on the user-agent
# 设置请求头伪装
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 显示指定类型的日志信息
LOG_LEVEL = 'ERROR'  # 只显示错误信息

# 指定要开启的管道类的名称和优先级
ITEM_PIPELINES = {
   "ImgCrawling.pipelines.imgsPileLine": 300,
}

# 指定图片存储的路径
IMAGES_STORE = './imgs'

# 下载中间件配置
DOWNLOADER_MIDDLEWARES = {
   "ImgCrawling.middlewares.ImgcrawlingDownloaderMiddleware": 543,
}

REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

中间件文件代码

from scrapy import signals

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

import random


# 爬虫中间件
class ImgcrawlingSpiderMiddleware:
    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_spider_input(self, response, spider):

        return None

    def process_spider_output(self, response, result, spider):

        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):

        pass

    def process_start_requests(self, start_requests, spider):

        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)


# 下载中间件
class ImgcrawlingDownloaderMiddleware:

    # UA池
    user_agent_list = [
        "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0",
        "Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.7.62 Version/11.00",
        "Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36"
    ]

    # http类型的IP代理池
    PROXY_http = [
        '192.168.12.5',
        '10.25.2.40',
        '10.24.5.66'
    ]

    # https类型的IP代理池
    PROXY_https = [
        '192.168.12.5',
        '10.25.2.40',
        '10.24.5.66'
    ]

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 拦截请求的方法
    def process_request(self, request, spider):
        # 获得拦截到的请求对象的请求头信息
        # 对请求头进行修改
        # random.choice(self.user_agent_list)  随机获得列表中的一个元素
        request.headers['User-Agent'] = random.choice(self.user_agent_list)
        return None

    # 拦截所有响应的方法
    def process_response(self, request, response, spider):

        return response

    # 拦截发生异常请求的方法
    def process_exception(self, request, exception, spider):
        # 先判断请求的URL类型
        # 当请求数据发生异常时
        # 设置代理IP
        if request.url.split(':') == 'http':
            # 如果请求URL为http类,就随机将http池的一个IP赋值给
            # 请求 对象request 的 meta字典 的 proxy键 中
            request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
        else:
            request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)

        # 将修改了代理IP的请求对象返回,重新进行请求发送
        return request

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

41. scrapy的下载中间件

概念

  • 下载中间件的位置在 引擎和下载器之间
  • 作用:批量拦截整个工程所有的请求和响应
    • 拦截请求:
      • 进行UA伪装
      • IP代理
    • 拦截响应:
      • 篡改 响应数据 和 响应对象
  • 注意要打开配置文件的下载中间件配置

示例代码

# 下载中间件
class ImgcrawlingDownloaderMiddleware:
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the downloader middleware does not modify the
    # passed objects.

    # UA池
    user_agent_list = [
        "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0",
        "Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.7.62 Version/11.00",
        "Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36"
    ]

    # http类型的IP代理池
    PROXY_http = [
        '192.168.12.5',
        '10.25.2.40',
        '10.24.5.66'
    ]

    # https类型的IP代理池
    PROXY_https = [
        '192.168.12.5',
        '10.25.2.40',
        '10.24.5.66'
    ]

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 拦截请求的方法
    def process_request(self, request, spider):
        # 获得拦截到的请求对象的请求头信息
        # 对请求头进行修改
        # random.choice(self.user_agent_list)  随机获得列表中的一个元素
        request.headers['User-Agent'] = random.choice(self.user_agent_list)
        return None

    # 拦截所有响应的方法
    def process_response(self, request, response, spider):
        # Called with the response returned from the downloader.

        # Must either;
        # - return a Response object
        # - return a Request object
        # - or raise IgnoreRequest
        return response

    # 拦截发生异常请求的方法
    def process_exception(self, request, exception, spider):
        # 先判断请求的URL类型
        # 当请求数据发生异常时
        # 设置代理IP
        if request.url.split(':') == 'http':
            # 如果请求URL为http类,就随机将http池的一个IP赋值给
            # 请求 对象request 的 meta字典 的 proxy键 中
            request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
        else:
            request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)

        # 将修改了代理IP的请求对象返回,重新进行请求发送
        return request

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

42. scrapy中间件处理响应数据,在中间的响应方法中使用selenium重新发送请求,获取动态数据

主爬虫程序代码

import scrapy
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from WangYiCrawling.items import WangyicrawlingItem

class MainSpider(scrapy.Spider):
    name = "main"
    # allowed_domains = ["www.xxx.com"]
    start_urls = ["https://news.163.com/"]

    # 用于存储板块的url
    module_urls = []

    def __init__(self):
        # 实例化一个浏览器对象
        service = Service('D:/App Data/PythonProject/WangYiCrawling/WangYiCrawling/utils/chromedriver.exe')
        self.bro = Chrome(service=service)
        self.By = By  # 将By对象存入变量中

    def parse(self, response):
        li_list = response.xpath('//div[@class="ns_area list"]/ul/li')
        # 定义一个下标列表,用于获取多个指定的li标签
        index_list = [1, 2]
        for index in index_list:
            # 获得所有指定板块的url
            m_url = li_list[index].xpath('./a/@href').extract_first()
            self.module_urls.append(m_url)
            print(m_url)

        # 依次对每一个板块的详情页面发送请求
        for url in self.module_urls:
            # 对每个板块的URL发送请求并将返回的响应数据传入回调函数中进行处理
            yield scrapy.Request(url=url, callback=self.parse_detail)

    # 详情页的数据的处理方法
    def parse_detail(self, response):
        titles = response.xpath('//div[@class="ndi_main"]//h3/a/text()').extract()
        detail_urls = response.xpath('//div[@class="ndi_main"]//h3/a/@href').extract()
        # print(response.text)
        for title, detail_url in zip(titles, detail_urls):
            # 示例化一个item对象
            item = WangyicrawlingItem()

            # 将标题存入item对象中
            item['title'] = title
            # 对每一条新闻的详情页发送请求
            yield scrapy.Request(url=detail_url, callback=self.parse_news_detail, meta={'item': item})

    def parse_news_detail(self, response):
        content = response.xpath('//div[@class="post_body"]//text()').extract()
        content = ''.join(content)

        # print(content)
        # 获得传过来的item对象 (回调时传入的meta参数中获得)
        item = response.meta['item']

        # 将新闻内容存入item对象中
        item['content'] = content

        print(item)
        # print(item['title'], item['content'])
        # 将item提交给管道,进行存储
        yield item

    # 关闭资源方法
    def closed(self, spider):
        self.bro.quit()

item 数据封装文件中的代码

import scrapy


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

中间件文件中的代码

from scrapy import signals

from itemadapter import is_item, ItemAdapter
from scrapy.http import HtmlResponse
from time import sleep


class WangyicrawlingDownloaderMiddleware:

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):

        return None

    def process_response(self, request, response, spider):
        # 获得爬虫类中定义的浏览器对象
        bro = spider.bro

        # 判断请求对象中的url是不是在指定的列表中
        if request.url in spider.module_urls:
            # 使用selenium 对URL重新发起请求,获得动态数据
            bro.get(request.url)
            sleep(3)
            # 获得请求的 html数据
            html_data = bro.page_source

            # 重新封装响应对象(篡改)
            # 使用HtmlResponse() 类对响应数据进行封装 (需要导包)
            # 需要传入4个参数
            # url 参数为该响应对象的请求url
            # body 参数为修改后的响应数据体
            # encoding 参数为编码格式
            # request 为该响应对象对应的请求对象
            new_response = HtmlResponse(url=request.url, body=html_data, encoding='utf-8', request=request)

            # 将新的响应对象返回给爬虫主程序的数据封装方法
            return new_response
        else:
            # 如果不是要处理的URL,直接返回响应对象给爬虫主程序的数据封装方法
            return response

    def process_exception(self, request, exception, spider):

        pass

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

管道文件中的代码

class WangyicrawlingPipeline:
    def process_item(self, item, spider):
        # 运行 scrapy crawl main -o ./shi.csv 可将返回的数据存入指定文件中
        return item

配置文件中代码

BOT_NAME = "WangYiCrawling"

SPIDER_MODULES = ["WangYiCrawling.spiders"]
NEWSPIDER_MODULE = "WangYiCrawling.spiders"

# 设置请求头伪装
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 显示指定类型的日志信息
LOG_LEVEL = 'ERROR'  # 只显示错误信息

# 指定图片存储的路径
IMAGES_STORE = './imgs'

# 指定媒体文件存储的路径
MEDIA_STORE = './medias'

# 开启下载中间件
DOWNLOADER_MIDDLEWARES = {
   "WangYiCrawling.middlewares.WangyicrawlingDownloaderMiddleware": 543,
}

REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

43.CrawlSpider爬取全站数据

简介

  • CrawlSpider是Spider的子类, 拥有比父类更多的方法
  • 在命令行创建CrawlSpider文件的方法
  • scrapy genspider -t crawl 文件名 网址
  • 该爬虫文件中的重要知识点
    • 链接提取器:用于提取页面中的所有规则指定的链接
    • 规则解析器:对链接提取器中的规则进行解析并将响应数据返回给回调函数

示例

主爬虫文件中的代码

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from CrawlSpiderPro.items import CrawlspiderproItem
from CrawlSpiderPro.items import DetailItem
from time import sleep


class StudySpider(CrawlSpider):

    name = "study"
    # allowed_domains = ["www.xxx.ccom"]
    start_urls = ["https://www.gushiwen.cn/"]

    # 实例化一个链接提取器对象
    # 根据指定的规则进行链接的提取
    # allow 参数传入一个正则表达式,表示提取的链接的规则  如果为空则表示爬取页面中所有的链接
    links = LinkExtractor(allow=r"(/default_\d+\.aspx|https://www\.gushiwen\.cn/)")  # 获取第一层的链接,并将使用规则解析器进行解析

    # 在所有页面的链接中获取第二层的所有连接进行解析
    link_details = LinkExtractor(allow=r"/shiwenv\.*?")

    # 多个规则解析器,用于自动发送请求
    rules = (
        # 规则解析器
        # 链接提取器里的链接会自动发送请求,
        # 然后将响应数据传给回调函数,去执行数据解析
        Rule(
            link_extractor=links,  # 连接提取器对象
            callback="parse_item",  # 回调函数,用于对数据进行解析
            # 当follow值为True 时用于爬取起始页中所有的页码连接,
            # 会将每一个页面的链接再次发送请求,
            # 然后提取出符合规则的链接,并且会进行去重操作
            # 当follow值为False 时只会获取起始页中符合规则的链接,并发送请求
            follow=True
        ),
        # 详情页规则解析器
        # 自动向这些连接发送请求
        Rule(
            link_extractor=link_details,  # 连接提取器对象
            callback="parse_detail",  # 回调函数,用于对数据进行解析
            # 当follow值为True 时用于爬取起始页中所有的页码连接,
            # 会将每一个页面的链接再次发送请求,
            # 然后提取出符合规则的链接,并且会进行去重操作
            # 当follow值为False 时只会获取起始页中符合规则的链接,并发送请求
            # follow=True
        ),
    )

    dic = dict()

    def parse_item(self, response):
        # 获得所有的包含诗的div 对象
        divs = response.xpath('//div[@class="cont"]/div[2]')
        for div in divs:
            # 从每个div中获得一首诗的标题id
            t_id = div.xpath('./@id').extract_first()
            # 从每个div中获得一首诗的标题id
            title = div.xpath('.//b/text()').extract_first()
            # 实例化一个标题item , 将每一首诗对应的id和标题都存入item中
            item = CrawlspiderproItem()
            item['id'] = t_id
            item['title'] = title
            # 提交给管道处理
            yield item

    def parse_detail(self, response):
        c_id = response.xpath('//div[@class="cont"]/div[2]/@id').extract_first()
        content_list = response.xpath('//div[@class="contyishang"]/p[1]//text()').extract()
        content = ''.join(content_list)

        item = DetailItem()
        item['id'] = c_id
        item['content'] = content
        yield item

item 文件代码

import scrapy


# 标题item
class CrawlspiderproItem(scrapy.Item):
    title = scrapy.Field()
    id = scrapy.Field()


# 详情Item
class DetailItem(scrapy.Item):
    content = scrapy.Field()
    id = scrapy.Field()

管道文件代码

class CrawlspiderproPipeline:
    dic = dict()

    def process_item(self, item, spider):
        if item.__class__.__name__ == 'DetailItem':
            content = item['content']
            c_id = item['id']
            # print("内容id:", c_id)
            # print("内容:", content)
            # if c_id in spider.dic and c_id in spider.dic[c_id]:
            #     spider.dic[c_id][1] = content
            # else:
            #     spider.dic[c_id] = ['', content]
        else:
            title = item['title']
            t_id = item['id']
            # print("标题id:", t_id)
            # print("标题:", title)
            # if t_id in spider.dic and t_id in spider.dic[t_id]:
            #     spider.dic[t_id][0] = title
            # else:
            #     spider.dic[t_id] = [title, '']
        return item

    def close_spider(self, spider):
        # print(spider.dic)
        # print(1)
        # return spider.dic
        pass

配置文件代码

BOT_NAME = "CrawlSpiderPro"

SPIDER_MODULES = ["CrawlSpiderPro.spiders"]
NEWSPIDER_MODULE = "CrawlSpiderPro.spiders"


# Crawl responsibly by identifying yourself (and your website) on the user-agent
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
}

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 只打印错误日志
LOG_LEVEL = 'ERROR'

ITEM_PIPELINES = {
   "CrawlSpiderPro.pipelines.CrawlspiderproPipeline": 300,
}

REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

44.分布式爬取数据

概念

  • 分布式就是让多台服务器对一组数据进行请求
  • 它的作用就是提高爬取数据的效率

分布式爬取数据步骤

  • 安装组件 pip install scrapy_redis

  • 创建一个工程

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

  • 修改当前爬虫文件

    • 导包:from scrapy_redis.spiders import RedisCrawlSpider
    • 将start_urls和allowed_domains进行注释
    • 添加一个新属性:redis_key = ‘列队名称’ (可以被共享的调度器队列的名称)
    • 编写数据解析相关的操作
    • 将当前爬虫类的父类修改为 RedisCrawlSpider
  • 修改配置文件settings

    • 指定使用可以被共享的管道

      ITEM_PIPELINES = {
      	'scrapy_redis.pipelines.ReadisPipeline': 400
      }
      
    • 指定调度器

      # 增加一个去重容器类配置,用于Redis的set集合来存储请求的指纹
      DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
      # 使用scrapy-Redis组件自己封装好的调度器
      SCHDULER = "scrapy_redis.scheduler.Scheduler"
      # 配置调度器是否持久化,当某台服务器宕机重启后,是否会接着上一次爬取的数据继续进行爬取
      SCHEDULER_PERSIST = True
      
    • 指定redis服务器

      # 指定redis
      REDIS_HOST = 'redis服务的IP地址'
      # redis数据库的端口号
      REDIS_PORT = 6379  
      
  • redis相关操作配置

    • 配置文件名称 redis.conf
    • 修改里面的配置
      • 注释 bind 127.0.0.1
      • 关闭保护模式: protected-mode yes该为no
    • 结合配置文件开启redis服务
      • src目录下执行命令 ./redis-server …/redis.conf (配置文件位置)
    • 启动客户端
      • redis-cli
  • 执行工程:scrapy runspider 主爬虫文件名.py

  • 向调度器的队列中放入一个起始url ,调度器的队列在redis的客户端中

    • lpush 队列名称 url网站
  • 爬取到的数据存储在redis的 项目名称:items

45. 增量式爬虫

概念

  • 不重复爬取 已爬取过的链接
  • 只爬取最新更新的链接

思路

  • 将详情页的链接存入redis数据库中
  • 如果链接不存在表示数据第一次爬取 返回 1
  • 如果链接已存在表示已经爬取过了 返回 0
  • 根据存入redis时返回的值判断是否爬取该链接

方法二

  • 也可以直接使用python中的set集合对链接进行存储

46.练习爬取梨视频中的所有视频

主程序

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from LiVideoCrawling.items import LivideocrawlingItem
import random
import json


class MainSpider(CrawlSpider):
    name = "main"
    # allowed_domains = ["www.xxx.com"]
    start_urls = ["https://www.pearvideo.com/category_1"]

    # 定义一个防盗链id列表
    json_ids = [""]
    # 多个规则解析器,用于自动发送请求
    rules = (
        # 向网站中所有的视频详情页发送请求
        Rule(LinkExtractor(allow=r"video_\d+"), callback="parse_item", follow=True),
    )

    def parse_item(self, response):
        # 将所有的请求数据的视频id存入本爬虫类的json_ids中
        # 用于在下载器中间件修改请求头,在请求头中添加防盗链
        self.json_ids.append(response.url.split('_')[-1])
        # print(response.url.split('_')[-1])
        # print(response)

        # 获得视频的id
        list_video_ids = response.xpath('//div[@id="poster"]/@data-cid').extract()
        for vid in list_video_ids:
            random_num = "{:.16f}".format(random.uniform(0, 1))
            mrd = random_num[2:18]
            # 因为视频链接存在于异步加载的链接中,所有组装请求url
            video_json_url = f'https://www.pearvideo.com/videoStatus.jsp?contId={vid}&mrd={mrd}'
            # 对json的URL地址发送请求,传入回调函数,
            # 传入请求参数视频id, 用于拼接完整的视频下载链接
            yield scrapy.Request(url=video_json_url, callback=self.parse_video_url, meta={'vid': vid})

    def parse_video_url(self, response):
        print(response)
        # 将响应数据转为字典格式
        dic_data = response.json()
        # 取出字典中的 假视频URL, 并拼接出真实的视频地址
        v_url = dic_data['videoInfo']['videos']['srcUrl']
        n_id = v_url.split('/')[-1].split('-')[0]
        # 拼接完整视频下载地址
        full_url = v_url.replace(n_id, 'cont-' + response.meta['vid'])
        # 获得视频的名称
        video_name = full_url.split('/')[-1].split('-')[2] + 'ad_hd.mp4'
        print(full_url)
        print(video_name)
        # 对视频地址发送请求,传入回调函数,传入请求传参用于文件的命名
        yield scrapy.Request(url=full_url, callback=self.video_content, meta={'video_name': video_name})
        # item = LivideocrawlingItem()
        # item['video_url'] = full_url
        # item['video_name'] = video_name
        # yield item

    def video_content(self, response):
        # 打开一个文件并使用传过了的文件名,模式为二进制写入
        with open(f"./video/{response.meta['video_name']}", 'wb') as f:
            # 将视频二进制数据写入文件中
            f.write(response.body)

中间件

from scrapy import signals

# useful for handling different item types with a single interface
from itemadapter import is_item, ItemAdapter
from scrapy.http import HtmlResponse
from selenium.webdriver import Chrome
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from time import sleep
from selenium.webdriver.chrome.options import Options  # 无头浏览器类
import re

# 无头浏览器配置
# 不打开浏览器界面的情况下进行请求发送的配置
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
# 避免被检测到使用selenium爬取数据的类
chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])

service = Service('D:/App Data/PythonProject/LiVideoCrawling/LiVideoCrawling/chrome_driver/chromedriver.exe')


class LivideocrawlingDownloaderMiddleware:
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the downloader middleware does not modify the
    # passed objects.
    # 实例化一个浏览器对象
    bro = Chrome(service=service, options=chrome_options)

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        # 设置一个正则表达式,用于匹配指定类型的url
        pattern = r'https://www\.pearvideo\.com/videoStatus\.jsp\?.*?'
        # 判断是否为视频的异步请求url
        if re.match(pattern, request.url):
            # print(request.url)
            # print(spider.json_ids)
            # 请确认为视频异步请求url后
            # 通过循环判断,本次的url对应的视频id
            for jid in spider.json_ids:
                # 获得主页中的预先存入的视频id
                fid = jid.split('_')[-1]
                # 获得当前请求url的视频id
                pattern = r'contId=(\d+)'
                match = re.search(pattern, request.url)
                vid = ''
                if match:
                    vid = match.group(1)
                # print("主页视频id", fid, "详情页视频id", vid)
                # 判断这两个id是否相等
                if fid.startswith(vid):
                    print('相等')
                    # 如果相等则对防盗链请求头进行拼接
                    request.headers['Referer'] = 'https://www.pearvideo.com/' + jid

    def process_response(self, request, response, spider):
        if request.url.startswith(spider.start_urls[0]):
            # 对页面中的首页进行selenium请求
            self.bro.get(request.url)
            html_data = self.bro.page_source
            # 创建一个新的响应对象
            new_response = HtmlResponse(url=request.url, body=html_data.encode(), encoding='utf-8', request=request)
            return new_response
        else:
            return response

    def process_exception(self, request, exception, spider):
        # Called when a download handler or a process_request()
        # (from other downloader middleware) raises an exception.

        # Must either:
        # - return None: continue processing this exception
        # - return a Response object: stops process_exception() chain
        # - return a Request object: stops process_exception() chain
        pass

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

管道

import scrapy
from itemadapter import ItemAdapter
from scrapy.pipelines.files import FilesPipeline
from twisted.web.http import urlparse


class LivideocrawlingPipeline:
    def process_item(self, item, spider):
        return item


# 定义一个文件存储管道类
class MyFilesPipeline(FilesPipeline):
    def get_media_requests(self, item, info):
        yield scrapy.Request(item['video_url'])

    def file_path(self, request, response=None, info=None, *, item=None):

        return item['video_name']

    def item_completed(self, results, item, info):
        # 处理文件下载完成后的结果
        file_paths = [x['path'] for ok, x in results if ok]
        if not file_paths:
            raise Exception('文件下载失败')
        return item

配置文件

BOT_NAME = "LiVideoCrawling"

SPIDER_MODULES = ["LiVideoCrawling.spiders"]
NEWSPIDER_MODULE = "LiVideoCrawling.spiders"


# Crawl responsibly by identifying yourself (and your website) on the user-agent
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
}

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 只显示错误日志
LOG_LEVEL = "ERROR"

# 设置下载请求延迟
DOWNLOAD_DELAY = 0.5

# 设置媒体文件存储路径
FILES_STORE = './video'


SPIDER_MIDDLEWARES = {
   "LiVideoCrawling.middlewares.LivideocrawlingSpiderMiddleware": 543,
}

# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
   "LiVideoCrawling.middlewares.LivideocrawlingDownloaderMiddleware": 543,

}

ITEM_PIPELINES = {
   "LiVideoCrawling.pipelines.LivideocrawlingPipeline": 300,
   "LiVideoCrawling.pipelines.MyFilesPipeline": 301,
}


# Set settings whose default value is deprecated to a future-proof value
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

item

import scrapy


class LivideocrawlingItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 视频url
    video_url = scrapy.Field()
    # 视频名称
    video_name = scrapy.Field()

47. ffmpeg合并视频和音频

使用步骤

  • 安装 ffmpeg 模块 pip install ffmpeg

示例代码

import ffmpeg  # 合并音频和视频的模块
import subprocess  # 进程模块,用于执行cmd命令

# ffmepg命令合并视频和音频
# ffmpeg -i 视频文件路径 -i 音频文件路径 -c:v copy -c:a aac -strict experimental 合并后的视频存放路径和文件名
cmd = f'ffmpeg -i video/“你才十几岁,却打算等她一辈子....”.mp4 -i video/“你才十几岁,却打算等她一辈子....”.mp3 -c:v copy -c:a aac -strict experimental video/output.mp4'

# 运行cmd命令
subprocess.run(cmd)

48.队列和多线程爬取视频

import json
import os.path
import requests
import re
import threading
from threading import Thread
from queue import Queue

# 实例化一个队列对象
q = Queue()

# 请求头伪装
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
}


# 将每一个片段的链接和索引存入队列的方法
def get_link(index_url):
    """
    将每一个片段的链接和索引存入队列的方法
    :param index_url: 视频播放的 url
    :return: 返回标题和视频片段的个数
    """
    # 获得播放页面响应数据
    res = requests.get(url=index_url, headers=headers)
    # 获得包含 m3u8 列表的json字符串
    json_str = re.findall('window.pageInfo = window.videoInfo = (.*?)window.videoResource', res.text, re.S)[0].strip()[:-1]
    # 获得 m3u8 列表的下载url,这里使用了两次 json.loads 目的是将字符串转为字典
    m3u3_list_url = json.loads(json.loads(json_str)['currentVideoInfo']['ksPlayJson'])['adaptationSet'][0]['representation'][1]['url']
    # 获得视频标题
    title = json.loads(json_str)['title']
    # 对 m3u8 列表URL发送请求,获得包含全部片段请求地址的文本内容
    m3u3_text = requests.get(url=m3u3_list_url, headers=headers).text
    # 取出 # 开头的行,并将其他行存入列表中
    # 获得每个视频片段的下载地址,并且将每一个URL作为一个元素存入列表中
    # flags=re.MULTILINE ^ 和 $ 这两个元字符会匹配每一行的开头和结尾作为一个元素存入列表中
    ts_list = re.findall(r'^(?!#).+', m3u3_text, flags=re.MULTILINE)

    # 循环片段请求地址的列表,并使用枚举获得每个元素对应的下标 从 0 开始
    for index, item in enumerate(ts_list):
        # 组装完整的片段请求地址
        ts_url = 'https://ali-safety-video.acfun.cn/mediacloud/acfun/acfun_video/' + item
        # 将每个片段的请求地址和对应的下标,封装为一个列表,作为一个元素提交到队列中
        q.put([ts_url, index])

    # 返回标题和视频片段的个数
    return title, len(ts_list)


# 循环提取队列中的片段url并下载到本地
def download(title):
    """
    循环提取队列中的片段url并下载到本地
    :param title: 视频标题
    :return: None
    """
    # 循环取出队列中的元素,直到取空为止
    while not q.empty():  # 不为空就继续取
        # 按顺序取出队列里面的第一个元素,获得第一个片段的url 和 下标索引,用于合并视频时使用
        ts_url, index = q.get()
        # 向视频片段发送请求,获得二进制数据
        video_content_sub = requests.get(url=ts_url, headers=headers).content
        # 将数据存入本地,并给文件名标记顺序,用于合并
        with open(f'm3u8/{title}_{index}.ts', 'wb') as f:
            f.write(video_content_sub)
            # 打印哪个线程下载了哪个片段
            print(f'{threading.current_thread().name},已下载{title}片段{index}')


# 合并所有视频片段的方法
def combine(title, length):
    """
    合并所有视频片段的方法
    :param title: 视频的标题
    :param length: 视频片段的个数
    :return: None
    """
    # 打开一个文件用于存储所有的片段
    full_video_file = open(f'm3u8/{title}.mp4', 'ab')
    # 循环视频片段的个数获得每一个片段的文件名
    for index in range(length):  # range(length) 从0开始
        # 如果 当前文件存在则打开视频片段文件并读取二进制数据,存入完整的视频文件中
        if os.path.exists(f'm3u8/{title}_{index}.ts'):
            # 打开每一个视频片段文件
            sub_file = open(f'm3u8/{title}_{index}.ts', 'rb')
            # 读取视频片段,并存入最终的视频文件中
            full_video_file.write(sub_file.read())
            # 关闭视频片段文件
            sub_file.close()
            # 转移完毕,删除这个视频片段文件
            os.remove(f'm3u8/{title}_{index}.ts')
    # 所有的片段合并完毕,关闭最终的视频文件
    full_video_file.close()


def main():
    # 要爬取的链接
    url = 'https://www.acfun.cn/v/ac43811169'
    # 根据视频播放地址获得标题和 视频片段的个数
    title, lent = get_link(url)
    # 创建一个任务列表
    tasks = []
    # 循环实例化 8个线程,用于爬取视频片段
    for i in range(8):
        # 实例化线程对象
        # 传入该线程要执行的函数  target=download
        # 传入要执行的函数的参数  args=(title,)
        # 传入该线程的名称  name=f'线程{i}'
        th = Thread(target=download, args=(title,), name=f'线程{i}')
        # 线程开始运行
        th.start()
        # 将每一个线程存入任务列表中
        tasks.append(th)

    # 循环任务列表,获得每一个线程对象
    for t in tasks:
        # 每个线程对象都 调用 join() 方法,让所有线程都执行完毕后再执行后面的代码
        t.join()

    # 当所有线程都执行完毕后,调用合并函数,将所有的视频片段合并为一个视频
    combine(title, lent)


if __name__ == '__main__':
    # 调用主函数运行程序
    main()

49.线程池爬取B站视频

import requests
import re
import ffmpeg  # 合并音频和视频的模块
import subprocess  # 进程模块,用于执行cmd命令
import json
from pprint import pprint  # 格式化输入数据的模块
import os
import time
import concurrent.futures
# 导入线程池类
from multiprocessing.dummy import Pool


def bilibili_video(args):
    bvid, v_title, header = args
    # 定义要删除的特殊字符的正则表达式模式
    pattern = r'[^\w\s]'

    # 使用正则表达式将特殊字符替换为空
    v_title = re.sub(pattern, '', v_title)
    print(v_title)
    # 请求地址
    url = f'https://www.bilibili.com/video/{bvid}/?spm_id_from=333.1073.channel.secondary_floor_video.click&vd_source=c3d1a960498e216c84464e70f74626d6'

    # 发送请求
    response = requests.get(url=url, headers=header)

    # 获取服务器返回的响应文本数据
    html = response.text

    # 获得视频标题, 并去除空格
    title = re.findall(r'property="og:title" content="(.*?)_哔哩哔哩_bilibili', html)[0].replace(' ', '')

    # 提示视频信息
    video_info = re.findall('<script>window.__playinfo__=(.*?)</script>', html)[0]

    # json字符串转为json字典数据
    json_dict = json.loads(video_info)

    # 格式化输出 json数据
    # pprint(json_dict)

    # 提取音频连接
    audio_url = json_dict['data']['dash']['audio'][0]['baseUrl']

    # 提取视频链接
    video_url = json_dict['data']['dash']['video'][0]['baseUrl']

    print(audio_url)
    print(video_url)

    # 获取视频和音频的二进制数据
    audio_content = requests.get(url=audio_url, headers=headers).content
    video_content = requests.get(url=video_url, headers=headers).content

    # 保存音频保存到本地MP3文件中
    with open('video/' + v_title + '.mp3', mode='wb') as audio:
        audio.write(audio_content)

    # 保存视频频保存到本地MP4文件中
    with open('video/' + v_title + '.mp4', mode='wb') as video:
        video.write(video_content)

    cmd = f'ffmpeg -i video/{v_title}.mp4 -i video/{v_title}.mp3 -c:v copy -c:a aac -strict experimental video/full_{v_title}.mp4'

    # 运行cmd命令,合并视频和音频
    subprocess.run(cmd)

    # 删除无用的音频和视频
    os.remove('video/' + v_title + '.mp3')
    os.remove('video/' + v_title + '.mp4')


# 爬取多个视频
# 请求的URL,从博主视频列表f12 点击 XHR 异步请求数据,点击search 的请求,从标头获取前半部分请求地址,从payload获得参数
# link = 'https://api.bilibili.com/x/space/wbi/arc/search'
link = 'https://api.bilibili.com/x/space/wbi/arc/search?mid=30502823&ps=30&tid=0&pn=1&keyword=&order=pubdate&platform=web&web_location=1550101&order_avoided=true&dm_img_list=[%7B%22x%22:2711,%22y%22:-1706,%22z%22:0,%22timestamp%22:161515,%22k%22:106,%22type%22:0%7D,%7B%22x%22:2725,%22y%22:-1653,%22z%22:12,%22timestamp%22:161615,%22k%22:83,%22type%22:0%7D,%7B%22x%22:2976,%22y%22:-1348,%22z%22:124,%22timestamp%22:161716,%22k%22:84,%22type%22:0%7D,%7B%22x%22:3159,%22y%22:-1171,%22z%22:325,%22timestamp%22:161817,%22k%22:94,%22type%22:0%7D,%7B%22x%22:3217,%22y%22:-1134,%22z%22:400,%22timestamp%22:161928,%22k%22:75,%22type%22:0%7D,%7B%22x%22:2980,%22y%22:-1370,%22z%22:160,%22timestamp%22:166358,%22k%22:100,%22type%22:0%7D,%7B%22x%22:2966,%22y%22:-1370,%22z%22:104,%22timestamp%22:166461,%22k%22:113,%22type%22:0%7D,%7B%22x%22:2869,%22y%22:-1473,%22z%22:2,%22timestamp%22:166768,%22k%22:123,%22type%22:0%7D,%7B%22x%22:2939,%22y%22:-1423,%22z%22:40,%22timestamp%22:166873,%22k%22:116,%22type%22:0%7D,%7B%22x%22:3400,%22y%22:-960,%22z%22:495,%22timestamp%22:166973,%22k%22:65,%22type%22:0%7D,%7B%22x%22:3477,%22y%22:-878,%22z%22:557,%22timestamp%22:167073,%22k%22:80,%22type%22:0%7D,%7B%22x%22:4201,%22y%22:-6,%22z%22:998,%22timestamp%22:167174,%22k%22:60,%22type%22:0%7D,%7B%22x%22:3589,%22y%22:-397,%22z%22:252,%22timestamp%22:167275,%22k%22:106,%22type%22:0%7D,%7B%22x%22:4244,%22y%22:323,%22z%22:873,%22timestamp%22:167375,%22k%22:90,%22type%22:0%7D,%7B%22x%22:4423,%22y%22:661,%22z%22:920,%22timestamp%22:167475,%22k%22:103,%22type%22:0%7D,%7B%22x%22:3823,%22y%22:95,%22z%22:287,%22timestamp%22:167576,%22k%22:88,%22type%22:0%7D,%7B%22x%22:4992,%22y%22:1292,%22z%22:1395,%22timestamp%22:167676,%22k%22:99,%22type%22:0%7D,%7B%22x%22:4462,%22y%22:778,%22z%22:817,%22timestamp%22:167777,%22k%22:85,%22type%22:0%7D,%7B%22x%22:3665,%22y%22:-13,%22z%22:2,%22timestamp%22:167878,%22k%22:113,%22type%22:0%7D,%7B%22x%22:3570,%22y%22:3847,%22z%22:1014,%22timestamp%22:169589,%22k%22:78,%22type%22:0%7D,%7B%22x%22:3397,%22y%22:2919,%22z%22:1657,%22timestamp%22:169689,%22k%22:67,%22type%22:0%7D,%7B%22x%22:2423,%22y%22:1716,%22z%22:1324,%22timestamp%22:169789,%22k%22:80,%22type%22:0%7D,%7B%22x%22:1641,%22y%22:859,%22z%22:721,%22timestamp%22:169889,%22k%22:109,%22type%22:0%7D,%7B%22x%22:1095,%22y%22:84,%22z%22:195,%22timestamp%22:169990,%22k%22:121,%22type%22:0%7D,%7B%22x%22:3095,%22y%22:1525,%22z%22:2032,%22timestamp%22:170090,%22k%22:79,%22type%22:0%7D,%7B%22x%22:3877,%22y%22:2220,%22z%22:2776,%22timestamp%22:170193,%22k%22:87,%22type%22:0%7D,%7B%22x%22:3738,%22y%22:2075,%22z%22:2632,%22timestamp%22:170432,%22k%22:102,%22type%22:0%7D,%7B%22x%22:4016,%22y%22:2035,%22z%22:2714,%22timestamp%22:170534,%22k%22:70,%22type%22:0%7D,%7B%22x%22:4660,%22y%22:2106,%22z%22:3168,%22timestamp%22:170636,%22k%22:60,%22type%22:0%7D,%7B%22x%22:4256,%22y%22:1038,%22z%22:2548,%22timestamp%22:170736,%22k%22:70,%22type%22:0%7D,%7B%22x%22:1937,%22y%22:-1738,%22z%22:13,%22timestamp%22:170836,%22k%22:94,%22type%22:0%7D,%7B%22x%22:4467,%22y%22:786,%22z%22:2538,%22timestamp%22:170942,%22k%22:92,%22type%22:0%7D,%7B%22x%22:5266,%22y%22:1577,%22z%22:3338,%22timestamp%22:171164,%22k%22:64,%22type%22:0%7D,%7B%22x%22:2922,%22y%22:-1017,%22z%22:985,%22timestamp%22:171265,%22k%22:117,%22type%22:0%7D,%7B%22x%22:4559,%22y%22:516,%22z%22:2635,%22timestamp%22:171366,%22k%22:68,%22type%22:0%7D,%7B%22x%22:1875,%22y%22:-2193,%22z%22:3,%22timestamp%22:171467,%22k%22:81,%22type%22:0%7D,%7B%22x%22:2902,%22y%22:-1201,%22z%22:1112,%22timestamp%22:171567,%22k%22:69,%22type%22:0%7D,%7B%22x%22:3763,%22y%22:-408,%22z%22:2085,%22timestamp%22:171668,%22k%22:90,%22type%22:0%7D,%7B%22x%22:2787,%22y%22:-1415,%22z%22:1179,%22timestamp%22:171769,%22k%22:118,%22type%22:0%7D,%7B%22x%22:2770,%22y%22:-1445,%22z%22:1155,%22timestamp%22:171869,%22k%22:124,%22type%22:0%7D,%7B%22x%22:4803,%22y%22:854,%22z%22:2252,%22timestamp%22:171969,%22k%22:83,%22type%22:0%7D,%7B%22x%22:7599,%22y%22:3761,%22z%22:4646,%22timestamp%22:172069,%22k%22:115,%22type%22:0%7D,%7B%22x%22:6559,%22y%22:2747,%22z%22:3551,%22timestamp%22:172170,%22k%22:112,%22type%22:0%7D,%7B%22x%22:6796,%22y%22:3008,%22z%22:3739,%22timestamp%22:172271,%22k%22:91,%22type%22:0%7D,%7B%22x%22:6390,%22y%22:2635,%22z%22:3303,%22timestamp%22:172371,%22k%22:62,%22type%22:0%7D,%7B%22x%22:5684,%22y%22:1959,%22z%22:2576,%22timestamp%22:172472,%22k%22:103,%22type%22:0%7D,%7B%22x%22:6751,%22y%22:3048,%22z%22:3600,%22timestamp%22:172572,%22k%22:122,%22type%22:0%7D,%7B%22x%22:7656,%22y%22:4000,%22z%22:4364,%22timestamp%22:172672,%22k%22:86,%22type%22:0%7D,%7B%22x%22:5367,%22y%22:1712,%22z%22:2072,%22timestamp%22:172773,%22k%22:111,%22type%22:0%7D,%7B%22x%22:7820,%22y%22:4165,%22z%22:4525,%22timestamp%22:172900,%22k%22:121,%22type%22:1%7D]&dm_img_str=V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ&dm_cover_img_str=QU5HTEUgKEFNRCwgQU1EIFJhZGVvbiBSWCA1ODAgMjA0OFNQICgweDAwMDA2RkRGKSBEaXJlY3QzRDExIHZzXzVfMCBwc181XzAsIEQzRDExKUdvb2dsZSBJbmMuIChBTU&dm_img_inter=%7B%22ds%22:[%7B%22t%22:4,%22c%22:%22bW9yZQ%22,%22p%22:[78,26,26],%22s%22:[9,9,18]%7D],%22wh%22:[3737,2344,73],%22of%22:[186,372,186]%7D&w_rid=9301bc3f8e4623e2690ad549441d93d5&wts=1708698961'

headers = {
    # 防盗链,合并视频会用到,否则获取不到数据
    'Referer': 'https://space.bilibili.com/30502823/video',
    # 伪装浏览器发送请求
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
    # 用户登录信息,获取高清视频
    "Cookie": "buvid3=CCF3C3B7-1253-4A6D-47AF-6B4E3280335F26080infoc; b_nut=1708267326; i-wanna-go-back=-1; b_ut=7; _uuid=D26ABB9E-9717-41092-DE6B-A1816A2104D10F92648infoc; buvid_fp=6901881735ebbe5ce0357817048ea598; enable_web_push=DISABLE; header_theme_version=CLOSE; buvid4=B72FB178-0CB2-BF22-DB0F-EE4346D4EC5694386-024021912-kuwcfWA1sfNyDQdAlApxtQ%3D%3D; DedeUserID=433125059; DedeUserID__ckMd5=5564be74f6941d32; rpdid=|(umYYmYu|~R0J'u~|)RJu|lR; b_lsid=AB17106104_18DD609646D; bmg_af_switch=1; bmg_src_def_domain=i2.hdslb.com; SESSDATA=ae9aa26b%2C1724244958%2C49a40%2A22CjCrYDgktoDFj1PpigEwOmJLWn3PorV50fX5vdNWdhcfcLZ98uGEh6v9H44Q-l-wKmsSVlYxcEp0OU5vd1BSSkVBdGdvNjZ4aF9VZnc5V09iazdzSzJqTzJXLXRzWE5IZTA0ZmV5cnZFalJjTl95aEppN3BkZVRHQW1nNTlqZjhWTjBrNE82ZU9RIIEC; bili_jct=d5c3a622e88ca77b3978a4c2deffb5a8; sid=7mdeyo6f; bp_video_offset_433125059=901273216085917746; share_source_origin=COPY; bsource=share_source_copy_link; hit-dyn-v2=1; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDg5NTIyMDksImlhdCI6MTcwODY5Mjk0OSwicGx0IjotMX0.AyYCYNpfUju0n7Q39PucR42swSoglckpOgMd43_MrMw; bili_ticket_expires=1708952149; home_feed_column=5; browser_resolution=1920-953; is-2022-channel=1; CURRENT_BLACKGAP=0; CURRENT_FNVAL=4048; CURRENT_QUALITY=80; PVID=4",
}


link_data = requests.get(url=link, headers=headers).json()
# print(link_data)

# 创建ThreadPoolExecutor
# with concurrent.futures.ThreadPoolExecutor() as executor:
#     # for 循环遍历,获取每个视频的数据
#     # 遍历每个视频,并提交任务给线程池
#     for item in link_data['data']['list']['vlist']:
#         bvid = item['bvid']
#         title = item['title']
#         print(title)
#         print(bvid)
#         # bilibili_video(bvid, headers, title)
#
#         executor.submit(bilibili_video, bvid, headers, bvid)
#
#     # 所有任务提交完毕,关闭线程池
#     executor.shutdown()

params_list = []
for item in link_data['data']['list']['vlist']:
    params_list.append((item['bvid'], item['bvid'], headers))

pool = Pool(10)

pool.map(bilibili_video, params_list)

pool.close()

pool.join()

print("完成")
exit()

50. 使用pandas将数据存入表格中

  • 安装pandas 和依赖

    pip install pandas
    # openpyxl 是 pandas 用于处理 Excel 文件的依赖库之一
    pip install openpyxl
    #  xlsxwriter 模块,这是 pandas 用来处理 Excel 文件的一个依赖库
    pip install xlsxwriter
    
  • 页面中导入模块

    import pandas
    import openpyxl
    import xlsxwriter
    
    # 给定的数据列表
    data = [
        {'姓名': '王先生', '会员卡号': '5512', '卡类型': '散客卡', '存值总额': 200.0, '消费总额': 118.0, '赠送金': 0.0, '最后消费时间': '2024-03-03'},
        {'姓名': '杨博善', '会员卡号': '5988', '卡类型': '会员卡', '存值总额': 500.0, '消费总额': 174.0, '赠送金': 100.0, '最后消费时间': '2024-01-14'},
        # 在这里添加其他数据
    ]
    
    # 将数据转为表格
    data_form = pandas.DataFrame(data_list)
    
    # 创建一个 ExcelWriter 对象
    # 指定文件的名称和存储路径
    with pandas.ExcelWriter('member_data.xlsx', engine='xlsxwriter') as writer:
        # 将数据保存到 Excel 文件的 Sheet1 中
        data_form.to_excel(writer, index=False, sheet_name='Sheet1')  
    
  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vermouth-1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值