登陆京东(滑块验证),验证码识别,Scrapy框架介绍及其使用,持久化存储到本地

Ⅰ 案例登陆京东(滑块验证)

【一】下载opencv库

pip install opencv-python

【二】数据准备

  • 先将京东的滑块图片下载到本地
  • 背景图background.png

  • 滑块图tag.png

【三】展示获取滑块的移动数据


import os.path

# 使用 opencv 识别图像计算滑块之间的距离
import cv2
import numpy as np


class CvImageDistance:

    def from_file_get_distanct(self, tag_img_path, background_img_path):
        '''
        根据文件进行识别
        :param tag_img_path: 滑块图片的文件路径
        :param background_img_path: 背景图片的文件路径
        :return:
        '''
        # 滑块
        target = cv2.imread(tag_img_path)
        # 读取到两个图片,进行灰值化处理
        # 背景缺口图
        template = cv2.imread(background_img_path, 0)
        # 转化到灰度
        target = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)
        # 返回绝对值
        target = abs(255 - target)
        # 单通道转3通道
        target = cv2.cvtColor(target, cv2.COLOR_GRAY2RGB)
        template = cv2.cvtColor(template, cv2.COLOR_GRAY2RGB)
        # 进行匹配
        result = cv2.matchTemplate(target, template, cv2.TM_CCOEFF_NORMED)
        # 通过np转化为数值,就是坐标
        x, y = np.unravel_index(result.argmax(), result.shape)
        return y, x

    def from_buffer_get_distanct(self, tag_img, background_img):
        '''
        根据二进制进行识别
        :param tag_img_path: 滑块图片的二进制
        :param bg: 背景图片的二进制
        :return:
        '''
        target = cv2.imdecode(np.frombuffer(tag_img, np.uint8), cv2.IMREAD_COLOR)

        # 如果是PIL.images就换读取方式
        template = cv2.imdecode(np.frombuffer(background_img, np.uint8), cv2.IMREAD_COLOR) if type(
            background_img) == bytes else cv2.cvtColor(
            np.asarray(background_img), cv2.COLOR_RGB2BGR)

        # 转化到灰度
        target = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)

        # 返回绝对值
        target = abs(255 - target)

        # 单通道转3通道
        target = cv2.cvtColor(target, cv2.COLOR_GRAY2RGB)

        # 进行匹配
        result = cv2.matchTemplate(target, template, cv2.TM_CCOEFF_NORMED)

        # 通过np转化为数值,就是坐标
        x, y = np.unravel_index(result.argmax(), result.shape)
        return y, x

    def get_distance(self, background_img_path, tag_img_path):
        # 读取到背景图片的 rgb
        background_rgb = cv2.imread(background_img_path)
        # 读取到滑块图片的 rgb
        tag_rgb = cv2.imread(tag_img_path)
        # 计算结果
        res = cv2.matchTemplate(background_rgb, tag_rgb, cv2.TM_CCOEFF_NORMED)
        # 获取最小长度
        lo = cv2.minMaxLoc(res)

        # 识别返回滑动距离
        return lo[2][0]


if __name__ == '__main__':
    tag_img_path = os.path.join(os.path.dirname(__file__), 'tag.png')
    background_img_path = os.path.join(os.path.dirname(__file__), 'background.png')
    with open(tag_img_path, 'rb') as f:
        tag_img = f.read()
    with open(background_img_path, 'rb') as f:
        background_img = f.read()
    cv_obj = CvImageDistance()

    print(cv_obj.from_buffer_get_distanct(tag_img, background_img))    # (np.int64(136), np.int64(24))
    print(cv_obj.get_distance(background_img_path, tag_img_path))   # 136
  • background.png

  • tag.png

【四】代码实现

import random
import time
import cv2
from urllib import request
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from cvImg import CvImageDistance
from selenium.webdriver.chrome.service import Service


class SpiderJd:
    def __init__(self, username, password):
        self.username = username
        self.password = password
        self.index_url = 'https://www.jd.com'
        self.cv_obj = CvImageDistance()
        self.browser = webdriver.Chrome(
            service=Service(executable_path="./chromedriver.exe")
        )

    def __sleep_time(self):
        time.sleep(random.randint(1, 3))

    def login(self):
        # 【一】访问京东官网
        self.browser.get(self.index_url)

        self.__sleep_time()

        # 【二】在页面上找到请登陆的按钮
        login_btn = self.browser.find_element(By.XPATH, '//*[@id="ttbar-login"]/a[1]')

        # 【三】点击登陆按钮跳转到登陆界面
        login_btn.click()
        self.__sleep_time()

        # 【四】获取到用户名和密码的输入框
        username_input = self.browser.find_element(By.XPATH, '//*[@id="loginname"]')
        password_input = self.browser.find_element(By.XPATH, '//*[@id="nloginpwd"]')

        # 输入用户名和密码
        username_input.send_keys(self.username)
        self.__sleep_time()
        password_input.send_keys(self.password)
        self.__sleep_time()

        # 【五】获取登陆按钮
        login_submit = self.browser.find_element(By.XPATH, '//*[@id="loginsubmit"]')

        # 【六】点击登陆按钮
        login_submit.click()
        self.__sleep_time()

        # 【七】弹出滑动验证码
        # (1)获取到当前验证码所在的标签
        # 需要移动的滑块的标签
        code_img_tag = self.browser.find_element(By.XPATH,
                                                 '//*[@id="JDJRV-wrap-loginsubmit"]/div/div/div/div[1]/div[2]/div[2]/img')
        # 滑块背景图完整的图片
        code_img_background_tag = self.browser.find_element(By.XPATH,
                                                            '//*[@id="JDJRV-wrap-loginsubmit"]/div/div/div/div[1]/div[2]/div[1]/img')
        code_btn_tag = self.browser.find_element(By.XPATH,
                                                 '//*[@id="JDJRV-wrap-loginsubmit"]/div/div/div/div[2]/div[3]')

        # (2)获取得到滑块背景图的完整图片和滑块图片
        background_img = code_img_background_tag.get_attribute("src")
        tag_img = code_img_tag.get_attribute("src")

        # (3)解析当前图片链接得到本地的图片
        request.urlretrieve(background_img, "background.png")
        request.urlretrieve(tag_img, "tag.png")
        self.__sleep_time()

        # (4)计算缺口之间的距离
        distance = self.cv_obj.get_distance(background_img_path="background.png", tag_img_path="tag.png")

        x_location = int(distance * 240 / 360)
        # 【八】准备滑块滑动
        # (1)创建滑块动作链对象
        action = ActionChains(self.browser)
        # (2)按住滑块
        action.click_and_hold(code_btn_tag)

        # (3)滑动到指定位置
        # 定义起始位置
        track = 0
        while track < x_location:
            # 每次只划一部分
            action.move_by_offset(2, 0).perform()
            # 累加
            track += 2
        self.__sleep_time()
        # (4)释放滑块
        action.release(code_btn_tag).perform()
        self.__sleep_time()


if __name__ == '__main__':
    spider = SpiderJd(
        username="15036883333",
        password="666666zczczczcz"
    )
    spider.login()

  • cvImg.py
import os.path

import cv2
import numpy as np


