目標很明確:
從混雜中文、英文、符號的文字中,穩定找出已知的 Family 名稱。
像這些:
ARGOS_V3ARGOS V3ARGOS-V3ARGOSV3ARTEMIS2-BU6ATLAS4.0GUIANA-CLX
都希望最後能對到同一份 canonical family 資料。
一、問題背景
很多人第一直覺會想:
- 用
jieba斷詞 - 再從 token 裡面找 family
但實際上會遇到幾個問題:
1. Family 名稱不一定只有一種寫法
例如原始資料是:ARGOS_V3
但實際文本裡可能出現:
ARGOS_V3ARGOS V3ARGOS-V3ARGOSV3
2. Family 內可能含特殊符號
例如:
ARTEMIS2-BU6ATLAS4.0GUIANA-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",
]然後配合這三個步驟:
- 把 canonical family 加進 jieba
- 對文字做斷詞
- 對 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-> 不是 familyV3-> 不是 family
自然就抓不到 ARGOS_V3。
七、N-gram 拼接的作用
解法就是:
不只看單一 token,還看相鄰 token 拼起來的結果。
像:
['ARGOS', ' ', 'V3']拼接後變成:
"ARGOS V3"再做 normalize:
norm("ARGOS V3") -> "argosv3"![Python 實戰:用 jieba + Normalize + N-gram 穩定抓出 Family 名稱 ; re.sub(r'[^a-z0-9]+', '', s.lower()) - 儲蓄保險王](https://savingking.com.tw/wp-content/uploads/2026/04/20260428111832_0_7c5ee9.png)
就能對到 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"![Python 實戰:用 jieba + Normalize + N-gram 穩定抓出 Family 名稱 ; re.sub(r'[^a-z0-9]+', '', s.lower()) - 儲蓄保險王](https://savingking.com.tw/wp-content/uploads/2026/04/20260428110138_0_e34bb8.png)
十、實測得到的關鍵結論
根據前面的實驗,可以整理成這幾點:
1. jieba.add_word() 在你的環境裡,對 - _ . 類 family 是有幫助的
例如:
ARTEMIS2-BU6argos_v3ATLAS4.0GUIANA-CLX
都能保留成單一 token。
2. 空白形式不一定能直接保護
例如:ARGOS V3
常常會被切成:
![Python 實戰:用 jieba + Normalize + N-gram 穩定抓出 Family 名稱 ; re.sub(r'[^a-z0-9]+', '', s.lower()) - 儲蓄保險王](https://savingking.com.tw/wp-content/uploads/2026/04/20260428113059_0_795b15.png)
所以不能只做 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


![Python: pandas.DataFrame()處理雙維度資料,dict跟2D list轉為DataFrame有何差別?如何用index及columns屬性客製化index跟欄位名稱?df.index = [“一”,”二”,”三”,”四”] ; df.columns = 使用.head(n) ; .tail(m) ;取首n列,尾m列; .at[index,欄位名稱] 取單一資料 ; .iat[index,欄位順序] 取單一資料 ; .loc[index,欄位名稱] 取資料 ; .iloc[index,欄位順序];df.iloc[ [0,1],[0,2]])取資料 ; df.iloc[ 0:3,0:2]切片 Python: pandas.DataFrame()處理雙維度資料,dict跟2D list轉為DataFrame有何差別?如何用index及columns屬性客製化index跟欄位名稱?df.index = [“一”,”二”,”三”,”四”] ; df.columns = 使用.head(n) ; .tail(m) ;取首n列,尾m列; .at[index,欄位名稱] 取單一資料 ; .iat[index,欄位順序] 取單一資料 ; .loc[index,欄位名稱] 取資料 ; .iloc[index,欄位順序];df.iloc[ [0,1],[0,2]])取資料 ; df.iloc[ 0:3,0:2]切片](https://i0.wp.com/savingking.com.tw/wp-content/uploads/2022/11/20221111093547_79.png?quality=90&zoom=2&ssl=1&resize=350%2C233)







近期留言