"""清洗规则单元测试。 fixtures 里是各脏数据模式的最小样例,跑一遍断言规则命中且正文无损。 """ import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from cleaner.cleaner import MarkdownCleaner from cleaner.rules import ( _drop_images, _normalize_tables, _rstrip_lines, _strip_page_lines, _strip_repeated_short_lines, _strip_toc_dots, load_rules, ) S = {} # 每个用例独立 stats def st(): return {} class TestPageLines(unittest.TestCase): def test_cn_page(self): text = "正文A\n第 3 页 共 53 页\n正文B\n第4页共4页\n结尾" out = _strip_page_lines(text, {}, st()) self.assertEqual(out, "正文A\n正文B\n结尾") def test_en_page(self): text = "foo\nPage 3 of 10\nbar\n- 4 -\nbaz" out = _strip_page_lines(text, {"keep_bare_numbers": False}, st()) self.assertNotIn("Page 3", out) self.assertNotIn("- 4 -", out) def test_bare_number_kept_by_default(self): text = "条款\n12\n下文" out = _strip_page_lines(text, {}, st()) self.assertIn("12", out) class TestRepeatedLines(unittest.TestCase): def test_header_removed(self): lines = ["某某采购项目招标文件"] + ["内容%d" % i for i in range(5)] text = "\n".join(("某某采购项目招标文件 \n" + l) for l in lines) out = _strip_repeated_short_lines(text, {"threshold": 3, "max_len": 40}, st()) self.assertNotIn("某某采购项目招标文件", out) self.assertIn("内容1", out) def test_protected_signature_kept(self): text = "\n".join(["投标人:(公章)"] * 4 + ["正文"]) out = _strip_repeated_short_lines( text, {"threshold": 3, "max_len": 40, "protect": ["公章"]}, st() ) self.assertIn("投标人:(公章)", out) def test_numbered_clause_kept(self): # 编号条款是正文不是页眉 text = "\n".join(["(1)乙方须接受甲方监督。"] * 4 + ["正文"]) out = _strip_repeated_short_lines(text, {"threshold": 3, "max_len": 40}, st()) self.assertIn("(1)乙方须接受甲方监督。", out) def test_ocr_burst_removed(self): text = "\n".join(["审计程序"] * 30) out = _strip_repeated_short_lines( text, {"threshold": 999, "max_len": 40, "burst_limit": 5, "burst_gap": 2}, st(), ) self.assertNotIn("审计程序", out) class TestTocDots(unittest.TestCase): def test_toc_line(self): text = "第一章 投标邀请函 ……………………………………… 2" out = _strip_toc_dots(text, {}, st()) self.assertEqual(out, "第一章 投标邀请函") def test_plain_toc_line_keeps_text(self): # 无标题目录行:去掉点线页码,保留"序号+标题"文字 text = "21 迷交的投标文件 ………………………………………………… 25" out = _strip_toc_dots(text, {}, st()) self.assertEqual(out, "21 迷交的投标文件") def test_body_with_ellipsis_kept(self): text = "此处省略部分内容……后续" out = _strip_toc_dots(text, {}, st()) self.assertIn("后续", out) class TestImages(unittest.TestCase): def test_image_line_dropped(self): text = "![](images/abc.jpg) \n正文" out = _drop_images(text, {}, st()) self.assertEqual(out, "正文") def test_placeholder(self): text = "![](images/abc.jpg)\n正文" out = _drop_images(text, {"placeholder": "[图]"}, st()) self.assertIn("[图]", out) def test_base64_image_dropped(self): text = "![名称](data:image/png;base64,AAAA)\n正文" out = _drop_images(text, {}, st()) self.assertNotIn("base64", out) class TestTables(unittest.TestCase): def test_table_to_pipe(self): html = "
序号名称
1保洁
" out = _normalize_tables(html, {}, st()) self.assertIn("| 序号 | 名称 |", out) self.assertIn("| 1 | 保洁 |", out) self.assertIn("|---|---|", out) def test_pipe_escaped(self): html = "
a|b
" out = _normalize_tables(html, {}, st()) self.assertIn("a\\|b", out) def test_br_in_cell(self): html = "

" out = _normalize_tables(html, {}, st()) self.assertIn("品 目", out) class TestEngine(unittest.TestCase): def test_full_pipeline(self): cleaner = MarkdownCleaner(rules=load_rules()) raw = ( "# 标题\n![](images/x.jpg) \n第 1 页 共 2 页 \n" "正文一段。 \n
a
\n\n\n\n尾部\n" ) result = cleaner.clean_text(raw) self.assertNotIn("images/", result.text) self.assertNotIn("第 1 页", result.text) self.assertNotIn("", result.text) self.assertIn("| a |", result.text) self.assertNotIn("\n\n\n", result.text) def test_default_rules_load(self): rules = load_rules() self.assertTrue(len(rules) >= 8) self.assertEqual(rules, sorted(rules, key=lambda r: r.order)) if __name__ == "__main__": unittest.main(verbosity=2)