mirror of https://github.com/scrapy/scrapy.git
Add and run pre-commit hook 'blacken-docs'
Change python code snippets to begin with '.. code-block:: python' to be recognized by the hook for formatting. All snippets under '::' (rst literal blocks) are ignored.
This commit is contained in:
parent
b337c986ca
commit
c1bbb299d7
|
|
@ -16,3 +16,9 @@ repos:
|
||||||
rev: 5.12.0
|
rev: 5.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
- repo: https://github.com/adamchainz/blacken-docs
|
||||||
|
rev: 1.13.0
|
||||||
|
hooks:
|
||||||
|
- id: blacken-docs
|
||||||
|
additional_dependencies:
|
||||||
|
- black==22.12.0
|
||||||
|
|
|
||||||
44
docs/faq.rst
44
docs/faq.rst
|
|
@ -35,8 +35,9 @@ for parsing HTML responses in Scrapy callbacks.
|
||||||
You just have to feed the response's body into a ``BeautifulSoup`` object
|
You just have to feed the response's body into a ``BeautifulSoup`` object
|
||||||
and extract whatever data you need from it.
|
and extract whatever data you need from it.
|
||||||
|
|
||||||
Here's an example spider using BeautifulSoup API, with ``lxml`` as the HTML parser::
|
Here's an example spider using BeautifulSoup API, with ``lxml`` as the HTML parser:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
@ -45,17 +46,12 @@ Here's an example spider using BeautifulSoup API, with ``lxml`` as the HTML pars
|
||||||
class ExampleSpider(scrapy.Spider):
|
class ExampleSpider(scrapy.Spider):
|
||||||
name = "example"
|
name = "example"
|
||||||
allowed_domains = ["example.com"]
|
allowed_domains = ["example.com"]
|
||||||
start_urls = (
|
start_urls = ("http://www.example.com/",)
|
||||||
'http://www.example.com/',
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
# use lxml to get decent HTML parsing speed
|
# use lxml to get decent HTML parsing speed
|
||||||
soup = BeautifulSoup(response.text, 'lxml')
|
soup = BeautifulSoup(response.text, "lxml")
|
||||||
yield {
|
yield {"url": response.url, "title": soup.h1.string}
|
||||||
"url": response.url,
|
|
||||||
"title": soup.h1.string
|
|
||||||
}
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|
@ -109,11 +105,13 @@ basically means that it crawls in `DFO order`_. This order is more convenient
|
||||||
in most cases.
|
in most cases.
|
||||||
|
|
||||||
If you do want to crawl in true `BFO order`_, you can do it by
|
If you do want to crawl in true `BFO order`_, you can do it by
|
||||||
setting the following settings::
|
setting the following settings:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
DEPTH_PRIORITY = 1
|
DEPTH_PRIORITY = 1
|
||||||
SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue'
|
SCHEDULER_DISK_QUEUE = "scrapy.squeues.PickleFifoDiskQueue"
|
||||||
SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'
|
SCHEDULER_MEMORY_QUEUE = "scrapy.squeues.FifoMemoryQueue"
|
||||||
|
|
||||||
While pending requests are below the configured values of
|
While pending requests are below the configured values of
|
||||||
:setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or
|
:setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or
|
||||||
|
|
@ -159,11 +157,13 @@ See also other suggestions at `StackOverflow`_.
|
||||||
|
|
||||||
.. note:: Remember to disable
|
.. note:: Remember to disable
|
||||||
:class:`scrapy.spidermiddlewares.offsite.OffsiteMiddleware` when you enable
|
:class:`scrapy.spidermiddlewares.offsite.OffsiteMiddleware` when you enable
|
||||||
your custom implementation::
|
your custom implementation:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
SPIDER_MIDDLEWARES = {
|
SPIDER_MIDDLEWARES = {
|
||||||
'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': None,
|
"scrapy.spidermiddlewares.offsite.OffsiteMiddleware": None,
|
||||||
'myproject.middlewares.CustomOffsiteMiddleware': 500,
|
"myproject.middlewares.CustomOffsiteMiddleware": 500,
|
||||||
}
|
}
|
||||||
|
|
||||||
.. _meet the installation requirements: https://github.com/andreasvc/pyre2#installation
|
.. _meet the installation requirements: https://github.com/andreasvc/pyre2#installation
|
||||||
|
|
@ -235,11 +235,13 @@ What does the response status code 999 means?
|
||||||
|
|
||||||
999 is a custom response status code used by Yahoo sites to throttle requests.
|
999 is a custom response status code used by Yahoo sites to throttle requests.
|
||||||
Try slowing down the crawling speed by using a download delay of ``2`` (or
|
Try slowing down the crawling speed by using a download delay of ``2`` (or
|
||||||
higher) in your spider::
|
higher) in your spider:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(CrawlSpider):
|
class MySpider(CrawlSpider):
|
||||||
|
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
|
|
||||||
download_delay = 2
|
download_delay = 2
|
||||||
|
|
||||||
|
|
@ -351,19 +353,21 @@ How to split an item into multiple items in an item pipeline?
|
||||||
input item. :ref:`Create a spider middleware <custom-spider-middleware>`
|
input item. :ref:`Create a spider middleware <custom-spider-middleware>`
|
||||||
instead, and use its
|
instead, and use its
|
||||||
:meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_spider_output`
|
:meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_spider_output`
|
||||||
method for this purpose. For example::
|
method for this purpose. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from itemadapter import is_item, ItemAdapter
|
from itemadapter import is_item, ItemAdapter
|
||||||
|
|
||||||
class MultiplyItemsMiddleware:
|
|
||||||
|
|
||||||
|
class MultiplyItemsMiddleware:
|
||||||
def process_spider_output(self, response, result, spider):
|
def process_spider_output(self, response, result, spider):
|
||||||
for item in result:
|
for item in result:
|
||||||
if is_item(item):
|
if is_item(item):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
for _ in range(adapter['multiply_by']):
|
for _ in range(adapter["multiply_by"]):
|
||||||
yield deepcopy(item)
|
yield deepcopy(item)
|
||||||
|
|
||||||
Does Scrapy support IPv6 addresses?
|
Does Scrapy support IPv6 addresses?
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,24 @@ In order to show you what Scrapy brings to the table, we'll walk you through an
|
||||||
example of a Scrapy Spider using the simplest way to run a spider.
|
example of a Scrapy Spider using the simplest way to run a spider.
|
||||||
|
|
||||||
Here's the code for a spider that scrapes famous quotes from website
|
Here's the code for a spider that scrapes famous quotes from website
|
||||||
https://quotes.toscrape.com, following the pagination::
|
https://quotes.toscrape.com, following the pagination
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class QuotesSpider(scrapy.Spider):
|
class QuotesSpider(scrapy.Spider):
|
||||||
name = 'quotes'
|
name = "quotes"
|
||||||
start_urls = [
|
start_urls = [
|
||||||
'https://quotes.toscrape.com/tag/humor/',
|
"https://quotes.toscrape.com/tag/humor/",
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {
|
yield {
|
||||||
'author': quote.xpath('span/small/text()').get(),
|
"author": quote.xpath("span/small/text()").get(),
|
||||||
'text': quote.css('span.text::text').get(),
|
"text": quote.css("span.text::text").get(),
|
||||||
}
|
}
|
||||||
|
|
||||||
next_page = response.css('li.next a::attr("href")').get()
|
next_page = response.css('li.next a::attr("href")').get()
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ that generates :class:`scrapy.Request <scrapy.Request>` objects from URLs,
|
||||||
you can just define a :attr:`~scrapy.Spider.start_urls` class attribute
|
you can just define a :attr:`~scrapy.Spider.start_urls` class attribute
|
||||||
with a list of URLs. This list will then be used by the default implementation
|
with a list of URLs. This list will then be used by the default implementation
|
||||||
of :meth:`~scrapy.Spider.start_requests` to create the initial requests
|
of :meth:`~scrapy.Spider.start_requests` to create the initial requests
|
||||||
for your spider::
|
for your spider.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -187,13 +189,13 @@ for your spider::
|
||||||
class QuotesSpider(scrapy.Spider):
|
class QuotesSpider(scrapy.Spider):
|
||||||
name = "quotes"
|
name = "quotes"
|
||||||
start_urls = [
|
start_urls = [
|
||||||
'https://quotes.toscrape.com/page/1/',
|
"https://quotes.toscrape.com/page/1/",
|
||||||
'https://quotes.toscrape.com/page/2/',
|
"https://quotes.toscrape.com/page/2/",
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
page = response.url.split("/")[-2]
|
page = response.url.split("/")[-2]
|
||||||
filename = f'quotes-{page}.html'
|
filename = f"quotes-{page}.html"
|
||||||
Path(filename).write_bytes(response.body)
|
Path(filename).write_bytes(response.body)
|
||||||
|
|
||||||
The :meth:`~scrapy.Spider.parse` method will be called to handle each
|
The :meth:`~scrapy.Spider.parse` method will be called to handle each
|
||||||
|
|
@ -438,7 +440,9 @@ extraction logic above into our spider.
|
||||||
|
|
||||||
A Scrapy spider typically generates many dictionaries containing the data
|
A Scrapy spider typically generates many dictionaries containing the data
|
||||||
extracted from the page. To do that, we use the ``yield`` Python keyword
|
extracted from the page. To do that, we use the ``yield`` Python keyword
|
||||||
in the callback, as you can see below::
|
in the callback, as you can see below:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
@ -446,16 +450,16 @@ in the callback, as you can see below::
|
||||||
class QuotesSpider(scrapy.Spider):
|
class QuotesSpider(scrapy.Spider):
|
||||||
name = "quotes"
|
name = "quotes"
|
||||||
start_urls = [
|
start_urls = [
|
||||||
'https://quotes.toscrape.com/page/1/',
|
"https://quotes.toscrape.com/page/1/",
|
||||||
'https://quotes.toscrape.com/page/2/',
|
"https://quotes.toscrape.com/page/2/",
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {
|
yield {
|
||||||
'text': quote.css('span.text::text').get(),
|
"text": quote.css("span.text::text").get(),
|
||||||
'author': quote.css('small.author::text').get(),
|
"author": quote.css("small.author::text").get(),
|
||||||
'tags': quote.css('div.tags a.tag::text').getall(),
|
"tags": quote.css("div.tags a.tag::text").getall(),
|
||||||
}
|
}
|
||||||
|
|
||||||
If you run this spider, it will output the extracted data with the log::
|
If you run this spider, it will output the extracted data with the log::
|
||||||
|
|
@ -543,7 +547,9 @@ There is also an ``attrib`` property available
|
||||||
'/page/2/'
|
'/page/2/'
|
||||||
|
|
||||||
Let's see now our spider modified to recursively follow the link to the next
|
Let's see now our spider modified to recursively follow the link to the next
|
||||||
page, extracting data from it::
|
page, extracting data from it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
@ -551,18 +557,18 @@ page, extracting data from it::
|
||||||
class QuotesSpider(scrapy.Spider):
|
class QuotesSpider(scrapy.Spider):
|
||||||
name = "quotes"
|
name = "quotes"
|
||||||
start_urls = [
|
start_urls = [
|
||||||
'https://quotes.toscrape.com/page/1/',
|
"https://quotes.toscrape.com/page/1/",
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {
|
yield {
|
||||||
'text': quote.css('span.text::text').get(),
|
"text": quote.css("span.text::text").get(),
|
||||||
'author': quote.css('small.author::text').get(),
|
"author": quote.css("small.author::text").get(),
|
||||||
'tags': quote.css('div.tags a.tag::text').getall(),
|
"tags": quote.css("div.tags a.tag::text").getall(),
|
||||||
}
|
}
|
||||||
|
|
||||||
next_page = response.css('li.next a::attr(href)').get()
|
next_page = response.css("li.next a::attr(href)").get()
|
||||||
if next_page is not None:
|
if next_page is not None:
|
||||||
next_page = response.urljoin(next_page)
|
next_page = response.urljoin(next_page)
|
||||||
yield scrapy.Request(next_page, callback=self.parse)
|
yield scrapy.Request(next_page, callback=self.parse)
|
||||||
|
|
@ -594,7 +600,9 @@ A shortcut for creating Requests
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
As a shortcut for creating Request objects you can use
|
As a shortcut for creating Request objects you can use
|
||||||
:meth:`response.follow <scrapy.http.TextResponse.follow>`::
|
:meth:`response.follow <scrapy.http.TextResponse.follow>`
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
@ -602,18 +610,18 @@ As a shortcut for creating Request objects you can use
|
||||||
class QuotesSpider(scrapy.Spider):
|
class QuotesSpider(scrapy.Spider):
|
||||||
name = "quotes"
|
name = "quotes"
|
||||||
start_urls = [
|
start_urls = [
|
||||||
'https://quotes.toscrape.com/page/1/',
|
"https://quotes.toscrape.com/page/1/",
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {
|
yield {
|
||||||
'text': quote.css('span.text::text').get(),
|
"text": quote.css("span.text::text").get(),
|
||||||
'author': quote.css('span small::text').get(),
|
"author": quote.css("span small::text").get(),
|
||||||
'tags': quote.css('div.tags a.tag::text').getall(),
|
"tags": quote.css("div.tags a.tag::text").getall(),
|
||||||
}
|
}
|
||||||
|
|
||||||
next_page = response.css('li.next a::attr(href)').get()
|
next_page = response.css("li.next a::attr(href)").get()
|
||||||
if next_page is not None:
|
if next_page is not None:
|
||||||
yield response.follow(next_page, callback=self.parse)
|
yield response.follow(next_page, callback=self.parse)
|
||||||
|
|
||||||
|
|
@ -622,57 +630,67 @@ need to call urljoin. Note that ``response.follow`` just returns a Request
|
||||||
instance; you still have to yield this Request.
|
instance; you still have to yield this Request.
|
||||||
|
|
||||||
You can also pass a selector to ``response.follow`` instead of a string;
|
You can also pass a selector to ``response.follow`` instead of a string;
|
||||||
this selector should extract necessary attributes::
|
this selector should extract necessary attributes:
|
||||||
|
|
||||||
for href in response.css('ul.pager a::attr(href)'):
|
.. code-block:: python
|
||||||
|
|
||||||
|
for href in response.css("ul.pager a::attr(href)"):
|
||||||
yield response.follow(href, callback=self.parse)
|
yield response.follow(href, callback=self.parse)
|
||||||
|
|
||||||
For ``<a>`` elements there is a shortcut: ``response.follow`` uses their href
|
For ``<a>`` elements there is a shortcut: ``response.follow`` uses their href
|
||||||
attribute automatically. So the code can be shortened further::
|
attribute automatically. So the code can be shortened further:
|
||||||
|
|
||||||
for a in response.css('ul.pager a'):
|
.. code-block:: python
|
||||||
|
|
||||||
|
for a in response.css("ul.pager a"):
|
||||||
yield response.follow(a, callback=self.parse)
|
yield response.follow(a, callback=self.parse)
|
||||||
|
|
||||||
To create multiple requests from an iterable, you can use
|
To create multiple requests from an iterable, you can use
|
||||||
:meth:`response.follow_all <scrapy.http.TextResponse.follow_all>` instead::
|
:meth:`response.follow_all <scrapy.http.TextResponse.follow_all>` instead:
|
||||||
|
|
||||||
anchors = response.css('ul.pager a')
|
.. code-block:: python
|
||||||
|
|
||||||
|
anchors = response.css("ul.pager a")
|
||||||
yield from response.follow_all(anchors, callback=self.parse)
|
yield from response.follow_all(anchors, callback=self.parse)
|
||||||
|
|
||||||
or, shortening it further::
|
or, shortening it further:
|
||||||
|
|
||||||
yield from response.follow_all(css='ul.pager a', callback=self.parse)
|
.. code-block:: python
|
||||||
|
|
||||||
|
yield from response.follow_all(css="ul.pager a", callback=self.parse)
|
||||||
|
|
||||||
|
|
||||||
More examples and patterns
|
More examples and patterns
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
Here is another spider that illustrates callbacks and following links,
|
Here is another spider that illustrates callbacks and following links,
|
||||||
this time for scraping author information::
|
this time for scraping author information:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class AuthorSpider(scrapy.Spider):
|
class AuthorSpider(scrapy.Spider):
|
||||||
name = 'author'
|
name = "author"
|
||||||
|
|
||||||
start_urls = ['https://quotes.toscrape.com/']
|
start_urls = ["https://quotes.toscrape.com/"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
author_page_links = response.css('.author + a')
|
author_page_links = response.css(".author + a")
|
||||||
yield from response.follow_all(author_page_links, self.parse_author)
|
yield from response.follow_all(author_page_links, self.parse_author)
|
||||||
|
|
||||||
pagination_links = response.css('li.next a')
|
pagination_links = response.css("li.next a")
|
||||||
yield from response.follow_all(pagination_links, self.parse)
|
yield from response.follow_all(pagination_links, self.parse)
|
||||||
|
|
||||||
def parse_author(self, response):
|
def parse_author(self, response):
|
||||||
def extract_with_css(query):
|
def extract_with_css(query):
|
||||||
return response.css(query).get(default='').strip()
|
return response.css(query).get(default="").strip()
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
'name': extract_with_css('h3.author-title::text'),
|
"name": extract_with_css("h3.author-title::text"),
|
||||||
'birthdate': extract_with_css('.author-born-date::text'),
|
"birthdate": extract_with_css(".author-born-date::text"),
|
||||||
'bio': extract_with_css('.author-description::text'),
|
"bio": extract_with_css(".author-description::text"),
|
||||||
}
|
}
|
||||||
|
|
||||||
This spider will start from the main page, it will follow all the links to the
|
This spider will start from the main page, it will follow all the links to the
|
||||||
|
|
@ -720,7 +738,9 @@ spider attributes by default.
|
||||||
|
|
||||||
In this example, the value provided for the ``tag`` argument will be available
|
In this example, the value provided for the ``tag`` argument will be available
|
||||||
via ``self.tag``. You can use this to make your spider fetch only quotes
|
via ``self.tag``. You can use this to make your spider fetch only quotes
|
||||||
with a specific tag, building the URL based on the argument::
|
with a specific tag, building the URL based on the argument:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
@ -729,20 +749,20 @@ with a specific tag, building the URL based on the argument::
|
||||||
name = "quotes"
|
name = "quotes"
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
url = 'https://quotes.toscrape.com/'
|
url = "https://quotes.toscrape.com/"
|
||||||
tag = getattr(self, 'tag', None)
|
tag = getattr(self, "tag", None)
|
||||||
if tag is not None:
|
if tag is not None:
|
||||||
url = url + 'tag/' + tag
|
url = url + "tag/" + tag
|
||||||
yield scrapy.Request(url, self.parse)
|
yield scrapy.Request(url, self.parse)
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {
|
yield {
|
||||||
'text': quote.css('span.text::text').get(),
|
"text": quote.css("span.text::text").get(),
|
||||||
'author': quote.css('small.author::text').get(),
|
"author": quote.css("small.author::text").get(),
|
||||||
}
|
}
|
||||||
|
|
||||||
next_page = response.css('li.next a::attr(href)').get()
|
next_page = response.css("li.next a::attr(href)").get()
|
||||||
if next_page is not None:
|
if next_page is not None:
|
||||||
yield response.follow(next_page, self.parse)
|
yield response.follow(next_page, self.parse)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,14 @@ Enforcing asyncio as a requirement
|
||||||
If you are writing a :ref:`component <topics-components>` that requires asyncio
|
If you are writing a :ref:`component <topics-components>` that requires asyncio
|
||||||
to work, use :func:`scrapy.utils.reactor.is_asyncio_reactor_installed` to
|
to work, use :func:`scrapy.utils.reactor.is_asyncio_reactor_installed` to
|
||||||
:ref:`enforce it as a requirement <enforce-component-requirements>`. For
|
:ref:`enforce it as a requirement <enforce-component-requirements>`. For
|
||||||
example::
|
example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.utils.reactor import is_asyncio_reactor_installed
|
from scrapy.utils.reactor import is_asyncio_reactor_installed
|
||||||
|
|
||||||
class MyComponent:
|
|
||||||
|
|
||||||
|
class MyComponent:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not is_asyncio_reactor_installed():
|
if not is_asyncio_reactor_installed():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,11 @@ Scrapy’s default scheduler priority queue is ``'scrapy.pqueues.ScrapyPriorityQ
|
||||||
It works best during single-domain crawl. It does not work well with crawling
|
It works best during single-domain crawl. It does not work well with crawling
|
||||||
many different domains in parallel
|
many different domains in parallel
|
||||||
|
|
||||||
To apply the recommended priority queue use::
|
To apply the recommended priority queue use:
|
||||||
|
|
||||||
SCHEDULER_PRIORITY_QUEUE = 'scrapy.pqueues.DownloaderAwarePriorityQueue'
|
.. code-block:: python
|
||||||
|
|
||||||
|
SCHEDULER_PRIORITY_QUEUE = "scrapy.pqueues.DownloaderAwarePriorityQueue"
|
||||||
|
|
||||||
.. _broad-crawls-concurrency:
|
.. _broad-crawls-concurrency:
|
||||||
|
|
||||||
|
|
@ -71,7 +73,9 @@ many different domains in parallel, so you will want to increase it. How much
|
||||||
to increase it will depend on how much CPU and memory your crawler will have
|
to increase it will depend on how much CPU and memory your crawler will have
|
||||||
available.
|
available.
|
||||||
|
|
||||||
A good starting point is ``100``::
|
A good starting point is ``100``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
CONCURRENT_REQUESTS = 100
|
CONCURRENT_REQUESTS = 100
|
||||||
|
|
||||||
|
|
@ -92,7 +96,9 @@ hitting DNS resolver timeouts. Possible solution to increase the number of
|
||||||
threads handling DNS queries. The DNS queue will be processed faster speeding
|
threads handling DNS queries. The DNS queue will be processed faster speeding
|
||||||
up establishing of connection and crawling overall.
|
up establishing of connection and crawling overall.
|
||||||
|
|
||||||
To increase maximum thread pool size use::
|
To increase maximum thread pool size use:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
REACTOR_THREADPOOL_MAXSIZE = 20
|
REACTOR_THREADPOOL_MAXSIZE = 20
|
||||||
|
|
||||||
|
|
@ -114,9 +120,11 @@ should not use ``DEBUG`` log level when preforming large broad crawls in
|
||||||
production. Using ``DEBUG`` level when developing your (broad) crawler may be
|
production. Using ``DEBUG`` level when developing your (broad) crawler may be
|
||||||
fine though.
|
fine though.
|
||||||
|
|
||||||
To set the log level use::
|
To set the log level use:
|
||||||
|
|
||||||
LOG_LEVEL = 'INFO'
|
.. code-block:: python
|
||||||
|
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
|
||||||
Disable cookies
|
Disable cookies
|
||||||
===============
|
===============
|
||||||
|
|
@ -126,7 +134,9 @@ doing broad crawls (search engine crawlers ignore them), and they improve
|
||||||
performance by saving some CPU cycles and reducing the memory footprint of your
|
performance by saving some CPU cycles and reducing the memory footprint of your
|
||||||
Scrapy crawler.
|
Scrapy crawler.
|
||||||
|
|
||||||
To disable cookies use::
|
To disable cookies use:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
COOKIES_ENABLED = False
|
COOKIES_ENABLED = False
|
||||||
|
|
||||||
|
|
@ -138,7 +148,9 @@ when sites causes are very slow (or fail) to respond, thus causing a timeout
|
||||||
error which gets retried many times, unnecessarily, preventing crawler capacity
|
error which gets retried many times, unnecessarily, preventing crawler capacity
|
||||||
to be reused for other domains.
|
to be reused for other domains.
|
||||||
|
|
||||||
To disable retries use::
|
To disable retries use:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
RETRY_ENABLED = False
|
RETRY_ENABLED = False
|
||||||
|
|
||||||
|
|
@ -149,7 +161,9 @@ Unless you are crawling from a very slow connection (which shouldn't be the
|
||||||
case for broad crawls) reduce the download timeout so that stuck requests are
|
case for broad crawls) reduce the download timeout so that stuck requests are
|
||||||
discarded quickly and free up capacity to process the next ones.
|
discarded quickly and free up capacity to process the next ones.
|
||||||
|
|
||||||
To reduce the download timeout use::
|
To reduce the download timeout use:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
DOWNLOAD_TIMEOUT = 15
|
DOWNLOAD_TIMEOUT = 15
|
||||||
|
|
||||||
|
|
@ -162,7 +176,9 @@ revisiting the site at a later crawl. This also help to keep the number of
|
||||||
request constant per crawl batch, otherwise redirect loops may cause the
|
request constant per crawl batch, otherwise redirect loops may cause the
|
||||||
crawler to dedicate too many resources on any specific domain.
|
crawler to dedicate too many resources on any specific domain.
|
||||||
|
|
||||||
To disable redirects use::
|
To disable redirects use:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
REDIRECT_ENABLED = False
|
REDIRECT_ENABLED = False
|
||||||
|
|
||||||
|
|
@ -179,7 +195,9 @@ Pages can indicate it in two ways:
|
||||||
"main", "index" website pages.
|
"main", "index" website pages.
|
||||||
|
|
||||||
Scrapy handles (1) automatically; to handle (2) enable
|
Scrapy handles (1) automatically; to handle (2) enable
|
||||||
:ref:`AjaxCrawlMiddleware <ajaxcrawl-middleware>`::
|
:ref:`AjaxCrawlMiddleware <ajaxcrawl-middleware>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
AJAXCRAWL_ENABLED = True
|
AJAXCRAWL_ENABLED = True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -617,7 +617,7 @@ Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
COMMANDS_MODULE = 'mybot.commands'
|
COMMANDS_MODULE = "mybot.commands"
|
||||||
|
|
||||||
.. _Deploying your project: https://scrapyd.readthedocs.io/en/latest/deploy.html
|
.. _Deploying your project: https://scrapyd.readthedocs.io/en/latest/deploy.html
|
||||||
|
|
||||||
|
|
@ -636,10 +636,11 @@ The following example adds ``my_command`` command:
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(name='scrapy-mymodule',
|
setup(
|
||||||
entry_points={
|
name="scrapy-mymodule",
|
||||||
'scrapy.commands': [
|
entry_points={
|
||||||
'my_command=my_scrapy_module.commands:MyCommand',
|
"scrapy.commands": [
|
||||||
],
|
"my_command=my_scrapy_module.commands:MyCommand",
|
||||||
},
|
],
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -66,16 +66,18 @@ version mismatch, while :exc:`ValueError` may be better if the issue is the
|
||||||
value of a setting.
|
value of a setting.
|
||||||
|
|
||||||
If your requirement is a minimum Scrapy version, you may use
|
If your requirement is a minimum Scrapy version, you may use
|
||||||
:attr:`scrapy.__version__` to enforce your requirement. For example::
|
:attr:`scrapy.__version__` to enforce your requirement. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from pkg_resources import parse_version
|
from pkg_resources import parse_version
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
class MyComponent:
|
|
||||||
|
|
||||||
|
class MyComponent:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if parse_version(scrapy.__version__) < parse_version('2.7'):
|
if parse_version(scrapy.__version__) < parse_version("2.7"):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{MyComponent.__qualname__} requires Scrapy 2.7 or "
|
f"{MyComponent.__qualname__} requires Scrapy 2.7 or "
|
||||||
f"later, which allow defining the process_spider_output "
|
f"later, which allow defining the process_spider_output "
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,13 @@ integrated way of testing your spiders by the means of contracts.
|
||||||
This allows you to test each callback of your spider by hardcoding a sample url
|
This allows you to test each callback of your spider by hardcoding a sample url
|
||||||
and check various constraints for how the callback processes the response. Each
|
and check various constraints for how the callback processes the response. Each
|
||||||
contract is prefixed with an ``@`` and included in the docstring. See the
|
contract is prefixed with an ``@`` and included in the docstring. See the
|
||||||
following example::
|
following example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
""" This function parses a sample response. Some contracts are mingled
|
"""
|
||||||
|
This function parses a sample response. Some contracts are mingled
|
||||||
with this docstring.
|
with this docstring.
|
||||||
|
|
||||||
@url http://www.amazon.com/s?field-keywords=selfish+gene
|
@url http://www.amazon.com/s?field-keywords=selfish+gene
|
||||||
|
|
@ -64,11 +67,13 @@ Custom Contracts
|
||||||
|
|
||||||
If you find you need more power than the built-in Scrapy contracts you can
|
If you find you need more power than the built-in Scrapy contracts you can
|
||||||
create and load your own contracts in the project by using the
|
create and load your own contracts in the project by using the
|
||||||
:setting:`SPIDER_CONTRACTS` setting::
|
:setting:`SPIDER_CONTRACTS` setting:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
SPIDER_CONTRACTS = {
|
SPIDER_CONTRACTS = {
|
||||||
'myproject.contracts.ResponseCheck': 10,
|
"myproject.contracts.ResponseCheck": 10,
|
||||||
'myproject.contracts.ItemValidate': 10,
|
"myproject.contracts.ItemValidate": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
Each contract must inherit from :class:`~scrapy.contracts.Contract` and can
|
Each contract must inherit from :class:`~scrapy.contracts.Contract` and can
|
||||||
|
|
@ -111,22 +116,26 @@ Raise :class:`~scrapy.exceptions.ContractFail` from
|
||||||
.. autoclass:: scrapy.exceptions.ContractFail
|
.. autoclass:: scrapy.exceptions.ContractFail
|
||||||
|
|
||||||
Here is a demo contract which checks the presence of a custom header in the
|
Here is a demo contract which checks the presence of a custom header in the
|
||||||
response received::
|
response received:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.contracts import Contract
|
from scrapy.contracts import Contract
|
||||||
from scrapy.exceptions import ContractFail
|
from scrapy.exceptions import ContractFail
|
||||||
|
|
||||||
|
|
||||||
class HasHeaderContract(Contract):
|
class HasHeaderContract(Contract):
|
||||||
""" Demo contract which checks the presence of a custom header
|
"""
|
||||||
@has_header X-CustomHeader
|
Demo contract which checks the presence of a custom header
|
||||||
|
@has_header X-CustomHeader
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = 'has_header'
|
name = "has_header"
|
||||||
|
|
||||||
def pre_process(self, response):
|
def pre_process(self, response):
|
||||||
for header in self.args:
|
for header in self.args:
|
||||||
if header not in response.headers:
|
if header not in response.headers:
|
||||||
raise ContractFail('X-CustomHeader not present')
|
raise ContractFail("X-CustomHeader not present")
|
||||||
|
|
||||||
.. _detecting-contract-check-runs:
|
.. _detecting-contract-check-runs:
|
||||||
|
|
||||||
|
|
@ -135,14 +144,17 @@ Detecting check runs
|
||||||
|
|
||||||
When ``scrapy check`` is running, the ``SCRAPY_CHECK`` environment variable is
|
When ``scrapy check`` is running, the ``SCRAPY_CHECK`` environment variable is
|
||||||
set to the ``true`` string. You can use :data:`os.environ` to perform any change to
|
set to the ``true`` string. You can use :data:`os.environ` to perform any change to
|
||||||
your spiders or your settings when ``scrapy check`` is used::
|
your spiders or your settings when ``scrapy check`` is used:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class ExampleSpider(scrapy.Spider):
|
class ExampleSpider(scrapy.Spider):
|
||||||
name = 'example'
|
name = "example"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if os.environ.get('SCRAPY_CHECK'):
|
if os.environ.get("SCRAPY_CHECK"):
|
||||||
pass # Do some scraper adjustments when a check is running
|
pass # Do some scraper adjustments when a check is running
|
||||||
|
|
|
||||||
|
|
@ -58,49 +58,58 @@ There are several use cases for coroutines in Scrapy.
|
||||||
|
|
||||||
Code that would return Deferreds when written for previous Scrapy versions,
|
Code that would return Deferreds when written for previous Scrapy versions,
|
||||||
such as downloader middlewares and signal handlers, can be rewritten to be
|
such as downloader middlewares and signal handlers, can be rewritten to be
|
||||||
shorter and cleaner::
|
shorter and cleaner:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
|
|
||||||
|
|
||||||
class DbPipeline:
|
class DbPipeline:
|
||||||
def _update_item(self, data, item):
|
def _update_item(self, data, item):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
adapter['field'] = data
|
adapter["field"] = data
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def process_item(self, item, spider):
|
def process_item(self, item, spider):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
dfd = db.get_some_data(adapter['id'])
|
dfd = db.get_some_data(adapter["id"])
|
||||||
dfd.addCallback(self._update_item, item)
|
dfd.addCallback(self._update_item, item)
|
||||||
return dfd
|
return dfd
|
||||||
|
|
||||||
becomes::
|
becomes:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
|
|
||||||
|
|
||||||
class DbPipeline:
|
class DbPipeline:
|
||||||
async def process_item(self, item, spider):
|
async def process_item(self, item, spider):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
adapter['field'] = await db.get_some_data(adapter['id'])
|
adapter["field"] = await db.get_some_data(adapter["id"])
|
||||||
return item
|
return item
|
||||||
|
|
||||||
Coroutines may be used to call asynchronous code. This includes other
|
Coroutines may be used to call asynchronous code. This includes other
|
||||||
coroutines, functions that return Deferreds and functions that return
|
coroutines, functions that return Deferreds and functions that return
|
||||||
:term:`awaitable objects <awaitable>` such as :class:`~asyncio.Future`.
|
:term:`awaitable objects <awaitable>` such as :class:`~asyncio.Future`.
|
||||||
This means you can use many useful Python libraries providing such code::
|
This means you can use many useful Python libraries providing such code:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpiderDeferred(Spider):
|
class MySpiderDeferred(Spider):
|
||||||
# ...
|
# ...
|
||||||
async def parse(self, response):
|
async def parse(self, response):
|
||||||
additional_response = await treq.get('https://additional.url')
|
additional_response = await treq.get("https://additional.url")
|
||||||
additional_data = await treq.content(additional_response)
|
additional_data = await treq.content(additional_response)
|
||||||
# ... use response and additional_data to yield items and requests
|
# ... use response and additional_data to yield items and requests
|
||||||
|
|
||||||
|
|
||||||
class MySpiderAsyncio(Spider):
|
class MySpiderAsyncio(Spider):
|
||||||
# ...
|
# ...
|
||||||
async def parse(self, response):
|
async def parse(self, response):
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get('https://additional.url') as additional_response:
|
async with session.get("https://additional.url") as additional_response:
|
||||||
additional_data = await additional_response.text()
|
additional_data = await additional_response.text()
|
||||||
# ... use response and additional_data to yield items and requests
|
# ... use response and additional_data to yield items and requests
|
||||||
|
|
||||||
|
|
@ -192,7 +201,9 @@ while maintaining support for older Scrapy versions, you may define
|
||||||
:term:`asynchronous generator` version of that method with an alternative name:
|
:term:`asynchronous generator` version of that method with an alternative name:
|
||||||
``process_spider_output_async``.
|
``process_spider_output_async``.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class UniversalSpiderMiddleware:
|
class UniversalSpiderMiddleware:
|
||||||
def process_spider_output(self, response, result, spider):
|
def process_spider_output(self, response, result, spider):
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,24 @@ Debugging Spiders
|
||||||
=================
|
=================
|
||||||
|
|
||||||
This document explains the most common techniques for debugging spiders.
|
This document explains the most common techniques for debugging spiders.
|
||||||
Consider the following Scrapy spider below::
|
Consider the following Scrapy spider below:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from myproject.items import MyItem
|
from myproject.items import MyItem
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
start_urls = (
|
start_urls = (
|
||||||
'http://example.com/page1',
|
"http://example.com/page1",
|
||||||
'http://example.com/page2',
|
"http://example.com/page2",
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
# <processing code not shown>
|
# <processing code not shown>
|
||||||
# collect `item_urls`
|
# collect `item_urls`
|
||||||
for item_url in item_urls:
|
for item_url in item_urls:
|
||||||
yield scrapy.Request(item_url, self.parse_item)
|
yield scrapy.Request(item_url, self.parse_item)
|
||||||
|
|
||||||
|
|
@ -28,7 +31,9 @@ Consider the following Scrapy spider below::
|
||||||
item = MyItem()
|
item = MyItem()
|
||||||
# populate `item` fields
|
# populate `item` fields
|
||||||
# and extract item_details_url
|
# and extract item_details_url
|
||||||
yield scrapy.Request(item_details_url, self.parse_details, cb_kwargs={'item': item})
|
yield scrapy.Request(
|
||||||
|
item_details_url, self.parse_details, cb_kwargs={"item": item}
|
||||||
|
)
|
||||||
|
|
||||||
def parse_details(self, response, item):
|
def parse_details(self, response, item):
|
||||||
# populate more `item` fields
|
# populate more `item` fields
|
||||||
|
|
@ -103,10 +108,13 @@ showing the response received and the output. How to debug the situation when
|
||||||
.. highlight:: python
|
.. highlight:: python
|
||||||
|
|
||||||
Fortunately, the :command:`shell` is your bread and butter in this case (see
|
Fortunately, the :command:`shell` is your bread and butter in this case (see
|
||||||
:ref:`topics-shell-inspect-response`)::
|
:ref:`topics-shell-inspect-response`):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.shell import inspect_response
|
from scrapy.shell import inspect_response
|
||||||
|
|
||||||
|
|
||||||
def parse_details(self, response, item=None):
|
def parse_details(self, response, item=None):
|
||||||
if item:
|
if item:
|
||||||
# populate more `item` fields
|
# populate more `item` fields
|
||||||
|
|
@ -121,10 +129,13 @@ Open in browser
|
||||||
|
|
||||||
Sometimes you just want to see how a certain response looks in a browser, you
|
Sometimes you just want to see how a certain response looks in a browser, you
|
||||||
can use the ``open_in_browser`` function for that. Here is an example of how
|
can use the ``open_in_browser`` function for that. Here is an example of how
|
||||||
you would use it::
|
you would use it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.utils.response import open_in_browser
|
from scrapy.utils.response import open_in_browser
|
||||||
|
|
||||||
|
|
||||||
def parse_details(self, response):
|
def parse_details(self, response):
|
||||||
if "item name" not in response.body:
|
if "item name" not in response.body:
|
||||||
open_in_browser(response)
|
open_in_browser(response)
|
||||||
|
|
@ -138,14 +149,16 @@ Logging
|
||||||
|
|
||||||
Logging is another useful option for getting information about your spider run.
|
Logging is another useful option for getting information about your spider run.
|
||||||
Although not as convenient, it comes with the advantage that the logs will be
|
Although not as convenient, it comes with the advantage that the logs will be
|
||||||
available in all future runs should they be necessary again::
|
available in all future runs should they be necessary again:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse_details(self, response, item=None):
|
def parse_details(self, response, item=None):
|
||||||
if item:
|
if item:
|
||||||
# populate more `item` fields
|
# populate more `item` fields
|
||||||
return item
|
return item
|
||||||
else:
|
else:
|
||||||
self.logger.warning('No item received for %s', response.url)
|
self.logger.warning("No item received for %s", response.url)
|
||||||
|
|
||||||
For more information, check the :ref:`topics-logging` section.
|
For more information, check the :ref:`topics-logging` section.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -237,17 +237,19 @@ on the request and open ``Open in new tab`` to get a better overview.
|
||||||
:alt: JSON-object returned from the quotes.toscrape API
|
:alt: JSON-object returned from the quotes.toscrape API
|
||||||
|
|
||||||
With this response we can now easily parse the JSON-object and
|
With this response we can now easily parse the JSON-object and
|
||||||
also request each page to get every quote on the site::
|
also request each page to get every quote on the site:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
class QuoteSpider(scrapy.Spider):
|
class QuoteSpider(scrapy.Spider):
|
||||||
name = 'quote'
|
name = "quote"
|
||||||
allowed_domains = ['quotes.toscrape.com']
|
allowed_domains = ["quotes.toscrape.com"]
|
||||||
page = 1
|
page = 1
|
||||||
start_urls = ['https://quotes.toscrape.com/api/quotes?page=1']
|
start_urls = ["https://quotes.toscrape.com/api/quotes?page=1"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
data = json.loads(response.text)
|
data = json.loads(response.text)
|
||||||
|
|
@ -275,7 +277,9 @@ requests, as we could need to add ``headers`` or ``cookies`` to make it work.
|
||||||
In those cases you can export the requests in `cURL <https://curl.haxx.se/>`_
|
In those cases you can export the requests in `cURL <https://curl.haxx.se/>`_
|
||||||
format, by right-clicking on each of them in the network tool and using the
|
format, by right-clicking on each of them in the network tool and using the
|
||||||
:meth:`~scrapy.Request.from_curl()` method to generate an equivalent
|
:meth:`~scrapy.Request.from_curl()` method to generate an equivalent
|
||||||
request::
|
request:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy import Request
|
from scrapy import Request
|
||||||
|
|
||||||
|
|
@ -286,7 +290,8 @@ request::
|
||||||
"-Requested-With: XMLHttpRequest' -H 'Proxy-Authorization: Basic QFRLLTAzM"
|
"-Requested-With: XMLHttpRequest' -H 'Proxy-Authorization: Basic QFRLLTAzM"
|
||||||
"zEwZTAxLTk5MWUtNDFiNC1iZWRmLTJjNGI4M2ZiNDBmNDpAVEstMDMzMTBlMDEtOTkxZS00MW"
|
"zEwZTAxLTk5MWUtNDFiNC1iZWRmLTJjNGI4M2ZiNDBmNDpAVEstMDMzMTBlMDEtOTkxZS00MW"
|
||||||
"I0LWJlZGYtMmM0YjgzZmI0MGY0' -H 'Connection: keep-alive' -H 'Referer: http"
|
"I0LWJlZGYtMmM0YjgzZmI0MGY0' -H 'Connection: keep-alive' -H 'Referer: http"
|
||||||
"://quotes.toscrape.com/scroll' -H 'Cache-Control: max-age=0'")
|
"://quotes.toscrape.com/scroll' -H 'Cache-Control: max-age=0'"
|
||||||
|
)
|
||||||
|
|
||||||
Alternatively, if you want to know the arguments needed to recreate that
|
Alternatively, if you want to know the arguments needed to recreate that
|
||||||
request you can use the :func:`~scrapy.utils.curl.curl_to_request_kwargs`
|
request you can use the :func:`~scrapy.utils.curl.curl_to_request_kwargs`
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ To activate a downloader middleware component, add it to the
|
||||||
:setting:`DOWNLOADER_MIDDLEWARES` setting, which is a dict whose keys are the
|
:setting:`DOWNLOADER_MIDDLEWARES` setting, which is a dict whose keys are the
|
||||||
middleware class paths and their values are the middleware orders.
|
middleware class paths and their values are the middleware orders.
|
||||||
|
|
||||||
Here's an example::
|
Here's an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
DOWNLOADER_MIDDLEWARES = {
|
DOWNLOADER_MIDDLEWARES = {
|
||||||
'myproject.middlewares.CustomDownloaderMiddleware': 543,
|
"myproject.middlewares.CustomDownloaderMiddleware": 543,
|
||||||
}
|
}
|
||||||
|
|
||||||
The :setting:`DOWNLOADER_MIDDLEWARES` setting is merged with the
|
The :setting:`DOWNLOADER_MIDDLEWARES` setting is merged with the
|
||||||
|
|
@ -42,11 +44,13 @@ previous (or subsequent) middleware being applied.
|
||||||
If you want to disable a built-in middleware (the ones defined in
|
If you want to disable a built-in middleware (the ones defined in
|
||||||
:setting:`DOWNLOADER_MIDDLEWARES_BASE` and enabled by default) you must define it
|
:setting:`DOWNLOADER_MIDDLEWARES_BASE` and enabled by default) you must define it
|
||||||
in your project's :setting:`DOWNLOADER_MIDDLEWARES` setting and assign ``None``
|
in your project's :setting:`DOWNLOADER_MIDDLEWARES` setting and assign ``None``
|
||||||
as its value. For example, if you want to disable the user-agent middleware::
|
as its value. For example, if you want to disable the user-agent middleware:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
DOWNLOADER_MIDDLEWARES = {
|
DOWNLOADER_MIDDLEWARES = {
|
||||||
'myproject.middlewares.CustomDownloaderMiddleware': 543,
|
"myproject.middlewares.CustomDownloaderMiddleware": 543,
|
||||||
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
|
"scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
Finally, keep in mind that some middlewares may need to be enabled through a
|
Finally, keep in mind that some middlewares may need to be enabled through a
|
||||||
|
|
@ -226,20 +230,25 @@ There is support for keeping multiple cookie sessions per spider by using the
|
||||||
:reqmeta:`cookiejar` Request meta key. By default it uses a single cookie jar
|
:reqmeta:`cookiejar` Request meta key. By default it uses a single cookie jar
|
||||||
(session), but you can pass an identifier to use different ones.
|
(session), but you can pass an identifier to use different ones.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
for i, url in enumerate(urls):
|
for i, url in enumerate(urls):
|
||||||
yield scrapy.Request(url, meta={'cookiejar': i},
|
yield scrapy.Request(url, meta={"cookiejar": i}, callback=self.parse_page)
|
||||||
callback=self.parse_page)
|
|
||||||
|
|
||||||
Keep in mind that the :reqmeta:`cookiejar` meta key is not "sticky". You need to keep
|
Keep in mind that the :reqmeta:`cookiejar` meta key is not "sticky". You need to keep
|
||||||
passing it along on subsequent requests. For example::
|
passing it along on subsequent requests. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse_page(self, response):
|
def parse_page(self, response):
|
||||||
# do some processing
|
# do some processing
|
||||||
return scrapy.Request("http://www.example.com/otherpage",
|
return scrapy.Request(
|
||||||
meta={'cookiejar': response.meta['cookiejar']},
|
"http://www.example.com/otherpage",
|
||||||
callback=self.parse_other_page)
|
meta={"cookiejar": response.meta["cookiejar"]},
|
||||||
|
callback=self.parse_other_page,
|
||||||
|
)
|
||||||
|
|
||||||
.. setting:: COOKIES_ENABLED
|
.. setting:: COOKIES_ENABLED
|
||||||
|
|
||||||
|
|
@ -339,16 +348,19 @@ HttpAuthMiddleware
|
||||||
domain of the first request, which will work for some spiders but not
|
domain of the first request, which will work for some spiders but not
|
||||||
for others. In the future the middleware will produce an error instead.
|
for others. In the future the middleware will produce an error instead.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spiders import CrawlSpider
|
from scrapy.spiders import CrawlSpider
|
||||||
|
|
||||||
|
|
||||||
class SomeIntranetSiteSpider(CrawlSpider):
|
class SomeIntranetSiteSpider(CrawlSpider):
|
||||||
|
|
||||||
http_user = 'someuser'
|
http_user = "someuser"
|
||||||
http_pass = 'somepass'
|
http_pass = "somepass"
|
||||||
http_auth_domain = 'intranet.example.com'
|
http_auth_domain = "intranet.example.com"
|
||||||
name = 'intranet.example.com'
|
name = "intranet.example.com"
|
||||||
|
|
||||||
# .. rest of the spider code omitted ...
|
# .. rest of the spider code omitted ...
|
||||||
|
|
||||||
|
|
@ -792,7 +804,9 @@ If you want to handle some redirect status codes in your spider, you can
|
||||||
specify these in the ``handle_httpstatus_list`` spider attribute.
|
specify these in the ``handle_httpstatus_list`` spider attribute.
|
||||||
|
|
||||||
For example, if you want the redirect middleware to ignore 301 and 302
|
For example, if you want the redirect middleware to ignore 301 and 302
|
||||||
responses (and pass them through to your spider) you can do this::
|
responses (and pass them through to your spider) you can do this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(CrawlSpider):
|
class MySpider(CrawlSpider):
|
||||||
handle_httpstatus_list = [301, 302]
|
handle_httpstatus_list = [301, 302]
|
||||||
|
|
|
||||||
|
|
@ -119,16 +119,20 @@ data from it depends on the type of response:
|
||||||
<topics-selectors>` as usual.
|
<topics-selectors>` as usual.
|
||||||
|
|
||||||
- If the response is JSON, use :func:`json.loads` to load the desired data from
|
- If the response is JSON, use :func:`json.loads` to load the desired data from
|
||||||
:attr:`response.text <scrapy.http.TextResponse.text>`::
|
:attr:`response.text <scrapy.http.TextResponse.text>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
data = json.loads(response.text)
|
data = json.loads(response.text)
|
||||||
|
|
||||||
If the desired data is inside HTML or XML code embedded within JSON data,
|
If the desired data is inside HTML or XML code embedded within JSON data,
|
||||||
you can load that HTML or XML code into a
|
you can load that HTML or XML code into a
|
||||||
:class:`~scrapy.Selector` and then
|
:class:`~scrapy.Selector` and then
|
||||||
:ref:`use it <topics-selectors>` as usual::
|
:ref:`use it <topics-selectors>` as usual:
|
||||||
|
|
||||||
selector = Selector(data['html'])
|
.. code-block:: python
|
||||||
|
|
||||||
|
selector = Selector(data["html"])
|
||||||
|
|
||||||
- If the response is JavaScript, or HTML with a ``<script/>`` element
|
- If the response is JavaScript, or HTML with a ``<script/>`` element
|
||||||
containing the desired data, see :ref:`topics-parsing-javascript`.
|
containing the desired data, see :ref:`topics-parsing-javascript`.
|
||||||
|
|
@ -250,11 +254,14 @@ automation. By installing the :ref:`asyncio reactor <install-asyncio>`,
|
||||||
it is possible to integrate ``asyncio``-based libraries which handle headless browsers.
|
it is possible to integrate ``asyncio``-based libraries which handle headless browsers.
|
||||||
|
|
||||||
One such library is `playwright-python`_ (an official Python port of `playwright`_).
|
One such library is `playwright-python`_ (an official Python port of `playwright`_).
|
||||||
The following is a simple snippet to illustrate its usage within a Scrapy spider::
|
The following is a simple snippet to illustrate its usage within a Scrapy spider:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from playwright.async_api import async_playwright
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
|
||||||
class PlaywrightSpider(scrapy.Spider):
|
class PlaywrightSpider(scrapy.Spider):
|
||||||
name = "playwright"
|
name = "playwright"
|
||||||
start_urls = ["data:,"] # avoid using the default Scrapy downloader
|
start_urls = ["data:,"] # avoid using the default Scrapy downloader
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,31 @@ Quick example
|
||||||
=============
|
=============
|
||||||
|
|
||||||
There are two ways to instantiate the mail sender. You can instantiate it using
|
There are two ways to instantiate the mail sender. You can instantiate it using
|
||||||
the standard ``__init__`` method::
|
the standard ``__init__`` method:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.mail import MailSender
|
from scrapy.mail import MailSender
|
||||||
|
|
||||||
mailer = MailSender()
|
mailer = MailSender()
|
||||||
|
|
||||||
Or you can instantiate it passing a Scrapy settings object, which will respect
|
Or you can instantiate it passing a Scrapy settings object, which will respect
|
||||||
the :ref:`settings <topics-email-settings>`::
|
the :ref:`settings <topics-email-settings>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
mailer = MailSender.from_settings(settings)
|
mailer = MailSender.from_settings(settings)
|
||||||
|
|
||||||
And here is how to use it to send an e-mail (without attachments)::
|
And here is how to use it to send an e-mail (without attachments):
|
||||||
|
|
||||||
mailer.send(to=["someone@example.com"], subject="Some subject", body="Some body", cc=["another@example.com"])
|
.. code-block:: python
|
||||||
|
|
||||||
|
mailer.send(
|
||||||
|
to=["someone@example.com"],
|
||||||
|
subject="Some subject",
|
||||||
|
body="Some body",
|
||||||
|
cc=["another@example.com"],
|
||||||
|
)
|
||||||
|
|
||||||
MailSender class reference
|
MailSender class reference
|
||||||
==========================
|
==========================
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,13 @@ CloseSpider
|
||||||
:param reason: the reason for closing
|
:param reason: the reason for closing
|
||||||
:type reason: str
|
:type reason: str
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse_page(self, response):
|
def parse_page(self, response):
|
||||||
if 'Bandwidth exceeded' in response.body:
|
if "Bandwidth exceeded" in response.body:
|
||||||
raise CloseSpider('bandwidth_exceeded')
|
raise CloseSpider("bandwidth_exceeded")
|
||||||
|
|
||||||
DontCloseSpider
|
DontCloseSpider
|
||||||
---------------
|
---------------
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,14 @@ the end of the exporting process
|
||||||
|
|
||||||
Here you can see an :doc:`Item Pipeline <item-pipeline>` which uses multiple
|
Here you can see an :doc:`Item Pipeline <item-pipeline>` which uses multiple
|
||||||
Item Exporters to group scraped items to different files according to the
|
Item Exporters to group scraped items to different files according to the
|
||||||
value of one of their fields::
|
value of one of their fields:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
from scrapy.exporters import XmlItemExporter
|
from scrapy.exporters import XmlItemExporter
|
||||||
|
|
||||||
|
|
||||||
class PerYearXmlExportPipeline:
|
class PerYearXmlExportPipeline:
|
||||||
"""Distribute items across multiple XML files according to their 'year' field"""
|
"""Distribute items across multiple XML files according to their 'year' field"""
|
||||||
|
|
||||||
|
|
@ -56,9 +59,9 @@ value of one of their fields::
|
||||||
|
|
||||||
def _exporter_for_item(self, item):
|
def _exporter_for_item(self, item):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
year = adapter['year']
|
year = adapter["year"]
|
||||||
if year not in self.year_to_exporter:
|
if year not in self.year_to_exporter:
|
||||||
xml_file = open(f'{year}.xml', 'wb')
|
xml_file = open(f"{year}.xml", "wb")
|
||||||
exporter = XmlItemExporter(xml_file)
|
exporter = XmlItemExporter(xml_file)
|
||||||
exporter.start_exporting()
|
exporter.start_exporting()
|
||||||
self.year_to_exporter[year] = (exporter, xml_file)
|
self.year_to_exporter[year] = (exporter, xml_file)
|
||||||
|
|
@ -94,12 +97,16 @@ If you use :class:`~scrapy.Item` you can declare a serializer in the
|
||||||
:ref:`field metadata <topics-items-fields>`. The serializer must be
|
:ref:`field metadata <topics-items-fields>`. The serializer must be
|
||||||
a callable which receives a value and returns its serialized form.
|
a callable which receives a value and returns its serialized form.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
def serialize_price(value):
|
def serialize_price(value):
|
||||||
return f'$ {str(value)}'
|
return f"$ {str(value)}"
|
||||||
|
|
||||||
|
|
||||||
class Product(scrapy.Item):
|
class Product(scrapy.Item):
|
||||||
name = scrapy.Field()
|
name = scrapy.Field()
|
||||||
|
|
@ -115,15 +122,17 @@ customize how your field value will be exported.
|
||||||
Make sure you call the base class :meth:`~BaseItemExporter.serialize_field()` method
|
Make sure you call the base class :meth:`~BaseItemExporter.serialize_field()` method
|
||||||
after your custom code.
|
after your custom code.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.exporters import XmlItemExporter
|
from scrapy.exporters import XmlItemExporter
|
||||||
|
|
||||||
class ProductXmlExporter(XmlItemExporter):
|
|
||||||
|
|
||||||
|
class ProductXmlExporter(XmlItemExporter):
|
||||||
def serialize_field(self, field, name, value):
|
def serialize_field(self, field, name, value):
|
||||||
if name == 'price':
|
if name == "price":
|
||||||
return f'$ {str(value)}'
|
return f"$ {str(value)}"
|
||||||
return super().serialize_field(field, name, value)
|
return super().serialize_field(field, name, value)
|
||||||
|
|
||||||
.. _topics-exporters-reference:
|
.. _topics-exporters-reference:
|
||||||
|
|
@ -132,10 +141,12 @@ Built-in Item Exporters reference
|
||||||
=================================
|
=================================
|
||||||
|
|
||||||
Here is a list of the Item Exporters bundled with Scrapy. Some of them contain
|
Here is a list of the Item Exporters bundled with Scrapy. Some of them contain
|
||||||
output examples, which assume you're exporting these two items::
|
output examples, which assume you're exporting these two items:
|
||||||
|
|
||||||
Item(name='Color TV', price='1200')
|
.. code-block:: python
|
||||||
Item(name='DVD player', price='200')
|
|
||||||
|
Item(name="Color TV", price="1200")
|
||||||
|
Item(name="DVD player", price="200")
|
||||||
|
|
||||||
BaseItemExporter
|
BaseItemExporter
|
||||||
----------------
|
----------------
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,13 @@ initialization code must be performed in the class ``__init__`` method.
|
||||||
|
|
||||||
To make an extension available, add it to the :setting:`EXTENSIONS` setting in
|
To make an extension available, add it to the :setting:`EXTENSIONS` setting in
|
||||||
your Scrapy settings. In :setting:`EXTENSIONS`, each extension is represented
|
your Scrapy settings. In :setting:`EXTENSIONS`, each extension is represented
|
||||||
by a string: the full Python path to the extension's class name. For example::
|
by a string: the full Python path to the extension's class name. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
EXTENSIONS = {
|
EXTENSIONS = {
|
||||||
'scrapy.extensions.corestats.CoreStats': 500,
|
"scrapy.extensions.corestats.CoreStats": 500,
|
||||||
'scrapy.extensions.telnet.TelnetConsole': 500,
|
"scrapy.extensions.telnet.TelnetConsole": 500,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,10 +66,12 @@ Disabling an extension
|
||||||
|
|
||||||
In order to disable an extension that comes enabled by default (i.e. those
|
In order to disable an extension that comes enabled by default (i.e. those
|
||||||
included in the :setting:`EXTENSIONS_BASE` setting) you must set its order to
|
included in the :setting:`EXTENSIONS_BASE` setting) you must set its order to
|
||||||
``None``. For example::
|
``None``. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
EXTENSIONS = {
|
EXTENSIONS = {
|
||||||
'scrapy.extensions.corestats.CoreStats': None,
|
"scrapy.extensions.corestats.CoreStats": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
Writing your own extension
|
Writing your own extension
|
||||||
|
|
@ -98,7 +102,9 @@ in the previous section. This extension will log a message every time:
|
||||||
The extension will be enabled through the ``MYEXT_ENABLED`` setting and the
|
The extension will be enabled through the ``MYEXT_ENABLED`` setting and the
|
||||||
number of items will be specified through the ``MYEXT_ITEMCOUNT`` setting.
|
number of items will be specified through the ``MYEXT_ITEMCOUNT`` setting.
|
||||||
|
|
||||||
Here is the code of such extension::
|
Here is the code of such extension:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from scrapy import signals
|
from scrapy import signals
|
||||||
|
|
@ -106,8 +112,8 @@ Here is the code of such extension::
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class SpiderOpenCloseLogging:
|
|
||||||
|
|
||||||
|
class SpiderOpenCloseLogging:
|
||||||
def __init__(self, item_count):
|
def __init__(self, item_count):
|
||||||
self.item_count = item_count
|
self.item_count = item_count
|
||||||
self.items_scraped = 0
|
self.items_scraped = 0
|
||||||
|
|
@ -116,11 +122,11 @@ Here is the code of such extension::
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler):
|
||||||
# first check if the extension should be enabled and raise
|
# first check if the extension should be enabled and raise
|
||||||
# NotConfigured otherwise
|
# NotConfigured otherwise
|
||||||
if not crawler.settings.getbool('MYEXT_ENABLED'):
|
if not crawler.settings.getbool("MYEXT_ENABLED"):
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
|
|
||||||
# get the number of items from settings
|
# get the number of items from settings
|
||||||
item_count = crawler.settings.getint('MYEXT_ITEMCOUNT', 1000)
|
item_count = crawler.settings.getint("MYEXT_ITEMCOUNT", 1000)
|
||||||
|
|
||||||
# instantiate the extension object
|
# instantiate the extension object
|
||||||
ext = cls(item_count)
|
ext = cls(item_count)
|
||||||
|
|
|
||||||
|
|
@ -290,10 +290,11 @@ class, which is the default value of the ``item_filter`` :ref:`feed option <feed
|
||||||
You can create your own custom filtering class by implementing :class:`~scrapy.extensions.feedexport.ItemFilter`'s
|
You can create your own custom filtering class by implementing :class:`~scrapy.extensions.feedexport.ItemFilter`'s
|
||||||
method ``accepts`` and taking ``feed_options`` as an argument.
|
method ``accepts`` and taking ``feed_options`` as an argument.
|
||||||
|
|
||||||
For instance::
|
For instance:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MyCustomFilter:
|
class MyCustomFilter:
|
||||||
|
|
||||||
def __init__(self, feed_options):
|
def __init__(self, feed_options):
|
||||||
self.feed_options = feed_options
|
self.feed_options = feed_options
|
||||||
|
|
||||||
|
|
@ -594,23 +595,27 @@ For a complete list of available values, access the `Canned ACL`_ section on Ama
|
||||||
FEED_STORAGES_BASE
|
FEED_STORAGES_BASE
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'': 'scrapy.extensions.feedexport.FileFeedStorage',
|
"": "scrapy.extensions.feedexport.FileFeedStorage",
|
||||||
'file': 'scrapy.extensions.feedexport.FileFeedStorage',
|
"file": "scrapy.extensions.feedexport.FileFeedStorage",
|
||||||
'stdout': 'scrapy.extensions.feedexport.StdoutFeedStorage',
|
"stdout": "scrapy.extensions.feedexport.StdoutFeedStorage",
|
||||||
's3': 'scrapy.extensions.feedexport.S3FeedStorage',
|
"s3": "scrapy.extensions.feedexport.S3FeedStorage",
|
||||||
'ftp': 'scrapy.extensions.feedexport.FTPFeedStorage',
|
"ftp": "scrapy.extensions.feedexport.FTPFeedStorage",
|
||||||
}
|
}
|
||||||
|
|
||||||
A dict containing the built-in feed storage backends supported by Scrapy. You
|
A dict containing the built-in feed storage backends supported by Scrapy. You
|
||||||
can disable any of these backends by assigning ``None`` to their URI scheme in
|
can disable any of these backends by assigning ``None`` to their URI scheme in
|
||||||
:setting:`FEED_STORAGES`. E.g., to disable the built-in FTP storage backend
|
:setting:`FEED_STORAGES`. E.g., to disable the built-in FTP storage backend
|
||||||
(without replacement), place this in your ``settings.py``::
|
(without replacement), place this in your ``settings.py``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
FEED_STORAGES = {
|
FEED_STORAGES = {
|
||||||
'ftp': None,
|
"ftp": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
.. setting:: FEED_EXPORTERS
|
.. setting:: FEED_EXPORTERS
|
||||||
|
|
@ -628,26 +633,30 @@ serialization formats and the values are paths to :ref:`Item exporter
|
||||||
|
|
||||||
FEED_EXPORTERS_BASE
|
FEED_EXPORTERS_BASE
|
||||||
-------------------
|
-------------------
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'json': 'scrapy.exporters.JsonItemExporter',
|
"json": "scrapy.exporters.JsonItemExporter",
|
||||||
'jsonlines': 'scrapy.exporters.JsonLinesItemExporter',
|
"jsonlines": "scrapy.exporters.JsonLinesItemExporter",
|
||||||
'jsonl': 'scrapy.exporters.JsonLinesItemExporter',
|
"jsonl": "scrapy.exporters.JsonLinesItemExporter",
|
||||||
'jl': 'scrapy.exporters.JsonLinesItemExporter',
|
"jl": "scrapy.exporters.JsonLinesItemExporter",
|
||||||
'csv': 'scrapy.exporters.CsvItemExporter',
|
"csv": "scrapy.exporters.CsvItemExporter",
|
||||||
'xml': 'scrapy.exporters.XmlItemExporter',
|
"xml": "scrapy.exporters.XmlItemExporter",
|
||||||
'marshal': 'scrapy.exporters.MarshalItemExporter',
|
"marshal": "scrapy.exporters.MarshalItemExporter",
|
||||||
'pickle': 'scrapy.exporters.PickleItemExporter',
|
"pickle": "scrapy.exporters.PickleItemExporter",
|
||||||
}
|
}
|
||||||
|
|
||||||
A dict containing the built-in feed exporters supported by Scrapy. You can
|
A dict containing the built-in feed exporters supported by Scrapy. You can
|
||||||
disable any of these exporters by assigning ``None`` to their serialization
|
disable any of these exporters by assigning ``None`` to their serialization
|
||||||
format in :setting:`FEED_EXPORTERS`. E.g., to disable the built-in CSV exporter
|
format in :setting:`FEED_EXPORTERS`. E.g., to disable the built-in CSV exporter
|
||||||
(without replacement), place this in your ``settings.py``::
|
(without replacement), place this in your ``settings.py``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
FEED_EXPORTERS = {
|
FEED_EXPORTERS = {
|
||||||
'csv': None,
|
"csv": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -677,7 +686,9 @@ generated:
|
||||||
number by introducing leading zeroes as needed, use ``%(batch_id)05d``
|
number by introducing leading zeroes as needed, use ``%(batch_id)05d``
|
||||||
(e.g. ``3`` becomes ``00003``, ``123`` becomes ``00123``).
|
(e.g. ``3`` becomes ``00003``, ``123`` becomes ``00123``).
|
||||||
|
|
||||||
For instance, if your settings include::
|
For instance, if your settings include:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
FEED_EXPORT_BATCH_ITEM_COUNT = 100
|
FEED_EXPORT_BATCH_ITEM_COUNT = 100
|
||||||
|
|
||||||
|
|
@ -746,16 +757,20 @@ The function signature should be as follows:
|
||||||
For example, to include the :attr:`name <scrapy.Spider.name>` of the
|
For example, to include the :attr:`name <scrapy.Spider.name>` of the
|
||||||
source spider in the feed URI:
|
source spider in the feed URI:
|
||||||
|
|
||||||
#. Define the following function somewhere in your project::
|
#. Define the following function somewhere in your project:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
# myproject/utils.py
|
# myproject/utils.py
|
||||||
def uri_params(params, spider):
|
def uri_params(params, spider):
|
||||||
return {**params, 'spider_name': spider.name}
|
return {**params, "spider_name": spider.name}
|
||||||
|
|
||||||
#. Point :setting:`FEED_URI_PARAMS` to that function in your settings::
|
#. Point :setting:`FEED_URI_PARAMS` to that function in your settings:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
# myproject/settings.py
|
# myproject/settings.py
|
||||||
FEED_URI_PARAMS = 'myproject.utils.uri_params'
|
FEED_URI_PARAMS = "myproject.utils.uri_params"
|
||||||
|
|
||||||
#. Use ``%(spider_name)s`` in your feed URI::
|
#. Use ``%(spider_name)s`` in your feed URI::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,19 +81,23 @@ Price validation and dropping items with no prices
|
||||||
Let's take a look at the following hypothetical pipeline that adjusts the
|
Let's take a look at the following hypothetical pipeline that adjusts the
|
||||||
``price`` attribute for those items that do not include VAT
|
``price`` attribute for those items that do not include VAT
|
||||||
(``price_excludes_vat`` attribute), and drops those items which don't
|
(``price_excludes_vat`` attribute), and drops those items which don't
|
||||||
contain a price::
|
contain a price:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
from scrapy.exceptions import DropItem
|
from scrapy.exceptions import DropItem
|
||||||
|
|
||||||
|
|
||||||
class PricePipeline:
|
class PricePipeline:
|
||||||
|
|
||||||
vat_factor = 1.15
|
vat_factor = 1.15
|
||||||
|
|
||||||
def process_item(self, item, spider):
|
def process_item(self, item, spider):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
if adapter.get('price'):
|
if adapter.get("price"):
|
||||||
if adapter.get('price_excludes_vat'):
|
if adapter.get("price_excludes_vat"):
|
||||||
adapter['price'] = adapter['price'] * self.vat_factor
|
adapter["price"] = adapter["price"] * self.vat_factor
|
||||||
return item
|
return item
|
||||||
else:
|
else:
|
||||||
raise DropItem(f"Missing price in {item}")
|
raise DropItem(f"Missing price in {item}")
|
||||||
|
|
@ -104,16 +108,18 @@ Write items to a JSON lines file
|
||||||
|
|
||||||
The following pipeline stores all scraped items (from all spiders) into a
|
The following pipeline stores all scraped items (from all spiders) into a
|
||||||
single ``items.jsonl`` file, containing one item per line serialized in JSON
|
single ``items.jsonl`` file, containing one item per line serialized in JSON
|
||||||
format::
|
format:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
|
|
||||||
class JsonWriterPipeline:
|
|
||||||
|
|
||||||
|
class JsonWriterPipeline:
|
||||||
def open_spider(self, spider):
|
def open_spider(self, spider):
|
||||||
self.file = open('items.jsonl', 'w')
|
self.file = open("items.jsonl", "w")
|
||||||
|
|
||||||
def close_spider(self, spider):
|
def close_spider(self, spider):
|
||||||
self.file.close()
|
self.file.close()
|
||||||
|
|
@ -135,14 +141,17 @@ MongoDB address and database name are specified in Scrapy settings;
|
||||||
MongoDB collection is named after item class.
|
MongoDB collection is named after item class.
|
||||||
|
|
||||||
The main point of this example is to show how to use :meth:`from_crawler`
|
The main point of this example is to show how to use :meth:`from_crawler`
|
||||||
method and how to clean up the resources properly.::
|
method and how to clean up the resources properly.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
|
|
||||||
|
|
||||||
class MongoPipeline:
|
class MongoPipeline:
|
||||||
|
|
||||||
collection_name = 'scrapy_items'
|
collection_name = "scrapy_items"
|
||||||
|
|
||||||
def __init__(self, mongo_uri, mongo_db):
|
def __init__(self, mongo_uri, mongo_db):
|
||||||
self.mongo_uri = mongo_uri
|
self.mongo_uri = mongo_uri
|
||||||
|
|
@ -151,8 +160,8 @@ method and how to clean up the resources properly.::
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler):
|
||||||
return cls(
|
return cls(
|
||||||
mongo_uri=crawler.settings.get('MONGO_URI'),
|
mongo_uri=crawler.settings.get("MONGO_URI"),
|
||||||
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
|
mongo_db=crawler.settings.get("MONGO_DATABASE", "items"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def open_spider(self, spider):
|
def open_spider(self, spider):
|
||||||
|
|
@ -183,7 +192,7 @@ render a screenshot of the item URL. After the request response is downloaded,
|
||||||
the item pipeline saves the screenshot to a file and adds the filename to the
|
the item pipeline saves the screenshot to a file and adds the filename to the
|
||||||
item.
|
item.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -231,23 +240,24 @@ Duplicates filter
|
||||||
|
|
||||||
A filter that looks for duplicate items, and drops those items that were
|
A filter that looks for duplicate items, and drops those items that were
|
||||||
already processed. Let's say that our items have a unique id, but our spider
|
already processed. Let's say that our items have a unique id, but our spider
|
||||||
returns multiples items with the same id::
|
returns multiples items with the same id:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
from scrapy.exceptions import DropItem
|
from scrapy.exceptions import DropItem
|
||||||
|
|
||||||
class DuplicatesPipeline:
|
|
||||||
|
|
||||||
|
class DuplicatesPipeline:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ids_seen = set()
|
self.ids_seen = set()
|
||||||
|
|
||||||
def process_item(self, item, spider):
|
def process_item(self, item, spider):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
if adapter['id'] in self.ids_seen:
|
if adapter["id"] in self.ids_seen:
|
||||||
raise DropItem(f"Duplicate item found: {item!r}")
|
raise DropItem(f"Duplicate item found: {item!r}")
|
||||||
else:
|
else:
|
||||||
self.ids_seen.add(adapter['id'])
|
self.ids_seen.add(adapter["id"])
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -255,11 +265,13 @@ Activating an Item Pipeline component
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
To activate an Item Pipeline component you must add its class to the
|
To activate an Item Pipeline component you must add its class to the
|
||||||
:setting:`ITEM_PIPELINES` setting, like in the following example::
|
:setting:`ITEM_PIPELINES` setting, like in the following example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
ITEM_PIPELINES = {
|
ITEM_PIPELINES = {
|
||||||
'myproject.pipelines.PricePipeline': 300,
|
"myproject.pipelines.PricePipeline": 300,
|
||||||
'myproject.pipelines.JsonWriterPipeline': 800,
|
"myproject.pipelines.JsonWriterPipeline": 800,
|
||||||
}
|
}
|
||||||
|
|
||||||
The integer values you assign to classes in this setting determine the
|
The integer values you assign to classes in this setting determine the
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,13 @@ make it the most feature-complete item type:
|
||||||
:class:`Field` objects used in the :ref:`Item declaration
|
:class:`Field` objects used in the :ref:`Item declaration
|
||||||
<topics-items-declaring>`.
|
<topics-items-declaring>`.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.item import Item, Field
|
from scrapy.item import Item, Field
|
||||||
|
|
||||||
|
|
||||||
class CustomItem(Item):
|
class CustomItem(Item):
|
||||||
one_field = Field()
|
one_field = Field()
|
||||||
another_field = Field()
|
another_field = Field()
|
||||||
|
|
@ -102,10 +105,13 @@ Additionally, ``dataclass`` items also allow to:
|
||||||
* define custom field metadata through :func:`dataclasses.field`, which can be used to
|
* define custom field metadata through :func:`dataclasses.field`, which can be used to
|
||||||
:ref:`customize serialization <topics-exporters-field-serialization>`.
|
:ref:`customize serialization <topics-exporters-field-serialization>`.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CustomItem:
|
class CustomItem:
|
||||||
one_field: str
|
one_field: str
|
||||||
|
|
@ -133,10 +139,13 @@ Additionally, ``attr.s`` items also allow to:
|
||||||
|
|
||||||
In order to use this type, the :doc:`attrs package <attrs:index>` needs to be installed.
|
In order to use this type, the :doc:`attrs package <attrs:index>` needs to be installed.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class CustomItem:
|
class CustomItem:
|
||||||
one_field = attr.ib()
|
one_field = attr.ib()
|
||||||
|
|
@ -152,10 +161,13 @@ Declaring Item subclasses
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
Item subclasses are declared using a simple class definition syntax and
|
Item subclasses are declared using a simple class definition syntax and
|
||||||
:class:`Field` objects. Here is an example::
|
:class:`Field` objects. Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class Product(scrapy.Item):
|
class Product(scrapy.Item):
|
||||||
name = scrapy.Field()
|
name = scrapy.Field()
|
||||||
price = scrapy.Field()
|
price = scrapy.Field()
|
||||||
|
|
@ -347,17 +359,21 @@ Extending Item subclasses
|
||||||
You can extend Items (to add more fields or to change some metadata for some
|
You can extend Items (to add more fields or to change some metadata for some
|
||||||
fields) by declaring a subclass of your original Item.
|
fields) by declaring a subclass of your original Item.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class DiscountedProduct(Product):
|
class DiscountedProduct(Product):
|
||||||
discount_percent = scrapy.Field(serializer=str)
|
discount_percent = scrapy.Field(serializer=str)
|
||||||
discount_expiration_date = scrapy.Field()
|
discount_expiration_date = scrapy.Field()
|
||||||
|
|
||||||
You can also extend field metadata by using the previous field metadata and
|
You can also extend field metadata by using the previous field metadata and
|
||||||
appending more values, or changing existing values, like this::
|
appending more values, or changing existing values, like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class SpecificProduct(Product):
|
class SpecificProduct(Product):
|
||||||
name = scrapy.Field(Product.fields['name'], serializer=my_serializer)
|
name = scrapy.Field(Product.fields["name"], serializer=my_serializer)
|
||||||
|
|
||||||
That adds (or replaces) the ``serializer`` metadata key for the ``name`` field,
|
That adds (or replaces) the ``serializer`` metadata key for the ``name`` field,
|
||||||
keeping all the previously existing metadata values.
|
keeping all the previously existing metadata values.
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,13 @@ loading that attribute from the job directory, when the spider starts and
|
||||||
stops.
|
stops.
|
||||||
|
|
||||||
Here's an example of a callback that uses the spider state (other spider code
|
Here's an example of a callback that uses the spider state (other spider code
|
||||||
is omitted for brevity)::
|
is omitted for brevity):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse_item(self, response):
|
def parse_item(self, response):
|
||||||
# parse item here
|
# parse item here
|
||||||
self.state['items_count'] = self.state.get('items_count', 0) + 1
|
self.state["items_count"] = self.state.get("items_count", 0) + 1
|
||||||
|
|
||||||
Persistence gotchas
|
Persistence gotchas
|
||||||
===================
|
===================
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ through a set of :class:`~scrapy.spiders.Rule` objects.
|
||||||
|
|
||||||
You can also use link extractors in regular spiders. For example, you can instantiate
|
You can also use link extractors in regular spiders. For example, you can instantiate
|
||||||
:class:`LinkExtractor <scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor>` into a class
|
:class:`LinkExtractor <scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor>` into a class
|
||||||
variable in your spider, and use it from your spider callbacks::
|
variable in your spider, and use it from your spider callbacks:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for link in self.link_extractor.extract_links(response):
|
for link in self.link_extractor.extract_links(response):
|
||||||
|
|
@ -132,7 +134,9 @@ LxmlLinkExtractor
|
||||||
|
|
||||||
.. highlight:: python
|
.. highlight:: python
|
||||||
|
|
||||||
You can use the following function in ``process_value``::
|
You can use the following function in ``process_value``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def process_value(value):
|
def process_value(value):
|
||||||
m = re.search("javascript:goToPage\('(.*?)'", value)
|
m = re.search("javascript:goToPage\('(.*?)'", value)
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,21 @@ using a proper processing function.
|
||||||
|
|
||||||
Here is a typical Item Loader usage in a :ref:`Spider <topics-spiders>`, using
|
Here is a typical Item Loader usage in a :ref:`Spider <topics-spiders>`, using
|
||||||
the :ref:`Product item <topics-items-declaring>` declared in the :ref:`Items
|
the :ref:`Product item <topics-items-declaring>` declared in the :ref:`Items
|
||||||
chapter <topics-items>`::
|
chapter <topics-items>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.loader import ItemLoader
|
from scrapy.loader import ItemLoader
|
||||||
from myproject.items import Product
|
from myproject.items import Product
|
||||||
|
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
l = ItemLoader(item=Product(), response=response)
|
l = ItemLoader(item=Product(), response=response)
|
||||||
l.add_xpath('name', '//div[@class="product_name"]')
|
l.add_xpath("name", '//div[@class="product_name"]')
|
||||||
l.add_xpath('name', '//div[@class="product_title"]')
|
l.add_xpath("name", '//div[@class="product_title"]')
|
||||||
l.add_xpath('price', '//p[@id="price"]')
|
l.add_xpath("price", '//p[@id="price"]')
|
||||||
l.add_css('stock', 'p#stock')
|
l.add_css("stock", "p#stock")
|
||||||
l.add_value('last_updated', 'today') # you can also use literal values
|
l.add_value("last_updated", "today") # you can also use literal values
|
||||||
return l.load_item()
|
return l.load_item()
|
||||||
|
|
||||||
By quickly looking at that code, we can see the ``name`` field is being
|
By quickly looking at that code, we can see the ``name`` field is being
|
||||||
|
|
@ -93,11 +96,14 @@ will be populated incrementally using the loader's :meth:`~ItemLoader.add_xpath`
|
||||||
:meth:`~ItemLoader.add_css` and :meth:`~ItemLoader.add_value` methods.
|
:meth:`~ItemLoader.add_css` and :meth:`~ItemLoader.add_value` methods.
|
||||||
|
|
||||||
One approach to overcome this is to define items using the
|
One approach to overcome this is to define items using the
|
||||||
:func:`~dataclasses.field` function, with a ``default`` argument::
|
:func:`~dataclasses.field` function, with a ``default`` argument:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class InventoryItem:
|
class InventoryItem:
|
||||||
name: Optional[str] = field(default=None)
|
name: Optional[str] = field(default=None)
|
||||||
|
|
@ -122,14 +128,16 @@ processor). The result of the output processor is the final value that gets
|
||||||
assigned to the item.
|
assigned to the item.
|
||||||
|
|
||||||
Let's see an example to illustrate how the input and output processors are
|
Let's see an example to illustrate how the input and output processors are
|
||||||
called for a particular field (the same applies for any other field)::
|
called for a particular field (the same applies for any other field):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
l = ItemLoader(Product(), some_selector)
|
l = ItemLoader(Product(), some_selector)
|
||||||
l.add_xpath('name', xpath1) # (1)
|
l.add_xpath("name", xpath1) # (1)
|
||||||
l.add_xpath('name', xpath2) # (2)
|
l.add_xpath("name", xpath2) # (2)
|
||||||
l.add_css('name', css) # (3)
|
l.add_css("name", css) # (3)
|
||||||
l.add_value('name', 'test') # (4)
|
l.add_value("name", "test") # (4)
|
||||||
return l.load_item() # (5)
|
return l.load_item() # (5)
|
||||||
|
|
||||||
So what happens is:
|
So what happens is:
|
||||||
|
|
||||||
|
|
@ -184,11 +192,14 @@ processors <itemloaders:built-in-processors>` built-in for convenience.
|
||||||
Declaring Item Loaders
|
Declaring Item Loaders
|
||||||
======================
|
======================
|
||||||
|
|
||||||
Item Loaders are declared using a class definition syntax. Here is an example::
|
Item Loaders are declared using a class definition syntax. Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemloaders.processors import TakeFirst, MapCompose, Join
|
from itemloaders.processors import TakeFirst, MapCompose, Join
|
||||||
from scrapy.loader import ItemLoader
|
from scrapy.loader import ItemLoader
|
||||||
|
|
||||||
|
|
||||||
class ProductLoader(ItemLoader):
|
class ProductLoader(ItemLoader):
|
||||||
|
|
||||||
default_output_processor = TakeFirst()
|
default_output_processor = TakeFirst()
|
||||||
|
|
@ -215,16 +226,20 @@ As seen in the previous section, input and output processors can be declared in
|
||||||
the Item Loader definition, and it's very common to declare input processors
|
the Item Loader definition, and it's very common to declare input processors
|
||||||
this way. However, there is one more place where you can specify the input and
|
this way. However, there is one more place where you can specify the input and
|
||||||
output processors to use: in the :ref:`Item Field <topics-items-fields>`
|
output processors to use: in the :ref:`Item Field <topics-items-fields>`
|
||||||
metadata. Here is an example::
|
metadata. Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from itemloaders.processors import Join, MapCompose, TakeFirst
|
from itemloaders.processors import Join, MapCompose, TakeFirst
|
||||||
from w3lib.html import remove_tags
|
from w3lib.html import remove_tags
|
||||||
|
|
||||||
|
|
||||||
def filter_price(value):
|
def filter_price(value):
|
||||||
if value.isdigit():
|
if value.isdigit():
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class Product(scrapy.Item):
|
class Product(scrapy.Item):
|
||||||
name = scrapy.Field(
|
name = scrapy.Field(
|
||||||
input_processor=MapCompose(remove_tags),
|
input_processor=MapCompose(remove_tags),
|
||||||
|
|
@ -263,10 +278,12 @@ declaring, instantiating or using Item Loader. They are used to modify the
|
||||||
behaviour of the input/output processors.
|
behaviour of the input/output processors.
|
||||||
|
|
||||||
For example, suppose you have a function ``parse_length`` which receives a text
|
For example, suppose you have a function ``parse_length`` which receives a text
|
||||||
value and extracts a length from it::
|
value and extracts a length from it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse_length(text, loader_context):
|
def parse_length(text, loader_context):
|
||||||
unit = loader_context.get('unit', 'm')
|
unit = loader_context.get("unit", "m")
|
||||||
# ... length parsing code goes here ...
|
# ... length parsing code goes here ...
|
||||||
return parsed_length
|
return parsed_length
|
||||||
|
|
||||||
|
|
@ -278,22 +295,28 @@ function (``parse_length`` in this case) can thus use them.
|
||||||
There are several ways to modify Item Loader context values:
|
There are several ways to modify Item Loader context values:
|
||||||
|
|
||||||
1. By modifying the currently active Item Loader context
|
1. By modifying the currently active Item Loader context
|
||||||
(:attr:`~ItemLoader.context` attribute)::
|
(:attr:`~ItemLoader.context` attribute):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
loader = ItemLoader(product)
|
loader = ItemLoader(product)
|
||||||
loader.context['unit'] = 'cm'
|
loader.context["unit"] = "cm"
|
||||||
|
|
||||||
2. On Item Loader instantiation (the keyword arguments of Item Loader
|
2. On Item Loader instantiation (the keyword arguments of Item Loader
|
||||||
``__init__`` method are stored in the Item Loader context)::
|
``__init__`` method are stored in the Item Loader context):
|
||||||
|
|
||||||
loader = ItemLoader(product, unit='cm')
|
.. code-block:: python
|
||||||
|
|
||||||
|
loader = ItemLoader(product, unit="cm")
|
||||||
|
|
||||||
3. On Item Loader declaration, for those input/output processors that support
|
3. On Item Loader declaration, for those input/output processors that support
|
||||||
instantiating them with an Item Loader context. :class:`~processor.MapCompose` is one of
|
instantiating them with an Item Loader context. :class:`~processor.MapCompose` is one of
|
||||||
them::
|
them:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class ProductLoader(ItemLoader):
|
class ProductLoader(ItemLoader):
|
||||||
length_out = MapCompose(parse_length, unit='cm')
|
length_out = MapCompose(parse_length, unit="cm")
|
||||||
|
|
||||||
|
|
||||||
ItemLoader objects
|
ItemLoader objects
|
||||||
|
|
@ -323,25 +346,29 @@ Example::
|
||||||
Without nested loaders, you need to specify the full xpath (or css) for each value
|
Without nested loaders, you need to specify the full xpath (or css) for each value
|
||||||
that you wish to extract.
|
that you wish to extract.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
loader = ItemLoader(item=Item())
|
loader = ItemLoader(item=Item())
|
||||||
# load stuff not in the footer
|
# load stuff not in the footer
|
||||||
loader.add_xpath('social', '//footer/a[@class = "social"]/@href')
|
loader.add_xpath("social", '//footer/a[@class = "social"]/@href')
|
||||||
loader.add_xpath('email', '//footer/a[@class = "email"]/@href')
|
loader.add_xpath("email", '//footer/a[@class = "email"]/@href')
|
||||||
loader.load_item()
|
loader.load_item()
|
||||||
|
|
||||||
Instead, you can create a nested loader with the footer selector and add values
|
Instead, you can create a nested loader with the footer selector and add values
|
||||||
relative to the footer. The functionality is the same but you avoid repeating
|
relative to the footer. The functionality is the same but you avoid repeating
|
||||||
the footer selector.
|
the footer selector.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
loader = ItemLoader(item=Item())
|
loader = ItemLoader(item=Item())
|
||||||
# load stuff not in the footer
|
# load stuff not in the footer
|
||||||
footer_loader = loader.nested_xpath('//footer')
|
footer_loader = loader.nested_xpath("//footer")
|
||||||
footer_loader.add_xpath('social', 'a[@class = "social"]/@href')
|
footer_loader.add_xpath("social", 'a[@class = "social"]/@href')
|
||||||
footer_loader.add_xpath('email', 'a[@class = "email"]/@href')
|
footer_loader.add_xpath("email", 'a[@class = "email"]/@href')
|
||||||
# no need to call footer_loader.load_item()
|
# no need to call footer_loader.load_item()
|
||||||
loader.load_item()
|
loader.load_item()
|
||||||
|
|
||||||
|
|
@ -370,25 +397,32 @@ three dashes (e.g. ``---Plasma TV---``) and you don't want to end up scraping
|
||||||
those dashes in the final product names.
|
those dashes in the final product names.
|
||||||
|
|
||||||
Here's how you can remove those dashes by reusing and extending the default
|
Here's how you can remove those dashes by reusing and extending the default
|
||||||
Product Item Loader (``ProductLoader``)::
|
Product Item Loader (``ProductLoader``):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemloaders.processors import MapCompose
|
from itemloaders.processors import MapCompose
|
||||||
from myproject.ItemLoaders import ProductLoader
|
from myproject.ItemLoaders import ProductLoader
|
||||||
|
|
||||||
|
|
||||||
def strip_dashes(x):
|
def strip_dashes(x):
|
||||||
return x.strip('-')
|
return x.strip("-")
|
||||||
|
|
||||||
|
|
||||||
class SiteSpecificLoader(ProductLoader):
|
class SiteSpecificLoader(ProductLoader):
|
||||||
name_in = MapCompose(strip_dashes, ProductLoader.name_in)
|
name_in = MapCompose(strip_dashes, ProductLoader.name_in)
|
||||||
|
|
||||||
Another case where extending Item Loaders can be very helpful is when you have
|
Another case where extending Item Loaders can be very helpful is when you have
|
||||||
multiple source formats, for example XML and HTML. In the XML version you may
|
multiple source formats, for example XML and HTML. In the XML version you may
|
||||||
want to remove ``CDATA`` occurrences. Here's an example of how to do it::
|
want to remove ``CDATA`` occurrences. Here's an example of how to do it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemloaders.processors import MapCompose
|
from itemloaders.processors import MapCompose
|
||||||
from myproject.ItemLoaders import ProductLoader
|
from myproject.ItemLoaders import ProductLoader
|
||||||
from myproject.utils.xml import remove_cdata
|
from myproject.utils.xml import remove_cdata
|
||||||
|
|
||||||
|
|
||||||
class XmlProductLoader(ProductLoader):
|
class XmlProductLoader(ProductLoader):
|
||||||
name_in = MapCompose(remove_cdata, ProductLoader.name_in)
|
name_in = MapCompose(remove_cdata, ProductLoader.name_in)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,22 @@ How to log messages
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Here's a quick example of how to log a message using the ``logging.WARNING``
|
Here's a quick example of how to log a message using the ``logging.WARNING``
|
||||||
level::
|
level:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.warning("This is a warning")
|
logging.warning("This is a warning")
|
||||||
|
|
||||||
There are shortcuts for issuing log messages on any of the standard 5 levels,
|
There are shortcuts for issuing log messages on any of the standard 5 levels,
|
||||||
and there's also a general ``logging.log`` method which takes a given level as
|
and there's also a general ``logging.log`` method which takes a given level as
|
||||||
argument. If needed, the last example could be rewritten as::
|
argument. If needed, the last example could be rewritten as:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.log(logging.WARNING, "This is a warning")
|
logging.log(logging.WARNING, "This is a warning")
|
||||||
|
|
||||||
On top of that, you can create different "loggers" to encapsulate messages. (For
|
On top of that, you can create different "loggers" to encapsulate messages. (For
|
||||||
|
|
@ -59,24 +65,33 @@ constructions.
|
||||||
The previous examples use the root logger behind the scenes, which is a top level
|
The previous examples use the root logger behind the scenes, which is a top level
|
||||||
logger where all messages are propagated to (unless otherwise specified). Using
|
logger where all messages are propagated to (unless otherwise specified). Using
|
||||||
``logging`` helpers is merely a shortcut for getting the root logger
|
``logging`` helpers is merely a shortcut for getting the root logger
|
||||||
explicitly, so this is also an equivalent of the last snippets::
|
explicitly, so this is also an equivalent of the last snippets:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.warning("This is a warning")
|
logger.warning("This is a warning")
|
||||||
|
|
||||||
You can use a different logger just by getting its name with the
|
You can use a different logger just by getting its name with the
|
||||||
``logging.getLogger`` function::
|
``logging.getLogger`` function:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger('mycustomlogger')
|
|
||||||
|
logger = logging.getLogger("mycustomlogger")
|
||||||
logger.warning("This is a warning")
|
logger.warning("This is a warning")
|
||||||
|
|
||||||
Finally, you can ensure having a custom logger for any module you're working on
|
Finally, you can ensure having a custom logger for any module you're working on
|
||||||
by using the ``__name__`` variable, which is populated with current module's
|
by using the ``__name__`` variable, which is populated with current module's
|
||||||
path::
|
path:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.warning("This is a warning")
|
logger.warning("This is a warning")
|
||||||
|
|
||||||
|
|
@ -94,33 +109,39 @@ Logging from Spiders
|
||||||
====================
|
====================
|
||||||
|
|
||||||
Scrapy provides a :data:`~scrapy.Spider.logger` within each Spider
|
Scrapy provides a :data:`~scrapy.Spider.logger` within each Spider
|
||||||
instance, which can be accessed and used like this::
|
instance, which can be accessed and used like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
|
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
start_urls = ['https://scrapy.org']
|
start_urls = ["https://scrapy.org"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
self.logger.info('Parse function called on %s', response.url)
|
self.logger.info("Parse function called on %s", response.url)
|
||||||
|
|
||||||
That logger is created using the Spider's name, but you can use any custom
|
That logger is created using the Spider's name, but you can use any custom
|
||||||
Python logger you want. For example::
|
Python logger you want. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
logger = logging.getLogger('mycustomlogger')
|
logger = logging.getLogger("mycustomlogger")
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
|
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
start_urls = ['https://scrapy.org']
|
start_urls = ["https://scrapy.org"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
logger.info('Parse function called on %s', response.url)
|
logger.info("Parse function called on %s", response.url)
|
||||||
|
|
||||||
.. _topics-logging-configuration:
|
.. _topics-logging-configuration:
|
||||||
|
|
||||||
|
|
@ -229,7 +250,9 @@ the crawl.
|
||||||
Next, we can see that the message has INFO level. To hide it
|
Next, we can see that the message has INFO level. To hide it
|
||||||
we should set logging level for ``scrapy.spidermiddlewares.httperror``
|
we should set logging level for ``scrapy.spidermiddlewares.httperror``
|
||||||
higher than INFO; next level after INFO is WARNING. It could be done
|
higher than INFO; next level after INFO is WARNING. It could be done
|
||||||
e.g. in the spider's ``__init__`` method::
|
e.g. in the spider's ``__init__`` method:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
@ -238,7 +261,7 @@ e.g. in the spider's ``__init__`` method::
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
# ...
|
# ...
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
logger = logging.getLogger('scrapy.spidermiddlewares.httperror')
|
logger = logging.getLogger("scrapy.spidermiddlewares.httperror")
|
||||||
logger.setLevel(logging.WARNING)
|
logger.setLevel(logging.WARNING)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
@ -249,43 +272,53 @@ You can also filter log records by :class:`~logging.LogRecord` data. For
|
||||||
example, you can filter log records by message content using a substring or
|
example, you can filter log records by message content using a substring or
|
||||||
a regular expression. Create a :class:`logging.Filter` subclass
|
a regular expression. Create a :class:`logging.Filter` subclass
|
||||||
and equip it with a regular expression pattern to
|
and equip it with a regular expression pattern to
|
||||||
filter out unwanted messages::
|
filter out unwanted messages:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class ContentFilter(logging.Filter):
|
class ContentFilter(logging.Filter):
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
match = re.search(r'\d{3} [Ee]rror, retrying', record.message)
|
match = re.search(r"\d{3} [Ee]rror, retrying", record.message)
|
||||||
if match:
|
if match:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
A project-level filter may be attached to the root
|
A project-level filter may be attached to the root
|
||||||
handler created by Scrapy, this is a wieldy way to
|
handler created by Scrapy, this is a wieldy way to
|
||||||
filter all loggers in different parts of the project
|
filter all loggers in different parts of the project
|
||||||
(middlewares, spider, etc.)::
|
(middlewares, spider, etc.):
|
||||||
|
|
||||||
import logging
|
.. code-block:: python
|
||||||
import scrapy
|
|
||||||
|
import logging
|
||||||
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
|
class MySpider(scrapy.Spider):
|
||||||
|
# ...
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
for handler in logging.root.handlers:
|
||||||
|
handler.addFilter(ContentFilter())
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
|
||||||
# ...
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
for handler in logging.root.handlers:
|
|
||||||
handler.addFilter(ContentFilter())
|
|
||||||
|
|
||||||
Alternatively, you may choose a specific logger
|
Alternatively, you may choose a specific logger
|
||||||
and hide it without affecting other loggers::
|
and hide it without affecting other loggers:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
# ...
|
# ...
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
logger = logging.getLogger('my_logger')
|
logger = logging.getLogger("my_logger")
|
||||||
logger.addFilter(ContentFilter())
|
logger.addFilter(ContentFilter())
|
||||||
|
|
||||||
|
|
||||||
scrapy.utils.log module
|
scrapy.utils.log module
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
|
@ -306,14 +339,14 @@ scrapy.utils.log module
|
||||||
so it is recommended to only use :func:`logging.basicConfig` together with
|
so it is recommended to only use :func:`logging.basicConfig` together with
|
||||||
:class:`~scrapy.crawler.CrawlerRunner`.
|
:class:`~scrapy.crawler.CrawlerRunner`.
|
||||||
|
|
||||||
This is an example on how to redirect ``INFO`` or higher messages to a file::
|
This is an example on how to redirect ``INFO`` or higher messages to a file:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename='log.txt',
|
filename="log.txt", format="%(levelname)s: %(message)s", level=logging.INFO
|
||||||
format='%(levelname)s: %(message)s',
|
|
||||||
level=logging.INFO
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Refer to :ref:`run-from-script` for more details about using Scrapy this
|
Refer to :ref:`run-from-script` for more details about using Scrapy this
|
||||||
|
|
|
||||||
|
|
@ -87,13 +87,17 @@ Enabling your Media Pipeline
|
||||||
To enable your media pipeline you must first add it to your project
|
To enable your media pipeline you must first add it to your project
|
||||||
:setting:`ITEM_PIPELINES` setting.
|
:setting:`ITEM_PIPELINES` setting.
|
||||||
|
|
||||||
For Images Pipeline, use::
|
For Images Pipeline, use:
|
||||||
|
|
||||||
ITEM_PIPELINES = {'scrapy.pipelines.images.ImagesPipeline': 1}
|
.. code-block:: python
|
||||||
|
|
||||||
For Files Pipeline, use::
|
ITEM_PIPELINES = {"scrapy.pipelines.images.ImagesPipeline": 1}
|
||||||
|
|
||||||
ITEM_PIPELINES = {'scrapy.pipelines.files.FilesPipeline': 1}
|
For Files Pipeline, use:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
ITEM_PIPELINES = {"scrapy.pipelines.files.FilesPipeline": 1}
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
You can also use both the Files and Images Pipeline at the same time.
|
You can also use both the Files and Images Pipeline at the same time.
|
||||||
|
|
@ -103,13 +107,17 @@ Then, configure the target storage setting to a valid value that will be used
|
||||||
for storing the downloaded images. Otherwise the pipeline will remain disabled,
|
for storing the downloaded images. Otherwise the pipeline will remain disabled,
|
||||||
even if you include it in the :setting:`ITEM_PIPELINES` setting.
|
even if you include it in the :setting:`ITEM_PIPELINES` setting.
|
||||||
|
|
||||||
For the Files Pipeline, set the :setting:`FILES_STORE` setting::
|
For the Files Pipeline, set the :setting:`FILES_STORE` setting:
|
||||||
|
|
||||||
FILES_STORE = '/path/to/valid/dir'
|
.. code-block:: python
|
||||||
|
|
||||||
For the Images Pipeline, set the :setting:`IMAGES_STORE` setting::
|
FILES_STORE = "/path/to/valid/dir"
|
||||||
|
|
||||||
IMAGES_STORE = '/path/to/valid/dir'
|
For the Images Pipeline, set the :setting:`IMAGES_STORE` setting:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
IMAGES_STORE = "/path/to/valid/dir"
|
||||||
|
|
||||||
.. _topics-file-naming:
|
.. _topics-file-naming:
|
||||||
|
|
||||||
|
|
@ -157,10 +165,11 @@ By overriding ``file_path`` like this:
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
def file_path(self, request, response=None, info=None, *, item=None):
|
def file_path(self, request, response=None, info=None, *, item=None):
|
||||||
image_url_hash = hashlib.shake_256(request.url.encode()).hexdigest(5)
|
image_url_hash = hashlib.shake_256(request.url.encode()).hexdigest(5)
|
||||||
image_perspective = request.url.split('/')[-2]
|
image_perspective = request.url.split("/")[-2]
|
||||||
image_filename = f'{image_url_hash}_{image_perspective}.jpg'
|
image_filename = f"{image_url_hash}_{image_perspective}.jpg"
|
||||||
|
|
||||||
return image_filename
|
return image_filename
|
||||||
|
|
||||||
|
|
@ -233,30 +242,38 @@ If botocore_ >= 1.4.87 is installed, :setting:`FILES_STORE` and
|
||||||
:setting:`IMAGES_STORE` can represent an Amazon S3 bucket. Scrapy will
|
:setting:`IMAGES_STORE` can represent an Amazon S3 bucket. Scrapy will
|
||||||
automatically upload the files to the bucket.
|
automatically upload the files to the bucket.
|
||||||
|
|
||||||
For example, this is a valid :setting:`IMAGES_STORE` value::
|
For example, this is a valid :setting:`IMAGES_STORE` value:
|
||||||
|
|
||||||
IMAGES_STORE = 's3://bucket/images'
|
.. code-block:: python
|
||||||
|
|
||||||
|
IMAGES_STORE = "s3://bucket/images"
|
||||||
|
|
||||||
You can modify the Access Control List (ACL) policy used for the stored files,
|
You can modify the Access Control List (ACL) policy used for the stored files,
|
||||||
which is defined by the :setting:`FILES_STORE_S3_ACL` and
|
which is defined by the :setting:`FILES_STORE_S3_ACL` and
|
||||||
:setting:`IMAGES_STORE_S3_ACL` settings. By default, the ACL is set to
|
:setting:`IMAGES_STORE_S3_ACL` settings. By default, the ACL is set to
|
||||||
``private``. To make the files publicly available use the ``public-read``
|
``private``. To make the files publicly available use the ``public-read``
|
||||||
policy::
|
policy:
|
||||||
|
|
||||||
IMAGES_STORE_S3_ACL = 'public-read'
|
.. code-block:: python
|
||||||
|
|
||||||
|
IMAGES_STORE_S3_ACL = "public-read"
|
||||||
|
|
||||||
For more information, see `canned ACLs`_ in the Amazon S3 Developer Guide.
|
For more information, see `canned ACLs`_ in the Amazon S3 Developer Guide.
|
||||||
|
|
||||||
You can also use other S3-like storages. Storages like self-hosted `Minio`_ or
|
You can also use other S3-like storages. Storages like self-hosted `Minio`_ or
|
||||||
`s3.scality`_. All you need to do is set endpoint option in you Scrapy
|
`s3.scality`_. All you need to do is set endpoint option in you Scrapy
|
||||||
settings::
|
settings:
|
||||||
|
|
||||||
AWS_ENDPOINT_URL = 'http://minio.example.com:9000'
|
.. code-block:: python
|
||||||
|
|
||||||
For self-hosting you also might feel the need not to use SSL and not to verify SSL connection::
|
AWS_ENDPOINT_URL = "http://minio.example.com:9000"
|
||||||
|
|
||||||
AWS_USE_SSL = False # or True (None by default)
|
For self-hosting you also might feel the need not to use SSL and not to verify SSL connection:
|
||||||
AWS_VERIFY = False # or True (None by default)
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
AWS_USE_SSL = False # or True (None by default)
|
||||||
|
AWS_VERIFY = False # or True (None by default)
|
||||||
|
|
||||||
.. _botocore: https://github.com/boto/botocore
|
.. _botocore: https://github.com/boto/botocore
|
||||||
.. _canned ACLs: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
.. _canned ACLs: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
||||||
|
|
@ -277,10 +294,12 @@ bucket. Scrapy will automatically upload the files to the bucket. (requires `goo
|
||||||
|
|
||||||
.. _google-cloud-storage: https://cloud.google.com/storage/docs/reference/libraries#client-libraries-install-python
|
.. _google-cloud-storage: https://cloud.google.com/storage/docs/reference/libraries#client-libraries-install-python
|
||||||
|
|
||||||
For example, these are valid :setting:`IMAGES_STORE` and :setting:`GCS_PROJECT_ID` settings::
|
For example, these are valid :setting:`IMAGES_STORE` and :setting:`GCS_PROJECT_ID` settings:
|
||||||
|
|
||||||
IMAGES_STORE = 'gs://bucket/images/'
|
.. code-block:: python
|
||||||
GCS_PROJECT_ID = 'project_id'
|
|
||||||
|
IMAGES_STORE = "gs://bucket/images/"
|
||||||
|
GCS_PROJECT_ID = "project_id"
|
||||||
|
|
||||||
For information about authentication, see this `documentation`_.
|
For information about authentication, see this `documentation`_.
|
||||||
|
|
||||||
|
|
@ -291,9 +310,11 @@ which is defined by the :setting:`FILES_STORE_GCS_ACL` and
|
||||||
:setting:`IMAGES_STORE_GCS_ACL` settings. By default, the ACL is set to
|
:setting:`IMAGES_STORE_GCS_ACL` settings. By default, the ACL is set to
|
||||||
``''`` (empty string) which means that Cloud Storage applies the bucket's default object ACL to the object.
|
``''`` (empty string) which means that Cloud Storage applies the bucket's default object ACL to the object.
|
||||||
To make the files publicly available use the ``publicRead``
|
To make the files publicly available use the ``publicRead``
|
||||||
policy::
|
policy:
|
||||||
|
|
||||||
IMAGES_STORE_GCS_ACL = 'publicRead'
|
.. code-block:: python
|
||||||
|
|
||||||
|
IMAGES_STORE_GCS_ACL = "publicRead"
|
||||||
|
|
||||||
For more information, see `Predefined ACLs`_ in the Google Cloud Platform Developer Guide.
|
For more information, see `Predefined ACLs`_ in the Google Cloud Platform Developer Guide.
|
||||||
|
|
||||||
|
|
@ -318,10 +339,13 @@ respectively), the pipeline will put the results under the respective field
|
||||||
When using :ref:`item types <item-types>` for which fields are defined beforehand,
|
When using :ref:`item types <item-types>` for which fields are defined beforehand,
|
||||||
you must define both the URLs field and the results field. For example, when
|
you must define both the URLs field and the results field. For example, when
|
||||||
using the images pipeline, items must define both the ``image_urls`` and the
|
using the images pipeline, items must define both the ``image_urls`` and the
|
||||||
``images`` field. For instance, using the :class:`~scrapy.Item` class::
|
``images`` field. For instance, using the :class:`~scrapy.Item` class:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class MyItem(scrapy.Item):
|
class MyItem(scrapy.Item):
|
||||||
# ... other item fields ...
|
# ... other item fields ...
|
||||||
image_urls = scrapy.Field()
|
image_urls = scrapy.Field()
|
||||||
|
|
@ -331,16 +355,20 @@ If you want to use another field name for the URLs key or for the results key,
|
||||||
it is also possible to override it.
|
it is also possible to override it.
|
||||||
|
|
||||||
For the Files Pipeline, set :setting:`FILES_URLS_FIELD` and/or
|
For the Files Pipeline, set :setting:`FILES_URLS_FIELD` and/or
|
||||||
:setting:`FILES_RESULT_FIELD` settings::
|
:setting:`FILES_RESULT_FIELD` settings:
|
||||||
|
|
||||||
FILES_URLS_FIELD = 'field_name_for_your_files_urls'
|
.. code-block:: python
|
||||||
FILES_RESULT_FIELD = 'field_name_for_your_processed_files'
|
|
||||||
|
FILES_URLS_FIELD = "field_name_for_your_files_urls"
|
||||||
|
FILES_RESULT_FIELD = "field_name_for_your_processed_files"
|
||||||
|
|
||||||
For the Images Pipeline, set :setting:`IMAGES_URLS_FIELD` and/or
|
For the Images Pipeline, set :setting:`IMAGES_URLS_FIELD` and/or
|
||||||
:setting:`IMAGES_RESULT_FIELD` settings::
|
:setting:`IMAGES_RESULT_FIELD` settings:
|
||||||
|
|
||||||
IMAGES_URLS_FIELD = 'field_name_for_your_images_urls'
|
.. code-block:: python
|
||||||
IMAGES_RESULT_FIELD = 'field_name_for_your_processed_images'
|
|
||||||
|
IMAGES_URLS_FIELD = "field_name_for_your_images_urls"
|
||||||
|
IMAGES_RESULT_FIELD = "field_name_for_your_processed_images"
|
||||||
|
|
||||||
If you need something more complex and want to override the custom pipeline
|
If you need something more complex and want to override the custom pipeline
|
||||||
behaviour, see :ref:`topics-media-pipeline-override`.
|
behaviour, see :ref:`topics-media-pipeline-override`.
|
||||||
|
|
@ -366,7 +394,9 @@ File expiration
|
||||||
The Image Pipeline avoids downloading files that were downloaded recently. To
|
The Image Pipeline avoids downloading files that were downloaded recently. To
|
||||||
adjust this retention delay use the :setting:`FILES_EXPIRES` setting (or
|
adjust this retention delay use the :setting:`FILES_EXPIRES` setting (or
|
||||||
:setting:`IMAGES_EXPIRES`, in case of Images Pipeline), which
|
:setting:`IMAGES_EXPIRES`, in case of Images Pipeline), which
|
||||||
specifies the delay in number of days::
|
specifies the delay in number of days:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
# 120 days of delay for files expiration
|
# 120 days of delay for files expiration
|
||||||
FILES_EXPIRES = 120
|
FILES_EXPIRES = 120
|
||||||
|
|
@ -400,11 +430,13 @@ images.
|
||||||
In order to use this feature, you must set :setting:`IMAGES_THUMBS` to a dictionary
|
In order to use this feature, you must set :setting:`IMAGES_THUMBS` to a dictionary
|
||||||
where the keys are the thumbnail names and the values are their dimensions.
|
where the keys are the thumbnail names and the values are their dimensions.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
IMAGES_THUMBS = {
|
IMAGES_THUMBS = {
|
||||||
'small': (50, 50),
|
"small": (50, 50),
|
||||||
'big': (270, 270),
|
"big": (270, 270),
|
||||||
}
|
}
|
||||||
|
|
||||||
When you use this feature, the Images Pipeline will create thumbnails of the
|
When you use this feature, the Images Pipeline will create thumbnails of the
|
||||||
|
|
@ -495,17 +527,19 @@ See here the methods that you can override in your custom Files Pipeline:
|
||||||
For example, if file URLs end like regular paths (e.g.
|
For example, if file URLs end like regular paths (e.g.
|
||||||
``https://example.com/a/b/c/foo.png``), you can use the following
|
``https://example.com/a/b/c/foo.png``), you can use the following
|
||||||
approach to download all files into the ``files`` folder with their
|
approach to download all files into the ``files`` folder with their
|
||||||
original filenames (e.g. ``files/foo.png``)::
|
original filenames (e.g. ``files/foo.png``):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from pathlib import PurePosixPath
|
from pathlib import PurePosixPath
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from scrapy.pipelines.files import FilesPipeline
|
from scrapy.pipelines.files import FilesPipeline
|
||||||
|
|
||||||
class MyFilesPipeline(FilesPipeline):
|
|
||||||
|
|
||||||
|
class MyFilesPipeline(FilesPipeline):
|
||||||
def file_path(self, request, response=None, info=None, *, item=None):
|
def file_path(self, request, response=None, info=None, *, item=None):
|
||||||
return 'files/' + PurePosixPath(urlparse(request.url).path).name
|
return "files/" + PurePosixPath(urlparse(request.url).path).name
|
||||||
|
|
||||||
Similarly, you can use the ``item`` to determine the file path based on some item
|
Similarly, you can use the ``item`` to determine the file path based on some item
|
||||||
property.
|
property.
|
||||||
|
|
@ -521,13 +555,16 @@ See here the methods that you can override in your custom Files Pipeline:
|
||||||
As seen on the workflow, the pipeline will get the URLs of the images to
|
As seen on the workflow, the pipeline will get the URLs of the images to
|
||||||
download from the item. In order to do this, you can override the
|
download from the item. In order to do this, you can override the
|
||||||
:meth:`~get_media_requests` method and return a Request for each
|
:meth:`~get_media_requests` method and return a Request for each
|
||||||
file URL::
|
file URL:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
|
|
||||||
|
|
||||||
def get_media_requests(self, item, info):
|
def get_media_requests(self, item, info):
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
for file_url in adapter['file_urls']:
|
for file_url in adapter["file_urls"]:
|
||||||
yield scrapy.Request(file_url)
|
yield scrapy.Request(file_url)
|
||||||
|
|
||||||
Those requests will be processed by the pipeline and, when they have finished
|
Those requests will be processed by the pipeline and, when they have finished
|
||||||
|
|
@ -567,15 +604,22 @@ See here the methods that you can override in your custom Files Pipeline:
|
||||||
guaranteed to retain the same order of the requests returned from the
|
guaranteed to retain the same order of the requests returned from the
|
||||||
:meth:`~get_media_requests` method.
|
:meth:`~get_media_requests` method.
|
||||||
|
|
||||||
Here's a typical value of the ``results`` argument::
|
Here's a typical value of the ``results`` argument:
|
||||||
|
|
||||||
[(True,
|
.. code-block:: python
|
||||||
{'checksum': '2b00042f7481c7b056c4b410d28f33cf',
|
|
||||||
'path': 'full/0a79c461a4062ac383dc4fade7bc09f1384a3910.jpg',
|
[
|
||||||
'url': 'http://www.example.com/files/product1.pdf',
|
(
|
||||||
'status': 'downloaded'}),
|
True,
|
||||||
(False,
|
{
|
||||||
Failure(...))]
|
"checksum": "2b00042f7481c7b056c4b410d28f33cf",
|
||||||
|
"path": "full/0a79c461a4062ac383dc4fade7bc09f1384a3910.jpg",
|
||||||
|
"url": "http://www.example.com/files/product1.pdf",
|
||||||
|
"status": "downloaded",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(False, Failure(...)),
|
||||||
|
]
|
||||||
|
|
||||||
By default the :meth:`get_media_requests` method returns ``None`` which
|
By default the :meth:`get_media_requests` method returns ``None`` which
|
||||||
means there are no files to download for the item.
|
means there are no files to download for the item.
|
||||||
|
|
@ -592,17 +636,20 @@ See here the methods that you can override in your custom Files Pipeline:
|
||||||
|
|
||||||
Here is an example of the :meth:`~item_completed` method where we
|
Here is an example of the :meth:`~item_completed` method where we
|
||||||
store the downloaded file paths (passed in results) in the ``file_paths``
|
store the downloaded file paths (passed in results) in the ``file_paths``
|
||||||
item field, and we drop the item if it doesn't contain any files::
|
item field, and we drop the item if it doesn't contain any files:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
from scrapy.exceptions import DropItem
|
from scrapy.exceptions import DropItem
|
||||||
|
|
||||||
|
|
||||||
def item_completed(self, results, item, info):
|
def item_completed(self, results, item, info):
|
||||||
file_paths = [x['path'] for ok, x in results if ok]
|
file_paths = [x["path"] for ok, x in results if ok]
|
||||||
if not file_paths:
|
if not file_paths:
|
||||||
raise DropItem("Item contains no files")
|
raise DropItem("Item contains no files")
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
adapter['file_paths'] = file_paths
|
adapter["file_paths"] = file_paths
|
||||||
return item
|
return item
|
||||||
|
|
||||||
By default, the :meth:`item_completed` method returns the item.
|
By default, the :meth:`item_completed` method returns the item.
|
||||||
|
|
@ -634,17 +681,19 @@ See here the methods that you can override in your custom Images Pipeline:
|
||||||
For example, if file URLs end like regular paths (e.g.
|
For example, if file URLs end like regular paths (e.g.
|
||||||
``https://example.com/a/b/c/foo.png``), you can use the following
|
``https://example.com/a/b/c/foo.png``), you can use the following
|
||||||
approach to download all files into the ``files`` folder with their
|
approach to download all files into the ``files`` folder with their
|
||||||
original filenames (e.g. ``files/foo.png``)::
|
original filenames (e.g. ``files/foo.png``):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from pathlib import PurePosixPath
|
from pathlib import PurePosixPath
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from scrapy.pipelines.images import ImagesPipeline
|
from scrapy.pipelines.images import ImagesPipeline
|
||||||
|
|
||||||
class MyImagesPipeline(ImagesPipeline):
|
|
||||||
|
|
||||||
|
class MyImagesPipeline(ImagesPipeline):
|
||||||
def file_path(self, request, response=None, info=None, *, item=None):
|
def file_path(self, request, response=None, info=None, *, item=None):
|
||||||
return 'files/' + PurePosixPath(urlparse(request.url).path).name
|
return "files/" + PurePosixPath(urlparse(request.url).path).name
|
||||||
|
|
||||||
Similarly, you can use the ``item`` to determine the file path based on some item
|
Similarly, you can use the ``item`` to determine the file path based on some item
|
||||||
property.
|
property.
|
||||||
|
|
@ -700,33 +749,35 @@ Custom Images pipeline example
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
Here is a full example of the Images Pipeline whose methods are exemplified
|
Here is a full example of the Images Pipeline whose methods are exemplified
|
||||||
above::
|
above:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from itemadapter import ItemAdapter
|
from itemadapter import ItemAdapter
|
||||||
from scrapy.exceptions import DropItem
|
from scrapy.exceptions import DropItem
|
||||||
from scrapy.pipelines.images import ImagesPipeline
|
from scrapy.pipelines.images import ImagesPipeline
|
||||||
|
|
||||||
class MyImagesPipeline(ImagesPipeline):
|
|
||||||
|
|
||||||
|
class MyImagesPipeline(ImagesPipeline):
|
||||||
def get_media_requests(self, item, info):
|
def get_media_requests(self, item, info):
|
||||||
for image_url in item['image_urls']:
|
for image_url in item["image_urls"]:
|
||||||
yield scrapy.Request(image_url)
|
yield scrapy.Request(image_url)
|
||||||
|
|
||||||
def item_completed(self, results, item, info):
|
def item_completed(self, results, item, info):
|
||||||
image_paths = [x['path'] for ok, x in results if ok]
|
image_paths = [x["path"] for ok, x in results if ok]
|
||||||
if not image_paths:
|
if not image_paths:
|
||||||
raise DropItem("Item contains no images")
|
raise DropItem("Item contains no images")
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item)
|
||||||
adapter['image_paths'] = image_paths
|
adapter["image_paths"] = image_paths
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
To enable your custom media pipeline component you must add its class import path to the
|
To enable your custom media pipeline component you must add its class import path to the
|
||||||
:setting:`ITEM_PIPELINES` setting, like in the following example::
|
:setting:`ITEM_PIPELINES` setting, like in the following example:
|
||||||
|
|
||||||
ITEM_PIPELINES = {
|
.. code-block:: python
|
||||||
'myproject.pipelines.MyImagesPipeline': 300
|
|
||||||
}
|
ITEM_PIPELINES = {"myproject.pipelines.MyImagesPipeline": 300}
|
||||||
|
|
||||||
.. _MD5 hash: https://en.wikipedia.org/wiki/MD5
|
.. _MD5 hash: https://en.wikipedia.org/wiki/MD5
|
||||||
|
|
|
||||||
|
|
@ -25,23 +25,27 @@ the one used by all Scrapy commands.
|
||||||
|
|
||||||
Here's an example showing how to run a single spider with it.
|
Here's an example showing how to run a single spider with it.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from scrapy.crawler import CrawlerProcess
|
from scrapy.crawler import CrawlerProcess
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
# Your spider definition
|
# Your spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
process = CrawlerProcess(settings={
|
|
||||||
"FEEDS": {
|
process = CrawlerProcess(
|
||||||
"items.json": {"format": "json"},
|
settings={
|
||||||
},
|
"FEEDS": {
|
||||||
})
|
"items.json": {"format": "json"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
process.crawl(MySpider)
|
process.crawl(MySpider)
|
||||||
process.start() # the script will block here until the crawling is finished
|
process.start() # the script will block here until the crawling is finished
|
||||||
|
|
||||||
Define settings within dictionary in CrawlerProcess. Make sure to check :class:`~scrapy.crawler.CrawlerProcess`
|
Define settings within dictionary in CrawlerProcess. Make sure to check :class:`~scrapy.crawler.CrawlerProcess`
|
||||||
documentation to get acquainted with its usage details.
|
documentation to get acquainted with its usage details.
|
||||||
|
|
@ -55,7 +59,7 @@ instance with your project settings.
|
||||||
What follows is a working example of how to do that, using the `testspiders`_
|
What follows is a working example of how to do that, using the `testspiders`_
|
||||||
project as example.
|
project as example.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.crawler import CrawlerProcess
|
from scrapy.crawler import CrawlerProcess
|
||||||
from scrapy.utils.project import get_project_settings
|
from scrapy.utils.project import get_project_settings
|
||||||
|
|
@ -63,8 +67,8 @@ project as example.
|
||||||
process = CrawlerProcess(get_project_settings())
|
process = CrawlerProcess(get_project_settings())
|
||||||
|
|
||||||
# 'followall' is the name of one of the spiders of the project.
|
# 'followall' is the name of one of the spiders of the project.
|
||||||
process.crawl('followall', domain='scrapy.org')
|
process.crawl("followall", domain="scrapy.org")
|
||||||
process.start() # the script will block here until the crawling is finished
|
process.start() # the script will block here until the crawling is finished
|
||||||
|
|
||||||
There's another Scrapy utility that provides more control over the crawling
|
There's another Scrapy utility that provides more control over the crawling
|
||||||
process: :class:`scrapy.crawler.CrawlerRunner`. This class is a thin wrapper
|
process: :class:`scrapy.crawler.CrawlerRunner`. This class is a thin wrapper
|
||||||
|
|
@ -84,23 +88,25 @@ returned by the :meth:`CrawlerRunner.crawl
|
||||||
Here's an example of its usage, along with a callback to manually stop the
|
Here's an example of its usage, along with a callback to manually stop the
|
||||||
reactor after ``MySpider`` has finished running.
|
reactor after ``MySpider`` has finished running.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
import scrapy
|
import scrapy
|
||||||
from scrapy.crawler import CrawlerRunner
|
from scrapy.crawler import CrawlerRunner
|
||||||
from scrapy.utils.log import configure_logging
|
from scrapy.utils.log import configure_logging
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
# Your spider definition
|
# Your spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
configure_logging({'LOG_FORMAT': '%(levelname)s: %(message)s'})
|
|
||||||
|
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
|
||||||
runner = CrawlerRunner()
|
runner = CrawlerRunner()
|
||||||
|
|
||||||
d = runner.crawl(MySpider)
|
d = runner.crawl(MySpider)
|
||||||
d.addBoth(lambda _: reactor.stop())
|
d.addBoth(lambda _: reactor.stop())
|
||||||
reactor.run() # the script will block here until the crawling is finished
|
reactor.run() # the script will block here until the crawling is finished
|
||||||
|
|
||||||
.. seealso:: :doc:`twisted:core/howto/reactor-basics`
|
.. seealso:: :doc:`twisted:core/howto/reactor-basics`
|
||||||
|
|
||||||
|
|
@ -115,29 +121,32 @@ the :ref:`internal API <topics-api>`.
|
||||||
|
|
||||||
Here is an example that runs multiple spiders simultaneously:
|
Here is an example that runs multiple spiders simultaneously:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from scrapy.crawler import CrawlerProcess
|
from scrapy.crawler import CrawlerProcess
|
||||||
from scrapy.utils.project import get_project_settings
|
from scrapy.utils.project import get_project_settings
|
||||||
|
|
||||||
|
|
||||||
class MySpider1(scrapy.Spider):
|
class MySpider1(scrapy.Spider):
|
||||||
# Your first spider definition
|
# Your first spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class MySpider2(scrapy.Spider):
|
class MySpider2(scrapy.Spider):
|
||||||
# Your second spider definition
|
# Your second spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
settings = get_project_settings()
|
settings = get_project_settings()
|
||||||
process = CrawlerProcess(settings)
|
process = CrawlerProcess(settings)
|
||||||
process.crawl(MySpider1)
|
process.crawl(MySpider1)
|
||||||
process.crawl(MySpider2)
|
process.crawl(MySpider2)
|
||||||
process.start() # the script will block here until all crawling jobs are finished
|
process.start() # the script will block here until all crawling jobs are finished
|
||||||
|
|
||||||
Same example using :class:`~scrapy.crawler.CrawlerRunner`:
|
Same example using :class:`~scrapy.crawler.CrawlerRunner`:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
@ -145,14 +154,17 @@ Same example using :class:`~scrapy.crawler.CrawlerRunner`:
|
||||||
from scrapy.utils.log import configure_logging
|
from scrapy.utils.log import configure_logging
|
||||||
from scrapy.utils.project import get_project_settings
|
from scrapy.utils.project import get_project_settings
|
||||||
|
|
||||||
|
|
||||||
class MySpider1(scrapy.Spider):
|
class MySpider1(scrapy.Spider):
|
||||||
# Your first spider definition
|
# Your first spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class MySpider2(scrapy.Spider):
|
class MySpider2(scrapy.Spider):
|
||||||
# Your second spider definition
|
# Your second spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
configure_logging()
|
configure_logging()
|
||||||
settings = get_project_settings()
|
settings = get_project_settings()
|
||||||
runner = CrawlerRunner(settings)
|
runner = CrawlerRunner(settings)
|
||||||
|
|
@ -161,37 +173,42 @@ Same example using :class:`~scrapy.crawler.CrawlerRunner`:
|
||||||
d = runner.join()
|
d = runner.join()
|
||||||
d.addBoth(lambda _: reactor.stop())
|
d.addBoth(lambda _: reactor.stop())
|
||||||
|
|
||||||
reactor.run() # the script will block here until all crawling jobs are finished
|
reactor.run() # the script will block here until all crawling jobs are finished
|
||||||
|
|
||||||
Same example but running the spiders sequentially by chaining the deferreds:
|
Same example but running the spiders sequentially by chaining the deferreds:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
from twisted.internet import reactor, defer
|
||||||
from scrapy.crawler import CrawlerRunner
|
from scrapy.crawler import CrawlerRunner
|
||||||
from scrapy.utils.log import configure_logging
|
from scrapy.utils.log import configure_logging
|
||||||
from scrapy.utils.project import get_project_settings
|
from scrapy.utils.project import get_project_settings
|
||||||
|
|
||||||
|
|
||||||
class MySpider1(scrapy.Spider):
|
class MySpider1(scrapy.Spider):
|
||||||
# Your first spider definition
|
# Your first spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class MySpider2(scrapy.Spider):
|
class MySpider2(scrapy.Spider):
|
||||||
# Your second spider definition
|
# Your second spider definition
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
settings = get_project_settings()
|
settings = get_project_settings()
|
||||||
configure_logging(settings)
|
configure_logging(settings)
|
||||||
runner = CrawlerRunner(settings)
|
runner = CrawlerRunner(settings)
|
||||||
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def crawl():
|
def crawl():
|
||||||
yield runner.crawl(MySpider1)
|
yield runner.crawl(MySpider1)
|
||||||
yield runner.crawl(MySpider2)
|
yield runner.crawl(MySpider2)
|
||||||
reactor.stop()
|
reactor.stop()
|
||||||
|
|
||||||
|
|
||||||
crawl()
|
crawl()
|
||||||
reactor.run() # the script will block here until the last crawl call is finished
|
reactor.run() # the script will block here until the last crawl call is finished
|
||||||
|
|
||||||
Different spiders can set different values for the same setting, but when they
|
Different spiders can set different values for the same setting, but when they
|
||||||
run in the same process it may be impossible, by design or because of some
|
run in the same process it may be impossible, by design or because of some
|
||||||
|
|
|
||||||
|
|
@ -78,23 +78,27 @@ Request objects
|
||||||
|
|
||||||
:param cookies: the request cookies. These can be sent in two forms.
|
:param cookies: the request cookies. These can be sent in two forms.
|
||||||
|
|
||||||
1. Using a dict::
|
1. Using a dict:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
request_with_cookies = Request(
|
request_with_cookies = Request(
|
||||||
url="http://www.example.com",
|
url="http://www.example.com",
|
||||||
cookies={'currency': 'USD', 'country': 'UY'},
|
cookies={"currency": "USD", "country": "UY"},
|
||||||
)
|
)
|
||||||
|
|
||||||
2. Using a list of dicts::
|
2. Using a list of dicts:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
request_with_cookies = Request(
|
request_with_cookies = Request(
|
||||||
url="http://www.example.com",
|
url="http://www.example.com",
|
||||||
cookies=[
|
cookies=[
|
||||||
{
|
{
|
||||||
'name': 'currency',
|
"name": "currency",
|
||||||
'value': 'USD',
|
"value": "USD",
|
||||||
'domain': 'example.com',
|
"domain": "example.com",
|
||||||
'path': '/currency',
|
"path": "/currency",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -114,12 +118,14 @@ Request objects
|
||||||
in :attr:`request.meta <scrapy.Request.meta>`.
|
in :attr:`request.meta <scrapy.Request.meta>`.
|
||||||
|
|
||||||
Example of a request that sends manually-defined cookies and ignores
|
Example of a request that sends manually-defined cookies and ignores
|
||||||
cookie storage::
|
cookie storage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
Request(
|
Request(
|
||||||
url="http://www.example.com",
|
url="http://www.example.com",
|
||||||
cookies={'currency': 'USD', 'country': 'UY'},
|
cookies={"currency": "USD", "country": "UY"},
|
||||||
meta={'dont_merge_cookies': True},
|
meta={"dont_merge_cookies": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
For more info see :ref:`cookies-mw`.
|
For more info see :ref:`cookies-mw`.
|
||||||
|
|
@ -259,11 +265,15 @@ The callback of a request is a function that will be called when the response
|
||||||
of that request is downloaded. The callback function will be called with the
|
of that request is downloaded. The callback function will be called with the
|
||||||
downloaded :class:`Response` object as its first argument.
|
downloaded :class:`Response` object as its first argument.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse_page1(self, response):
|
def parse_page1(self, response):
|
||||||
return scrapy.Request("http://www.example.com/some_page.html",
|
return scrapy.Request(
|
||||||
callback=self.parse_page2)
|
"http://www.example.com/some_page.html", callback=self.parse_page2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_page2(self, response):
|
def parse_page2(self, response):
|
||||||
# this would log http://www.example.com/some_page.html
|
# this would log http://www.example.com/some_page.html
|
||||||
|
|
@ -274,15 +284,18 @@ functions so you can receive the arguments later, in the second callback.
|
||||||
The following example shows how to achieve this by using the
|
The following example shows how to achieve this by using the
|
||||||
:attr:`Request.cb_kwargs` attribute:
|
:attr:`Request.cb_kwargs` attribute:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
request = scrapy.Request('http://www.example.com/index.html',
|
request = scrapy.Request(
|
||||||
callback=self.parse_page2,
|
"http://www.example.com/index.html",
|
||||||
cb_kwargs=dict(main_url=response.url))
|
callback=self.parse_page2,
|
||||||
request.cb_kwargs['foo'] = 'bar' # add more arguments for the callback
|
cb_kwargs=dict(main_url=response.url),
|
||||||
|
)
|
||||||
|
request.cb_kwargs["foo"] = "bar" # add more arguments for the callback
|
||||||
yield request
|
yield request
|
||||||
|
|
||||||
|
|
||||||
def parse_page2(self, response, main_url, foo):
|
def parse_page2(self, response, main_url, foo):
|
||||||
yield dict(
|
yield dict(
|
||||||
main_url=main_url,
|
main_url=main_url,
|
||||||
|
|
@ -308,7 +321,9 @@ It receives a :exc:`~twisted.python.failure.Failure` as first parameter and can
|
||||||
be used to track connection establishment timeouts, DNS errors etc.
|
be used to track connection establishment timeouts, DNS errors etc.
|
||||||
|
|
||||||
Here's an example spider logging all errors and catching some specific
|
Here's an example spider logging all errors and catching some specific
|
||||||
errors if needed::
|
errors if needed
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
@ -316,24 +331,28 @@ errors if needed::
|
||||||
from twisted.internet.error import DNSLookupError
|
from twisted.internet.error import DNSLookupError
|
||||||
from twisted.internet.error import TimeoutError, TCPTimedOutError
|
from twisted.internet.error import TimeoutError, TCPTimedOutError
|
||||||
|
|
||||||
|
|
||||||
class ErrbackSpider(scrapy.Spider):
|
class ErrbackSpider(scrapy.Spider):
|
||||||
name = "errback_example"
|
name = "errback_example"
|
||||||
start_urls = [
|
start_urls = [
|
||||||
"http://www.httpbin.org/", # HTTP 200 expected
|
"http://www.httpbin.org/", # HTTP 200 expected
|
||||||
"http://www.httpbin.org/status/404", # Not found error
|
"http://www.httpbin.org/status/404", # Not found error
|
||||||
"http://www.httpbin.org/status/500", # server issue
|
"http://www.httpbin.org/status/500", # server issue
|
||||||
"http://www.httpbin.org:12345/", # non-responding host, timeout expected
|
"http://www.httpbin.org:12345/", # non-responding host, timeout expected
|
||||||
"https://example.invalid/", # DNS error expected
|
"https://example.invalid/", # DNS error expected
|
||||||
]
|
]
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
for u in self.start_urls:
|
for u in self.start_urls:
|
||||||
yield scrapy.Request(u, callback=self.parse_httpbin,
|
yield scrapy.Request(
|
||||||
errback=self.errback_httpbin,
|
u,
|
||||||
dont_filter=True)
|
callback=self.parse_httpbin,
|
||||||
|
errback=self.errback_httpbin,
|
||||||
|
dont_filter=True,
|
||||||
|
)
|
||||||
|
|
||||||
def parse_httpbin(self, response):
|
def parse_httpbin(self, response):
|
||||||
self.logger.info('Got successful response from {}'.format(response.url))
|
self.logger.info("Got successful response from {}".format(response.url))
|
||||||
# do something useful here...
|
# do something useful here...
|
||||||
|
|
||||||
def errback_httpbin(self, failure):
|
def errback_httpbin(self, failure):
|
||||||
|
|
@ -347,16 +366,16 @@ errors if needed::
|
||||||
# these exceptions come from HttpError spider middleware
|
# these exceptions come from HttpError spider middleware
|
||||||
# you can get the non-200 response
|
# you can get the non-200 response
|
||||||
response = failure.value.response
|
response = failure.value.response
|
||||||
self.logger.error('HttpError on %s', response.url)
|
self.logger.error("HttpError on %s", response.url)
|
||||||
|
|
||||||
elif failure.check(DNSLookupError):
|
elif failure.check(DNSLookupError):
|
||||||
# this is the original request
|
# this is the original request
|
||||||
request = failure.request
|
request = failure.request
|
||||||
self.logger.error('DNSLookupError on %s', request.url)
|
self.logger.error("DNSLookupError on %s", request.url)
|
||||||
|
|
||||||
elif failure.check(TimeoutError, TCPTimedOutError):
|
elif failure.check(TimeoutError, TCPTimedOutError):
|
||||||
request = failure.request
|
request = failure.request
|
||||||
self.logger.error('TimeoutError on %s', request.url)
|
self.logger.error("TimeoutError on %s", request.url)
|
||||||
|
|
||||||
|
|
||||||
.. _errback-cb_kwargs:
|
.. _errback-cb_kwargs:
|
||||||
|
|
@ -367,21 +386,27 @@ Accessing additional data in errback functions
|
||||||
In case of a failure to process the request, you may be interested in
|
In case of a failure to process the request, you may be interested in
|
||||||
accessing arguments to the callback functions so you can process further
|
accessing arguments to the callback functions so you can process further
|
||||||
based on the arguments in the errback. The following example shows how to
|
based on the arguments in the errback. The following example shows how to
|
||||||
achieve this by using ``Failure.request.cb_kwargs``::
|
achieve this by using ``Failure.request.cb_kwargs``
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
request = scrapy.Request('http://www.example.com/index.html',
|
request = scrapy.Request(
|
||||||
callback=self.parse_page2,
|
"http://www.example.com/index.html",
|
||||||
errback=self.errback_page2,
|
callback=self.parse_page2,
|
||||||
cb_kwargs=dict(main_url=response.url))
|
errback=self.errback_page2,
|
||||||
|
cb_kwargs=dict(main_url=response.url),
|
||||||
|
)
|
||||||
yield request
|
yield request
|
||||||
|
|
||||||
|
|
||||||
def parse_page2(self, response, main_url):
|
def parse_page2(self, response, main_url):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def errback_page2(self, failure):
|
def errback_page2(self, failure):
|
||||||
yield dict(
|
yield dict(
|
||||||
main_url=failure.request.cb_kwargs['main_url'],
|
main_url=failure.request.cb_kwargs["main_url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -528,18 +553,20 @@ in your :meth:`fingerprint` method implementation:
|
||||||
.. autofunction:: scrapy.utils.request.fingerprint
|
.. autofunction:: scrapy.utils.request.fingerprint
|
||||||
|
|
||||||
For example, to take the value of a request header named ``X-ID`` into
|
For example, to take the value of a request header named ``X-ID`` into
|
||||||
account::
|
account:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
# my_project/settings.py
|
# my_project/settings.py
|
||||||
REQUEST_FINGERPRINTER_CLASS = 'my_project.utils.RequestFingerprinter'
|
REQUEST_FINGERPRINTER_CLASS = "my_project.utils.RequestFingerprinter"
|
||||||
|
|
||||||
# my_project/utils.py
|
# my_project/utils.py
|
||||||
from scrapy.utils.request import fingerprint
|
from scrapy.utils.request import fingerprint
|
||||||
|
|
||||||
class RequestFingerprinter:
|
|
||||||
|
|
||||||
|
class RequestFingerprinter:
|
||||||
def fingerprint(self, request):
|
def fingerprint(self, request):
|
||||||
return fingerprint(request, include_headers=['X-ID'])
|
return fingerprint(request, include_headers=["X-ID"])
|
||||||
|
|
||||||
You can also write your own fingerprinting logic from scratch.
|
You can also write your own fingerprinting logic from scratch.
|
||||||
|
|
||||||
|
|
@ -555,13 +582,16 @@ you use :class:`~weakref.WeakKeyDictionary` to cache request fingerprints:
|
||||||
references to them in your cache dictionary.
|
references to them in your cache dictionary.
|
||||||
|
|
||||||
For example, to take into account only the URL of a request, without any prior
|
For example, to take into account only the URL of a request, without any prior
|
||||||
URL canonicalization or taking the request method or body into account::
|
URL canonicalization or taking the request method or body into account:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
from scrapy.utils.python import to_bytes
|
from scrapy.utils.python import to_bytes
|
||||||
|
|
||||||
|
|
||||||
class RequestFingerprinter:
|
class RequestFingerprinter:
|
||||||
|
|
||||||
cache = WeakKeyDictionary()
|
cache = WeakKeyDictionary()
|
||||||
|
|
@ -577,21 +607,25 @@ If you need to be able to override the request fingerprinting for arbitrary
|
||||||
requests from your spider callbacks, you may implement a request fingerprinter
|
requests from your spider callbacks, you may implement a request fingerprinter
|
||||||
that reads fingerprints from :attr:`request.meta <scrapy.http.Request.meta>`
|
that reads fingerprints from :attr:`request.meta <scrapy.http.Request.meta>`
|
||||||
when available, and then falls back to
|
when available, and then falls back to
|
||||||
:func:`scrapy.utils.request.fingerprint`. For example::
|
:func:`scrapy.utils.request.fingerprint`. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.utils.request import fingerprint
|
from scrapy.utils.request import fingerprint
|
||||||
|
|
||||||
class RequestFingerprinter:
|
|
||||||
|
|
||||||
|
class RequestFingerprinter:
|
||||||
def fingerprint(self, request):
|
def fingerprint(self, request):
|
||||||
if 'fingerprint' in request.meta:
|
if "fingerprint" in request.meta:
|
||||||
return request.meta['fingerprint']
|
return request.meta["fingerprint"]
|
||||||
return fingerprint(request)
|
return fingerprint(request)
|
||||||
|
|
||||||
If you need to reproduce the same fingerprinting algorithm as Scrapy 2.6
|
If you need to reproduce the same fingerprinting algorithm as Scrapy 2.6
|
||||||
without using the deprecated ``'2.6'`` value of the
|
without using the deprecated ``'2.6'`` value of the
|
||||||
:setting:`REQUEST_FINGERPRINTER_IMPLEMENTATION` setting, use the following
|
:setting:`REQUEST_FINGERPRINTER_IMPLEMENTATION` setting, use the following
|
||||||
request fingerprinter::
|
request fingerprinter:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
|
|
@ -599,6 +633,7 @@ request fingerprinter::
|
||||||
from scrapy.utils.python import to_bytes
|
from scrapy.utils.python import to_bytes
|
||||||
from w3lib.url import canonicalize_url
|
from w3lib.url import canonicalize_url
|
||||||
|
|
||||||
|
|
||||||
class RequestFingerprinter:
|
class RequestFingerprinter:
|
||||||
|
|
||||||
cache = WeakKeyDictionary()
|
cache = WeakKeyDictionary()
|
||||||
|
|
@ -608,7 +643,7 @@ request fingerprinter::
|
||||||
fp = sha1()
|
fp = sha1()
|
||||||
fp.update(to_bytes(request.method))
|
fp.update(to_bytes(request.method))
|
||||||
fp.update(to_bytes(canonicalize_url(request.url)))
|
fp.update(to_bytes(canonicalize_url(request.url)))
|
||||||
fp.update(request.body or b'')
|
fp.update(request.body or b"")
|
||||||
self.cache[request] = fp.digest()
|
self.cache[request] = fp.digest()
|
||||||
return self.cache[request]
|
return self.cache[request]
|
||||||
|
|
||||||
|
|
@ -737,7 +772,9 @@ Stopping the download of a Response
|
||||||
|
|
||||||
Raising a :exc:`~scrapy.exceptions.StopDownload` exception from a handler for the
|
Raising a :exc:`~scrapy.exceptions.StopDownload` exception from a handler for the
|
||||||
:class:`~scrapy.signals.bytes_received` or :class:`~scrapy.signals.headers_received`
|
:class:`~scrapy.signals.bytes_received` or :class:`~scrapy.signals.headers_received`
|
||||||
signals will stop the download of a given response. See the following example::
|
signals will stop the download of a given response. See the following example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
@ -749,7 +786,9 @@ signals will stop the download of a given response. See the following example::
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler):
|
||||||
spider = super().from_crawler(crawler)
|
spider = super().from_crawler(crawler)
|
||||||
crawler.signals.connect(spider.on_bytes_received, signal=scrapy.signals.bytes_received)
|
crawler.signals.connect(
|
||||||
|
spider.on_bytes_received, signal=scrapy.signals.bytes_received
|
||||||
|
)
|
||||||
return spider
|
return spider
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
|
|
@ -878,11 +917,17 @@ Using FormRequest to send data via HTTP POST
|
||||||
|
|
||||||
If you want to simulate a HTML Form POST in your spider and send a couple of
|
If you want to simulate a HTML Form POST in your spider and send a couple of
|
||||||
key-value fields, you can return a :class:`FormRequest` object (from your
|
key-value fields, you can return a :class:`FormRequest` object (from your
|
||||||
spider) like this::
|
spider) like this:
|
||||||
|
|
||||||
return [FormRequest(url="http://www.example.com/post/action",
|
.. code-block:: python
|
||||||
formdata={'name': 'John Doe', 'age': '27'},
|
|
||||||
callback=self.after_post)]
|
return [
|
||||||
|
FormRequest(
|
||||||
|
url="http://www.example.com/post/action",
|
||||||
|
formdata={"name": "John Doe", "age": "27"},
|
||||||
|
callback=self.after_post,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
.. _topics-request-response-ref-request-userlogin:
|
.. _topics-request-response-ref-request-userlogin:
|
||||||
|
|
||||||
|
|
@ -894,25 +939,28 @@ type="hidden">`` elements, such as session related data or authentication
|
||||||
tokens (for login pages). When scraping, you'll want these fields to be
|
tokens (for login pages). When scraping, you'll want these fields to be
|
||||||
automatically pre-populated and only override a couple of them, such as the
|
automatically pre-populated and only override a couple of them, such as the
|
||||||
user name and password. You can use the :meth:`FormRequest.from_response`
|
user name and password. You can use the :meth:`FormRequest.from_response`
|
||||||
method for this job. Here's an example spider which uses it::
|
method for this job. Here's an example spider which uses it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
def authentication_failed(response):
|
def authentication_failed(response):
|
||||||
# TODO: Check the contents of the response and return True if it failed
|
# TODO: Check the contents of the response and return True if it failed
|
||||||
# or False if it succeeded.
|
# or False if it succeeded.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LoginSpider(scrapy.Spider):
|
class LoginSpider(scrapy.Spider):
|
||||||
name = 'example.com'
|
name = "example.com"
|
||||||
start_urls = ['http://www.example.com/users/login.php']
|
start_urls = ["http://www.example.com/users/login.php"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
return scrapy.FormRequest.from_response(
|
return scrapy.FormRequest.from_response(
|
||||||
response,
|
response,
|
||||||
formdata={'username': 'john', 'password': 'secret'},
|
formdata={"username": "john", "password": "secret"},
|
||||||
callback=self.after_login
|
callback=self.after_login,
|
||||||
)
|
)
|
||||||
|
|
||||||
def after_login(self, response):
|
def after_login(self, response):
|
||||||
|
|
@ -952,13 +1000,15 @@ dealing with JSON requests.
|
||||||
JsonRequest usage example
|
JsonRequest usage example
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
Sending a JSON POST request with a JSON payload::
|
Sending a JSON POST request with a JSON payload:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'name1': 'value1',
|
"name1": "value1",
|
||||||
'name2': 'value2',
|
"name2": "value2",
|
||||||
}
|
}
|
||||||
yield JsonRequest(url='http://www.example.com/post/action', data=data)
|
yield JsonRequest(url="http://www.example.com/post/action", data=data)
|
||||||
|
|
||||||
|
|
||||||
Response objects
|
Response objects
|
||||||
|
|
|
||||||
|
|
@ -981,25 +981,34 @@ Selector examples on HTML response
|
||||||
|
|
||||||
Here are some :class:`Selector` examples to illustrate several concepts.
|
Here are some :class:`Selector` examples to illustrate several concepts.
|
||||||
In all cases, we assume there is already a :class:`Selector` instantiated with
|
In all cases, we assume there is already a :class:`Selector` instantiated with
|
||||||
a :class:`~scrapy.http.HtmlResponse` object like this::
|
a :class:`~scrapy.http.HtmlResponse` object like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
sel = Selector(html_response)
|
sel = Selector(html_response)
|
||||||
|
|
||||||
1. Select all ``<h1>`` elements from an HTML response body, returning a list of
|
1. Select all ``<h1>`` elements from an HTML response body, returning a list of
|
||||||
:class:`Selector` objects (i.e. a :class:`SelectorList` object)::
|
:class:`Selector` objects (i.e. a :class:`SelectorList` object):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
sel.xpath("//h1")
|
sel.xpath("//h1")
|
||||||
|
|
||||||
2. Extract the text of all ``<h1>`` elements from an HTML response body,
|
2. Extract the text of all ``<h1>`` elements from an HTML response body,
|
||||||
returning a list of strings::
|
returning a list of strings:
|
||||||
|
|
||||||
sel.xpath("//h1").getall() # this includes the h1 tag
|
.. code-block:: python
|
||||||
|
|
||||||
|
sel.xpath("//h1").getall() # this includes the h1 tag
|
||||||
sel.xpath("//h1/text()").getall() # this excludes the h1 tag
|
sel.xpath("//h1/text()").getall() # this excludes the h1 tag
|
||||||
|
|
||||||
3. Iterate over all ``<p>`` tags and print their class attribute::
|
3. Iterate over all ``<p>`` tags and print their class attribute:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
for node in sel.xpath("//p"):
|
for node in sel.xpath("//p"):
|
||||||
print(node.attrib['class'])
|
print(node.attrib["class"])
|
||||||
|
|
||||||
|
|
||||||
.. _selector-examples-xml:
|
.. _selector-examples-xml:
|
||||||
|
|
@ -1008,17 +1017,23 @@ Selector examples on XML response
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
Here are some examples to illustrate concepts for :class:`Selector` objects
|
Here are some examples to illustrate concepts for :class:`Selector` objects
|
||||||
instantiated with an :class:`~scrapy.http.XmlResponse` object::
|
instantiated with an :class:`~scrapy.http.XmlResponse` object:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
sel = Selector(xml_response)
|
sel = Selector(xml_response)
|
||||||
|
|
||||||
1. Select all ``<product>`` elements from an XML response body, returning a list
|
1. Select all ``<product>`` elements from an XML response body, returning a list
|
||||||
of :class:`Selector` objects (i.e. a :class:`SelectorList` object)::
|
of :class:`Selector` objects (i.e. a :class:`SelectorList` object):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
sel.xpath("//product")
|
sel.xpath("//product")
|
||||||
|
|
||||||
2. Extract all prices from a `Google Base XML feed`_ which requires registering
|
2. Extract all prices from a `Google Base XML feed`_ which requires registering
|
||||||
a namespace::
|
a namespace:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
sel.register_namespace("g", "http://base.google.com/ns/1.0")
|
sel.register_namespace("g", "http://base.google.com/ns/1.0")
|
||||||
sel.xpath("//g:price").getall()
|
sel.xpath("//g:price").getall()
|
||||||
|
|
|
||||||
|
|
@ -67,13 +67,15 @@ Example::
|
||||||
|
|
||||||
Spiders (See the :ref:`topics-spiders` chapter for reference) can define their
|
Spiders (See the :ref:`topics-spiders` chapter for reference) can define their
|
||||||
own settings that will take precedence and override the project ones. They can
|
own settings that will take precedence and override the project ones. They can
|
||||||
do so by setting their :attr:`~scrapy.Spider.custom_settings` attribute::
|
do so by setting their :attr:`~scrapy.Spider.custom_settings` attribute:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
|
|
||||||
custom_settings = {
|
custom_settings = {
|
||||||
'SOME_SETTING': 'some value',
|
"SOME_SETTING": "some value",
|
||||||
}
|
}
|
||||||
|
|
||||||
3. Project settings module
|
3. Project settings module
|
||||||
|
|
@ -115,14 +117,17 @@ class or a function, there are two different ways you can specify that object:
|
||||||
|
|
||||||
- As the object itself
|
- As the object itself
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from mybot.pipelines.validate import ValidateMyItem
|
from mybot.pipelines.validate import ValidateMyItem
|
||||||
|
|
||||||
ITEM_PIPELINES = {
|
ITEM_PIPELINES = {
|
||||||
# passing the classname...
|
# passing the classname...
|
||||||
ValidateMyItem: 300,
|
ValidateMyItem: 300,
|
||||||
# ...equals passing the class path
|
# ...equals passing the class path
|
||||||
'mybot.pipelines.validate.ValidateMyItem': 300,
|
"mybot.pipelines.validate.ValidateMyItem": 300,
|
||||||
}
|
}
|
||||||
|
|
||||||
.. note:: Passing non-callable objects is not supported.
|
.. note:: Passing non-callable objects is not supported.
|
||||||
|
|
@ -133,11 +138,13 @@ How to access settings
|
||||||
|
|
||||||
.. highlight:: python
|
.. highlight:: python
|
||||||
|
|
||||||
In a spider, the settings are available through ``self.settings``::
|
In a spider, the settings are available through ``self.settings``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
start_urls = ['http://example.com']
|
start_urls = ["http://example.com"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
print(f"Existing settings: {self.settings.attributes.keys()}")
|
print(f"Existing settings: {self.settings.attributes.keys()}")
|
||||||
|
|
@ -150,7 +157,9 @@ In a spider, the settings are available through ``self.settings``::
|
||||||
|
|
||||||
Settings can be accessed through the :attr:`scrapy.crawler.Crawler.settings`
|
Settings can be accessed through the :attr:`scrapy.crawler.Crawler.settings`
|
||||||
attribute of the Crawler that is passed to ``from_crawler`` method in
|
attribute of the Crawler that is passed to ``from_crawler`` method in
|
||||||
extensions, middlewares and item pipelines::
|
extensions, middlewares and item pipelines:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MyExtension:
|
class MyExtension:
|
||||||
def __init__(self, log_is_enabled=False):
|
def __init__(self, log_is_enabled=False):
|
||||||
|
|
@ -160,7 +169,7 @@ extensions, middlewares and item pipelines::
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler):
|
||||||
settings = crawler.settings
|
settings = crawler.settings
|
||||||
return cls(settings.getbool('LOG_ENABLED'))
|
return cls(settings.getbool("LOG_ENABLED"))
|
||||||
|
|
||||||
The settings object can be used like a dict (e.g.,
|
The settings object can be used like a dict (e.g.,
|
||||||
``settings['LOG_ENABLED']``), but it's usually preferred to extract the setting
|
``settings['LOG_ENABLED']``), but it's usually preferred to extract the setting
|
||||||
|
|
@ -365,11 +374,13 @@ Scrapy shell <topics-shell>`.
|
||||||
DEFAULT_REQUEST_HEADERS
|
DEFAULT_REQUEST_HEADERS
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
'Accept-Language': 'en',
|
"Accept-Language": "en",
|
||||||
}
|
}
|
||||||
|
|
||||||
The default headers used for Scrapy HTTP Requests. They're populated in the
|
The default headers used for Scrapy HTTP Requests. They're populated in the
|
||||||
|
|
@ -404,9 +415,11 @@ Scope: ``scrapy.spidermiddlewares.depth.DepthMiddleware``
|
||||||
An integer that is used to adjust the :attr:`~scrapy.Request.priority` of
|
An integer that is used to adjust the :attr:`~scrapy.Request.priority` of
|
||||||
a :class:`~scrapy.Request` based on its depth.
|
a :class:`~scrapy.Request` based on its depth.
|
||||||
|
|
||||||
The priority of a request is adjusted as follows::
|
The priority of a request is adjusted as follows:
|
||||||
|
|
||||||
request.priority = request.priority - ( depth * DEPTH_PRIORITY )
|
.. code-block:: python
|
||||||
|
|
||||||
|
request.priority = request.priority - (depth * DEPTH_PRIORITY)
|
||||||
|
|
||||||
As depth increases, positive values of ``DEPTH_PRIORITY`` decrease request
|
As depth increases, positive values of ``DEPTH_PRIORITY`` decrease request
|
||||||
priority (BFO), while negative values increase request priority (DFO). See
|
priority (BFO), while negative values increase request priority (DFO). See
|
||||||
|
|
@ -595,23 +608,25 @@ orders. For more info see :ref:`topics-downloader-middleware-setting`.
|
||||||
DOWNLOADER_MIDDLEWARES_BASE
|
DOWNLOADER_MIDDLEWARES_BASE
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,
|
"scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware": 100,
|
||||||
'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,
|
"scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware": 300,
|
||||||
'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350,
|
"scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware": 350,
|
||||||
'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400,
|
"scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware": 400,
|
||||||
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,
|
"scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": 500,
|
||||||
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
|
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
|
||||||
'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,
|
"scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware": 560,
|
||||||
'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,
|
"scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware": 580,
|
||||||
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,
|
"scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware": 590,
|
||||||
'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,
|
"scrapy.downloadermiddlewares.redirect.RedirectMiddleware": 600,
|
||||||
'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,
|
"scrapy.downloadermiddlewares.cookies.CookiesMiddleware": 700,
|
||||||
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,
|
"scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750,
|
||||||
'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,
|
"scrapy.downloadermiddlewares.stats.DownloaderStats": 850,
|
||||||
'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900,
|
"scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware": 900,
|
||||||
}
|
}
|
||||||
|
|
||||||
A dict containing the downloader middlewares enabled by default in Scrapy. Low
|
A dict containing the downloader middlewares enabled by default in Scrapy. Low
|
||||||
|
|
@ -687,15 +702,17 @@ See :setting:`DOWNLOAD_HANDLERS_BASE` for example format.
|
||||||
DOWNLOAD_HANDLERS_BASE
|
DOWNLOAD_HANDLERS_BASE
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'data': 'scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler',
|
"data": "scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler",
|
||||||
'file': 'scrapy.core.downloader.handlers.file.FileDownloadHandler',
|
"file": "scrapy.core.downloader.handlers.file.FileDownloadHandler",
|
||||||
'http': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
|
"http": "scrapy.core.downloader.handlers.http.HTTPDownloadHandler",
|
||||||
'https': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
|
"https": "scrapy.core.downloader.handlers.http.HTTPDownloadHandler",
|
||||||
's3': 'scrapy.core.downloader.handlers.s3.S3DownloadHandler',
|
"s3": "scrapy.core.downloader.handlers.s3.S3DownloadHandler",
|
||||||
'ftp': 'scrapy.core.downloader.handlers.ftp.FTPDownloadHandler',
|
"ftp": "scrapy.core.downloader.handlers.ftp.FTPDownloadHandler",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -705,10 +722,12 @@ You should never modify this setting in your project, modify
|
||||||
|
|
||||||
You can disable any of these download handlers by assigning ``None`` to their
|
You can disable any of these download handlers by assigning ``None`` to their
|
||||||
URI scheme in :setting:`DOWNLOAD_HANDLERS`. E.g., to disable the built-in FTP
|
URI scheme in :setting:`DOWNLOAD_HANDLERS`. E.g., to disable the built-in FTP
|
||||||
handler (without replacement), place this in your ``settings.py``::
|
handler (without replacement), place this in your ``settings.py``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
DOWNLOAD_HANDLERS = {
|
DOWNLOAD_HANDLERS = {
|
||||||
'ftp': None,
|
"ftp": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
.. _http2:
|
.. _http2:
|
||||||
|
|
@ -718,10 +737,12 @@ The default HTTPS handler uses HTTP/1.1. To use HTTP/2:
|
||||||
#. Install ``Twisted[http2]>=17.9.0`` to install the packages required to
|
#. Install ``Twisted[http2]>=17.9.0`` to install the packages required to
|
||||||
enable HTTP/2 support in Twisted.
|
enable HTTP/2 support in Twisted.
|
||||||
|
|
||||||
#. Update :setting:`DOWNLOAD_HANDLERS` as follows::
|
#. Update :setting:`DOWNLOAD_HANDLERS` as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
DOWNLOAD_HANDLERS = {
|
DOWNLOAD_HANDLERS = {
|
||||||
'https': 'scrapy.core.downloader.handlers.http2.H2DownloadHandler',
|
"https": "scrapy.core.downloader.handlers.http2.H2DownloadHandler",
|
||||||
}
|
}
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
@ -890,18 +911,20 @@ A dict containing the extensions enabled in your project, and their orders.
|
||||||
EXTENSIONS_BASE
|
EXTENSIONS_BASE
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'scrapy.extensions.corestats.CoreStats': 0,
|
"scrapy.extensions.corestats.CoreStats": 0,
|
||||||
'scrapy.extensions.telnet.TelnetConsole': 0,
|
"scrapy.extensions.telnet.TelnetConsole": 0,
|
||||||
'scrapy.extensions.memusage.MemoryUsage': 0,
|
"scrapy.extensions.memusage.MemoryUsage": 0,
|
||||||
'scrapy.extensions.memdebug.MemoryDebugger': 0,
|
"scrapy.extensions.memdebug.MemoryDebugger": 0,
|
||||||
'scrapy.extensions.closespider.CloseSpider': 0,
|
"scrapy.extensions.closespider.CloseSpider": 0,
|
||||||
'scrapy.extensions.feedexport.FeedExporter': 0,
|
"scrapy.extensions.feedexport.FeedExporter": 0,
|
||||||
'scrapy.extensions.logstats.LogStats': 0,
|
"scrapy.extensions.logstats.LogStats": 0,
|
||||||
'scrapy.extensions.spiderstate.SpiderState': 0,
|
"scrapy.extensions.spiderstate.SpiderState": 0,
|
||||||
'scrapy.extensions.throttle.AutoThrottle': 0,
|
"scrapy.extensions.throttle.AutoThrottle": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
A dict containing the extensions available by default in Scrapy, and their
|
A dict containing the extensions available by default in Scrapy, and their
|
||||||
|
|
@ -988,11 +1011,13 @@ A dict containing the item pipelines to use, and their orders. Order values are
|
||||||
arbitrary, but it is customary to define them in the 0-1000 range. Lower orders
|
arbitrary, but it is customary to define them in the 0-1000 range. Lower orders
|
||||||
process before higher orders.
|
process before higher orders.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
ITEM_PIPELINES = {
|
ITEM_PIPELINES = {
|
||||||
'mybot.pipelines.validate.ValidateMyItem': 300,
|
"mybot.pipelines.validate.ValidateMyItem": 300,
|
||||||
'mybot.pipelines.validate.StoreMyItem': 800,
|
"mybot.pipelines.validate.StoreMyItem": 800,
|
||||||
}
|
}
|
||||||
|
|
||||||
.. setting:: ITEM_PIPELINES_BASE
|
.. setting:: ITEM_PIPELINES_BASE
|
||||||
|
|
@ -1417,12 +1442,14 @@ testing spiders. For more info see :ref:`topics-contracts`.
|
||||||
SPIDER_CONTRACTS_BASE
|
SPIDER_CONTRACTS_BASE
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'scrapy.contracts.default.UrlContract' : 1,
|
"scrapy.contracts.default.UrlContract": 1,
|
||||||
'scrapy.contracts.default.ReturnsContract': 2,
|
"scrapy.contracts.default.ReturnsContract": 2,
|
||||||
'scrapy.contracts.default.ScrapesContract': 3,
|
"scrapy.contracts.default.ScrapesContract": 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
A dict containing the Scrapy contracts enabled by default in Scrapy. You should
|
A dict containing the Scrapy contracts enabled by default in Scrapy. You should
|
||||||
|
|
@ -1431,10 +1458,12 @@ instead. For more info see :ref:`topics-contracts`.
|
||||||
|
|
||||||
You can disable any of these contracts by assigning ``None`` to their class
|
You can disable any of these contracts by assigning ``None`` to their class
|
||||||
path in :setting:`SPIDER_CONTRACTS`. E.g., to disable the built-in
|
path in :setting:`SPIDER_CONTRACTS`. E.g., to disable the built-in
|
||||||
``ScrapesContract``, place this in your ``settings.py``::
|
``ScrapesContract``, place this in your ``settings.py``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
SPIDER_CONTRACTS = {
|
SPIDER_CONTRACTS = {
|
||||||
'scrapy.contracts.default.ScrapesContract': None,
|
"scrapy.contracts.default.ScrapesContract": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
.. setting:: SPIDER_LOADER_CLASS
|
.. setting:: SPIDER_LOADER_CLASS
|
||||||
|
|
@ -1483,14 +1512,16 @@ orders. For more info see :ref:`topics-spider-middleware-setting`.
|
||||||
SPIDER_MIDDLEWARES_BASE
|
SPIDER_MIDDLEWARES_BASE
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
Default::
|
Default:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'scrapy.spidermiddlewares.httperror.HttpErrorMiddleware': 50,
|
"scrapy.spidermiddlewares.httperror.HttpErrorMiddleware": 50,
|
||||||
'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': 500,
|
"scrapy.spidermiddlewares.offsite.OffsiteMiddleware": 500,
|
||||||
'scrapy.spidermiddlewares.referer.RefererMiddleware': 700,
|
"scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
|
||||||
'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware': 800,
|
"scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
|
||||||
'scrapy.spidermiddlewares.depth.DepthMiddleware': 900,
|
"scrapy.spidermiddlewares.depth.DepthMiddleware": 900,
|
||||||
}
|
}
|
||||||
|
|
||||||
A dict containing the spider middlewares enabled by default in Scrapy, and
|
A dict containing the spider middlewares enabled by default in Scrapy, and
|
||||||
|
|
@ -1506,9 +1537,11 @@ Default: ``[]``
|
||||||
|
|
||||||
A list of modules where Scrapy will look for spiders.
|
A list of modules where Scrapy will look for spiders.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
SPIDER_MODULES = ['mybot.spiders_prod', 'mybot.spiders_dev']
|
.. code-block:: python
|
||||||
|
|
||||||
|
SPIDER_MODULES = ["mybot.spiders_prod", "mybot.spiders_dev"]
|
||||||
|
|
||||||
.. setting:: STATS_CLASS
|
.. setting:: STATS_CLASS
|
||||||
|
|
||||||
|
|
@ -1597,60 +1630,65 @@ If a reactor is already installed,
|
||||||
third-party libraries will make Scrapy raise :exc:`Exception` when
|
third-party libraries will make Scrapy raise :exc:`Exception` when
|
||||||
it checks which reactor is installed.
|
it checks which reactor is installed.
|
||||||
|
|
||||||
In order to use the reactor installed by Scrapy::
|
In order to use the reactor installed by Scrapy:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
|
||||||
class QuotesSpider(scrapy.Spider):
|
class QuotesSpider(scrapy.Spider):
|
||||||
name = 'quotes'
|
name = "quotes"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.timeout = int(kwargs.pop('timeout', '60'))
|
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
super(QuotesSpider, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
reactor.callLater(self.timeout, self.stop)
|
reactor.callLater(self.timeout, self.stop)
|
||||||
|
|
||||||
urls = ['https://quotes.toscrape.com/page/1']
|
urls = ["https://quotes.toscrape.com/page/1"]
|
||||||
for url in urls:
|
for url in urls:
|
||||||
yield scrapy.Request(url=url, callback=self.parse)
|
yield scrapy.Request(url=url, callback=self.parse)
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {'text': quote.css('span.text::text').get()}
|
yield {"text": quote.css("span.text::text").get()}
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.crawler.engine.close_spider(self, 'timeout')
|
self.crawler.engine.close_spider(self, "timeout")
|
||||||
|
|
||||||
|
|
||||||
which raises :exc:`Exception`, becomes::
|
which raises :exc:`Exception`, becomes:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class QuotesSpider(scrapy.Spider):
|
class QuotesSpider(scrapy.Spider):
|
||||||
name = 'quotes'
|
name = "quotes"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.timeout = int(kwargs.pop('timeout', '60'))
|
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
super(QuotesSpider, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
reactor.callLater(self.timeout, self.stop)
|
reactor.callLater(self.timeout, self.stop)
|
||||||
|
|
||||||
urls = ['https://quotes.toscrape.com/page/1']
|
urls = ["https://quotes.toscrape.com/page/1"]
|
||||||
for url in urls:
|
for url in urls:
|
||||||
yield scrapy.Request(url=url, callback=self.parse)
|
yield scrapy.Request(url=url, callback=self.parse)
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {'text': quote.css('span.text::text').get()}
|
yield {"text": quote.css("span.text::text").get()}
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.crawler.engine.close_spider(self, 'timeout')
|
self.crawler.engine.close_spider(self, "timeout")
|
||||||
|
|
||||||
|
|
||||||
The default value of the :setting:`TWISTED_REACTOR` setting is ``None``, which
|
The default value of the :setting:`TWISTED_REACTOR` setting is ``None``, which
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,9 @@ getting there.
|
||||||
|
|
||||||
This can be achieved by using the ``scrapy.shell.inspect_response`` function.
|
This can be achieved by using the ``scrapy.shell.inspect_response`` function.
|
||||||
|
|
||||||
Here's an example of how you would call it from your spider::
|
Here's an example of how you would call it from your spider:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
@ -259,6 +261,7 @@ Here's an example of how you would call it from your spider::
|
||||||
# We want to inspect one specific response.
|
# We want to inspect one specific response.
|
||||||
if ".org" in response.url:
|
if ".org" in response.url:
|
||||||
from scrapy.shell import inspect_response
|
from scrapy.shell import inspect_response
|
||||||
|
|
||||||
inspect_response(response, self)
|
inspect_response(response, self)
|
||||||
|
|
||||||
# Rest of parsing code.
|
# Rest of parsing code.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ deliver the arguments that the handler receives.
|
||||||
You can connect to signals (or send your own) through the
|
You can connect to signals (or send your own) through the
|
||||||
:ref:`topics-api-signals`.
|
:ref:`topics-api-signals`.
|
||||||
|
|
||||||
Here is a simple example showing how you can catch signals and perform some action::
|
Here is a simple example showing how you can catch signals and perform some action:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy import signals
|
from scrapy import signals
|
||||||
from scrapy import Spider
|
from scrapy import Spider
|
||||||
|
|
@ -30,17 +32,14 @@ Here is a simple example showing how you can catch signals and perform some acti
|
||||||
"http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/",
|
"http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler, *args, **kwargs):
|
def from_crawler(cls, crawler, *args, **kwargs):
|
||||||
spider = super(DmozSpider, cls).from_crawler(crawler, *args, **kwargs)
|
spider = super(DmozSpider, cls).from_crawler(crawler, *args, **kwargs)
|
||||||
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
|
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
|
||||||
return spider
|
return spider
|
||||||
|
|
||||||
|
|
||||||
def spider_closed(self, spider):
|
def spider_closed(self, spider):
|
||||||
spider.logger.info('Spider closed: %s', spider.name)
|
spider.logger.info("Spider closed: %s", spider.name)
|
||||||
|
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
pass
|
pass
|
||||||
|
|
@ -56,11 +55,13 @@ you to run asynchronous code that does not block Scrapy. If a signal
|
||||||
handler returns one of these objects, Scrapy waits for that asynchronous
|
handler returns one of these objects, Scrapy waits for that asynchronous
|
||||||
operation to finish.
|
operation to finish.
|
||||||
|
|
||||||
Let's take an example using :ref:`coroutines <topics-coroutines>`::
|
Let's take an example using :ref:`coroutines <topics-coroutines>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class SignalSpider(scrapy.Spider):
|
class SignalSpider(scrapy.Spider):
|
||||||
name = 'signals'
|
name = "signals"
|
||||||
start_urls = ['https://quotes.toscrape.com/page/1/']
|
start_urls = ["https://quotes.toscrape.com/page/1/"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler, *args, **kwargs):
|
def from_crawler(cls, crawler, *args, **kwargs):
|
||||||
|
|
@ -71,19 +72,19 @@ Let's take an example using :ref:`coroutines <topics-coroutines>`::
|
||||||
async def item_scraped(self, item):
|
async def item_scraped(self, item):
|
||||||
# Send the scraped item to the server
|
# Send the scraped item to the server
|
||||||
response = await treq.post(
|
response = await treq.post(
|
||||||
'http://example.com/post',
|
"http://example.com/post",
|
||||||
json.dumps(item).encode('ascii'),
|
json.dumps(item).encode("ascii"),
|
||||||
headers={b'Content-Type': [b'application/json']}
|
headers={b"Content-Type": [b"application/json"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for quote in response.css('div.quote'):
|
for quote in response.css("div.quote"):
|
||||||
yield {
|
yield {
|
||||||
'text': quote.css('span.text::text').get(),
|
"text": quote.css("span.text::text").get(),
|
||||||
'author': quote.css('small.author::text').get(),
|
"author": quote.css("small.author::text").get(),
|
||||||
'tags': quote.css('div.tags a.tag::text').getall(),
|
"tags": quote.css("div.tags a.tag::text").getall(),
|
||||||
}
|
}
|
||||||
|
|
||||||
See the :ref:`topics-signals-ref` below to know which signals support
|
See the :ref:`topics-signals-ref` below to know which signals support
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,12 @@ To activate a spider middleware component, add it to the
|
||||||
:setting:`SPIDER_MIDDLEWARES` setting, which is a dict whose keys are the
|
:setting:`SPIDER_MIDDLEWARES` setting, which is a dict whose keys are the
|
||||||
middleware class path and their values are the middleware orders.
|
middleware class path and their values are the middleware orders.
|
||||||
|
|
||||||
Here's an example::
|
Here's an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
SPIDER_MIDDLEWARES = {
|
SPIDER_MIDDLEWARES = {
|
||||||
'myproject.middlewares.CustomSpiderMiddleware': 543,
|
"myproject.middlewares.CustomSpiderMiddleware": 543,
|
||||||
}
|
}
|
||||||
|
|
||||||
The :setting:`SPIDER_MIDDLEWARES` setting is merged with the
|
The :setting:`SPIDER_MIDDLEWARES` setting is merged with the
|
||||||
|
|
@ -44,11 +46,13 @@ previous (or subsequent) middleware being applied.
|
||||||
If you want to disable a builtin middleware (the ones defined in
|
If you want to disable a builtin middleware (the ones defined in
|
||||||
:setting:`SPIDER_MIDDLEWARES_BASE`, and enabled by default) you must define it
|
:setting:`SPIDER_MIDDLEWARES_BASE`, and enabled by default) you must define it
|
||||||
in your project :setting:`SPIDER_MIDDLEWARES` setting and assign ``None`` as its
|
in your project :setting:`SPIDER_MIDDLEWARES` setting and assign ``None`` as its
|
||||||
value. For example, if you want to disable the off-site middleware::
|
value. For example, if you want to disable the off-site middleware:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
SPIDER_MIDDLEWARES = {
|
SPIDER_MIDDLEWARES = {
|
||||||
'myproject.middlewares.CustomSpiderMiddleware': 543,
|
"myproject.middlewares.CustomSpiderMiddleware": 543,
|
||||||
'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': None,
|
"scrapy.spidermiddlewares.offsite.OffsiteMiddleware": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
Finally, keep in mind that some middlewares may need to be enabled through a
|
Finally, keep in mind that some middlewares may need to be enabled through a
|
||||||
|
|
@ -261,7 +265,9 @@ specify which response codes the spider is able to handle using the
|
||||||
:setting:`HTTPERROR_ALLOWED_CODES` setting.
|
:setting:`HTTPERROR_ALLOWED_CODES` setting.
|
||||||
|
|
||||||
For example, if you want your spider to handle 404 responses you can do
|
For example, if you want your spider to handle 404 responses you can do
|
||||||
this::
|
this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(CrawlSpider):
|
class MySpider(CrawlSpider):
|
||||||
handle_httpstatus_list = [404]
|
handle_httpstatus_list = [404]
|
||||||
|
|
|
||||||
|
|
@ -157,15 +157,21 @@ scrapy.Spider
|
||||||
|
|
||||||
If you want to change the Requests used to start scraping a domain, this is
|
If you want to change the Requests used to start scraping a domain, this is
|
||||||
the method to override. For example, if you need to start by logging in using
|
the method to override. For example, if you need to start by logging in using
|
||||||
a POST request, you could do::
|
a POST request, you could do:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
return [scrapy.FormRequest("http://www.example.com/login",
|
return [
|
||||||
formdata={'user': 'john', 'pass': 'secret'},
|
scrapy.FormRequest(
|
||||||
callback=self.logged_in)]
|
"http://www.example.com/login",
|
||||||
|
formdata={"user": "john", "pass": "secret"},
|
||||||
|
callback=self.logged_in,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def logged_in(self, response):
|
def logged_in(self, response):
|
||||||
# here you would extract links to follow and return Requests for
|
# here you would extract links to follow and return Requests for
|
||||||
|
|
@ -200,63 +206,71 @@ scrapy.Spider
|
||||||
Called when the spider closes. This method provides a shortcut to
|
Called when the spider closes. This method provides a shortcut to
|
||||||
signals.connect() for the :signal:`spider_closed` signal.
|
signals.connect() for the :signal:`spider_closed` signal.
|
||||||
|
|
||||||
Let's see an example::
|
Let's see an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'example.com'
|
name = "example.com"
|
||||||
allowed_domains = ['example.com']
|
allowed_domains = ["example.com"]
|
||||||
start_urls = [
|
start_urls = [
|
||||||
'http://www.example.com/1.html',
|
"http://www.example.com/1.html",
|
||||||
'http://www.example.com/2.html',
|
"http://www.example.com/2.html",
|
||||||
'http://www.example.com/3.html',
|
"http://www.example.com/3.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
self.logger.info('A response from %s just arrived!', response.url)
|
self.logger.info("A response from %s just arrived!", response.url)
|
||||||
|
|
||||||
Return multiple Requests and items from a single callback::
|
Return multiple Requests and items from a single callback:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'example.com'
|
name = "example.com"
|
||||||
allowed_domains = ['example.com']
|
allowed_domains = ["example.com"]
|
||||||
start_urls = [
|
start_urls = [
|
||||||
'http://www.example.com/1.html',
|
"http://www.example.com/1.html",
|
||||||
'http://www.example.com/2.html',
|
"http://www.example.com/2.html",
|
||||||
'http://www.example.com/3.html',
|
"http://www.example.com/3.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for h3 in response.xpath('//h3').getall():
|
for h3 in response.xpath("//h3").getall():
|
||||||
yield {"title": h3}
|
yield {"title": h3}
|
||||||
|
|
||||||
for href in response.xpath('//a/@href').getall():
|
for href in response.xpath("//a/@href").getall():
|
||||||
yield scrapy.Request(response.urljoin(href), self.parse)
|
yield scrapy.Request(response.urljoin(href), self.parse)
|
||||||
|
|
||||||
Instead of :attr:`~.start_urls` you can use :meth:`~.start_requests` directly;
|
Instead of :attr:`~.start_urls` you can use :meth:`~.start_requests` directly;
|
||||||
to give data more structure you can use :class:`~scrapy.Item` objects::
|
to give data more structure you can use :class:`~scrapy.Item` objects:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from myproject.items import MyItem
|
from myproject.items import MyItem
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'example.com'
|
name = "example.com"
|
||||||
allowed_domains = ['example.com']
|
allowed_domains = ["example.com"]
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
yield scrapy.Request('http://www.example.com/1.html', self.parse)
|
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/2.html", self.parse)
|
||||||
yield scrapy.Request('http://www.example.com/3.html', self.parse)
|
yield scrapy.Request("http://www.example.com/3.html", self.parse)
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
for h3 in response.xpath('//h3').getall():
|
for h3 in response.xpath("//h3").getall():
|
||||||
yield MyItem(title=h3)
|
yield MyItem(title=h3)
|
||||||
|
|
||||||
for href in response.xpath('//a/@href').getall():
|
for href in response.xpath("//a/@href").getall():
|
||||||
yield scrapy.Request(response.urljoin(href), self.parse)
|
yield scrapy.Request(response.urljoin(href), self.parse)
|
||||||
|
|
||||||
.. _spiderargs:
|
.. _spiderargs:
|
||||||
|
|
@ -274,34 +288,42 @@ Spider arguments are passed through the :command:`crawl` command using the
|
||||||
|
|
||||||
scrapy crawl myspider -a category=electronics
|
scrapy crawl myspider -a category=electronics
|
||||||
|
|
||||||
Spiders can access arguments in their `__init__` methods::
|
Spiders can access arguments in their `__init__` methods:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
|
|
||||||
def __init__(self, category=None, *args, **kwargs):
|
def __init__(self, category=None, *args, **kwargs):
|
||||||
super(MySpider, self).__init__(*args, **kwargs)
|
super(MySpider, self).__init__(*args, **kwargs)
|
||||||
self.start_urls = [f'http://www.example.com/categories/{category}']
|
self.start_urls = [f"http://www.example.com/categories/{category}"]
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
The default `__init__` method will take any spider arguments
|
The default `__init__` method will take any spider arguments
|
||||||
and copy them to the spider as attributes.
|
and copy them to the spider as attributes.
|
||||||
The above example can also be written as follows::
|
The above example can also be written as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class MySpider(scrapy.Spider):
|
class MySpider(scrapy.Spider):
|
||||||
name = 'myspider'
|
name = "myspider"
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
yield scrapy.Request(f'http://www.example.com/categories/{self.category}')
|
yield scrapy.Request(f"http://www.example.com/categories/{self.category}")
|
||||||
|
|
||||||
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
|
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
|
||||||
specify spider arguments when calling
|
specify spider arguments when calling
|
||||||
:class:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
|
:class:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
|
||||||
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`::
|
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
process = CrawlerProcess()
|
process = CrawlerProcess()
|
||||||
process.crawl(MySpider, category="electronics")
|
process.crawl(MySpider, category="electronics")
|
||||||
|
|
@ -337,10 +359,13 @@ common scraping cases, like following all links on a site based on certain
|
||||||
rules, crawling from `Sitemaps`_, or parsing an XML/CSV feed.
|
rules, crawling from `Sitemaps`_, or parsing an XML/CSV feed.
|
||||||
|
|
||||||
For the examples used in the following spiders, we'll assume you have a project
|
For the examples used in the following spiders, we'll assume you have a project
|
||||||
with a ``TestItem`` declared in a ``myproject.items`` module::
|
with a ``TestItem`` declared in a ``myproject.items`` module:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
|
|
||||||
|
|
||||||
class TestItem(scrapy.Item):
|
class TestItem(scrapy.Item):
|
||||||
id = scrapy.Field()
|
id = scrapy.Field()
|
||||||
name = scrapy.Field()
|
name = scrapy.Field()
|
||||||
|
|
@ -436,38 +461,46 @@ Crawling rules
|
||||||
CrawlSpider example
|
CrawlSpider example
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Let's now take a look at an example CrawlSpider with rules::
|
Let's now take a look at an example CrawlSpider with rules:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
from scrapy.spiders import CrawlSpider, Rule
|
from scrapy.spiders import CrawlSpider, Rule
|
||||||
from scrapy.linkextractors import LinkExtractor
|
from scrapy.linkextractors import LinkExtractor
|
||||||
|
|
||||||
|
|
||||||
class MySpider(CrawlSpider):
|
class MySpider(CrawlSpider):
|
||||||
name = 'example.com'
|
name = "example.com"
|
||||||
allowed_domains = ['example.com']
|
allowed_domains = ["example.com"]
|
||||||
start_urls = ['http://www.example.com']
|
start_urls = ["http://www.example.com"]
|
||||||
|
|
||||||
rules = (
|
rules = (
|
||||||
# Extract links matching 'category.php' (but not matching 'subsection.php')
|
# Extract links matching 'category.php' (but not matching 'subsection.php')
|
||||||
# and follow links from them (since no callback means follow=True by default).
|
# and follow links from them (since no callback means follow=True by default).
|
||||||
Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),
|
Rule(LinkExtractor(allow=("category\.php",), deny=("subsection\.php",))),
|
||||||
|
|
||||||
# Extract links matching 'item.php' and parse them with the spider's method parse_item
|
# Extract links matching 'item.php' and parse them with the spider's method parse_item
|
||||||
Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'),
|
Rule(LinkExtractor(allow=("item\.php",)), callback="parse_item"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_item(self, response):
|
def parse_item(self, response):
|
||||||
self.logger.info('Hi, this is an item page! %s', response.url)
|
self.logger.info("Hi, this is an item page! %s", response.url)
|
||||||
item = scrapy.Item()
|
item = scrapy.Item()
|
||||||
item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
|
item["id"] = response.xpath('//td[@id="item_id"]/text()').re(r"ID: (\d+)")
|
||||||
item['name'] = response.xpath('//td[@id="item_name"]/text()').get()
|
item["name"] = response.xpath('//td[@id="item_name"]/text()').get()
|
||||||
item['description'] = response.xpath('//td[@id="item_description"]/text()').get()
|
item["description"] = response.xpath(
|
||||||
item['link_text'] = response.meta['link_text']
|
'//td[@id="item_description"]/text()'
|
||||||
|
).get()
|
||||||
|
item["link_text"] = response.meta["link_text"]
|
||||||
url = response.xpath('//td[@id="additional_data"]/@href').get()
|
url = response.xpath('//td[@id="additional_data"]/@href').get()
|
||||||
return response.follow(url, self.parse_additional_page, cb_kwargs=dict(item=item))
|
return response.follow(
|
||||||
|
url, self.parse_additional_page, cb_kwargs=dict(item=item)
|
||||||
|
)
|
||||||
|
|
||||||
def parse_additional_page(self, response, item):
|
def parse_additional_page(self, response, item):
|
||||||
item['additional_data'] = response.xpath('//p[@id="additional_data"]/text()').get()
|
item["additional_data"] = response.xpath(
|
||||||
|
'//p[@id="additional_data"]/text()'
|
||||||
|
).get()
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -568,25 +601,30 @@ XMLFeedSpider
|
||||||
XMLFeedSpider example
|
XMLFeedSpider example
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
These spiders are pretty easy to use, let's have a look at one example::
|
These spiders are pretty easy to use, let's have a look at one example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spiders import XMLFeedSpider
|
from scrapy.spiders import XMLFeedSpider
|
||||||
from myproject.items import TestItem
|
from myproject.items import TestItem
|
||||||
|
|
||||||
|
|
||||||
class MySpider(XMLFeedSpider):
|
class MySpider(XMLFeedSpider):
|
||||||
name = 'example.com'
|
name = "example.com"
|
||||||
allowed_domains = ['example.com']
|
allowed_domains = ["example.com"]
|
||||||
start_urls = ['http://www.example.com/feed.xml']
|
start_urls = ["http://www.example.com/feed.xml"]
|
||||||
iterator = 'iternodes' # This is actually unnecessary, since it's the default value
|
iterator = "iternodes" # This is actually unnecessary, since it's the default value
|
||||||
itertag = 'item'
|
itertag = "item"
|
||||||
|
|
||||||
def parse_node(self, response, node):
|
def parse_node(self, response, node):
|
||||||
self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.getall()))
|
self.logger.info(
|
||||||
|
"Hi, this is a <%s> node!: %s", self.itertag, "".join(node.getall())
|
||||||
|
)
|
||||||
|
|
||||||
item = TestItem()
|
item = TestItem()
|
||||||
item['id'] = node.xpath('@id').get()
|
item["id"] = node.xpath("@id").get()
|
||||||
item['name'] = node.xpath('name').get()
|
item["name"] = node.xpath("name").get()
|
||||||
item['description'] = node.xpath('description').get()
|
item["description"] = node.xpath("description").get()
|
||||||
return item
|
return item
|
||||||
|
|
||||||
Basically what we did up there was to create a spider that downloads a feed from
|
Basically what we did up there was to create a spider that downloads a feed from
|
||||||
|
|
@ -627,26 +665,29 @@ CSVFeedSpider example
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Let's see an example similar to the previous one, but using a
|
Let's see an example similar to the previous one, but using a
|
||||||
:class:`CSVFeedSpider`::
|
:class:`CSVFeedSpider`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spiders import CSVFeedSpider
|
from scrapy.spiders import CSVFeedSpider
|
||||||
from myproject.items import TestItem
|
from myproject.items import TestItem
|
||||||
|
|
||||||
|
|
||||||
class MySpider(CSVFeedSpider):
|
class MySpider(CSVFeedSpider):
|
||||||
name = 'example.com'
|
name = "example.com"
|
||||||
allowed_domains = ['example.com']
|
allowed_domains = ["example.com"]
|
||||||
start_urls = ['http://www.example.com/feed.csv']
|
start_urls = ["http://www.example.com/feed.csv"]
|
||||||
delimiter = ';'
|
delimiter = ";"
|
||||||
quotechar = "'"
|
quotechar = "'"
|
||||||
headers = ['id', 'name', 'description']
|
headers = ["id", "name", "description"]
|
||||||
|
|
||||||
def parse_row(self, response, row):
|
def parse_row(self, response, row):
|
||||||
self.logger.info('Hi, this is a row!: %r', row)
|
self.logger.info("Hi, this is a row!: %r", row)
|
||||||
|
|
||||||
item = TestItem()
|
item = TestItem()
|
||||||
item['id'] = row['id']
|
item["id"] = row["id"]
|
||||||
item['name'] = row['name']
|
item["name"] = row["name"]
|
||||||
item['description'] = row['description']
|
item["description"] = row["description"]
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -728,19 +769,22 @@ SitemapSpider
|
||||||
<lastmod>2005-01-01</lastmod>
|
<lastmod>2005-01-01</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
We can define a ``sitemap_filter`` function to filter ``entries`` by date::
|
We can define a ``sitemap_filter`` function to filter ``entries`` by date:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from scrapy.spiders import SitemapSpider
|
from scrapy.spiders import SitemapSpider
|
||||||
|
|
||||||
|
|
||||||
class FilteredSitemapSpider(SitemapSpider):
|
class FilteredSitemapSpider(SitemapSpider):
|
||||||
name = 'filtered_sitemap_spider'
|
name = "filtered_sitemap_spider"
|
||||||
allowed_domains = ['example.com']
|
allowed_domains = ["example.com"]
|
||||||
sitemap_urls = ['http://example.com/sitemap.xml']
|
sitemap_urls = ["http://example.com/sitemap.xml"]
|
||||||
|
|
||||||
def sitemap_filter(self, entries):
|
def sitemap_filter(self, entries):
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
date_time = datetime.strptime(entry['lastmod'], '%Y-%m-%d')
|
date_time = datetime.strptime(entry["lastmod"], "%Y-%m-%d")
|
||||||
if date_time.year >= 2005:
|
if date_time.year >= 2005:
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
|
|
@ -765,60 +809,72 @@ SitemapSpider examples
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Simplest example: process all urls discovered through sitemaps using the
|
Simplest example: process all urls discovered through sitemaps using the
|
||||||
``parse`` callback::
|
``parse`` callback:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spiders import SitemapSpider
|
from scrapy.spiders import SitemapSpider
|
||||||
|
|
||||||
|
|
||||||
class MySpider(SitemapSpider):
|
class MySpider(SitemapSpider):
|
||||||
sitemap_urls = ['http://www.example.com/sitemap.xml']
|
sitemap_urls = ["http://www.example.com/sitemap.xml"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
pass # ... scrape item here ...
|
pass # ... scrape item here ...
|
||||||
|
|
||||||
Process some urls with certain callback and other urls with a different
|
Process some urls with certain callback and other urls with a different
|
||||||
callback::
|
callback:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spiders import SitemapSpider
|
from scrapy.spiders import SitemapSpider
|
||||||
|
|
||||||
|
|
||||||
class MySpider(SitemapSpider):
|
class MySpider(SitemapSpider):
|
||||||
sitemap_urls = ['http://www.example.com/sitemap.xml']
|
sitemap_urls = ["http://www.example.com/sitemap.xml"]
|
||||||
sitemap_rules = [
|
sitemap_rules = [
|
||||||
('/product/', 'parse_product'),
|
("/product/", "parse_product"),
|
||||||
('/category/', 'parse_category'),
|
("/category/", "parse_category"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse_product(self, response):
|
def parse_product(self, response):
|
||||||
pass # ... scrape product ...
|
pass # ... scrape product ...
|
||||||
|
|
||||||
def parse_category(self, response):
|
def parse_category(self, response):
|
||||||
pass # ... scrape category ...
|
pass # ... scrape category ...
|
||||||
|
|
||||||
Follow sitemaps defined in the `robots.txt`_ file and only follow sitemaps
|
Follow sitemaps defined in the `robots.txt`_ file and only follow sitemaps
|
||||||
whose url contains ``/sitemap_shop``::
|
whose url contains ``/sitemap_shop``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spiders import SitemapSpider
|
from scrapy.spiders import SitemapSpider
|
||||||
|
|
||||||
|
|
||||||
class MySpider(SitemapSpider):
|
class MySpider(SitemapSpider):
|
||||||
sitemap_urls = ['http://www.example.com/robots.txt']
|
sitemap_urls = ["http://www.example.com/robots.txt"]
|
||||||
sitemap_rules = [
|
sitemap_rules = [
|
||||||
('/shop/', 'parse_shop'),
|
("/shop/", "parse_shop"),
|
||||||
]
|
]
|
||||||
sitemap_follow = ['/sitemap_shops']
|
sitemap_follow = ["/sitemap_shops"]
|
||||||
|
|
||||||
def parse_shop(self, response):
|
def parse_shop(self, response):
|
||||||
pass # ... scrape shop here ...
|
pass # ... scrape shop here ...
|
||||||
|
|
||||||
Combine SitemapSpider with other sources of urls::
|
Combine SitemapSpider with other sources of urls:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spiders import SitemapSpider
|
from scrapy.spiders import SitemapSpider
|
||||||
|
|
||||||
|
|
||||||
class MySpider(SitemapSpider):
|
class MySpider(SitemapSpider):
|
||||||
sitemap_urls = ['http://www.example.com/robots.txt']
|
sitemap_urls = ["http://www.example.com/robots.txt"]
|
||||||
sitemap_rules = [
|
sitemap_rules = [
|
||||||
('/shop/', 'parse_shop'),
|
("/shop/", "parse_shop"),
|
||||||
]
|
]
|
||||||
|
|
||||||
other_urls = ['http://www.example.com/about']
|
other_urls = ["http://www.example.com/about"]
|
||||||
|
|
||||||
def start_requests(self):
|
def start_requests(self):
|
||||||
requests = list(super(MySpider, self).start_requests())
|
requests = list(super(MySpider, self).start_requests())
|
||||||
|
|
@ -826,10 +882,10 @@ Combine SitemapSpider with other sources of urls::
|
||||||
return requests
|
return requests
|
||||||
|
|
||||||
def parse_shop(self, response):
|
def parse_shop(self, response):
|
||||||
pass # ... scrape shop here ...
|
pass # ... scrape shop here ...
|
||||||
|
|
||||||
def parse_other(self, response):
|
def parse_other(self, response):
|
||||||
pass # ... scrape other here ...
|
pass # ... scrape other here ...
|
||||||
|
|
||||||
.. _Sitemaps: https://www.sitemaps.org/index.html
|
.. _Sitemaps: https://www.sitemaps.org/index.html
|
||||||
.. _Sitemap index files: https://www.sitemaps.org/protocol.html#index
|
.. _Sitemap index files: https://www.sitemaps.org/protocol.html#index
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,11 @@ Common Stats Collector uses
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
Access the stats collector through the :attr:`~scrapy.crawler.Crawler.stats`
|
Access the stats collector through the :attr:`~scrapy.crawler.Crawler.stats`
|
||||||
attribute. Here is an example of an extension that access stats::
|
attribute. Here is an example of an extension that access stats:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class ExtensionThatAccessStats:
|
class ExtensionThatAccessStats:
|
||||||
|
|
||||||
def __init__(self, stats):
|
def __init__(self, stats):
|
||||||
self.stats = stats
|
self.stats = stats
|
||||||
|
|
||||||
|
|
@ -41,21 +42,29 @@ attribute. Here is an example of an extension that access stats::
|
||||||
def from_crawler(cls, crawler):
|
def from_crawler(cls, crawler):
|
||||||
return cls(crawler.stats)
|
return cls(crawler.stats)
|
||||||
|
|
||||||
Set stat value::
|
Set stat value:
|
||||||
|
|
||||||
stats.set_value('hostname', socket.gethostname())
|
.. code-block:: python
|
||||||
|
|
||||||
Increment stat value::
|
stats.set_value("hostname", socket.gethostname())
|
||||||
|
|
||||||
stats.inc_value('custom_count')
|
Increment stat value:
|
||||||
|
|
||||||
Set stat value only if greater than previous::
|
.. code-block:: python
|
||||||
|
|
||||||
stats.max_value('max_items_scraped', value)
|
stats.inc_value("custom_count")
|
||||||
|
|
||||||
Set stat value only if lower than previous::
|
Set stat value only if greater than previous:
|
||||||
|
|
||||||
stats.min_value('min_free_memory_percent', value)
|
.. code-block:: python
|
||||||
|
|
||||||
|
stats.max_value("max_items_scraped", value)
|
||||||
|
|
||||||
|
Set stat value only if lower than previous:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
stats.min_value("min_free_memory_percent", value)
|
||||||
|
|
||||||
Get stat value:
|
Get stat value:
|
||||||
|
|
||||||
|
|
|
||||||
549
sep/sep-001.rst
549
sep/sep-001.rst
|
|
@ -1,274 +1,275 @@
|
||||||
======= ============================================
|
======= ============================================
|
||||||
SEP 1
|
SEP 1
|
||||||
Title API for populating item fields (comparison)
|
Title API for populating item fields (comparison)
|
||||||
Author Ismael Carnales, Pablo Hoffman, Daniel Grana
|
Author Ismael Carnales, Pablo Hoffman, Daniel Grana
|
||||||
Created 2009-07-19
|
Created 2009-07-19
|
||||||
Status Obsoleted by :ref:`sep-008`
|
Status Obsoleted by :ref:`sep-008`
|
||||||
======= ============================================
|
======= ============================================
|
||||||
|
|
||||||
=====================================================
|
=====================================================
|
||||||
SEP-001 - API for populating item fields (comparison)
|
SEP-001 - API for populating item fields (comparison)
|
||||||
=====================================================
|
=====================================================
|
||||||
|
|
||||||
This page shows different usage scenarios for the two new proposed API for
|
This page shows different usage scenarios for the two new proposed API for
|
||||||
populating item field values (which will replace the old deprecated !RobustItem
|
populating item field values (which will replace the old deprecated !RobustItem
|
||||||
API) and compares them. One of these will be chosen as the recommended (and
|
API) and compares them. One of these will be chosen as the recommended (and
|
||||||
supported) mechanism in Scrapy 0.7.
|
supported) mechanism in Scrapy 0.7.
|
||||||
|
|
||||||
Candidates and their API
|
Candidates and their API
|
||||||
========================
|
========================
|
||||||
|
|
||||||
RobustItem (old, deprecated)
|
RobustItem (old, deprecated)
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
- ``attribute(field_name, selector_or_value, **modifiers_and_adaptor_args)``
|
- ``attribute(field_name, selector_or_value, **modifiers_and_adaptor_args)``
|
||||||
|
|
||||||
.. note:: ``attribute()`` modifiers (like ``add=True``) are passed together
|
.. note:: ``attribute()`` modifiers (like ``add=True``) are passed together
|
||||||
with adaptor args as keyword arguments (this is ugly)
|
with adaptor args as keyword arguments (this is ugly)
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
--------
|
--------
|
||||||
|
|
||||||
- ``__init__(response, item=None, **adaptor_args)``
|
- ``__init__(response, item=None, **adaptor_args)``
|
||||||
- instantiate an ``ItemForm`` with a item instance with predefined adaptor arguments
|
- instantiate an ``ItemForm`` with a item instance with predefined adaptor arguments
|
||||||
- ``__setitem__(field_name, selector_or_value)``
|
- ``__setitem__(field_name, selector_or_value)``
|
||||||
- set field value
|
- set field value
|
||||||
- ``__getitem__(field_name)``
|
- ``__getitem__(field_name)``
|
||||||
- return the "computed" value of a field (the one that would be set to the item).
|
- return the "computed" value of a field (the one that would be set to the item).
|
||||||
returns ``None`` if not set.
|
returns ``None`` if not set.
|
||||||
- ``get_item()``
|
- ``get_item()``
|
||||||
- return the item populated with the data provided so far
|
- return the item populated with the data provided so far
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
- ``__init__(response, item=None, **adaptor_args)``
|
- ``__init__(response, item=None, **adaptor_args)``
|
||||||
- instantiate an ``ItemBuilder`` with predefined adaptor arguments
|
- instantiate an ``ItemBuilder`` with predefined adaptor arguments
|
||||||
- ``add_value(field_name, selector_or_value, **adaptor_args)``
|
- ``add_value(field_name, selector_or_value, **adaptor_args)``
|
||||||
- add value to field
|
- add value to field
|
||||||
- ``replace_value(field_name, selector_or_value, **adaptor_args)``
|
- ``replace_value(field_name, selector_or_value, **adaptor_args)``
|
||||||
- replace existing field value
|
- replace existing field value
|
||||||
- ``get_value(field_name)``
|
- ``get_value(field_name)``
|
||||||
- return the "computed" value of a field (the one that would be set to the
|
- return the "computed" value of a field (the one that would be set to the
|
||||||
item). returns ``None`` if not set.
|
item). returns ``None`` if not set.
|
||||||
- ``get_item()``
|
- ``get_item()``
|
||||||
- return the item populated with the data provided so far
|
- return the item populated with the data provided so far
|
||||||
|
|
||||||
Pros and cons of each candidate
|
Pros and cons of each candidate
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Pros:
|
Pros:
|
||||||
- same API used for Items (see https://docs.scrapy.org/en/latest/topics/items.html)
|
- same API used for Items (see https://docs.scrapy.org/en/latest/topics/items.html)
|
||||||
- some people consider setitem API more elegant than methods API
|
- some people consider setitem API more elegant than methods API
|
||||||
|
|
||||||
Cons:
|
Cons:
|
||||||
- doesn't allow passing run-time arguments to adaptors on assign, you have to
|
- doesn't allow passing run-time arguments to adaptors on assign, you have to
|
||||||
override the adaptors for your spider if you need specific parameters, which
|
override the adaptors for your spider if you need specific parameters, which
|
||||||
can be an overhead. Example:
|
can be an overhead. Example:
|
||||||
|
|
||||||
Neutral:
|
Neutral:
|
||||||
- solves the add=True problem using standard ``__add__`` and ``list.append()`` method
|
- solves the add=True problem using standard ``__add__`` and ``list.append()`` method
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
Pros:
|
Pros:
|
||||||
- allows passing run-time arguments to adaptors on assigned
|
- allows passing run-time arguments to adaptors on assigned
|
||||||
|
|
||||||
Cons:
|
Cons:
|
||||||
- some people consider setitem API more elegant than methods API
|
- some people consider setitem API more elegant than methods API
|
||||||
|
|
||||||
Neutral:
|
Neutral:
|
||||||
- solves the "add=True" problem by implementing different methods per action
|
- solves the "add=True" problem by implementing different methods per action
|
||||||
(replacing or adding)
|
(replacing or adding)
|
||||||
|
|
||||||
Usage Scenarios for each candidate
|
Usage Scenarios for each candidate
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
Defining adaptors
|
Defining adaptors
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class NewsForm(ItemForm):
|
class NewsForm(ItemForm):
|
||||||
item_class = NewsItem
|
item_class = NewsItem
|
||||||
|
|
||||||
url = adaptor(extract, remove_tags(), unquote(), strip)
|
url = adaptor(extract, remove_tags(), unquote(), strip)
|
||||||
headline = adaptor(extract, remove_tags(), unquote(), strip)
|
headline = adaptor(extract, remove_tags(), unquote(), strip)
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class NewsBuilder(ItemBuilder):
|
class NewsBuilder(ItemBuilder):
|
||||||
item_class = NewsItem
|
item_class = NewsItem
|
||||||
|
|
||||||
url = adaptor(extract, remove_tags(), unquote(), strip)
|
url = adaptor(extract, remove_tags(), unquote(), strip)
|
||||||
headline = adaptor(extract, remove_tags(), unquote(), strip)
|
headline = adaptor(extract, remove_tags(), unquote(), strip)
|
||||||
|
|
||||||
Creating an Item
|
Creating an Item
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
ia = NewsForm(response)
|
ia = NewsForm(response)
|
||||||
ia['url'] = response.url
|
ia["url"] = response.url
|
||||||
ia['headline'] = x.x('//h1[@class="headline"]')
|
ia["headline"] = x.x('//h1[@class="headline"]')
|
||||||
|
|
||||||
# if we want to add another value to the same field
|
# if we want to add another value to the same field
|
||||||
ia['headline'] += x.x('//h1[@class="headline2"]')
|
ia["headline"] += x.x('//h1[@class="headline2"]')
|
||||||
|
|
||||||
# if we want to replace the field value other value to the same field
|
# if we want to replace the field value other value to the same field
|
||||||
ia['headline'] = x.x('//h1[@class="headline3"]')
|
ia["headline"] = x.x('//h1[@class="headline3"]')
|
||||||
|
|
||||||
return ia.get_item()
|
return ia.get_item()
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
il = NewsBuilder(response)
|
il = NewsBuilder(response)
|
||||||
il.add_value('url', response.url)
|
il.add_value("url", response.url)
|
||||||
il.add_value('headline', x.x('//h1[@class="headline"]'))
|
il.add_value("headline", x.x('//h1[@class="headline"]'))
|
||||||
|
|
||||||
# if we want to add another value to the same field
|
# if we want to add another value to the same field
|
||||||
il.add_value('headline', x.x('//h1[@class="headline2"]'))
|
il.add_value("headline", x.x('//h1[@class="headline2"]'))
|
||||||
|
|
||||||
# if we want to replace the field value other value to the same field
|
# if we want to replace the field value other value to the same field
|
||||||
il.replace_value('headline', x.x('//h1[@class="headline3"]'))
|
il.replace_value("headline", x.x('//h1[@class="headline3"]'))
|
||||||
|
|
||||||
return il.get_item()
|
return il.get_item()
|
||||||
|
|
||||||
Using different adaptors per Spider/Site
|
Using different adaptors per Spider/Site
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SiteNewsFrom(NewsForm):
|
class SiteNewsFrom(NewsForm):
|
||||||
published = adaptor(HtmlNewsForm.published, to_date('%d.%m.%Y'))
|
published = adaptor(HtmlNewsForm.published, to_date("%d.%m.%Y"))
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SiteNewsBuilder(NewsBuilder):
|
class SiteNewsBuilder(NewsBuilder):
|
||||||
published = adaptor(HtmlNewsBuilder.published, to_date('%d.%m.%Y'))
|
published = adaptor(HtmlNewsBuilder.published, to_date("%d.%m.%Y"))
|
||||||
|
|
||||||
Check the value of an item being-extracted
|
Check the value of an item being-extracted
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
ia = NewsForm(response)
|
ia = NewsForm(response)
|
||||||
ia['headline'] = x.x('//h1[@class="headline"]')
|
ia["headline"] = x.x('//h1[@class="headline"]')
|
||||||
if not ia['headline']:
|
if not ia["headline"]:
|
||||||
ia['headline'] = x.x('//h1[@class="title"]')
|
ia["headline"] = x.x('//h1[@class="title"]')
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
il = NewsBuilder(response)
|
il = NewsBuilder(response)
|
||||||
il.add_value('headline', x.x('//h1[@class="headline"]'))
|
il.add_value("headline", x.x('//h1[@class="headline"]'))
|
||||||
if not nf.get_value('headline'):
|
if not nf.get_value("headline"):
|
||||||
il.add_value('headline', x.x('//h1[@class="title"]'))
|
il.add_value("headline", x.x('//h1[@class="title"]'))
|
||||||
|
|
||||||
Adding a value to a list attribute/field
|
Adding a value to a list attribute/field
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
ia['headline'] += x.x('//h1[@class="headline"]')
|
ia["headline"] += x.x('//h1[@class="headline"]')
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
il.add_value('headline', x.x('//h1[@class="headline"]'))
|
il.add_value("headline", x.x('//h1[@class="headline"]'))
|
||||||
|
|
||||||
Passing run-time arguments to adaptors
|
Passing run-time arguments to adaptors
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
# Only approach is passing arguments when instantiating the form
|
# Only approach is passing arguments when instantiating the form
|
||||||
ia = NewsForm(response, default_unit='cm')
|
ia = NewsForm(response, default_unit="cm")
|
||||||
ia['width'] = x.x('//p[@class="width"]')
|
ia["width"] = x.x('//p[@class="width"]')
|
||||||
|
|
||||||
ItemBuilder
|
ItemBuilder
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
il.add_value('width', x.x('//p[@class="width"]'), default_unit='cm')
|
il.add_value("width", x.x('//p[@class="width"]'), default_unit="cm")
|
||||||
|
|
||||||
# an alternative approach (more efficient)
|
# an alternative approach (more efficient)
|
||||||
il = NewsBuilder(response, default_unit='cm')
|
il = NewsBuilder(response, default_unit="cm")
|
||||||
il.add_value('width', x.x('//p[@class="width"]'))
|
il.add_value("width", x.x('//p[@class="width"]'))
|
||||||
|
|
||||||
Passing run-time arguments to adaptors (same argument name)
|
Passing run-time arguments to adaptors (same argument name)
|
||||||
-----------------------------------------------------------
|
-----------------------------------------------------------
|
||||||
|
|
||||||
ItemForm
|
ItemForm
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class MySiteForm(ItemForm):
|
class MySiteForm(ItemForm):
|
||||||
width = adaptor(ItemForm.width, default_unit='cm')
|
width = adaptor(ItemForm.width, default_unit="cm")
|
||||||
volume = adaptor(ItemForm.width, default_unit='lt')
|
volume = adaptor(ItemForm.width, default_unit="lt")
|
||||||
|
|
||||||
ia['width'] = x.x('//p[@class="width"]')
|
|
||||||
ia['volume'] = x.x('//p[@class="volume"]')
|
ia["width"] = x.x('//p[@class="width"]')
|
||||||
|
ia["volume"] = x.x('//p[@class="volume"]')
|
||||||
# another example passing parameters on instance
|
|
||||||
ia = NewsForm(response, encoding='utf-8')
|
# another example passing parameters on instance
|
||||||
ia['name'] = x.x('//p[@class="name"]')
|
ia = NewsForm(response, encoding="utf-8")
|
||||||
|
ia["name"] = x.x('//p[@class="name"]')
|
||||||
ItemBuilder
|
|
||||||
~~~~~~~~~~~
|
ItemBuilder
|
||||||
|
~~~~~~~~~~~
|
||||||
::
|
|
||||||
|
.. code-block:: python
|
||||||
#!python
|
|
||||||
il.add_value('width', x.x('//p[@class="width"]'), default_unit='cm')
|
#!python
|
||||||
il.add_value('volume', x.x('//p[@class="volume"]'), default_unit='lt')
|
il.add_value("width", x.x('//p[@class="width"]'), default_unit="cm")
|
||||||
|
il.add_value("volume", x.x('//p[@class="volume"]'), default_unit="lt")
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,19 @@ called !ListField.
|
||||||
Proposed Implementation
|
Proposed Implementation
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.item.fields import BaseField
|
from scrapy.item.fields import BaseField
|
||||||
|
|
||||||
|
|
||||||
class ListField(BaseField):
|
class ListField(BaseField):
|
||||||
def __init__(self, field, default=None):
|
def __init__(self, field, default=None):
|
||||||
self._field = field
|
self._field = field
|
||||||
super(ListField, self).__init__(default)
|
super(ListField, self).__init__(default)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if hasattr(value, '__iter__'): # str/unicode not allowed
|
if hasattr(value, "__iter__"): # str/unicode not allowed
|
||||||
return [self._field.to_python(v) for v in value]
|
return [self._field.to_python(v) for v in value]
|
||||||
else:
|
else:
|
||||||
raise TypeError("Expected iterable, got %s" % type(value).__name__)
|
raise TypeError("Expected iterable, got %s" % type(value).__name__)
|
||||||
|
|
@ -42,12 +43,13 @@ Usage Scenarios
|
||||||
Defining a list field
|
Defining a list field
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.item.models import Item
|
from scrapy.item.models import Item
|
||||||
from scrapy.item.fields import ListField, TextField, DateField, IntegerField
|
from scrapy.item.fields import ListField, TextField, DateField, IntegerField
|
||||||
|
|
||||||
|
|
||||||
class Article(Item):
|
class Article(Item):
|
||||||
categories = ListField(TextField)
|
categories = ListField(TextField)
|
||||||
dates = ListField(DateField, default=[])
|
dates = ListField(DateField, default=[])
|
||||||
|
|
@ -56,57 +58,59 @@ Defining a list field
|
||||||
Another case of products and variants which highlights the fact that it's
|
Another case of products and variants which highlights the fact that it's
|
||||||
important to instantiate !ListField with field instances, not classes:
|
important to instantiate !ListField with field instances, not classes:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.item.models import Item
|
from scrapy.item.models import Item
|
||||||
from scrapy.item.fields import ListField, TextField
|
from scrapy.item.fields import ListField, TextField
|
||||||
|
|
||||||
|
|
||||||
class Variant(Item):
|
class Variant(Item):
|
||||||
name = TextField()
|
name = TextField()
|
||||||
|
|
||||||
|
|
||||||
class Product(Variant):
|
class Product(Variant):
|
||||||
variants = ListField(ItemField(Variant))
|
variants = ListField(ItemField(Variant))
|
||||||
|
|
||||||
Assigning a list field
|
Assigning a list field
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
i = Article()
|
i = Article()
|
||||||
|
|
||||||
i['categories'] = []
|
i["categories"] = []
|
||||||
i['categories'] = ['politics', 'sport']
|
i["categories"] = ["politics", "sport"]
|
||||||
i['categories'] = ['test', 1] -> raises TypeError
|
i["categories"] = ["test", 1] # -> raises TypeError
|
||||||
i['categories'] = asd -> raises TypeError
|
i["categories"] = asd # -> raises TypeError
|
||||||
|
|
||||||
i['dates'] = []
|
i["dates"] = []
|
||||||
i['dates'] = ['2009-01-01'] # raises TypeError? (depends on TextField)
|
i["dates"] = ["2009-01-01"] # raises TypeError? (depends on TextField)
|
||||||
|
|
||||||
i['numbers'] = ['1', 2, '3']
|
i["numbers"] = ["1", 2, "3"]
|
||||||
i['numbers'] # returns [1, 2, 3]
|
i["numbers"] # returns [1, 2, 3]
|
||||||
|
|
||||||
Default values
|
Default values
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
i = Article()
|
i = Article()
|
||||||
|
|
||||||
i['categories'] # raises KeyError
|
i["categories"] # raises KeyError
|
||||||
i.get('categories') # returns None
|
i.get("categories") # returns None
|
||||||
|
|
||||||
i['numbers'] # returns []
|
i["numbers"] # returns []
|
||||||
|
|
||||||
Appending values
|
Appending values
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
i = Article()
|
i = Article()
|
||||||
|
|
||||||
i['categories'] = ['one', 'two']
|
i["categories"] = ["one", "two"]
|
||||||
i['categories'].append(3) # XXX: should this fail?
|
i["categories"].append(3) # XXX: should this fail?
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,21 @@ This API proposal relies on the following API:
|
||||||
Proposed Implementation of ItemField
|
Proposed Implementation of ItemField
|
||||||
====================================
|
====================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.item.fields import BaseField
|
from scrapy.item.fields import BaseField
|
||||||
|
|
||||||
|
|
||||||
class ItemField(BaseField):
|
class ItemField(BaseField):
|
||||||
def __init__(self, item_type, default=None):
|
def __init__(self, item_type, default=None):
|
||||||
self._item_type = item_type
|
self._item_type = item_type
|
||||||
super(ItemField, self).__init__(default)
|
super(ItemField, self).__init__(default)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
return self._item_type(value) if not isinstance(value, self._item_type) else value
|
return (
|
||||||
|
self._item_type(value) if not isinstance(value, self._item_type) else value
|
||||||
|
)
|
||||||
|
|
||||||
def get_default(self):
|
def get_default(self):
|
||||||
# WARNING: returns default item instead of a copy - this must be
|
# WARNING: returns default item instead of a copy - this must be
|
||||||
|
|
@ -54,25 +57,28 @@ Usage Scenarios
|
||||||
Defining an item containing ItemField's
|
Defining an item containing ItemField's
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.item.models import Item
|
from scrapy.item.models import Item
|
||||||
from scrapy.item.fields import ListField, ItemField, TextField, UrlField, DecimalField
|
from scrapy.item.fields import ListField, ItemField, TextField, UrlField, DecimalField
|
||||||
|
|
||||||
|
|
||||||
class Supplier(Item):
|
class Supplier(Item):
|
||||||
name = TextField(default="anonymous supplier")
|
name = TextField(default="anonymous supplier")
|
||||||
url = UrlField()
|
url = UrlField()
|
||||||
|
|
||||||
|
|
||||||
class Variant(Item):
|
class Variant(Item):
|
||||||
name = TextField(required=True)
|
name = TextField(required=True)
|
||||||
url = UrlField()
|
url = UrlField()
|
||||||
price = DecimalField()
|
price = DecimalField()
|
||||||
|
|
||||||
|
|
||||||
class Product(Variant):
|
class Product(Variant):
|
||||||
supplier = ItemField(Supplier, default=Supplier(name="default supplier")
|
supplier = ItemField(Supplier, default=Supplier(name="default supplier"))
|
||||||
variants = ListField(ItemField(Variant))
|
variants = ListField(ItemField(Variant))
|
||||||
|
|
||||||
# these ones are used for documenting default value examples
|
# these ones are used for documenting default value examples
|
||||||
supplier2 = ItemField(Supplier)
|
supplier2 = ItemField(Supplier)
|
||||||
variants2 = ListField(ItemField(Variant), default=[])
|
variants2 = ListField(ItemField(Variant), default=[])
|
||||||
|
|
@ -81,16 +87,16 @@ It's important to note here that the (perhaps most intuitive) way of defining a
|
||||||
Product-Variant relationship (i.e. defining a recursive !ItemField) doesn't
|
Product-Variant relationship (i.e. defining a recursive !ItemField) doesn't
|
||||||
work. For example, this fails to compile:
|
work. For example, this fails to compile:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class Product(Item):
|
class Product(Item):
|
||||||
variants = ItemField(Product) # Fails to compile
|
variants = ItemField(Product) # Fails to compile
|
||||||
|
|
||||||
Assigning an item field
|
Assigning an item field
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
supplier = Supplier(name="Supplier 1", url="http://example.com")
|
supplier = Supplier(name="Supplier 1", url="http://example.com")
|
||||||
|
|
@ -98,69 +104,69 @@ Assigning an item field
|
||||||
p = Product()
|
p = Product()
|
||||||
|
|
||||||
# standard assignment
|
# standard assignment
|
||||||
p['supplier'] = supplier
|
p["supplier"] = supplier
|
||||||
# this also works as it tries to instantiate a Supplier with the given dict
|
# this also works as it tries to instantiate a Supplier with the given dict
|
||||||
p['supplier'] = {'name': 'Supplier 1' url='http://example.com'}
|
p["supplier"] = {"name": "Supplier 1", url: "http://example.com"}
|
||||||
# this fails because it can't instantiate a Supplier
|
# this fails because it can't instantiate a Supplier
|
||||||
p['supplier'] = 'Supplier 1'
|
p["supplier"] = "Supplier 1"
|
||||||
# this fails because url doesn't have the valid type
|
# this fails because url doesn't have the valid type
|
||||||
p['supplier'] = {'name': 'Supplier 1' url=123}
|
p["supplier"] = {"name": "Supplier 1", url: 123}
|
||||||
|
|
||||||
v1 = Variant()
|
v1 = Variant()
|
||||||
v1['name'] = "lala"
|
v1["name"] = "lala"
|
||||||
v1['price'] = Decimal("100")
|
v1["price"] = Decimal("100")
|
||||||
|
|
||||||
v2 = Variant()
|
v2 = Variant()
|
||||||
v2['name'] = "lolo"
|
v2["name"] = "lolo"
|
||||||
v2['price'] = Decimal("150")
|
v2["price"] = Decimal("150")
|
||||||
|
|
||||||
# standard assignment
|
# standard assignment
|
||||||
p['variants'] = [v1, v2] # OK
|
p["variants"] = [v1, v2] # OK
|
||||||
# can also instantiate at assignment time
|
# can also instantiate at assignment time
|
||||||
p['variants'] = [v1, Variant(name="lolo", price=Decimal("150")]
|
p["variants"] = [v1, Variant(name="lolo", price=Decimal("150"))]
|
||||||
# this also works as it tries to instantiate a Variant with the given dict
|
# this also works as it tries to instantiate a Variant with the given dict
|
||||||
p['variants'] = [v1, {'name': 'lolo', 'price': Decimal("150")]
|
p["variants"] = [v1, {"name": "lolo", "price": Decimal("150")}]
|
||||||
# this fails because it can't instantiate a Variant
|
# this fails because it can't instantiate a Variant
|
||||||
p['variants'] = [v1, 'test']
|
p["variants"] = [v1, "test"]
|
||||||
# this fails because 'coco' is not a valid value for price
|
# this fails because 'coco' is not a valid value for price
|
||||||
p['variants'] = [v1, {'name': 'lolo', 'price': 'coco']
|
p["variants"] = [v1, {"name": "lolo", "price": "coco"}]
|
||||||
|
|
||||||
Default values
|
Default values
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
p = Product()
|
p = Product()
|
||||||
|
|
||||||
p['supplier'] # returns: Supplier(name='default supplier')
|
p["supplier"] # returns: Supplier(name='default supplier')
|
||||||
p['supplier2'] # raises KeyError
|
p["supplier2"] # raises KeyError
|
||||||
p['supplier2'] = Supplier()
|
p["supplier2"] = Supplier()
|
||||||
p['supplier2'] # returns: Supplier(name='anonymous supplier')
|
p["supplier2"] # returns: Supplier(name='anonymous supplier')
|
||||||
|
|
||||||
p['variants'] # raises KeyError
|
p["variants"] # raises KeyError
|
||||||
p['variants2'] # returns []
|
p["variants2"] # returns []
|
||||||
|
|
||||||
p['categories'] # raises KeyError
|
p["categories"] # raises KeyError
|
||||||
p.get('categories') # returns None
|
p.get("categories") # returns None
|
||||||
|
|
||||||
p['numbers'] # returns []
|
p["numbers"] # returns []
|
||||||
|
|
||||||
Accessing and changing nested item values
|
Accessing and changing nested item values
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
p = Product(supplier=Supplier(name="some name", url="http://example.com"))
|
p = Product(supplier=Supplier(name="some name", url="http://example.com"))
|
||||||
p['supplier']['url'] # returns 'http://example.com'
|
p["supplier"]["url"] # returns 'http://example.com'
|
||||||
p['supplier']['url'] = "http://www.other.com" # works as expected
|
p["supplier"]["url"] = "http://www.other.com" # works as expected
|
||||||
p['supplier']['url'] = 123 # fails: wrong type for supplier url
|
p["supplier"]["url"] = 123 # fails: wrong type for supplier url
|
||||||
|
|
||||||
p['variants'] = [v1, v2]
|
p["variants"] = [v1, v2]
|
||||||
p['variants'][0]['name'] # returns v1 name
|
p["variants"][0]["name"] # returns v1 name
|
||||||
p['variants'][1]['name'] # returns v2 name
|
p["variants"][1]["name"] # returns v2 name
|
||||||
|
|
||||||
# XXX: decide what to do about these cases:
|
# XXX: decide what to do about these cases:
|
||||||
p['variants'].append(v3) # works but doesn't check type of v3
|
p["variants"].append(v3) # works but doesn't check type of v3
|
||||||
p['variants'].append(1) # works but shouldn't?
|
p["variants"].append(1) # works but shouldn't?
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Proposed API
|
||||||
|
|
||||||
Here's a simple proof-of-concept code of such script:
|
Here's a simple proof-of-concept code of such script:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from scrapy.http import Request
|
from scrapy.http import Request
|
||||||
|
|
@ -35,21 +35,24 @@ Here's a simple proof-of-concept code of such script:
|
||||||
# a container to hold scraped items
|
# a container to hold scraped items
|
||||||
scraped_items = []
|
scraped_items = []
|
||||||
|
|
||||||
|
|
||||||
def parse_start_page(response):
|
def parse_start_page(response):
|
||||||
# collect urls to follow into urls_to_follow list
|
# collect urls to follow into urls_to_follow list
|
||||||
requests = [Request(url, callback=parse_other_page) for url in urls_to_follow]
|
requests = [Request(url, callback=parse_other_page) for url in urls_to_follow]
|
||||||
return requests
|
return requests
|
||||||
|
|
||||||
|
|
||||||
def parse_other_page(response):
|
def parse_other_page(response):
|
||||||
# ... parse items from response content ...
|
# ... parse items from response content ...
|
||||||
scraped_items.extend(parsed_items)
|
scraped_items.extend(parsed_items)
|
||||||
|
|
||||||
|
|
||||||
start_urls = ["http://www.example.com/start_page.html"]
|
start_urls = ["http://www.example.com/start_page.html"]
|
||||||
|
|
||||||
cr = Crawler(start_urls, callback=parse_start_page)
|
cr = Crawler(start_urls, callback=parse_start_page)
|
||||||
cr.run() # blocking call - this populates scraped_items
|
cr.run() # blocking call - this populates scraped_items
|
||||||
|
|
||||||
print "%d items scraped" % len(scraped_items)
|
print("%d items scraped" % len(scraped_items))
|
||||||
# ... do something more interesting with scraped_items ...
|
# ... do something more interesting with scraped_items ...
|
||||||
|
|
||||||
The behaviour of the Scrapy crawler would be controller by the Scrapy settings,
|
The behaviour of the Scrapy crawler would be controller by the Scrapy settings,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ SEP-005: Detailed ``ItemBuilder`` API use
|
||||||
|
|
||||||
Item class for examples:
|
Item class for examples:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class NewsItem(Item):
|
class NewsItem(Item):
|
||||||
|
|
@ -25,7 +25,7 @@ Item class for examples:
|
||||||
gSetting expanders
|
gSetting expanders
|
||||||
==================
|
==================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class NewsItemBuilder(ItemBuilder):
|
class NewsItemBuilder(ItemBuilder):
|
||||||
|
|
@ -44,7 +44,7 @@ on their Item Field class:
|
||||||
gSetting reducers
|
gSetting reducers
|
||||||
=================
|
=================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class NewsItemBuilder(ItemBuilder):
|
class NewsItemBuilder(ItemBuilder):
|
||||||
|
|
@ -60,7 +60,7 @@ content
|
||||||
gSetting expanders/reducers new way
|
gSetting expanders/reducers new way
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class NewsItemBuilder(ItemBuilder):
|
class NewsItemBuilder(ItemBuilder):
|
||||||
|
|
@ -76,28 +76,29 @@ gSetting expanders/reducers new way
|
||||||
gExtending ``ItemBuilder``
|
gExtending ``ItemBuilder``
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SiteNewsItemBuilder(NewsItemBuilder):
|
class SiteNewsItemBuilder(NewsItemBuilder):
|
||||||
published = reducers.Reducer(extract, remove_tags(), unquote(),
|
published = reducers.Reducer(
|
||||||
strip, to_date('%d.%m.%Y'))
|
extract, remove_tags(), unquote(), strip, to_date("%d.%m.%Y")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
gExtending ``ItemBuilder`` using statich methods
|
gExtending ``ItemBuilder`` using statich methods
|
||||||
================================================
|
================================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SiteNewsItemBuilder(NewsItemBuilder):
|
class SiteNewsItemBuilder(NewsItemBuilder):
|
||||||
published = reducers.Reducer(NewsItemBuilder.published, to_date('%d.%m.%Y'))
|
published = reducers.Reducer(NewsItemBuilder.published, to_date("%d.%m.%Y"))
|
||||||
|
|
||||||
|
|
||||||
gUsing default_builder
|
gUsing default_builder
|
||||||
======================
|
======================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class DefaultedNewsItemBuilder(ItemBuilder):
|
class DefaultedNewsItemBuilder(ItemBuilder):
|
||||||
|
|
@ -112,7 +113,7 @@ As a reducer is not set reducers will be set based on Item Field classes.
|
||||||
gReset default_builder for a field
|
gReset default_builder for a field
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class DefaultedNewsItemBuilder(ItemBuilder):
|
class DefaultedNewsItemBuilder(ItemBuilder):
|
||||||
|
|
@ -125,18 +126,20 @@ gReset default_builder for a field
|
||||||
gExtending default ``ItemBuilder``
|
gExtending default ``ItemBuilder``
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SiteNewsItemBuilder(NewsItemBuilder):
|
class SiteNewsItemBuilder(NewsItemBuilder):
|
||||||
published = reducers.Reducer(extract, remove_tags(), unquote(), strip, to_date('%d.%m.%Y'))
|
published = reducers.Reducer(
|
||||||
|
extract, remove_tags(), unquote(), strip, to_date("%d.%m.%Y")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
gExtending default ``ItemBuilder`` using static methods
|
gExtending default ``ItemBuilder`` using static methods
|
||||||
=======================================================
|
=======================================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SiteNewsItemBuilder(NewsItemBuilder):
|
class SiteNewsItemBuilder(NewsItemBuilder):
|
||||||
published = reducers.Reducer(NewsItemBuilder.default_builder, to_date('%d.%m.%Y'))
|
published = reducers.Reducer(NewsItemBuilder.default_builder, to_date("%d.%m.%Y"))
|
||||||
|
|
|
||||||
|
|
@ -87,11 +87,12 @@ Alternative Public API Proposal
|
||||||
Usage example: declaring Item Parsers
|
Usage example: declaring Item Parsers
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.contrib.itemparser import XPathItemParser, parsers
|
from scrapy.contrib.itemparser import XPathItemParser, parsers
|
||||||
|
|
||||||
|
|
||||||
class ProductParser(XPathItemParser):
|
class ProductParser(XPathItemParser):
|
||||||
name_in = parsers.MapConcat(removetags, filterx)
|
name_in = parsers.MapConcat(removetags, filterx)
|
||||||
price_in = parsers.MapConcat(...)
|
price_in = parsers.MapConcat(...)
|
||||||
|
|
@ -101,7 +102,7 @@ Usage example: declaring Item Parsers
|
||||||
Usage example: declaring parsers in Fields
|
Usage example: declaring parsers in Fields
|
||||||
==========================================
|
==========================================
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class Product(Item):
|
class Product(Item):
|
||||||
|
|
|
||||||
|
|
@ -76,28 +76,30 @@ which we haven't documented so far (partly because of this).
|
||||||
|
|
||||||
So, for a typical middleware ``__init__`` method code, instead of this:
|
So, for a typical middleware ``__init__`` method code, instead of this:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.core.exceptions import NotConfigured
|
from scrapy.core.exceptions import NotConfigured
|
||||||
from scrapy.conf import settings
|
from scrapy.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class SomeMiddleware(object):
|
class SomeMiddleware(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not settings.getbool('SOMEMIDDLEWARE_ENABLED'):
|
if not settings.getbool("SOMEMIDDLEWARE_ENABLED"):
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
|
|
||||||
We'd write this:
|
We'd write this:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.core.exceptions import NotConfigured
|
from scrapy.core.exceptions import NotConfigured
|
||||||
|
|
||||||
|
|
||||||
class SomeMiddleware(object):
|
class SomeMiddleware(object):
|
||||||
def __init__(self, crawler):
|
def __init__(self, crawler):
|
||||||
if not crawler.settings.getbool('SOMEMIDDLEWARE_ENABLED'):
|
if not crawler.settings.getbool("SOMEMIDDLEWARE_ENABLED"):
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
|
|
||||||
Running from command line
|
Running from command line
|
||||||
=========================
|
=========================
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,10 @@ example:
|
||||||
|
|
||||||
$ cat project/spiders/google.py
|
$ cat project/spiders/google.py
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
class GooglecomSpider(BaseSpider):
|
class GooglecomSpider(BaseSpider):
|
||||||
name = 'google'
|
name = "google"
|
||||||
allowed_domains = ['google.com']
|
allowed_domains = ["google.com"]
|
||||||
|
|
||||||
.. note:: ``spider_allowed_domains`` becomes optional as only ``OffsiteMiddleware`` uses it.
|
.. note:: ``spider_allowed_domains`` becomes optional as only ``OffsiteMiddleware`` uses it.
|
||||||
|
|
|
||||||
170
sep/sep-014.rst
170
sep/sep-014.rst
|
|
@ -92,7 +92,7 @@ Usage Examples
|
||||||
Basic Crawling
|
Basic Crawling
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
|
|
@ -101,20 +101,20 @@ Basic Crawling
|
||||||
class SampleSpider(CrawlSpider):
|
class SampleSpider(CrawlSpider):
|
||||||
rules = [
|
rules = [
|
||||||
# The dispatcher uses first-match policy
|
# The dispatcher uses first-match policy
|
||||||
Rule(UrlRegexMatch(r'product\.html\?id=\d+'), 'parse_item', follow=False),
|
Rule(UrlRegexMatch(r"product\.html\?id=\d+"), "parse_item", follow=False),
|
||||||
# by default, if the first param is string is wrapped into UrlRegexMatch
|
# by default, if the first param is string is wrapped into UrlRegexMatch
|
||||||
Rule(r'.+', 'parse_page'),
|
Rule(r".+", "parse_page"),
|
||||||
]
|
]
|
||||||
|
|
||||||
request_extractors = [
|
request_extractors = [
|
||||||
# crawl all links looking for products and images
|
# crawl all links looking for products and images
|
||||||
SgmlRequestExtractor(),
|
SgmlRequestExtractor(),
|
||||||
]
|
]
|
||||||
|
|
||||||
request_processors = [
|
request_processors = [
|
||||||
# canonicalize all requests' urls
|
# canonicalize all requests' urls
|
||||||
Canonicalize(),
|
Canonicalize(),
|
||||||
]
|
]
|
||||||
|
|
||||||
def parse_item(self, response):
|
def parse_item(self, response):
|
||||||
# parse and extract items from response
|
# parse and extract items from response
|
||||||
|
|
@ -127,7 +127,7 @@ Basic Crawling
|
||||||
Custom Processor and External Callback
|
Custom Processor and External Callback
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
|
|
@ -137,30 +137,32 @@ Custom Processor and External Callback
|
||||||
# Custom Processor
|
# Custom Processor
|
||||||
def filter_today_links(requests):
|
def filter_today_links(requests):
|
||||||
# only crawl today links
|
# only crawl today links
|
||||||
today = datetime.datetime.today().strftime('%Y-%m-%d')
|
today = datetime.datetime.today().strftime("%Y-%m-%d")
|
||||||
return [r for r in requests if today in r.url]
|
return [r for r in requests if today in r.url]
|
||||||
|
|
||||||
|
|
||||||
# Callback defined out of spider
|
# Callback defined out of spider
|
||||||
def my_external_callback(response):
|
def my_external_callback(response):
|
||||||
# process item
|
# process item
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SampleSpider(CrawlSpider):
|
class SampleSpider(CrawlSpider):
|
||||||
rules = [
|
rules = [
|
||||||
# The dispatcher uses first-match policy
|
# The dispatcher uses first-match policy
|
||||||
Rule(UrlRegexMatch(r'/news/(.+)/'), my_external_callback),
|
Rule(UrlRegexMatch(r"/news/(.+)/"), my_external_callback),
|
||||||
]
|
]
|
||||||
|
|
||||||
request_extractors = [
|
request_extractors = [
|
||||||
RegexRequestExtractor(r'/sections/.+'),
|
RegexRequestExtractor(r"/sections/.+"),
|
||||||
RegexRequestExtractor(r'/news/.+'),
|
RegexRequestExtractor(r"/news/.+"),
|
||||||
]
|
]
|
||||||
|
|
||||||
request_processors = [
|
request_processors = [
|
||||||
# canonicalize all requests' urls
|
# canonicalize all requests' urls
|
||||||
Canonicalize(),
|
Canonicalize(),
|
||||||
filter_today_links,
|
filter_today_links,
|
||||||
]
|
]
|
||||||
|
|
||||||
Implementation
|
Implementation
|
||||||
==============
|
==============
|
||||||
|
|
@ -199,7 +201,7 @@ Package Structure
|
||||||
Request/Response Matchers
|
Request/Response Matchers
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
"""
|
"""
|
||||||
|
|
@ -208,6 +210,7 @@ Request/Response Matchers
|
||||||
Perform evaluation to Request or Response attributes
|
Perform evaluation to Request or Response attributes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BaseMatcher(object):
|
class BaseMatcher(object):
|
||||||
"""Base matcher. Returns True by default."""
|
"""Base matcher. Returns True by default."""
|
||||||
|
|
||||||
|
|
@ -229,11 +232,11 @@ Request/Response Matchers
|
||||||
|
|
||||||
def matches_url(self, url):
|
def matches_url(self, url):
|
||||||
"""Returns True if given url is equal to matcher's url"""
|
"""Returns True if given url is equal to matcher's url"""
|
||||||
return self._url url
|
return self._url == url
|
||||||
|
|
||||||
def matches_request(self, request):
|
def matches_request(self, request):
|
||||||
"""Returns True if Request's url matches initial url"""
|
"""Returns True if Request's url matches initial url"""
|
||||||
return self.matches_url(request.url)
|
return self.matches_url(request.url)
|
||||||
|
|
||||||
def matches_response(self, response):
|
def matches_response(self, response):
|
||||||
"""REturns True if Response's url matches initial url"""
|
"""REturns True if Response's url matches initial url"""
|
||||||
|
|
@ -254,7 +257,7 @@ Request/Response Matchers
|
||||||
Request Extractor
|
Request Extractor
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
|
|
@ -262,21 +265,21 @@ Request Extractor
|
||||||
# Extractors receive response and return list of Requests
|
# Extractors receive response and return list of Requests
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class BaseSgmlRequestExtractor(FixedSGMLParser):
|
class BaseSgmlRequestExtractor(FixedSGMLParser):
|
||||||
"""Base SGML Request Extractor"""
|
"""Base SGML Request Extractor"""
|
||||||
|
|
||||||
def __init__(self, tag='a', attr='href'):
|
def __init__(self, tag="a", attr="href"):
|
||||||
"""Initialize attributes"""
|
"""Initialize attributes"""
|
||||||
FixedSGMLParser.__init__(self)
|
FixedSGMLParser.__init__(self)
|
||||||
|
|
||||||
self.scan_tag = tag if callable(tag) else lambda t: t tag
|
self.scan_tag = tag if callable(tag) else lambda t: t = tag
|
||||||
self.scan_attr = attr if callable(attr) else lambda a: a attr
|
self.scan_attr = attr if callable(attr) else lambda a: a = attr
|
||||||
self.current_request = None
|
self.current_request = None
|
||||||
|
|
||||||
def extract_requests(self, response):
|
def extract_requests(self, response):
|
||||||
"""Returns list of requests extracted from response"""
|
"""Returns list of requests extracted from response"""
|
||||||
return self._extract_requests(response.body, response.url,
|
return self._extract_requests(response.body, response.url, response.encoding)
|
||||||
response.encoding)
|
|
||||||
|
|
||||||
def _extract_requests(self, response_text, response_url, response_encoding):
|
def _extract_requests(self, response_text, response_url, response_encoding):
|
||||||
"""Extract requests with absolute urls"""
|
"""Extract requests with absolute urls"""
|
||||||
|
|
@ -303,20 +306,19 @@ Request Extractor
|
||||||
def _fix_link_text_encoding(self, encoding):
|
def _fix_link_text_encoding(self, encoding):
|
||||||
"""Convert link_text to unicode for each request"""
|
"""Convert link_text to unicode for each request"""
|
||||||
for req in self.requests:
|
for req in self.requests:
|
||||||
req.meta.setdefault('link_text', '')
|
req.meta.setdefault("link_text", "")
|
||||||
req.meta['link_text'] = str_to_unicode(req.meta['link_text'],
|
req.meta["link_text"] = str_to_unicode(req.meta["link_text"], encoding)
|
||||||
encoding)
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Reset state"""
|
"""Reset state"""
|
||||||
FixedSGMLParser.reset(self)
|
FixedSGMLParser.reset(self)
|
||||||
self.requests = []
|
self.requests = []
|
||||||
self.base_url = None
|
self.base_url = None
|
||||||
|
|
||||||
def unknown_starttag(self, tag, attrs):
|
def unknown_starttag(self, tag, attrs):
|
||||||
"""Process unknown start tag"""
|
"""Process unknown start tag"""
|
||||||
if 'base' tag:
|
if "base" == tag:
|
||||||
self.base_url = dict(attrs).get('href')
|
self.base_url = dict(attrs).get("href")
|
||||||
|
|
||||||
if self.scan_tag(tag):
|
if self.scan_tag(tag):
|
||||||
for attr, value in attrs:
|
for attr, value in attrs:
|
||||||
|
|
@ -333,8 +335,8 @@ Request Extractor
|
||||||
def handle_data(self, data):
|
def handle_data(self, data):
|
||||||
"""Process data"""
|
"""Process data"""
|
||||||
current = self.current_request
|
current = self.current_request
|
||||||
if current and not 'link_text' in current.meta:
|
if current and not "link_text" in current.meta:
|
||||||
current.meta['link_text'] = data.strip()
|
current.meta["link_text"] = data.strip()
|
||||||
|
|
||||||
|
|
||||||
class SgmlRequestExtractor(BaseSgmlRequestExtractor):
|
class SgmlRequestExtractor(BaseSgmlRequestExtractor):
|
||||||
|
|
@ -343,8 +345,8 @@ Request Extractor
|
||||||
def __init__(self, tags=None, attrs=None):
|
def __init__(self, tags=None, attrs=None):
|
||||||
"""Initialize with custom tag & attribute function checkers"""
|
"""Initialize with custom tag & attribute function checkers"""
|
||||||
# defaults
|
# defaults
|
||||||
tags = tuple(tags) if tags else ('a', 'area')
|
tags = tuple(tags) if tags else ("a", "area")
|
||||||
attrs = tuple(attrs) if attrs else ('href', )
|
attrs = tuple(attrs) if attrs else ("href",)
|
||||||
|
|
||||||
tag_func = lambda x: x in tags
|
tag_func = lambda x: x in tags
|
||||||
attr_func = lambda x: x in attrs
|
attr_func = lambda x: x in attrs
|
||||||
|
|
@ -362,25 +364,26 @@ Request Extractor
|
||||||
def extract_requests(self, response):
|
def extract_requests(self, response):
|
||||||
"""Restrict to XPath regions"""
|
"""Restrict to XPath regions"""
|
||||||
hxs = HtmlXPathSelector(response)
|
hxs = HtmlXPathSelector(response)
|
||||||
fragments = (''.join(
|
fragments = (
|
||||||
html_frag for html_frag in hxs.select(xpath).extract()
|
"".join(html_frag for html_frag in hxs.select(xpath).extract())
|
||||||
) for xpath in self.restrict_xpaths)
|
for xpath in self.restrict_xpaths
|
||||||
html_slice = ''.join(html_frag for html_frag in fragments)
|
)
|
||||||
return self._extract_requests(html_slice, response.url,
|
html_slice = "".join(html_frag for html_frag in fragments)
|
||||||
response.encoding)
|
return self._extract_requests(html_slice, response.url, response.encoding)
|
||||||
|
|
||||||
Request Processor
|
Request Processor
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
# Request Processors
|
# Request Processors
|
||||||
# Processors receive list of requests and return list of requests
|
# Processors receive list of requests and return list of requests
|
||||||
#
|
#
|
||||||
"""Request Processors"""
|
"""Request Processors"""
|
||||||
|
|
||||||
|
|
||||||
class Canonicalize(object):
|
class Canonicalize(object):
|
||||||
"""Canonicalize Request Processor"""
|
"""Canonicalize Request Processor"""
|
||||||
|
|
||||||
|
|
@ -390,14 +393,14 @@ Request Processor
|
||||||
# replace in-place
|
# replace in-place
|
||||||
req.url = canonicalize_url(req.url)
|
req.url = canonicalize_url(req.url)
|
||||||
yield req
|
yield req
|
||||||
|
|
||||||
|
|
||||||
class Unique(object):
|
class Unique(object):
|
||||||
"""Filter duplicate Requests"""
|
"""Filter duplicate Requests"""
|
||||||
|
|
||||||
def __init__(self, *attributes):
|
def __init__(self, *attributes):
|
||||||
"""Initialize comparison attributes"""
|
"""Initialize comparison attributes"""
|
||||||
self._attributes = attributes or ['url']
|
self._attributes = attributes or ["url"]
|
||||||
|
|
||||||
def _requests_equal(self, req1, req2):
|
def _requests_equal(self, req1, req2):
|
||||||
"""Attribute comparison helper"""
|
"""Attribute comparison helper"""
|
||||||
|
|
@ -430,20 +433,24 @@ Request Processor
|
||||||
"""Filter request's domain"""
|
"""Filter request's domain"""
|
||||||
|
|
||||||
def __init__(self, allow=(), deny=()):
|
def __init__(self, allow=(), deny=()):
|
||||||
"""Initialize allow/deny attributes"""
|
"""Initialize allow/deny attributes"""
|
||||||
self.allow = tuple(arg_to_iter(allow))
|
self.allow = tuple(arg_to_iter(allow))
|
||||||
self.deny = tuple(arg_to_iter(deny))
|
self.deny = tuple(arg_to_iter(deny))
|
||||||
|
|
||||||
def __call__(self, requests):
|
def __call__(self, requests):
|
||||||
"""Filter domains"""
|
"""Filter domains"""
|
||||||
processed = (req for req in requests)
|
processed = (req for req in requests)
|
||||||
|
|
||||||
if self.allow:
|
if self.allow:
|
||||||
processed = (req for req in requests
|
processed = (
|
||||||
if url_is_from_any_domain(req.url, self.allow))
|
req for req in requests if url_is_from_any_domain(req.url, self.allow)
|
||||||
|
)
|
||||||
if self.deny:
|
if self.deny:
|
||||||
processed = (req for req in requests
|
processed = (
|
||||||
if not url_is_from_any_domain(req.url, self.deny))
|
req
|
||||||
|
for req in requests
|
||||||
|
if not url_is_from_any_domain(req.url, self.deny)
|
||||||
|
)
|
||||||
|
|
||||||
return processed
|
return processed
|
||||||
|
|
||||||
|
|
@ -453,24 +460,28 @@ Request Processor
|
||||||
|
|
||||||
def __init__(self, allow=(), deny=()):
|
def __init__(self, allow=(), deny=()):
|
||||||
"""Initialize allow/deny attributes"""
|
"""Initialize allow/deny attributes"""
|
||||||
_re_type = type(re.compile('', 0))
|
_re_type = type(re.compile("", 0))
|
||||||
|
|
||||||
self.allow_res = [x if isinstance(x, _re_type) else re.compile(x)
|
self.allow_res = [
|
||||||
for x in arg_to_iter(allow)]
|
x if isinstance(x, _re_type) else re.compile(x) for x in arg_to_iter(allow)
|
||||||
self.deny_res = [x if isinstance(x, _re_type) else re.compile(x)
|
]
|
||||||
for x in arg_to_iter(deny)]
|
self.deny_res = [
|
||||||
|
x if isinstance(x, _re_type) else re.compile(x) for x in arg_to_iter(deny)
|
||||||
|
]
|
||||||
|
|
||||||
def __call__(self, requests):
|
def __call__(self, requests):
|
||||||
"""Filter request's url based on allow/deny rules"""
|
"""Filter request's url based on allow/deny rules"""
|
||||||
#TODO: filter valid urls here?
|
# TODO: filter valid urls here?
|
||||||
processed = (req for req in requests)
|
processed = (req for req in requests)
|
||||||
|
|
||||||
if self.allow_res:
|
if self.allow_res:
|
||||||
processed = (req for req in requests
|
processed = (
|
||||||
if self._matches(req.url, self.allow_res))
|
req for req in requests if self._matches(req.url, self.allow_res)
|
||||||
|
)
|
||||||
if self.deny_res:
|
if self.deny_res:
|
||||||
processed = (req for req in requests
|
processed = (
|
||||||
if not self._matches(req.url, self.deny_res))
|
req for req in requests if not self._matches(req.url, self.deny_res)
|
||||||
|
)
|
||||||
|
|
||||||
return processed
|
return processed
|
||||||
|
|
||||||
|
|
@ -481,7 +492,7 @@ Request Processor
|
||||||
Rule Object
|
Rule Object
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
|
|
@ -490,8 +501,10 @@ Rule Object
|
||||||
#
|
#
|
||||||
class Rule(object):
|
class Rule(object):
|
||||||
"""Crawler Rule"""
|
"""Crawler Rule"""
|
||||||
def __init__(self, matcher, callback=None, cb_args=None,
|
|
||||||
cb_kwargs=None, follow=True):
|
def __init__(
|
||||||
|
self, matcher, callback=None, cb_args=None, cb_kwargs=None, follow=True
|
||||||
|
):
|
||||||
"""Store attributes"""
|
"""Store attributes"""
|
||||||
self.matcher = matcher
|
self.matcher = matcher
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
|
@ -499,12 +512,14 @@ Rule Object
|
||||||
self.cb_kwargs = cb_kwargs if cb_kwargs else {}
|
self.cb_kwargs = cb_kwargs if cb_kwargs else {}
|
||||||
self.follow = follow
|
self.follow = follow
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rules Manager takes list of Rule objects and normalize matcher and callback
|
# Rules Manager takes list of Rule objects and normalize matcher and callback
|
||||||
# into CompiledRule
|
# into CompiledRule
|
||||||
#
|
#
|
||||||
class CompiledRule(object):
|
class CompiledRule(object):
|
||||||
"""Compiled version of Rule"""
|
"""Compiled version of Rule"""
|
||||||
|
|
||||||
def __init__(self, matcher, callback=None, follow=False):
|
def __init__(self, matcher, callback=None, follow=False):
|
||||||
"""Initialize attributes checking type"""
|
"""Initialize attributes checking type"""
|
||||||
assert isinstance(matcher, BaseMatcher)
|
assert isinstance(matcher, BaseMatcher)
|
||||||
|
|
@ -518,15 +533,16 @@ Rule Object
|
||||||
Rules Manager
|
Rules Manager
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
# Handles rules matcher/callbacks
|
# Handles rules matcher/callbacks
|
||||||
# Resolve rule for given response
|
# Resolve rule for given response
|
||||||
#
|
#
|
||||||
class RulesManager(object):
|
class RulesManager(object):
|
||||||
"""Rules Manager"""
|
"""Rules Manager"""
|
||||||
|
|
||||||
def __init__(self, rules, spider, default_matcher=UrlRegexMatcher):
|
def __init__(self, rules, spider, default_matcher=UrlRegexMatcher):
|
||||||
"""Initialize rules using spider and default matcher"""
|
"""Initialize rules using spider and default matcher"""
|
||||||
self._rules = tuple()
|
self._rules = tuple()
|
||||||
|
|
@ -542,8 +558,9 @@ Rules Manager
|
||||||
# instance default matcher
|
# instance default matcher
|
||||||
matcher = default_matcher(rule.matcher)
|
matcher = default_matcher(rule.matcher)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Not valid matcher given %r in %r' \
|
raise ValueError(
|
||||||
% (rule.matcher, rule))
|
"Not valid matcher given %r in %r" % (rule.matcher, rule)
|
||||||
|
)
|
||||||
|
|
||||||
# prepare callback
|
# prepare callback
|
||||||
if callable(rule.callback):
|
if callable(rule.callback):
|
||||||
|
|
@ -553,8 +570,9 @@ Rules Manager
|
||||||
callback = getattr(spider, rule.callback)
|
callback = getattr(spider, rule.callback)
|
||||||
|
|
||||||
if not callable(callback):
|
if not callable(callback):
|
||||||
raise AttributeError('Invalid callback %r can not be resolved' \
|
raise AttributeError(
|
||||||
% callback)
|
"Invalid callback %r can not be resolved" % callback
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
callback = None
|
callback = None
|
||||||
|
|
||||||
|
|
@ -564,7 +582,7 @@ Rules Manager
|
||||||
|
|
||||||
# append compiled rule to rules list
|
# append compiled rule to rules list
|
||||||
crule = CompiledRule(matcher, callback, follow=rule.follow)
|
crule = CompiledRule(matcher, callback, follow=rule.follow)
|
||||||
self._rules += (crule, )
|
self._rules += (crule,)
|
||||||
|
|
||||||
def get_rule(self, response):
|
def get_rule(self, response):
|
||||||
"""Returns first rule that matches response"""
|
"""Returns first rule that matches response"""
|
||||||
|
|
@ -575,7 +593,7 @@ Rules Manager
|
||||||
Request Generator
|
Request Generator
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
|
|
@ -605,7 +623,7 @@ Request Generator
|
||||||
``CrawlSpider``
|
``CrawlSpider``
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
#
|
#
|
||||||
|
|
@ -625,9 +643,9 @@ Request Generator
|
||||||
# wrap rules
|
# wrap rules
|
||||||
self._rulesman = RulesManager(self.rules, spider=self)
|
self._rulesman = RulesManager(self.rules, spider=self)
|
||||||
# generates new requests with given callback
|
# generates new requests with given callback
|
||||||
self._reqgen = RequestGenerator(self.request_extractors,
|
self._reqgen = RequestGenerator(
|
||||||
self.request_processors,
|
self.request_extractors, self.request_processors, self.parse
|
||||||
self.parse)
|
)
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
"""Dispatch callback and generate requests"""
|
"""Dispatch callback and generate requests"""
|
||||||
|
|
|
||||||
|
|
@ -67,21 +67,21 @@ Regex (HTML) Link Extractor
|
||||||
|
|
||||||
A typical application of LegSpider's is to build Link Extractors. For example:
|
A typical application of LegSpider's is to build Link Extractors. For example:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class RegexHtmlLinkExtractor(LegSpider):
|
class RegexHtmlLinkExtractor(LegSpider):
|
||||||
|
|
||||||
def process_response(self, response):
|
def process_response(self, response):
|
||||||
if isinstance(response, HtmlResponse):
|
if isinstance(response, HtmlResponse):
|
||||||
allowed_regexes = self.spider.url_regexes_to_follow
|
allowed_regexes = self.spider.url_regexes_to_follow
|
||||||
# extract urls to follow using allowed_regexes
|
# extract urls to follow using allowed_regexes
|
||||||
return [Request(x) for x in urls_to_follow]
|
return [Request(x) for x in urls_to_follow]
|
||||||
|
|
||||||
|
|
||||||
class MySpider(LegSpider):
|
class MySpider(LegSpider):
|
||||||
|
|
||||||
legs = [RegexHtmlLinkExtractor()]
|
legs = [RegexHtmlLinkExtractor()]
|
||||||
url_regexes_to_follow = ['/product.php?.*']
|
url_regexes_to_follow = ["/product.php?.*"]
|
||||||
|
|
||||||
def parse_response(self, response):
|
def parse_response(self, response):
|
||||||
# parse response and extract items
|
# parse response and extract items
|
||||||
|
|
@ -92,13 +92,12 @@ RSS2 link extractor
|
||||||
|
|
||||||
This is a Leg Spider that can be used for following links from RSS2 feeds.
|
This is a Leg Spider that can be used for following links from RSS2 feeds.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class Rss2LinkExtractor(LegSpider):
|
class Rss2LinkExtractor(LegSpider):
|
||||||
|
|
||||||
def process_response(self, response):
|
def process_response(self, response):
|
||||||
if response.headers.get('Content-type') 'application/rss+xml':
|
if response.headers.get("Content-type") == "application/rss+xml":
|
||||||
xs = XmlXPathSelector(response)
|
xs = XmlXPathSelector(response)
|
||||||
urls = xs.select("//item/link/text()").extract()
|
urls = xs.select("//item/link/text()").extract()
|
||||||
return [Request(x) for x in urls]
|
return [Request(x) for x in urls]
|
||||||
|
|
@ -108,11 +107,10 @@ Callback dispatcher based on rules
|
||||||
|
|
||||||
Another example could be to build a callback dispatcher based on rules:
|
Another example could be to build a callback dispatcher based on rules:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class CallbackRules(LegSpider):
|
class CallbackRules(LegSpider):
|
||||||
|
|
||||||
def __init__(self, *a, **kw):
|
def __init__(self, *a, **kw):
|
||||||
super(CallbackRules, self).__init__(*a, **kw)
|
super(CallbackRules, self).__init__(*a, **kw)
|
||||||
for regex, method_name in self.spider.callback_rules.items():
|
for regex, method_name in self.spider.callback_rules.items():
|
||||||
|
|
@ -128,12 +126,13 @@ Another example could be to build a callback dispatcher based on rules:
|
||||||
return method(response)
|
return method(response)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
class MySpider(LegSpider):
|
class MySpider(LegSpider):
|
||||||
|
|
||||||
legs = [CallbackRules()]
|
legs = [CallbackRules()]
|
||||||
callback_rules = {
|
callback_rules = {
|
||||||
'/product.php.*': 'parse_product',
|
"/product.php.*": "parse_product",
|
||||||
'/category.php.*': 'parse_category',
|
"/category.php.*": "parse_category",
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_product(self, response):
|
def parse_product(self, response):
|
||||||
|
|
@ -145,19 +144,19 @@ URL Canonicalizers
|
||||||
|
|
||||||
Another example could be for building URL canonicalizers:
|
Another example could be for building URL canonicalizers:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class CanonicalizeUrl(LegSpider):
|
class CanonicalizeUrl(LegSpider):
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
curl = canonicalize_url(request.url, rules=self.spider.canonicalization_rules)
|
curl = canonicalize_url(request.url, rules=self.spider.canonicalization_rules)
|
||||||
return request.replace(url=curl)
|
return request.replace(url=curl)
|
||||||
|
|
||||||
|
|
||||||
class MySpider(LegSpider):
|
class MySpider(LegSpider):
|
||||||
|
|
||||||
legs = [CanonicalizeUrl()]
|
legs = [CanonicalizeUrl()]
|
||||||
canonicalization_rules = ['sort-query-args', 'normalize-percent-encoding', ...]
|
canonicalization_rules = ["sort-query-args", "normalize-percent-encoding", ...]
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
@ -167,22 +166,22 @@ Setting item identifier
|
||||||
Another example could be for setting a unique identifier to items, based on
|
Another example could be for setting a unique identifier to items, based on
|
||||||
certain fields:
|
certain fields:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class ItemIdSetter(LegSpider):
|
class ItemIdSetter(LegSpider):
|
||||||
|
|
||||||
def process_item(self, item):
|
def process_item(self, item):
|
||||||
id_field = self.spider.id_field
|
id_field = self.spider.id_field
|
||||||
id_fields_to_hash = self.spider.id_fields_to_hash
|
id_fields_to_hash = self.spider.id_fields_to_hash
|
||||||
item[id_field] = make_hash_based_on_fields(item, id_fields_to_hash)
|
item[id_field] = make_hash_based_on_fields(item, id_fields_to_hash)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
class MySpider(LegSpider):
|
class MySpider(LegSpider):
|
||||||
|
|
||||||
legs = [ItemIdSetter()]
|
legs = [ItemIdSetter()]
|
||||||
id_field = 'guid'
|
id_field = "guid"
|
||||||
id_fields_to_hash = ['supplier_name', 'supplier_id']
|
id_fields_to_hash = ["supplier_name", "supplier_id"]
|
||||||
|
|
||||||
def process_response(self, item):
|
def process_response(self, item):
|
||||||
# extract item from response
|
# extract item from response
|
||||||
|
|
@ -193,24 +192,24 @@ Combining multiple leg spiders
|
||||||
|
|
||||||
Here's an example that combines functionality from multiple leg spiders:
|
Here's an example that combines functionality from multiple leg spiders:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class MySpider(LegSpider):
|
class MySpider(LegSpider):
|
||||||
|
|
||||||
legs = [RegexLinkExtractor(), ParseRules(), CanonicalizeUrl(), ItemIdSetter()]
|
legs = [RegexLinkExtractor(), ParseRules(), CanonicalizeUrl(), ItemIdSetter()]
|
||||||
|
|
||||||
url_regexes_to_follow = ['/product.php?.*']
|
url_regexes_to_follow = ["/product.php?.*"]
|
||||||
|
|
||||||
parse_rules = {
|
parse_rules = {
|
||||||
'/product.php.*': 'parse_product',
|
"/product.php.*": "parse_product",
|
||||||
'/category.php.*': 'parse_category',
|
"/category.php.*": "parse_category",
|
||||||
}
|
}
|
||||||
|
|
||||||
canonicalization_rules = ['sort-query-args', 'normalize-percent-encoding', ...]
|
canonicalization_rules = ["sort-query-args", "normalize-percent-encoding", ...]
|
||||||
|
|
||||||
id_field = 'guid'
|
id_field = "guid"
|
||||||
id_fields_to_hash = ['supplier_name', 'supplier_id']
|
id_fields_to_hash = ["supplier_name", "supplier_id"]
|
||||||
|
|
||||||
def process_product(self, item):
|
def process_product(self, item):
|
||||||
# extract item from response
|
# extract item from response
|
||||||
|
|
@ -249,7 +248,7 @@ important to keep in mind their scope and limitations, such as:
|
||||||
|
|
||||||
Here's a proof-of-concept implementation of ``LegSpider``:
|
Here's a proof-of-concept implementation of ``LegSpider``:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from scrapy.http import Request
|
from scrapy.http import Request
|
||||||
|
|
|
||||||
|
|
@ -35,16 +35,15 @@ gExample URL for simple callback
|
||||||
The ``parse_product`` callback must return items containing the fields given in
|
The ``parse_product`` callback must return items containing the fields given in
|
||||||
``@scrapes``.
|
``@scrapes``.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class ProductSpider(BaseSpider):
|
class ProductSpider(BaseSpider):
|
||||||
|
|
||||||
def parse_product(self, response):
|
def parse_product(self, response):
|
||||||
"""
|
"""
|
||||||
@url http://www.example.com/store/product.php?id=123
|
@url http://www.example.com/store/product.php?id=123
|
||||||
@scrapes name, price, description
|
@scrapes name, price, description
|
||||||
""""
|
"""
|
||||||
|
|
||||||
gChained callbacks
|
gChained callbacks
|
||||||
------------------
|
------------------
|
||||||
|
|
@ -55,11 +54,10 @@ other for scraping user profile info.
|
||||||
The contracts assert that the first callback returns a Request and the second
|
The contracts assert that the first callback returns a Request and the second
|
||||||
one scrape ``user, name, email`` fields.
|
one scrape ``user, name, email`` fields.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class UserProfileSpider(BaseSpider):
|
class UserProfileSpider(BaseSpider):
|
||||||
|
|
||||||
def parse_login_page(self, response):
|
def parse_login_page(self, response):
|
||||||
"""
|
"""
|
||||||
@url http://www.example.com/login.php
|
@url http://www.example.com/login.php
|
||||||
|
|
@ -71,7 +69,7 @@ one scrape ``user, name, email`` fields.
|
||||||
"""
|
"""
|
||||||
@after parse_login_page
|
@after parse_login_page
|
||||||
@scrapes user, name, email
|
@scrapes user, name, email
|
||||||
""""
|
"""
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
Tags reference
|
Tags reference
|
||||||
|
|
|
||||||
150
sep/sep-018.rst
150
sep/sep-018.rst
|
|
@ -166,27 +166,32 @@ written, it should work both globally and per spider.
|
||||||
Here's an example that combines functionality from multiple middlewares into
|
Here's an example that combines functionality from multiple middlewares into
|
||||||
the same spider:
|
the same spider:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class MySpider(BaseSpider):
|
class MySpider(BaseSpider):
|
||||||
|
|
||||||
middlewares = [RegexLinkExtractor(), CallbackRules(), CanonicalizeUrl(),
|
middlewares = [
|
||||||
ItemIdSetter(), OffsiteMiddleware()]
|
RegexLinkExtractor(),
|
||||||
|
CallbackRules(),
|
||||||
|
CanonicalizeUrl(),
|
||||||
|
ItemIdSetter(),
|
||||||
|
OffsiteMiddleware(),
|
||||||
|
]
|
||||||
|
|
||||||
allowed_domains = ['example.com', 'sub.example.com']
|
allowed_domains = ["example.com", "sub.example.com"]
|
||||||
|
|
||||||
url_regexes_to_follow = ['/product.php?.*']
|
url_regexes_to_follow = ["/product.php?.*"]
|
||||||
|
|
||||||
callback_rules = {
|
callback_rules = {
|
||||||
'/product.php.*': 'parse_product',
|
"/product.php.*": "parse_product",
|
||||||
'/category.php.*': 'parse_category',
|
"/category.php.*": "parse_category",
|
||||||
}
|
}
|
||||||
|
|
||||||
canonicalization_rules = ['sort-query-args', 'normalize-percent-encoding', ...]
|
canonicalization_rules = ["sort-query-args", "normalize-percent-encoding", ...]
|
||||||
|
|
||||||
id_field = 'guid'
|
id_field = "guid"
|
||||||
id_fields_to_hash = ['supplier_name', 'supplier_id']
|
id_fields_to_hash = ["supplier_name", "supplier_id"]
|
||||||
|
|
||||||
def parse_product(self, item):
|
def parse_product(self, item):
|
||||||
# extract item from response
|
# extract item from response
|
||||||
|
|
@ -234,35 +239,34 @@ Regex (HTML) Link Extractor
|
||||||
A typical application of spider middlewares could be to build Link Extractors.
|
A typical application of spider middlewares could be to build Link Extractors.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class RegexHtmlLinkExtractor(object):
|
class RegexHtmlLinkExtractor(object):
|
||||||
|
|
||||||
def process_response(self, response, request, spider):
|
def process_response(self, response, request, spider):
|
||||||
if isinstance(response, HtmlResponse):
|
if isinstance(response, HtmlResponse):
|
||||||
allowed_regexes = spider.url_regexes_to_follow
|
allowed_regexes = spider.url_regexes_to_follow
|
||||||
# extract urls to follow using allowed_regexes
|
# extract urls to follow using allowed_regexes
|
||||||
return [Request(x) for x in urls_to_follow]
|
return [Request(x) for x in urls_to_follow]
|
||||||
|
|
||||||
|
|
||||||
# Example spider using this middleware
|
# Example spider using this middleware
|
||||||
class MySpider(BaseSpider):
|
class MySpider(BaseSpider):
|
||||||
|
|
||||||
middlewares = [RegexHtmlLinkExtractor()]
|
middlewares = [RegexHtmlLinkExtractor()]
|
||||||
url_regexes_to_follow = ['/product.php?.*']
|
url_regexes_to_follow = ["/product.php?.*"]
|
||||||
|
|
||||||
# parsing callbacks below
|
# parsing callbacks below
|
||||||
|
|
||||||
RSS2 link extractor
|
RSS2 link extractor
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class Rss2LinkExtractor(object):
|
class Rss2LinkExtractor(object):
|
||||||
|
|
||||||
def process_response(self, response, request, spider):
|
def process_response(self, response, request, spider):
|
||||||
if response.headers.get('Content-type') 'application/rss+xml':
|
if response.headers.get("Content-type") == "application/rss+xml":
|
||||||
xs = XmlXPathSelector(response)
|
xs = XmlXPathSelector(response)
|
||||||
urls = xs.select("//item/link/text()").extract()
|
urls = xs.select("//item/link/text()").extract()
|
||||||
return [Request(x) for x in urls]
|
return [Request(x) for x in urls]
|
||||||
|
|
@ -272,11 +276,10 @@ Callback dispatcher based on rules
|
||||||
|
|
||||||
Another example could be to build a callback dispatcher based on rules:
|
Another example could be to build a callback dispatcher based on rules:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class CallbackRules(object):
|
class CallbackRules(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.rules = {}
|
self.rules = {}
|
||||||
dispatcher.connect(signals.spider_opened, self.spider_opened)
|
dispatcher.connect(signals.spider_opened, self.spider_opened)
|
||||||
|
|
@ -300,13 +303,14 @@ Another example could be to build a callback dispatcher based on rules:
|
||||||
return method(response)
|
return method(response)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
# Example spider using this middleware
|
# Example spider using this middleware
|
||||||
class MySpider(BaseSpider):
|
class MySpider(BaseSpider):
|
||||||
|
|
||||||
middlewares = [CallbackRules()]
|
middlewares = [CallbackRules()]
|
||||||
callback_rules = {
|
callback_rules = {
|
||||||
'/product.php.*': 'parse_product',
|
"/product.php.*": "parse_product",
|
||||||
'/category.php.*': 'parse_category',
|
"/category.php.*": "parse_category",
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_product(self, response):
|
def parse_product(self, response):
|
||||||
|
|
@ -318,22 +322,20 @@ URL Canonicalizers
|
||||||
|
|
||||||
Another example could be for building URL canonicalizers:
|
Another example could be for building URL canonicalizers:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class CanonicalizeUrl(object):
|
class CanonicalizeUrl(object):
|
||||||
|
|
||||||
def process_request(self, request, response, spider):
|
def process_request(self, request, response, spider):
|
||||||
curl = canonicalize_url(request.url,
|
curl = canonicalize_url(request.url, rules=spider.canonicalization_rules)
|
||||||
rules=spider.canonicalization_rules)
|
|
||||||
return request.replace(url=curl)
|
return request.replace(url=curl)
|
||||||
|
|
||||||
|
|
||||||
# Example spider using this middleware
|
# Example spider using this middleware
|
||||||
class MySpider(BaseSpider):
|
class MySpider(BaseSpider):
|
||||||
|
|
||||||
middlewares = [CanonicalizeUrl()]
|
middlewares = [CanonicalizeUrl()]
|
||||||
canonicalization_rules = ['sort-query-args',
|
canonicalization_rules = ["sort-query-args", "normalize-percent-encoding", ...]
|
||||||
'normalize-percent-encoding', ...]
|
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
@ -343,23 +345,23 @@ Setting item identifier
|
||||||
Another example could be for setting a unique identifier to items, based on
|
Another example could be for setting a unique identifier to items, based on
|
||||||
certain fields:
|
certain fields:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class ItemIdSetter(object):
|
class ItemIdSetter(object):
|
||||||
|
|
||||||
def process_item(self, item, response, spider):
|
def process_item(self, item, response, spider):
|
||||||
id_field = spider.id_field
|
id_field = spider.id_field
|
||||||
id_fields_to_hash = spider.id_fields_to_hash
|
id_fields_to_hash = spider.id_fields_to_hash
|
||||||
item[id_field] = make_hash_based_on_fields(item, id_fields_to_hash)
|
item[id_field] = make_hash_based_on_fields(item, id_fields_to_hash)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
# Example spider using this middleware
|
# Example spider using this middleware
|
||||||
class MySpider(BaseSpider):
|
class MySpider(BaseSpider):
|
||||||
|
|
||||||
middlewares = [ItemIdSetter()]
|
middlewares = [ItemIdSetter()]
|
||||||
id_field = 'guid'
|
id_field = "guid"
|
||||||
id_fields_to_hash = ['supplier_name', 'supplier_id']
|
id_fields_to_hash = ["supplier_name", "supplier_id"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
# extract item from response
|
# extract item from response
|
||||||
|
|
@ -370,11 +372,10 @@ robots.txt exclusion
|
||||||
|
|
||||||
A spider middleware to avoid visiting pages forbidden by robots.txt:
|
A spider middleware to avoid visiting pages forbidden by robots.txt:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SpiderInfo(object):
|
class SpiderInfo(object):
|
||||||
|
|
||||||
def __init__(self, useragent):
|
def __init__(self, useragent):
|
||||||
self.useragent = useragent
|
self.useragent = useragent
|
||||||
self.parsers = {}
|
self.parsers = {}
|
||||||
|
|
@ -382,7 +383,6 @@ A spider middleware to avoid visiting pages forbidden by robots.txt:
|
||||||
|
|
||||||
|
|
||||||
class AllowAllParser(object):
|
class AllowAllParser(object):
|
||||||
|
|
||||||
def can_fetch(useragent, url):
|
def can_fetch(useragent, url):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -397,7 +397,7 @@ A spider middleware to avoid visiting pages forbidden by robots.txt:
|
||||||
dispatcher.connect(self.spider_closed, signal=signals.spider_closed)
|
dispatcher.connect(self.spider_closed, signal=signals.spider_closed)
|
||||||
|
|
||||||
def process_request(self, request, response, spider):
|
def process_request(self, request, response, spider):
|
||||||
return self.process_start_request(self, request)
|
return self.process_start_request(request)
|
||||||
|
|
||||||
def process_start_request(self, request, spider):
|
def process_start_request(self, request, spider):
|
||||||
info = self.spiders[spider]
|
info = self.spiders[spider]
|
||||||
|
|
@ -415,17 +415,21 @@ A spider middleware to avoid visiting pages forbidden by robots.txt:
|
||||||
res = None
|
res = None
|
||||||
else:
|
else:
|
||||||
robotsurl = "%s://%s/robots.txt" % (url.scheme, netloc)
|
robotsurl = "%s://%s/robots.txt" % (url.scheme, netloc)
|
||||||
meta = {'spider': spider, {'handle_httpstatus_list': [403, 404, 500]}
|
meta = {"spider": spider, "handle_httpstatus_list": [403, 404, 500]}
|
||||||
res = Request(robotsurl, callback=self.parse_robots,
|
res = Request(
|
||||||
meta=meta, priority=self.REQUEST_PRIORITY)
|
robotsurl,
|
||||||
|
callback=self.parse_robots,
|
||||||
|
meta=meta,
|
||||||
|
priority=self.REQUEST_PRIORITY,
|
||||||
|
)
|
||||||
info.pending[netloc].append(request)
|
info.pending[netloc].append(request)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def parse_robots(self, response):
|
def parse_robots(self, response):
|
||||||
spider = response.request.meta['spider']
|
spider = response.request.meta["spider"]
|
||||||
netloc urlparse_cached(response).netloc
|
netloc = urlparse_cached(response).netloc
|
||||||
info = self.spiders[spider]
|
info = self.spiders[spider]
|
||||||
if response.status 200;
|
if response.status == 200:
|
||||||
rp = robotparser.RobotFileParser(response.url)
|
rp = robotparser.RobotFileParser(response.url)
|
||||||
rp.parse(response.body.splitlines())
|
rp.parse(response.body.splitlines())
|
||||||
info.parsers[netloc] = rp
|
info.parsers[netloc] = rp
|
||||||
|
|
@ -434,7 +438,7 @@ A spider middleware to avoid visiting pages forbidden by robots.txt:
|
||||||
return info.pending[netloc]
|
return info.pending[netloc]
|
||||||
|
|
||||||
def spider_opened(self, spider):
|
def spider_opened(self, spider):
|
||||||
ua = getattr(spider, 'user_agent', None) or settings['USER_AGENT']
|
ua = getattr(spider, "user_agent", None) or settings["USER_AGENT"]
|
||||||
self.spiders[spider] = SpiderInfo(ua)
|
self.spiders[spider] = SpiderInfo(ua)
|
||||||
|
|
||||||
def spider_closed(self, spider):
|
def spider_closed(self, spider):
|
||||||
|
|
@ -445,18 +449,16 @@ Offsite middleware
|
||||||
|
|
||||||
This is a port of the Offsite middleware to the new spider middleware API:
|
This is a port of the Offsite middleware to the new spider middleware API:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SpiderInfo(object):
|
class SpiderInfo(object):
|
||||||
|
|
||||||
def __init__(self, host_regex):
|
def __init__(self, host_regex):
|
||||||
self.host_regex = host_regex
|
self.host_regex = host_regex
|
||||||
self.hosts_seen = set()
|
self.hosts_seen = set()
|
||||||
|
|
||||||
|
|
||||||
class OffsiteMiddleware(object):
|
class OffsiteMiddleware(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.spiders = {}
|
self.spiders = {}
|
||||||
dispatcher.connect(self.spider_opened, signal=signals.spider_opened)
|
dispatcher.connect(self.spider_opened, signal=signals.spider_opened)
|
||||||
|
|
@ -472,19 +474,19 @@ This is a port of the Offsite middleware to the new spider middleware API:
|
||||||
info = self.spiders[spider]
|
info = self.spiders[spider]
|
||||||
host = urlparse_cached(x).hostname
|
host = urlparse_cached(x).hostname
|
||||||
if host and host not in info.hosts_seen:
|
if host and host not in info.hosts_seen:
|
||||||
spider.log("Filtered offsite request to %r: %s" % (host, request))
|
spider.log("Filtered offsite request to %r: %s" % (host, request))
|
||||||
info.hosts_seen.add(host)
|
info.hosts_seen.add(host)
|
||||||
|
|
||||||
def should_follow(self, request, spider):
|
def should_follow(self, request, spider):
|
||||||
info = self.spiders[spider]
|
info = self.spiders[spider]
|
||||||
# hostname can be None for wrong urls (like javascript links)
|
# hostname can be None for wrong urls (like javascript links)
|
||||||
host = urlparse_cached(request).hostname or ''
|
host = urlparse_cached(request).hostname or ""
|
||||||
return bool(info.regex.search(host))
|
return bool(info.regex.search(host))
|
||||||
|
|
||||||
def get_host_regex(self, spider):
|
def get_host_regex(self, spider):
|
||||||
"""Override this method to implement a different offsite policy"""
|
"""Override this method to implement a different offsite policy"""
|
||||||
domains = [d.replace('.', r'\.') for d in spider.allowed_domains]
|
domains = [d.replace(".", r"\.") for d in spider.allowed_domains]
|
||||||
regex = r'^(.*\.)?(%s)$' % '|'.join(domains)
|
regex = r"^(.*\.)?(%s)$" % "|".join(domains)
|
||||||
return re.compile(regex)
|
return re.compile(regex)
|
||||||
|
|
||||||
def spider_opened(self, spider):
|
def spider_opened(self, spider):
|
||||||
|
|
@ -499,35 +501,36 @@ Limit URL length
|
||||||
|
|
||||||
A middleware to filter out requests with long urls:
|
A middleware to filter out requests with long urls:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
|
|
||||||
class LimitUrlLength(object):
|
|
||||||
|
|
||||||
|
class LimitUrlLength(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.maxlength = settings.getint('URLLENGTH_LIMIT')
|
self.maxlength = settings.getint("URLLENGTH_LIMIT")
|
||||||
|
|
||||||
def process_request(self, request, response, spider):
|
def process_request(self, request, response, spider):
|
||||||
return self.process_start_request(self, request)
|
return self.process_start_request(self, request)
|
||||||
|
|
||||||
def process_start_request(self, request, spider):
|
def process_start_request(self, request, spider):
|
||||||
if len(request.url) <= self.maxlength:
|
if len(request.url) <= self.maxlength:
|
||||||
return request
|
return request
|
||||||
spider.log("Ignoring request (url length > %d): %s " % (self.maxlength, request.url))
|
spider.log(
|
||||||
|
"Ignoring request (url length > %d): %s " % (self.maxlength, request.url)
|
||||||
|
)
|
||||||
|
|
||||||
Set Referer
|
Set Referer
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
A middleware to set the Referer:
|
A middleware to set the Referer:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SetReferer(object):
|
class SetReferer(object):
|
||||||
|
|
||||||
def process_request(self, request, response, spider):
|
def process_request(self, request, response, spider):
|
||||||
request.headers.setdefault('Referer', response.url)
|
request.headers.setdefault("Referer", response.url)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
Set and limit crawling depth
|
Set and limit crawling depth
|
||||||
|
|
@ -536,23 +539,22 @@ Set and limit crawling depth
|
||||||
A middleware to set (and limit) the request/response depth, taken from the
|
A middleware to set (and limit) the request/response depth, taken from the
|
||||||
start requests:
|
start requests:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class SetLimitDepth(object):
|
class SetLimitDepth(object):
|
||||||
|
|
||||||
def __init__(self, maxdepth=0):
|
def __init__(self, maxdepth=0):
|
||||||
self.maxdepth = maxdepth or settings.getint('DEPTH_LIMIT')
|
self.maxdepth = maxdepth or settings.getint("DEPTH_LIMIT")
|
||||||
|
|
||||||
def process_request(self, request, response, spider):
|
def process_request(self, request, response, spider):
|
||||||
depth = response.request.meta['depth'] + 1
|
depth = response.request.meta["depth"] + 1
|
||||||
request.meta['depth'] = depth
|
request.meta["depth"] = depth
|
||||||
if not self.maxdepth or depth <= self.maxdepth:
|
if not self.maxdepth or depth <= self.maxdepth:
|
||||||
return request
|
return request
|
||||||
spider.log("Ignoring link (depth > %d): %s " % (self.maxdepth, request)
|
spider.log("Ignoring link (depth > %d): %s " % (self.maxdepth, request))
|
||||||
|
|
||||||
def process_start_request(self, request, spider):
|
def process_start_request(self, request, spider):
|
||||||
request.meta['depth'] = 0
|
request.meta["depth"] = 0
|
||||||
return request
|
return request
|
||||||
|
|
||||||
Filter duplicate requests
|
Filter duplicate requests
|
||||||
|
|
@ -560,17 +562,16 @@ Filter duplicate requests
|
||||||
|
|
||||||
A middleware to filter out requests already seen:
|
A middleware to filter out requests already seen:
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
class FilterDuplicates(object):
|
class FilterDuplicates(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
clspath = settings.get('DUPEFILTER_CLASS')
|
clspath = settings.get("DUPEFILTER_CLASS")
|
||||||
self.dupefilter = load_object(clspath)()
|
self.dupefilter = load_object(clspath)()
|
||||||
dispatcher.connect(self.spider_opened, signal=signals.spider_opened)
|
dispatcher.connect(self.spider_opened, signal=signals.spider_opened)
|
||||||
dispatcher.connect(self.spider_closed, signal=signals.spider_closed)
|
dispatcher.connect(self.spider_closed, signal=signals.spider_closed)
|
||||||
|
|
||||||
def enqueue_request(self, spider, request):
|
def enqueue_request(self, spider, request):
|
||||||
seen = self.dupefilter.request_seen(spider, request)
|
seen = self.dupefilter.request_seen(spider, request)
|
||||||
if not seen or request.dont_filter:
|
if not seen or request.dont_filter:
|
||||||
|
|
@ -587,22 +588,25 @@ Scrape data using Parsley
|
||||||
|
|
||||||
A middleware to Scrape data using Parsley as described in UsingParsley
|
A middleware to Scrape data using Parsley as described in UsingParsley
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
#!python
|
#!python
|
||||||
from pyparsley import PyParsley
|
from pyparsley import PyParsley
|
||||||
|
|
||||||
class ParsleyExtractor(object):
|
|
||||||
|
|
||||||
|
class ParsleyExtractor(object):
|
||||||
def __init__(self, parsley_json_code):
|
def __init__(self, parsley_json_code):
|
||||||
parsley = json.loads(parselet_json_code)
|
parsley = json.loads(parselet_json_code)
|
||||||
|
|
||||||
class ParsleyItem(Item):
|
class ParsleyItem(Item):
|
||||||
def __init__(self, *a, **kw):
|
def __init__(self, *a, **kw):
|
||||||
for name in parsley.keys():
|
for name in parsley.keys():
|
||||||
self.fields[name] = Field()
|
self.fields[name] = Field()
|
||||||
|
|
||||||
super(ParsleyItem, self).__init__(*a, **kw)
|
super(ParsleyItem, self).__init__(*a, **kw)
|
||||||
self.item_class = ParsleyItem
|
|
||||||
self.parsley = PyParsley(parsley, output='python')
|
self.item_class = ParsleyItem
|
||||||
|
self.parsley = PyParsley(parsley, output="python")
|
||||||
|
|
||||||
def process_response(self, response, request, spider):
|
def process_response(self, response, request, spider):
|
||||||
return self.item_class(self.parsley.parse(string=response.body))
|
return self.item_class(self.parsley.parse(string=response.body))
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,11 @@ consistent way, while taking the chance to refactor the settings population
|
||||||
and whole crawl workflow.
|
and whole crawl workflow.
|
||||||
|
|
||||||
In short, you will be able to overwrite settings (on a per-spider basis) by
|
In short, you will be able to overwrite settings (on a per-spider basis) by
|
||||||
implementing a class method in your spider::
|
implementing a class method in your spider:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(Spider):
|
class MySpider(Spider):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_settings(cls):
|
def custom_settings(cls):
|
||||||
return {
|
return {
|
||||||
|
|
@ -197,10 +198,11 @@ Spiders
|
||||||
|
|
||||||
A new class method ``custom_settings`` is proposed, that could be use to
|
A new class method ``custom_settings`` is proposed, that could be use to
|
||||||
override project and default settings before they're used to instantiate the
|
override project and default settings before they're used to instantiate the
|
||||||
crawler::
|
crawler:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MySpider(Spider):
|
class MySpider(Spider):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_settings(cls):
|
def custom_settings(cls):
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -54,18 +54,18 @@ required.
|
||||||
Before
|
Before
|
||||||
------
|
------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
xpath = '//div[@class="geeks"]/dl/dt[contains(text(),"%s")]/following-sibling::dd[1]//text()'
|
xpath = '//div[@class="geeks"]/dl/dt[contains(text(),"%s")]/following-sibling::dd[1]//text()'
|
||||||
gl = XPathItemLoader(response=response, item=dict())
|
gl = XPathItemLoader(response=response, item=dict())
|
||||||
gl.default_output_processor = Compose(TakeFirst(), lambda v: v.strip())
|
gl.default_output_processor = Compose(TakeFirst(), lambda v: v.strip())
|
||||||
gl.add_xpath('hacker', xpath % 'hacker')
|
gl.add_xpath("hacker", xpath % "hacker")
|
||||||
gl.add_xpath('nerd', xpath % 'nerd')
|
gl.add_xpath("nerd", xpath % "nerd")
|
||||||
|
|
||||||
After
|
After
|
||||||
-----
|
-----
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
bil = BulkItemLoader(response=response)
|
bil = BulkItemLoader(response=response)
|
||||||
bil.parse_dl('//div[@class="geeks"]/dl')
|
bil.parse_dl('//div[@class="geeks"]/dl')
|
||||||
|
|
@ -75,33 +75,34 @@ Code Proposal
|
||||||
|
|
||||||
This is a working code sample that covers just the basics.
|
This is a working code sample that covers just the basics.
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.contrib.loader import XPathItemLoader
|
from scrapy.contrib.loader import XPathItemLoader
|
||||||
from scrapy.contrib.loader.processor import MapCompose
|
from scrapy.contrib.loader.processor import MapCompose
|
||||||
|
|
||||||
|
|
||||||
class BulkItemLoader(XPathItemLoader):
|
class BulkItemLoader(XPathItemLoader):
|
||||||
""" Item loader based on specified pattern recognition
|
"""Item loader based on specified pattern recognition"""
|
||||||
"""
|
|
||||||
default_item_class = dict
|
default_item_class = dict
|
||||||
base_xpath = '//body'
|
base_xpath = "//body"
|
||||||
ignore = ()
|
ignore = ()
|
||||||
|
|
||||||
def _get_label(self, entity):
|
def _get_label(self, entity):
|
||||||
""" Pull the text label out of selected markup
|
"""Pull the text label out of selected markup
|
||||||
|
|
||||||
:param entity: Found markup
|
:param entity: Found markup
|
||||||
:type entity: Selector
|
:type entity: Selector
|
||||||
"""
|
"""
|
||||||
label = ' '.join(entity.xpath('.//text()').extract())
|
label = " ".join(entity.xpath(".//text()").extract())
|
||||||
label = label.encode('ascii', 'xmlcharrefreplace') if label else ''
|
label = label.encode("ascii", "xmlcharrefreplace") if label else ""
|
||||||
label = label.strip(' ') if ' ' in label else label
|
label = label.strip(" ") if " " in label else label
|
||||||
label = label.strip(':') if ':' in label else label
|
label = label.strip(":") if ":" in label else label
|
||||||
label = label.strip()
|
label = label.strip()
|
||||||
return label
|
return label
|
||||||
|
|
||||||
def _get_entities(self, xpath):
|
def _get_entities(self, xpath):
|
||||||
""" Retrieve the list of selectors for a given sub-pattern
|
"""Retrieve the list of selectors for a given sub-pattern
|
||||||
|
|
||||||
:param xpath: The xpath to select
|
:param xpath: The xpath to select
|
||||||
:type xpath: String
|
:type xpath: String
|
||||||
|
|
@ -110,20 +111,21 @@ This is a working code sample that covers just the basics.
|
||||||
"""
|
"""
|
||||||
return self.selector.xpath(self.base_xpath + xpath)
|
return self.selector.xpath(self.base_xpath + xpath)
|
||||||
|
|
||||||
def parse_dl(self, xpath=u'//dl'):
|
def parse_dl(self, xpath="//dl"):
|
||||||
""" Look for the specified definition list pattern and store all found
|
"""Look for the specified definition list pattern and store all found
|
||||||
values for the enclosed terms and descriptions.
|
values for the enclosed terms and descriptions.
|
||||||
|
|
||||||
:param xpath: The xpath to select
|
:param xpath: The xpath to select
|
||||||
:type xpath: String
|
:type xpath: String
|
||||||
"""
|
"""
|
||||||
for term in self._get_entities(xpath + '/dt'):
|
for term in self._get_entities(xpath + "/dt"):
|
||||||
label = self._get_label(term)
|
label = self._get_label(term)
|
||||||
if label and label not in self.ignore:
|
if label and label not in self.ignore:
|
||||||
value = term.xpath('following-sibling::dd[1]//text()')
|
value = term.xpath("following-sibling::dd[1]//text()")
|
||||||
if value:
|
if value:
|
||||||
self.add_value(label, value.extract(),
|
self.add_value(
|
||||||
MapCompose(lambda v: v.strip()))
|
label, value.extract(), MapCompose(lambda v: v.strip())
|
||||||
|
)
|
||||||
|
|
||||||
Example Spider
|
Example Spider
|
||||||
==============
|
==============
|
||||||
|
|
@ -133,22 +135,24 @@ This spider uses the bulk loader above.
|
||||||
Spider code
|
Spider code
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.spider import BaseSpider
|
from scrapy.spider import BaseSpider
|
||||||
from scrapy.contrib.loader.bulk import BulkItemLoader
|
from scrapy.contrib.loader.bulk import BulkItemLoader
|
||||||
|
|
||||||
|
|
||||||
class W3cSpider(BaseSpider):
|
class W3cSpider(BaseSpider):
|
||||||
name = "w3c"
|
name = "w3c"
|
||||||
allowed_domains = ["w3.org"]
|
allowed_domains = ["w3.org"]
|
||||||
start_urls = ('http://www.w3.org/TR/html401/struct/lists.html',)
|
start_urls = ("http://www.w3.org/TR/html401/struct/lists.html",)
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
el = BulkItemLoader(response=response)
|
el = BulkItemLoader(response=response)
|
||||||
el.parse_dl('//dl[2]')
|
el.parse_dl("//dl[2]")
|
||||||
item = el.load_item()
|
item = el.load_item()
|
||||||
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
pprint(item)
|
pprint(item)
|
||||||
|
|
||||||
Log Output
|
Log Output
|
||||||
|
|
|
||||||
|
|
@ -76,14 +76,18 @@ addon_configure
|
||||||
Receives the Settings object and modifies it to enable the required components.
|
Receives the Settings object and modifies it to enable the required components.
|
||||||
If it raises an exception, Scrapy will print it and exit.
|
If it raises an exception, Scrapy will print it and exit.
|
||||||
|
|
||||||
Examples::
|
Examples:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def addon_configure(settings):
|
def addon_configure(settings):
|
||||||
settings.overrides['DOWNLOADER_MIDDLEWARES'].update({
|
settings.overrides["DOWNLOADER_MIDDLEWARES"].update(
|
||||||
'scrapy.contrib.downloadermiddleware.httpcache.HttpCacheMiddleware': 900,
|
{
|
||||||
})
|
"scrapy.contrib.downloadermiddleware.httpcache.HttpCacheMiddleware": 900,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
::
|
.. code-block:: python
|
||||||
|
|
||||||
def addon_configure(settings):
|
def addon_configure(settings):
|
||||||
try:
|
try:
|
||||||
|
|
@ -100,8 +104,10 @@ is meant to be used to perform post-initialization checks like making sure the
|
||||||
extension and its dependencies were configured properly. If it raises an
|
extension and its dependencies were configured properly. If it raises an
|
||||||
exception, Scrapy will print and exit.
|
exception, Scrapy will print and exit.
|
||||||
|
|
||||||
Examples::
|
Examples:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
def crawler_ready(crawler):
|
def crawler_ready(crawler):
|
||||||
if 'some.other.addon' not in crawler.extensions.enabled:
|
if "some.other.addon" not in crawler.extensions.enabled:
|
||||||
raise RuntimeError("Some other addon is required to use this addon")
|
raise RuntimeError("Some other addon is required to use this addon")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue