diff --git a/src/agent.py b/src/agent.py index fee23fd..277fc57 100644 --- a/src/agent.py +++ b/src/agent.py @@ -331,14 +331,31 @@ class EvaClient(Http): return self._retrying(lambda: self.fetch_bytes(url[len(self.base):], limit)) +class _ParseError(Exception): + """Разбор HTML споткнулся (ParserBase.error в 3.9): см. _LinkParser.error.""" + + class _LinkParser(HTMLParser): - """Собирает img[src] и a[href] в порядке появления: (тег, значение атрибута после unescape, alt/title, текст ссылки).""" + """Собирает img[src] и a[href] в порядке появления: (тег, значение атрибута после unescape, alt/title, текст ссылки). + + На 3.9 `_markupbase` зовёт error() на `` и подобном, а базовый error() бросает NotImplementedError (I-1). + error() бросает _ParseError, а parse_marked_section пропускает такую конструкцию до `>`, как мусорный комментарий.""" def __init__(self): HTMLParser.__init__(self, convert_charrefs=True) self.items = [] # [tag, ref, hint, text] self._open = None # запись открытой , в неё копится текст + def error(self, message): + raise _ParseError(message) + + def parse_marked_section(self, i, report=1): + try: + return HTMLParser.parse_marked_section(self, i, report) + except _ParseError: + j = self.rawdata.find(">", i + 3) + return -1 if j < 0 else j + 1 # -1: ждать данных; при close() goahead сам пропустит до `>` + def handle_starttag(self, tag, attrs): a = dict(attrs) if tag == "img": @@ -374,12 +391,18 @@ def classify_links(html: str, base_url: str) -> Tuple[list, list]: ref — значение атрибута после unescape (design 12). Чужой хост и static/ не передаются.""" parser = _LinkParser() - parser.feed(html or "") - parser.close() + try: + parser.feed(html or "") + parser.close() + except (_ParseError, NotImplementedError, AssertionError) as e: # запасной путь: собранное до сбоя сохраняется + log(" разбор HTML прерван (%s: %s), ссылок до сбоя: %d" % (type(e).__name__, e, len(parser.items))) base = urllib.parse.urlsplit(base_url) attachments, links = [], [] for _tag, ref, hint, text in parser.items: - parts = urllib.parse.urlsplit(urllib.parse.urljoin(base_url.rstrip("/") + "/", ref.strip())) + try: + parts = urllib.parse.urlsplit(urllib.parse.urljoin(base_url.rstrip("/") + "/", ref.strip())) + except ValueError: # `http://[::1` — ссылка битая, пропускаем (I-1) + continue if (parts.scheme, parts.netloc) != (base.scheme, base.netloc): continue path = parts.path diff --git a/tests/test_links.py b/tests/test_links.py index fd9b7cd..b6ee710 100644 --- a/tests/test_links.py +++ b/tests/test_links.py @@ -78,6 +78,29 @@ class ClassifyLinksTest(unittest.TestCase): '' % (OBJ, OBJ, OBJ), BASE) self.assertEqual([a["name"] for a in att], ["a.bin", "b.png", "c.png"]) + def test_unknown_marked_section_does_not_crash(self): + """I-1: на 3.9 `` уходит в ParserBase.error() → NotImplementedError; ссылки до и после сохраняются.""" + html = ('

текст

x' + 'c' % (OBJ, OBJ)) + att, links = agent.classify_links(html, BASE) + self.assertEqual([a["name"] for a in att], ["a.png", "b.png"]) + self.assertEqual(links, [{"ref": "/project/Document/DOC-3", "code": "DOC-3"}]) + + def test_marked_section_bad_name_and_unclosed(self): + for html in ('' % (OBJ, OBJ), + 'x' + 'd' % OBJ) + att, links = agent.classify_links(html, BASE) + self.assertEqual([a["name"] for a in att], ["b.png"]) + self.assertEqual([l["code"] for l in links], ["DOC-4"]) + if __name__ == "__main__": unittest.main()