銀月の符号

Python 使い見習いの日記・雑記

ファイル名に使えない、使わないほうがよい文字を置き換え

一月前ほどにつくったID3 タグを読むスクリプトを育てて、読むだけでなく、修正・作成もできるようにしていく過程で、ちょっと脱線して ID3 タグの曲名をもとにファイル名を修正するスクリプトができた(どちらもまだ未公開…手入れ中)。でも使えない文字関連でファイルを作成できないエラーが頻発した。せっかくの機会なのできちんと調べなおしてみた。

使えない文字

Linux では '\x00'(NUL) と '/' 。わかりやすい。シェル上で '*' という名のファイルを消そうとしたときにクォートし忘れると…。

Windows ではたくさんあるようだけれども .NETPath.GetInvalidFileNameChars に聞いてみたところ(なぜ C# ではなく IronPython つかった?)

from System.IO import Path

for c in Path.GetInvalidFileNameChars():
    print ord(c), repr(c)

'"', '<', '>', '|', '\x00'(NUL) から '\x1f'(US) までの制御文字32字すべて, ':', '*', '?', '\\', '/' という答えが返ってきた。

とりあえず Python の関数にしてみる。 safefilename, safefilenames 関数。 unicode.transrate メソッドに初期値をつけただけといえなくもない。

>>> safefilename(u'a*b')
u'a_b'
>>> list(safefilenames([u'a*b', u'c?d']))
[u'a_b', u'c_d']
# coding: utf-8

u"""ファイル名に使えない文字を置き換える, Windows 版
"""

_invalid = (
        34,  # " QUOTATION MARK
        60,  # < LESS-THAN SIGN
        62,  # > GREATER-THAN SIGN
        124, # | VERTICAL LINE
        0, 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,
        58, # : COLON
        42, # * ASTERISK
        63, # ? QUESTION MARK
        92, # \ REVERSE SOLIDUS
        47, # / SOLIDUS
        )

table1 = {}
for i in _invalid:
    table1[i] = 95 # LOW LINE _

table2 = dict(table1)
table2.update((
        (34, 0x201d), # ”
        (60, 0xff1c), # <
        (62, 0xff1e), # >
        (124, 0xff5c), # |
        (58, 0xff1a), # :
        (42, 0xff0a), # *
        (63, 0xff1f), # ?
        (92, 0xffe5), # ¥
        (47, 0xff0f), # /
        ))

def safefilenames(names, table=table1, add_table=None):
    if add_table is None:
        m = table
    else:
        m = dict(table)
        m.update(add_table)
    for name in names:
        yield name.translate(m)

def safefilename(name, table=table1, add_table=None):
    return next(safefilenames([name], table, add_table))

def demo():
    print safefilename(u'a*?:~b')
    print safefilename(u'a*?:~b', add_table={ord(u'~'): u'z'})

if __name__ == '__main__':
    demo()

使わないほうがよい文字

どちらの OS も '(', ')', '[', ']', '{', '}', '&', '$', '`', '^', '~', '\xff'(DEL) あたりは大丈夫なんだな。おい、 DEL 大丈夫なのか!? さすがに DEL は避けたほうが無難だと思う。そのほかは好みか。 Unixシェルスクリプトで扱う場合、エスケープする必要あり。

あとは ' '(スペース) を避けるかどうかか。 Unix シェル、 cmd.exe らの区切り文字なので邪魔くさいと思う時もある。今の自分は、アルファベットのみのファイル名を手打ちする時は避けるけれど、日本語交じりのファイル名を付ける時には気にしない、他人やソフトがスペース入りファイル名を作ったりするのも気にしない、という立ち位置。

上記スクリプトにて、置き換え対象となる文字を一時的に追加したい場合は add_table 引数を使う。たとえば '\xff' を '_' にするときはこう。

>>> safefilename(u'a\xffc', add_table={ord(u'\xff'): u'_'})
u'a_c'

置き換え後の文字の変更

上記スクリプトでは、使えない文字は '_' に置き換えるようにしてあるが、これを別のもの、たとえば '.' にするにはこう。もはや unicode.transrate を直接使うのとの違いがない…。関数名からなにやりたがっているのかが伝わる程度。

>>> my_table = dict(table1)
>>> for k in my_table:
...   my_table[k] = ord(u'.')
...
>>> safefilename(u'a*b', table=my_table)
u'a.b'

全角文字で置き換えるための table2 も用意(あとで適当な名前を考える)。

>>> print safefilename(u'a<bcd>', table=table2)
a<bcd>