class CvImageDistance:

    def from_file_get_distanct(self, tag_img_path, background_img_path):
        '''
        根据文件进行识别
        :param tag_img_path: 滑块图片的文件路径
        :param background_img_path: 背景图片的文件路径
        :return:
        '''
        target = cv2.imread(tag_img_path)
        # 读取到两个图片,进行灰值化处理
        template = cv2.imread(background_img_path, 0)
        # 转化到灰度
        target = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)
        # 返回绝对值
        target = abs(255 - target)
        # 单通道转3通道
        target = cv2.cvtColor(target, cv2.COLOR_GRAY2RGB)
        template = cv2.cvtColor(template, cv2.COLOR_GRAY2RGB)
        # 进行匹配
        result = cv2.matchTemplate(target, template, cv2.TM_CCOEFF_NORMED)
        # 通过np转化为数值,就是坐标
        x, y = np.unravel_index(result.argmax(), result.shape)
        return y, x

    def from_buffer_get_distanct(self, tag_img, background_img):
        '''
        根据二进制进行识别
        :param tag_img_path: 滑块图片的二进制
        :param bg: 背景图片的二进制
        :return:
        '''
        target = cv2.imdecode(np.frombuffer(tag_img, np.uint8), cv2.IMREAD_COLOR)

        # 如果是PIL.images就换读取方式
        template = cv2.imdecode(np.frombuffer(background_img, np.uint8), cv2.IMREAD_COLOR) if type(
            background_img) == bytes else cv2.cvtColor(
            np.asarray(background_img), cv2.COLOR_RGB2BGR)

        # 转化到灰度
        target = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)

        # 返回绝对值
        target = abs(255 - target)

        # 单通道转3通道
        target = cv2.cvtColor(target, cv2.COLOR_GRAY2RGB)

        # 进行匹配
        result = cv2.matchTemplate(target, template, cv2.TM_CCOEFF_NORMED)

        # 通过np转化为数值,就是坐标
        x, y = np.unravel_index(result.argmax(), result.shape)
        return y, x

    def get_distance(self, background_img_path, tag_img_path):
        # 读取到背景图片的 rgb
        background_rgb = cv2.imread(background_img_path)
        # 读取到滑块图片的 rgb
        tag_rgb = cv2.imread(tag_img_path)
        # 计算结果
        res = cv2.matchTemplate(background_rgb, tag_rgb, cv2.TM_CCOEFF_NORMED)
        # 获取最小长度
        lo = cv2.minMaxLoc(res)

        # 识别返回滑动距离
        return lo[2][0]


if __name__ == '__main__':
    tag_img_path = os.path.join(os.path.dirname(__file__), 'tag.png')
    background_img_path = os.path.join(os.path.dirname(__file__), 'background.png')
    with open(tag_img_path, 'rb') as f:
        tag_img = f.read()
    with open(background_img_path, 'rb') as f:
        background_img = f.read()
    cv_obj = CvImageDistance()

    print(cv_obj.from_buffer_get_distanct(tag_img, background_img))
    print(cv_obj.get_distance(background_img_path, tag_img_path))

Ⅱ 验证码识别

【一】图鉴平台

【1】官网

【2】介绍

  • 比较好用的平台

【3】价格

【4】使用

import base64
import json
import requests
# 一、图片文字类型(默认 3 数英混合):
# 1 : 纯数字
# 1001:纯数字2
# 2 : 纯英文
# 1002:纯英文2
# 3 : 数英混合
# 1003:数英混合2
#  4 : 闪动GIF
# 7 : 无感学习(独家)
# 11 : 计算题
# 1005:  快速计算题
# 16 : 汉字
# 32 : 通用文字识别(证件、单据)
# 66:  问答题
# 49 :recaptcha图片识别
# 二、图片旋转角度类型:
# 29 :  旋转类型
#
# 三、图片坐标点选类型:
# 19 :  1个坐标
# 20 :  3个坐标
# 21 :  3 ~ 5个坐标
# 22 :  5 ~ 8个坐标
# 27 :  1 ~ 4个坐标
# 48 : 轨迹类型
#
# 四、缺口识别
# 18 : 缺口识别(需要2张图 一张目标图一张缺口图)
# 33 : 单缺口识别(返回X轴坐标 只需要1张图)
# 五、拼图识别
# 53:拼图识别
def base64_api(uname, pwd, img, typeid):
    with open(img, 'rb') as f:
        base64_data = base64.b64encode(f.read())
        b64 = base64_data.decode()
    data = {"username": uname, "password": pwd, "typeid": typeid, "image": b64}
    result = json.loads(requests.post("http://api.ttshitu.com/predict", json=data).text)
    if result['success']:
        return result["data"]["result"]
    else:
        #!!!!!!!注意:返回 人工不足等 错误情况 请加逻辑处理防止脚本卡死 继续重新 识别
        return result["message"]
    return ""


if __name__ == "__main__":
    img_path = "C:/Users/Administrator/Desktop/file.jpg"
    result = base64_api(uname='你的账号', pwd='你的密码', img=img_path, typeid=3)
    print(result)

【二】超级鹰

【1】官网

【2】介绍

【3】价格

【4】使用

import requests
from hashlib import md5

class Chaojiying_Client(object):

    def __init__(self, username, password, soft_id):
        self.username = username
        password = password.encode('utf8')
        self.password = md5(password).hexdigest()
        self.soft_id = soft_id
        self.base_params = {
            'user': self.username,
            'pass2': self.password,
            'softid': self.soft_id,
        }
        self.headers = {
            'Connection': 'Keep-Alive',
            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
        }

    def PostPic(self, im, codetype):
        """
        im: 图片字节
        codetype: 题目类型 参考 http://www.chaojiying.com/price.html
        """
        params = {
            'codetype': codetype,
        }
        params.update(self.base_params)
        files = {'userfile': ('ccc.jpg', im)}
        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
                          headers=self.headers)
        return r.json()

    def PostPic_base64(self, base64_str, codetype):
        """
        im: 图片字节
        codetype: 题目类型 参考 http://www.chaojiying.com/price.html
        """
        params = {
            'codetype': codetype,
            'file_base64': base64_str
        }
        params.update(self.base_params)
        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, headers=self.headers)
        return r.json()

    def ReportError(self, im_id):
        """
        im_id:报错题目的图片ID
        """
        params = {
            'id': im_id,
        }
        params.update(self.base_params)
        r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
        return r.json()


if __name__ == '__main__':
    chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰用户名的密码', '96001')  # 用户中心>>软件ID 生成一个替换 96001
    im = open('a.jpg', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
    print(chaojiying.PostPic(im, 1902))  # 1902 验证码类型  官方网站>>价格体系 3.4+版 print 后要加()
    # print chaojiying.PostPic(base64_str, 1902)  #此处为传入 base64代码

Ⅲ Scrapy框架介绍

【一】开源和协作的框架

  • 其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,

  • 使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。

  • 但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

  • Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。

  • 因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。

  • Scrapy框架类似于Django框架

【二】整体架构大致如下

【1】引擎(EGINE)

  • 引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。

【2】调度器(SCHEDULER)

  • 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回.
  • 可以想像成一个URL的优先级队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址

【3】下载器(DOWLOADER)

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

【4】爬虫(SPIDERS)

  • SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求

【5】项目管道(ITEM PIPLINES)

  • 在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作
  • 下载器中间件(Downloader Middlewares)位于Scrapy引擎和下载器之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response,

【6】爬虫中间件(Spider Middlewares)

  • 位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)

【三】官网链接

Ⅳ Scrapy框架之安装

# window系统
pip install scrapy
# macos / linux
pip install scrapy

【一】Windows平台

【1】安装wheel文件

  • 在 Windows 平台上安装 Scrapy 之前
    • 首先需要安装 wheel 文件。wheel 文件是一种 Python 包的分发格式,可以方便地进行安装。
    • 可以通过以下命令来安装 wheel:
pip3 install wheel

【2】安装 lxml 解析器

  • Scrapy 使用 lxml 解析器来进行 HTML 和 XML 的解析工作。
  • 要安装 lxml,可以执行以下命令:
pip3 install lxml

【3】安装 pyopenssl

  • Scrapy 在进行 HTTPS 请求时,需要使用 pyopenssl 模块来提供 SSL/TLS 支持。
  • 安装 pyopenssl 可以使用以下命令:
pip3 install pyopenssl

【4】下载并安装pywin32

【5】下载twisted的wheel文件

【6】安装twisted

  • 下载完成 twisted 的 wheel 文件后,可以使用以下命令安装 twisted:
pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd**.whl
  • 将 下载目录 替换为你实际下载 twistedwheel 文件所在的目录
  • 并根据您的 Python 环境选择正确的文件名进行替换。

【7】安装scrapy

  • 可以优先尝试这个命令,报错再按照上述安装对应的依赖
  • 当上述依赖项都安装完成后,可以使用以下命令来安装 Scrapy:
pip3 install scrapy

【二】Linux平台

  • 在 Linux 平台上安装 Scrapy,可以通过以下命令进行安装:
pip3 install scrapy

Ⅴ Scrapy框架之基本使用

# 【一】创建爬虫项目
# startproject 项目名字

# 【二】创建爬虫文件
# cd 项目
# scrapy genspider 爬虫文件名 允许访问的域名

# 【三】启动爬虫
# 执行爬虫程序

# 【四】创建一个 Request 对象
# 将 start_urls 列表里面的网址包装成 Request 对象 向目标地址发起请求

# 【五】去调度器引擎 把 请求交给引擎 让引擎分配任务

# 【六】调度器引擎 把 请求 拿过来 下一步 在会给引擎

# 【七】引擎交给下载器 下载器下载数据

# 【八】将下载好的数据返回给引擎并生成一个 Response 对象

# 【九】爬虫程序逻辑中
# 对数据进行解析和下载

# 【十】下载完成后又回到引擎 再调度 地址生成request 对象

【一】查看帮助

scrapy -h
scrapy <command> -h
  • 第一个命令用于查看全部可用命令的帮助信息
  • 第二个命令用于查看特定命令的帮助信息

【二】全局命令和项目命令

  • Project-only必须切到项目文件夹下才能执行
  • Global的命令则不需要
Global commands:
startproject #创建项目
genspider    #创建爬虫程序
settings     #如果是在项目目录下,则得到的是该项目的配置
runspider    #运行一个独立的python文件,不必创建项目
shell        #scrapy shell url地址  在交互式调试,如选择器规则正确与否
fetch        #独立于程单纯地爬取一个页面,可以拿到请求头
view         #下载完毕后直接弹出浏览器,以此可以分辨出哪些数据是ajax请求
version      #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依赖库的版本
Project-only commands:
crawl        #运行爬虫,必须创建项目才行,确保配置文件中ROBOTSTXT_OBEY = False
check        #检测项目中有无语法错误
list         #列出项目中所包含的爬虫名
parse        #scrapy parse url地址 --callback 回调函数  #以此可以验证我们的回调函数是否正确
bench        #scrapy bentch压力测试

【1】全局命令(Global commands)

  • startproject:创建一个新的 Scrapy 项目。
  • genspider:创建一个新的爬虫程序。
  • settings:显示一个 Scrapy 项目的配置信息。
  • runspider:运行一个独立的 Python 文件作为爬虫,不需要创建项目。
  • shell:进入 Scrapy 的交互式调试环境,可以检查选择器规则是否正确。
  • fetch:单独请求一个页面,并获取响应结果。
  • view:下载指定页面并在浏览器中打开,用于检查通过哪些请求获取数据。
  • version:查看当前安装的 Scrapy 版本号。

【2】项目命令(Project-only commands)

  • crawl:运行一个 Scrapy 爬虫,必须在项目目录下执行且确保配置文件中的 ROBOTSTXT_OBEY 设置为 False。
  • check:检查项目中是否存在语法错误。
  • list:列出项目中包含的所有爬虫名称。
  • parse:使用回调函数解析给定的 URL,用于验证回调函数是否正确。
  • bench:用于对 Scrapy 进行压力测试。

【3】官网链接

【三】创建项目

【1】创建步骤

(1)创建项目命令

  • 通过 Scrapy 命令创建项目
scrapy startproject 项目名

scrapy startproject SpiderNewsProjects

(2)进入爬虫项目文件

  • 切换到爬虫项目目录
cd SpiderNewsProjects

(3)创建spider项目

  • 创建自定义的爬虫程序脚本
scrapy genspider 自定爬虫程序文件名 目标网址
  • 示例
scrapy genspider wangyi news.163.com
scrapy genspider huanqiu huanqiu.com
  • 创建成功后会在 spiders文件夹下看到我们创建的自定义的文件名的py文件

【2】目录结构

(1)概览

├── NewsPro 						  # 项目名
│   ├── __init__.py
│   ├── items.py					# 类似于django的 models表模型,一个个模型类
│   ├── middlewares.py		# 中间件
│   ├── pipelines.py 			# 管道---》写持久化
│   ├── settings.py				# 项目配置文件
│   └── spiders						# 里面放了自定义的爬虫,类似于app
│       ├── __init__.py
│       ├── huanqiu.py		# 自定义爬虫文件
│       └── wangyi.py			# 自定义爬虫文件
└── scrapy.cfg						# 项目上线配置

(2)说明

  • scrapy.cfg
    • 项目的主配置信息,用来部署scrapy时使用,爬虫相关的配置信息在settings.py文件中。
  • items.py
    • 设置数据存储模板,用于结构化数据
    • 如:Django的Model
  • pipelines
    • 数据处理行为
    • 如:一般结构化的数据持久化
  • settings.py
    • 配置文件
    • 如:递归的层数、并发数,延迟下载等。强调:配置文件的选项必须大写否则视为无效,正确写法USER_AGENT='xxxx'
  • spiders
    • 爬虫目录
    • 如:创建文件,编写爬虫规则

【四】简单使用

  • wangyi.py
import scrapy

# 自动以当前文件夹命名的类型
class WangyiSpider(scrapy.Spider):
    # 爬虫文件的唯一标识 
    name = "wangyi"
    # 允许访问的域名
    allowed_domains = ["news.163.com"]
    # 起始 的 URL 列表 (重要)
    # 列表内部的 url 都会被框架进行异步请求发送
    start_urls = ["http://news.163.com/"]
		
    # 数据解析主函数:自动调用
    # 调用的次数取决于 start_urls 列表内元素的个数
    def parse(self, response):
        # response : 表示的就是响应对象
        print("response:::", response.text)

【五】启动项目

  • 通过指定的命令启动 Scrapy 项目,而非通过文件启动
# 【1】纯命令行
scrapy crawl baidu
# 【2】py 文件启动
在项目根目录下创建一个启动py文件

from scrapy.cmdline import execute

execute(['scrapy', 'crawl', '自定义爬虫文件名', "--nolog"])

【六】优化启动

【1】降低日期等级

  • 在 settings.py 文件中添加指定配置项
# 指定输出日志的类型:
LOG_LEVEL = 'ERROR'

【2】不遵循ROBOTS协议

ROBOTSTXT_OBEY = False

【3】以启动文件启动项目

  • 每一次终端启动和麻烦,我们可以在项目根目录下新建启动文件
  • bin.py:
from scrapy.cmdline import execute


execute(['scrapy', 'crawl', '自定义爬虫文件名', "--nolog"])

Ⅵ Scrapy框架之数据解析

【一】项目准备

【1】前言

  • 如果终端还在第一个项目的文件夹中,则需要在终端中执行cd ../返回到上级目录,在去新建另一个项目。

【2】创建项目步骤

(1)创建工程

scrapy startproject 项目名称

(2)切换到项目目录

cd 项目名称

(3)创建爬虫文件

scrapy genspider 爬虫文件名 www.xxx.com

(4)配置文件的修改

  • settings.py
# 不遵从robots协议
ROBOTSTXT_OBEY = False

# 指定输出日志的类型:
LOG_LEVEL = 'ERROR'

from fake_useragent import UserAgent

# 指定UA:
USER_AGENT = UserAgent().random

(5)编写爬虫文件:

  • spiders/爬虫文件名.py

【3】启动项目

  • 命令启动
scrapy crawl spider_news
  • 项目根目录下新建启动文件
    • start.py:
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', '爬虫文件名', "--nolog"])

【4】项目执行流程简解

  • 启动爬虫:
    • 开始执行爬虫程序。
  • 创建Request对象:
    • 将start_urls地址包装成request对象,其中包含待爬取的网页链接、回调函数等信息。
  • 交给引擎:
    • 将创建好的request对象交给引擎进行处理。
  • 调度器排队:
    • 引擎将请求交给调度器,调度器按照一定的策略(如先进先出)将请求放入队列中进行排队。
  • 引擎处理:
    • 引擎从调度器中取出请求,并选择合适的下载中间件进行处理。
  • 下载器处理:
    • 下载器根据请求中的链接发起网络请求,下载相应的页面内容,并将下载结果返回给引擎。
  • 下载完成:
    • 下载器将网页内容下载完成后,将下载结果返回给引擎。
  • 引擎传递给爬虫:
    • 引擎将下载结果传递给相应的爬虫解析函数(例如parse函数)进行处理。
  • 爬虫解析:
    • 爬虫解析函数对下载下来的网页内容进行解析,提取出需要的数据,并生成新的Request对象或是Item对象。
  • 回到步骤2:
    • 解析函数可以继续生成新的Request对象,或是处理Item对象
    • 然后回到步骤2,继续执行后续的请求和解析过程。

【二】爬虫文件结构解析

import scrapy

