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


298

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

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

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


17
これはos.pathモ​​ジュールに組み込まれるべきではありませんか?
エンドリス

2
おそらく、彼女のユースケースでは、現在のパスだけでなく、すべてのプラットフォームで安全な単一のパスが必要ですが、これはos.pathが処理するように設計されていないものです。
javawizard 2013年

2
上記のコメントを拡張すると、の現在の設計でos.pathは、OSに応じて実際に異なるライブラリがロードされます(ドキュメントの 2番目のメモを参照)。したがって、引用関数が実装されos.pathている場合、POSIXシステムで実行する場合はPOSIXの安全性、Windowsで実行する場合はWindowsの安全性の文字列のみを引用できます。結果のファイル名は必ずしもウィンドウとPOSIXの両方で有効であるとは限りません。
dshepherd 2015年

回答:


164

あなたはDjangoフレームワークを見ることができます、彼らは任意のテキストから「スラグ」を作成する方法について。slugはURLおよびファイル名に対応しています。

Django text utilsは関数を定義します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))
    # ...
    return value

まだまだありますが、スラッグ化については取り上げていないので省略しましたが、エスケープしています。


11
最後の行は次のようになります。value = unicode(re.sub( '[-\ s] +'、 '-'、value))
Joseph Turian

1
ありがとう-何か不足している可能性がありますが、「normalize()引数2はstrではなくユニコードでなければなりません」
Alex Cook

「normalize()引数2」。を意味しvalueます。値がUnicodeでなければならない場合は、実際にUnicodeであることを確認する必要があります。または。実際の値が実際にASCII文字列である場合は、Unicodeの正規化を省略できます。
S.Lott、

8
このアプローチの良い面に誰も気付いていない場合は、非アルファ文字を削除するのではなく、最初に(NFKD正規化を介して)良い代替物を見つけようとするため、éはeになり、上付き文字1はaになります。通常の1など。ありがとう
マイケルスコットカスバート

48
slugify機能が移動されたのジャンゴ/ utilsの/ text.py、そのファイルも含まれているget_valid_filename機能を。
デニルソンサマイア

104

