scrapy1.6中文文档第 1 天

scrapy1.6中文文档第 1 天


这是一篇什么文章

文章内容概述

该一系列文章将记录我根据scrapy官方文档零基础scrapy学习的过程,其中主要包括了文档的翻译及阅读心得,还可能包含一些常见问题的解决方案。

开发环境

系统 python解释器
manjaro 18.0.4 x64 + DDE Cpython 3.7.3

什么是scrapy

Scrapy
An open source and collaborative framework for extracting the data you need from websites.
In a fast, simple, yet extensible way.

官网首页的介绍描述scrapy为一个可以快速/简单/可拓展地提取所需web网站数据的开源协作框架。

Start with scrapy

第一个栗子(可忽略)

pip install scrapy
cat > myspider.py <<EOF
import scrapy

class BlogSpider(scrapy.Spider):
    name = 'blogspider'
    start_urls = ['https://blog.scrapinghub.com']

    def parse(self, response):
        for title in response.css('.post-header>h2'):
            yield {'title': title.css('a ::text').get()}

        for next_page in response.css('a.next-posts-link'):
            yield response.follow(next_page, self.parse)
EOF
scrapy runspider myspider.py

创建项目(使用pycharm + pipenv)

文档阅读

文档阅读中的标题与官方文档标题相对应


1. 第一步

一句话简述scrapy(概述scrapy)

Even though Scrapy was originally designed for web scraping, it can also be used to extract data using APIs (such as Amazon Associates Web Services) or as a general purpose web crawler.

尽管scrapy原本是为爬取web页面而设计,但是他也可以用来通过API提取数据或当作一个一般用途的web爬虫。

实践一个简单的爬虫示例

此处文档提供了一个爬取http://quotes.toscrape.com简单的scrapy爬虫示例。

import scrapy


class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    start_urls = [
        'http://quotes.toscrape.com/tag/humor/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.xpath('span/small/text()').get(),
            }

        next_page = response.css('li.next a::attr("href")').get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

将上面的代码放到一个文本文件中,并重命名为xxx.py比如quotes_spider.py然后使用scrapy runspider命令运行这个脚本。

scrapy runspider quotes_spider.py -o quotes.json

当脚本执行完,你就会在quotes.json中得到许多以json形式组织的引用名句(未调整格式的,可以用pycharm打开后按crtl + shift + alt + L重新调整格式以提高可读性)。

刚刚发生了什么呢?

当你运行命令scrapy runspider quotes_spider.py时,Scrapy在该脚本文件中寻找爬虫的定义并且通过Scrapy它自己的爬虫引擎运行爬虫。

爬虫开始时先是向start_urls定属性中定义的地址们发送请求,然后调用默认的回调方法parse,并将请求的response body对象作为一个参数传递给毁掉方法。在parse回调中,我们使用CSS选择器循环选择每一个引用句元素,然后暴露(yield)一个包含这句子文本和作者信息的python dict对象(python中对哈西表的实现),然后寻找下一页的链接并且调度下一个请求(下面的请求使用同样的parse方法作为回调函数)。

现在你应该已经发现了scrapy的一个最主要的优点:请求都是有计划的并且是异步执行的(后面会具体说明)。
这意味着Scrapy不需要阻塞等待一个请求结束并被处理,在等待时它可以发送另一个请求或做一些其他事情。这也意味着即使这个请求失败了或处理时出现了错误,其他请求也会继续。

这可以让你很快速地进行爬取。scrapy也提供了一些配置,让你不仅仅是表面控制,而是完全掌控。
你可以设置很多东西,比如在两次请求之间设置间隔,限制每个域名或每个ip的并发请求量,甚至可以使用一个自动限速拓展来自动完成这些配置。

关于修改导出数据的格式

This is using feed exports to generate the JSON file, you can easily change the export format (XML or CSV, for example) or the storage backend (FTP or Amazon S3, for example). You can also write an item pipeline to store the items in a database.