# 爬虫类,继承了scrapy.Spider
class FirstSpider(scrapy.Spider):
    # 爬虫文件的名称,是当前爬虫文件的唯一标识
    name = "first"
    # 允许访问的域名
    allowed_domains = ["www.cnblogs.com/"]
    # 起始的url列表:可以将即将被请求的url,存放在当前列表中。
    # 默认情况,列表中存储的url都会被scrapy框架进行get请求的发送
    start_urls = ["https://www.cnblogs.com/"]

    # 实现数据解析
    # 参数response表示请求对应的响应对象
    # parse方法调用的次数取决于请求的次数
    def parse(self, response):
        # 可以在响应对象中直接使用xpath进行数据解析
        pass

【三】CSS解析器

【1】概览

  • response.css(‘a’)返回的是selector对象,
  • response.css(‘a’).extract()返回的是a标签对象
  • response.css(‘a::text’).extract_first()返回的是第一个a标签中文本的值
  • response.css(‘a::attr(href)’).extract_first()返回的是第一个a标签中href属性的值
  • response.css(‘a[href*=image]::attr(href)’).extract()返回所有a标签中href属性包含image的值
  • response.css(‘a[href*=image] img::attr(src)’).extract()返回所有a标签下image标签的src属性

【2】基本语法

语法 说明
* 选择所有节点
#container 选择id为container的节点
.container 选择所有class包含container的节点
div,p 选择所有 div 元素和所有 p 元素
li a 选取所有li 下所有a节点
ul + p 选取ul后面的第一个p元素
div#container > ul 选取id为container的div的第一个ul子元素
ul ~p 选取与ul相邻的所有p元素
a[title] 选取所有有title属性的a元素
a[href=”http://baidu.com”] 选取所有href属性为http://baidu.com的a元素
a[href*=”baidu”] 选取所有href属性值中包含baidu的a元素
a[href^=”http”] 选取所有href属性值中以http开头的a元素
a[href$=”.jpg”] 选取所有href属性值中以.jpg结尾的a元素
input[type=radio]:checked 选择选中的radio的元素
div:not(#container) 选取所有id为非container 的div属性
li:nth-child(3) 选取第三个li元素
li:nth-child(2n) 选取第偶数个li元素
a::attr(href) 选取a标签的href属性
a::text 选取a标签下的文本

【3】更多语法

  • 选择器是一种模式,用于选择需要添加样式的元素。
  • CSS列指示该属性是在哪个 CSS 版本中定义的。(CSS1、CSS2 还是 CSS3
选择器 例子 例子描述
.class .intro 选择 class=”intro” 的所有元素。
#id #firstname 选择 id=”firstname” 的所有元素。
* * 选择所有元素。
element p 选择所有

元素。

element,element div,p 选择所有
元素和所有

元素。

element element] div p 选择
元素内部的所有

元素。

element>element div>p 选择父元素为
元素的所有

元素。

element+element div+p 选择紧接在
元素之后的所有

元素。

[attribute] [target] 选择带有 target 属性所有元素。
[attribute=value] [target=_blank] 选择 target=”_blank” 的所有元素。
[attribute~=value] [title~=flower] 选择 title 属性包含单词 “flower” 的所有元素。
[attribute|=value] [lang|=en] 选择 lang 属性值以 “en” 开头的所有元素。
:link a:link 选择所有未被访问的链接。
:visited a:visited 选择所有已被访问的链接。
:active a:active 选择活动链接。
:hover a:hover 选择鼠标指针位于其上的链接。
:focus input:focus 选择获得焦点的 input 元素。
:first-letter p:first-letter 选择每个

元素的首字母。

:first-line p:first-line 选择每个

元素的首行。

:first-child p:first-child 选择属于父元素的第一个子元素的每个

元素。

:before p:before 在每个

元素的内容之前插入内容。

:after p:after 在每个

元素的内容之后插入内容。

:lang(language) p:lang(it) 选择带有以 “it” 开头的 lang 属性值的每个

元素。

element1~element2 p~ul 选择前面有

元素的每个

    元素。
[attribute^=value] a[src^=”https”] 选择其 src 属性值以 “https” 开头的每个 元素。
[attribute$=value] a[src$=”.pdf”] 选择其 src 属性以 “.pdf” 结尾的所有 元素。
[attribute*=value] a[src*=”abc”] 选择其 src 属性中包含 “abc” 子串的每个 元素。
:first-of-type p:first-of-type 选择属于其父元素的首个

元素的每个

元素。

:last-of-type p:last-of-type 选择属于其父元素的最后

元素的每个

元素。

:only-of-type p:only-of-type 选择属于其父元素唯一的

元素的每个

元素。

:only-child p:only-child 选择属于其父元素的唯一子元素的每个

元素。

:nth-child(n) p:nth-child(2) 选择属于其父元素的第二个子元素的每个

元素。

:nth-last-child(n) p:nth-last-child(2) 同上,从最后一个子元素开始计数。
:nth-of-type(n) p:nth-of-type(2) 选择属于其父元素第二个

元素的每个

元素。

:nth-last-of-type(n) p:nth-last-of-type(2) 同上,但是从最后一个子元素开始计数。
:last-child p:last-child 选择属于其父元素最后一个子元素每个

元素。

:root :root 选择文档的根元素。
:empty p:empty 选择没有子元素的每个

元素(包括文本节点)。

:target #news:target 选择当前活动的 #news 元素。
:enabled input:enabled 选择每个启用的 元素。
:disabled input:disabled 选择每个禁用的 元素
:checked input:checked 选择每个被选中的 元素。
:not(selector) :not(p) 选择非

元素的每个元素。

::selection ::selection 选择被用户选取的元素部分。

【四】Xpath解析器

【1】概览

  • Scrapy xpath语法,Xpath是XML Path的简介,基于XML树状结构,可以在整个树中寻找锁定目标节点。由于HTML文档本身就是一个标准的XML页面,因此我们可以使用XPath的语法来定位页面元素。

【2】路径表达式

表达式 描述 实例
nodename 选取nodename节点的所有子节点 //div
/ 从根节点选取 /div
// 选取任意位置的节点,不考虑他们的位置 //div
. 选取当前节点 ./div
.. 选取当前节点的父节点 ..
@ 选取属性 //@calss
  • 示例
语法 说明
artical 选取所有artical元素的子节点
/artical 选取根元素artical
./artical 选取当前元素下的artical
../artical 选取父元素下的artical
artical/a 选取所有属于artical的子元素a元素
//div 选取所有div 子元素,无论div在任何地方
artical//div 选取所有属于artical的div 元素,无论div元素在artical的任何位置
//@class 选取所有class属性
a/@href 选取a标签的href属性
a/text() 选取a标签下的文本
string(.) 解析出当前节点下所有文字
string(..) 解析出父节点下所有文字

【3】谓语

  • 谓语被嵌在方括号内,用来查找某个特定的节点或包含某个制定的值的节点
语法 说明
/artical/div[1] 选取所有属于artical 子元素的第一个div元素
/artical/div[last()] 选取所有属于artical子元素的最后一个div元素
/artical/div[last()-1] 选取所有属于artical子元素的倒数第2个div元素
/artical/div[position()❤️] 选取所有属于artical子元素的前2个div元素
//div[@class] 选取所有拥有属性为class的div节点
//div[@class=”main”] 选取所有div下class属性为main的div节点
//div[price>3.5] 选取所有div下元素值price大于3.5的div节点

【4】通配符

  • Xpath通过通配符来选取未知的XML元素
表达式 结果
//* 选取所有元素
//div/* 选取所有属于div元素的所有子节点
//div[@*] 选取所有带属性的元素

【5】 取多个路径

  • 使用 | 运算符可以选取多个路径
表达式 结果
//div | //table 选取文档中所有的div和table节点
//div/a | //div/p 选取所有div元素的a和p 元素
artical/div/pl | //span 选取所有div下的pl和文档中所有span

【6】轴

  • 轴可以定义相对于当前节点的节点集
轴名称 表达式 描述
ancestor ./ancestor:😗 选取当前节点的所有先辈节点(父、祖父)
ancestor-or-self ./ancestor-or-self:😗 选取当前节点的所有先辈节点以及节点本身
descendant ./descendant:😗 返回当前节点的所有后代节点(子节点、孙节点)
child ./child:😗 返回当前节点的所有子节点
parent ./parent:😗 选取当前节点的父节点
following ./following:😗 选取文档中当前节点结束标签后的所有节点
following-sibling ./following-sibling:😗 选取当前节点之后的兄弟节点
preceding ./preceding:😗 选取文档中当前节点开始标签前的所有节点
preceding-sibling ./preceding-sibling:😗 选取当前节点之前的兄弟节点
self ./self:😗 选取当前节点
attribute ./attribute:😗 选取当前节点的所有属性

【五】获取数据

  • 在Scrapy框架中,虽然.extract_first()和.extract()并非直接属于Scrapy API的一部分,但在处理解析响应内容时,它们通常与Scrapy选择器(Selectors)一起配合使用来提取数据。
  • Scrapy基于XPath或CSS选择器选取网页元素,并将其转换为可操作的对象。

【1】.extract_first()

  • 在Scrapy选择器返回的SelectorList对象上调用此方法时,将提取匹配该选择器的第一个HTML/XML元素的内容。
  • 如果找到匹配项,则返回该元素的字符串形式;否则返回None。
  • 例如
response.css('div.title').extract_first()

【2】.extract()

  • 当调用此方法时,它将提取所有匹配该选择器的HTML/XML元素的内容,返回一个包含所有匹配元素字符串形式的列表。
  • 这对于一次性获取多个相同结构的数据非常有用。
all_titles = response.css('div.title').extract()

【六】案例

【1】用css选择器语法 拿百度热搜上的小说信息

  • baidu.py
import scrapy


class BaiduSpider(scrapy.Spider):
    name = "baidu"
    # 域名 和 start_urls 地址中的域名要统一
    allowed_domains = ["top.baidu.com"]
    # # 目标网址
    start_urls = ["https://top.baidu.com/board?tab=novel"]

    def parse(self, response):
        # 【一】css选择器语法
        div_list = response.css(
            '#sanRoot > main > div.container.right-container_2EFJr > div > div:nth-child(2) > div')

        # 遍历循环获取每一个数据

        novel_list = []
        for div in div_list:
            # #sanRoot > main > div.container.right-container_2EFJr > div > div:nth-child(2) > div:nth-child(30) > a.img-wrapper_29V76 > img
            index_img_url = div.css("div > a > img::attr(src)").extract_first()

            # 获取小说名字
            #  #sanRoot > main > div.container.right-container_2EFJr > div > div:nth-child(2) > div:nth-child(1) > div > a > div
            #  div > a > div
            # 提取文本信息 ::text
            # 提取标签中的属性 ::attr(属性名)
            noval_detail_div = div.css("div > div")
            detail_url = noval_detail_div.css("a::attr(href)").extract_first()
            novel_name = noval_detail_div.css("a > div::text").extract_first().strip()

            # #sanRoot > main > div.container.right-container_2EFJr > div > div:nth-child(2) > div:nth-child(1) > div > div:nth-child(2)
            novel_author = div.css('div > div > div:nth-child(2)::text').extract_first().strip()
            novel_type = div.css('div > div > div:nth-child(3)::text').extract_first().strip()
            novel_desc = div.css('div > div > div:nth-child(4)::text').extract_first().strip()
            novel_list.append({
                "novel_name": novel_name,
                "index_img_url": index_img_url,
                "novel_author": novel_author,
                "novel_type": novel_type,
                "novel_desc": novel_desc,
                "detail_url": detail_url,
            })

        print(novel_list)
  • settings.py
BOT_NAME = "SpiderNewsProjects"

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

# 可以控制日志级别
LOG_LEVEL = "WARNING"



# Obey robots.txt rules

# 遵循  机器人协议
ROBOTSTXT_OBEY = False



# 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"

  • start.py
from scrapy.cmdline import execute


execute(['scrapy', 'crawl', 'baidu', "--nolog"])

【2】xpath 语法 拿百度热搜上的小说信息

  • baidu.py
import scrapy


class BaiduSpider(scrapy.Spider):
    name = "baidu"
    # 域名 和 start_urls 地址中的域名要统一
    allowed_domains = ["top.baidu.com"]
    # # 目标网址
    start_urls = ["https://top.baidu.com/board?tab=novel"]

    def parse(self, response):
        # //*[@id="sanRoot"]/main/div[2]/div/div[2] 全部
        # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]  全部里面的一个
        div_list = response.xpath('//*[@id="sanRoot"]/main/div[2]/div/div[2]/div')

        div_dict = []
        for div in div_list:
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/a[1]/img
            # ./a[1]/img
            index_img_url = div.xpath('./a[1]/img/@src').extract_first()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a/div
            # ./div/a/div
            novel_name = div.xpath('./div/a/div/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[1]
            # ./div/div[1]
            novel_author = div.xpath('./div/div[1]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[2]
            # ./div/div[2]
            novel_type = div.xpath('./div/div[2]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[3]
            # ./div/div[3]
            novel_desc = div.xpath('./div/div[3]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a
            # ./div/a
            detail_url = div.xpath('./div/a/@href').extract_first().strip()
            div_dict.append({
                "novel_name": novel_name,
                "index_img_url": index_img_url,
                "novel_author": novel_author,
                "novel_type": novel_type,
                "novel_desc": novel_desc,
                "detail_url": detail_url,
            })

        print(div_dict)

【七】配置文件参数解析

  • settings.py
# 当前项目名字
BOT_NAME = "SpiderNewsProjects"

# 当前爬虫项目所在位置
SPIDER_MODULES = ["SpiderNewsProjects.spiders"]
NEWSPIDER_MODULE = "SpiderNewsProjects.spiders"

# 可以控制日志级别
# LOG_LEVEL = "WARNING"


# from fake_useragent import UserAgent
# # 自定义请求头
# USER_AGENT = UserAgent.random


# 是否遵循爬虫协议,一般都设为False
ROBOTSTXT_OBEY = False

# 最大请求数量
# CONCURRENT_REQUESTS = 32

# 下载延迟 下载3s还没有响应  会直接抛出异常
# DOWNLOAD_DELAY = 3


# 并发请求的最大数量
# CONCURRENT_REQUESTS_PER_DOMAIN = 16
# CONCURRENT_REQUESTS_PER_IP = 16

# 是否启用Cookies
# COOKIES_ENABLED = False

# 禁用控制台输出
# TELNETCONSOLE_ENABLED = False

# 默认请求头参数
# DEFAULT_REQUEST_HEADERS = {
#    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
#    "Accept-Language": "en",
# }

# 是否启用爬虫中间件
# SPIDER_MIDDLEWARES = {
#    "SpiderNewsProjects.middlewares.SpidernewsprojectsSpiderMiddleware": 543,
# }

# 是否启用下载中间件
# DOWNLOADER_MIDDLEWARES = {
#    "SpiderNewsProjects.middlewares.SpidernewsprojectsDownloaderMiddleware": 543,
# }

# 调度引擎
# EXTENSIONS = {
#    "scrapy.extensions.telnet.TelnetConsole": None,
# }

# 管道类    ITEM_PIPELINES 持久化配置
# ITEM_PIPELINES = {
#    "SpiderNewsProjects.pipelines.SpidernewsprojectsPipeline": 300,
# }

# 自动限速
# AUTOTHROTTLE_ENABLED = True
# 下载延迟
# AUTOTHROTTLE_START_DELAY = 5
# 最大并发数延迟
# AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
# HTTPCACHE_ENABLED = True
# HTTPCACHE_EXPIRATION_SECS = 0
# HTTPCACHE_DIR = "httpcache"
# HTTPCACHE_IGNORE_HTTP_CODES = []
# HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"

# 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"

Ⅶ 持久化存储到本地

【一】持久化存储方案

  • 在利用Scrapy框架进行网络数据抓取时,为了能够有效地管理和复用所获取的数据,我们可以选择将其持久化存储到本地磁盘。
  • Scrapy提供了一种便捷的方式来将数据解析(parse)得到的结果直接保存为JSON或CSV等常见格式。

【1】Scrapy数据持久化

  • Scrapy内建了Item Pipeline机制,可以通过编写自定义Pipeline实现数据的持久化存储。
  • 在Pipeline中可以调用内置方法将Item对象序列化为JSON或CSV格式,并写入文件。

【2】parse函数返回值

  • 每个爬虫中的parse方法以及其他后续解析方法通常需要返回一个可迭代对象(如列表),其中包含的是经过处理后的数据结构,通常是符合Item定义的字典。
def parse(self, response):
    items = []
    for element in response.css('some_selector'):
        item = {
            'field1': element.xpath('...').get(),
            'field2': element.xpath('...').get(),
        }
        items.append(item)
    return items

【3】命令行工具

  • Scrapy提供了命令行工具来执行数据的导出操作,无需修改代码即可完成。
  • 例如,可以使用以下命令将结果保存为JSON或CSV:

(1)JSON格式

scrapy crawl your_spider_name -o output.json

(2)CSV格式

scrapy crawl your_spider_name -o output.csv -t csv
  • 这些命令会在项目运行结束后,将爬取到的数据自动按照指定格式保存到本地文件中。
  • 这样不仅便于后期数据分析和处理,也方便与其他系统进行数据集成。

【4】总结

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

  • 优点
    • 简单
    • 便捷
  • 缺点
    • 局限性强
    • 只可以将数据存储到文本文件无法写入数据库
    • 存储数据的后缀必须按照指定格式,通常是 .csv
    • 需要将存储的数据封装到 parse 方法的返回值中

(2)基于管道的持久化存储

  • 优点
    • 极大程度的提升数据存储的效率
  • 缺点
    • 编码流程较多

【二】案例之百度热搜上的小说信息

【1】方式一:使用命令行保存当前数据

(1)保存成本地的 json 文件

import scrapy


class BaiduSpider(scrapy.Spider):
    name = "baidu"
    # 域名 和 start_urls 地址中的域名要统一
    allowed_domains = ["top.baidu.com"]
    # # 目标网址
    start_urls = ["https://top.baidu.com/board?tab=novel"]

    def parse(self, response):
        # //*[@id="sanRoot"]/main/div[2]/div/div[2] 全部
        # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]  全部里面的一个
        div_list = response.xpath('//*[@id="sanRoot"]/main/div[2]/div/div[2]/div')

        novel_data = {}
        for div in div_list:
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/a[1]/img
            # ./a[1]/img
            index_img_url = div.xpath('./a[1]/img/@src').extract_first()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a/div
            # ./div/a/div
            novel_name = div.xpath('./div/a/div/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[1]
            # ./div/div[1]
            novel_author = div.xpath('./div/div[1]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[2]
            # ./div/div[2]
            novel_type = div.xpath('./div/div[2]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[3]
            # ./div/div[3]
            novel_desc = div.xpath('./div/div[3]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a
            # ./div/a
            detail_url = div.xpath('./div/a/@href').extract_first().strip()
            novel_data[novel_name] ={
                "novel_name": novel_name,
                "index_img_url": index_img_url,
                "novel_author": novel_author,
                "novel_type": novel_type,
                "novel_desc": novel_desc,
                "detail_url": detail_url,
            }
        # 不要忘记返回数据
        return novel_data
        # 持久化存储数据   
        # 【方式一】:使用命令行保存当前数据   在终端输入  scrapy crawl baidu -o baidu.json
        # 保存成本地的 json 文件
        # scrapy crawl your_spider_name -o output.json
        # scrapy crawl baidu -o baidu.json

(2)保存成本地的csv文件

import scrapy


class BaiduSpider(scrapy.Spider):
    name = "baidu"
    # 域名 和 start_urls 地址中的域名要统一
    allowed_domains = ["top.baidu.com"]
    # # 目标网址
    start_urls = ["https://top.baidu.com/board?tab=novel"]

    def parse(self, response):
        # //*[@id="sanRoot"]/main/div[2]/div/div[2] 全部
        # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]  全部里面的一个
        div_list = response.xpath('//*[@id="sanRoot"]/main/div[2]/div/div[2]/div')

        novel_data = {}
        for div in div_list:
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/a[1]/img
            # ./a[1]/img
            index_img_url = div.xpath('./a[1]/img/@src').extract_first()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a/div
            # ./div/a/div
            novel_name = div.xpath('./div/a/div/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[1]
            # ./div/div[1]
            novel_author = div.xpath('./div/div[1]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[2]
            # ./div/div[2]
            novel_type = div.xpath('./div/div[2]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[3]
            # ./div/div[3]
            novel_desc = div.xpath('./div/div[3]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a
            # ./div/a
            detail_url = div.xpath('./div/a/@href').extract_first().strip()
            novel_data[novel_name] ={
                "novel_name": novel_name,
                "index_img_url": index_img_url,
                "novel_author": novel_author,
                "novel_type": novel_type,
                "novel_desc": novel_desc,
                "detail_url": detail_url,
            }
        # 不要忘记返回数据
        return novel_data
        # 【持久化存储数据】
        # 【方式一】:使用命令行保存当前数据  在终端输入  scrapy crawl baidu -o baidu.csv
        
        # 保存成本地的csv文件
        # scrapy crawl your_spider_name -o output.csv
        # scrapy crawl baidu -o baidu.csv

【三】Pipline存储(管道)

【1】管道介绍

  • process_item(self,item,spider):
    • 管道类中必须有的函数
    • 实现对item数据的处理
    • 必须return item
  • open_spider(self, spider): 在爬虫开启的时候仅执行一次
  • close_spider(self, spider): 在爬虫关闭的时候仅执行一次

【2】第一步:创建管道数据模型

  • 在item.py中写一个类
import scrapy

class BlogItem(scrapy.Item):
    # 创建管道字典:类似于Django中的模型表

    # 当前文章的文章标题
    article_title = scrapy.Field()
    # 当前文章的详情链接
    article_link = scrapy.Field()
    # 当前文章的简介
    article_desc = scrapy.Field()
    # 当前文章作者的头像链接
    article_author_avatar_url = scrapy.Field()
    # 补充一个字典 : 我们虽然没有爬取文章详情,但是不影响字段存在
    article_content = scrapy.Field()

    # 当前文章的作者名字
    article_author_name = scrapy.Field()
    # 当前作者的详情链接
    article_author_url = scrapy.Field()
    # 当前文章的发布日期
    article_publish_date = scrapy.Field()
    # 当前文章的点赞数
    article_up_num = scrapy.Field()
    # 当前文章的评论数
    article_comment_num = scrapy.Field()
    # 当前文章的阅读数
    article_read_num = scrapy.Field()

【3】第二步:定义管道数据处理类

(1)本地存储

  • 在pipline.py中创建一个类,并重写下述方法
    • open_spide:开启爬虫会触发
    • close_spider:爬完会触发
    • process_item:每次要保存一个对象会触发
class BlogLocalFilePipeline:
    # 启动爬虫项目会触发
    def open_spider(self, spider):
        # 打开文件对象存储到对象中
        self.fp = open('article_data.txt', 'w', encoding='utf-8')

    # 关闭爬虫项目会触发
    def close_spider(self, spider):
        self.fp.close()

    # 这个很重要
    # 管道中转站,所有管道数据都必须经过的方法
    def process_item(self, item, spider):
        # 持久化存储到本地 txt 文件中
        self.fp.write(item['article_title'] + '\n')
        return item

(2)数据库存储

  • 数据库建表和建库

用PDManer生成MYSQL操作代码

  • MYSQL操作代码

  • 在mysql数据库 创建一个baidu数据库

C:\WINDOWS\system32>mysql -uroot -p
Enter password: ******
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.44 MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases
    -> ;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| bbs                |
| bms                |
| django001          |
| library            |
| mysql              |
| performance_schema |
| six                |
| study              |
| study001           |
| study003           |
| study888           |
| sys                |
| user_book          |
+--------------------+
14 rows in set (0.28 sec)

mysql> create database baidu;
Query OK, 1 row affected (0.20 sec)

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| baidu              |
| bbs                |
| bms                |
| django001          |
| library            |
| mysql              |
| performance_schema |
| six                |
| study              |
| study001           |
| study003           |
| study888           |
| sys                |
| user_book          |
+--------------------+
15 rows in set (0.00 sec)

mysql> use baidu;
Database changed

(3)数据库中建表

'''
DROP TABLE IF EXISTS novel;
CREATE TABLE novel(
    `id` INT NOT NULL AUTO_INCREMENT  COMMENT '主键ID' ,
    `novel_name` VARCHAR(255)    COMMENT '小说名称' ,
    `index_img_url` VARCHAR(2555)    COMMENT '封面图片链接' ,
    `novel_author` VARCHAR(255)    COMMENT '小说作者' ,
    `novel_type` VARCHAR(255)    COMMENT '小说类型' ,
    `novel_desc` TEXT    COMMENT '小说简介' ,
    `detail_url` VARCHAR(2555)    COMMENT '详情链接' ,
    PRIMARY KEY (id)
)  COMMENT = '热搜小说表';

'''

mysql> use baidu;
Database changed
mysql> CREATE TABLE novel(
    ->     `id` INT NOT NULL AUTO_INCREMENT  COMMENT '主键ID' ,
    ->     `novel_name` VARCHAR(255)    COMMENT '小说名称' ,
    ->     `index_img_url` VARCHAR(2555)    COMMENT '封面图片链接' ,
    ->     `novel_author` VARCHAR(255)    COMMENT '小说作者' ,
    ->     `novel_type` VARCHAR(255)    COMMENT '小说类型' ,
    ->     `novel_desc` TEXT    COMMENT '小说简介' ,
    ->     `detail_url` VARCHAR(2555)    COMMENT '详情链接' ,
    ->     PRIMARY KEY (id)
    -> )  COMMENT = '热搜小说表';
Query OK, 0 rows affected (2.36 sec)
  • 配置文件
import pymysql


class BlogCloudFilePipeline:
    # 启动爬虫项目会触发
    def open_spider(self, spider):
        # 定义起始调用次数
        self.count = 0
        # 创建mysql链接对象
        self.conn = pymysql.connect(
            user="root",
            password="1314521",
            host="127.0.0.1",
            port=3306,
            database='cnblogs'
        )

        # 创建句柄
        self.cursor = self.conn.cursor()

    # 关闭爬虫项目会触发
    def close_spider(self, spider):
        # 关闭句柄
        self.cursor.close()

        # 关闭mysql数据库链接
        self.conn.close()

    # 这个很重要
    # 管道中转站,所有管道数据都必须经过的方法
    def process_item(self, item, spider):
        # 调用次数 +1
        self.count += 1
        # 打印调用次数
        print(self.count)
        # 定义插入数据的SQL语句
        # sql = "INSERT INTO article(title, link, desc, content, author_avatar_url, author_name, author_url, publish_date, up_num, comment_num, read_num) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
        sql = "INSERT INTO article(title, link) VALUES ( %s, %s)"
        # 从管道中获取数据并拼接SQL语句,使用get,如果取不到是 None 不会报错
        # self.cursor.execute(sql,
        #                     args=(
        #                         item.get('article_title'),
        #                         item.get('article_link'),
        #                         item.get('article_desc'),
        #                         item.get('article_content'),
        #                         item.get('article_author_avatar_url'),
        #                         item.get('article_author_name'),
        #                         item.get('article_author_url'),
        #                         item.get('article_publish_date'),
        #                         item.get('article_up_num'),
        #                         item.get('article_comment_num'),
        #                         item.get('article_read_num')
        #                     ))
        self.cursor.execute(sql,
                            args=(
                                item.get('article_title'),
                                item.get('article_link'),
                            ))
        # 每次存储数据都提交数据,防止数据丢失
        self.conn.commit()
        # 必须将 item 对象返回,下次调用
        return item

(4)第三步:配置文件注册管道类

  • 配置文件 settings.py 配置
# 管道类
ITEM_PIPELINES = {
    # 项目自带的默认管道类
    # "SpiderFirst.pipelines.SpiderfirstPipeline": 300,
    # 注册自己写的管道类 : 保存数据到本地,数字越小,优先级越高,依次执行
    "SpiderFirst.pipelines.BlogLocalFilePipeline": 300,
    # 注册自己写的管道类 : 保存数据到数据库,数字越小,优先级越高,依次执行
    "SpiderFirst.pipelines.BlogCloudFilePipeline": 301,
}

(5)第四步:处理数据

  • 在解析方法parse中yield item对象
import scrapy
# 从 items.py 中导入创建好的管道类模型表
from ..items import BlogItem


# 自动创建以当前文件名为名的爬虫类
class CnblogSpider(scrapy.Spider):
    # 爬虫程序的唯一标识
    name = "cnblog"
    # 允许访问的域名
    allowed_domains = ["www.123.com"]
    # 起始 URL
    start_urls = ["https://www.cnblogs.com/"]

    # 解析函数
    def parse(self, response):
        # 构建数据列表,存储文章数据
        article_data_list = []
        # 创建 管道 对象 (item对象)
        item = BlogItem()

        # 获取所有的文章对象
        article_section_list = response.xpath('//*[@id="post_list"]/article/section')

        # 遍历获取每一篇文章的数据
        for article_section in article_section_list:
            # 获取当前文章信息
            article_info = article_section.xpath('./div')
            # 获取当前文章的文章标题
            article_title = article_info.xpath('./a/text()').extract_first()
            # 获取当前文章的详情链接
            article_link = article_info.xpath('./a/@href').extract_first()
            # 获取当前文章的简介
            try:
                article_desc = article_info.xpath('./p/text()').extract()[1].strip()
            except:
                article_desc = ""
            # 获取当前文章作者的头像链接
            article_author_avatar_url = article_info.xpath('./p/a/img/@src').extract_first()

            # 获取当前作者信息
            article_author_info = article_section.xpath('./footer')
            # 获取当前文章的作者名字
            article_author_name = article_author_info.xpath('./a[1]/span/text()').extract_first()
            # 获取当前作者的详情链接
            article_author_url = article_author_info.xpath('./a[1]/@href').extract_first()
            # 获取当前文章的发布日期
            article_publish_date = article_author_info.xpath('./span[1]/span/text()').extract_first()
            # 获取当前文章的点赞数
            article_up_num = article_author_info.xpath('./a[2]/span/text()').extract_first()
            # 获取当前文章的评论数
            article_comment_num = article_author_info.xpath('./a[3]/span/text()').extract_first()
            # 获取当前文章的阅读数
            article_read_num = article_author_info.xpath('./a[4]/span/text()').extract_first()

            # 构造当前文章的字典信息
            data = {
                "article_title": article_title,
                "article_link": article_link,
                "article_desc": article_desc,
                "article_author_name": article_author_name,
                "article_author_avatar_url": article_author_avatar_url,
                "article_author_url": article_author_url,
                "article_publish_date": article_publish_date,
                "article_up_num": article_up_num,
                "article_comment_num": article_comment_num,
                "article_read_num": article_read_num,
                "article_content": ""
            }
            # print(data)
            # 向管道中提交数据 : 注意这里是抓到一条提交一条,也可以全部抓取后批量处理
            item.update(data)
            # 不要忘了提价管道,否则管道数据不生效!
            yield item
        # 全部抓取后更新数据
        # item.update(*article_data_list)
        # yield item

        # 将当前文章的字典信息添加到列表中
        # article_data_list.append(data)
        # return article_data_list

(6)启动项目

  • 存储本地文件时,可以指定存储路径
[1]方式一:命令行启动
scrapy crawl cnblog
[2]方式二:启动文件启动
from scrapy.cmdline import execute

execute(['scrapy', 'crawl', 'cnblog', "--nolog"])

【4】案例之百度热搜上的小说信息永久储存

  • baidu.py
import scrapy

from SpiderNewsProjects.items import BaiduItem


class BaiduSpider(scrapy.Spider):
    name = "baidu"
    # 域名 和 start_urls 地址中的域名要统一
    allowed_domains = ["top.baidu.com"]
    # # 目标网址
    start_urls = ["https://top.baidu.com/board?tab=novel"]

    def parse(self, response):
        # //*[@id="sanRoot"]/main/div[2]/div/div[2] 全部
        # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]  全部里面的一个
        div_list = response.xpath('//*[@id="sanRoot"]/main/div[2]/div/div[2]/div')

        # 创建一个管道类对象
        item = BaiduItem()


        novel_data = {}
        for div in div_list:
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/a[1]/img
            # ./a[1]/img
            index_img_url = div.xpath('./a[1]/img/@src').extract_first()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a/div
            # ./div/a/div
            novel_name = div.xpath('./div/a/div/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[1]
            # ./div/div[1]
            novel_author = div.xpath('./div/div[1]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[2]
            # ./div/div[2]
            novel_type = div.xpath('./div/div[2]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/div[3]
            # ./div/div[3]
            novel_desc = div.xpath('./div/div[3]/text()').extract_first().strip()
            # //*[@id="sanRoot"]/main/div[2]/div/div[2]/div[1]/div/a
            # ./div/a
            detail_url = div.xpath('./div/a/@href').extract_first().strip()
            novel_data[novel_name] = {
                "novel_name": novel_name,
                "index_img_url": index_img_url,
                "novel_author": novel_author,
                "novel_type": novel_type,
                "novel_desc": novel_desc,
                "detail_url": detail_url,
            }

            # item管道是一个字典 ---> 字典.update() 更新的前提是 你的所有字段要和你 管道类里面的字段一模一样
            # novel_data = {"novel_name":{"novel_name":novel_name}}
            item.update({
                "novel_name": novel_name,
                "index_img_url": index_img_url,
                "novel_author": novel_author,
                "novel_type": novel_type,
                "novel_desc": novel_desc,
                "detail_url": detail_url,
            })
            # 将 item 对象返回出去 但是 return 有结束所有程序的效果
            # yield 可以保存 item 的状态
            yield item


        # 【方式二】持久化存储到数据库
        # 管道
        #  process_item(self,item,spider):
        #    管道类中必须有的函数
        #    实现对item数据的处理
        #    必须return item
        #  open_spider(self, spider): 在爬虫开启的时候仅执行一次
        #  close_spider(self, spider): 在爬虫关闭的时候仅执行一次


# 【1】创建一个管道类
'''
# 创建管道类 ---> 相当于 在Django中创建模型表
 class BaiduItem(scrapy.Item):
     # 管道类字段 起始就相当于你的 模型类中的字段
     novel_name = scrapy.Field()
     index_img_url = scrapy.Field()
     novel_author = scrapy.Field()
     novel_type = scrapy.Field()
     novel_desc = scrapy.Field()
     detail_url = scrapy.Field()
'''
# 【2】定义管道类处理函数 在 pipelines.py 里面写处理函数
'''
class BaiduSpiderPipeline:
    def process_item(self, item, spider):
        print(item)
        return item
'''
# 【3】在配置文件中注册处理类
'''
ITEM_PIPELINES = {
    # 项目默认的处理类,但是没有任何操作
    "SpiderNew.pipelines.SpidernewPipeline": 300,
    # 自己写的注册的处理类,后面的数字越小优先级就越高 , 根据优先级一次执行 管道处理类
    "SpiderNew.pipelines.BaiduSpiderPipeline": 300,
}
'''
# 【4】在你的爬虫函数中向管道中提交数据

  • items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


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


# 创建管道类 ---> 相当于 在Django中创建模型表
class BaiduItem(scrapy.Item):
    # 管道类字段 起始就相当于你的 模型类中的字段
    novel_name = scrapy.Field()
    index_img_url = scrapy.Field()
    novel_author = scrapy.Field()
    novel_type = scrapy.Field()
    novel_desc = scrapy.Field()
    detail_url = scrapy.Field()

  • pipelines.py
import pymysql
from pymysql.cursors import DictCursor


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


class BaiduSpiderPipeline:
    # 爬虫程序开始的时候会触发
    def open_spider(self, spider):
        # 做数据库的初始化操作
        print('爬虫开始 , 初始化数据库连接 ... ')
        self.conn = pymysql.connect(
            user="root",
            password="123456",
            host="127.0.0.1",
            port=3306,
            database="baidu",
            charset='utf8mb4',
            cursorclass=DictCursor
        )
        self.cursor = self.conn.cursor()
        print('爬虫开始 , 初始化数据库连接成功 ... ')

    # 当爬虫程序结束的时候会触发
    def close_spider(self, spider):
        # 可以将数据库链接关闭
        print('爬虫结束 关闭数据库 ')
        self.cursor.close()
        self.conn.close()

    def process_item(self, item, spider):
        # print(item)
        # 获取数据
        novel_name = item.get("novel_name")
        index_img_url = item.get("index_img_url")
        novel_author = item.get("novel_author")
        novel_type = item.get("novel_type")
        novel_desc = item.get("novel_desc")
        detail_url = item.get("detail_url")
        # 插入数据
        sql = "insert into novel(novel_name,index_img_url,novel_author,novel_type,novel_desc,detail_url) values(%s,%s,%s,%s,%s,%s)"

        # 执行SQL代码
        self.cursor.execute(sql, [novel_name, index_img_url, novel_author, novel_type, novel_desc, detail_url])

        # 提交事务
        self.conn.commit()

        print(f"当前数据 :>>>> {novel_name} 插入成功!")

        # 每次用完 item 要将 item 送回去
        return item


"""
DROP TABLE IF EXISTS novel;
CREATE TABLE novel(
    `id` INT NOT NULL AUTO_INCREMENT  COMMENT '主键ID' ,
    `novel_name` VARCHAR(255)    COMMENT '小说名称' ,
    `index_img_url` VARCHAR(2555)    COMMENT '封面图片链接' ,
    `novel_author` VARCHAR(255)    COMMENT '小说作者' ,
    `novel_type` VARCHAR(255)    COMMENT '小说类型' ,
    `novel_desc` TEXT    COMMENT '小说简介' ,
    `detail_url` VARCHAR(2555)    COMMENT '详情链接' ,
    PRIMARY KEY (id)
)  COMMENT = '热搜小说表';

"""

'''
mysql> create database baidu;
Query OK, 1 row affected (0.01 sec)

mysql> use baidu;
Database changed
mysql> CREATE TABLE novel(
    ->     `id` INT NOT NULL AUTO_INCREMENT  COMMENT 'ID' ,
    ->     `novel_name` VARCHAR(255)    COMMENT '' ,
    ->     `index_img_url` VARCHAR(2555)    COMMENT '' ,
    ->     `novel_author` VARCHAR(255)    COMMENT '' ,
    ->     `novel_type` VARCHAR(255)    COMMENT '' ,
    ->     `novel_desc` TEXT    COMMENT '' ,
    ->     `detail_url` VARCHAR(2555)    COMMENT '' ,
    ->     PRIMARY KEY (id)
    -> )  COMMENT = '';
Query OK, 0 rows affected (0.03 sec)
'''

  • settings.py
# 当前项目名字
BOT_NAME = "SpiderNewsProjects"

# 当前爬虫项目所在位置
SPIDER_MODULES = ["SpiderNewsProjects.spiders"]
NEWSPIDER_MODULE = "SpiderNewsProjects.spiders"

# 可以控制日志级别
# LOG_LEVEL = "WARNING"


# from fake_useragent import UserAgent
# # 自定义请求头
# USER_AGENT = UserAgent.random


# 遵循  机器人协议
ROBOTSTXT_OBEY = False

# 最大请求数量
# CONCURRENT_REQUESTS = 32

# 下载延迟 下载3s还没有响应  会直接抛出异常
# DOWNLOAD_DELAY = 3


# 并发请求的最大数量
# CONCURRENT_REQUESTS_PER_DOMAIN = 16
# CONCURRENT_REQUESTS_PER_IP = 16

# 是否启用Cookies
# COOKIES_ENABLED = False

# 禁用控制台输出
# TELNETCONSOLE_ENABLED = False

# 默认请求头参数
# DEFAULT_REQUEST_HEADERS = {
#    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
#    "Accept-Language": "en",
# }

# 是否启用爬虫中间件
# SPIDER_MIDDLEWARES = {
#    "SpiderNewsProjects.middlewares.SpidernewsprojectsSpiderMiddleware": 543,
# }

# 是否启用下载中间件
# DOWNLOADER_MIDDLEWARES = {
#    "SpiderNewsProjects.middlewares.SpidernewsprojectsDownloaderMiddleware": 543,
# }

# 调度引擎
# EXTENSIONS = {
#    "scrapy.extensions.telnet.TelnetConsole": None,
# }

# 管道类
ITEM_PIPELINES = {
    # 项目默认的处理类 但是没有任何操作
   # "SpiderNewsProjects.pipelines.SpidernewsprojectsPipeline": 300,

    # 自己写的注册的处理类  后面的数字越小优先级就越高 根据优先级 依次处理 管道处理类
   "SpiderNewsProjects.pipelines.BaiduSpiderPipeline": 300,
}

# 自动限速
# AUTOTHROTTLE_ENABLED = True
# 下载延迟
# AUTOTHROTTLE_START_DELAY = 5
# 最大并发数延迟
# AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
# HTTPCACHE_ENABLED = True
# HTTPCACHE_EXPIRATION_SECS = 0
# HTTPCACHE_DIR = "httpcache"
# HTTPCACHE_IGNORE_HTTP_CODES = []
# HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"

# 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"

posted on 2024-07-22 21:03  silence^  阅读(496)  评论(0编辑  收藏  举报

导航