Skip to main content

Scrapy学习

前言

之前用写 NodeJS写的爬虫感谢效率太低了,所以尝试一下这个爬虫框架,算是熟悉一下 Python 的项目结构

官方文档 中文文档

大佬发布的用来练习爬虫的网站 说明地址

概述

参考资料 迟暮有话说--Scrapy工作原理分析(简单易懂) 参考资料 爱你如--Scrapy框架原理及使用

Scrapy 框架执行流程

Scrapy框架主要由六大组件组成,它们分别是 调度器(Scheduler)、下载器(Downloader)、爬虫(Spider)、中间件(Middleware)、实体管道(Item Pipeline)和 Scrapy 引擎(Scrapy Engine)

image.png

1、Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。

2、Scheduler(调度器): 它负责接受引擎发送过来的 Request 请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。

3、Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有 Requests 请求,并将其获取到的Responses 交还给Scrapy Engine(引擎),由引擎交给 Spider 来处理,

4、Spider(爬虫): 它负责处理所有 Responses,从中分析提取数据,获取 Item 字段需要的数据,并将需要跟进的 URL 提交给引擎,再次进入Scheduler(调度器),

5、Item Pipeline(管道): Item Pipeline 负责处理被 spider 提取出来的 item。典型的处理有清理、 验证及持久化。

6、Downloader Middlewares(下载中间件): 可以自定义扩展下载功能的组件(代理、cokies 等)。

7、Spider Middlewares(Spider 中间件): 可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider 的 Responses;和从 Spider 出去的 Requests)

image.png

项目文件结构

spiders目录: 负责存放继承自 scrapy 的爬虫类。里面主要是用于分析 response 并提取返回的 item 或者是下一个 URL 信息,每个 Spider 负责处理特定的网站或一些网站。

__init__.py: 项目的初始化文件。

items.py: 负责数据模型的建立,类似于实体类。定义我们所要爬取的信息的相关属性。Item 对象是种容器,用来保存获取到的数据。

middlewares.py: 自己定义的中间件。可以定义相关的方法,用以处理爬虫的响应输入和请求输出。

(这个 pipelines 就是拿到处理的结果,可以将其保存)
pipelines.py: 负责对 spider 返回数据的处理。在 item 被 Spider 收集之后,就会将数据放入到 item pipelines中,在这个组件是一个独立的类,他们接收到item并通过它执行一些行为,同时也会决定 item 是否能留在 pipeline,或者被丢弃。

settings.py: 负责对整个爬虫的配置。提供了scrapy组件的方法,通过在此文件中的设置可以控制包括核心、插件、pipeline以及Spider组件。

scrapy.cfg: scrapy基础配置

配置环境

pip install scrapy

# 安装好之后,测试是否能运行
scrapy bench
# 创建项目
scrapy startproject 项目名

# 生成爬虫,爬虫名不能和项目一样(这个 gen 是 generate:产生 的意思)
scrapy genspider 爬虫名 被扒域名
# 例:scrapy genspider study01 ssr1.scrape.center

然后就会自动生成这个文件

# -*- coding:utf-8 -*-
import scrapy


# 创建爬虫类,并且继承 scrapy.Spider
class Study01Spider(scrapy.Spider):
name = 'study01' # 名字必须唯一
allowed_domains = ['ssr1.scrape.center'] # 允许采集的域名
start_urls = ['http://ssr1.scrape.center/'] # 开始采集的网站

# 解析响应数据
def parse(self, response):
pass

注意:这里 parse 父类方法会抛出错误,目的是防止用户自己写的 Spider 没有覆盖这个方法,当没有覆盖这个方法时会自动抛出这个错误

def parse(self, response, **kwargs):
raise NotImplementedError('{}.parse callback is not defined'.format(self.__class__.__name__))

然后进入 settings.py 里把 ROBOTSTXT_OBEY 改成 False(大写) 这个 ROBOTSTXT_OBEY 是一个反爬协议,开启了就表示遵守这个协议,哪些字段不能爬取 一般在网站根目录下的 robots.txt 文件 例如:

https://www.nike.com/robots.txt

Disallow 表示不可以爬
Allow 表示可以爬

启动爬虫

# 启动
scrapy crawl 爬虫名

设置日志

因为默认的日志打印一堆看着有点烦,实际上这个日志是可以关的

