爱湃森学院

使用Scrapy

2019-01-28

本节是 《Python爬虫从入门到进阶》课程中的一节,课程购买链接(PC访问需要微信扫码) ,目前已更新80% 课程

购买课程请扫码:

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。只需要编写很少的代码就能实现抓取功能,另外由于它底层用了twisted,性能也非常优越。使用Scrapy框架编写的抓取代码,可读性很强,非常利于维护,是现在最流行的抓取框架。

安装

pip install scrapy SQLAlchemy

SQLAlchemy之后我们会用到

一个简单爬虫

我们写一个简单地例子,抓取开发者头条最近5天feed页面的文章地址和标题。先抓包分析下怎么抓比较好。

(略过抓包过程, 课程视频中有..)

接着我们就可以写爬虫代码了:

from datetime import date, timedelta

import scrapy

today = date.today()


class ToutiaoItem(scrapy.Item):
    title = scrapy.Field()
    link = scrapy.Field()


class ToutiaoSpider(scrapy.Spider):
    name = 'toutiao'
    start_urls = [f'https://toutiao.io/prev/{today - timedelta(days=i)}'
                  for i in range(5)]

    def parse(self, response):
        for post in response.xpath('//h3[@class="title"]/a'):
            item = ToutiaoItem()
            item['title'] = post.xpath('@title').extract_first()
            item['link'] = post.xpath('@href').extract_first()
            yield item

好的习惯是先定义Item,这里每项包含标题和链接。start_urls就是要抓取的里面列表。重载了parse方法,里面做页面解析:我还是用xpath的方法。另外在parse里面我没有延伸,就是没有翻页或者解析出新的链接再抓取,一共就抓了这5个页面

接下来就是抓取,使用 runspider 就能运行了,不过头条要求使用一个正确的UA。所以在命令行用-s指定了USER_AGENT变量。另外-o表示把结果导出到toutiao.json文件中

❯ scrapy runspider toutiao.py -s USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" -o toutiao.json

在运行中通过DEBUG信息其实大家就可以看到抓到了什么,当然json文件里面就是最终结果。假如你的程序写的有问题,在运行过程中会抛错,看着堆栈改bug就好了。

上面就是Scrapy的一个基本用法了。可以感受到,我们写的代码非常少,就实现了一个异步的抓取和处理。说的再明确一点,你不需要等待一个任务完成,他们是一起抓取的,充分利用CPU时间,另外即使一些请求失败了,或者处理过程中出错了,其他请求还可以继续完成。

而且我这里使用ITEM定义结构,让这个项目更清晰好理解

爬虫工程

上面我们演示的是一个简单地小例子。事实上Scrapy更倾向于Django那样的的企业级用法。可以在命令行下非常容易的创建一个复杂的爬虫项目

我们看一下项目下的目录结构

❯ scrapy startproject toutiao_project  # 创建一个叫做toutiao_project的项目
❯ tree toutiao_project
toutiao_project
├── scrapy.cfg
└── toutiao_project
    ├── __init__.py
    ├── items.py
    ├── middlewares.py
    ├── pipelines.py
    ├── settings.py
    └── spiders
        ├── __init__.py

4 directories, 7 files
❯ cd toutiao_project

scrapy.cfg是Scrapy的配置文件。item.py存放了一些抓取对象的schema,或者说Item是保存爬取到的数据的容器。 middlewares.py是Spider中间件,使用中间件可以在抓取过程中的对应事件钩子上加上额外的行为, 比如添加代理,自动登录等等。pipelines.py是项目管道,当Item在Spider中被收集之后,它将会被传递到Item Pipeline,会按照一定的顺序执行对Item的处理。常见的如去重,验证item值,将爬取结果保存到数据库中等等。上面说的scrapy.cfg是针对Scrapy框架的配置,settings.py是针对于项目本身的设置,比如用什么中间件、并发数量、UA、Pipelines等等。全部选项可以看官方文档,如果你要深入了解和使用这个框架,这些设置项都是应该了解的。spiders目录下就是具体的抓取代码

定义Item

接着我们把开发者头条的抓取的逻辑迁移到这种项目用法中。定义Item这个结构之前我们已经做过,我觉得无论你使用Scrapy的哪种方式:是一个小脚本去抓,还是一个很复杂的大项目去爬,都建议要有一个良好的schema结构,现在我们只是需要把ToutiaoItem放到items.py文件中

 cat  toutiao_project/items.py
import scrapy


class ToutiaoItem(scrapy.Item):
    title = scrapy.Field()
    link = scrapy.Field()

这种Item很像ORM的用法,对吧。每个字段是一个Field。每种获取的数据都可以写成一个这样的Item类,它要继承scrapy.Item。

添加抓取逻辑

可以在spiders子目录下创建一个文件,从items.py里面import ToutiaoItem。这里要注意,我写全了import模块路径。

 cat spiders/toutiao.py
from datetime import date, timedelta

import scrapy

