2023年5月14日

NVDA神祕的正規表達式應用

2023/10/31 更新
上一篇〈NVDA老是讀錯?改一下就好了〉一文已經說明了 NVDA 讀音字典的使用方式,不過並未觸及規則類型的「正規表達式」,這個項目非等閒之輩卻因 NVDA 使用者都不知是什麼而被忽略,就竟什麼情況下才用得上呢?舉一些例子來說明,本文提到的朗讀語音皆使用 Windows OneCore 合成器的 Microsoft Yating 中文語音。

範例一
語音朗讀「上面」的時候「上」後面的字讀音聽起來有問題,將「上」改為其它的同音字例如「尚」則後面的字讀音就正常了,我們利用讀音字典來修正,規則如下。

原來文字:上面
替代文字:尚面

同樣的「上頭、上邊」的「上」後面的字也有讀音問題,可依此方式來建立對應的讀音修正項目,這三個項目若使用正規表達式建立規則只需一個項目即可,規則如下。

原來文字:上([面頭邊])
替代文字:尚\1

分開為三個項目不好嗎?為何要使用看不懂的正規表達式呢?當三個項目同時建立規則影響不大,若是不同時間建立且字典項目數量較多的話有可能會重複項目,管理起來就不甚方便了。

範例二
開啟記事本或 Microsoft Word 時 NVDA 都會提示「多行」,但語音將「行」這個字讀成ㄒㄧㄥˊ而不是ㄏㄤˊ,我們可以在讀音字典中新增字典項目利用同音字將「多行」改為「多航」,然而當碰到以下這樣的句子:

路上有許多行人沒撐傘。

因為我們已經建立一個修正「多行」讀音的規則,語音朗讀的結果變成:

路上有許多航人沒撐傘。

這時應該建立什麼樣的規則來修正讀音呢?再來看另一個例子。

範例三
數字前有貨幣符號例如 $123 語音朗讀的結果是「一百二十三美元」而不是「一百二十三元」,$12345 語音朗讀的結果變成「錢一二三四五」,有千分位符號例如 $123,456 朗讀的結果竟變成了「一百二十三美元,四百五十六」,需要建立規則來修正朗讀的方式,但是金額的數字可大可小,要如何建立規則呢?很顯然並非都只是你熟悉的把 A 換成 B 無此簡單,有些情況不用正規表達式根本無法處理。

正規表達式英文是 Regular Expression,中文翻譯有多種稱呼,如正規表示法、正規表示式、正規式、正則表達式、規則運算式等。正規表達式簡單來說就是用來處理字串的一種規則方法,用來表達字元符號在字串中出現的規則,有些複雜但強大,任何工具只要支援這種規則方法就能以正規表達式處理字串。
正規表達式對程式開發者並不陌生,然而 NVDA 使用者沒有多少人知道如何在讀音字典中使用正規表達式,查閱 NVDA 用戶指南發現正規表達式這個主題並不在說明的範疇,點開提供的參考連結一看……嗯,還是算了,就當它不存在吧!不用放棄,本文將為 NVDA 使用者揭開正規表達式的神祕面紗。

正規表達式就如同程式語言一樣有它自己的語法規則以及一些用來表達特定含義的字元與符號所組成,因此我們需要先認識它們才能在讀音字典建立或編輯規則。