このホワイトリストのアプローチ(つまり、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'

7
valid_chars = frozenset(valid_chars)害はありません。allcharsに適用すると、1.5倍速くなります。
jfs 2008年

2
警告:これは、2つの異なる文字列を同じ文字列にマッピングします>>>インポート文字列>>> valid_chars = "- 。()%s%s"%(string.ascii_letters、string.digits)>>> valid_chars '-。() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '>>>ファイル名= "a.com/hello/world" >>>' '.join(c in valid_charsの場合はcのファイル名)' a.comhelloworld '>>>ファイル名= "a.com/helloworld ">>> '' .join(c in valid_charsの場合はファイル名のcに対してc) 'a.comhelloworld' >>>
robert king

3
"CON"Windowsでファイルに名前を付けると問題が発生することは言うまでもありません...
Nathan Osman

2
わずかな再配置により、代替文字の指定が簡単になります。最初に元の機能: '' .join(c if valid_chars else '' for c for filename)、またはすべての無効な文字を置換した文字または文字列: '' .join(c if valid_chars else '。' forファイル名のc)
PeterVermont 2014

101

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

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

3
角括弧は省略できることに注意してください。この場合、ジェネレータ式がjoinに渡されます。これにより、それ以外は使用されていないリストを作成するステップが省略されます。
Oben Sonne

31
+1気に入りました。私が行ったわずかな変更: "" .join([x if x.isalnum()else "_" for x in s])-無効なアイテムが空白のように_であるような結果になります。たぶん他の誰かを助ける。
Eddie Parker

12
このソリューションは素晴らしいです!私は若干の修正をしたものの:filename = "".join(i for i in s if i not in "\/:*?<>|")
アレックス・クライチェック

1
残念ながら、スペースやドットすら許可されていませんが、私はこのアイデアが好きです。
tiktak 2013

9
@tiktak:スペース、ドット、アンダースコアを許可する(また)ため"".join( x for x in s if (x.isalnum() or x in "._- "))
hardmooth

95

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

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

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


1
明らかにそうであれば、これが最良の答えです。
user32141 2008年

60
警告!base64エンコーディングには、デフォルトで「/」文字が有効な出力として含まれていますが、これは多くのシステムのファイル名では無効です。代わりにbase64.urlsafe_b64encode(your_string)を使用してください
マシュー

15
実際のところ、デバッグのみを目的とする場合でも、人間の可読性はほとんど常に要因です。
static_rtti 2015年

5
Python 3では、これが機能your_stringするためにはバイト配列またはその結果である必要がありますencode('ascii')
Noumenon

4
def url2filename(url): url = url.encode('UTF-8') return base64.urlsafe_b64encode(url).decode('UTF-8') def filename2url(f): return base64.urlsafe_b64decode(f).decode('UTF-8')
JeffProd

40

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

  • 文字列はすべて無効な文字です(空の文字列のままにします)

  • あなたは特別な意味を持つ文字列、例えば "。"になってしまいます。または「..」

  • 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

これらの問題のいずれにも該当しないファイル名の前に文字列を追加し、無効な文字を削除することにより、これらの問題を回避できます。


24

Githubにはpython-slugifyという素晴らしいプロジェクトがあります。

インストール:

pip install python-slugify

次に使用します:

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

2
このライブラリは好きですが、思ったほど良くありません。初期テストは問題ありませんが、ドットも変換します。だから、test.txt取得test-txtすぎです。
therealmarv 2017年

23

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を参照してください


4
すでにジャンゴにいる怠惰な人のために:django.utils.text import get_valid_filename
アナウンサー

2
正規表現に慣れていない場合re.sub(r'(?u)[^-\w.]', '', s)は、文字、数字(0-9)、アンダースコア( '_')、ダッシュ( '-')、およびピリオド( '。'ではないすべての文字を削除します。 )。ここでの「文字」には、漢語などのすべてのUnicode文字が含まれます。
カウリネーター

3
長さも確認することをお勧めします。ファイル名は255文字に制限されています(または、32、FSによって異なります)
Matthias Winkelmann

19

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

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呼び出しは、アクセント付き文字をアクセントなしの同等のものに置き換えます。これは、単にそれらを取り除くよりも優れています。その後、許可されていない文字はすべて削除されます。

私のソリューションでは、許可されていない可能性のあるファイル名を回避するために、既知の文字列を先頭に追加していません。より一般的なソリューションでは、そうする必要があります。


あなたはあなたのユニークなプレフィックスにuuid.uuid4()を使うことができるはずです
slf

6
ラクダのケース..ああ
痴呆ハリネズミ

これはPython 3.6で動作するように編集/更新できますか?
Wavesailor 2018年

13

覚えておいてください、実際にはUnixシステム以外のファイル名には制限がありません。

  • \ 0が含まれていない可能性があります
  • /が含まれていない可能性があります

他のすべては公正なゲームです。

$タッチ "
>複数行
>はは
> ^ [[31m赤^ [[0m
>悪」
$ ls -la 
-rw-r--r-- 0 Nov 17 23:39?even multiline?haha ?? [31m red?[0m?evil
$ ls -lab
-rw-r--r-- 0 Nov 17 23:39 \ neven \ multiline \ nhaha \ n \ 033 [31m \ red \ \ 033 [0m \ nevil
$ perl -e '私の$ i(glob(q {./* even *})){print $ i; } '
./
複数行でも
ははは
 赤 
悪の

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

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


OPは、「ファイル名は複数のオペレーティングシステムで有効である必要がある」と述べています
カウリネーター

1
私の回答が投稿されてから10時間後に説明が追加されたことを@cowlinatorに伝えました:) OPの編集ログを確認してください。
ケントフレドリック

12

1行で:

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

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


7

re.sub()メソッドを使用して、「ファイルライク」ではないものを置き換えることができます。しかし実際には、すべての文字が有効である可能性があります。だから、それを成し遂げるためのビルド済みの関数(私は信じています)はありません。

import re

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

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


5
範囲として表示されないように、グループマッチャーの最初にダッシュを配置する必要があります。re.sub( '[^-a-zA-Z0-9 _。()] +'、 ''、str)
10

7
>>> 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」など)は処理しません。


変換テーブルの場合は+1、これははるかに効率的な方法です。特別なファイル名/空の場合は、単純な事前条件チェックで十分であり、無関係な期間も単純な修正です。
クリスチャンウィッツ

1
変換は正規表現よりもわずかに効率的ですが、実際にファイルを開こうとすると、その時間はおそらく短縮されます。したがって、私は上記の混乱よりも読みやすい正規表現ソリューションを好む
nosatalian 2009

ブラックリストも心配です。確かに、それはホワイトリストに基づいたブラックリストですが、それでもまだです。あまり安全ではないようです... 「allchars」が実際に完全であることをどうやって知っていますか?
isaaclw 2012

@isaaclw: '.translate()'は、変換テーブルとして256文字の文字列を受け入れます(バイトからバイトへの変換)。'.maketrans()'はそのような文字列を作成します。すべての値がカバーされています。それは純粋なホワイトリストアプローチです
jfs '25

ファイル名「。」はどうですか?(単一のドット)。現在のディレクトリがその名前を使用しているため、UNIXでは機能しません。
FinnÅrupNielsen、2015

6

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

あなたが「For potPoésie」(森の詩)を持っていると想像してみてください、あなたのサニタイズは「fort-posie」(強い+意味のない何か)を与えるかもしれません

漢字を扱う必要がある場合はさらに悪い。

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


6

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

これは作業がはるかに少ないようで、どのOSを使用しても有効です。


5
名前は有効ですか?つまり、OSが満足できない場合でも、何かをする必要がありますよね?
jeromej 14

1
場合によっては、OS /言語が静かにファイル名を別の形式に変換することがありますが、ディレクトリ一覧を作成すると、別の名前が表示されます。そして、これは「ファイルをそこに書き込んだときに、ファイルを探すときに別の名前が付けられた」という問題につながる可能性があります。(私はVAXで聞いた行動について話している...)
ケントフレドリック

さらに、「ファイル名は複数のオペレーティングシステムで有効である必要があります」。これはosopen、1台のマシンでの実行では検出できません。
LarsH

5

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

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はスペースを嫌い、「。」を使用します。ディレクトリセパレータとして。


4

私はここで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こちら)。


1
私はこれが好きです、ホイールを再発明しないでください、それが必要でなければDjangoフレームワーク全体をインポートしないでください、将来それを維持するつもりがない場合はコードを直接貼り付けないでください、そして生成された文字列は試行します同様の文字を安全な文字に一致させるため、新しい文字列は読みやすくなります。
vicenteherrera

1
ダッシュの代わりにアンダースコアを使用するには:name = slugify(s、separator = '_')
vicenteherrera

3

Python 3.6用に変更された回答

import string
import unicodedata

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)

答えを詳しく説明していただけますか?
セレニティ

その答えはソフィー・ゲージによって受け入れられました。しかし、python 3.6で動作するように変更されています
Jean-Robin Tremblay

2

答えはたくさんあると思いますが、ほとんどが正規表現や外部モジュールに依存しているので、自分で答えたいと思います。純粋な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パス区切り文字を含めるようにを変更するのは簡単です。


1

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

'/ hello / world'-> 'helloworld'

'/ helloworld' /-> 'helloworld'

これは一般的に必要なことではありません。たとえば、リンクごとにhtmlを保存すると、別のWebページのhtmlが上書きされることになります。

私は次のような辞書を漬けます:

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

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

辞書から毎回ファイル名を調べます。ない場合は、新しいものを作成し、必要に応じて最大数を追加します。


helloworld1を使用して、あなたもhelloworld1をチェックする必要がある場合はノートでは、使用中とそう..上にない
ロバート・キング

1

正確には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の観点からは、「ある程度」読み取り可能です。


ファイル名にスペースを含まない、このラッパー:def safe_filename(filename): return safePath(filename.strip().replace(' ','_'))
SpeedCoder5

1

パッケージのインストールを気にしない場合、これは便利です:https : //pypi.org/project/pathvalidate/

https://pypi.org/project/pathvalidate/#sanitize-a-filenameから:

from pathvalidate import sanitize_filename

fname = "fi:l*e/p\"a?t>h|.t<xt"
print(f"{fname} -> {sanitize_filename(fname)}\n")
fname = "\0_a*b:c<d>e%f/(g)h+i_0.txt"
print(f"{fname} -> {sanitize_filename(fname)}\n")

出力

fi:l*e/p"a?t>h|.t<xt -> filepath.txt
_a*b:c<d>e%f/(g)h+i_0.txt -> _abcde%f(g)h+i_0.txt

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, '')

私は"".join( x for x in s if (x.isalnum() or x in "._- "))この投稿のコメントでこれを見つけました
SergioAraujo '30年

0

更新

この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モジュールが含まれています。

完全に堅牢ではありません。この投稿とこれを参照してください返信を。。

したがって、前述のようにbase64、読みやすさが問題にならない場合は、おそらくエンコーディングの方が適しています。


すべてのリンクが停止しています。男、何かしなさい。
Peaceful Coder
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.