Scrapy Учебник

В этом руководстве мы предполагаем, что Scrapy уже установлен в вашей системе. Если это не так, см. Инструкцию по установке.

Мы собираемся парсить quotes.toscrape.com, веб-сайт, на котором перечислены цитаты известных авторов.

Данный учебник проведёт вас через данные задачи:

  1. Создание нового проекта Scrapy

  2. Написание паука для сканирования сайта и извлечения данных

  3. Экспорт полученных данных с помощью командной строки

  4. Изменение паука для рекурсивного перехода по ссылкам

  5. Использование аргументов паука

Scrapy написан на Python. Если вы новичок в этом языке, возможно, вы захотите начать с понимания того, что это за язык, чтобы получить максимальную отдачу от Scrapy.

Если вы уже знакомы с другими языками и хотите быстро изучить Python, Учебник по Python — хороший ресурс.

Если вы новичок в программировании и хотите начать с Python, вам могут быть полезны следующие книги:

  • Автоматизируйте скучные вещи с помощью Python

  • Как думать как компьютерный ученый

  • Изучите Python 3 трудным путем

Вы также можете взглянуть на список ресурсов Python для не-программистов, а также на предлагаемых ресурсах в Learnpython-subreddit.

Создание проекта

Перед тем, как начать парсинг, вам нужно настроить новый проект 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 использует для сбора информации с веб-сайта (или группы веб-сайтов). Они должны создать подкласс Spider и определить начальные запросы, которые необходимо сделать, необязательно, как переходить по ссылкам на страницах и как анализировать загруженное содержимое страницы для извлечения данных.

Это код нашего первого паука. Сохранить его в файле с именем quotes_spider.py в каталоге tutorial/spiders вашего проекта:

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 = f'quotes-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log(f'Saved file {filename}')

Как видите, наш Spider является подклассом scrapy.Spider и определяет некоторые атрибуты и методы:

  • name: идентифицирует паука. Он должен быть уникальным в рамках проекта, т. е. вы не можете установить одно и то же имя для разных пауков.

  • start_requests(): должен возвращать итерацию запросов (вы можете возвращать список запросов или написать функцию генератора), с которых Spider начнет сканирование. Последующие запросы будут создаваться последовательно из данных начальных запросов.

  • parse(): метод, который будет вызываться для обработки ответа, загруженного для каждого из сделанных запросов. Параметр ответа — это экземпляр TextResponse, который содержит содержимое страницы и имеет дополнительные полезные методы для его обработки.

    Метод parse() обычно анализирует ответ, извлекая данные в виде словарей, а также находя новые URL-адреса и создавая из них новые запросы (Request).

Как запустить нашего паука?

Чтобы заставить нашего паука работать, перейдите в каталог верхнего уровня проекта и запустите:

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.html и quotes-2.html с содержимым для соответствующих URL-адресов, как указывает наш метод parse.

Примечание

Если вам интересно, почему мы ещё не парсили HTML, подождите, мы скоро рассмотрим это.

Что только что произошло под капотом?

Scrapy планирует объекты scrapy.Request, возвращаемые методом start_requests Spider. После получения ответа для каждого из них он создаёт экземпляры объектов Response и вызывает метод обратного вызова, связанный с запросом (в данном случае метод parse), передавая ответ в качестве аргумента.

Ярлык для метода start_requests

Вместо реализации метода start_requests(), который генерирует объекты scrapy.Request из URL-адресов, вы можете просто определить атрибут класса 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 = f'quotes-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)

Метод parse() будет вызываться для обработки каждого запроса для данных URL-адресов, даже если мы явно не сказали Scrapy сделать это. Это происходит потому, что parse() — это метод обратного вызова по умолчанию для Scrapy, который вызывается для запросов без явно назначенного обратного вызова.

Извлечение данных

Лучший способ научиться извлекать данные с помощью Scrapy — поиграть с селекторами в Scrapy оболочке. Запуск:

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

