web-dev-qa-db-ja.com

文字列を有効なファイル名に変えますか?

ファイル名として使用する文字列があるので、Pythonを使用して、ファイル名に使用できないすべての文字を削除します。

他の場合よりも厳密にしたいので、文字、数字、および"_-.() "のような他の文字の小さなセットのみを保持したいとします。最もエレガントなソリューションは何ですか?

ファイル名は、複数のオペレーティングシステム(Windows、Linux、およびMac OS)で有効である必要があります。これは、曲名をファイル名とするライブラリ内のMP3ファイルであり、3台のマシン間で共有およびバックアップされます。

257
Sophie Gage

これは私が最終的に使用したソリューションです:

import unicodedata

validFilenameChars = "-_.() %s%s" % (string.ascii_letters, string.digits)

def removeDisallowedFilenameChars(filename):
    cleanedFilename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore')
    return ''.join(c for c in cleanedFilename if c in validFilenameChars)

Unicodedata.normalizeの呼び出しは、アクセント記号付きの文字をアクセント記号のない同等の文字に置き換えます。これは単純にそれらを取り除くよりも優れています。その後、許可されていない文字はすべて削除されます。

私の解決策は、特定のファイル名形式では発生しないことがわかっているため、許可されていないファイル名を避けるために既知の文字列を追加しません。より一般的な解決策はそうする必要があります。

18
Sophie Gage

Django framework を見て、任意のテキストから「スラッグ」を作成する方法を確認できます。スラッグはURLおよびファイル名に対応しています。

Djangoテキストユーティリティは、関数 slugify() を定義します。これはおそらく、この種のもののゴールドスタンダードです。基本的に、それらのコードは次のとおりです。

def slugify(value):
    """
    Normalizes string, converts to lowercase, removes non-alpha characters,
    and converts spaces to hyphens.
    """
    import unicodedata
    value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
    value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
    value = unicode(re.sub('[-\s]+', '-', value))

まだありますが、スラッジ化に対処するのではなく、エスケープするので、私は除外しました。

138
S.Lott

このホワイトリストアプローチ(つまり、valid_charsに存在する文字のみを許可する)は、ファイルの形式に制限がない場合、または無効な(「..」など)有効な文字の組み合わせに制限がない場合に機能します。 「。txt」という名前のファイル名を許可しますが、これはWindowsでは無効だと思います。これは私がvalid_charsから空白を削除してエラーの場合に既知の有効な文字列を追加しようとする最も簡単なアプローチであるため、他のアプローチは何に対処できるかについて知る必要があります Windowsファイルの命名制限 したがって、より複雑になります。

>>> import string
>>> valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
>>> valid_chars
'-_.() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
>>> filename = "This Is a (valid) - filename%$&$ .txt"
>>> ''.join(c for c in filename if c in valid_chars)
'This Is a (valid) - filename .txt'
97
Vinko Vrsalovic

リスト内包表記と文字列メソッドを併用できます。

>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'
93
lutz

ファイル名として文字列を使用する理由は何ですか?人間の可読性が要素でない場合、ファイルシステムの安全な文字列を生成できるbase64モジュールを使用します。読むことはできませんが、衝突に対処する必要はなく、元に戻すことができます。

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

更新:マシューのコメントに基づいて変更されました。

89
Igal Serban

さらに複雑なことに、無効な文字を削除するだけで有効なファイル名を取得できるとは限りません。許可される文字はファイル名によって異なるため、保守的なアプローチでは有効な名前が無効な名前に変わる可能性があります。次の場合に特別な処理を追加できます。

  • 文字列はすべて無効な文字です(空の文字列が残ります)

  • 「。」などの特別な意味を持つ文字列になります。または「..」

  • Windowsでは、 特定のデバイス名 は予約されています。たとえば、「nul」、「nul.txt」(または実際にはnul.anything)という名前のファイルを作成することはできません。予約名は次のとおりです。

    CON、PRN、AUX、NUL、COM1、COM2、COM3、COM4、COM5、COM6、COM7、COM8、COM9、LPT1、LPT2、LPT3、LPT4、LPT5、LPT6、LPT7、LPT8、およびLPT9

これらの問題を回避するには、ファイル名の前に文字列を追加して、これらのケースの1つにならないようにし、無効な文字を削除します。

37
Brian

Githubには python-slugify という素敵なプロジェクトがあります:

インストール:

pip install python-slugify

次に使用します:

>>> from slugify import slugify
>>> txt = "This\ is/ a%#$ test ---"
>>> slugify(txt)
'this-is-a-test'
22
Shoham

S.Lott と同じように、文字列を有効なファイル名に変換する方法について Django Framework を見ることができます。

