私は仕事において、第三者が作成したWord文書が指定されたフォーマットに従っているかどうかをチェックすることをしています。たとえば文章中のコンマ「,」やピリオド「.」は全角にするとか、数字を列挙するときは「1, 2, 3, 4」のように半角の数字を半角のコンマとスペースで連結するといった独自のフォーマットルールです。ですが、コンマやピリオド、スペースはそれが半角なのか全角なのかは、Wordファイルを見ただけではすぐに分かりません。しかもWordの検索機能はあまり使いやすくなく、半角と全角をうまく区別してくれません。以前は一つ一つカーソルの動きを見て半角かどうかをチェックしていたのですが、とても苦痛な作業でした。
プログラムで色付けして、全角なのか半角なのかをハイライトすれば一目で区別できるのではないかと思って作成したのが、今日紹介するPythonのプログラムです。全角と半角で色分けをしてくれるので、仕事がかなりラクになりました。同じような業務をしている人のお役に立てればと思います。
準備(ライブラリのインストール)
前回同様、Python-Docxライブラリを使いますので、事前にインストールしておく必要があります。
コマンドプロンプトで以下のコマンドを入力して、Python-Docxライブラリをインストールしてください。
|
1 |
pip install python-docx |
プログラムの動作
下のようなWord文章があるとします。この文章には半角のスペースやコンマ、ピリオド、数字が混ざっているのですが、それらを全て全角にしたいとします。

あとで紹介するプログラムを実行するとファイルを選択するダイアログが開くので、チェックしたいWordファイルを選択します。

選択後、少し待つと処理が終わり、ハイライトされた文章を保存するためのダイアログが表示されます。元のファイル名の末尾に「(ハイライト)」という文字列が自動的に入るので、そのまま保存しても元のファイルとは別ファイルで保存されます。たとえば「Wordファイル.docx」は「Wordファイル(ハイライト).docx」となります。

保存されたファイルを開いてみると以下のようにハイライトされています。半角のコンマは明るい緑、全角のコンマは濃い緑、半角の数字は赤、全角の数字はピンクに、半角のスペースは青、全角のスペースは水色にハイライトされているので、一目で半角と全角の区別がつきます。