同样在 settings.py 里加上 LOG_LEVEL = "WARNING" 设置日志等级为警告及以上才显示

如果要使用日志的话可以引入日志模块

import logging


logging.info('测试 INFO')
logging.warning('测试 WARING')
logging.debug('测试 DEBUG')
logging.error('测试 ERROR')


# 或者在使用前加上调用者名字,日志就能显示调用的类名了
logger = logging.getLogger(__name__)

logger.warning('测试')

可以把日志输出到某个地方 在 settings.py 里加上 LOG_FILE = "./temp.log"

搭建一个服务

直接上来就去拿各种网站练手效率太低了,所以这里可以先搭建一个 NodeJS 服务

但是注意:如果要测试代理的话最好把这个部署到远程服务器上去

# 初始化项目
npm init

# 下载依赖
npm install express --save

编写服务

// 引入模块
const express = require('express')

// 创建网站服务器
const app = express();

// 如果是 / 表示这个是根项目(和javaWeb 一样)
app.get('/',(req,res)=>{
// 响应方法是 send() 能够自动把 响应类型、类型编码、http状态码 等信息写到响应头里
console.log('==================请求头====================');
console.log(req.headers);
console.log('==================响应头====================');
console.log(res.getHeaders());
console.log('==================IP地址====================');
let getClientIp = function (req) {
return req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress || '';
};

console.log(getClientIp(req));
res.send('Hello Express !');
})

// 监听端口
app.listen(3000);
console.log('网站服务器启动成功');
# 启动服务
node .\main.js

# 访问 http://localhost:3000/

编写脚本:

import scrapy


class TestspiderSpider(scrapy.Spider):
name = 'testSpider'
allowed_domains = ['localhost']
start_urls = ['http://localhost:3000/']

def parse(self, response):
print(response)


if __name__ == '__main__':
from scrapy import cmdline

cmdline.execute("scrapy crawl testSpider".split())

自带工具

Scrapy Shell

参考资料 Scrapy终端(Scrapy shell)

# 安装 ipython
pip install ipython
# Tab键 可补齐所需代码
# 方向键 可显示命令历史
# Ctrl + u 清除所在行所有内容
# Ctrl + l 清屏
# Ctrl + c 终止当前执行的代码
# Ctrl + shift+v 从剪切板粘贴文本
# Ctrl + f 前移一个字符
# Ctrl + b 后移一个字符

# 启动(如果安装了 `ipython` 这个终端,会默认跳转到那里)
# <url> 是要爬取的网页的地址
scrapy shell <url>

# 注:如果直接执行上面的命令会有很多无用的 log弹出来干扰,所以最好加上 --nolog
scrapy shell <url> --nolog

可用的快捷命令(shortcut)

shelp() - 打印可用对象及快捷命令的帮助列表 fetch(request_or_url) - 根据给定的请求(request)或 URL 获取一个新的 response,并更新相关的对象 view(response) - 在本机的浏览器打开给定的 response。 其会在 response 的 body 中添加一个 <base> tag ,使得外部链接(例如图片及css)能正确显示。 注意,该操作会在本地创建一个临时文件,且该文件不会被自动删除。

Scrapy 终端根据下载的页面会自动创建一些方便使用的对象

crawler - 当前 Crawler 对象. spider - 处理 URL 的 spider。 对当前 URL 没有处理的 Spider 时创建一个 Spider 对象。 request - 最近获取到的页面的 Request 对象。 可以使用 replace() 修改该 request。或使用 fetch 快捷方式来获取新的 request response - 包含最近获取到的页面的 Response 对象。 sel - 根据最近获取到的 Response 构建的 Selector 对象。 settings - 当前的 Scrapy settings

所以可以直接在这里调试,例如调试 xpath

response.xpath('//button[@class="btn-next"]/../@href') 

Setting 文件

参考资料 Settings 单纯就是一个配置文件,里面存放一些公共的变量

一般用全大写字母命名变量例如:SQL_HOST = '192.168.0.1'

如何想要读取某项自定的字段或者已有的字段

from TempSpider.settings import DEFAULT_REQUEST_HEADERS

def process_item(self, item, spider):
# 取得字段
header = DEFAULT_REQUEST_HEADERS()
# 或者直接在 self 里就能取得
self.settings['DEFAULT_REQUEST_HEADERS']
# 也可以使用 spider 取得
spider.settings.get('DEFAULT_REQUEST_HEADERS')

