latexのソースからMeCabを用いて名詞だけ抜き出す

■記事の概要:latexのソースから形態素解析器を用いて名詞だけ抜き出したリストを作る.
■環境:MinGW または MSYS2 (Cygwin でも多分大丈夫だろうな~と思うが試していない)
■主な道具:MeCab, python3, make

■動機と背景:latexで作成しているPDFファイルのページ数が増えてきたので,索引を付けようと考えた.(索引はlatex の\indexという命令で作れるが,この記事ではこれ以上触れない).最初は索引の項目を手で付けていたが,思いついたものを十個ぐらい挙げたら飽きてしまった.そこで,《latexソースファイルの中から名詞だけ抜き出す》ことを考えた.

■環境整備
1.MinGW または MSYS2 については環境整備が済んでいるものとする.
2.MeCabのインストール:Windows用に配布されているバイナリを使う.公式からリンクされているダウンロードページ(http://taku910.github.io/mecab/#download)からBinary package for MS-Windowsという項目の直下の mecab-0.996.exe をダウンロードし,インストールした.インストール時にエンコーディングを指定するように言われる.今回の目的はlatexのソース処理なので utf-8 を選ぶ.(デフォルトだとShift-JPだったように記憶している).
3.Pythonのインストール:Windows用に配布されているバイナリを使う.公式ページ(https://www.python.org/)からDownload/latestに飛び,そこから「Windows x86-64 executable installer」と題されたファイルをダウンロードし,インストールした.この記事が書かれた時点での最新版は3.6.4であった.
4.パスを通す.MecabとPythonがMinGWシェルから見えるようにパスを通す必要がある.(MinGW なら
C:\MinGW\msys\1.0\home\YourUserName, MSYS2 なら C:\msys64\home\YourUserName などに置いてある).bashrc の下の方に次のような行を加える:

export MECABPATH=/c/MeCab
export PATH=$PATH:$MECABPATH/bin

export PYTHONPATH=/c/Python3.6.4
export PATH=$PATH:$PYTHONPATH
export PYTHONIOENCODING=UTF-8

※《/c/MeCab》や《/c/Python3.6.4》は各自のインストールの実態に合わせて適宜変更してほしい.
※最後の《export PYTHONIOENCODING=UTF-8》という行はパスを通しているのではなく,ユニコード文字(utf-8)をpython3で扱うときに必須らしい設定である.

■MeCab用のユーザー辞書作成
MeCab には「微分」や「積分」のような一般的な用語は(Windowsバイナリに同梱の)辞書ファイルに登録されている.しかし,それほど一般的でない用語は登録されておらず,バラバラの言葉や文字として認識されてしまう.そこでユーザ辞書を作成する.ユーザ辞書の元になるデータは次のような形式のcsvファイルである:

無限小,,,10,名詞,一般,*,*,*,*,個別名,アアアア,REGISTERED
無限大,,,10,名詞,一般,*,*,*,*,個別名,アアアア,REGISTERED
無限小量,,,10,名詞,一般,*,*,*,*,個別名,アアアア,REGISTERED
無限大量,,,10,名詞,一般,*,*,*,*,個別名,アアアア,REGISTERED

「アアアア」などとなっている箇所は本来きちんとした読みを登録するのだが面倒であり,今回の業務と無関係なのでこのようにしてある.(今回は説明を省略するが,一行に一つ単語を書いたファイルからこのようなファイルを自動生成している.)「REGISTERED」は後で解析結果を加工するためのヒントとするための追加タグである.

■Makefileの作成
次のようなMakefileを用意する.

help:
	@grep -E '^[0-9a-zA-Z_-]+[[:blank:]]*:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[1;32m%-30s\033[0m %s\n", $$1, $$2}'

dict : mecab.csv ## add user dictionary for mecab by mecab.csv
	mecab-dict-index -d$(MECABPATH)/dic/ipadic -u ./mecab.dic -f utf8 -t utf8 mecab.csv

mecab : hoge.tex ## analyze hoge.tex by mecab
	$(eval MECAB_OUT := $(strip $(basename .tex $ $(UNIQ)
	python extractNoun.py  $(UNIQ) > $(RESULT)
	rm  $(MECAB_OUT) $(UNIQ)

make dict としてやると mecab.csv が処理され mecab.dic というMeCab用ユーザー辞書が作成される.(helpターゲットのルールについては以前の記事を参考にしてほしい).

make mecab としてやるとまずターゲットの hoge.tex が MeCab で処理され,hoge-out.txt が生成される.このファイルはhoge.texのすべての内容を品詞分解して一行に一つずつ並べたものになっているので重複が多い.そこでhoge-out.txt sort し uniq して重複を除去して hoge-out-u.txtを生成する.(前段の sed については以前の記事を参考にされたい.)今回は「名詞」とタグ付けされたものだけ欲しいので,hoge-out-u.txt を自作のpythonスクリプト extractNoun.py で処理して最終結果の hoge-nouns.txt を得る.

★Makeルールの中で変数を定義する方法については『makefile 動的に変数に代入 共通して使う』を参考にさせて頂いた.

■pythonスクリプト extractNoun.py

# -*- coding: utf-8 -*-
import sys
import re #正規表現
import codecs
import copy

reNegIgnoreFlag = re.compile('REGISTERED')
reNeg = re.compile('接頭詞')
rePos = re.compile('(.+?)名詞')
reNum = re.compile('(\d+)')
reSymbols = re.compile('[!\$%\#&\(\){}\+,\-@\.\\\\=\"\'\*:;\?–\[\]|_`\^,]+')

def main():
    f = open(sys.argv[1], 'r', encoding="utf-8_sig")
    input = f.readlines()

    for line in input:
        myFilter(line)

def myFilter(line):
    pos = rePos.search(line)
    neg1 = reNeg.search(line)
    neg2 = reNegIgnoreFlag.search(line)

    if pos and (not neg1) and (not neg2):
        result = pos.group(1).strip()
        isnumber = reNum.search(result)
        containsSymbols = reSymbols.search(result)
        if (not isnumber) and (not containsSymbols):
            sys.stdout.write(result)
            sys.stdout.write('\n')

if __name__ == '__main__' : main()

解説:関数 main() では,一番目の引数 sys.argv[1] として与えられたファイル名を持つファイルを開き,readlines で行の集まりに分解し,各行を関数 myFilter() で処理している.
関数 myFilter() では,事前にコンパイルされた5つの正規表現 reNegIgnoreFlag,reNeg,rePos,reNum,reSymbols を使って「欲しそうなものを抜き出し,要らないものを弾く」ことをやっている.それぞれの正規表現の目的は次の通り:

・reNegIgnoreFlag:MeCabのユーザー辞書に登録済みの単語を検出するため.(MeCabのユーザー辞書のcsvファイルは索引づくりのヒントファイルを兼ねており,そこにまだ登録されていない単語を探すのが今回の目的なので).
・reNeg:「名詞」タグが付いているもののうち,さらに「接頭詞」というタグが付いたものを排除するため.このタグがつくものは「不」「無」「第」「最」などの一文字単語(?)であり,今回の目的にはノイズなので弾く.
・rePos:「名詞」タグを検出するため.
・reNum:年号などの数字列を弾くため.
・reSymbols:latexソースを自然言語解析器にかけた結果として大量の無意味記号列が(MeCabによって,仕方なく)「名詞」に分類されてしまうためそれらを弾くため.

広告

Makefile自己文書化の仕組み(メモ)

Makefileを自己文書化するという翻訳記事を見つけた.

Makefileをこんな風に作ったとする:

help:
	@grep -E '^[0-9a-zA-Z_-]+[[:blank:]]*:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[1;32m%-30s\033[0m %s\n", $$1, $$2}'

foo : bar.cc baz.cc  ## compiles foo

bar : aaa.cc bbb.cc ddd.cc

baz : hoge.h poyo.cc  ## makes baz

$ make すると help が実行されて

baz                            makes baz
foo                            compiles foo

のように出力される.この help ターゲットに書かれてるワンライナーがこの仕事を引き受けてくれている.(なお,オリジナルのワンライナーを少し改造してある).あとで挙動を変えてみたくなるかもしれないので,このスクリプトが何をやっているのか調べてみた.

tricks in the 'self documented makefile'

Makefile自己文書化の鍵となるワンライナー

[[:blank:]]
オリジナルのワンライナーから変えたところの一つです.私は行儀が悪いので make のターゲットを
targetname: hogehoge
ではなく
targetname : hogehoge
のようにターゲット名の後ろにも空白を置く癖があります.grep で空白にマッチする正規表現は[:blank:]なので,任意個の空白は空白の文字クラス[[:blank:]]によって[[:blank:]]*と表されるわけです.

Field Separator
FS = ":.*?##" のようにフィールドセパレータが定義されている.これにより## に引き続いてコメントが置かれたMakefileのターゲットの行が,ターゲット名($1)とコメント部分($2)に分かれてくれます.

$(MAKEFILE_LIST)
makefile の名称を取り出します.言っていることがわかりにくいですか? ではこうしましょう.
OtherMakefile という名前のファイル

.PHONY : a
a :
    @echo "this file is: $(MAKEFILE_LIST)"

として作り,ファイル名指定オプション -f を使って make を呼びます:
$ make -f OtherMakefile
すると
this file is: OtherMakefile
と返ってくるはずです.

カラーコード
\033m[0m で色の設定が解除されています.次のMakefileで色々試せます:

attribute :
	@awk 'BEGIN{printf "\033[1;32mhello\033[0m\n"}'
	@awk 'BEGIN{printf "\033[2;32mhello\033[0m\n"}'
	@awk 'BEGIN{printf "\033[3;32mhello\033[0m\n"}'
	@awk 'BEGIN{printf "\033[4;32mhello\033[0m\n"}'
	@awk 'BEGIN{printf "\033[5;32mhello\033[0m\n"}'
	@awk 'BEGIN{printf "\033[7;32mhello\033[0m\n"}'

foreground :
	@awk 'BEGIN{printf "39:Default foreground color [\033[1;39mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "30:Black [\033[1;30mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "31:Red [\033[1;31mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "32:Green [\033[1;32mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "33:Yellow [\033[1;33mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "34:Blue [\033[1;34mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "35:Magenta [\033[1;35mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "36:Cyan [\033[1;36mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "37:Light Gray [\033[1;37mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "90:Dark Gray [\033[1;90mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "91:Light Red [\033[1;91mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "92:Light Green [\033[1;92mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "93:Light Yellow [\033[1;93mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "94:Light Blue [\033[1;94mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "95:Light Magenta [\033[1;95mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "96:Light Cyan [\033[1;96mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "97:White [\033[1;97mHere\033[0m]\n"}'

background:
	@awk 'BEGIN{printf "49:Default background color [\033[0;49mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "40:bg:Black [\033[0;40mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "41:bg:Red [\033[0;41mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "42:bg:Green [\033[0;42mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "43:bg:Yellow [\033[0;43mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "44:bg:Blue [\033[0;44mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "45:bg:Magenta [\033[0;45mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "46:bg:Cyan [\033[0;46mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "47:bg:Light gray [\033[0;47mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "100:bg:Dark gray [\033[0;100mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "101:bg:Light red [\033[0;101mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "102:bg:Light green [\033[0;102mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "103:bg:Light yellow [\033[0;103mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "104:bg:Light blue [\033[0;104mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "105:bg:Light magenta [\033[0;105mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "106:bg:Light cyan [\033[0;106mHere\033[0m]\n"}'
	@awk 'BEGIN{printf "107:bg:White [\033[0;107mHere\033[0m]\n"}'

Makefile で basename や strip を使う(自分用メモ)

こんな Makefile があったとします:

.PHONY : aaa

aaa : bbb.hs   
    ghc  bbb.hs
    ./bbb

Makefile に少し慣れてくると,$< を使って

.PHONY : aaa

aaa : bbb.hs   
    ghc  $<
    ./bbb

のように書くかもしれませんが,今度は ./bbb の箇所が気になります.しかし,コマンド basename を使って

### DOES NOT WORK
.PHONY : aaa

aaa : bbb.hs   
    ghc  $<
    ./$(basename .hs $<)

としてやってもうまくいきません.先頭に空白が残るからです.そこで Make のコマンド strip を使って

aaa : bbb.hs
    ghc $<
    ./$(strip $(basename .hs $<))

としてやると,期待した通りの動作になります.