Загрузчики элементов

Загрузчики элементов предоставляют удобный механизм для загрузки собранных элементов. Несмотря на то, что элементы можно заполнять напрямую, загрузчики элементов предоставляют гораздо более удобный API для их заполнения из процесса очистки, автоматизируя некоторые общие задачи, такие как анализ необработанных извлеченных данных перед их назначением.

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

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

Примечание

Загрузчики элементов — это расширение библиотеки itemloaders, которое упрощает работу с Scrapy, добавляя поддержку ответов.

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

Чтобы использовать загрузчик элементов, вы должны сначала создать его экземпляр. Вы можете создать его экземпляр с объекта элемента или без него, и в этом случае объект элемента автоматически создаётся в методе Item Loader __init__ с использованием класса элемента, указанного в атрибуте ItemLoader.default_item_class.

Затем вы начинаете собирать значения в загрузчик элементов, обычно используя селекторы. Вы можете добавить более одного значения в одно и то же поле элемента; Загрузчик элементов будет знать, как «объединить» данные значения позже, используя соответствующую функцию обработки.

Примечание

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

Вот типичное использование загрузчика элемента в пауке с использованием Product элемента, заявленного в главе элементов:

from scrapy.loader import ItemLoader
from myproject.items import Product

def parse(self, response):
    l = ItemLoader(item=Product(), response=response)
    l.add_xpath('name', '//div[@class="product_name"]')
    l.add_xpath('name', '//div[@class="product_title"]')
    l.add_xpath('price', '//p[@id="price"]')
    l.add_css('stock', 'p#stock')
    l.add_value('last_updated', 'today') # you can also use literal values
    return l.load_item()

Быстро посмотрев на данный код, мы увидим, что поле name извлекается из двух разных мест XPath на странице:

  1. //div[@class="product_name"]

  2. //div[@class="product_title"]

Другими словами, данные собираются путём извлечения их из двух местоположений XPath с использованием метода add_xpath(). Это данные, которые позже будут присвоены полю name.

Впоследствии аналогичные вызовы используются для полей price и stock (последнее с использованием селектора CSS с методом add_css()), и, наконец, поле last_update заполняется непосредственно буквальным значением (today) с использованием другого метода: add_value().

Наконец, когда все данные собраны, вызывается метод ItemLoader.load_item(), который фактически возвращает элемент, заполненный данными, ранее извлеченными и собранными с помощью вызовов add_xpath(), add_css() и add_value().

Работа с элементами класса данных

По умолчанию элементы класса данных требует, чтобы все поля были переданы при создании. Это может быть проблемой при использовании элементов класса данных с загрузчиками элементов: если предварительно заполненный элемент не передан загрузчику, поля будут заполняться постепенно с использованием методов загрузчика add_xpath(), add_css() и add_value().

Один из подходов к преодолению этого — определение элементов с помощью функции field() с аргументом default:

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class InventoryItem:
    name: Optional[str] = field(default=None)
    price: Optional[float] = field(default=None)
    stock: Optional[int] = field(default=None)

Процессоры ввода и вывода

Загрузчик элементов содержит один процессор ввода и один процессор вывода для каждого поля (элемента). Процессор ввода обрабатывает извлеченные данные сразу после их получения (с помощью методов add_xpath(), add_css() или add_value()), а результат процессора ввода собирается и сохраняется в ItemLoader. После сбора всех данных вызывается метод ItemLoader.load_item() для заполнения и получения заполненного объект элемента. Когда процессор вывода вызывается с данными, ранее собранными (и обработанными с помощью процессора ввода). Результатом процессора вывода является окончательное значение, которое присваивается элементу.

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

l = ItemLoader(Product(), some_selector)
l.add_xpath('name', xpath1) # (1)
l.add_xpath('name', xpath2) # (2)
l.add_css('name', css) # (3)
l.add_value('name', 'test') # (4)
return l.load_item() # (5)

Итак, что происходит:

  1. Данные из xpath1 извлекаются и проходят через процессор ввода поля name. Результат обработчика ввода собирается и сохраняется в загрузчике элементов (но ещё не назначен элементу).

  2. Данные из xpath2 извлекаются и проходят через тот же процессор ввода, что и в (1). Результат процессора ввода добавляется к данным, собранным в (1) (если есть).

  3. Данный случай аналогичен предыдущим, за исключением того, что данные извлекаются из селектора CSS css и передаются через тот же процессор ввода, который использовался в (1) и (2). Результат процессора ввода добавляется к данным, собранным в (1) и (2) (если есть).

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

  5. Данные, собранные на этапах (1), (2), (3) и (4), передаются через процессор вывода поля name. Результатом процессора вывода является значение, присвоенное полю name в элементе.

Стоит отметить, что процессоры — это просто вызываемые объекты, которые вызываются с данными для анализа и возвращают проанализированное значение. Таким образом, вы можете использовать любую функцию в качестве процессора ввода или вывода. Единственное требование — они должны принимать один (и только один) позиционный аргумент, который будет повторяться.

Изменено в версии 2.0: Процессоры больше не должны быть методами.

Примечание

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

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

И последнее, но не менее важное: для удобства itemloaders поставляется со встроенными часто используемыми процессорами.

Объявление загрузчиков элементов

Загрузчики элементов объявляются с использованием синтаксиса определения класса. Вот пример:

from itemloaders.processors import TakeFirst, MapCompose, Join
from scrapy.loader import ItemLoader

class ProductLoader(ItemLoader):

    default_output_processor = TakeFirst()

    name_in = MapCompose(str.title)
    name_out = Join()

    price_in = MapCompose(str.strip)

    # ...

Как видите, входные процессоры объявляются с использованием суффикса _in, тогда как выходные процессоры объявляются с использованием суффикса _out. И вы также можете объявить процессоры ввода/вывода по умолчанию, используя атрибуты ItemLoader.default_input_processor и ItemLoader.default_output_processor.

Объявление процессоров ввода и вывода

Как было показано в предыдущем разделе, процессоры ввода и вывода могут быть объявлены в определении загрузчика элементов, и очень часто так объявляют процессоры ввода. Однако есть ещё одно место, где вы можете указать используемые процессоры ввода и вывода: в метаданных поля элемента. Вот пример:

import scrapy
from itemloaders.processors import Join, MapCompose, TakeFirst
from w3lib.html import remove_tags

def filter_price(value):
    if value.isdigit():
        return value

class Product(scrapy.Item):
    name = scrapy.Field(
        input_processor=MapCompose(remove_tags),
        output_processor=Join(),
    )
    price = scrapy.Field(
        input_processor=MapCompose(remove_tags, filter_price),
        output_processor=TakeFirst(),
    )
>>> from scrapy.loader import ItemLoader
>>> il = ItemLoader(item=Product())
>>> il.add_value('name', ['Welcome to my', '<strong>website</strong>'])
>>> il.add_value('price', ['&euro;', '<span>1000</span>'])
>>> il.load_item()
{'name': 'Welcome to my website', 'price': '1000'}

Порядок приоритета как для входных, так и для выходных процессоров следующий:

  1. Атрибуты, зависящие от поля загрузчика элементов: field_in и field_out (наибольший приоритет)

  2. Метаданные поля (ключ input_processor и output_processor)

  3. Значения по умолчанию загрузчика элементов: ItemLoader.default_input_processor() и ItemLoader.default_output_processor() (наименьший приоритет)

См. также: Повторное использование и расширение загрузчиков элементов.

Контекст загрузчика элементов

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

Например, предположим, что у вас есть функция parse_length, которая получает текстовое значение и извлекает из него длину:

def parse_length(text, loader_context):
    unit = loader_context.get('unit', 'm')
    # ... length parsing code goes here ...
    return parsed_length

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

Есть несколько способов изменить значения контекста загрузчика элементов:

  1. Путём изменения текущего активного контекста загрузчика элементов (атрибут context):

    loader = ItemLoader(product)
    loader.context['unit'] = 'cm'
    
  2. При создании экземпляра загрузчика элементов (ключевые аргументы метода __init__ загрузчика элементов хранятся в контексте загрузчика элементов):

    loader = ItemLoader(product, unit='cm')
    
  3. В объявлении загрузчика элементов для тех процессоров ввода/вывода, которые поддерживают создание их экземпляров в контексте загрузчика элементов. MapCompose — один из них:

    class ProductLoader(ItemLoader):
        length_out = MapCompose(parse_length, unit='cm')
    

Объекты ItemLoader

Вложенные загрузчики

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

Пример:

<footer>
    <a class="social" href="https://facebook.com/whatever">Like Us</a>
    <a class="social" href="https://twitter.com/whatever">Follow Us</a>
    <a class="email" href="mailto:[email protected]">Email Us</a>
</footer>

Без вложенных загрузчиков вам необходимо указать полный xpath (или css) для каждого значения, которое вы хотите извлечь.

Пример:

loader = ItemLoader(item=Item())
# load stuff not in the footer
loader.add_xpath('social', '//footer/a[@class = "social"]/@href')
loader.add_xpath('email', '//footer/a[@class = "email"]/@href')
loader.load_item()

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

Пример:

loader = ItemLoader(item=Item())
# load stuff not in the footer
footer_loader = loader.nested_xpath('//footer')
footer_loader.add_xpath('social', 'a[@class = "social"]/@href')
footer_loader.add_xpath('email', 'a[@class = "email"]/@href')
# no need to call footer_loader.load_item()
loader.load_item()

Вы можете произвольно вкладывать загрузчики, и они работают с селекторами xpath или css. Как правило, используйте вложенные загрузчики, когда они упрощают ваш код, но не переусердствуйте с вложением, иначе ваш синтаксический анализатор может стать трудным для чтения.

Повторное использование и расширение загрузчиков элементов

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

Загрузчики элементов предназначены для облегчения бремени обслуживания правил синтаксического анализа без потери гибкости и, в то же время, предоставления удобного механизма для их расширения и отмены. По этой причине загрузчики элементов поддерживают традиционное наследование классов Python для работы с различиями конкретных пауков (или групп пауков).

Предположим, например, что на каком-то конкретном сайте названия продуктов заключены в три тире (например, ---Plasma TV---), и вы не хотите, чтобы данные тире в конечном итоге получались из названий конечных продуктов.

Вот как вы можете удалить данные дефисы, повторно используя и расширив загрузчик Product Item по умолчанию (ProductLoader):

from itemloaders.processors import MapCompose
from myproject.ItemLoaders import ProductLoader

def strip_dashes(x):
    return x.strip('-')

class SiteSpecificLoader(ProductLoader):
    name_in = MapCompose(strip_dashes, ProductLoader.name_in)

Другой случай, когда расширение Item Loaders может быть очень полезным, — это когда у вас есть несколько исходных форматов, например XML и HTML. В версии XML вы можете удалить вхождения CDATA. Вот пример того, как это сделать:

from itemloaders.processors import MapCompose
from myproject.ItemLoaders import ProductLoader
from myproject.utils.xml import remove_cdata

class XmlProductLoader(ProductLoader):
    name_in = MapCompose(remove_cdata, ProductLoader.name_in)

Вот как обычно расширяются процессоры ввода.

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

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