下面记录些常用的配置项

# 项目名    默认为 scrapybot
BOT_NAME

# 是否遵循 robots.txt 策略
ROBOTSTXT_OBEY

# Item Pipeline 同时处理(每个 response 的)item的最大值 默认: 100
CONCURRENT_ITEMS

# Scrapy downloader 并发请求的最大值 默认:16
# 提供了一个粗略的控制,无论如何不会有超过该数目的请求被并发下载
CONCURRENT_REQUESTS

# 对单个网站进行并发请求的最大值 默认:8
# 针对目标域名提供对并发请求数目的更进一步的限制
CONCURRENT_REQUESTS_PER_DOMAIN

# 默认请求头
DEFAULT_REQUEST_HEADERS

# 最大深度,如果为 0 表示不限制 默认:0
DEPTH_LIMIT

# 关键!! 下载器在下载同一个网站下一个页面前需要等待的时间
# 该选项可以用来限制爬取速度 DOWNLOAD_DELAY = 0.25 # 250 ms of delay
DOWNLOAD_DELAY

# 如果启用,当从相同的网站获取数据时,Scrapy将会等待一个随机的值 (0.5 到 1.5 之间的一个随机值 * DOWNLOAD_DELAY)
# 默认 True
# 若 DOWNLOAD_DELAY 为 0(默认值),该选项将不起作用
RANDOMIZE_DOWNLOAD_DELAY


# 超时时间 默认:180
DOWNLOAD_TIMEOUT

# 是否启用 cookies middleware 如果关闭,cookies 将不会发送给 web server 默认: True
COOKIES_ENABLED

# 开启后会将请求携带的 Cookie 打印到 DEBUG Log 上去 默认: False
COOKIES_DEBUG

# 用于检测过滤重复请求的类
# 默认:'scrapy.dupefilters.RFPDupeFilter'
DUPEFILTER_CLASS

# 下载中间件
DOWNLOADER_MIDDLEWARES

# 爬虫中间件
SPIDER_MIDDLEWARES



# 插件
EXTENSIONS

# 是否启用 Log 默认开启
LOG_ENABLED
# 日志使用的编码 默认:utf-8
LOG_ENCODING
# 日志输出的文件名 默认 None
LOG_FILE
# 日志输出格式 默认:'%Y-%m-%d %H:%M:%S'
LOG_FORMAT
# 日志日期格式 默认:'%Y-%m-%d %H:%M:%S'
LOG_DATEFORMAT
# 日志级别 默认:DEBUG
# CRITICAL、 ERROR、WARNING、INFO、DEBUG
LOG_LEVEL
# 如果为 True ,进程所有的标准输出(及错误)将会被重定向到 log 中。例如, 执行 print 'hello'
# 默认是:False
LOG_STDOUT


# 自动限速插件(算法根据以下规则调整下载延迟及并发数:) 默认关闭
# 中文 https://scrapy-chs.readthedocs.io/zh_CN/1.0/topics/autothrottle.html?highlight=autothrottle
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
# 设置是否启用
AUTOTHROTTLE_ENABLED
# 初始下载延迟(单位:秒) 默认: 5.0
AUTOTHROTTLE_START_DELAY
# 在高延迟情况下最大的下载延迟(单位秒)。默认 60
AUTOTHROTTLE_MAX_DELAY
# The average number of requests Scrapy should be sending in parallel to each remote server
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# 启用 AutoThrottle 调试(debug)模式,展示每个接收到的 response 默认:False
AUTOTHROTTLE_DEBUG




# 启用和配置 HTTP 缓存
# 中文:https://scrapy-chs.readthedocs.io/zh_CN/1.0/topics/downloader-middleware.html?highlight=httpcache_enabled#std:setting-HTTPCACHE_ENABLED
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
HTTPCACHE_ENABLED = True
# 缓存的 request 的超时时间,单位秒。
# 超过这个时间的缓存 request 将会被重新下载。如果为0,则缓存的 request 将永远不会超时
HTTPCACHE_EXPIRATION_SECS = 0
# 存储(底层的)HTTP 缓存的目录。如果为空,则 HTTP 缓存将会被关闭
HTTPCACHE_DIR = 'httpcache'
# 不缓存指定的 HTTP (code)的 request
HTTPCACHE_IGNORE_HTTP_CODES = []
# 实现缓存的类
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'



