diff --git a/scrapy/utils/sitemap.py b/scrapy/utils/sitemap.py index 1520a4ff0..03b7bf3b1 100644 --- a/scrapy/utils/sitemap.py +++ b/scrapy/utils/sitemap.py @@ -97,10 +97,11 @@ class Sitemap: @staticmethod def _get_tag_name(elem: lxml.etree._Element) -> str: - if TYPE_CHECKING: - assert isinstance(elem.tag, str) - _, _, localname = elem.tag.partition("}") - return localname or elem.tag + tag = elem.tag + if not isinstance(tag, str): + return "" + _, _, localname = tag.partition("}") + return localname or tag def sitemap_urls_from_robots( diff --git a/tests/test_utils_sitemap.py b/tests/test_utils_sitemap.py index 3599c4824..2ff916b42 100644 --- a/tests/test_utils_sitemap.py +++ b/tests/test_utils_sitemap.py @@ -311,3 +311,21 @@ def test_xml_entity_expansion(): """ ) assert list(s) == [{"loc": "http://127.0.0.1:8000/"}] + + +def test_sitemap_non_string_tag(): + """Regression test for non-string elem.tag crashing _get_tag_name. + + When parsing malformed XML with recover=True and resolve_entities=False, + libxml2 can produce nodes whose .tag is not a string (e.g. a Cython + function). _get_tag_name must handle this gracefully instead of raising + AttributeError. + """ + from base64 import b64decode + + data = b64decode( + "PHVybGluaz48dXI8dXJsPgAAAAAAAAAEaW5rIHhtbG5zOnhodG1sPSJoZGwiIGhyZWY9ImhsPjxsaW4mazsiaGxybGtuazx4aHRtbDpsaW5rIHhtbG5zOnhodG1sPSJoZGwiIGhyZWY9ImhsaW5rbCIgaHJlZj0iaGw+PHhodG1sZGxzZXQ+" + ) + # Must not raise AttributeError + results = list(Sitemap(data)) + assert isinstance(results, list)