Примечание

Не забывайте всегда заключать URL-адреса в кавычки при запуске оболочки Scrapy из командной строки, иначе URL-адреса, содержащие аргументы (например, символ &), не будут работать.

В Windows используйте вместо этого двойные кавычки:

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

Вы увидите что-то вроде:

[ ... 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='<title>Quotes to Scrape</title>'>]

Результатом выполнения response.css('title') является объект в виде списка с именем SelectorList, который представляет собой список объектов Selector, которые охватывают элементы XML/HTML и позволяют выполнять дополнительные запросы для уточнения выбора или извлечения данных.

Вы можете извлечь текст из заголовка выше:

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

Здесь следует отметить два момента: во-первых, мы добавили ::text в запрос CSS, что означает, что мы хотим выбирать только текстовые элементы непосредственно внутри элемента <title>. Если мы не укажем ::text, мы получим полный элемент заголовка, включая его теги:

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

Другое дело, что результатом вызова .getall() является список: возможно, что селектор возвращает более одного результата, поэтому мы извлекаем их все. Когда вы знаете, что хотите только первый результат, как в этом случае, вы можете выполнить:

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

В качестве альтернативы вы могли бы написать:

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

Доступ к индексу в экземпляре SelectorList вызовет исключение IndexError, если результатов нет:

>>> response.css('noelement')[0].get()
Traceback (most recent call last):
...
IndexError: list index out of range

Вместо этого вы можете использовать .get() непосредственно в экземпляре SelectorList, который возвращает None, если результатов нет:

>>> response.css("noelement").get()

Здесь есть урок: для большей части кода парсинга вам нужно, чтобы он был устойчивым к ошибкам из-за того, что не было найдено на странице, чтобы даже если некоторые части не удалось получить, вы могли, по крайней мере, получить немного данных.

Помимо методов getall() и get(), вы также можете использовать метод re() для извлечения с помощью регулярных выражений:

>>> 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 и придумать селектор (см. Использование инструментов разработчика вашего браузера для парсинга).

Selector Gadget — также хороший инструмент для быстрого поиска CSS-селектора для визуально выбранных элементов, который работает во многих браузерах.

XPath: краткое знакомство

Помимо CSS, селекторы Scrapy также поддерживают использование выражений XPath:

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

Выражения XPath очень эффективны и составляют основу селекторов Scrapy. Фактически, селекторы CSS «под капотом» преобразуются в XPath. Вы можете увидеть это, если внимательно прочитаете текстовое представление объектов селектора в оболочке.

Хотя выражения XPath, возможно, не так популярны, как селекторы CSS, они предлагают больше возможностей, потому что, помимо навигации по структуре, они также могут просматривать содержимое. Используя XPath, вы можете выбирать такие вещи, как: выберать ссылку, содержащую текст «Следующая страница». Это делает XPath очень подходящим для задачи получения данных с сайта, и мы рекомендуем вам изучить XPath, даже если вы уже знаете, как создавать селекторы CSS, это значительно упростит скраппинг.

Мы не будем здесь подробно рассматривать XPath, но вы можете узнать больше о использовании XPath с селекторами Scrapy здесь.

Извлечение цитат и авторов

Теперь, когда вы немного знаете о выборе и извлечении, давайте завершим работу нашего паука, написав код для извлечения цитат с веб-страницы.

Каждая цитата на http://quotes.toscrape.com представлена элементами 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 Einstein</small>
        <a href="/author/Albert-Einstein">(about)</a>
    </span>
    <div class="tags">
        Tags:
        <a class="tag" href="/tag/change/page/1/">change</a>
        <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
        <a class="tag" href="/tag/thinking/page/1/">thinking</a>
        <a class="tag" href="/tag/world/page/1/">world</a>
    </div>
</div>

Давайте откроем оболочку scrapy и немного поиграем, чтобы узнать, как извлечь нужные данные:

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

Мы получаем список селекторов для HTML-элементов цитаты с:

>>> response.css("div.quote")
[<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 ...]

Каждый из селекторов, возвращаемых приведенным выше запросом, позволяет нам выполнять дальнейшие запросы по их подэлементам. Давайте назначим первый селектор переменной, чтобы мы могли запускать наши селекторы CSS непосредственно на определенной цитате:

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

Теперь давайте извлечём text, author и tags из этой цитаты, используя только что созданный объект quote:

>>> text = quote.css("span.text::text").get()
>>> text
'“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))
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']}
...

Извлечение данных в нашем пауке

Вернёмся к нашему пауку. До сих пор он не извлекал никаких данных, а просто сохранял всю HTML-страницу в локальный файл. Давайте интегрируем приведённую выше логику извлечения в нашего паука.

Паук Scrapy обычно генерирует множество словарей, содержащих данные, извлеченные со страницы. Для этого мы используем ключевой аргумент Python yield в обратном вызове, как вы можете видеть ниже:

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.”"}

Хранение полученных данных

Самый простой способ сохранить полученные данные — использовать Экспорты фидов следующей командой:

scrapy crawl quotes -O quotes.json

Эта инструкция сгенерирует файл quotes.json, содержащий все полученные элементы, сериализованные в JSON.

Параметр командной строки -O перезаписывает любой существующий файл; вместо него используйте -o, чтобы добавить новое содержимое к любому существующему файлу. Однако добавление к файлу JSON делает его содержимое недействительным JSON. При добавлении в файл рассмотрите возможность использования другого формата сериализации, например JSON Строки:

scrapy crawl quotes -o quotes.jl

Формат JSON Строк полезен, потому что он похож на поток, вы можете легко добавлять в него новые записи. У него нет той же проблемы, что и JSON, когда вы запускаете дважды. Кроме того, поскольку каждая запись представляет собой отдельную строку, вы можете обрабатывать большие файлы, не помещая все в память, есть такие инструменты, как JQ, которые помогают сделать это из командной строки.

В небольших проектах (например, в этом руководстве) этого должно быть достаточно. Однако, если вы хотите выполнять более сложные операции с полученными элементами, вы можете написать Конвейер элементов. Файл-заполнитель для конвейеров элементов был настроен для вас при создании проекта в tutorial/pipelines.py. Хотя вам не нужно реализовывать какие-либо конвейеры элементов, если вы просто хотите хранить полученные элементы.

Использование аргументов паука

Вы можете предоставить своим паукам аргументы командной строки, используя параметр -a при их запуске:

scrapy crawl quotes -O quotes-humor.json -a tag=humor

Данные аргументы передаются методу паука __init__ и по умолчанию становятся атрибутами паука.

В этом примере значение, указанное для аргумента tag, будет доступно через self.tag. Вы можете использовать его, чтобы ваш паук выбирал только цитаты с определённым тегом, создавая URL-адрес на основе аргумента:

import scrapy


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

    def start_requests(self):
        url = 'http://quotes.toscrape.com/'
        tag = getattr(self, 'tag', None)
        if tag is not None:
            url = url + 'tag/' + tag
        yield scrapy.Request(url, self.parse)

    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(),
            }

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

Если вы передадите этому пауку аргумент tag=humor, вы заметите, что он будет посещать только URL-адреса из тега humor, например http://quotes.toscrape.com/tag/humor.

Можно узнайте больше об обработке аргументов паука здесь.

Следующие шаги

В этом руководстве были рассмотрены только основы Scrapy, но есть много других функций, не упомянутых здесь. Обратитесь к разделу Что ещё? в главе Краткий обзор Scrapy для быстрого обзора наиболее важных из них.

Вы можете продолжить с раздела Базовые концепты, чтобы узнать больше об инструменте командной строки, пауках, селекторах и других вещах, которые не рассматривались в руководстве, например, моделировании сканированных данных. Если вы предпочитаете поиграть с примером проекта, посмотрите раздел Примеры.