Пауки

Пауки — это классы, которые определяют, как будет сканироваться определенный сайт (или группа сайтов), в том числе как выполнять сканирование (т. е. переходить по ссылкам) и как извлекать структурированные данные со своих страниц (т. е. извлекать элементы). Другими словами, пауки — это место, где вы определяете настраиваемое поведение для сканирования и анализа страниц для определённого сайта (или, в некоторых случаях, группы сайтов).

У пауков цикл сканирования проходит примерно так:

  1. Вы начинаете с создания начальных запросов для сканирования первых URL-адресов и указываете функцию обратного вызова, которая будет вызываться с ответом, загруженным из данных запросов.

    Первые запросы для выполнения получаются путём вызова метода start_requests(), который (по умолчанию) генерирует Request для URL-адресов, указанных в методах start_urls и parse в качестве функции обратного вызова для запросов.

  2. В функции обратного вызова вы анализируете ответ (веб-страницу) и возвращаете объекты элементов, Request или итерацию данных объектов. Данные запросы также будут содержать обратный вызов (возможно, такой же), а затем будут загружены Scrapy, а затем их ответ будет обработан указанным обратным вызовом.

  3. В функциях обратного вызова вы анализируете содержимое страницы, обычно используя Селекторы (но вы также можете использовать BeautifulSoup, lxml или любой другой механизм, который вам нравится) и генерируете элементы с проанализированными данными.

  4. Наконец, элементы, возвращаемые пауком, обычно сохраняются в базе данных (в некоторых случаях Конвейер элементов) или записываются в файл с помощью Экспорта фидов.

Несмотря на то, что данный цикл применяется (более или менее) к любому виду пауков, существуют различные типы пауков по умолчанию, объединённые в Scrapy для разных целей. Мы поговорим об данных типах далее.

scrapy.Spider

class scrapy.spiders.Spider
class scrapy.Spider

Это простейший паук, от которого должен унаследоваться любой другой паук (включая пауков, поставляемых в комплекте с Scrapy, а также пауков, которых вы пишете сами). Никаких особых функций он не предоставляет. Он просто предоставляет реализацию по умолчанию start_requests(), которая отправляет запросы от атрибута паука start_urls и вызывает метод паука parse для каждого из полученных ответов.

name

Строка, определяющая имя этого паука. Имя паука — это то, как паук находится (и создаётся) Scrapy, поэтому он должен быть уникальным. Однако ничто не мешает вам создать более одного экземпляра одного и того же паука. Это самый важный атрибут паука и он обязателен.

Если паук сканирует один домен, обычная практика состоит в том, чтобы назвать паука в честь домена, с TLD или без него. Так, например, паук, который ползает по mywebsite.com, часто будет называться mywebsite.

allowed_domains

Необязательный список строк, содержащих домены, которые данный паук может сканировать. Запросы URL-адресов, не принадлежащих указанным в этом списке доменным именам (или их поддоменам), не будут выполняться, если включён OffsiteMiddleware.

Допустим, ваш целевой URL-адрес — https://www.example.com/1.html, затем добавить в список 'example.com'.

start_urls

Список URL-адресов, с которых паук начнёт сканирование, если не указаны конкретные URL-адреса. Итак, первыми загруженными страницами будут те, которые перечислены далее. Последующий Request будет генерироваться последовательно из данных, содержащихся в начальных URL.

custom_settings

Словарь настроек, которые будут переопределены из конфигурации всего проекта при запуске этого паука. Он должен быть определён как атрибут класса, поскольку настройки обновляются перед созданием экземпляра.

Список доступных встроенных настроек см .: Справочник по встроенным настройкам.

crawler

Данный атрибут устанавливается методом класса from_crawler() после инициализации класса и связывается с объектом Crawler, к которому привязан данный экземпляр паука.

Парсеры инкапсулируют в проект множество компонентов для единого доступа (например, расширения, промежуточное ПО, менеджеры сигналов и т. д.). См. Crawler API, чтобы узнать о них больше.

settings

Конфигурация для запуска этого паука. Это экземпляр Settings, подробное введение по этому вопросу см. в теме Настройки.

logger

Логгер Python, созданный с помощью Spider name. Вы можете использовать его для отправки сообщений журнала через него, как приведено на Журналирование от пауков.

state

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

from_crawler(crawler, *args, **kwargs)

Метод класса используется Scrapy для создания ваших пауков.

Вероятно, вам не потребуется переопределять это напрямую, потому что реализация по умолчанию действует как прокси для метода __init__(), вызывая его с заданными аргументами args и именованными аргументами kwargs.

Тем не менее, данный метод устанавливает атрибуты crawler и settings в новом экземпляре, чтобы к ним можно было получить доступ позже внутри кода паука.

Параметры
  • crawler (Crawler instance) – краулер, к которому будет привязан паук

  • args (list) – аргументы, переданные методу __init__()

  • kwargs (dict) – ключевые аргументы, переданные методу __init__()

start_requests()

Данный метод должен возвращать итерацию с первыми запросами на сканирование для этого паука. Он вызывается Scrapy, когда паук открывается для сканирования. Scrapy вызывает его только один раз, поэтому можно безопасно реализовать start_requests() в качестве генератора.

Реализация по умолчанию генерирует Request(url, dont_filter=True) для каждого URL-адреса в start_urls.

Если вы хотите изменить запросы, используемые для запуска сканирования домена, данный метод следует переопределить. Например, если вам нужно начать с входа в систему с помощью запроса POST, вы можете это сделать:

class MySpider(scrapy.Spider):
    name = 'myspider'

    def start_requests(self):
        return [scrapy.FormRequest("http://www.example.com/login",
                                   formdata={'user': 'john', 'pass': 'secret'},
                                   callback=self.logged_in)]

    def logged_in(self, response):
        # here you would extract links to follow and return Requests for
        # each of them, with another callback
        pass
parse(response)

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

Метод parse отвечает за обработку ответа и возврат извлеченных данных и/или дополнительных URL-адресов. Другие обратные вызовы запросов имеют те же требования, что и класс Spider.

Данный метод, как и любой другой обратный вызов Request, должен возвращать итерацию Request и/или объекты элементов.

Параметры

response (Response) – ответ на парсинг

log(message[, level, component])

Оболочка, которая отправляет сообщение журнала через logger Spider, сохраненная для обратной совместимости. Для получения дополнительной информации см. Журналирование от пауков.

closed(reason)

Вызывается, когда паук закрывается. Данный метод обеспечивает ярлык для signal.connect() для сигнала spider_closed.

Посмотрим на пример:

import scrapy