安装向导

安装scrapy(略)
你最好知道的事

scrapy是直接纯python编写的(但间接依赖其他非python包),依赖几个关键的python库。

  • lxml,
  • parsel, an HTML/XML data extraction library written on top of lxml,
  • w3lib, a multi-purpose helper for dealing with URLs and web page encodings
  • twisted, an asynchronous networking framework
  • cryptography and pyOpenSSL, to deal with various network-level security needs

scrapy依赖测试过的最小版本为:

  • Twisted 14.0
  • lxml 3.4
  • pyOpenSSL 0.14

scrapy可能也能与更老版本依赖包配合工作,但是我们不能保证它一直会正常工作,因为没有测试过。

这些依赖包他们本身可能依赖一些需要额外安装步骤的非python包,你可以查看指定平台的安装向导

为了避免这些依赖包产生各种问题,请查看他们各自的安装指导。

  • lxml 安装指南
  • cryptography安装指南

各平台安装具体步骤及问题解答略


2. Scrapy教程

在这个教程中,我假设你已经安装好了scrapy。

我们将会去爬quotes.toscrape.com——一个列举了很多名人名言的网站。

这个教程将会带你完成下面几个任务:

  1. 创建一个项目
  2. 写一个爬虫脚本去爬取网站并提取数据
  3. 使用命令行将爬取到的数据导出
  4. 将爬虫改写为递归寻找链接
  5. 使用爬虫参数

创建一个项目

在你开始爬取之前,你应该先创建好一个Scrapy项目,进入你想要存放你的代码的文件夹,然后运行

scrapy startproject tutorial

这就会创建一个tutorial文件夹,文件夹中包含下述内容:

tutorial/
    scrapy.cfg            # deploy configuration file

    tutorial/             # project's Python module, you'll import your code from here
        __init__.py

        items.py          # project items definition file

        middlewares.py    # project middlewares file

        pipelines.py      # project pipelines file

        settings.py       # project settings file

        spiders/          # a directory where you'll later put your spiders
            __init__.py

我们的第一个爬虫

爬虫就是你定义的那些类,也是Scrapy用来从网站(或者是一组网站)爬取数据的东西。他们必须是scrapy.Spider的子类,并且必须定义初始请求,可以选择定义如何寻找下一个链接,也可以选择定义如何解析下载下来的网页内容来提取数据。

下面是我们的第一个爬虫,将它保存在tutorial/spiders目录下一个叫quotes_spider.py的文件中。

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)

正如你所见,我们的爬虫子类中定义了一些属性和方法

  • name爬虫的id,它在一个项目里必须是独一无二的,就意味着你不能给两个爬虫设置相同的名字。
  • start_request()必须返回一个可迭代请求对象,爬虫将会从此开始爬取。
  • parse()一个将会在每个请求结束后被调用来处理返回内容的方法。response参数是一个TextResponse的实例,它包含了页面的内容,并且包含一些有用的方法。
如何运行我们的爬虫呢?

为了让我们的爬虫工作,到项目的根目录运行

scrapy crawl quotes

这个命令就爱你工会运行名字叫quotes的爬虫,这个爬虫将会向quotes.toscrape.com域名发送请求,你会获取像下面这样的输出

... (omitted for brevity)
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Spider opened
2016-12-16 21:24:05 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-12-16 21:24:05 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-1.html
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-2.html
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Closing spider (finished)
...

现在检查当前目录中的文件,你应该发现有两个新文件被创建了,quotes-1.htmlquotes-2.html,其内容就是每个请求url的内容。

如果你想知道,为什么我们还不开始解析HTML,别着急,我们马上就会说到。

刚刚究竟发生了什么呢

Scrapy调度爬虫类中定义的start_request方法返回的scrapy.Request,当收到响应(response)时,它会实例化一个Response对象并且调用与请求相关连的回调方法,将response作为一个参数。

start_request的一种简写