from toutiao_project.items import ToutiaoItem

today = date.today()


class ToutiaoSpider(scrapy.Spider):
    name = 'toutiao'
    start_urls = [f'https://toutiao.io/prev/{today - timedelta(days=i)}'
                  for i in range(5)]

    def parse(self, response):
        for post in response.xpath('//h3[@class="title"]/a'):
            item = ToutiaoItem()
            item['title'] = post.xpath('@title').extract_first()
            item['link'] = post.xpath('@href').extract_first()
            yield item

代码文件的名字没有要求,关键是类中name的名字。

通过 Item Pipeline 把数据存到数据库

在之前的小例子中,我们只是把数据存在json文件中。这次我们把数据存到数据库,反正本地测试我就存进SQLite,而且由于用到SQLAlchemy,上线时直接改成MySQL或者PostgreSQL的DB_URL就好了。

顺便提一下去重,去重是一个基本的抓取优化,但是对于我们这个需求,feed里面肯定不会有重复的内容,所以就不考虑了

首先我们用SQLAlchemy写一个头条的模型

 cat toutiao_project/models.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from toutiao_project.settings import DB_URL


DeclarativeBase = declarative_base()


def db_connect():
    return create_engine(DB_URL)


class Toutiao(DeclarativeBase):
    __tablename__ = 'toutiao'
    id = Column(Integer, primary_key=True)
    title = Column('title', String)
    link = Column('link', String)


def create_tables(engine):
    DeclarativeBase.metadata.create_all(engine)

除了toutiao这个模型还写了2个功能函数,一个是连接数据库的,一个是创建表的。DB_URL在配置文件中:

❯ less toutiao_project/settings.py
here = os.path.abspath(os.path.dirname(__file__))
DB_URL = f'sqlite:///{here}/toutiao.db'

接着我们看pipelines.py:

 cat toutiao_project/pipelines.py
from sqlalchemy.orm import sessionmaker

from toutiao_project.models import Toutiao, db_connect, create_tables


class ToutiaoPipeline:
    def __init__(self):
        engine = db_connect()
        create_tables(engine)
        self.session = sessionmaker(bind=engine)()

    def process_item(self, item, spider):
        session = self.session
        post = Toutiao(**item)

        try:
            session.add(post)
            session.commit()
        except:
            session.rollback()
            raise
        finally:
            session.close()
        return item

里面使用session的形式,需要在processitem方法内加上添加记录的逻辑。在\_init__里面获得了engine,然后通过engine创建数据库。当然这个创建数据库的步骤可以只做一次,虽然重复执行也不会有什么影响.

接着需要在settings里面指定这个pipeline:

ITEM_PIPELINES = {
    'toutiao_project.pipelines.ToutiaoPipeline': 300,
}

后面那个300表示执行的顺序, 值越小越靠前,下面说的中间件也是:同类中间件中,这个顺序值越小越先执行

项目配置

当然到这里还是不能抓取,是因为开发者头条要求我们使用一个正确的UA。所以要修改settings.py. 改一下USER_AGENT。这个ua是我个人电脑的UA。

❯ grep USER_AGENT toutiao_project/settings.py
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"

设置中间件

接着我们看看中间件怎么用,在Scrapy中有 2 种中间件,

  1. 下载器中间件(Downloader Middleware)
  2. Spider中间件(Middleware)

一个是下载器中间件,在request/response之间。另外一个是Spider中间件,发生在spider处理过程中。我们先看一个下载器中间件的例子。之前我们在配置文件中指定了USER_AGENT, 但是ua只有一个。这里实现随机换一个正确UA的例子。

# pip install fake_useragent
from fake_useragent import UserAgent


class RandomUserAgentMiddleware:
    def process_request(self, request, spider):
        ua = UserAgent()
        request.headers['User-Agent'] = ua.random

其中用到的fake-useragent这个库会下载一个数据文件,可能需要想办法去国外下载下。每次调用会随机拿一个ua,避免了重复用一个。当然大家还可以扩展思路,有代理池的话,每次代理也随机换。

看settings.py里面对应中间件的配置:

DOWNLOADER_MIDDLEWARES = {
    toutiao_project.middleware.RandomUserAgentMiddleware': 543,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None
}

这里注意,其实scrapy里面其实已经实现了一个UserAgent的中间件,这样在settings里面指定USER_AGENT就能让抓取使用对应的ua了。不过既然我们实现了随机ua中间件,自带的那个就可以让他的执行顺序为None,让它不起作用了

Spider Middleware这种我们日常开发基本用不到,不过呢,为了演示我们这里就实现一个用logging模块记录日志的中间件,要不然scarpy默认的抓取过程debug日志很多,抓不到重点,使用这个日志中间件,可以很有针对性的了解抓取情况

import logging

from scrapy.exceptions import NotConfigured

logger = logging.getLogger('Toutiao')
fh = logging.FileHandler('scrapy.log')
fh.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)