class MySpider(scrapy.Spider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = [
        'http://www.example.com/1.html',
        'http://www.example.com/2.html',
        'http://www.example.com/3.html',
    ]

    def parse(self, response):
        self.logger.info('A response from %s just arrived!', response.url)

Возврат нескольких запросов и элементов из одного обратного вызова:

import scrapy

class MySpider(scrapy.Spider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = [
        'http://www.example.com/1.html',
        'http://www.example.com/2.html',
        'http://www.example.com/3.html',
    ]

    def parse(self, response):
        for h3 in response.xpath('//h3').getall():
            yield {"title": h3}

        for href in response.xpath('//a/@href').getall():
            yield scrapy.Request(response.urljoin(href), self.parse)

Вместо start_urls можно напрямую использовать start_requests(); чтобы придать данным больше структуры, вы можете использовать объекты Item:

import scrapy
from myproject.items import MyItem

class MySpider(scrapy.Spider):
    name = 'example.com'
    allowed_domains = ['example.com']

    def start_requests(self):
        yield scrapy.Request('http://www.example.com/1.html', self.parse)
        yield scrapy.Request('http://www.example.com/2.html', self.parse)
        yield scrapy.Request('http://www.example.com/3.html', self.parse)

    def parse(self, response):
        for h3 in response.xpath('//h3').getall():
            yield MyItem(title=h3)

        for href in response.xpath('//a/@href').getall():
            yield scrapy.Request(response.urljoin(href), self.parse)

Аргументы паука

Пауки могут получать аргументы, изменяющие их поведение. Некоторые распространенные варианты использования аргументов паука — определение начальных URL-адресов или ограничение сканирования определенными разделами сайта, но их можно использовать для настройки любых функций паука.

Аргументы паука передаются через команду crawl с использованием параметра -a. Например:

scrapy crawl myspider -a category=electronics

Пауки могут получить доступ к аргументам в своих методах __init__:

import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    def __init__(self, category=None, *args, **kwargs):
        super(MySpider, self).__init__(*args, **kwargs)
        self.start_urls = [f'http://www.example.com/categories/{category}']
        # ...

По умолчанию метод __init__ принимает любые аргументы паука и копирует их в паук как атрибуты. Приведённый выше пример также можно записать следующим образом:

import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    def start_requests(self):
        yield scrapy.Request(f'http://www.example.com/categories/{self.category}')

Если вы запускаете Scrapy из скрипта, вы можете указать аргументы паука при вызове CrawlerProcess.crawl или CrawlerRunner.crawl:

process = CrawlerProcess()
process.crawl(MySpider, category="electronics")

Имейте в виду, что аргументы паука — это всего лишь строки. Паук не будет выполнять парсинг сам по себе. Если бы вам нужно было установить атрибут start_urls из командной строки, вам пришлось бы самостоятельно проанализировать его в список, используя что-то вроде ast.literal_eval() или json.loads(), а затем установить его как атрибут. В противном случае вы вызовете итерацию строки start_urls (очень распространенная ошибка Python), в результате чего каждый символ будет отображаться как отдельный URL-адрес.

Допустимый вариант использования — установить учётные данные http-аутентификации, используемые HttpAuthMiddleware или пользовательским агентом, используемым UserAgentMiddleware:

scrapy crawl myspider -a http_user=myuser -a http_pass=mypassword -a user_agent=mybot

Аргументы паука также можно передавать через API Scrapyd schedule.json. См. документацию по Scrapyd.

Универсальные пауки

Scrapy поставляется с некоторыми полезными универсальными пауками, которые вы можете использовать для создания подклассов своих пауков. Их цель — предоставить удобные функции для нескольких распространенных случаев парсинга, таких как переход по всем ссылкам на сайте на основе определенных правил, сканирование с Sitemaps.xml или парсинг фида XML/CSV.

Для примеров, используемых в следующих пауках, мы предполагаем, что у вас есть проект с TestItem, объявленным в модуле myproject.items:

import scrapy

class TestItem(scrapy.Item):
    id = scrapy.Field()
    name = scrapy.Field()
    description = scrapy.Field()

CrawlSpider

class scrapy.spiders.CrawlSpider

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

Помимо атрибутов, унаследованных от Spider (которые вы должны указать), данный класс поддерживает новый атрибут:

rules

Это список из одного (или нескольких) объектов Rule. Каждый Rule определяет определенное поведение при сканировании сайта. Объекты правил описаны ниже. Если несколько правил соответствуют одной и той же ссылке, будет использоваться первое в соответствии с порядком, в котором они определены в этом атрибуте.

Данный паук также предоставляет замещаемый метод:

parse_start_url(response, **kwargs)

Данный метод вызывается для каждого ответа, созданного для URL-адресов в атрибуте start_urls паука. Он позволяет анализировать начальные ответы и должен возвращать либо объект элемента, либо объект Request, либо итерацию, содержащую любой из них.

Правила сканирования

Пример CrawlSpider

Давайте теперь посмотрим на пример CrawlSpider с правилами:

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com']

    rules = (
        # Extract links matching 'category.php' (but not matching 'subsection.php')
        # and follow links from them (since no callback means follow=True by default).
        Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),

        # Extract links matching 'item.php' and parse them with the spider's method parse_item
        Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'),
    )

    def parse_item(self, response):
        self.logger.info('Hi, this is an item page! %s', response.url)
        item = scrapy.Item()
        item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
        item['name'] = response.xpath('//td[@id="item_name"]/text()').get()
        item['description'] = response.xpath('//td[@id="item_description"]/text()').get()
        item['link_text'] = response.meta['link_text']
        url = response.xpath('//td[@id="additional_data"]/@href').get()
        return response.follow(url, self.parse_additional_page, cb_kwargs=dict(item=item))

    def parse_additional_page(self, response, item):
        item['additional_data'] = response.xpath('//p[@id="additional_data"]/text()').get()
        return item

Данный паук начнет сканировать домашнюю страницу example.com, собирая ссылки на категории и ссылки на элементы, анализируя последние с помощью метода parse_item. Для каждого ответа элемента некоторые данные будут извлечены из HTML с помощью XPath, и они будут заполнены Item.

XMLFeedSpider

class scrapy.spiders.XMLFeedSpider

XMLFeedSpider разработан для синтаксического анализа XML-фидов путём их итерации по определенному имени узла. Итератор можно выбрать из: iternodes, xml и html. Рекомендуется использовать итератор iternodes по соображениям производительности, поскольку итераторы xml и html генерируют сразу всю DOM для её анализа. Однако использование html в качестве итератора может быть полезно при парсинге XML с плохой разметкой.

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

iterator

Строка, определяющая используемый итератор. Это может быть и то, и другое:

  • 'iternodes' — быстрый итератор на основе регулярных выражений

  • 'html' — итератор, использующий Selector. Имейте в виду, что при этом используется анализ DOM и необходимо загружать всю DOM в память, что может быть проблемой для больших фидов

  • 'xml' — итератор, использующий Selector. Имейте в виду, что при этом используется анализ DOM и необходимо загружать всю DOM в память, что может быть проблемой для больших фидов

По умолчанию: 'iternodes'.

itertag

Строка с именем узла (или элемента) для итерации. Пример:

itertag = 'product'
namespaces

Список кортежей (prefix, uri), которые определяют пространства имён, доступные в этом документе, которые будут обрабатываться этим пауком. prefix и uri будут использоваться для автоматической регистрации пространств имён с помощью метода register_namespace().

Затем вы можете указать узлы с пространствами имён в атрибуте itertag.

Пример:

class YourSpider(XMLFeedSpider):

    namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
    itertag = 'n:url'
    # ...

Помимо данных новых атрибутов, у этого паука также есть следующие переопределяемые методы:

adapt_response(response)

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

parse_node(response, selector)

Данный метод вызывается для узлов, соответствующих указанному имени тега (itertag). Получает ответ и Selector для каждого узла. Переопределение этого метода обязательно. Иначе у вас, паук, ничего не получится. Данный метод должен возвращать объект элемента, Request или итерацию, содержащую любой из них.

process_results(response, results)

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

Предупреждение

Из-за его внутренней реализации вы должны явно устанавливать обратные вызовы для новых запросов при написании пауков на основе XMLFeedSpider; В противном случае может произойти неожиданное поведение.

Пример XMLFeedSpider

Данные пауки довольно просты в использовании, давайте рассмотрим один пример:

from scrapy.spiders import XMLFeedSpider
from myproject.items import TestItem

class MySpider(XMLFeedSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/feed.xml']
    iterator = 'iternodes'  # This is actually unnecessary, since it's the default value
    itertag = 'item'

    def parse_node(self, response, node):
        self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.getall()))

        item = TestItem()
        item['id'] = node.xpath('@id').get()
        item['name'] = node.xpath('name').get()
        item['description'] = node.xpath('description').get()
        return item