最新の更新バージョンはutils/text.pyにあり、「get_valid_filename」を次のように定義しています。

def get_valid_filename(s):
    s = str(s).strip().replace(' ', '_')
    return re.sub(r'(?u)[^-\w.]', '', s)

https://github.com/Django/django/blob/master/Django/utils/text.py を参照)

17
cowlinator

Unixシステムのファイル名には、実際には以下の制限はありません。

  • \ 0を含めることはできません
  • /を含まない場合があります

それ以外はすべて公平なゲームです。

 $タッチ「
>複数行
>ハハ
> ^ [[31m赤^ [[0m 
>悪」] 
 $ ls -la 
-rw-r--r-- 0 Nov 17 23:39?マルチライン?haha ?? [31m赤?[0m?evil 
 $ ls -lab 
-rw-r--r-- 0 Nov 17 23:39\neven\multiline\nhaha\n\033 [31m\red\\ 033 [0m\nevil 
 $ Perl -e ' for $ i(glob(q {./* even *})){print $ i; } '
 ./ 
マルチライン
 haha​​ 
赤
 evil 

はい、ANSIカラーコードをファイル名に保存して、それらを有効にしました。

エンターテインメントのために、ディレクトリ名にBELキャラクターを入れて、CDを挿入したときの楽しみを見てください;)

13
Kent Fredric

1行で:

valid_file_name = re.sub('[^\w_.)( -]', '', any_string)

'_'文字を入力して読みやすくすることもできます(たとえば、スラッシュを置き換える場合)

8
mnach
>>> import string
>>> safechars = bytearray(('_-.()' + string.digits + string.ascii_letters).encode())
>>> allchars = bytearray(range(0x100))
>>> deletechars = bytearray(set(allchars) - set(safechars))
>>> filename = u'#ab\xa0c.$%.txt'
>>> safe_filename = filename.encode('ascii', 'ignore').translate(None, deletechars).decode()
>>> safe_filename
'abc..txt'

空の文字列、特別なファイル名(「nul」、「con」など)は処理しません。

7
jfs

Re.sub()メソッドを使用して、「filelike」以外のものを置き換えることができます。しかし、実際には、すべての文字が有効です。そのため、事前に構築された関数(私は信じています)はありません。

import re

str = "File!name?.txt"
f = open(os.path.join("/tmp", re.sub('[^-a-zA-Z0-9_.() ]+', '', str))

/tmp/filename.txtへのファイルハンドルになります。

7
gx.

注意する必要がありますが。ラテン語のみを見る場合、イントロで明確に言われていません。 ASCII文字のみでサニタイズすると、一部の単語は無意味になったり、別の意味になったりする場合があります。

「forêtpoésie」(森の詩)があると想像してください。消毒すると「fort-posie」(強い+意味のないもの)になる可能性があります

漢字を処理する必要がある場合はさらに悪い。

「下北沢」は、システムが「---」を実行することになり、しばらくすると失敗する運命にあり、あまり役に立ちません。したがって、ファイルのみを扱う場合は、それらを制御する汎用チェーンと呼ぶか、文字をそのままにしておくことをお勧めします。 URIについても、ほぼ同じです。

6
karlcow

なぜ「osopen」をtry/exceptでラップし、基礎となるOSにファイルが有効かどうかを選別させないのはなぜですか?

これは作業量がはるかに少ないようで、使用するOSに関係なく有効です。

6
James Anderson

他のコメントがまだ対処していない別の問題は、空の文字列です。これは明らかに有効なファイル名ではありません。また、あまりにも多くの文字を削除することにより、空の文字列で終わることがあります。

Windowsで予約されているファイル名とドットの問題については、「任意のユーザー入力から有効なファイル名を正規化するにはどうすればよいか」という質問に対する最も安全な答えは「わざわざ試してはいけない」です。それ(たとえば、データベースからの整数の主キーをファイル名として使用する)、それを行います。

名前の一部としてファイル拡張子にスペースと「。」を許可する必要がある場合は、次のようにしてください。

import re
badchars= re.compile(r'[^A-Za-z0-9_. ]+|^\.|\.$|^ | $|^$')
badnames= re.compile(r'(aux|com[1-9]|con|lpt[1-9]|prn)(\.|$)')

def makeName(s):
    name= badchars.sub('_', s)
    if badnames.match(name):
        name= '_'+name
    return name

これは、特に予期しないOSでは特に保証できません-たとえば、RISC OSはスペースを嫌い、ディレクトリ区切り文字として「。」を使用します。

5
bobince

私はここでpython-slugifyアプローチが好きでしたが、それは望まれないドットを取り除くことでもありました。そこで、この方法でs3にクリーンなファイル名をアップロードするように最適化しました。

pip install python-slugify

サンプルコード:

s = 'Very / Unsafe / file\nname hähä \n\r .txt'
clean_basename = slugify(os.path.splitext(s)[0])
clean_extension = slugify(os.path.splitext(s)[1][1:])
if clean_extension:
    clean_filename = '{}.{}'.format(clean_basename, clean_extension)
Elif clean_basename:
    clean_filename = clean_basename
else:
    clean_filename = 'none' # only unclean characters

出力:

>>> clean_filename
'very-unsafe-file-name-haha.txt'

これは非常にフェイルセーフであり、拡張子のないファイル名で動作し、安全でない文字のファイル名のみでも動作します(ここでは結果はnoneです)。

2
therealmarv

これらのソリューションのほとんどは機能しません。

'/ hello/world'-> 'helloworld'

'/ helloworld' /-> 'helloworld'

これは一般に望んでいることではありません。たとえば、各リンクのhtmlを保存している場合、別のWebページのhtmlを上書きします。

次のような辞書をピクルスにします。

{'helloworld': 
    (
    {'/hello/world': 'helloworld', '/helloworld/': 'helloworld1'},
    2)
    }

2は、次のファイル名に追加する必要がある番号を表します。

Dictから毎回ファイル名を検索します。存在しない場合、新しいものを作成し、必要に応じて最大数を追加します。

2
robert king

OPが求めていたものとはまったく異なりますが、ユニークで可逆的な変換が必要なため、これが私が使用するものです:

# p3 code
def safePath (url):
    return ''.join(map(lambda ch: chr(ch) if ch in safePath.chars else '%%%02x' % ch, url.encode('utf-8')))
safePath.chars = set(map(lambda x: ord(x), '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-_ .'))

結果は、少なくともsysadminの観点からは、「ある程度」読みやすいです。

1
makeroo

UPDATE

この6年前の回答では、すべてのリンクは修復できないほど壊れています。

また、私はもうこの方法ではなく、単にbase64で安全でない文字をエンコードまたはドロップします。 Python 3の例:

import re
t = re.compile("[a-zA-Z0-9.,_-]")
unsafe = "abc∂éåß®∆˚˙©¬ñ√ƒµ©∆∫ø"
safe = [ch for ch in unsafe if t.match(ch)]
# => 'abc'

base64を使用すると、エンコードおよびデコードできるため、元のファイル名を再度取得できます。

ただし、ユースケースによっては、ランダムなファイル名を生成し、メタデータを別のファイルまたはDBに保存する方が良い場合があります。

from random import choice
from string import ascii_lowercase, ascii_uppercase, digits
allowed_chr = ascii_lowercase + ascii_uppercase + digits

safe = ''.join([choice(allowed_chr) for _ in range(16)])
# => 'CYQ4JDKE9JfcRzAZ'

オリジナルのリンクされた回答

bobcatプロジェクトには、これを行うpythonモジュールが含まれています。

完全に堅牢ではありません。この post およびこの reply を参照してください。

したがって、前述のように、読みやすさが重要でない場合は、おそらくbase64エンコードの方が適切です。

0
wires

私は多くの答えがあることを知っていますが、それらは主に正規表現または外部モジュールに依存しているので、私は自分の答えを投げたいです。純粋なpython関数、外部モジュールは不要、正規表現は使用されていません。私のアプローチは、無効な文字を消去するのではなく、有効な文字のみを許可することです。

def normalizefilename(fn):
    validchars = "-_.() "
    out = ""
    for c in fn:
      if str.isalpha(c) or str.isdigit(c) or (c in validchars):
        out += c
      else:
        out += "_"
    return out    

必要に応じて、英語のアルファベットにはない国の文字など、独自の有効な文字を先頭のvalidchars変数に追加できます。これは、必要な場合もそうでない場合もあります。UTF-8で実行されないファイルシステムの中には、非ASCII文字で問題が発生する場合があります。

この関数は、単一のファイル名の有効性をテストするため、無効な文字を考慮してパス区切り文字を_に置き換えます。これを追加する場合は、ifを変更してosパス区切り文字を含めるのは簡単です。

0

ループしている文字列を変更するため、これは素晴らしい答えではないと確信していますが、うまくいくようです:

import string
for chr in your_string:
 if chr == ' ':
   your_string = your_string.replace(' ', '_')
 Elif chr not in string.ascii_letters or chr not in string.digits:
    your_string = your_string.replace(chr, '')
0
TankorSmash

python 3.6の回答が変更されました

validFilenameChars = "-_.() %s%s" % (string.ascii_letters, string.digits)
def removeDisallowedFilenameChars(filename):
    cleanedFilename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore')
    return ''.join(chr(c) for c in cleanedFilename if chr(c) in validFilenameChars)
0