登陆京东(滑块验证),验证码识别,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】使用
- http://www.ttshitu.com/docs/python.html#pageTitle
- 查看开发文档按照开发文档使用即可
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
- 下载并安装 pywin32 可以从其官网下载最适合您的版本:https://sourceforge.net/projects/pywin32/files/pywin32/
- 请根据您的操作系统和Python版本选择正确的安装文件进行下载。
- 下载完成后,按照安装向导进行安装。
【5】下载twisted的wheel文件
- 要安装 Scrapy,需要先下载 twisted 的 wheel 文件。
- 可以从官方网站下载 twisted 的 wheel 文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
- 请注意选择与您的Python环境相匹配的 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】官网链接
- 如果需要更详细的命令信息,可以参考 Scrapy 官方文档的命令行工具部分
- https://docs.scrapy.org/en/latest/topics/commands.html
【三】创建项目
【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"