- 為什麼要關心 sub 和 subn?
在處理文件內容(例如 Word 的 document.xml)時,常需要「找到一段結構化內容並刪掉或替換」。Python 的 re 模組提供兩個替換工具:re.sub 與 re.subn。
它們都能改寫字串,但如果你還需要知道「到底替換了幾次?」或判斷「是否有命中」,re.subn 會更適合。這在清理 XML、刪除特定段落、統計替換數時特別有用。 - 基本差異一眼看懂

範例:
import re
s = "A1 B2 C3"
print(re.sub(r"\d", "#", s)) # A# B# C#
print(re.subn(r"\d", "#", s)) # ('A# B# C#', 3)3. 實戰場景:刪除 Word XML 中含目標內容的段落
假設我們從 DOCX 解壓得到 word/document.xml 裡的一段段落(<w:p>...</w:p>):
xml = """<w:p w14:paraId="48292F68" w14:textId="11BAA91B" w:rsidR="000844DA" w:rsidRDefault="004B1354">
<w:r>
<w:rPr><w:rFonts w:hint="eastAsia"/></w:rPr>
<w:t>底下插入一個圖檔</w:t>
</w:r>
<w:r>
<w:rPr><w:rFonts w:hint="eastAsia"/></w:rPr>
<w:t>:</w:t>
</w:r>
</w:p>"""我們想把「第一個包含指定文字(例如 ‘底下插入一個圖檔’)」的段落整段移除。
3.1 使用 re.sub
import re
# xml = open("document.xml", "r", encoding="utf-8").read()
pattern = re.compile(r'<w:p[^>]*>.*?底下插入一個圖檔.*?</w:p>', flags = re.DOTALL)
"""
用位元 OR:|。例如同時忽略大小寫並讓 . 包含換行:
flags = re.DOTALL | re.IGNORECASE
支援的常見旗標:re.DOTALL / re.IGNORECASE / re.MULTILINE / re.VERBOSE / re.ASCII 等。
"""
new_xml = pattern.sub('', xml, count=1) # 只替換第一個
# 但此時不知道是否真的發生替換,還得自己比對前後差異輸出:

在 Python 的正則裡,. 預設匹配「除\n以外的任意單一字元」。
加上 re.DOTALL(別名 re.S)後,. 也會匹配換行 \n,等於跨行整塊吃。
補充一些細節與延伸:
預設行為
. 不匹配 \n(也不匹配某些極少見的未映射字元,但一般可忽略)。
在 Windows 文本裡換行是 \r\n,正則掃描時 Python 會逐字看:. 預設不匹配 \n,但會匹配 \r,所以如果你的文本是 \r\n 串,沒有 DOTALL 的情況下 . 可以吃到 \r 但在 \n 停住;這常導致看起來像「跨了一半」。
啟用 DOTALL (re.DOTALL / re.S)
. 變成「包含換行在內的任意字元」。
例如 re.findall(r’.+’, text) 在多行時,本來每行分開匹配;加 DOTALL 後可能整段一次匹配(取決於貪婪與錨點)。
與 re.MULTILINE 的差異
re.MULTILINE (re.M) 只影響 ^ 和 $ 的含義(行首/行尾),不改變 . 的匹配集合。
可以同時用:re.compile(pattern, re.DOTALL | re.MULTILINE)。
常見替代寫法(不想用 DOTALL 但要跨行)
使用字元類雙保險:[\s\S]、[\d\D]、[\w\W] 都「必定匹配任何字元」,包含換行。
例:re.findall(r’BEGIN([\s\S]*?)END’, text) 不需要 DOTALL。
好處:局部明確,不必對整個正則啟用全域旗標。
控制貪婪
跨行取內容時常配合懶惰量詞:.? 或 [\s\S]?;否則容易吃到最後一個結尾標記。
若用 . 而又開了 DOTALL,務必確認量詞(* / +)是否應該加 ?。
性能與可讀性
re.DOTALL 全域打開後每一個 . 都可能跨行,影響閱讀意圖;若只是某一段需要跨行,推薦用局部類 [\s\S]*?。
但在需要大量「任意字符」段落提取(例如大塊 XML/HTML 內部)時,直接使用 DOTALL 會更簡潔。
小心 CRLF 邊界
如果你有分行相關判斷(例如行數統計)先標準化換行:text = text.replace(‘\r\n’,’\n’) 再做模式匹配可以減少「. 吃到 \r」造成的微妙偏差。
總結選擇指南
只偶爾在某個子模式需要跨行:用 [\s\S]*?。
整個模式大量跨行段:用 re.DOTALL。
需要同時逐行錨點和跨行內容:re.DOTALL | re.MULTILINE。
3.2 使用 re.subn(更方便)
pattern = re.compile(r'<w:p[^>]*>.*?底下插入一個圖檔.*?</w:p>', re.DOTALL)
new_xml, n = pattern.subn('', xml, count=1)
if n:
print(f"刪除成功,替換了 {n} 個段落")
else:
print("沒找到目標段落")輸出:

n 就是替換次數(0 或 1),不需再手動比較 xml != new_xml。
- 參數說明:count 的角色
count=0(預設)代表不限制,替換所有匹配。
count=1 限制只處理第一個匹配,常用於「只刪一個」的情境。
與 sub / subn 共通,行為一致。
示例:
# 刪除所有符合的段落(危險,可能過度)
new_xml_all = pattern.sub('', xml) # = count=0
# 只刪第一個(安全)
new_xml_one, n1 = pattern.subn('', xml, count=1)- 為什麼不直接用 sub 然後比較?
你可以這樣做:
new_xml = pattern.sub('', xml, count=1)
removed = (new_xml != xml)但這種方式:
需要進行一次整體字串比較(大文件時多餘)。
無法直接知道替換幾次(若 count=0 且多段落命中時會不清楚數量)。
subn 已經在執行替換的同一迴圈記錄次數,無額外效能負擔,因此更直覺。
- 正則拆解(本例)
模式:]>.?底下插入一個圖檔.*?
片段說明:
]>:匹配段落起始標籤與屬性(不跨過 >)。 .?底下插入一個圖檔.*?:在該段落內容中非貪婪尋找文字。
:段落結束。
re.DOTALL:讓 . 包含換行。
改良建議:若怕誤吃過多內容,可精簡為:
r'<w:p\b[^>]*>[\s\S]*?底下插入一個圖檔[\s\S]*?</w:p>'用 [\s\S] 避免過度依賴 DOTALL,同時更明確跨行。
[\s\S] 是一個字符類,裡面同時包含:
\s:所有「空白字元」(whitespace)(空格、tab、換行、垂直製表、Form Feed 等)
\S:所有「非空白字元」
因為一個字元要嘛是空白,要嘛不是空白,所以把 \s 和 \S 放在同一個字符類裏面就等於「匹配任何單一字元」,包含換行 \n。這是實務上常用的「不開 DOTALL 但仍想跨行」的技巧。
常見用法:
任意多字元(含換行)最小匹配:
[\s\S]? 任意多字元(含換行)貪婪匹配: [\s\S]
取兩個標記中間所有內容(跨行):
BEGIN([\s\S]*?)END
為什麼不用 . 而用它?
預設 . 不匹配 \n;加 re.DOTALL 可以,但會讓整個模式中所有 . 都跨行,有時過度寬鬆。
用 [\s\S] 只在需要的位置顯式寫「包含換行的任意字元」,控制範圍更精準。
替代寫法(效果一樣):
[\d\D]
[\w\W]
它們都是「一組 + 其補集」的形式,結果仍是所有字元;[\s\S] 最語意清晰。
- 常見陷阱與風險

- XML 解析替代方案(更精準)
使用 lxml:
from lxml import etree
tree = etree.fromstring(xml.encode('utf-8'))
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
for p in tree.xpath('//w:p', namespaces=ns):
# 將段落序列化成字串判斷是否含關鍵文字
frag = etree.tostring(p, encoding='unicode')
if '底下插入一個圖檔' in frag:
parent = p.getparent()
parent.remove(p)
break
new_xml = etree.tostring(tree, encoding='unicode')
#XMLSyntaxError: Namespace prefix w14 for paraId on p is not defined, line 1, column 94
"""
錯誤的核心原因:丟給 etree.fromstring 的片段裡有屬性或元素使用前綴 w14:(例如 w14:paraId),但片段本身沒有宣告 xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"。XML 解析器看到前綴卻不知道它代表哪個命名空間,就報 Namespace prefix w14 ... is not defined.
"""
#補齊命名空間宣告:
from lxml import etree
xml_fragment = '''
<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
w14:paraId="12345678" w14:textId="ABCDEF12">
<w:r><w:t>Hello</w:t></w:r>
</w:p>
'''
node = etree.fromstring(xml_fragment)
print(node.tag) # {http://schemas.openxmlformats.org/wordprocessingml/2006/main}p
print(node.attrib.keys()) # dict_keys(['{http://schemas.microsoft.com/office/word/2010/wordml}paraId', '{http://schemas.microsoft.com/office/word/2010/wordml}textId'])優點:不會誤跨段落,語義正確。缺點:需解析樹。
9. 何時選擇 sub vs subn

加強版範例(列印狀態+後續清理條件)
pattern = re.compile(r'<w:p[^>]*>[\s\S]*?底下插入一個圖檔[\s\S]*?</w:p>')
new_xml, n = pattern.subn('', xml, count=1)
if n:
print("刪除段落成功,準備檢查是否也要移除圖片資源...")
# TODO: 移除 image rId / relationship / media 檔案
else:
print("未找到該段落,無需後續清理。")- 效能簡述
小文件:差異可忽略。
大文件(MB 級 XML):subn 不比 sub 慢;反而避免了手動比對字串是否改變的額外操作。
正則複雜度決定主要成本;是否取回次數只是多一個整數記錄。 - 最終建議與心智模型
把 subn 視為 “sub + 命中次數”。
有決策分支(例如:命中才刪圖片 / 更新索引)就用 subn。
不確定是否命中且不想做字串比較 → subn。
需要更安全的 XML 節點級刪除 → 改用解析器。
要批次清理多類型資源 → 先解析 relationships → 搭配多次替換。 - 一句話總結
re.sub 只是替換;re.subn 在替換的同時告訴你“到底改了幾次”,對文件清理、條件後處理特別實用。用在刪除 Word XML 的 段落示例裡,它讓你立即知道是否成功刪掉目標段落,而不必再手動比對原始與結果。
推薦hahow線上學習python: https://igrape.net/30afN

![一文搞懂Python pandas.DataFrame去重:df.drop_duplicates() 與 df[~df.duplicated()] 的等價、差異與最佳實踐 一文搞懂Python pandas.DataFrame去重:df.drop_duplicates() 與 df[~df.duplicated()] 的等價、差異與最佳實踐](https://i2.wp.com/savingking.com.tw/wp-content/uploads/2025/08/20250808202701_0_66f9bc.png?quality=90&zoom=2&ssl=1&resize=350%2C233)








近期留言