攝影或3C

Python 實戰:用 jieba + Normalize + N-gram 穩定抓出 Family 名稱 ; re.sub(r'[^a-z0-9]+’, ”, s.lower())

目標很明確:

從混雜中文、英文、符號的文字中,穩定找出已知的 Family 名稱。

像這些:

  • ARGOS_V3
  • ARGOS V3
  • ARGOS-V3
  • ARGOSV3
  • ARTEMIS2-BU6
  • ATLAS4.0
  • GUIANA-CLX

都希望最後能對到同一份 canonical family 資料。


一、問題背景

很多人第一直覺會想:

  • jieba 斷詞
  • 再從 token 裡面找 family

但實際上會遇到幾個問題:

1. Family 名稱不一定只有一種寫法

例如原始資料是:ARGOS_V3

但實際文本裡可能出現:

  • ARGOS_V3
  • ARGOS V3
  • ARGOS-V3
  • ARGOSV3

2. Family 內可能含特殊符號

例如:

  • ARTEMIS2-BU6
  • ATLAS4.0
  • GUIANA-CLX

這些符號對一般 tokenizer 來說,常常會被當作分隔符。


3. 就算加進 jieba 詞典,也不保證所有變形都直接命中

例如 canonical 是:ARGOS_V3
但輸入寫成:ARGOS V3

這種情況即使你 jieba.add_word("ARGOS_V3"),也不會自動幫你匹配空白版。


二、核心思路

這個問題最穩定的解法,不是手工列一堆 alias,而是:

只保留 canonical family

例如:

families = [
    "ARGOS_V3",
    "ARTEMIS2-BU6",
    "GUIANA-CLX",
    "ATLAS4.0",
]

然後配合這三個步驟:

  1. 把 canonical family 加進 jieba
  2. 對文字做斷詞
  3. 對 token 或 token chunk 做 normalize 後比對

三、為什麼不用人工定義一堆 alias?

你可能會想把資料寫成:

{
    "ARGOS_V3": "ARGOS_V3",
    "ARGOS V3": "ARGOS_V3",
    "ARGOS-V3": "ARGOS_V3",
    "ARGOSV3": "ARGOS_V3",
}

這樣當然可以,但問題是:

  • 維護成本高
  • 新 family 進來要一直補變形
  • 不 scalable

更好的方式是:

只存 canonical,其他形式交給 normalize 處理。


四、Normalize 是什麼?

Normalize 的目的,是把看起來不同、但其實是同一個 family 的寫法,轉成同一個 key。

例如:

import re

def norm(s):
    return re.sub(r'[^a-z0-9]+', '', s.lower())

這個規則會:

  • 全部轉小寫
  • 移除非英數字元

所以:

norm("ARGOS_V3")   # argosv3
norm("ARGOS V3")   # argosv3
norm("ARGOS-V3")   # argosv3
norm("ARGOSV3")    # argosv3

都會收斂成同一個值:argosv3

五、為什麼還需要 jieba?

因為你的文本通常不是只有 family,還混著很多內容,例如:

"project__ARGOS V3__AAPL check"

你需要先把整段字串切成 token,
再從 token 中找可能的 family。

jieba 的角色就是:

先提供一個可操作的 token 序列。


六、為什麼只做 single token lookup 不夠?

假設你有:

text = "project__ARGOS V3__AAPL check"

jieba 裡可能會切成:

['project', '__', 'ARGOS', ' ', 'V3', '__', 'AAPL', ' ', 'check']

這時如果你只查單一 token:

  • ARGOS -> 不是 family
  • V3 -> 不是 family

自然就抓不到 ARGOS_V3


七、N-gram 拼接的作用

解法就是:

不只看單一 token,還看相鄰 token 拼起來的結果。

像:

['ARGOS', ' ', 'V3']

拼接後變成:

"ARGOS V3"

再做 normalize:

norm("ARGOS V3") -> "argosv3"

就能對到 canonical:ARGOS_V3

八、完整可用程式碼

下面是一個夠短、夠實用的版本。

import re
import jieba

def norm(s):
    return re.sub(r'[^a-z0-9]+', '', s.lower())

def find_families(text, families, max_window=5):
    #  canonical family 加進 jieba
    for f in families:
        jieba.add_word(f)

    # normalize lookup: normalized_key -> canonical_family
    lookup = {norm(f): f for f in families}

    # jieba 斷詞
    tokens = jieba.lcut(text, HMM=False)

    hits = []

    # 1. 先檢查單一 token
    for t in tokens:
        key = norm(t)
        if key in lookup:
            hits.append(lookup[key])

    # 2. 再檢查相鄰 token chunk
    for i in range(len(tokens)):
        for j in range(i + 2, min(i + max_window + 1, len(tokens) + 1)):
            chunk = ''.join(tokens[i:j])
            key = norm(chunk)
            if key in lookup:
                hits.append(lookup[key])

    # 去重但保留順序
    return list(dict.fromkeys(hits))

九、使用範例

text = "project__ARGOS V3__AAPL check"

十、實測得到的關鍵結論

根據前面的實驗,可以整理成這幾點:

1. jieba.add_word() 在你的環境裡,對 - _ . 類 family 是有幫助的

例如:

  • ARTEMIS2-BU6
  • argos_v3
  • ATLAS4.0
  • GUIANA-CLX

都能保留成單一 token。


2. 空白形式不一定能直接保護

例如:ARGOS V3

常常會被切成:

所以不能只做 single token lookup。


3. 只要做 token chunk 拼接,多 token family 仍然能找回來

例如:

'ARGOS' + ' ' + 'V3'

拼接後再 normalize,就能 match 到:ARGOS_V3

十一、這套方法的優點

優點 1:不用人工列一堆 alias

只要維護 canonical family list 即可。


優點 2:能容忍使用者輸入不同分隔方式

像:

  • _
  • -
  • 空白
  • 連寫

都能收斂比對。


優點 3:能直接用在混雜文本

例如:

  • 測試名稱
  • 規格字串
  • log
  • ticket title
  • command 描述

都能套。

推薦hahow線上學習python: https://igrape.net/30afN


儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