小组爬虫项目一步一步按计划进行,翻译也提上了日程,先看一遍Scrapy 1.5的官方文档热热身先 :)

翻译文档到时会统一整合到gitbook上!这里只分享一些自己用Scrapy的练习~

~实践是检验真理的唯一标准~

Novel

单线程

先写个简单的没有JS异步加载的爬小说吧!

关键点在于用选择器来定位你要爬的内容。。单单在这一点上,我就觉得Scrapy比BeautifulSoup方便不知道多少倍了。。因为Scrapy的shell简直是神器,手动测试=>精准定位=>直接复制=>bingo!

chapter = response.xpath("//div[@class='bookname']/h1/text()").extract()
content = response.xpath("//div[@id='content']/text()").extract()
next_page = response.xpath("//div[@class='bottem']/a/@href")[3].extract()

至于如何储存提取的数据,那就根据需求来选择就好了。可以选择python的写入文件,也可以存储在json、jl里面。

但是,这种单线程爬取,实在是太慢了。。。对于上千上万的请求。。怕是要挂一晚上哦~~

所以,继续阅读文档,再学一波多线程爬取。。那就perfect啦!(后来发现Scrapy就是协程的,所以。。如果是多核电脑,你只可以开多个进程来加快速度~)

多线程

暂时如果不跳出Scrapy的限制的话,可能只能同时启动多个spider来实现效率上的提升吧?

爬图片

process_item()

题库

最近做了个爬题库的项目,根据金主爸爸的需求去爬,统一格式。。方才明白,爬虫容易,洗数据,统一格式才是。。让人抓狂的。

不过一件事情翻炒翻炒,就会有不同的收获!

储存方式

pipeline

​ scrapy默认 -o 参数是储存到一个文件里面,但是如果数据动不动上G,打开都容易死机~更别说处理的时候了(而且面对大数据,现成工具的用处并不大。。。千万不要尝试手动处理。。。自己写脚本才是比较geek的方法!!)

​ 这次储存的是json格式的数据,而且金主爸爸要求每个文件大小不超过20M。

一开始我是一整套爬下来,再用脚本分片,感觉效率还是不够高(关键是还很麻烦)。

后来再查了点资料,翻炒了文档,就去模仿自定义了一个pipeline

# 自定义pipeline
import json
import codecs
import math
import os


point = {}
class ChuZhongPipeline(object):


    # 初始化时指定要操作的文件
    def __init__(self):
        print('\nstoring..ChuZhong..')

    # 存储数据,将 Item 实例作为 json 数据写入到文件中
    def process_item(self, item, spider):
        global point
        pointName = item['pointName']
        grade = item['grade']
        dir_path = "./地理/%s" % grade
        # 不存在文件夹则创建
        if not os.path.exists(dir_path):
            os.makedirs(dir_path)
        # 不存在该知识点的文件名,则创建
        if pointName not in point:
            point[pointName] = 1
            count = str(math.floor(point[pointName] / 1000))
            filename = dir_path + "/" + pointName + "_" + count + ".json"
            self.file = codecs.open(filename, 'w', encoding='utf-8')
            self.file.close()
        elif pointName in point:
            point[pointName]+= 1

        # 写入文件
        if (point[pointName] % 1000) != 0:
            count = str(math.floor(point[pointName] / 1000))
            currentname = dir_path + "/" + pointName + "_" + count + ".json"
            self.file = codecs.open(currentname,'a', encoding='utf-8')
            lines = json.dumps(dict(item), ensure_ascii=False) + ',\n'
            self.file.write(lines)
            self.file.close()
        elif (point[pointName] % 1000) == 0:
            count = str(math.floor(point[pointName] / 1000))
            currentname = dir_path + "/" + pointName + "_" + count + ".json"
            self.file = codecs.open(currentname,'a', encoding='utf-8')
            lines = json.dumps(dict(item), ensure_ascii=False) + ',\n'
            self.file.write(lines)
            self.file.close()
            count = str(math.floor(point[pointName] / 1000))
            filename = dir_path + "/" + pointName + "_" + count + ".json"
            self.file = codecs.open(filename, 'w', encoding='utf-8')
        return item