プログラムのソースコード
以下がPythonのコードになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
import copy import re import os import tkinter as tk import tkinter.filedialog as fd from docx import Document from docx.enum.text import WD_COLOR_INDEX def find_occurances_in_paragraph(pattern, paragraph): res1 = [] res2 = [] for m in pattern.finditer(paragraph.text): res1.append(m.start()) res2.append(m.end()) #print(res1) #print(res2) return res1, res2 def get_target_runs(paragraph, start, end): i = 0 i_start = 0 sum_tmp = 0 targets = [] for index1, index2 in zip(start, end): past_start = False while(i < len(paragraph.runs)): for r in paragraph.runs[i_start:i]: sum_tmp = sum_tmp + len(r.text) run = paragraph.runs[i] i_start = i run_start = sum_tmp run_end = run_start + len(run.text) run_contains_start = (run_start <= index1 <= run_end) run_contains_end = (run_start <= index2 <= run_end) #Split run in three, take middle part if(run_contains_start and run_contains_end): if index2-run_end == 0: run_end = 0 split_runs = split_run_in_three(paragraph, run, index1-run_start, index2-run_end) targets.append(split_runs[1]) #print([r.text for r in targets]) break #Split run, take second half elif(run_contains_start and not run_contains_end): past_start = True split_runs = split_run_in_two(paragraph, run, index1-run_start) #print([1, split_runs[1].text]) targets.append(split_runs[1]) i += 1 #skip run that was added by splitting run #Take whole run elif(past_start and not run_contains_end): #print([2, run.text]) targets.append(run) #Split run, take first half elif(past_start and run_contains_end): split_runs = split_run_in_two(paragraph, run, index2-run_start) #print([3, split_runs[0].text]) targets.append(split_runs[0]) break i += 1 return targets def split_run_in_two(paragraph, run, split_index): index_in_paragraph = paragraph._p.index(run.element) text_before_split = run.text[0:split_index] text_after_split = run.text[split_index:] run.text = text_before_split new_run = paragraph.add_run(text_after_split) copy_format_manual(run, new_run) paragraph._p[index_in_paragraph+1:index_in_paragraph+1] = [new_run.element] return [run, new_run] def split_run_in_three(paragraph, run, split_start, split_end): #print(['three', split_start, split_end]) first_split = split_run_in_two(paragraph, run, split_end) second_split = split_run_in_two(paragraph, run, split_start) return second_split + [first_split[-1]] def copy_format_manual(runA, runB): fontB = runB.font fontA = runA.font fontB.bold = fontA.bold fontB.italic = fontA.italic fontB.underline = fontA.underline fontB.strike = fontA.strike fontB.subscript = fontA.subscript fontB.superscript = fontA.superscript fontB.size = fontA.size fontB.highlight_color = fontA.highlight_color fontB.color.rgb = fontA.color.rgb def ask_input_filename(msg = None, types = [('', '*.*')]): rt = tk.Tk() rt.withdraw() filename = fd.askopenfilename(title = msg, filetypes = types) rt.destroy() return filename def ask_output_filename(filename, msg = None, types = [('', '*.*')], deftype = '.txt'): rt = tk.Tk() rt.withdraw() filename = fd.asksaveasfilename(initialfile = filename, title = msg, filetypes = types, defaultextension = deftype) rt.destroy() return filename def main(): #=========================================================================== # ファイルを選択する #=========================================================================== input_filename = ask_input_filename('入力データファイル', types = [('Wordファイル(*.docx)', '*.docx')]) if input_filename == '': print('キャンセルされました。何かキーを押してください。') input() exit() #=========================================================================== # ファイルを開く #=========================================================================== try: doc = Document(input_filename) except: print('エラーが起こりました。ファイルに問題があります。\n何かキーを押してください。') input() exit() #=========================================================================== # 検索する正規表現と、ハイライトさせる色 #=========================================================================== searchs = ["[,.]", "[,.]", "[0-9]", "[0-9]", "[ ]", "[ ]"] hlcolors = [WD_COLOR_INDEX.BRIGHT_GREEN, WD_COLOR_INDEX.GREEN, WD_COLOR_INDEX.RED, WD_COLOR_INDEX.PINK, WD_COLOR_INDEX.BLUE, WD_COLOR_INDEX.TURQUOISE] #=========================================================================== for i, search in enumerate(searchs): format_func = lambda x:x.font.__setattr__('highlight_color', hlcolors[i]) pattern = re.compile(search) for paragraph in doc.paragraphs: start, end = find_occurances_in_paragraph(pattern, paragraph) runs = get_target_runs(paragraph, start, end) for run in runs: format_func(run) #=========================================================================== # ハイライトしたファイルを保存する #=========================================================================== initial_filename = os.path.basename(input_filename) initial_filename = initial_filename[:-5] + '(ハイライト).docx' output_filename = ask_output_filename(initial_filename, msg = '出力ファイル', types = [('テキスト (*.docx)', '*.docx')]) # キャンセルされた場合 if output_filename == '': print('キャンセルされました。何かキーを押してください。') input() exit() doc.save(output_filename) if __name__ == "__main__": main() |
こちらからソースコードをダウンロードできます。
プログラムの解説
簡単にプログラムの解説をしていきます。
まずは必要なライブラリをインポートします。
|
1 2 3 4 5 6 7 |
import copy import re import os import tkinter as tk import tkinter.filedialog as fd from docx import Document from docx.enum.text import WD_COLOR_INDEX |
find_occurances_in_paragraph関数でWord文章(paragraph)から正規表現(pattern)に一致する部位を見つけます。
get_target_runs関数は、find_occurances_in_paragraph関数が見つけた正規表現に対応するWordの文章部分を取り出します。
split_run_in_two関数とsplit_run_in_three関数はget_target_runs関数から呼ばれる関数です。またcopy_format_manual関数はsplit_run_in_two関数から呼ばれる関数です。
|
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
def find_occurances_in_paragraph(pattern, paragraph): res1 = [] res2 = [] for m in pattern.finditer(paragraph.text): res1.append(m.start()) res2.append(m.end()) #print(res1) #print(res2) return res1, res2 def get_target_runs(paragraph, start, end): i = 0 i_start = 0 sum_tmp = 0 targets = [] for index1, index2 in zip(start, end): past_start = False while(i < len(paragraph.runs)): for r in paragraph.runs[i_start:i]: sum_tmp = sum_tmp + len(r.text) run = paragraph.runs[i] i_start = i run_start = sum_tmp run_end = run_start + len(run.text) run_contains_start = (run_start <= index1 <= run_end) run_contains_end = (run_start <= index2 <= run_end) #Split run in three, take middle part if(run_contains_start and run_contains_end): if index2-run_end == 0: run_end = 0 split_runs = split_run_in_three(paragraph, run, index1-run_start, index2-run_end) targets.append(split_runs[1]) #print([r.text for r in targets]) break #Split run, take second half elif(run_contains_start and not run_contains_end): past_start = True split_runs = split_run_in_two(paragraph, run, index1-run_start) #print([1, split_runs[1].text]) targets.append(split_runs[1]) i += 1 #skip run that was added by splitting run #Take whole run elif(past_start and not run_contains_end): #print([2, run.text]) targets.append(run) #Split run, take first half elif(past_start and run_contains_end): split_runs = split_run_in_two(paragraph, run, index2-run_start) #print([3, split_runs[0].text]) targets.append(split_runs[0]) break i += 1 return targets def split_run_in_two(paragraph, run, split_index): index_in_paragraph = paragraph._p.index(run.element) text_before_split = run.text[0:split_index] text_after_split = run.text[split_index:] run.text = text_before_split new_run = paragraph.add_run(text_after_split) copy_format_manual(run, new_run) paragraph._p[index_in_paragraph+1:index_in_paragraph+1] = [new_run.element] return [run, new_run] def split_run_in_three(paragraph, run, split_start, split_end): #print(['three', split_start, split_end]) first_split = split_run_in_two(paragraph, run, split_end) second_split = split_run_in_two(paragraph, run, split_start) return second_split + [first_split[-1]] def copy_format_manual(runA, runB): fontB = runB.font fontA = runA.font fontB.bold = fontA.bold fontB.italic = fontA.italic fontB.underline = fontA.underline fontB.strike = fontA.strike fontB.subscript = fontA.subscript fontB.superscript = fontA.superscript fontB.size = fontA.size fontB.highlight_color = fontA.highlight_color fontB.color.rgb = fontA.color.rgb |
こちらで検索する正規表現と、対応させるハイライトの色を指定しています。
|
145 146 147 148 149 150 151 152 153 154 |
#=========================================================================== # 検索する正規表現と、ハイライトさせる色 #=========================================================================== searchs = ["[,.]", "[,.]", "[0-9]", "[0-9]", "[ ]", "[ ]"] hlcolors = [WD_COLOR_INDEX.BRIGHT_GREEN, WD_COLOR_INDEX.GREEN, WD_COLOR_INDEX.RED, WD_COLOR_INDEX.PINK, WD_COLOR_INDEX.BLUE, WD_COLOR_INDEX.TURQUOISE] |
詳しい正規表現の説明はこちらを参照してください。
https://murashun.jp/article/programming/regular-expression.html
またPython-DocxライブラリのWD_COLOR_INDEXでハイライトできる色は以下のようになります。

