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()