Scrapy 爬虫的简单使用

Table of Contents

1. 简单的爬虫流程

一般的,程序向网站请求网页

程序 =========> 网站

而后,网站返回网页与程序

程序 <========= 网站

接着程序负责解析得到的 HTML 数据即可

2. Scrapy 如何扩展这一流程

程序请求网页时, scrapy 添加了中间件在双方之间

程序 =====> 中间件 ======> 网站

网站返回数据时,也需要通过中间件

程序 <===== 中间件 <====== 网站

程序解析完数据后,不会马上处理数据,而是把一个网页的数据打包成一个 Item 数据,传递给 pipeline 处理

pipeline <==== item <==== 程序

3. Scrapy 简单示例

这里以爬取美图录的图片为例(注意,我不打算用默认的 start_urls 数据)
首先我们到一个模特的主页上,比如

2022-04-10_20-13-21_screenshot.png
接下来我们要从这个 指定模特页面 上的每一个专辑上爬取图片

3.1. 准备项目

  1. 安装 scrapy

    sudo pip3 install scrapy
    
  2. 创建新项目

    scrapy startproject meitulu
    

3.2. 基础设定

meitulu/spiders/ 下我们新建一个爬虫文件 evelyn.py

class Spider(scrapy.Spider):
    name = 'evelyn'

其中的 name 属性是为了调用的时候能找到爬虫位置,比如调用这个爬虫的时候,我们指定 name

scrapy crawl evelyn

3.3. 添加命令行参数

def __init__(self, albumUrl=None, *args, **kwargs):
    if albumUrl == None:
	raise Exception('usage: -a albumUrl=...')

    super(Spider, self).__init__(*args, **kwargs)
    self.start_url = albumUrl

def start_requests(self):
    yield scrapy.Request(url=self.start_url, callback=self.parse)

这里在类的 __init__ 方法上我们做了修改,第二个参数是命令行参数名称,默认为 None ,第三,第四个参数不需要管
我们这样调用

scrapy crawl evelyn -a albumUrl='http://meitulu.cn/t/Evelynaili/'

注意这里我们没有定义 start_urls 而是 start_url ,这里我覆盖了这种默认行为,重新定义了 start_requests
并将获取到的响应交给 self.parse 回调来解析

3.4. 解析函数 parseAlbum

这里的函数解析一张专辑中的每一张图片,然后解析出 item ,传递给 pipeline ,如果有下一页,继续请求

def parseAlbum(self, response, firstUrl, albumName, count):
    url = response.css('div.content center a img.content_img::attr(src)').extract_first()
    name = str(count) + '.jpg'

    yield Image(url=url, name=name, albumName=albumName, fromUrl=response.url)

    nextPage = response.urljoin(response.css('div.content center a::attr(href)').extract_first())
    if nextPage != firstUrl:
	yield scrapy.Request(
	    url=nextPage,
	    callback=self.parseAlbum,
	    cb_kwargs=dict(firstUrl=nextPage, albumName=albumName, count=count+1))

在函数中,

  • firstUrl
    表示专辑的第一页网址,也是专辑的入口地址,设置他是因为最后一页的下一页是 firstUrl
    需要用来判断当前页是否为最后一页
  • albumName
    表示专辑名称,设置他是因为我看了下html代码,每一张图片的 alt 属性都是专辑名,为其命名也麻烦
  • count
    所以我就设置了这个参数,需要请求下一页的时候,这个参数就加1

3.5. 解析函数 parse

这个函数获取模特主页中每一张专辑的地址,然后传递给 self.parseAlbum 回调,并通过 cb_kwargs 添加额外的函数参数

def parse(self, response):
    urls = response.css('div.main div.boxs ul.img li>a::attr(href)').extract()
    albumUrls = list(map(lambda url: response.urljoin(url), urls))
    names = response.css('div.main div.boxs ul.img li p.p_title> a::text').extract()

    for (albumUrl, name) in zip(albumUrls, names):
	yield scrapy.Request(
	    url=albumUrl,
	    callback=self.parseAlbum,
	    cb_kwargs=dict(firstUrl=albumUrl, albumName=name, count=1))

3.6. Pipeline 处理结果

由于直接请求图片会被网站检测,并返回 403 代码,这里我们伪造下请求头

default_headers = {
    'Accept': 'image/avif,image/webp,*/*',
    'Accept-Encoding': 'gzip, deflate',
    'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'Host': 'image.meitulu.cn',
    'Referer': 'http://meitulu.cn/',
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0'
}

接下来我们写一个类来继承 FilesPipeline ,处理结果

from scrapy.pipelines.images import FilesPipeline

class MeituluPipeline(FilesPipeline):
    default_headers = {
	'Accept': 'image/avif,image/webp,*/*',
	'Accept-Encoding': 'gzip, deflate',
	'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
	'Host': 'image.meitulu.cn',
	'Referer': 'http://meitulu.cn/',
	'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0'
    }

    def file_path(self, request, response=None, info=None, *, item=None):
	dirname = item['albumName']
	basename = item['name']

	return os.path.join(dirname, basename)

    def get_media_requests(self, item, info):
	yield scrapy.Request(item['url'], headers=self.default_headers)

    def item_completed(self, results, item, info):
	return item

其中有三个函数可以挑选着来重写

  • file_path
    指定 item 存贮的文件名
  • get_media_requests
    自定义如何请求图片的函数
  • item_completed
    下载完图片后如何处理

另外这个 file_path 的重写我忘了从哪个教程里粘贴过来的了,先用着吧

3.7. 全局设置

3.7.1. 开启 pipeline

ITEM_PIPELINES = {
   'meitulu.pipelines.MeituluPipeline': 300,
}

3.7.2. 设置 文件存贮 默认位置

FILES_STORE = '/home/steiner/workspace/meitulu/capture/'

Author: Steiner

Created: 2022-04-10 日 23:20

Validate