这里实现的功能是,根据传过来的item,创建相应文件夹,和文件名,并限制一个文件的行数(scrapy写入json是一行一个数据),代码比较累赘,但是。。起码完成了我的需求是吧:)

这个代码一开始是遇到了坑的:

  1. Process_item()里面只能用item里面的数据来自动化,如果你直接在这个pipeline里面设置一个全局变量,试图通过它来完成自动化创建与写入文件,我感觉是行不通的~~(关键是我太菜了!)
  2. 由于scrapy的协程机制,写入文件的时候,你必须重新打开相应文件,来写入。。不然就会把一些奇奇怪怪的数据写到一个与他无关的文件里面,数据就混乱了!!
  3. 还有一点就是,注意代码逻辑!!!注意代码逻辑!!!注意代码逻辑!!!

爬取数据任务完成了之后,只需要再写个脚本,把格式统一一下,把json文件的两个 [] 补上,就大功告成了!

item exporter

这是用另一种方式储存json,但是这样储存是把所有数据写到一行里,不利于后续处理,所以我舍弃了。

from scrapy.exporters import JsonItemExporter
class ExercisesPipeline(object):
    # 调用 scrapy 提供的 json exporter 导出 json 文件
    def __init__(self):
        self.file = open('questions_exporter.json', 'wb')
        # 初始化 exporter 实例,执行输出的文件和编码
        self.exporter = JsonItemExporter(self.file,encoding='utf-8',ensure_ascii=False)
        # 开启倒数
        self.exporter.start_exporting()

    def close_spider(self, spider):
        self.exporter.finish_exporting()
        self.file.close()

    # 将 Item 实例导出到 json 文件
    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

Settings设置的高级应用

官方文档中scrapy中settings参数有四个级别:

  1. 命令行选项(Command line Options)(最高优先级)
  2. 项目设定模块(Project settings module)
  3. 命令默认设定模块(Default settings per-command)
  4. 全局默认设定(Default global settings) (最低优先级)

在这个项目中,我主要用了第二个setting。众所周知,一个scrapy项目中只有一个全局settings.py,所以我一般只会在里面设置一些全局参数(UA等一系列可以不变的变量),然后就可以用custom_settings来自定义项目的一些设置:

Eg:

spiders/somespider.py

from ..custom_settings import *

class Spider1(CrawlSpider):
    name = "spider1"
    custom_settings = custom_settings_for_spider1
    pass

class Spider2(CrawlSpider):
    name = "spider2"
    custom_settings = custom_settings_for_spider2

custom_settings.py

custom_settings_for_spider1 = {
    'LOG_FILE': 'spider1.log',                              # 日志文件
    'CONCURRENT_REQUESTS': 100,                             # 请求并发数
    'DOWNLOADER_MIDDLEWARES': {                             # 下载中间件
        'spider.middleware_for_spider1.Middleware': 667,
    },
    'ITEM_PIPELINES': {
        'spider.mysql_pipeline_for_spider1.Pipeline': 400,  # 管道中间件
    },
}

custom_settings_for_spider2 = {
    'LOG_FILE': 'spider2.log',
    'CONCURRENT_REQUESTS': 40,
    'DOWNLOADER_MIDDLEWARES': {
        'spider.middleware_for_spider2.Middleware': 667,
    },
    'ITEM_PIPELINES': {
        'spider.mysql_pipeline_for_spider2.Pipeline': 400,
    },
}

这里的设置优先级会比settings高,就可以为某一特定spider写一个定制的下载中间件,来绕过一下反爬机制

如果每个爬虫的储存方式,有不同需求,就可以定制不同的pipeline来储存,只需要把你的想法加到pipeline.py即可(记住对应名字)

参考资料

scrapy中settings参数的使用详解