По сути, мы создали паука, который загружает фид из заданного start_urls, а затем выполняет итерацию по каждому из своих тегов item, распечатывает их и сохраняет некоторые случайные данные в Item.

CSVFeedSpider

class scrapy.spiders.CSVFeedSpider

Данный паук очень похож на XMLFeedSpider, за исключением того, что он выполняет итерацию по строкам, а не по узлам. На каждой итерации вызывается метод parse_row().

delimiter

Строка с символом-разделителем для каждого поля в файле CSV. По умолчанию используется ',' (запятая).

quotechar

Строка с символом вложения для каждого поля в файле CSV. По умолчанию используется '"' (кавычки).

headers

Список имён столбцов в файле CSV.

parse_row(response, row)

Получает ответ и текст (представляющий каждую строку) с ключом для каждого предоставленного (или обнаруженного) заголовка CSV-файла. Данный паук также дает возможность переопределить методы adapt_response и process_results для целей предварительной и постобработки.

Пример CSVFeedSpider

Давайте посмотрим на пример, аналогичный предыдущему, но с использованием CSVFeedSpider:

from scrapy.spiders import CSVFeedSpider
from myproject.items import TestItem

class MySpider(CSVFeedSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/feed.csv']
    delimiter = ';'
    quotechar = "'"
    headers = ['id', 'name', 'description']

    def parse_row(self, response, row):
        self.logger.info('Hi, this is a row!: %r', row)

        item = TestItem()
        item['id'] = row['id']
        item['name'] = row['name']
        item['description'] = row['description']
        return item

SitemapSpider

class scrapy.spiders.SitemapSpider

SitemapSpider позволяет сканировать сайт, обнаруживая URL-адреса с помощью Sitemaps.xml.

Он поддерживает вложенные карты сайта и обнаружение URL-адресов карты сайта из robots.txt.

sitemap_urls

Список URL-адресов, указывающих на карты сайта, URL-адреса которых вы хотите сканировать.

Вы также можете указать на robots.txt, и он будет проанализирован для извлечения из него URL-адресов карты сайта.

sitemap_rules

Список кортежей (regex, callback), где:

  • regex — это регулярное выражение для соответствия URL-адресам, извлеченным из карт сайта. regex может быть либо строкой, либо скомпилированным объектом регулярного выражения.

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

Например:

sitemap_rules = [('/product/', 'parse_product')]

Правила применяются по порядку, и будет использоваться только первое совпадение.

Если вы опустите данный атрибут, все URL-адреса, найденные в файлах Sitemap, будут обработаны обратным вызовом parse.

sitemap_follow

Список регулярных выражений карты сайта, которым необходимо следовать. Это только для сайтов, использующих Sitemap индексные файлы, которые указывают на другие файлы Sitemap.xml.

По умолчанию отслеживаются все карты сайта.

Указывает, следует ли использовать альтернативные ссылки для одного url. Это ссылки на тот же веб-сайт на другом языке, переданные в том же блоке url.

Например:

<url>
    <loc>http://example.com/</loc>
    <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/>
</url>

Если задано значение sitemap_alternate_links, будут извлечены оба URL-адреса. Если sitemap_alternate_links отключён, будет извлечен только http://example.com/.

По умолчанию sitemap_alternate_links отключён.

sitemap_filter(entries)

Это функция фильтра, которую можно переопределить для выбора записей карты сайта на основе их атрибутов.

Например:

<url>
    <loc>http://example.com/</loc>
    <lastmod>2005-01-01</lastmod>
</url>

Мы можем определить функцию sitemap_filter для фильтрации entries по дате:

from datetime import datetime
from scrapy.spiders import SitemapSpider

class FilteredSitemapSpider(SitemapSpider):
    name = 'filtered_sitemap_spider'
    allowed_domains = ['example.com']
    sitemap_urls = ['http://example.com/sitemap.xml']

    def sitemap_filter(self, entries):
        for entry in entries:
            date_time = datetime.strptime(entry['lastmod'], '%Y-%m-%d')
            if date_time.year >= 2005:
                yield entry

Это позволит получить только entries, измененные в 2005 году и в последующие годы.

Записи — это объекты dict, извлеченные из документа карты сайта. Обычно ключ — это имя тега, а значение — текст внутри него.

Это важно заметить:

  • поскольку атрибут loc является обязательным, записи без этого тега отбрасываются

  • альтернативные ссылки хранятся в списке с ключом alternate (см. sitemap_alternate_links)

  • пространства имён удаляются, поэтому теги lxml с именем {namespace}tagname становятся только tagname

Если вы опустите данный метод, все записи, найденные в файлах Sitemap, будут обработаны с учётом других атрибутов и их настроек.

Примеры SitemapSpider

Самый простой пример: обработать все URL-адреса, обнаруженные через карты сайта, с помощью обратного вызова parse:

from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']

    def parse(self, response):
        pass # ... scrape item here ...

Обработать некоторые URL-адреса с определенным обратным вызовом и другие URL-адреса с другим обратным вызовом:

from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']
    sitemap_rules = [
        ('/product/', 'parse_product'),
        ('/category/', 'parse_category'),
    ]

    def parse_product(self, response):
        pass # ... scrape product ...

    def parse_category(self, response):
        pass # ... scrape category ...

Следуйте картам сайта, определенным в файле robots.txt, и следовать только за картами сайта, URL-адрес которых содержит /sitemap_shop:

from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
        ('/shop/', 'parse_shop'),
    ]
    sitemap_follow = ['/sitemap_shops']

    def parse_shop(self, response):
        pass # ... scrape shop here ...

Совместить SitemapSpider с другими источниками URL-адресов:

from scrapy.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
        ('/shop/', 'parse_shop'),
    ]

    other_urls = ['http://www.example.com/about']

    def start_requests(self):
        requests = list(super(MySpider, self).start_requests())
        requests += [scrapy.Request(x, self.parse_other) for x in self.other_urls]
        return requests

    def parse_shop(self, response):
        pass # ... scrape shop here ...

    def parse_other(self, response):
        pass # ... scrape other here ...