跳到主要内容

7 篇博文 含有标签「Python」

查看所有标签

scrapy 的五大核心组件

· 阅读需 8 分钟

scrapy 的五大核心组件

学习 scrapy 核心组件的目的:

  1. 大概了解 scrapy 的运行机制
  2. 为我们后面学习分布式爬虫做铺垫

五大核心组件及其作用:

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

![scrapy-architecture](/img/scrapy 高级用法/scrapy-architecture.png)

如上图所示,scrapy 五大核心组件的请求调度流程为:

  1. 请求最先从爬虫中发出,发送给引擎。
  2. 引擎将请求交给调度器。
  3. 调度器分为两部分,过滤器和队列。请求经过滤器进行去重后,交给队列。调度器将按照队列中元素的次序,将请求返回给引擎。
  4. 引擎将调度好的请求交给下载器下载资源。
  5. 下载器从网络中把资源下载好后,把响应结果交给引擎。
  6. 引擎把响应结果交给爬虫,进行数据解析
  7. 爬虫将解析好的数据放到实体(Item)中,交给引擎
  8. 引擎根据爬虫返回的结果进行不同的处理。如果返回的是实体对象,则交给管道进行数据持久化操作;如果返回的是请求对象,则交给调度器,重复步骤 2。

请求传参实现深度爬取

深度爬取指的爬取的数据没有在同一张页面中,比如首页数据和详情页数据。

在 scrapy 中如果没有请求传参我们是无法持久化存储数据的。

请求传参的实现方式:

scrapy.Request(url, callback, meta)

meta 是一个字典,可以将 meta 传递给 callback。我们可以在回调函数 callback 中取出 meta:

response.meta

需求:爬取 2345 电影网电影前五页中所有的电影标题和电影简介信息

网址 url:https://www.4567kan.com/index.php/vod/show/id/5.html

分析:首页只能看见电影标题,电影简介要在电影的详情页才能看见。这就涉及了网页的深度爬取,需要结合请求传参来实现。

爬虫源文件的写法为:

import scrapyfrom moviePro.items import MovieproItemclass MovieSpider(scrapy.Spider):    name = 'movie'    # allowed_domains = ['www.xxx.com']    start_urls = ['https://www.4567kan.com/index.php/vod/show/id/5.html']    url = 'https://www.4567kan.com/index.php/vod/show/id/5/page/%s.html'    page = 2    def parse(self, response):        li_list = response.xpath('//ul[@class="stui-vodlist clearfix"]/li')        for li in li_list:            url = 'https://www.4567kan.com' + li.xpath('./div/a/@href').extract_first()            name = li.xpath('./div/a/@title').extract_first()            item = MovieproItem()            item['name'] = name            # 对详情页url发起请求            # meta作用:可以将meta字典传递给callback            yield scrapy.Request(url, self.parse_detail, meta={'item': item})        if self.page <= 5:            new_url = self.url % self.page            self.page += 1            yield scrapy.Request(new_url, self.parse)    # 被用作于解析详情页的数据    def parse_detail(self, response):        desc = response.xpath('//span[@class="detail-content"]/text()').extract_first()        if not desc:            desc = response.xpath('//span[@class="detail-sketch"]/text()').extract_first()        # 接收传递过来的meta        item = response.meta['item']        item['desc'] = desc        yield item

settings 里面要配置 UA 伪装,要在 items 里面写好字段,还要再管道中写好数据持久化代码,这里就不列举了。

中间件

在 Django 中我们已经学习过中间件了,scrapy 的中间件与 Django 的中间件类似,也是用来批量拦截处理请求和响应。

scrapy 的中间件有两种:

  • 爬虫中间件
  • 下载中间件(推荐)

爬虫中间件和下在中间件的作用是类似的。稍微有点区别是,下在中间件处理的请求是经过调度器调度去重了的。

通过中间件,我们可以在三个方面进行处理:拦截请求、拦截响应和拦截异常的请求

拦截请求可以做到:

  • 篡改请求 url(可以,但是没必要)
  • 伪装请求头信息(通常在 settings 里面配置)
    • UA 伪装
    • Cookie 伪装

拦截响应可以用来:

  • 篡改响应数据(一般不这么做)

拦截异常的请求通常用来:

  • 代理操作必须使用中间件才可以实现(重点)

    process_exception:    request.meta['proxy'] = 'http://ip:port'

中间件的使用示例:

class MiddleproDownloaderMiddleware(object):    # 拦截所有(正常&异常)的请求    # 参数:request就是拦截到的请求,spider就是爬虫类实例化的对象    def process_request(self, request, spider):        print('process_request()')        request.headers['User-Agent'] = 'xxx'        # request.headers['Cookie'] = 'xxxxx'        return None #or request    # 拦截所有的响应对象    # 参数:response拦截到的响应对象,request响应对象对应的请求对象    def process_response(self, request, response, spider):        print('process_response()')        return response    # 拦截异常的请求    # 参数:request就是拦截到的发生异常的请求    # 作用:想要将异常的请求进行修正,将其变成正常的请求,然后对其进行重新发送    def process_exception(self, request, exception, spider):        # 如果请求的ip被禁掉,该请求就会变成一个异常的请求        request.meta['proxy'] = 'http://ip:port' #设置代理        print('process_exception()')        return request #将异常的请求修正后将其进行重新发送

不要忘了在 settings 中,把下载中间件的代码取消注释:

DOWNLOADER_MIDDLEWARES = {   'middlePro.middlewares.MiddleproDownloaderMiddleware': 543,}

大文件(图片视频等)下载

大文件数据是在管道中请求到的。下载管道类是 scrapy 封装好的我们直接用即可:

from scrapy.pipelines.images import ImagesPipeline    # 提供了数据下载功能

创建一个管道类,继承自 ImagesPipeline(类似地,还有 MediaPipeline 和 FilePipeline,用法大同小异),重写该管道类的三个方法:

  • get_media_requests:对图片地址发起请求
  • file_path:返回图片名称即可
  • item_completed:返回 item,将其返回给下一个即将被执行的管道类

在配置文件中指定文件下载后存放的文件夹:

IMAGES_STORE = 'dirName'

需求:使用 scrapy 爬取校花网的图片

网址 url:http://www.521609.com/daxuexiaohua/

管道类代码:

# 该默认管道无法帮助我们请求图片数据,因此该管道我们就不用# class ImgproPipeline(object):#     def process_item(self, item, spider):#         return item# 管道需要接受item中的图片地址和名称,然后再管道中请求到图片的数据对其进行持久化存储from scrapy.pipelines.images import ImagesPipeline    # 提供了数据下载功能# from scrapy.pipelines.media import MediaPipeline# from scrapy.pipelines.files import FilesPipelineimport scrapyclass ImgsPipiLine(ImagesPipeline):    # 根据图片地址发起请求    def get_media_requests(self, item, info):        # print(item)        yield scrapy.Request(url=item['src'],meta={'item':item})    # 返回图片名称即可    def file_path(self, request, response=None, info=None):        # 通过request获取meta        item = request.meta['item']        filePath = item['name']        return filePath    # 只需要返回图片名称    # 将item传递给下一个即将被执行的管道类    def item_completed(self, results, item, info):        return item

爬虫源文件代码:

import scrapyfrom imgPro.items import ImgproItemclass ImgSpider(scrapy.Spider):    name = 'img'    # allowed_domains = ['www.xxx.com']    start_urls = ['http://www.521609.com/daxuexiaohua/']    def parse(self, response):        li_list = response.xpath('//div[@id="content"]/div[2]/div[2]/ul/li')        for li in li_list:            name = li.xpath('./a/img/@alt').extract_first() + '.jpg'            src = 'http://www.521609.com' + li.xpath('./a/img/@src').extract_first()            item = ImgproItem()            item['name'] = name            item['src'] = src            yield item

items 和 settings 也要进行常规配置,就不一一列举了。

settings.py 中的常用配置

增加并发。默认 scrapy 开启的并发线程为 32 个,可以适当进行增加。在 settings 配置文件中修改 CONCURRENT_REQUESTS 的值即可:

CONCURRENT_REQUESTS = 100

降低日志级别。在运行 scrapy 时,会有大量日志信息的输出,为了减少 CPU 的使用率,可以设置 log 输出信息为 INFO 或者 ERROR。在配置文件中编写:

LOG_LEVEL = 'INFO'# 或者LOG_LEVEL = 'ERROR'

禁止 cookie。如果不是真的需要 cookie,则在 scrapy 爬取数据时可以禁止 cookie 从而减少 CPU 的使用率,提升爬取效率。在配置文件中编写(如果要启用 cookie,将这个值改成 True 即可):

COOKIES_ENABLED = False

禁止重试。对失败的 HTTP 进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:

RETRY_ENABLED = False

减少下载超时。如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:

DOWNLOAD_TIMEOUT = 10    # 超时时间为10s

selenium 在 scrapy 中的使用

· 阅读需 5 分钟

一直以来,我们都是直接使用 scrapy 框架的 Request 模块进行网页数据的请求。但是如果网页中有动态加载的数据,这种方式就不容易实现了。

其实 scrapy 更多的处理的还是没有动态加载数据的页面。对于动态加载的页面,我们还是比较倾向于使用 requests。

但是如果真的有这么个需求,需要我们使用 scrapy 爬取动态页面的话,通过 selenium 发送请求获取数据,将会是一个不错的选择。

接下来,我们通过爬取网易新闻,来演示如何在 scrapy 中,使用 selenium 爬取数据。

需求:爬取网易新闻中的国内,国际,军事,航空,无人机这五个板块下所有的新闻数据(标题 + 内容)

网址 url:https://news.163.com/

分析:

  • 首页没有动态加载的数据,可以直接爬取到五个板块对应的 url
  • 每一个板块对应的页面中的新闻标题是动态加载,需要使用 selenium 爬取新闻标题和详情页的 url(关键)
  • 每一条新闻详情页面中的数据不是动态加载,在这里可以爬取到新闻内容

selenium 在 scrapy 中的使用流程

  1. 在爬虫类中实例化一个浏览器对象,将其作为爬虫类的一个属性
  2. 在中间件中实现浏览器自动化相关的操作
  3. 在爬虫类中重写 closed(self, spider) 方法,在其内部关闭浏览器对象

接下来,我们就按照流程,依次编写我们的代码。

首先,编写爬虫源文件中的代码,一切都按照正常思路走就行。先从首页到每个模块页,在模块页中拿到新闻的标题和详情页的 url。再从模块页进入到详情页,拿到文章内容。

用代码表示就是:

import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem

class WangyiSpider(scrapy.Spider):
name = 'wangyi'
# allowed_domains = ['news.163.com']
start_urls = ['http://news.163.com/']
module_urls = []
# 实例化了一个全局的浏览器对象,稍后在中间件中会用到
bro = webdriver.Chrome()

def parse(self, response):
# 国内,国际,军事,航空,无人机这五个板块的索引
target_list = [3, 4, 6, 7, 8]
li_list = response.xpath('//div[@class="ns_area list"]/ul/li')
for target in target_list:
li = li_list[target]
url = li.xpath('./a/@href').extract_first()
self.module_urls.append(url)
# 对每一个板块的url发起请求
yield scrapy.Request(url, callback=self.parse_module)

# 数据解析:新闻标题+新闻详情页的url(动态加载的数据)
def parse_module(self, response):
# 直接对response解析新闻标题数据是无法获取该数据(动态加载的数据)
# response是不满足当下需求的response,需要将其变成满足需求的response
# 满足需求的response就是包含了动态加载数据的response
# 满足需求的response和不满足的response区别在哪里?
# 区别就在于响应数据不同。我们可以使用中间件将不满足需求的响应对象中的响应数据篡改成包含
# 了动态加载数据的响应数据,将其变成满足需求的响应对象
div_list = response.xpath('//div[@class="newsdata_wrap"]/ul/li[1]/div/div')
for div in div_list:
url = div.xpath('./a/@href').extract_first()
title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
if url and title: # 因为广告等原因,有些链接取不到url和title
item = WangyiproItem()
item['title'] = title
yield scrapy.Request(url, callback=self.parse_detail, meta={'item': item})

def parse_detail(self, response):
"""解析新闻详情"""
item = response.meta['item']
content = response.xpath('//div[@id="endText"]/p/text()').extract()
item['content'] = content
yield item

# 爬虫类父类的方法,该方法是在爬虫结束前最后一刻执行
def closed(self, spider):
self.bro.close()

但是这样是解析不出数据来的。因为我们前面分析过,模块页中的每个文章都是动态加载的。我们需要修改响应数据,让响应数据变成我们想要的那种,加载好了新闻链接和标题的网页数据。这就要在中间件中,通过 selenium 发送请求,获取数据了。

用代码实现就是:

from scrapy.http import HtmlResponse
from time import sleep

