mirror of https://github.com/scrapy/scrapy.git
Drop AjaxCrawlMiddleware and escape_ajax().
This commit is contained in:
parent
8ecfd20fcd
commit
af7dcabebb
|
|
@ -809,7 +809,6 @@ Default:
|
|||
"scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware": 400,
|
||||
"scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": 500,
|
||||
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
|
||||
"scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware": 560,
|
||||
"scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware": 580,
|
||||
"scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware": 590,
|
||||
"scrapy.downloadermiddlewares.redirect.RedirectMiddleware": 600,
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
from warnings import warn
|
||||
|
||||
from w3lib import html
|
||||
|
||||
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
|
||||
from scrapy.http import HtmlResponse, Response
|
||||
from scrapy.utils.url import escape_ajax
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# typing.Self requires Python 3.11
|
||||
from typing_extensions import Self
|
||||
|
||||
from scrapy import Request, Spider
|
||||
from scrapy.crawler import Crawler
|
||||
from scrapy.settings import BaseSettings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AjaxCrawlMiddleware:
|
||||
"""
|
||||
Handle 'AJAX crawlable' pages marked as crawlable via meta tag.
|
||||
"""
|
||||
|
||||
def __init__(self, settings: BaseSettings):
|
||||
if not settings.getbool("AJAXCRAWL_ENABLED"):
|
||||
raise NotConfigured
|
||||
|
||||
warn(
|
||||
"scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware is deprecated"
|
||||
" and will be removed in a future Scrapy version.",
|
||||
ScrapyDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# XXX: Google parses at least first 100k bytes; scrapy's redirect
|
||||
# middleware parses first 4k. 4k turns out to be insufficient
|
||||
# for this middleware, and parsing 100k could be slow.
|
||||
# We use something in between (32K) by default.
|
||||
self.lookup_bytes: int = settings.getint("AJAXCRAWL_MAXSIZE")
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||
return cls(crawler.settings)
|
||||
|
||||
def process_response(
|
||||
self, request: Request, response: Response, spider: Spider
|
||||
) -> Request | Response:
|
||||
if not isinstance(response, HtmlResponse) or response.status != 200:
|
||||
return response
|
||||
|
||||
if request.method != "GET":
|
||||
# other HTTP methods are either not safe or don't have a body
|
||||
return response
|
||||
|
||||
if "ajax_crawlable" in request.meta: # prevent loops
|
||||
return response
|
||||
|
||||
if not self._has_ajax_crawlable_variant(response):
|
||||
return response
|
||||
|
||||
ajax_crawl_request = request.replace(url=escape_ajax(request.url + "#!"))
|
||||
logger.debug(
|
||||
"Downloading AJAX crawlable %(ajax_crawl_request)s instead of %(request)s",
|
||||
{"ajax_crawl_request": ajax_crawl_request, "request": request},
|
||||
extra={"spider": spider},
|
||||
)
|
||||
|
||||
ajax_crawl_request.meta["ajax_crawlable"] = True
|
||||
return ajax_crawl_request
|
||||
|
||||
def _has_ajax_crawlable_variant(self, response: Response) -> bool:
|
||||
"""
|
||||
Return True if a page without hash fragment could be "AJAX crawlable".
|
||||
"""
|
||||
body = response.text[: self.lookup_bytes]
|
||||
return _has_ajaxcrawlable_meta(body)
|
||||
|
||||
|
||||
_ajax_crawlable_re: re.Pattern[str] = re.compile(
|
||||
r'<meta\s+name=["\']fragment["\']\s+content=["\']!["\']/?>'
|
||||
)
|
||||
|
||||
|
||||
def _has_ajaxcrawlable_meta(text: str) -> bool:
|
||||
"""
|
||||
>>> _has_ajaxcrawlable_meta('<html><head><meta name="fragment" content="!"/></head><body></body></html>')
|
||||
True
|
||||
>>> _has_ajaxcrawlable_meta("<html><head><meta name='fragment' content='!'></head></html>")
|
||||
True
|
||||
>>> _has_ajaxcrawlable_meta('<html><head><!--<meta name="fragment" content="!"/>--></head><body></body></html>')
|
||||
False
|
||||
>>> _has_ajaxcrawlable_meta('<html></html>')
|
||||
False
|
||||
"""
|
||||
|
||||
# Stripping scripts and comments is slow (about 20x slower than
|
||||
# just checking if a string is in text); this is a quick fail-fast
|
||||
# path that should work for most pages.
|
||||
if "fragment" not in text:
|
||||
return False
|
||||
if "content" not in text:
|
||||
return False
|
||||
|
||||
text = html.remove_tags_with_content(text, ("script", "noscript"))
|
||||
text = html.replace_entities(text)
|
||||
text = html.remove_comments(text)
|
||||
return _ajax_crawlable_re.search(text) is not None
|
||||
|
|
@ -289,7 +289,6 @@ DOWNLOADER_MIDDLEWARES_BASE = {
|
|||
"scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware": 400,
|
||||
"scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": 500,
|
||||
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
|
||||
"scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware": 560,
|
||||
"scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware": 580,
|
||||
"scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware": 590,
|
||||
"scrapy.downloadermiddlewares.redirect.RedirectMiddleware": 600,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,9 @@ import re
|
|||
import warnings
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
from urllib.parse import ParseResult, urldefrag, urlparse, urlunparse
|
||||
from warnings import warn
|
||||
from urllib.parse import ParseResult, urlparse, urlunparse
|
||||
|
||||
from w3lib.url import __all__ as _public_w3lib_objects
|
||||
from w3lib.url import add_or_replace_parameter as _add_or_replace_parameter
|
||||
from w3lib.url import any_to_uri as _any_to_uri
|
||||
from w3lib.url import parse_url as _parse_url
|
||||
|
||||
|
|
@ -70,39 +68,6 @@ def url_has_any_extension(url: UrlT, extensions: Iterable[str]) -> bool:
|
|||
return any(lowercase_path.endswith(ext) for ext in extensions)
|
||||
|
||||
|
||||
def escape_ajax(url: str) -> str:
|
||||
"""
|
||||
Return the crawlable url
|
||||
|
||||
>>> escape_ajax("www.example.com/ajax.html#!key=value")
|
||||
'www.example.com/ajax.html?_escaped_fragment_=key%3Dvalue'
|
||||
>>> escape_ajax("www.example.com/ajax.html?k1=v1&k2=v2#!key=value")
|
||||
'www.example.com/ajax.html?k1=v1&k2=v2&_escaped_fragment_=key%3Dvalue'
|
||||
>>> escape_ajax("www.example.com/ajax.html?#!key=value")
|
||||
'www.example.com/ajax.html?_escaped_fragment_=key%3Dvalue'
|
||||
>>> escape_ajax("www.example.com/ajax.html#!")
|
||||
'www.example.com/ajax.html?_escaped_fragment_='
|
||||
|
||||
URLs that are not "AJAX crawlable" (according to Google) returned as-is:
|
||||
|
||||
>>> escape_ajax("www.example.com/ajax.html#key=value")
|
||||
'www.example.com/ajax.html#key=value'
|
||||
>>> escape_ajax("www.example.com/ajax.html#")
|
||||
'www.example.com/ajax.html#'
|
||||
>>> escape_ajax("www.example.com/ajax.html")
|
||||
'www.example.com/ajax.html'
|
||||
"""
|
||||
warn(
|
||||
"escape_ajax() is deprecated and will be removed in a future Scrapy version.",
|
||||
ScrapyDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
defrag, frag = urldefrag(url)
|
||||
if not frag.startswith("!"):
|
||||
return url
|
||||
return _add_or_replace_parameter(defrag, "_escaped_fragment_", frag[1:])
|
||||
|
||||
|
||||
def add_http_if_no_scheme(url: str) -> str:
|
||||
"""Add http as the default scheme if it is missing from the url."""
|
||||
match = re.match(r"^\w+://", url, flags=re.IGNORECASE)
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from scrapy.downloadermiddlewares.ajaxcrawl import AjaxCrawlMiddleware
|
||||
from scrapy.http import HtmlResponse, Request, Response
|
||||
from scrapy.spiders import Spider
|
||||
from scrapy.utils.test import get_crawler
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::scrapy.exceptions.ScrapyDeprecationWarning")
|
||||
class TestAjaxCrawlMiddleware:
|
||||
def setup_method(self):
|
||||
crawler = get_crawler(Spider, {"AJAXCRAWL_ENABLED": True})
|
||||
self.spider = crawler._create_spider("foo")
|
||||
self.mw = AjaxCrawlMiddleware.from_crawler(crawler)
|
||||
|
||||
def _ajaxcrawlable_body(self):
|
||||
return b'<html><head><meta name="fragment" content="!"/></head><body></body></html>'
|
||||
|
||||
def _req_resp(self, url, req_kwargs=None, resp_kwargs=None):
|
||||
req = Request(url, **(req_kwargs or {}))
|
||||
resp = HtmlResponse(url, request=req, **(resp_kwargs or {}))
|
||||
return req, resp
|
||||
|
||||
def test_non_get(self):
|
||||
req, resp = self._req_resp("http://example.com/", {"method": "HEAD"})
|
||||
resp2 = self.mw.process_response(req, resp, self.spider)
|
||||
assert resp == resp2
|
||||
|
||||
def test_binary_response(self):
|
||||
req = Request("http://example.com/")
|
||||
resp = Response("http://example.com/", body=b"foobar\x00\x01\x02", request=req)
|
||||
resp2 = self.mw.process_response(req, resp, self.spider)
|
||||
assert resp is resp2
|
||||
|
||||
def test_ajaxcrawl(self):
|
||||
req, resp = self._req_resp(
|
||||
"http://example.com/",
|
||||
{"meta": {"foo": "bar"}},
|
||||
{"body": self._ajaxcrawlable_body()},
|
||||
)
|
||||
req2 = self.mw.process_response(req, resp, self.spider)
|
||||
assert req2.url == "http://example.com/?_escaped_fragment_="
|
||||
assert req2.meta["foo"] == "bar"
|
||||
|
||||
def test_ajaxcrawl_loop(self):
|
||||
req, resp = self._req_resp(
|
||||
"http://example.com/", {}, {"body": self._ajaxcrawlable_body()}
|
||||
)
|
||||
req2 = self.mw.process_response(req, resp, self.spider)
|
||||
resp2 = HtmlResponse(req2.url, body=resp.body, request=req2)
|
||||
resp3 = self.mw.process_response(req2, resp2, self.spider)
|
||||
|
||||
assert isinstance(resp3, HtmlResponse), (resp3.__class__, resp3)
|
||||
assert resp3.request.url == "http://example.com/?_escaped_fragment_="
|
||||
assert resp3 is resp2
|
||||
|
||||
def test_noncrawlable_body(self):
|
||||
req, resp = self._req_resp(
|
||||
"http://example.com/", {}, {"body": b"<html></html>"}
|
||||
)
|
||||
resp2 = self.mw.process_response(req, resp, self.spider)
|
||||
assert resp is resp2
|
||||
Loading…
Reference in New Issue