Skip to content

Commit df1178c

Browse files
authored
fix(crawlers): Parse HTML responses starting with an XML declaration as HTML in ParselCrawler (#2262)
### Description - `ParselCrawler` now parses `text/html` and `application/xhtml+xml` responses as HTML, even when the page starts with an XML declaration. Since `parsel` 1.12, such pages were parsed as XML, so CSS selectors and `enqueue_links` found nothing on XHTML pages.
1 parent 3da1880 commit df1178c

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

‎src/crawlee/crawlers/_parsel/_parsel_parser.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ def find_links(self, parsed_content: Selector, selector: str, attribute: str) ->
5050

5151
@staticmethod
5252
def _parse_body(body: bytes, content_type: str | None) -> Selector:
53+
media_type = (content_type or '').split(';')[0].strip().lower()
54+
# Other responses keep Parsel's own detection of JSON, XML and HTML.
55+
selector_type = 'html' if media_type in {'text/html', 'application/xhtml+xml'} else None
56+
5357
encoding = get_declared_html_encoding(body, content_type)
5458
if encoding is None:
55-
return Selector(body=body)
56-
return Selector(text=decode_html_body(body, encoding))
59+
return Selector(body=body, type=selector_type)
60+
return Selector(text=decode_html_body(body, encoding), type=selector_type)

‎tests/unit/crawlers/_parsel/test_parsel_crawler.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,3 +548,24 @@ async def test_parse_decodes_page_encoding(texts: list[str], body: bytes, conten
548548
selector = await ParselParser().parse(response)
549549

550550
assert selector.css('p::text').getall() == texts
551+
552+
553+
@pytest.mark.parametrize(
554+
('content_type', 'expected_type'),
555+
[
556+
pytest.param('text/html', 'html', id='html'),
557+
pytest.param('application/xhtml+xml; charset=utf-8', 'html', id='xhtml'),
558+
pytest.param('application/xml', 'xml', id='xml'),
559+
],
560+
)
561+
async def test_parse_xml_declaration(content_type: str, expected_type: str) -> None:
562+
"""A page starting with an XML declaration is read as HTML unless the response is XML."""
563+
body = (
564+
b'<?xml version="1.0" encoding="UTF-8"?>'
565+
b'<html xmlns="http://www.w3.org/1999/xhtml"><body><a href="/next">Next</a></body></html>'
566+
)
567+
response = mock.Mock(headers=HttpHeaders({'Content-Type': content_type}), read=mock.AsyncMock(return_value=body))
568+
569+
selector = await ParselParser().parse(response)
570+
571+
assert selector.type == expected_type

0 commit comments

Comments
 (0)