NVDA 讀音字典常用的正規表達式語法
表示 說明 範例解說
^x 一行的開頭是 x ^abc 表示以 a 作為一行開頭的 abc
x$ 一行的結尾是 x abc$ 表示以 c 作為一行結尾的 abc
x* x 重複 0 次或 1 次以上,0 次表示沒有 go*d 可以是 gd 或 god 或 good
x+ x 重複 1 次以上 go+d 可以是 god 或 good 或 goood
x? x 重複 0 次或 1 次,表示沒有或有 12-?34 表示 1234 或 12-34
. 任一字元 (換行字元 \n 除外) .+ 表示不限長度的任何字元
(x) 群組,每個群組預設會按群組順序自動分配從 1 開始的編號,群組能以群組參照的方式供後續使用 (ab)c(de) 的 (ab) 和 (de) 皆為群組,(ab) 編號為 1,(de) 編號為 2
\數字編號 群組參照,反向參照先前的群組 (ab)c(de)abcde 可以表示為 (ab)c(de)\1c\2
x(?=y) 條件判斷,x 右邊有 y,表示符合 y
x(?!y) 條件判斷,x 右邊沒有 y,表示排除 y
(?<=y)x 條件判斷,x 左邊有 y,表示符合 y
(?<!y)x 條件判斷,x 左邊沒有 y,表示排除 y
x|y x 或 y keyboard|mouse 表示 keyboard 或 mouse
x{n} x 重複 n 次,n 為正整數 a{2}bc 表示 aabc,(ab){2}c 表示 ababc
x{n,} x 重複 n 次以上,n 為正整數 ab{2,}c 表示 abbc 或 abbbc 等,a 與 c 之間的 b 數量為 2 個以上
x{,n} x 重複 n 次以下 (包含 0 次),n 為正整數 ab{,2}c 表示 ac 或 abc 或 abbc
x{n,m} x 重複 n 次以上 m 次以下,n 與 m 皆為正整數,n 要小於 m ab{1,2}c 表示 abc 或 abbc
[a-z] 任一小寫英文字母 若是 [c-m] 表示 c 到 m 之間的任一小寫英文字母
[A-Z] 任一大寫英文字母 若是 [C-M] 表示 C 到 M 之間的任一大寫英文字母
[0-9] 任一數字 若是 [2-7] 表示 2 到 7 之間的任一數字
[\u4e00-\u9fff] 任一漢字 (Unicode 中日韓統一表意文字區段的字) [\u4e00-\u9fff],表示逗號前面是中文字
[xyz] x 或 y 或 z [word] 表示 w 或 o 或 r 或 d 任一字元,不是指 word 整個單字
[^xyz] 不包含 x 或 y 或 z,也就是排除 x 或 y 或 z [^word] 表示不包含 w 或 o 或 r 或 d 任一字元,不是指不包含 word 整個單字
\d 任一數字 等於 [0-9]
\D 不包含數字的任一字元 等於 [^0-9]
\w 包含 Unicode 字元、數字、大小寫英文字母、底線在內的任一字元 不包含例如 (),.+-:@/ 空格等符號
\W 不包含 Unicode 字元、數字、大小寫英文字母、底線在內的任一字元 包含例如 (),.+-:@/ 空格等符號
\s 空白字元 (包含空格、定位字元 \t、換行字元 \n 等)
\S 不包含空白字元的任一字元

從上表中可以看到一些符號如加號在正規表達式有其特殊的作用,如果要表示加號本身就必須在前面加上反斜線像 \+ 這樣,以下這些符號若要表示符號本身的話都要加上反斜線。
+.*?()[]{}^$\|

在讀音字典使用正規表達式的一些提示:
  • 正規表達式規則一次比對一行的內容。
  • 正規表達式規則比對的方向是由左往右,有時需注意規則排列順序的影響。
  • 字典項目清單中是以由上往下的順序處理規則項目,當不同規則彼此有相關性,則規則的上下排列順序會影響規則的執行結果。
  • 建立的規則非語法表示的部分有大小寫英文字母且需區分大小寫,記得要勾選「大小寫須相符」核取方塊,否則視為大小寫不分。
  • 建立的規則有使用正規表達式,在類型的部分記得要選擇「正規表達式「,建立的字典項目才能被正確處理。
  • 使用正規表達式建立的規則最好在註解欄位加上相關說明,時間一久可能會忘記為何這麼做。

以下例子解說中各種規則使用到的正規表達式語法皆可對照參考上表的分項說明。

範例一提到的「上面、上頭、上邊」這三個詞的情況,第一個字都是「上」,不同的是在第二個字,也就是說「上」的後面接的可以是「面、頭、邊」的任何一個字,所以建立規則時原來文字欄位的內容就會是:

上[面頭邊]

[面頭邊] 表示可以為「面」或「頭」或「邊」,中括弧內的每個字彼此間是「或」的關係。

接下來替代文字欄位要是什麼內容呢?替代文字欄位的內容是語音要朗讀的部分,你無法將原來文字欄位含有正規表達式的內容原封不動的複製貼上,唯一能夠出現在替代文字欄位的正規表達式語法只有群組參照,因此剛才原來文字欄位的內容並不是最終的結果,還要根據朗讀替貸文字欄位內容的需要進行調整。

原來文字欄位的「上」用同音字的「尚」來替換,而緊接的中括弧部分是正規表達式,需要將它轉換為替代文字欄位可接受的型式,把這個部分用小括弧包起來當作一個群組,當作群組時就會自動分配數字編號,數字編號是可以用在替代文字欄位的,只要在數字前加上反斜線成為群組參照即可,這個群組在原來文字欄位裡的群組順序是第一個,所以編號就是 1,因此 ([面頭邊]) 的群組參照是 \1,最終的結果就會是:

原來文字:上([面頭邊])
替代文字:尚\1

我們也可以建立有相同意思的第二種規則如下。

原來文字:上(面|頭|邊)
替代文字:尚\1

這兩種規則相較,中括弧裡的項目彼此間為「或」的關係,而直線符號分隔開來的項目彼此間也是「或」的關係,以此例來說兩者皆可,不過兩者使用上仍有差別,中括弧內的項目只能是單一字元間「或」的關係,直線符號隔開的每個項目則可以是兩個以上字元的組合。另外直線符號隔開的規則項目有時候需要注意順序,因為正規表達式由左往右比對時先比對到符合的規則項目就不再比對後面其它的規則項目,順序不對將使結果和預期的不同。

除了上述兩種有相同意思的規則外,我們還可以使用條件判斷的方式來建立第三種規則,「上」這個字的右邊有「面、頭、邊」的其中一個字,「上」才會以同音字的「尚」來替換,建立的規則如下。

原來文字:上(?=[面頭邊])
替代文字:尚

(?=[面頭邊]) 是條件判斷不是實際內容,所以不需轉換為群組參照放到替代文字欄位讀出。第二種規則若也使用條件判斷的方式就可建立第四種規則如下。

原來文字:上(?=面|頭|邊)
替代文字:尚

修正中文字的讀音往往需要以詞的方式來建立規則,以「新北市三重區」及「重灌電腦」的「重」為例,語音對「重」的讀音是ㄓㄨㄥˋ,這兩個例子的「重」則要讀成ㄔㄨㄥˊ,然而「三重」與「重灌」兩個詞「重」的位置卻是一個在後面,一個在前面,需要分開建立兩個規則,使用條件判斷建立的規則如下。

原來文字:(?<=三)重
替代文字:蟲
原來文字:重(?=灌)
替代文字:蟲

雖然替代文字欄位的內容一樣,但因「重」在兩個詞中的位置不同,以致於需要使用不同的條件判斷來建立規則,一個是「重」的左邊有「三」,一個是「重」的右邊有「灌」,其實兩個規則還是可以合併為一個規則,方法是利用直線符號「或」的關係將兩者接在一起,建立的規則如下。

原來文字:(?<=三)重|重(?=灌)
替代文字:蟲

若「重考」、「重來」的「重」讀音俢正再加進去的話規則修改如下。

原來文字:(?<=三)重|重(?=[灌考來])
替代文字:蟲

接下來看範例二的情況,原本的規則是「多行」讀成「多航」,而「多行」的後面要接什麼字並沒有限制,當「多行」的後面接的字是「人」的時候則「行」就不能讀成「航」,可使用條件判斷來排除「人」這個字,也就是「行」的右邊不能是「人」,原先建立的規則修改如下。

原來文字:多行(?!人)
替代文字:多航

後來又發現例如「許多行道樹」的「行」也不能讀成「航」,所以建立的規則再度修改如下。

原來文字:多行(?![人道])
替代文字:多航

「多行」的後面可能還會有其它的字使得「多行」不能讀成「多航」,一時間也想不到還要新增什麼字在中括弧裡,我們不妨換個角度思考,只有在 NVDA 提示「多行」的地方才修正讀音,由於聽到 NVDA 提示「多行」的頻率高過一般文句中出現的「多行」,因此以出現頻率較高的對象為主著手修正讀音。
開啟記事本或 Microsoft Word 時 NVDA 的提示訊息「文字編輯器 編輯區 多行 空白」以及「Microsoft Word 文件 編輯區 多行 第 1 頁 第 1 節 空白」兩者的內容聽起來都像是在同一行的文字,但從「語音檢視器」中檢視語音朗讀記錄可知「多行」本身是個獨立的項目,與前後項目分隔開來並沒有關係,也就是「多行」這兩個字自成一行,開頭是「多」,結尾是」行」,建立的規則如下。

原來文字:^多行$
替代文字:多航

將「多行」限縮在當一行中只有這兩個字才改變「行」的讀音,因為「多行」通常前後都會有其它的文字,不太有機會單獨出現,如此一來就只在 NVDA 提示「多行」時才會讀成「多航」,比起原先使用條件判斷來排除特定文字的方式更符合大多數情況下對「多行」讀音的要求。