指定した正規表現の数だけforループで回し(このサンプルプログラムでは6つ)、文章全体から該当する部分を見つけ、見つかった部分をハイライトしています。
|
156 157 158 159 160 161 162 163 |
for i, search in enumerate(searchs): format_func = lambda x:x.font.__setattr__('highlight_color', hlcolors[i]) pattern = re.compile(search) for paragraph in doc.paragraphs: start, end = find_occurances_in_paragraph(pattern, paragraph) runs = get_target_runs(paragraph, start, end) for run in runs: format_func(run) |
ハイライトが終わったらファイルを保存します。
|
165 166 167 168 169 170 171 172 173 174 175 176 177 |
#=========================================================================== # ハイライトしたファイルを保存する #=========================================================================== initial_filename = os.path.basename(input_filename) initial_filename = initial_filename[:-5] + '(ハイライト).docx' output_filename = ask_output_filename(initial_filename, msg = '出力ファイル', types = [('テキスト (*.docx)', '*.docx')]) # キャンセルされた場合 if output_filename == '': print('キャンセルされました。何かキーを押してください。') input() exit() doc.save(output_filename) |
以上が簡単な説明です。Wordファイルから正規表現に一致する部分を見つけてくるロジックはこちらを参考にさせていただきました(多少改変してあります)。
https://github.com/Sriram2001/Document-Highlighter/blob/master/source.py
もし参考になるようでしたら幸いです(動作に責任は持てませんので、お試しになる場合は必ず元のファイルをバックアップしておいてください)。