# 是一个扩展插件,通过 TELENET 可以监听到当前爬虫的一些状态,默认是 True 开启状态
# 默认端口为 6023
# 命令行输入:telnet localhost 6023 就可以检查和控制 Scrapy 运行过程
# 详情参考:https://www.osgeo.cn/scrapy/topics/extensions.html#topics-extensions-ref-telnetconsole
TELNETCONSOLE_ENABLED

Pipeline 管道

这个 Pipeline 就是用来处理引擎传过来的数据(item)的管道

生命周期

# 当 spider 被开启时,这个方法被调用。(只执行一次)
# 注意:这个 spider 参数可以用来创建对象,这样后面就能直接在 spider 爬虫里读取到这个参数了
def open_spider(self, spider):
# 可以在这里对爬虫的参数初始化或者添加参数
spider.hello = 'world' # 这样就能在爬虫执行时通过 self.hello 读取到这个属性
print("当前开启的爬虫为:", spider)


# 每个item pipeline 组件都需要调用该方法
def process_item(self, item, spider):
return item


# 当spider被关闭时,这个方法被调用 (只执行一次)
def close_spider(self, spider):
print("爬虫%s关闭!" % spider)

"""
注意:在进行文件操作时,如果在启动时打开,结束时关闭文件 这样的操作是有一定问题的
因为如果读取到一半文件报错时,就不会关闭文件,这样会使还在内存里的数据丢失
(要 close 之后才会写入数据,否则在大于设置的阈值之前都在内存里)
所以最好手动写入文件 f.flush()
"""

CrawlSpider

参考资料 CrawlSpider介绍

Scrapy 框架中分两类爬虫

Spider 类和 CrawlSpider 类。

CrawlSpider 是 Spider 的子类,Spider 类的设计原则是只爬取 start_url 列表中的网页,而 CrawlSpider 类定义了一些规则(rule)来提供跟进 link 的方便的机制,从爬取的网页中获取 link 并继续爬取的工作更适合。

CrawlSpider 是爬取那些具有一定规则网站的常用的爬虫,它基于 Spider 并有一些独特属性

生成 CrawlSpider 的命令

scrapy genspider -t crawl 名字 "网站域名"

参数的介绍

# 是 Rule 对象的集合,用于匹配目标网站的地址
# 注意:不同的 Rule 之间不存在先后顺序,所以就无法传递参数了,
# 如果需要先后顺序的可以继续使用 yield scrapy.Request(...)
# 无顺序的!!!!!!!!!
rules
# 在 rules 中包含一个或多个 Rule 对象
# 每个 Rule 对爬取网站的动作定义了特定的操作。
# 如果多个 rule 匹配了相同的链接,则根据规则在本集合中被定义的顺序,第一个会被使用
# 参数 link、callback、follow、process_links、process_request

# ========== Rule 主要参数介绍 =========

# 是一个 LinkExtractor 对象,用于定义需要提取的链接
link # 参数看下面 LinkExtractor

# 提取出 url 地址的 response 会交给 callback 处理
# 注意:当编写爬虫规则是,避免使用 parse 作为回调函数。由于 CrawlSpider 使用 parse 方法来实现其逻辑,如果覆盖了 parse方法,CrawlSpider将会运行失败
callback
# response 中提取的链接是否需要跟进
# 即:在不指定 callback 函数的请求下,如果 follow 为 True,满足该 rule 的 url 还会继续被请求
# 默认是 False
follow # 是一个布尔值(boolean)
# 指定该 Spider 中哪个的函数将会被调用,从 link 中获取到链接列表将会调用该函数。该方法主要用来处理 url
process_links # 就是对匹配到 url 再做一些调整

# 例如
rules = (
Rule(LinkExtractor(allow=('/tag/\w+/$',)),
follow=True, # 如果有指定回调函数,默认不跟进
callback='parse_item',
process_links='process_links',),

@staticmethod
def process_links(links): # 对提取到的链接进行处理
for link in links:
link.url = link.url + 'page/1/'
yield link

# 指定该 Spider 中哪个的函数将会被调用,该规则提取到每个 request 是都会调用该函数。(用来过滤 request)
process_request
# 使用例
rules = (
#规则解析器:可以将连接提取器提取到的所有连接表示的页面进行指定规则(回调函数)的解析
Rule(link, callback='parse_item', follow=True),
)

# ========== LinkExtractor 主要参数介绍 =========

# 满足括号中“正则表达式”的值会被提取,如果为空,则会全部匹配。
allow
# 与这个正则表达式(或正则表达式列表)不匹配的 URL 一定不提取(优先级高于 allow)
deny
# 会被提取的链接 domains
allow_domains
# 一定不会被提取链接的 domains
deny_domains
# 使用 xpath 表达式,和 allow 共同作用过滤链接
restrick_xpaths

使用例:

# -*- coding: utf-8 -*-
import scrapy
# 导入 CrawlSpider 相关模块
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

# 该爬虫程序是基于 CrawlSpider 类的
class CrawldemoSpider(CrawlSpider):
name = 'crawlDemo' # 爬虫文件名称
# allowed_domains = ['example.com']
start_urls = ['http://www.example.com/']

# 连接提取器:会去起始 url 响应回来的页面中提取指定的 url
# 虽然这里匹配的不是完整的链接,但是 CrawlSpider 会自动将其补充完整
link = LinkExtractor(allow=r'/8hr/page/\d+')
# rules 元组中存放的是不同的规则解析器(封装好了某种解析规则)
rules = (
#规则解析器:可以将连接提取器提取到的所有连接表示的页面进行指定规则(回调函数)的解析
Rule(link, callback='parse_item', follow=True),
)
# 解析方法
def parse_item(self, response):
# print(response.url)
dives = response.xpath('//div[@id="content-left"]/div')
for div in dives:
author = div.xpath('./div[@class="author clearfix"]/a[2]/h2/text()').extract_first()
print(author)

# CrawlSpider 类和 Spider 类的最大不同是 CrawlSpider 多了一个 rules 属性,
# 其作用是定义”提取动作“。在 rules 中可以包含一个或多个 Rule 对象,在 Rule 对象中包含了 LinkExtractor 对象。

使用例 2

import scrapy
import re
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class Study02Spider(CrawlSpider):
name = 'study02'
allowed_domains = ['ssr1.scrape.center']
start_urls = ['http://ssr1.scrape.center/']

# 定义提取 url 地址的规则
rules = (
# 在字符前面加个 r 表示正则表达式
# 提取出 url 地址的 response 会交给 callback 处理
Rule(LinkExtractor(allow=r'/detail/\d+'), callback='parse_item'),
# 翻页就无需回调了
Rule(LinkExtractor(allow=r'/page/\d+'), follow=True),
)

# 注意:这个 CrawlSpider 没有 parse 函数,也不能写 parse 函数,因为会覆盖掉父类的 parse 函数
def parse_item(self, response):
# 取得电影说明:
drama = re.search('[^\\n\s].?[^\\n]*',
response.xpath('//div[@class="el-card__body"]//div[@class="drama"]/p/text()').get()).group(0)
# 导演列表
directors = response.xpath(
'//div[@class="directors el-row"]//div[@class="el-card is-hover-shadow"]/div[@class="el-card__body"]')
directors_list = []
for ji in directors:
directors_list.append({
'director': ji.xpath('./p/text()').get(),
'director_img_url': ji.xpath('./img/@src').get()
})

# 演员列表
actors = response.xpath('//div[@class="actor el-col el-col-4"]')
actors_list = []
for actor in actors:
actors_list.append({
'actor_name': actor.xpath('.//p[1]/text()').get(),
'actor_image_url': actor.xpath('.//img/@src').get(),
'actor_role': re.search('[^饰:].*', actor.xpath('.//p[2]/text()').get()).group(0)
})
# 剧照地址
movie_poster = response.xpath('//div[@class="photos el-row"]')
movie_poster_list = []
for it in movie_poster:
movie_poster_list.append(it.xpath('.//img/@src').get())

return {
'directors_list': directors_list,
'drama': drama,
'actors_list': actors_list,
'movie_poster_list': movie_poster_list
}


if __name__ == '__main__':
from scrapy import cmdline

cmdline.execute("scrapy crawl study02".split())

下载中间件

Downloader Middlewares 是下载器完成 http 请求

首先需要先 Setting 文件里面打开

DOWNLOADER_MIDDLEWARES = {
'TempSpider.middlewares.TempDownloaderMiddleware': 543,
}

当每个 request 传递给下载中间件时会调用 process_request(self, request, spider)

当下载器完成 HTTP 请求,传递响应给引擎时会调用 process_response(self, request, response ,spider)

例如随机加上 User-Agent 请求头字段 先在 setting 文件里加上

USER_AGENTS = [
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0) ,Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.2.9",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre",
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)",
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
"Opera/9.25 (Windows NT 5.1; U; en), Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.2.9",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"
]

