Scrapy Учебник
В этом руководстве мы предполагаем, что Scrapy уже установлен в вашей системе. Если это не так, см. Инструкцию по установке.
Мы собираемся парсить quotes.toscrape.com, веб-сайт, на котором перечислены цитаты известных авторов.
Данный учебник проведёт вас через данные задачи:
Создание нового проекта Scrapy
Написание паука для сканирования сайта и извлечения данных
Экспорт полученных данных с помощью командной строки
Изменение паука для рекурсивного перехода по ссылкам
Использование аргументов паука
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 здесь.
Извлечение данных в нашем пауке
Вернёмся к нашему пауку. До сих пор он не извлекал никаких данных, а просто сохранял всю 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
. Хотя вам не нужно реализовывать какие-либо конвейеры элементов, если вы просто хотите хранить полученные элементы.
Следование за ссылками
Скажем, вместо того, чтобы просто извлекать данные с первых двух страниц с http://quotes.toscrape.com, вам нужны цитаты со всех страниц веб-сайта.
Теперь, когда вы знаете, как извлекать данные со страниц, давайте посмотрим, как переходить по их ссылкам.
Первым делом нужно извлечь ссылку на страницу, по которой мы хотим перейти. Изучая нашу страницу, мы видим, что есть ссылка на следующую страницу со следующей разметкой:
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
Мы можем попробовать извлечь её в оболочке:
>>> response.css('li.next a').get()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
Получаем элемент ссылки, но нам нужен атрибут href
. Для этого Scrapy поддерживает расширение CSS, которое позволяет вам выбирать содержимое атрибута:
>>> response.css('li.next a::attr(href)').get()
'/page/2/'
Также доступно свойство attrib
(подробнее см. Выбор атрибутов элемента):
>>> 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)
Теперь, после извлечения данных, метод parse()
ищет ссылку на следующую страницу, создаёт полный абсолютный URL-адрес с помощью метода urljoin()
(поскольку ссылки могут быть относительными) и выдает новый запрос на следующую страницу, регистрируя себя как обратный вызов для обработки извлечения данных для следующей страницы и для продолжения сканирования всех страниц.
Здесь вы видите механизм следования ссылкам в Scrapy: когда вы передаёте Request в методе обратного вызова, Scrapy планирует отправить данный запрос и зарегистрирует метод обратного вызова, который будет выполняться, когда данный запрос завершится.
Используя это, вы можете создавать сложные сканеры, которые переходят по ссылкам в соответствии с определенными вами правилами и извлекают различные типы данных в зависимости от страницы, которую они посещают.
В нашем примере он создаёт своего рода цикл, переходя по всем ссылкам на следующую страницу до тех пор, пока не найдет ни одной — это удобно для сканирования блогов, форумов и других сайтов с разбивкой на страницы.
Ярлык для создания запросов
В качестве ярлыка для создания объектов запроса вы можете использовать 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.Request, response.follow
поддерживает относительные URL-адреса напрямую — вызывать urljoin не нужно. Обратите внимание, что response.follow
просто возвращает экземпляр запроса; вы все равно должны передать данный запрос.
Вы также можете передать селектор в response.follow
вместо строки; данный селектор должен извлекать необходимые атрибуты:
for href in response.css('ul.pager a::attr(href)'):
yield response.follow(href, callback=self.parse)
Для элементов <a>
есть ярлык: response.follow
автоматически использует их атрибут href. Таким образом, код может быть сокращен ещё больше:
for a in response.css('ul.pager a'):
yield response.follow(a, callback=self.parse)
Чтобы создать несколько запросов из итерируемого объекта, вы можете вместо этого использовать response.follow_all
:
anchors = response.css('ul.pager a')
yield from response.follow_all(anchors, callback=self.parse)
или, сокращая его дальше:
yield from response.follow_all(css='ul.pager a', callback=self.parse)
Ещё примеры и шаблоны
Вот ещё один паук, который иллюстрирует обратные вызовы и переход по ссылкам, на данный раз для сбора информации об авторе:
import scrapy
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
author_page_links = response.css('.author + a')
yield from response.follow_all(author_page_links, self.parse_author)
pagination_links = response.css('li.next a')
yield from response.follow_all(pagination_links, self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).get(default='').strip()
yield {
'name': extract_with_css('h3.author-title::text'),
'birthdate': extract_with_css('.author-born-date::text'),
'bio': extract_with_css('.author-description::text'),
}
Данный паук будет запускаться с главной страницы, он будет следовать по всем ссылкам на страницы авторов, вызывая обратный вызов parse_author
для каждой из них, а также по ссылкам пагинации с обратным вызовом parse
, как мы видели ранее.
Здесь мы передаем обратные вызовы response.follow_all
в качестве позиционных аргументов, чтобы сделать код короче; он также работает для Request
.
Обратный вызов parse_author
определяет вспомогательную функцию для извлечения и очистки данных из запроса CSS и дает Python dict с данными автора.
Ещё одна интересная вещь, которую демонстрирует данный паук, заключается в том, что даже если есть много цитат одного и того же автора, нам не нужно беспокоиться о посещении одной и той же страницы автора несколько раз. По умолчанию Scrapy отфильтровывает повторяющиеся запросы к уже посещенным URL-адресам, избегая проблемы слишком частого обращения к серверам из-за ошибки программирования. Это можно настроить с помощью параметра DUPEFILTER_CLASS
.
Надеюсь, теперь вы хорошо понимаете, как использовать механизм перехода по ссылкам и обратного вызова с Scrapy.
В качестве ещё одного примера паука, который использует механизм перехода по ссылкам, обратите внимание на класс CrawlSpider
для общего паука, который реализует небольшой механизм правил, который вы можете использовать для написания своих поисковых роботов поверх него.
Кроме того, распространенным шаблоном является создание элемента с данными с более чем одной страницы с использованием трюк, чтобы передать доп. данные для обратных вызовов.
Использование аргументов паука
Вы можете предоставить своим паукам аргументы командной строки, используя параметр -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 для быстрого обзора наиболее важных из них.
Вы можете продолжить с раздела Базовые концепты, чтобы узнать больше об инструменте командной строки, пауках, селекторах и других вещах, которые не рассматривались в руководстве, например, моделировании сканированных данных. Если вы предпочитаете поиграть с примером проекта, посмотрите раздел Примеры.