From 2d616d01e8f7955cbf916ac5ea441be4b180e538 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 17:10:43 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=B4=E6=B3=95=E8=A1=A8=E7=9A=84=20scan=20?= =?UTF-8?q?=E5=8A=A0=20bare=3DTrue=EF=BC=9A=E5=8F=AA=E6=8C=89=E8=AF=8D?= =?UTF-8?q?=E8=AE=A4=E3=80=81=E4=B8=8D=E7=9C=8B=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=EF=BC=88=E6=9E=84=E6=88=90=E3=80=81=E5=BB=BA=E8=AE=AE=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E3=80=81=E4=B8=9A=E5=8A=A1=E7=8A=B6=E6=80=81=E3=80=81?= =?UTF-8?q?=E3=80=8C=E7=9B=B8=E5=BD=93=E3=80=8D=E7=9A=84=E6=AF=94=E6=B3=95?= =?UTF-8?q?=E3=80=81=E3=80=8C=E6=9C=AA=E8=A7=81=E5=BC=82=E5=B8=B8=E3=80=8D?= =?UTF-8?q?=E7=9A=84=E4=B8=BB=E8=AF=AD=E8=BF=99=E4=BA=9B=E5=8F=AA=E5=AF=B9?= =?UTF-8?q?=E6=95=B4=E5=8F=A5=E8=AF=9D=E6=88=90=E7=AB=8B=E7=9A=84=E8=A7=84?= =?UTF-8?q?=E7=9F=A9=E9=83=BD=E4=B8=8D=E7=94=A8=EF=BC=89=EF=BC=8C=E7=BB=99?= =?UTF-8?q?=E8=AE=A4=E5=8F=AB=E6=B3=95=EF=BC=88=E5=BA=A6=E9=87=8F=E7=9A=84?= =?UTF-8?q?=E5=8F=AB=E6=B3=95=E3=80=81=E5=9B=A0=E5=AD=90=E5=90=8D=EF=BC=89?= =?UTF-8?q?=E9=87=8C=E5=A4=B9=E5=B8=A6=E7=9A=84=E5=88=A4=E6=96=AD=E8=AF=8D?= =?UTF-8?q?=E7=94=A8=E2=80=94=E2=80=94=E5=8F=AB=E6=B3=95=E6=98=AF=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E5=90=8D=E5=AD=97=E4=B8=8D=E6=98=AF=E4=B8=80=E5=8F=A5?= =?UTF-8?q?=E8=AF=9D=EF=BC=8C=E3=80=8C=E4=B8=BB=E8=A6=81=E6=9D=A5=E8=87=AA?= =?UTF-8?q?=E5=8D=8E=E4=B8=9C=E7=9A=84=E9=94=80=E5=94=AE=E9=A2=9D=E3=80=8D?= =?UTF-8?q?=E7=85=A7=E6=A0=B7=E8=AE=A4=E5=87=BA=E6=9D=A5=E6=BA=90=EF=BC=9B?= =?UTF-8?q?=E7=BC=BA=E7=9C=81=E7=85=A7=E6=97=A7=E6=8C=89=E6=95=B4=E5=8F=A5?= =?UTF-8?q?=E7=9A=84=E8=A7=84=E7=9F=A9=E3=80=82=E5=8E=9F=E5=9B=A0=E5=8F=AA?= =?UTF-8?q?=E6=98=AF=E6=8C=87=E4=BB=A3=E7=9A=84=E9=82=A3=E6=9D=A1=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=94=B9=E5=87=86=E3=80=82=E6=B5=8B=E8=AF=95=EF=BC=9A?= =?UTF-8?q?=E5=87=A0=E4=B8=AA=E5=8F=AB=E6=B3=95=E6=8C=89=E8=AF=8D=E8=AE=A4?= =?UTF-8?q?=E3=80=81=E6=8C=89=E6=95=B4=E5=8F=A5=E4=B8=8D=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/engine/analysis_claims.py | 14 +++++++++----- backend/tests/test_analysis_claims_lexicon.py | 9 +++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/backend/app/engine/analysis_claims.py b/backend/app/engine/analysis_claims.py index 309f32ea..5ef1baeb 100644 --- a/backend/app/engine/analysis_claims.py +++ b/backend/app/engine/analysis_claims.py @@ -395,10 +395,13 @@ def _count(word: str) -> int | None: return (_CN_DIGITS.get(tens, 0) if tens else 1) * 10 + (_CN_DIGITS.get(ones, 0) if ones else 0) -def scan(text: str, *, version: int | None = None) -> list[Hit]: +def scan(text: str, *, version: int | None = None, bare: bool = False) -> list[Hit]: """一句话里的判断词(text 是写作者自己写的字,别的片段已经涂成 \\x00)。同一处有几种读法时取最靠前、最长的: 「超出正常范围」是异常不是正常,「没有明显的上升或下降趋势」是无趋势不是趋势。不核的写法(_counts)连同和它重叠的 - 读法一起跳过。""" + 读法一起跳过。 + + bare=True 只按词认、不看上下文:构成、建议计划、业务状态、「相当」的比法、「未见异常」的主语这些只对整句话成立的 + 规矩都不用——认叫法(度量的叫法、因子名)里夹带的判断词时用它,叫法是一个名字,不是一句话。""" lex = lexicon(version) found: list[tuple[int, int, int, Claim]] = [] for order, claim in enumerate(lex.claims): @@ -413,7 +416,7 @@ def scan(text: str, *, version: int | None = None) -> list[Hit]: continue taken = end left, right = _clause(text, start, end) - if not _counts(lex, claim, text, start, end, left, right): + if not bare and not _counts(lex, claim, text, start, end, left, right): continue before = text[left:start] key, negated = claim.key, False @@ -423,7 +426,8 @@ def scan(text: str, *, version: int | None = None) -> list[Hit]: continue key = claim.flip word = text[start:end] - if key == "normal" and (claim.subject or (negated and "异" in word)) and not lex.subject.search(text[:right]): + if not bare and key == "normal" and (claim.subject or (negated and "异" in word)) \ + and not lex.subject.search(text[:right]): continue # 「未见异常」「没有出现异动」:主语不是指标、波动的(运行平稳、系统正常)不核 direction = _direction(lex, word, text[end:right], text[left:right]) run = claim.run and not negated @@ -848,7 +852,7 @@ def _cause(sentence: Sentence, hit: Hit, left: int, right: int) -> str: return names[left + by[-1]:hit.start] if by else names[left:hit.start] -#: 原因只是指代(「以上是下降的主要原因」「原因如下:」后面的列表各自核):这一句不核 +#: 原因只是指代(「以上是下降的主要原因」「下降的主要原因:」,列出原因的那几句各自核):这一句不核 _POINTER = re.compile(r"(?:这|那|此|其|以上|上述|前述|如下|以下|下列)(?:些|个|几个|几点|两点|方面|原因|因素)?") _FILLING = re.compile(r"[\x00\s\W_]|的|都|也|就|主要|正是|便是|即") diff --git a/backend/tests/test_analysis_claims_lexicon.py b/backend/tests/test_analysis_claims_lexicon.py index bd841a8c..ad30351c 100644 --- a/backend/tests/test_analysis_claims_lexicon.py +++ b/backend/tests/test_analysis_claims_lexicon.py @@ -143,3 +143,12 @@ def test_the_sample_table_is_not_shorter_than_the_review(): def test_runs_carry_the_number_of_periods_they_claim(text, count): [run] = [h for h in ac.scan(text) if h.run] assert run.count == count + + +@pytest.mark.parametrize("label, claims", [("主要来自华东的销售额", ["source"]), ("集中在线上的订单", ["source"]), + ("建议持续降低的退款率", ["trend"]), ("部分订单异常", ["anomaly"]), + ("销售额", [])]) +def test_bare_scanning_finds_judgement_words_in_a_name(label, claims): + """认叫法里夹带的判断词(bare=True)只按词认:叫法是一个名字不是一句话,构成、建议计划、业务状态这些整句话的规矩不用。""" + assert [h.claim for h in ac.scan(label, bare=True)] == claims + assert ac.scan(label) == []