然后就可以引入这个属性了

import random
from TempSpider.settings import USER_AGENTS
from scrapy import signals

# 所以可以在 process_request 里添加请求头
class TempDownloaderMiddleware:
def process_request(self, request, spider):
useragent = random.choice(USER_AGENTS)
request.headers["User-Agent"] = useragent

def process_response(self, request, response, spider):
print(request.headers["User-Agent"])
# 注意!! 这里要把 response 返回出去
return response

编写爬虫

scrapy.Request

scrapy.Request 可以传入的参数:

scrapy.Request(url, callback, method='GET', headers, body, cookies, meta, dont_filter=False)

# callback 指定传入的 url 交给哪个解析函数去处理
# meta 实现在不同的解析函数中传递数据,meta 默认会携带部分信息(传入的参数是一个字典)
# dont_filter 让 scrapy 的去重不会过滤当前 url(scrapy 默认会去除重复的请求),用途是去爬一些响应会变的数据

# meta 的用法
yield scrapy.Request(
response.urljoin(next_url),
# 这个回调可以设置成其它的函数,不一定是要调用自己
self.parse02,
meta = {'item':item}
)

def parse02(self, response):
item = response.meta['item']

Request

参考资料 scrapy的重要对象request和response

requests

属性

url
method
headers
body
meta
copy() 复制一个相同的request
replace()

通过Request传递数据的方法(在两个不同的解析函数之间传递数据的方法。)

def parse_page1(self, response):
item = MyItem()
item['main_url'] = response.url
request = scrapy.Request("http://www.example.com/some_page.html",
callback=self.parse_page2)
request.meta['item'] = item
return request

def parse_page2(self, response):
item = response.meta['item']
item['other_url'] = response.url
return item

主要的子类是 FormRequest(可以模拟表单登陆)

# 用于存储用户名,密码等数据
formdata

# 模拟表单或 Ajax 提交 post 请求的时候(默认是 post 请求)
from_response

实例:使用 FormRequest.from_response() 方法模拟用户登录

通常网站通过 实现对某些表单字段(如数据或是登录界面中的认证令牌等)的预填充。 使用 Scrapy 抓取网页时,如果想要预填充或重写像用户名、用户密码这些表单字段, 可以使用 FormRequest.from_response() 方法实现。下面是使用这种方法的爬虫例子:

import scrapy

class LoginSpider(scrapy.Spider):
name = 'example.com'
start_urls = ['http://www.example.com/users/login.php']

# 之所以还需要传递给另一个方法来发送登陆请求是因为默认 parse 发送的是 POST 请求
def parse(self, response):
return scrapy.FormRequest.from_response( # 从 response 返回一个request(FormRequest)
response,
formdata={'username': 'john', 'password': 'secret'},
callback=self.after_login
)
# 或者
# yield FormRequest.from_response( # 从 response 返回一个request(FormRequest)
# response,
# formdata={'username': 'john', 'password': 'secret'},
# callback=self.after_login
# )

def after_login(self, response):
# check login succeed before going on
if "authentication failed" in response.body:
self.logger.error("Login failed")
return

# continue scraping with authenticated session...

如果一开始就想发送 POST 请求可以重写 start_requests(self) 方法,并且不再调用 start_urls 里的 url

class mySpider(scrapy.Spider):
# start_urls = ["http://www.example.com/"]

def start_requests(self):
url = 'http://www.renren.com/PLogin.do'

# FormRequest 是Scrapy发送POST请求的方法
yield scrapy.FormRequest(
url = url,
formdata = {"email" : "xxx", "password" : "xxxxx"},
callback = self.parse_page
)
def parse_page(self, response):
# do something

Response

属性

url
status
headers
body
request # 是产生该 response 的 request
meta # 是 request.meta 的简要形式
flags
copy()
replace()
urljoin() # 用相对连接生成绝对连接。

如果想要获取 js 代码的字段例子:

# 搭配正则表达式找到字段的数据
page_count = re.findall("var pagecount=(.*?);", response.body.decode)[0]

模拟携带 Cookie 登陆 应用场景:

1、cookie 过期时间很长
2、配合其它程序使用,例如使用 selenium 把登陆之后的 cookie 保存本地,scrapy 发送请求前先读取本地 cookie

如果要观察 Cookie 可以在 Setting 里面打开 COOKIES_DEBUG=True

因为默认 Scrapy 会先发送请求,所以如果第一次请求就携带 Cookie 需要重写 Scrapy 的 start_requests(self) 方法

class mySpider(scrapy.Spider):
allowed_domains = ['example.com']
start_urls = ["http://www.example.com/"]

def start_requests(self):
# FormRequest 是Scrapy发送POST请求的方法
# 默认的 cookies 是如下这种字符串
cookies = "a=1;b=2;c=3;"
# 所以需要手动切割字符串(可以直接把 for 写在里面)
cookies = {i.split("=")[0]:i.split("=")[1] for i in cookies.split(";")}
yield scrapy.Request(
self.start_urls[0],
callback=self.parse,
cookies=cookies
)

def parse(self, response):
pass

修改请求头

如果要修改请求头也是在 setting.py 文件里面加,里面有个被注释的 DEFAULT_REQUEST_HEADERS 属性,取消注释然后修改下就好了

DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}

代理池

免费代理池 kuaidaili 免费代理池 kxdaili 免费代理池 66ip

就算再怎么修改请求头但是每次去请求的都是同一个 ip 依然会给检测出来,所以可以使用 ip 池来更换不同的 ip对服务器进行访问

设置代理的这个需要在下载中间件里配置

添加代理


读取页内例子

读取列表里面的页面的例子

import scrapy
import re
import time
import random


class Study01Spider(scrapy.Spider):
name = 'study01'
allowed_domains = ['ssr1.scrape.center']
start_urls = ['https://ssr1.scrape.center/']
max_time = 2.0
min_time = 0.5

def parse(self, response):

items = response.xpath('//div[@class="el-col el-col-18 el-col-offset-3"]//div[@class="el-row"]')
for it in items:
# 内循环遍历获取类型
categories = it.xpath('.//button/span/text()')
category_list = []
for category in categories:
category_list.append(category.get())

item = {
"image_url": it.xpath('.//img/@src').get(),
"href_url": it.xpath('.//a[@class="name"]/@href').get(),
"name": it.xpath('.//h2/text()').get(),
"categories": category_list,
"country": it.xpath('.//div[@class="m-v-sm info"][1]/span[1]/text()').get(),
"release_time": it.xpath('.//div[@class="m-v-sm info"][2]/span[1]/text()').get(),
"duration": it.xpath('.//div[@class="m-v-sm info"][1]/span[3]/text()').get(),
"grade": float(re.search('[0-9].?[0-9]', it.xpath('.//p/text()').get()).group(0)),
}

# 先睡眠下再发
time.sleep((random.random() * self.max_time) + self.min_time)
# 跳转到页面
yield scrapy.Request(
response.urljoin(item["href_url"]),
self.parse02,
meta={'item': item}
)

# 找到下一页地址
next_url = response.xpath('//button[@class="btn-next"]/../@href').get()
# 要先判断非空
if next_url is not None:
print('=========================下一页========================')
yield scrapy.Request(
response.urljoin(next_url),
# 这个回调可以设置成其它的函数,不一定是要调用自己
self.parse
)

def parse02(self, response):
# 可以直接取得之前就获取的信息
item = response.meta['item']

# 取得电影说明:
drama = re.search('[^\\n\s].?[^\\n]*',
response.xpath('//div[@class="el-card__body"]//div[@class="drama"]/p/text()').get()).group(0)
# 导演列表
directors = response.xpath(
'//div[@class="directors el-row"]//div[@class="el-card is-hover-shadow"]/div[@class="el-card__body"]')
directors_list = []
for ji in directors:
directors_list.append({
'director': ji.xpath('./p/text()').get(),
'director_img_url': ji.xpath('./img/@src').get()
})

