"BOKU"のITな日常

還暦越えの文系システムエンジニアの”BOKU”は新しいことが大好きです。

置換前と後の単語の組合せを辞書管理し、文章の複数単語を一括置換/pythonのtranlate()とre.sub()

今回は、文章中のある単語を別の単語に置き換える処理を、辞書を使って一括処理したい時の2つのやり方「str.translate()とre.sub()」を整理してみます。

f:id:arakan_no_boku:20190223210405j:plain

 

 今回やりたいこと

 

例えば、こんな文章があった時。

東京大学大阪大学では社会人向けの「AI講座」を開いている。
また経済産業省は「AIプログラム技術」や課題解決を企業や学生らが教え合う「AI学校」を9月にも立ち上げる。
実務経験を積んだ人材が互いに鍛え合い、専門人材を育成する。

「」に囲まれたAIがついた単語・・「AI講座」や「AI学校」・「AIプログラム技術」など・・を、「AI関連」みたいな一つの単語に置き換えるのなら、正規表現をつかってこんな感じで「re.sub()」で可能です。

intext = '''東京大学大阪大学では社会人向けの「AI講座」を開いている。
また経済産業省は「AIプログラム技術」や課題解決を企業や学生らが教え合う「AI学校」を9月にも立ち上げる。
実務経験を積んだ人材が互いに鍛え合い、専門人材を育成する。
'''
print(re.sub('「AI.+?」','「AI関連」',intext))

でも、「AI学校」は「AIスクール」、「AIプログラム技術」は「AIスキル」、「AI講座」は「AIクラス」に、それぞれ変更したい場合は、これでは無理です。

こんな時にどうしたらよいのか?・・をテーマとします。

 

 str.translate()は使えるか?

 

最初に頭に浮かぶ選択肢は「str.translate()」です。

これは置換対象が1文字の場合だと、見事に機能します。

例えば、半角の「A」を全角の「A」、「i」を全角の「I」、漢字の「実」を「業」に置き換えるような用途なら、以下のようにすればできます。

def trans_single(inputtext):
    encode_dic = {'A':'A','i':'I','実':'業'}
    encode_table = str.maketrans(encode_dic)
    return inputtext.translate(encode_table)
    
intext = '''東京大学大阪大学では社会人向けの「AI講座」を開いている。
また経済産業省は「AIプログラム技術」や課題解決を企業や学生らが教え合う「AI学校」を9月にも立ち上げる。
実務経験を積んだ人材が互いに鍛え合い、専門人材を育成する。
'''
print(trans_single(intext))

この結果はこんな感じ。

東京大学大阪大学では社会人向けの「AI講座」を開いている。
また経済産業省は「AIプログラム技術」や課題解決を企業や学生らが教え合う「AI学校」を9月にも立ち上げる。
務経験を積んだ人材が互いに鍛え合い、専門人材を育成する。

 きれいに辞書通りに置き換わってます。

これで辞書を以下のように単語レベルに変更してみます。

replacements = {'AI講座':'AIクラス','AIプログラム技術':'AIスキル','AI学校':'AIスクール'}

すると、こんなエラーをはいて終了してしまいます。

string keys in translate table must be of length 1

 長さが1じゃなきゃダメ・・ということです。

 

長さ1を超える単語の置き換えは「re.sub()」でないと無理

 

じゃあ、やっぱり「re.sub()」じゃないと無理・・なのですが、普通にやると、辞書通りの置き換えはできません。

でも、laambda式と組合せてやると、以下のような感じでできます。

import re

def trans_word2(inputtext):
    replacements = {'AI講座':'AIクラス','AIプログラム技術':'AIスキル','AI学校':'AIスクール'}
    print('({})'.format('|'.join(map(re.escape, replacements.keys()))))
    return re.sub('({})'.format('|'.join(map(re.escape, replacements.keys()))), lambda m: replacements[m.group()], inputtext)
   
intext = '''東京大学大阪大学では社会人向けの「AI講座」を開いている。
また経済産業省は「AIプログラム技術」や課題解決を企業や学生らが教え合う「AI学校」を9月にも立ち上げる。
実務経験を積んだ人材が互いに鍛え合い、専門人材を育成する。
'''
print(trans_word2(intext))

これを実行すると。 

東京大学大阪大学では社会人向けの「AIクラス」を開いている。
また経済産業省は「AIスキル」や課題解決を企業や学生らが教え合う「AIスクール」を9月にも立ち上げる。
実務経験を積んだ人材が互いに鍛え合い、専門人材を育成する。

うまく意図通りの置き換えができてます。

 

ちょっと補足説明です

 

上記はうまく行くのですが、以下がパッと見で複雑なので、補足しときます。

 re.sub('({})'.format('|'.join(map(re.escape, replacements.keys()))), lambda m: replacements[m.group()], inputtext)

 まず。

'({})'.format('|'.join(map(re.escape, replacements.keys())))

 の部分です。

これは辞書(例だとreplacements)を使って、正規表現の条件式を生成します。

map関数は、「map(関数, 配列)」なので、re.escape関数で辞書のキーを処理して、エスケープ処理をさせてるだけで、その結果をjoinで連結してます。

前段の例で実行すると。

(AI\講\座|AI\プ\ロ\グ\ラ\ム\技\術|AI\学\校)

 こんな感じの条件が生成されます。

条件が括弧()でくくられているので、結果はgroup()にはいっていくわけです。

それを。

lambda m: replacements[m.group()],

で一つずつ取り出して、その結果(置換前文字列がはいっている)をキーにして、 replacementsの値(置換後文字列)をとりだして、順番にre.sub()に置き換えさせている・・というわけです。

 

まとめ

 

自分の感覚だと、今回のような一括置換って「知ってれば楽勝だけど、知らないと思いつくのに結構苦しむ」典型的な処理だと思ってます。

実際。

こういう処理って、忘れたころに必要になって、前にどうしてたか?を思い出せないときはすったもんだするのですね。

なので、自分の備忘もかねて(笑)書いておきます。

ではでは。