Wordの「検索と置換」について
この記事では、Wordの「検索と置換」(Find and Replace)を使うにあたって存在するいくつかの問題に関して述べる。
VBAによる文書全体での置換
Wordの「検索と置換」のGUIダイアログを用いて検索・置換を行う場合は、フッターや図形の中の文章など含めて文書の全てが対象となるが、VBAで同じことを行うのは実は結構難しい。Wordには、ユーザーの操作に応じてマクロを生成する「マクロの記録」という機能があるが、これを使って生成したマクロを実行しても普通のテキストしか置換されず、フッターや図形の中は対象にならない(そんな適当な「記録」機能は無い方がマシである)。
頑張って調べると、まさにこの問題について丁寧に解説したUsing a Macro to Replace Text Wherever It Appears in a Documentというサイトが見つかった。詳しくはそちらに譲るとして、最低限の内容だけ解説する。
まず、WordのVBAでFindが使える対象にはSelectionとRangeというものがあるようで、どちらも文章内のテキストを置換するのに使えるが、挙動が異なるようである。少なくとも、今回使うのは(そしておそらく使うのに適しているのは)Rangeのほうである。
この2つの違いについては以下も参照。
【Word VBA】RangeオブジェクトとSelectionオブジェクト及び【Word VBA】RangeオブジェクトとSelectionオブジェクト(2)
文書全体の置換のためには、簡潔にいえば、①文書内の必要なRangeを全て取得する②取得した全てのRangeごとに検索や置換を実行する、という2ステップが必要である。このうち難しいのは①のほうで、②は割と簡単である。とりあえず②を担当する関数の名前をMy_Find_and_Replace()
としておこう。そうすると、①の部分については、前述の英語サイトを参考に結論からいえば、以下のように書けばいいということになる。
Sub Macro0()
Set objUndo = Application.UndoRecord
objUndo.StartCustomRecord ("Replace All With VBA")
'Fix the skipped blank Header/Footer problem.
lngValidate = ActiveDocument.Sections(1).Headers(1).Range.StoryType
'Iterate through all story types in the current document.
Dim rngStory As Range
For Each rngStory In ActiveDocument.StoryRanges
'Iterate through all linked stories.
Do
My_Find_and_Replace rngStory
On Error Resume Next
Select Case rngStory.StoryType
Case 6, 7, 8, 9, 10, 11
If rngStory.ShapeRange.Count > 0 Then
For Each oShp In rngStory.ShapeRange
If oShp.TextFrame.HasText Then
My_Find_and_Replace oShp.TextFrame.TextRange
End If
Next
End If
Case Else
'Do Nothing
End Select
On Error GoTo 0
'Get next linked story (if any)
Set rngStory = rngStory.NextStoryRange
Loop Until rngStory Is Nothing
Next
objUndo.EndCustomRecord
End Sub
My_Find_and_Replace
が二か所あることに注意。また、objUndo
に関する部分は、これを設定しておくことでこのマクロによる置換操作全体が「ひとまとめの操作」と認識され、Ctrl+Zだけで全て戻せるようになる(これがないと何度か必要になる)。不要ならこの3行は削ってよい。
objUndo
関連以外はほぼそのまま元サイトの転載となってしまっているが、特に利用にあたって制限はないと書いてあるのと、初歩的な操作で使用頻度が高いにもかかわらずあまり知られていない貴重な情報であるということでご理解いただきたい。
②の部分であるMy_Find_and_Replace
の例は以下のようなものになるだろう。
Sub My_Find_and_Replace(r As Range)
With r.Find
.ClearFormatting
.Replacement.ClearFormatting
.text = "前"
.Replacement.text = "後"
.Font.Name = "MS 明朝"
.Replacement.Font.Name = "MS ゴシック"
.Wrap = wdFindContinue
.Execute Replace:=wdReplaceAll
End With
End Sub
この例ではMS明朝で書かれた「前」という文字をMSゴシックの「後」という文字に全て置換する。「MS」は全角である。wdFindContinueは文書の先頭に戻って置換を続けるオプションであるが、今回のようにSelectionではなくRangeに対してFindする場合はおそらく不要。その他色々とオプションがあるので調べてみるとよい。
「見出しのフォント」などの指定
選択範囲内にある特定のフォントの文字列を別のフォントに置き換えるWordマクロ | 初心者備忘録 この記事によると、見た目は普通のフォントでも内部的に「+見出しのフォント - 日本語
」などといったフォント指定がされている場合があるようである。その他にもいくつか落とし穴がありそうなので、フォントを置換するときは参考にするとよいだろう。
この記事でもRange対象の置換を行っているようなので上記の「Rangeを全て取得する」部分はそのまま応用が可能かと思われる。
ワイルドカードにOR検索がない
さすがに冗談だろうと思ったが、本当に無いらしい。Word ワイルドカード検索 例題シリーズ ≫ Or検索するには-教えて!HELPDESK が面白いので読んでみるとよい。
対処法としては、マクロで頑張って書くしかないだろう。
合字・異体字・BMP外の文字の検索・置換
Wordの検索・置換は、Unicodeの内部表現の単位ではなく、(拡張)書記素クラスタ(参考: Unicodeのgrapheme cluster (書記素クラスタ) | hydroculのメモなど)(簡単にいえば、カーソルを動かす際に1文字として扱われるまとまり)の単位でしか文字を認識してくれないようである。
例えば有名な「pͪoͣnͬpͣoͥnͭpͣa͡inͥ」の最初の「pͪ」は、内部的には普通のローマ字のpと合字用の上付きアルファベットのhという2文字で表現されているが、カーソルを移動させる限りではまとめて1文字のように扱われる(メモ帳などにコピペすれば確認できる)。従って、「pͪoͣnͬpͣoͥnͭpͣa͡inͥ」の「p」を全て削除しようと思ってWordの置換でpを””に置換しても、書記素クラスタとしては「p」に一致するものがないので何も起こらない。「pͪ」と書記素クラスタごと入れれば正しく置換される。「pの上に合字用アルファベットが乗ったもの」を全て消したりフォントを変えたりしたければ、以下のように合字用の文字をあわせてワイルドカード指定することになる(合字用の文字を入れているので奇妙な見た目になっているが問題なく動作する)。
p[ͣ-ͯ]
Wordの置換では正規表現のグループは使えないので「上付きアルファベットはそのままでpだけをbに変えてbͪoͣnͬbͣoͥnͭbͣa͡inͥにする」などはおそらく単一の「検索と置換」の実行では不可能である。もちろんマクロで場合分けしてやればできるはず。
また、合字と呼ぶのかわからないが異体字セレクタを用いて表現された異体字についても同様で、例えば「艹︀」は見た目は1文字だが内部的にはU+8279とU+FE00の2文字であるため、U+8279(艹)を対象として置換しても何も起こらない。
BMP外の文字の範囲検索・置換
Unicodeの文字は65536文字(16進数では”10000”文字)ごとに一つの「面(Plane)」として分けられており、U+0000からU+FFFFまでの最初の65536文字がBMP(Basic Multilingual Plane, 基本多言語面)と呼ばれる。BMPの外にある文字、すなわち16進数で5桁(以上)になる文字については、扱いが変わるため、ソフトによってサポートが不十分な場合がある。普段使う多くの文字はBMP内にあるが、「𩸽(ほっけ)」など一部の漢字や「🥺」のような絵文字はBMP外の文字である。
Wordでは、BMP外の文字を普通に入力して使う分には全く問題なく、「検索と置換」でも多くのユースケースでは問題ないのだが、ワイルドカードの-(ハイフン)を使用した範囲検索がうまくできないという不具合がある。例えば、U+20000-U+2FFFFの文字を一括で削除しようとして「検索と置換」ダイアログに[𠀀-]
などと入力しても「[検索する文字列]に指定した範囲が正しくありません。」とエラーが出てしまう。このエラーは[9-0]のようにUnicode上の順序が逆転した範囲を指定したときに表示されるものである。
実はここでも先ほどの「合字」と似たようなことが起こっている。どうやらWordは内部的に文字コードとしてUTF-16を使用しているようで、UTF-16ではBMP外の文字は「サロゲート文字」と呼ばれるものを2文字組み合わせて表現される(サロゲートペアと呼ばれる)(詳しくは他サイトを参照)。従って、先ほどの𠀀
も、見た目としては1文字でも、内部的には合字と同じようにU+D840とU+DC00という2つの文字で表現されているということになる。同じくU+2FFFFはU+D87FとU+DFFFの2文字であるから、先ほどの検索クエリは、意味としては[<U+D840><U+DC00>-<U+D87F><U+DFFF>]、つまり「<U+D840>と、<U+DC00>から<U+D87F>までと、<U+DFFF>」を対象にしたものと解釈されてしまう。しかし、<U+DC00>は<U+D87F>よりUnicode上で後の文字であり、範囲指定の最初と最後の位置が逆転しているため、「[検索する文字列]に指定した範囲が正しくありません。」と表示されたのである。
というわけで、BMP外の文字を範囲指定で検索・置換するには、サロゲート文字を用いて2文字まとめてヒットするようにすればよい。例えばU+20000-U+2FFFFであれば
[<U+D840>-<U+D87F>][<U+DC00>-<U+DFFF>]
というクエリ(分かりやすいように表記しただけで、もちろんこのままでは使えない)を作ればよいということになる。
なお、こうしてサロゲートペアという「組み合わせ」で指定する以上、置換したい対象のUnicode範囲を単純に上のようなクエリに直せるとは限らないことに注意が必要である。すなわち、この方式の単一のクエリで置換対象にできるのは「上位サロゲート文字0xD800~0xDBFFと下位サロゲート文字0xDC00~0xDFFFによる1024×1024の表にサロゲート文字を並べたときに長方形の枠で囲える範囲」だけである。実際にこの表でU+20000-U+2FFFFがどこになるか見てみると
DC00 | DC01 | … | DFFE | DFFF | |
---|---|---|---|---|---|
D800 | 10000 | 10001 | … | 103FE | 103FF |
… | … | … | … | … | … |
D840 | 20000 | 20001 | … | 203FE | 203FF |
… | … | … | … | … | … |
D87F | 2FC00 | 2FC01 | … | 2FFFE | 2FFFF |
… | … | … | … | … | … |
DBFF | 10FC00 | 10FC01 | … | 10FFFE | 10FFFF |
と、めでたく長方形の枠内に収まっているが、そうでない範囲を指定するときは(ORも使えないので)最大3回クエリを実行することになるだろう。
また、先ほど異体字セレクタとして例に挙げたU+FE00はStandardized Variation Sequence (SVS)で利用されるもので、BMP内のブロックであるVariation Selectors(U+FE00-U+FE0F)のものであるが、もう一つの異体字シーケンスの枠組みであるIdeographic Variation Sequence (IVS)で用いられる異体字セレクタはBMP外のはるか彼方にあるVariation Selectors Supplement(U+E0100-U+E01EF)のものであるため、これに対してもサロゲートペアを使用する必要がある。これも
U+<DB40>[<U+DD00>-<U+DDEF>]
のようにちゃんと1つのクエリで表現できる。
実際に置換する
ここまでサロゲート文字を<U+D840>のように模式的に表現してきたが、実際にWordでこれを入力するにはどうすればいいのかというと、一つはまずAlt+XというWordの便利機能を使って無理やりサロゲート文字を生成して(当然「豆腐」で表示される)それを頑張って切り貼りするなどして置換するという方法がある。しかしかなり不自然な表示になったりして扱いづらいので普通はVBAを用いるのがいいだろう。VBAではChrW(&HD840)
のようにして16進数で文字を指定することができる。
例えば筆者が公開している謎乃明朝(文字数が多いため「謎乃明朝」「謎乃明朝+」の2フォントに分かれている)がサポートしている全ての文字で使われるように置換する場合は以下のようになる。与えられたRangeに対して置換するという前述の②の部分だけ載せるので適宜①の部分と組み合わせること。
Sub SearchAndReplaceInStory(myStoryRange As Range)
Dim CJK As String
Dim CJKComp As String
Dim CJKA As String
Dim IVS As String
Dim SVS As String
Dim CJKBtoF As String
Dim CJKCompS As String
Dim TIP As String
Dim KRadi As String
Dim RadiSup As String
CJK = "[" & ChrW(&H4E00) & "-" & ChrW(&H9FFF) & "]"
CJKComp = "[" & ChrW(&HF900) & "-" & ChrW(&HFAFF) & "]"
CJKA = "[" & ChrW(&H3400) & "-" & ChrW(&H4DBF) & "]"
KRadi = "[" & ChrW(&H2F00) & "-" & ChrW(&H2FDF) & "]"
RadiSup = "[" & ChrW(&H2E80) & "-" & ChrW(&H2EFF) & "]"
IVS = ChrW(&HDB40) & "[" & ChrW(&HDD00) & "-" & ChrW(&HDDEF) & "]"
SVS = "[" & ChrW(&HFE00) & "-" & ChrW(&HFE0F) & "]"
CJKBtoF = "[" & ChrW(&HD840) & "-" & ChrW(&HD87D) & "][" & ChrW(&HDC00) & "-" & ChrW(&HDFFF) & "]"
CJKCompS = ChrW(&HD87E) & "[" & ChrW(&HDC00) & "-" & ChrW(&HDFFF) & "]"
TIP = "[" & ChrW(&HD880) & "-" & ChrW(&HD8BF) & "][" & ChrW(&HDC00) & "-" & ChrW(&HDFFF) & "]"
replaceFont myStoryRange, "謎乃明朝 Regular", CJK
replaceFont myStoryRange, "謎乃明朝 Regular", CJKComp
replaceFont myStoryRange, "謎乃明朝 Regular", CJKA
replaceFont myStoryRange, "謎乃明朝 Regular", KRadi
replaceFont myStoryRange, "謎乃明朝 Regular", RadiSup
replaceFont myStoryRange, "謎乃明朝 Regular", CJKCompS
replaceFont myStoryRange, "謎乃明朝 Regular", CJK & IVS
replaceFont myStoryRange, "謎乃明朝 Regular", CJKA & IVS
replaceFont myStoryRange, "謎乃明朝 Regular", CJKBtoF & IVS
replaceFont myStoryRange, "謎乃明朝 Regular", TIP & IVS
replaceFont myStoryRange, "謎乃明朝 Regular", CJK & SVS
replaceFont myStoryRange, "謎乃明朝 Regular", CJKA & SVS
replaceFont myStoryRange, "謎乃明朝 Regular", CJKBtoF & SVS
' replaceFont myStoryRange, "謎乃明朝 Regular", TIP & SVS
replaceFont myStoryRange, "謎乃明朝+ Regular", CJKBtoF
replaceFont myStoryRange, "謎乃明朝 Regular", TIP
End Sub
Sub replaceFont(myStoryRange As Range, fontName As String, text As String)
myStoryRange.Find.ClearFormatting
myStoryRange.Find.Replacement.ClearFormatting
myStoryRange.Find.Replacement.Font.Name = fontName
With myStoryRange.Find
.text = text
.Replacement.text = ""
.Wrap = wdFindContinue
.Format = True
.MatchWholeWord = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchFuzzy = False
.MatchWildcards = True
.Execute Replace:=wdReplaceAll
End With
End Sub
康煕部首なども置換するため長ったらしくなってしまったが、サロゲート文字をChrWで指定するというところさえ理解してしまえば、やっていることは単純である。ChrW(&H4E00)などは普通に漢字で「一」と書いてももちろん構わない。
20000-2FFFFのうち謎乃明朝+に収録されているのはCJK統合漢字拡張B, C, D, E, Fで、U+2F800(サロゲートペアでいうと<U+D87E><U+DC00>)以降にあるCJK互換漢字補助は謎乃明朝に収録されている(これは元ネタの花園明朝Aと花園明朝Bについても同様である)。そのため
CJKBtoF = "[" & ChrW(&HD840) & "-" & ChrW(&HD87D) & "][" & ChrW(&HDC00) & "-" & ChrW(&HDFFF) & "]"
CJKCompS = ChrW(&HD87E) & "[" & ChrW(&HDC00) & "-" & ChrW(&HDFFF) & "]"
と範囲を分けて書いている。
TIPは第三漢字面(Tertiary Ideographic Plane)であり、30000-3FFFFを指定している。TIP & SVS
がコメントアウトされているのは単に現在TIPの文字がSVSに登録されておらず対応が不要だからであり、将来登録される可能性はある。