1. re.M 是做什麼的?
re.M 是 re.MULTILINE 的縮寫,中文通常稱為「多行模式」。
它只會改變兩個符號的行為:
^:可以匹配每一行的開頭。$:可以匹配每一行的結尾。
它不會讓 . 匹配換行字元。
若要讓 . 跨越多行,
使用的是 re.S(re.DOTALL)。
記憶 re.S 可以聯想成:
S = Single-line view
= 把多行文字「看成一條連續文字」
= 讓 . 可以跨過換行這是方便記憶的聯想,Single-line view 不是 re.S 的官方全名。
官方名稱是 re.DOTALL,意思是:
DOT + ALL
. 匹配所有字元,包括換行注意:re.S 不會真的刪除換行,
也不會改變 ^ 和 $ 的規則;
它只改變 . 是否能匹配換行。
S = Single-line view
= 把多行文字「看成一條連續文字」
= 讓 . 可以跨過換行2. re.M、re.MULTILINE、(?m)
以下三種寫法代表相同的多行模式:
import re
pattern_a = re.compile(r"^ERROR.*$", re.M)
pattern_b = re.compile(r"^ERROR.*$", re.MULTILINE)
pattern_c = re.compile(r"(?m)^ERROR.*$")常見對照:

3. 沒有 re.M 時
import re
text = "INFO start\nERROR failed\nINFO end"
pattern = re.compile(r"^ERROR.*$")
print(pattern.findall(text))
原因是整份字串的開頭是 INFO start,不是 ERROR。
在預設模式下,^ 只看整份字串的開頭,$ 只看整份字串的結尾。
4. 加上 re.M 後
import re
text = "INFO start\nERROR failed\nINFO end"
pattern = re.compile(r"^ERROR.*$", re.M)
print(pattern.findall(text))
因為 re.M 讓 ^ 和 $ 可以逐行工作,所以第二行的 ERROR failed 能被找到。
同樣的規則也可以寫成:
pattern = re.compile(r"(?m)^ERROR.*$")5. ^ 與 $ 的簡單記法
可以把它們想成一場馬拉松:
^ 像一顆子彈往上飛
也像鳴槍起跑
代表一行文字的起點
$ 像馬拉松的終點線
跑到終點後可以獲得 $
代表一行文字的結尾完整聯想是:
^ 鳴槍,從起點出發
↓
跑過這一行文字
↓
$ 抵達終點,拿到 $在這個聯想中,$ 是「終點線」的符號,
不是要真的把文字替換成金錢符號。
沒有 re.M:整份字串只有一個起點和一個終點
有 re.M:每一行都可以重新鳴槍起跑,
每一行也都有自己的終點線例如:
import re
text = "apple\nbanana\napricot"
print(re.findall(r"^a.*$", text, re.M))
# ['apple', 'apricot']這裡的 ^a 表示「從每一行起點開始,第一個字母是 a」,
而 $ 表示「一路匹配到該行終點」。
沒有 re.M:
^ = 整份字串的開頭
$ = 整份字串的結尾
有 re.M:
^ = 每一行的開頭
$ = 每一行的結尾例如:
import re
text = "apple\nbanana\napricot"
print(re.findall(r"^a.*", text, re.M))
這個規則只找每一行開頭是 a 的文字。
6. (?m) 的實際例子:找行首標記
假設文件內容如下:
import re
text = """// Settings
{\"name\": \"demo\"}
// Specs
{\"name\": \"spec\"}
// Config
{\"name\": \"config\"}"""
pattern = re.compile(r"(?m)^//[ \t]*(Settings|Specs|Config)\b")
print(pattern.findall(text))輸出:

拆解這個正則表達式:
(?m) 開啟多行模式
^ 從每一行的開頭開始
// 找兩個斜線
[ \t]* 允許空格或 Tab,但不跨行
(Settings|Specs|Config) 只接受三種標記
\b 避免誤抓 SettingsHelper 等較長單字7. re.M 與 re.S 不一樣
這是最常見的混淆:

可以用這個畫面來聯想:
re.M / (?m)
文字仍然分成一行一行
只是 ^ 和 $ 願意在每一行工作
re.S / (?s)
文字仍然包含換行
只是 . 不再把換行當成障礙比較:
import re
text = "first\nsecond"
print(re.findall(r"^second$", text, re.M))
# ['second']
print(re.findall(r"first.second", text))
# []
print(re.findall(r"first.second", text, re.S))
# ['first\nsecond']
re.S 的記憶重點
M = Multiline
多行模式,影響 ^ 與 $
S = Single-line view
單行視角,影響 .
DOTALL
. 匹配所有字元,包括 \n所以,看到以下正則表達式時:
re.compile(r"BEGIN.*END", re.S)可以直接聯想成:
從 BEGIN 開始
中間的 . 可以跨越多行
直到 END記住:
M = Multiline,影響 ^ 與 $
S = Dotall,影響 . 是否可以跨換行8. 為什麼常用 [ \t]*?
在行首解析時,通常只想允許目前這一行的縮排:
pattern = re.compile(r"(?m)^[ \t]*//[ \t]*Settings\b")[ \t]* 只接受:
- 空格
- Tab
不會吃掉換行。
要小心這種寫法:
pattern = re.compile(r"(?m)^\s*//[ \t]*Settings\b")因為 \s 不只包含空格與 Tab,也包含換行。
在某些解析情況下,可能把下一行內容一起吞進匹配範圍。
若需求是「只處理行首縮排」,使用 [ \t]* 比較清楚。
9. 和本專案程式碼的對應
本專案在 Markdown 的 // Settings、// Specs、// Config marker 解析中使用 (?m):
COMMENT_MARKER_RE = re.compile(
r"(?m)^[ \t]*//[ \t]*(?P<kind>Settings|Specs|Config)\b",
re.IGNORECASE,
)這表示:
(?m)讓^可以檢查每一行的開頭。[ \t]*接受行首縮排。//必須出現在行首縮排之後。(?P<kind>...)把 marker 類型存到kind。\b避免把SettingsHelper誤認成Settings。re.IGNORECASE讓settings、Settings、SETTINGS都能匹配。
10. 一句話總結
re.M 或 (?m) 不會把文字變成一行;
它只是讓 ^ 和 $ 能夠逐行判斷。推薦hahow線上學習python: https://igrape.net/30afN










近期留言