Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL)

加入好友
加入社群
Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

1. re.M 是做什麼的?

re.M 是 re.MULTILINE 的縮寫,中文通常稱為「多行模式」。

它只會改變兩個符號的行為:

  • ^:可以匹配每一行的開頭。
  • $:可以匹配每一行的結尾。

不會讓 . 匹配換行字元。
若要讓 . 跨越多行,
使用的是 re.Sre.DOTALL)。

記憶 re.S 可以聯想成:

S = Single-line view
    = 把多行文字看成一條連續文字
    =  . 可以跨過換行

這是方便記憶的聯想,
Single-line view 不是 re.S 的官方全名。
官方名稱是 re.DOTALL,意思是:

DOT + ALL
        . 匹配所有字元包括換行

注意:re.S 不會真的刪除換行,
也不會改變 ^ 和 $ 的規則;
它只改變 . 是否能匹配換行。

S = Single-line view
    = 把多行文字看成一條連續文字
    =  . 可以跨過換行

2. re.Mre.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.*$")

常見對照:

Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

3. 沒有 re.M 時

import re

text = "INFO start\nERROR failed\nINFO end"

pattern = re.compile(r"^ERROR.*$")
print(pattern.findall(text))
Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

原因是整份字串的開頭是 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))
Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

因為 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))
Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

這個規則只找每一行開頭是 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))

輸出:

Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

拆解這個正則表達式:

(?m)                    開啟多行模式
^                       從每一行的開頭開始
//                      找兩個斜線
[ \t]*                  允許空格或 Tab但不跨行
(Settings|Specs|Config) 只接受三種標記
\b                      避免誤抓 SettingsHelper 等較長單字

7. re.M 與 re.S 不一樣

這是最常見的混淆:

Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

可以用這個畫面來聯想:

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']
Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

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,
)

這表示:

  1. (?m) 讓 ^ 可以檢查每一行的開頭。
  2. [ \t]* 接受行首縮排。
  3. // 必須出現在行首縮排之後。
  4. (?P<kind>...) 把 marker 類型存到 kind
  5. \b 避免把 SettingsHelper 誤認成 Settings
  6. re.IGNORECASE 讓 settingsSettingsSETTINGS 都能匹配。

10. 一句話總結

re.M  (?m) 不會把文字變成一行
它只是讓 ^  $ 能夠逐行判斷

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

加入好友
加入社群
Python 正則表示法(Regular Expression) : re.M、re.MULTILINE、(?m) 簡化教學; 若要讓 . 跨越多行,使用的是 re.S(re.DOTALL) - 儲蓄保險王

儲蓄保險王

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

You may also like...

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *