|
8 | 8 | from scrapy import Spider |
9 | 9 | from scrapy.http.headers import Headers |
10 | 10 | from scrapy.utils.misc import load_object |
11 | | -from scrapy.utils.request import request_from_dict |
| 11 | +from scrapy.utils.request import RequestFingerprinter, request_from_dict |
12 | 12 |
|
13 | 13 | from crawlee._request import UserData |
14 | 14 | from crawlee._types import HttpHeaders |
@@ -61,23 +61,28 @@ def to_apify_request(scrapy_request: ScrapyRequest, spider: Spider) -> ApifyRequ |
61 | 61 | logger.warning('Failed to convert to Apify request: Scrapy request must be a ScrapyRequest instance.') |
62 | 62 | return None |
63 | 63 |
|
64 | | - # Configuration to behave as similarly as possible to Scrapy's default RFPDupeFilter. |
65 | | - # |
66 | | - # The body is stored twice on purpose: as `payload` (used for the extended unique key) and inside the serialized |
67 | | - # Scrapy request below (used to reconstruct it). Both come from `scrapy_request.body`. |
| 64 | + # The body is stored as `payload` for the Apify-platform view of the request; the authoritative copy used to |
| 65 | + # reconstruct the Scrapy request travels inside the serialized blob below. Both come from `scrapy_request.body`. |
68 | 66 | request_kwargs: dict[str, Any] = { |
69 | 67 | 'url': scrapy_request.url, |
70 | 68 | 'method': scrapy_request.method, |
71 | 69 | 'payload': scrapy_request.body, |
72 | | - 'use_extended_unique_key': True, |
73 | | - 'keep_url_fragment': False, |
74 | 70 | } |
75 | 71 |
|
76 | 72 | try: |
77 | 73 | if scrapy_request.dont_filter: |
78 | 74 | request_kwargs['always_enqueue'] = True |
79 | 75 | elif scrapy_request.meta.get('apify_request_unique_key'): |
80 | 76 | request_kwargs['unique_key'] = scrapy_request.meta['apify_request_unique_key'] |
| 77 | + else: |
| 78 | + # Deduplicate exactly like Scrapy's own RFPDupeFilter, whose fingerprint canonicalizes the URL |
| 79 | + # case-sensitively (`w3lib.url.canonicalize_url`), keeps `utm_*` params, and ignores headers. Left to |
| 80 | + # Crawlee, the unique key would instead come from `normalize_url`, which lowercases the whole URL and |
| 81 | + # strips `utm_*` — silently collapsing distinct pages Scrapy would crawl — while its extended key hashes |
| 82 | + # headers Scrapy ignores. A custom `REQUEST_FINGERPRINTER_CLASS` is honored when the spider has a crawler. |
| 83 | + crawler = getattr(spider, 'crawler', None) |
| 84 | + fingerprinter = getattr(crawler, 'request_fingerprinter', None) or RequestFingerprinter() |
| 85 | + request_kwargs['unique_key'] = fingerprinter.fingerprint(scrapy_request).hex() |
81 | 86 |
|
82 | 87 | # Serialize the Scrapy request now, before `Request.from_url()` runs below. `from_url()` mutates the |
83 | 88 | # `user_data` dict it receives in place (it injects a live `CrawleeRequestData` under `__crawlee`), and that |
@@ -105,12 +110,8 @@ def to_apify_request(scrapy_request: ScrapyRequest, spider: Spider) -> ApifyRequ |
105 | 110 |
|
106 | 111 | # Store an Apify-platform view of the headers. The authoritative copy with exact bytes travels in |
107 | 112 | # the serialized scrapy_request below, so non-UTF-8 headers (which make `to_unicode_dict()` raise) are |
108 | | - # tolerated rather than dropping the whole request. |
109 | | - # |
110 | | - # Trade-off: with `use_extended_unique_key=True` the unique key includes the headers, so when non-UTF-8 |
111 | | - # headers are omitted here two requests differing only in those headers share a unique key and one is |
112 | | - # deduplicated away. This is rare (header values are normally ASCII/UTF-8) and still strictly better than |
113 | | - # the old behavior, which dropped such requests entirely. |
| 113 | + # omitted here rather than dropping the whole request. Dedup is unaffected: the unique key comes from |
| 114 | + # Scrapy's fingerprint, which ignores headers. |
114 | 115 | if isinstance(scrapy_request.headers, Headers): |
115 | 116 | try: |
116 | 117 | headers = cast('dict[str, str]', dict(scrapy_request.headers.to_unicode_dict())) |
|
0 commit comments