# 演员列表
actors = response.xpath('//div[@class="actor el-col el-col-4"]')
actors_list = []
for actor in actors:
actors_list.append({
'actor_name': actor.xpath('.//p[1]/text()').get(),
'actor_image_url': actor.xpath('.//img/@src').get(),
'actor_role': re.search('[^饰:].*', actor.xpath('.//p[2]/text()').get()).group(0)
})
# 剧照地址
movie_poster = response.xpath('//div[@class="photos el-row"]')
movie_poster_list = []
for it in movie_poster:
movie_poster_list.append(it.xpath('.//img/@src').get())

yield ({
'item': item,
'directors_list': directors_list,
'drama': drama,
'actors_list': actors_list,
'movie_poster_list': movie_poster_list
})


if __name__ == '__main__':
from scrapy import cmdline

cmdline.execute("scrapy crawl study01".split())

然后再把读取的数据通过 pipelines 保存到 Json 文件里

import json


# 可以有多个 Pipeline,执行顺序是在设置文件里更改
class TempPipeline:
# 给文件追加写入的权限
f = open("./temp.json", "ta", encoding='UTF-8')

# 一个管道类有以下三个生命周期函数
def open_spider(self, spider):
print("当前开启的爬虫为:", spider)

def process_item(self, item, spider):
# 写入 json 文件
self.f.write(json.dumps(item, ensure_ascii=False) + ',\n')
return item

def close_spider(self, spider):
# 在这里关闭,避免频繁的开关文件
print("爬虫%s关闭!" % spider)

基本例子

# -*- coding:utf-8 -*-
import scrapy

# 创建爬虫类,并且继承 scrapy.Spider
class TestspiderSpider(scrapy.Spider):
name = 'testSpider'
# 这里就爬取自己的博客吧
allowed_domains = ['alsritter.icu'] # 允许爬取的范围,防止爬虫爬到了别的网站
start_urls = ['http://alsritter.icu/']
# 先赋初值为 0
page_count = 0
# 当前页数
next_page = 1

def parse(self, response):
div = response.xpath('//div[@class="recent-post-info"]')
for li in div:
item = {'title': li.xpath('./a[@class="article-title"]/text()').get(),
'time': li.xpath('.//span[@class="post-meta__date-updated"]/text()').get()}
# 把这个结果返回给 pipelines,使之处理这个结果
yield item

# 翻页
# 取得页数(第一次才需要获取)
if self.page_count == 0:
self.page_count = int(response.xpath('//div[@class="pagination"]/a[last()-1]/text()').get())

if self.next_page < self.page_count:
self.next_page = self.next_page + 1
next_url = response.urljoin(f'/page/{self.next_page}/')
print(next_url)
# 发出请求
yield scrapy.Request(next_url, self.parse)
else:
print('================结束=================')

# 直接在这个脚本里设置启动,调用命令行包,执行命令
if __name__ == '__main__':
from scrapy import cmdline

cmdline.execute("scrapy crawl testSpider".split())

可以把 MySpider 的取得的值传递给 pipelines ,然后在 pipelines 里进行相应操作(例如持久化)

# pipelines.py 的内容
# 可以有多个 Pipeline,执行顺序是在设置文件里更改
class TempspiderPipeline:
# 拿到 TestspiderSpider 传过来的 item 进行相应处理(例如这里插入数据)
def process_item(self, item, spider):
# 这个 Pipeline 用来加多一个字段
item['temp'] = '这个是测试插入的数据'
# 如果要传递给下一个 Pipeline 这里必须 return
return item


class TempspiderPipeline02:
def process_item(self, item, spider):
print(item)
return item

先开启 pipelines 使之能取得 MySpider 返回的 item 还是修改 setting.py 把这个 ITEM_PIPELINES 注释删掉

ITEM_PIPELINES = {
# TempspiderPipeline 表示要启动的类名,这个 300是一个优先级
# 顺序是从小到大
'TempSpider.pipelines.TempspiderPipeline': 300,
'TempSpider.pipelines.TempspiderPipeline02': 301,
}

可以加上一个随机访问时间:

import time
import random

...
max_time = 5.0
min_time = 1.3

def parse(self, response):
# 先睡眠 1秒再发一次请求(单位是秒)
time.sleep((random.random() * self.max_time) + self.min_time)
...