除了实现一个返回scrapy.Request对象的start_requests()方法,你也可以就仅仅定义一个start_urls类属性,包含一系列的URL。这个列表将会被作为start_requests()的默认实现来为你的爬虫创建初始请求。

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
提取数据

学习如何使用scrapy提取数据的最好方法是在Scrapy命令行中尝试选择器(网页中所有的内容都是由一个一个元素标签组成,选择器可以通过元素标签的属性如标签名,类名,id等属性选择指定的一类标签),运行:

scrapy shell 'http://quotes.toscrape.com/page/1/'

记得一定要闭合链接的引号,windows上要把单引号改成双引号

执行命令你将会看到想这样的一些东西

[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x7fa91d888c90>
[s]   item       {}
[s]   request    <GET http://quotes.toscrape.com/page/1/>
[s]   response   <200 http://quotes.toscrape.com/page/1/>
[s]   settings   <scrapy.settings.Settings object at 0x7fa91d888c10>
[s]   spider     <DefaultSpider 'default' at 0x7fa91c8af990>
[s] Useful shortcuts:
[s]   shelp()           Shell help (print this help)
[s]   fetch(req_or_url) Fetch request (or URL) and update local objects
[s]   view(response)    View response in a browser
>>>

使用命令行你可以尝试使用CSS选择其选择元素。

>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='Quotes to Scrape'>]

该命令的运行结果是一个像列表一样的对象SelectorList,它表示一个Selector对象的列表,其中Selector对象被XML/HTML元素包裹,允许你进行更深度的查询来精准获取选项或提取数据。

你可以通过如下操作提取所有的标题title:

>>> response.css('title::text').getall()
['Quotes to Scrape']

有两件事要注意:一个是我们在css查询中添加了::text,意思是我们想仅仅选择元素内的文字部分,如果我们不指定::text我们将会获得完整的标题元素,包含他的标签。

>>> response.css('title').getall()
['Quotes to Scrape']

另一件事是调用.getall()的结果是一个列表,可能会返回不止一个结果,所以我们要提取全部返回结果。但你知道你只要第一个结果的时候,你可以使用.get()

>>> response.css('title::text').get()
'Quotes to Scrape'

或者你也可以这样写

>>> response.css('title::text')[0].get()
'Quotes to Scrape'

然而如果没有匹配到元素的时候第一种写法会返回None可以避免出现IndexError

有一个教训:对于大多数爬虫代码,你希望它在找不到东西时可以保持高容错性,所以即使一部分爬虫失败了,你还是至少能获取到一些数据的。

除了getall()get()方法,你可以都使用正则表达式方法来提取

>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']

为了找到合适的css选择器,你可能需要通过view(response)命令把返回的页面在浏览器中打开,你可以使用浏览器的开发者工具来查看html并找到一个选择器。

XPath: 一个简短的介绍

除了Css选择器,Scrapy选择器还支持使用XPath拓展。

>>> response.xpath('//title')
[]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'

XPath不仅能根据结构选择,还可以根据内容欧冠选择,十分强大。
在Scrapy中使用XPath选择器
详细教程

提取名言和作者

现在你已经对选择和提取有一点了解了,让我们来完成我们的爬虫吧。

每一个名言都在HTML元素中被呈现,看起来都像这样:

<div class="quote">
    <span class="text">“The world as we have created it is a process of our
    thinking. It cannot be changed without changing our thinking.”span>
    <span>
        by <small class="author">Albert Einsteinsmall>
        <a href="/author/Albert-Einstein">(about)a>
    span>
    <div class="tags">
        Tags:
        <a class="tag" href="/tag/change/page/1/">changea>
        <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughtsa>
        <a class="tag" href="/tag/thinking/page/1/">thinkinga>
        <a class="tag" href="/tag/world/page/1/">worlda>
    div>
div>

现在让我们打开scrapy的命令行工具来试一下如何提取我们所需的数据吧:

$ scrapy shell 'http://quotes.toscrape.com'

我们可以通过下面这条命令得到一系列的包含引用名言的HTML元素:

>>> response.css("div.quote")

上述查询返回的没一个选项都允许我们在他们的子元素中做更深度的查询
现在让我们吧刚刚的地一个选择复制到一个变量,这样我们就可以在这一个包含引用名言的元素上直接调试css选择器了。

>>> quote = response.css("div.quote")[0]

现在呢,让我们利用quote对象从刚刚这个元素中提取出tittle,authortags

>>> title = quote.css("span.text::text").get()
>>> title
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").get()
>>> author
'Albert Einstein'

假定所有的标签是一个字符串的列表,我们可以使用.getall()方法取出全部标签。

>>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']

搞清楚了如何提取每一位后,我们现在可以便利全部提取出来的元素并且把它们都放进一个python的字典型变量中。

>>> for quote in response.css("div.quote"):
...     text = quote.css("span.text::text").get()
...     author = quote.css("small.author::text").get()
...     tags = quote.css("div.tags a.tag::text").getall()
...     print(dict(text=text, author=author, tags=tags))
{'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'}
{'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'}
    ... a few more of these, omitted for brevity
>>>
直接在我们的爬虫中提取数据

让我们回到我们的爬虫,直到现在,我们还没有针对性地提取任何数据,只是将整个HTML页面保存到了本地文件。现在让我们把上面的提取逻辑合并到我们的爬虫里吧!

一个Scrapy 爬虫通常会生成很多包含被提取数据的字典。为了做到这一点,我们在回调函数中使用yield这个python关键字,如你在下面所看到的:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

如果你运行这个爬虫,他就会在日志中输出提取出来的数据。

2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}
给爬到的数据排序

保存爬到的数据最简单的方法就是使用Feed exports,你可以通过如下命令实现:

scrapy crawl quotes -o quotes.json

这个命令就会生成一个quotes.json文件,其中包含了所有被爬取到的东西,并且内容已经被序列化为JSON(JavaScript Object Notation, JS 对象简谱)

因为一些历史原因,Scrapy会在制定文件后面追加内容而不会覆盖,如果运行了两遍这个命令并且没有删除之前的文件,你就会得到一个格式不正确的json。

你也可以使用其他格式,就像JSON LINES

scrapy crawl quotes -o quotes.jl

JSON LINES 格式是很有用的,因为它和流很像(每一行都是有效的JSON值),你可以很轻松地在后面追加新的记录。而且如果文件很大你不必一次性全部加载到内存。

在小项目里,比如教程中的这个,json已经够用了。然而你想使用爬到的数据做更复杂的事情,你可以写一个Item Pipeline.你创建项目时已经自动创建了这个空文件tutorial/piplines.py。如果你只是想保存一下爬到的东西你就不用实现了。

跟踪链接(Following links)

可能说你不仅想从前面一两页爬取东西,你想从这个网站的所有页面爬取名言引用。
现在你已经知道如何从页面中提取数据了,那让我们来看看如何跟踪链接。

第一件事就是要提取出我们要跟随(follow)的页面的链接。检查我们的页面,我们可以看到有一个带有下述标志的链接指向下一个页面

<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">span>a>
    li>
ul>

我们可以在shell中尝试提取它

>>> response.css('li.next a').get()
'Next '

这样我们获取了另一个元素,但是我们想要属性href。为了做到这一点,scrapy支持CSS选择器拓展,这样我们就可以像下面这样选择属性内容:

>>> response.css('li.next a::attr(href)').get()
'/page/2/'

也可以用attribSelecting element attributes

>>> response.css('li.next a').attrib['href']
'/page/2'

现在我们发现我们的爬虫会不停递归跟踪下一个链接并从页面中提取数据。

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)
创建请求的简写

创建请求对象的一种简写是使用response.follow

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('span small::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)

你可能感兴趣的:(scrapy文档学习)