文字列の数字でソートした後、連続した数字になるように文字列を書き直すには。


pythonのosライブラリのos.listdir()で格納した以下のようなファイルリストを、sortした後に連続した数字になるようにファイル名を変更したいです。

[code]

    sample1.png 
    sample15.png    
    sample21.png    
    sample27.png    
    sample31.png    
    sample35.gif    
    sample41.jpg    
    sample8.png
    sample13.png    
    sample18.png
    
[/code]

この例では全て数字の前をsampleとしているため単純なsort()でも順番を整えることができますが、実際にはいろんなファイル名が考えられるため、数字の方でソートしなければなりません。（ファイル名には必ず数字がつきます）つまり、上のリストを：

[code]

    sample1.png
    sample8.png 
    sample13.png
    sample15.png  
    sample18.png  
    sample21.png    
    sample27.png    
    sample31.png    
    sample35.gif    
    
[/code]

のように数字順にした後、連続した番号にするため最終的に以下のようなファイル名に変更したいです。

[code]

    sample1.png
    sample2.png 
    sample3.png
    sample4.png  
    sample5.png  
    sample6.png    
    sample7.png    
    sample8.png    
    sample9.gif
    
[/code]

幸い、ファイル名の変更はos.rename()で可能ですが、その前にsortしないといけません。  
以下の二つを試していますが、どちらもIndexError: list index out of rangeになってしまいます。  
どなたか良い方法をご存知ないですか？

[code]

    files = os.listdir(path)
    def sort_by_num(test_string):
            return list(map(int, re.findall(r'\d+', test_string)))[0]
    files.sort(key=sort_by_num)
    
[/code]

[code]

    files.sort(key=lambda file : list(
            map(int, re.findall(r'\d+', file)))[0])
    
[/code]




ファイル名（正確には拡張子を除く
basename）の数字以外の部分は何であれ，後ろの数字の部分だけを取り出してソートして再度1から順番に採番して，元の名前に新たに採番した番号を付けた名前にリネームするということでよいのでしょうか．

基本的に「ファイル名から数字部分を取り出す」「ソートする」「リネームする」を同時にやろうとせずに，順番にやった方がいいのではないかと思います．その方針で実装したコード例を以下に示します．

[code]

    import os
    import re
    
    
    def parse_filename(fn):
        """Parse a filename into (original, base, num, ext)"""
        m = re.match(r'(\D*)(\d*)\.(.*)$', fn)
        return (m[0], m[1], int(m[2]), m[3])
    
    
    def main():
        path = 'path/to/somewhere'
        filenames = os.listdir(path)
    
        parsed_filenames = [parse_filename(fn) for fn in filenames]
    
        for i, m in enumerate(sorted(parsed_filenames, key=lambda x: x[2])):
            os.rename(path + '/' + m[0],
                      path + '/' + m[1] + str(i + 1) + '.' + m[3])
    
    
    if __name__ == '__main__':
        main()
    
[/code]

このコードでやっていることを念のため簡単に説明しておくと，まず `os.listdir(path)` で取得した各ファイル名を
`parse_filename()` 関数で以下の要素を持つタプルに分解します．

  * オリジナルのファイル名 (str)
  * basename の非数字部分 (str)
  * basename の数字部分 (int)
  * 拡張子 (str)

その上で，basename の数字部分を用いて普通にソートし，最後に for 文の中で目的のリネームを実行しています．




ファイル名（正確には拡張子を除く
basename）の数字以外の部分は何であれ，後ろの数字の部分だけを取り出してソートして再度1から順番に採番して，元の名前に新たに採番した番号を付けた名前にリネームするということでよいのでしょうか．

基本的に「ファイル名から数字部分を取り出す」「ソートする」「リネームする」を同時にやろうとせずに，順番にやった方がいいのではないかと思います．その方針で実装したコード例を以下に示します．

[code]

    import os
    import re
    
    
    def parse_filename(fn):
        """Parse a filename into (original, base, num, ext)"""
        m = re.match(r'(\D*)(\d*)\.(.*)$', fn)
        return (m[0], m[1], int(m[2]), m[3])
    
    
    def main():
        path = 'path/to/somewhere'
        filenames = os.listdir(path)
    
        parsed_filenames = [parse_filename(fn) for fn in filenames]
    
        for i, m in enumerate(sorted(parsed_filenames, key=lambda x: x[2])):
            os.rename(path + '/' + m[0],
                      path + '/' + m[1] + str(i + 1) + '.' + m[3])
    
    
    if __name__ == '__main__':
        main()
    
[/code]

このコードでやっていることを念のため簡単に説明しておくと，まず `os.listdir(path)` で取得した各ファイル名を
`parse_filename()` 関数で以下の要素を持つタプルに分解します．

  * オリジナルのファイル名 (str)
  * basename の非数字部分 (str)
  * basename の数字部分 (int)
  * 拡張子 (str)

その上で，basename の数字部分を用いて普通にソートし，最後に for 文の中で目的のリネームを実行しています．