class LoggingSpiderMiddleware:
    @classmethod
    def from_crawler(cls, crawler):
        if not crawler.settings.getbool('FILE_LOGGING_ENABLED'):
            raise NotConfigured
        return cls()

    def _msg(self, msg, level=logging.INFO):
        logger._log(level, msg, [])

    def process_spider_output(self, response, result, spider):
        for spider_result in result:
            self._msg(f'Result: {spider_result}')
        return result

    def process_spider_exception(self, response, exception, spider):
        self._msg(exception, logging.ERROR)

这样就可以把抓取的内容写入日志,同时由于我们定制了logging模块的日志格式,日志中会记录时间。基于时间和结果可以有助于未来排查问题。接着我们要改一下settings.py设置

FILE_LOGGING_ENABLED = True
SPIDER_MIDDLEWARES = {
    'toutiao_project.middlewares.LoggingSpiderMiddleware': 401
 }

PS:如果不指定FILE_LOGGING_ENABLED这个中间件是不生效的。

中间件自带的其他方法其实在 startproject 时候创建的 middlewares.py 里面都有了,不了解的可以具体看看API,注意按需使用。

运行抓取

最后呢,我们就可以执行抓取了,在命令行运行:

❯ scrapy crawl toutiao

运行正常。接着来确认下程序正确性。首先看看SQLite中的数据:

❯ sqlite3 toutiao_project/toutiao.db
SQLite version 3.24.0 2018-06-04 14:10:15
Enter ".help" for usage hints.
sqlite> .tables
toutiao
sqlite> select * from toutiao limit 3;
1|[] PyTorch 可视化理解卷积神经网络 - 开发者头条|/k/10o3v8
2|Go slice 的一些使用技巧 - 开发者头条|/k/xs8z3b
3|AI Challenger 2018:细粒度用户评论情感分类冠军思路总结 - 开发者头条|/k/56re9x
❯ less scrapy.log

这样就完成抓取和存入数据库啦。

接着看一下日志scrapy.log,里面已经可以看到对应的记录:

2019-01-26 08:44:06,338 - Toutiao - INFO - Result: {'link': '/k/41yprr', 'title': '再谈源码阅读 - 开发者头条'}
2019-01-26 08:44:06,339 - Toutiao - INFO - Result: {'link': '/k/10o3v8', 'title': '[译] PyTorch 可视化理解卷积神经网络 - 开发者头条'}
2019-01-26 08:44:06,339 - Toutiao - INFO - Result: {'link': '/k/xs8z3b', 'title': 'Go slice 的一些使用技巧 - 开发者头条'}

通过LoggingSpiderMiddleware,抓取结果都被写进了日志。

好啦,这个爬虫项目就完成了,我们相对完整的体验了Scrapy的用法。

什么情况下应该用Scrapy这类框架?

大部分优秀项目能出现的原因,都是作者或者团队在对过去已有的工作模式和轮子深入之后发现问题之后提出更先进的思想,并实现出来。框架非常好,比如作为web开发者,我不能不用flask django这类web框架,而爬虫框架嘛,利用成熟的框架基本能避免常见的坑。可以写非常少的代码就能实现抓取,其中的一些细节都被框架封装了,开发者不再需要关注,专心实现业务逻辑就好了。

不过我是不推荐用框架的,我不做运维的一个重要原因是不想做一个天天翻阅软件文档的运维,运维嘛,日常一个工作是搭建环境,用人家写好的东西,按文档运行起来,最多google一些最佳实践或者解决一些报错。但是我们是不知道它怎么实现的、运行原理是什么的。你用这种框架也是,比如flask, 就是按照人家规定你的玩法填东西就好了,填多了不过是个熟练工而已。

我在12-13年底写了很多的爬虫,每个通常都会尝试一些新的技术,后来我突然意识到,当你掌握了爬虫技能,爬一个还是爬一百个区别已经不大,量已经没有意义,关键是质了。

所以,我的建议是:

  1. 如果你是新学者,没有工作安排的话,我建议从零开始不要用框架。
  2. 如果已经写过可用的爬虫,但是还不能灵活运用,更多的是多了解我上面提到的那些技术,多造轮子。
  3. 如果你已经踩过该踩的坑,灵活运用,用什么都无所谓,造轮子意义不大。这个时候用scrapy是很好的选择。

再强调一次基础。千万别把自己在某些领域的能力限制在某个框架上,会影响你未来的发展。

延伸阅读

Scrapy是一个功能很齐全的抓取框架,支持的特性、配置项等非常多,需要花很多时间学习和熟悉。这里有几个延伸阅读的链接。第一个是Scrapy创始人自己搞的scrapinghub服务中的视频学习教程。应该是市面上最好的教程之一了,大家可以看看。

  1. https://learn.scrapinghub.com/scrapy/
  2. https://doc.scrapy.org/en/latest/intro/tutorial.html

扫描二维码,分享此文章

还没有评论
空空如也