class WangyiproDownloaderMiddleware(object):
# 拦截所有的响应对象
# 整个工程发起的请求:1+5+n,相应也会有1+5+n个响应
# 只有指定的5个响应对象是不满足需求
# 只将不满足需求的5个指定的响应对象的响应数据进行篡改即可
def process_response(self, request, response, spider):
# 在所有拦截到的响应对象中找出指定的5个响应对象
if request.url in spider.module_urls:
bro = spider.bro
# response表示的就是指定的不满足需求的5个响应对象
# 篡改响应数据:首先先获取满足需求的响应数据,将其篡改到响应对象中即可
# 满足需求的响应数据就可以使用selenium获取
bro.get(request.url) # 对五个板块的url发起请求
sleep(2)
bro.execute_script('window.scrollTo(0, document.body.scrollHeight)')
sleep(2)
# 捕获到了板块页面中加载出来的全部数据(包含了动态加载的数据)
# response.text = bro.page_source
# 返回一个新的响应对象,新的对象替换原来不满足需求的旧的响应对象
return HtmlResponse(url=request.url, body=bro.page_source, encoding='utf-8', request=request)
return response # 1+n

剩下的就是在配置中开启管道和中间件,在 items 中写上相应字段,在管道中进行数据持久化存储,就不一一介绍了。

至此,我们实现了在 scrapy 中使用 selenium 发送请求。

selenium 介绍和安装

· 阅读需 6 分钟

selenium 介绍和安装

爬虫过程中,各种反爬机制让人头疼。由于动态网页的存在,明明浏览器看得见可以点击的东西,却不能直接通过 requests 请求得到。非要绕很多弯才能获取我们想要的数据。有的请求还要携带 cookie 和一些乱七八糟的字符串。

selenium 模块就是为了帮我们解决这些困扰而诞生的。

selenium 是一种基于浏览器的自动化的模块。

自动化的含义是,可以通过代码指定一些列的行为动作,然后将其作用到浏览器中。

因为 selenium 本身就是基于浏览器的,所以几乎不会受到反爬机制的束缚。

selenium 的安装:

pip install selenium

selenium 和爬虫之间的关联

  1. 便捷的捕获到任意形式动态加载的数据(可见即可得)
  2. 实现模拟登录

selenium 虽然能给我们带来很大的便捷,但是却有一个显著的缺点 -- 效率太低。因为每次爬取信息,都要打开浏览器。

在使用 selenium 之前,我们需要下载各种浏览器的驱动,注意驱动版本要和当前使用的浏览器版本相符合。

谷歌驱动下载:http://chromedriver.storage.googleapis.com/index.html

驱动下载好后,最好保存在环境变量中存在的文件夹中,比如 Python 安装目录的 bin 目录。这样,我们在使用 selenium 的时候,就不必每次都指定驱动了。

这里的示例,我把谷歌浏览器驱动放在了工作目录中,每次使用都要指定驱动的位置。

selenium 的基本使用流程如下:

from selenium import webdriver
from time import sleep
from lxml import etree
# 基于浏览器的驱动程序实例化一个浏览器对象
bro = webdriver.Chrome(executable_path='./chromedriver.exe')
# 对目的网站发起请求
bro.get('https://www.jd.com')
# 标签定位,最常用的是id和xpath定位
search_box = bro.find_element_by_xpath('//input[@id="key"]')
# 向标签中输入数据
search_box.send_keys('iphone X')

search_btn = bro.find_element_by_xpath('//button[@aria-label="搜索"]')
# 点击按钮
search_btn.click()

sleep(2)

# 在搜索结果页面进行滚轮向下滑动的操作(执行js操作:js注入)
bro.execute_script('window.scrollTo(0, document.body.scrollHeight)')
sleep(2)
bro.quit()

再看一个处理百度网页的例子:

from selenium import webdriver
from time import sleep

# 后面是你的浏览器驱动位置,记得前面加r'','r'是防止字符转义的
driver = webdriver.Chrome(r'./chromedriver.exe')
# 用get打开百度页面
driver.get("http://www.baidu.com")
# 查找页面的“设置”选项,并进行点击
driver.find_elements_by_link_text('设置')[0].click()
sleep(2)
# # 打开设置后找到“搜索设置”选项,设置为每页显示50条
driver.find_elements_by_link_text('搜索设置')[0].click()
sleep(2)

# 选中每页显示50条
m = driver.find_element_by_id('nr')
sleep(2)
m.find_element_by_xpath('//*[@id="nr"]/option[3]').click()
m.find_element_by_xpath('.//option[3]').click()
sleep(2)

# 点击保存设置
driver.find_elements_by_class_name("prefpanelgo")[0].click()
sleep(2)

# 处理弹出的警告页面 确定accept() 和 取消dismiss()
driver.switch_to.alert.accept()
sleep(2)
# 找到百度的输入框,并输入 美女
driver.find_element_by_id('kw').send_keys('美女')
sleep(2)
# 点击搜索按钮
driver.find_element_by_id('su').click()
sleep(2)
# 在打开的页面中找到第一个图片的标签,并打开这个页面
driver.find_element_by_xpath('//*[@id="1"]/div[1]/a[1]/img').click()
sleep(3)

# 关闭浏览器
driver.quit()

我们从前爬取过药监局的网页,那个网页时动态生成的。当时我们是通过浏览器的抓包工具,找到获取数据的请求,从而实现了页面信息的抓取。

如果使用 selenium,抓取动态网页将变得很容易。

需求:抓取药监局页面前三页所有企业名称信息

网址 url:http://125.35.6.84:81/xk/

url = 'http://125.35.6.84:81/xk/'
bro = webdriver.Chrome(executable_path='./chromedriver')
bro.get(url)
page_text_list = [] # 每一页的页面源码数据都会放到这里面
sleep(2)
# 捕获到当前页面对应的页面源码数据,也就是当前页面全部加载完毕后对应的所有的数据
page_text_list.append(bro.page_source)

for i in range(2):
next_btn = bro.find_element_by_id('pageIto_next')
next_btn.click()
sleep(1)
page_text_list.append(bro.page_source)

# 点击下一页
for page_text in page_text_list:
tree = etree.HTML(page_text)
text_list = tree.xpath('//ul[@id="gzlist"]//dl//text()')
for text in text_list:
print(text)

sleep(2)
bro.quit()

动作链 ActionChains

有时候,我们除了点击操作之外,还会有一些比如拖动等操作。这时候,就需要用到动作链。

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

# 如果通过find系列的函数进行标签定位,如果标签是存在于iframe下面,则会定位失败
# 解决方案:使用switch_to即可
bro.switch_to.frame('iframeResult')
div_tag = bro.find_element_by_id('draggable')

# 对div_tag进行滑动操作
action = webdriver.ActionChains(bro)
action.click_and_hold(div_tag) # 点击且长按

for i in range(4):
# perform让动作链立即执行
action.move_by_offset(10, 15).perform()
sleep(0.5)

sleep(2)
bro.quit()

让 selenium 规避检测

虽然 selenium 很强大,但依然有可能会漏出马脚,让人家检测出我们是在使用 selenium 发起的浏览器请求。有的网站会检测请求是否为 selenium 发起,如果是的话则让该次请求失败。

规避检测的方法是让 selenium 接管 chrome 浏览器。

实现步骤

  1. 必须将你电脑中安装的谷歌浏览器的主程序所在的目录找到,且将目录添加到环境变量中。

  2. 打开 cmd,在命令行中输入命令:

    chrome.exe --remote-debugging-port=9222 --user-data-dir="一个空文件夹的目录"

    指定执行结束后,会打开你本机安装好的谷歌浏览器。

  3. 执行如下代码:可以使用下述代码接管步骤 2 打开的真实的浏览器

    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options

    chrome_options = Options()
    chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
    # 本机安装好谷歌的驱动程序路径
    chrome_driver = "./chromedriver.exe"

    driver = webdriver.Chrome(executable_path=chrome_driver,chrome_options=chrome_options)
    print(driver.title)

无头浏览器

我们看到,每次执行 selenium 代码,都要打开浏览器窗口,然后亲眼看着它操作。有的时候,如果不小心把鼠标放到浏览器的窗口里面,还有可能会影响到自动化程序的正常运行。

无头浏览器,就是将浏览器的页面隐藏起来,我们看不见它的操作。那么就不会在每次允许 selenium 代码的时候跳出窗口来犯我们。我们也不会因为误操作而影响程序的运行。

常用的无头浏览器有两个:

  • 谷歌无头浏览器(推荐)
  • phantomJs(停止维护)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

# 创建一个参数对象,用来控制chrome以无界面模式打开
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')

# 创建浏览器对象
browser = webdriver.Chrome(executable_path='./chromedriver', chrome_options=chrome_options)

# 上网
url = 'https://www.baidu.com/'
browser.get(url)
time.sleep(3)
#截图
browser.save_screenshot('baidu.png')
print(browser.page_source)
browser.quit()