我們知道中文字形成詞之後可能會造成讀音上的變化,不過當單獨數字 2 後面接特定中文字時語音會把數字 2 讀成「兩」,例如「有 2 張紙、第 2 名」,改變讀音的反而是數字而不是中文字,如果希望數字 2 不要讀成「兩」可以建立以下的規則。

原來文字:(?<!\d)2 ?([張名])
替代文字:貳\1

(?<!\d) 為條件判斷,表示數字 2 的左邊沒有數字才符合單獨數字 2 的情況,數字 2 與後面的中文字之間有時會有空格,所以空格加上問號表示可以有空格或沒有空格,[張名] 用小括弧包起來轉換為群組以獲得群組編號,如此在替代文字欄位即可使用群組參照的方式取得該群組的內容,數字 2 在替代文字欄位以中文字的「二」替換就不會再讀成「兩」了。

值得一提的是,條件判斷的 (?<!\d) 或許有人會改為 (?<=\D),兩者看似相同但其實不一樣,使用 (?<=\D) 表示 2 的左邊有非數字的字元,使用 (?<!\d) 只表示 2 的左邊排除數字,並不表示一定有非數字的字元,也可以是沒有任何字元的情況,當數字 2 在一行的最開頭時 2 的左邊沒有其它字元,若使用 (?<=\D) 無法包含這樣的情況。

會使數字 2 讀成「兩」的中文字不會只有「張、名」而已,所以規則的中括弧裡的字會在你發現新的字時繼續增加,或者若要數字 2 的後面接中文字時都不要把 2 讀成「兩」,建立的規則如下。

原來文字:(?<!\d)2 ?([\u4e00-\u9fff])
替代文字:二\1

[\u4e00-\u9fff] 表示任何一個中文字,此部分也可改為條件判斷的方式即 2 的右邊有中文字,建立的規則如下。

原來文字:(?<!\d)2 ?(?=[\u4e00-\u9fff])
替代文字:二

再來看範例三表示多少錢的情況,當數字前沒有加上貨幣符號,語音朗讀的方式其實是正常的,而多了貨幣符號後才變了個樣。
首先找出數字構成的規則,以有千分位符號的 123,456,789 為例,千分位符號是從個位數開始往左每三位數才有一個,也就是當出現千分位符號代表右邊一定有三位數字,所以千分位符號及右邊的三位數字可視為一個群組,這個群組可重複 0 次或 1 次以上,0 次表示只有最左邊 123 的部分,數字的規則就會是:

(,\d{3})*

而最左邊 123 的部分,當有右邊千分位符號的數字群組則最少一位數最多三位數,若沒有右邊千分位符號的數字群組即成為沒有千分位符號的數字,此時最少一位數以上,數字的規則就會是:

\d+

貨幣金額可以有小數,我們再加上小數的部分,當有小數點才會有右邊的數字,最少一位小數,所以小數點及右邊的數字可視為一個群組,這個群組可重複 0 次或 1 次,0 次表示只有整數的部分,1 次表示有小數,數字的規則就會是:

(\.\d+)?

將上面三個部分的規則按數字構成順序組合起來並加上貨幣符號,完整的規則就會是:

\$\d+(,\d{3})*(\.\d+)?

考慮朗讀替代文字欄位內容的需求,貨幣符號後面整串數字的部分要被讀出來,因此需要把這個部分用小括弧包起來轉換為群組以獲得群組編號,如此在替代文字欄位即可使用群組參照的方式取得該群組的內容,栭貨幣符號則是消音不讀出來,改以在整串數字的後面加上「元」這個字,最終的結果就會是:

原來文字:\$(\d+(,\d{3})*(\.\d+)?)
替代文字:\1元

說到數字,還有一個是瀏覽網頁很常見到的日期,例如 2023/5/13 或 2023-05-13,其中 2023/5/13 語音不會讀成「二零二三年五月十三日」,若想要以日期的方式讀出,只要將年月日中間的符號替換為「年」及「月」,最後面加上「日」就能達成。
首先建立日期的規則,西元年是四位數字,月及日的部份最少一位最多二位數字,而間隔年月日的符號是斜線,建立的規則就會是:

\d{4}/\d{1,2}/\d{1,2}

考慮朗讀替代文字欄位內容的需求,年月日的數字要被讀出來,因此需要把這三個部分分別用小括弧包起來轉換為群組以獲得群組編號,如此在替代文字欄位即可使用群組參照的方式取得三個群組的內容,栭間隔數字的符號則是分別改以「年、月」替換,最後面加上「日」,最終的結果就會是:

