機種依存文字を含むファイルの探索と一括置換

#1.はじめに
部署内の「IT管理者」を担当しています。この役自体は嫌いではないのですが、本業という訳でもないので作業は効率よく片付けたいです。今回の作業には結構時間を使ってしまったので、今後、似た作業に悩む人の助けになればと思い、まとめました。

#2.経緯
今回、所属する部署の文書管理システムを新しいものに変更することになりました。
困ったことに、既存データを新しいシステムに一括インポートする際、機種依存文字があるとインポートできないことが判明。既存のシステムのメーカーサポートはすでに切れており、また当時、部単独で導入したこともあって「インポートの準備までは自力で頑張ってね！」という状況でした。

ということで本題です。次の準備が必要です。
　　1）対象ファイルに含まれるすべての機種依存文字を適切な文字に置換
　　2）置換後のファイル名のリスト(csv)

**以下、作業にあたってはバックアップを取ってから作業されることをおススメします。**

#3.素材：文書データ
「データ多いけどひとりで大丈夫？手分けできるところは若者にも手伝ってもらったら？」と提案される人海戦術を全力の笑顔で辞退し、ひとり素材と向き合います（手分けしたら収拾つかなくなる）。

　　・ファイル数：約10,000
　　・容量：20GB
　　・種類：docx, xlsx, pptx, pdf, jpg など
　　・見た感じ、半角カタカナ、①②…、ⅢⅣ…がありそう

――文書管理DB中のデータなので、実際には5,000件くらいの件名にいくつかの属性と添付ファイルが紐づくリストを作成したのですが、ここでは枝葉になるので割愛します――

#4.作業環境
本業でPythonを扱う機会があったので、その環境を使いました。
　　・OS: Windows10
　　・jupyter / Python3.7

#5.どんな機種依存文字が使われているか
機種依存文字にも色々ありそうでしたが、文字コードの成り立ちとかには全く興味が無く「まぁあっても10か20くらいでしょ」くらいのスタンスで挑みました。
具体的には「以下のプログラムを走らせたときに機種依存文字があると途中エラーで処理が止まる」ことを利用して、リストをつくりながら進むことにしました。

```python:fileList.ipynb
import os
import csv
import numpy as np

path = './target'    #targetディレクトリ内に対象ファイルを配置

files = os.listdir(path)

files_replace = [f.replace('aaaaa', 'bbbbb') for f in files]    #ファイル名にaaaaaが含まれていたらbbbbbに置換

#ファイルリストの作成
np.savetxt('fileList_vtl.csv', files_replace, fmt ='%s', encoding='shift-jis')
```
このプログラムを実行したとき、例えば、ファイル名に㈱を含むファイルがtargetディレクトリ内にあると、

```
UnicodeEncodeError: 'shift_jis' codec can't encode character '\u3231' in position 6: illegal multibyte sequence
```
「'\u3231'とか言われても、shift-jis使ってるし、ちょっと無理っす」というエラーが出て処理が止まります。Python(Unicode)で㈱を読み込むと'\u3231'に変換されますが、shift-jisのルールにこの番号はないので元の記号：㈱に戻せないということです、たぶん。プログラムの動作自体は狙い通り（キリッ）なので、次の様に少し書き加えます。

```python:fileList.ipynb_R1
import os
import csv
import numpy as np

path = './target'

files = os.listdir(path)

files_replace = [f.replace('\u3231', '(株)')    #エラーが出た文字と置換文字をセットで追加
                .replace('aaaaa', 'bbbbb')
                for f in files]

np.savetxt('fileList_vtl.csv', files_replace, fmt ='%s', encoding='shift-jis')
```
これを実行すると、'\u3231'をクリアした次の置換候補'\u0000'がエラーに出てくるので、これをまた追加して...とこの作業を繰り返します。機種依存文字以外にも、紛らわしかった文字や、スペースやアンダーバーが重複した場合に1文字に置き換えるような内容も含めました。最終的に出来上がったのが以下です（files_replace = 部分のみ抜粋）。

```python
files_replace = [f.replace('\u3231', '(株)')
                 .replace('\uff5e', '-')    #機種依存〜
                 .replace('〜', '-')    #全角～
                 .replace(' ', '_')    #半角スペース->半角アンダースコア
                 .replace('　', '_')    #全角スペース->半角アンダースコア
                 .replace('__', '_')    #半角アンダースコアx2個->半角アンダースコアx1個
                 .replace('（', '(')    #全角カッコ->半角カッコ
                 .replace('）', ')')    #全角カッコ->半角カッコ
                 .replace('\uff0d', '-')    #機種依存マイナス
                 .replace('‐', '-')
                 .replace('~', '-')
                 .replace('\u2160', 'I')
                 .replace('\u2161', 'II')
                 .replace('\u2162', 'III')
                 .replace('\u2163', 'IV')
                 .replace('\u2164', 'V')
                 .replace('\u2165', 'VI')
                 .replace('\u2166', 'VII')
                 .replace('\u2167', 'VIII')
                 .replace('\u2168', 'IX')
                 .replace('\u2169', 'X')
                 .replace('\u2460', '(1)')
                 .replace('\u2461', '(2)')
                 .replace('\u2462', '(3)')
                 .replace('\u2463', '(4)')
                 .replace('\u2464', '(5)')
                 .replace('\u2465', '(6)')
                 .replace('\u2466', '(7)')
                 .replace('\u2467', '(8)')
                 .replace('\u2468', '(9)')
                 .replace('\u2469', '(10)')
                 .replace('\u2116', 'No')
                 .replace('\u339c', 'mm')
                 for f in files]
```
そして、エラーが出なくなり機種依存文字がクリアになった時点で、置換が終わったファイル名のリスト fileList_vtl.csv が出来上がります。

もしnumpyをインポートできない環境の場合は、

```python
np.savetxt('fileList_vtl.csv', files_replace, fmt ='%s', encoding='shift-jis')
```
の部分を

```python
with open('fileList_hzl.csv', 'w', encoding='shift_jis') as f:
    writer = csv.writer(f, lineterminator='\n')
    writer.writerow(files_replace) 
```
にすることで同様にチェックが可能です。リストは横になりますが、後で縦横反転してcsvからExcel等にコピーすれば縦になるので、実質一緒です（当初こっちで満足してた）。

#6.機種依存文字の一括置換
次にメインディッシュです。targetディレクトリにあるファイル名を一括変換します。上でつくった「置換前-置換後」の文字列ペアからsigns=( ), new_signs=( )を作成し、ファイル名にsigns中の文字があったら、new_signs中の対応する文字列に置換してファイル名を更新します。また同時にmojimojiを使って、ファイル名中の半角カタカナも全角カタカナに置換します。
**しつこいですが、ミスると悲惨なので、作業の前には必ずバックアップを取りましょう。**

```python:fileRename.ipynb
import os
from glob import glob
import mojimoji

path = './target'    #対象ディレクトリ指定
j=0

files = glob(path + '/*.*')    # '*.jpg'  #拡張子を指定する場合

signs = ('\u3231','\uff5e','〜',' ','　','__','（','）','\uff0d','‐','~','...','..',
         '\u2160','\u2161','\u2162','\u2163','\u2164','\u2165','\u2166','\u2167','\u2168','\u2169',
         '\u2460','\u2461','\u2462','\u2463','\u2464','\u2465','\u2466','\u2467','\u2468','\u2469',
         '\u2116','\u339c','#','%','&#x2017','&')

new_signs = ('(株)','-','-','_','_','_','(',')','-','-','-','.','.',
             'I','II','III','IV','V','VI','VII','VIII','IX','X',
             '(1)','(2)','(3)','(4)','(5)','(6)','(7)','(8)','(9)','(10)',
             'No','mm','＃','％','_','＆')

for f in files:
    filename = os.path.basename(f)
    
    for i, sign in enumerate(signs):    #新しいファイル名をfilenameに上書き
        filename = filename.replace(sign, new_signs[i])
    
    filename = mojimoji.han_to_zen(filename, digit=False, ascii=False)    #半角カタカナ->全角
    os.rename(files[j], path+'\\'+filename)    #rename 旧ファイル名はfiles[j]
    j+=1
```
後から見つかった機種依存文字以外の紛らわしい記号などもリストに追加したので、上でつくったリストの内容とは完全には一致していません。
また当然ですが、signs, new_signs内で文字列の順番がズレると、その通りに変換されるので、区切りのいいところで改行するなど間違えないように工夫しています。
で、恐る恐る実行すると、対象ディレクトリのファイル名がイイ感じに変換されます。
こちらも正規表現を使って書くと、もう少しカッコよくできるかもしれません。

#7.所感
終わってみればとてもシンプルな内容になりましたが、実際は初歩的なところで何度も試行錯誤しました。実際にやってみると、機種依存文字がひと通りクリアになったのと前後して、スクリプトの禁則文字が使われていることが判明したり（&, #）、Windowsエクスプローラーの検索が激遅だったりして苦労しましたが、何とかすべて変換できました（約10,000ファイル／数秒）。
また普段はやりっぱなしだった、知識、思考やプログラムを整理する良い機会になりました。先人たちの知恵を活用できる場、サイコーです。この場をお借りして、感謝、感謝です。

#【参考】悩ましかったファイル名
途中説明を省きましたが、実際にはこんな感じのファイル名があって参りました。
　　・&#x2017二重下線&#x2017.pdf
　　・〜機種依存文字と全角記号の混合～.xlsx
　　・半角スペースの連続   .docx
　　・半角ドットの連続....pptx