原來文字:(?<!\d)(\d{4})/(\d{1,2})/(\d{1,2})
替代文字:\1年\2月\3日

(?<!\d) 為條件判斷,表示四位數字的左邊沒有數字才認定為西元年。此規則當月或日的數字以例如「03」這種 0 開頭的數字呈現時,語音會讀成「零三月」或是「零三日」,不讀出「零」聽起來更順耳,規則修改如下。

原來文字:(?<!\d)(\d{4})/0?(\d{1,2})/0?(\d{1,2})
替代文字:\1年\2月\3日

月及日的數字若是 01 到 09,0 在替代文字欄位要消音不讀出來,因此 0 要與後面的數字分開不包含在 (\d{1,2}) 群組中,若是 1 到 9 或 10 以後的數字則開頭沒有 0,因此 0 加上問號表示可以有 0 或沒有 0。

當年月日的中間以斜線間隔時語音雖然不會以日期方式讀出,不過以小數點間隔例如 2023.5.13 語音反而會以日期方式讀出,NVDA 用戶指南中的章節編號例如 2.3.1. 或是 12.2.2. 同樣以小數點間隔,所以也會以日期方式讀出。
這種非西元年的數字不希望語音當作日期讀出,相同的處理方式也能用在對章節編號的讀法上,規則與日期的規則相近,原本西元年的四位數字改為二位數字,而間隔數字的符號是小數點,小數點在替代文字欄位以中文字的「點」替換,建立的規則如下。

原來文字:(?<!\d)(\d{1,2})\.(\d{1,2})\.(\d{1,2})
替代文字:\1點\2點\3

值得一提的是,語音朗讀替代文字欄位的內容會將「2點」讀成「兩點」,也就是單獨數字 2 讀成「兩」的情況,因此還需要搭配前面提過處理單獨數字 2 讀成「兩」的規則,不過由於字典項目中是由上往下處理規則項目的關係,因此處理單獨數字 2 讀成「兩」的規則必須放在處理章節編號的規則下面 (兩者不一定要相鄰),如此安排規則的順序才能處理得到章節編號的「2點」讀成「兩點」的問題。有了這兩個規則項目就能使章節編號以正常的方式讀出。

有些章節編號是以連字號間隔,例如 2-3-1 也被當作日期讀出,可建立相同的規則把小數點換成連字號,連字號在替代文字欄位以中文字的「之」替換,而像 2-2 這樣的章節編號若要讀成「二之二」的話作法亦相同,不妨試試看。

建立的規則如果包含符號在內,則符號在 NVDA 符號讀音中的設定變更也會影響規則的執行結果,例如前述提到的連字號在 NVDA 2023.2 之前的版本預設不將符號送到合成器,語音朗讀時會將連字號忽略當不存在。
以手機號碼 0912-345-678 為例,語音會把 345 及 678 當數值讀出而不是數字讀出,再者中間的連字號也沒有朗讀停頓的效果,與我們習慣的說法不同。
我們可以為此建立規則來修正朗讀的方式,中間三碼及後三碼的數字彼此間用空格隔開不連續就能以數字的方式讀出,數字間的連字號用英文逗點替換可以產生朗讀停頓的效果。

原來文字:(09\d{2})-(\d)(\d)(\d)-(\d)(\d)(\d)
替代文字:\1,\2 \3 \4,\5 \6 \7

NVDA 2023.2 以後的版本預設將連字號送到合成器,語音可將手機號碼依照我們習慣的方式讀出,原本建立的規則已不再需要可以刪除。

從本文提到的各種例子可知建立正規表達式的規則可能有不只一種方式,站在一般使用者的立場,只要按可理解的方式建立能夠達到目的的規則即可,不需太過講求正規表達式的技術細節與規則是否嚴謹。
隨著 NVDA 使用時間不斷累積,建立的字典項目也會有一定數量,尤其有些中文字會因為組成的詞或前後文的關係導致一字多音的情況,所以建立的規則當遇到新的情況時就需要修改調整使其更為完善,並非一成不變,不過建立的規則未必能涵蓋所有情況,因為你無法想到所有可能,此時以常用常見的為主建立規則就好,不要陷入面面俱到的泥淖之中。

經由以上的說明,相信對讀音字典的應用有了更進一步的認識,雖然正規表達式對 NVDA 使用者是陌生的領域,即便只是懂一點皮毛也能有效改善語音朗讀的聽覺體驗。

延伸閱讀