文件名匹配 - 字符串中间部分

0 投票
4 回答
2535 浏览
提问于 2025-04-18 14:09

我有一个文件夹,里面的文件名格式是:LnLnnnnLnnn.txt

其中,L代表字母,n代表数字。例如:p2c0789c001.txt

我想根据第二个数字(也就是0789)是奇数还是偶数来把这些文件分开。

我目前只成功处理了第二个数字在0001到0009之间的情况,使用的代码是:

odd_files = []
for root, dirs, filenames in os.walk('.'):
   for filename in fnmatch.filter(filenames, 'p2c000[13579]*.txt'):
       odd_files.append(os.path.join(root, filename))

这样会返回这些文件:['./p2c0001c054.txt', './p2c0003c055.txt', './p2c0005c056.txt', './p2c0007c057.txt', './p2c0009c058.txt']

有没有什么建议可以让我处理任何四位数字的情况呢?

4 个回答

1

构建这种过滤器其实并没有什么特别复杂的地方。主要就是要仔细写出合适的正则表达式,然后用它来测试。当使用复杂的模式,特别是那些有很多重复部分的模式时,容易出现错误。因此,我喜欢定义一些辅助函数,这样可以让规则看起来更容易理解,也方便以后修改。

import re
import os

# helper functions for legible re construction
LETTER = lambda n='': '({0}{1})'.format('[A-Za-z]', n)
NUM    = lambda n='': '({0}{1})'.format('\d', n)

FILENAME = LETTER() + NUM() + LETTER() + NUM('{4}') + LETTER() + NUM('{3}') + '\.txt'
FILENAME_RE = re.compile(FILENAME)

is_odd = lambda n: int(n) % 2 > 0


def odd_nnnn(f):
    """
    Determine if the given filename `f` matches our desired LnLnnnnLnnn.txt pattern
    with the second group of numbers (nnnn) odd.
    """
    m = FILENAME_RE.search(f)
    return m is not None and is_odd(m.group(4))


if __name__ == '__main__':
    print "Search pattern:", FILENAME

    files = ['./p2c0001c054.txt', './p2c0001c055.txt', './p2c0003c055.txt', './p2c0005c056.txt', './p2c0022c056.txt', './p2c0004c056.txt', './p2c0007c057.txt', './p2c0009c058.txt', './p2c8888c056.txt', ]
    files = [ os.path.normpath(f) for f in files ]

    root = '/users/test/whatever'

    odd_paths = [ os.path.join(root, f) for f in files if odd_nnnn(f) ]

    print odd_paths

唯一的缺点就是这样写会稍微多一些文字,特别是和Brad Beattie那种简洁的答案相比。


[更新] 后来我想到,定义正则表达式还有一种更简洁的方法:

FILENAME = "LnL(nnnn)Lnnn\.txt"

FILENAME_PAT = FILENAME.replace('L', r'[A-Za-z]').replace('n', r'\d')
FILENAME_RE = re.compile(FILENAME_PAT)

这个方法更贴近最初的 'LnLnnnLnnn.txt' 描述。匹配表达式需要从 m.group(4) 改为 m.group(1),因为这样只捕获了一个组。

1

如果事情变得有点复杂,你可以把它变成一个生成器,然后手动编写测试代码:

def odd_files_generator():
    for root, dirs, filenames in os.walk('.'):
        for filename in filenames:
            if filename[6] in '13579':
                yield filename

odd_files = list(odd_files_generator)

如果你的测试变得很难简洁地表达,可以把 if filename ... 这一行换成你明确的测试代码。

1

这样做可以吗?

import re
regex = re.compile("[a-z][0-9][a-z]([0-9]{4})[a-z][0-9]{3}.txt")
filter(lambda x: int(regex.match(x).groups()[0]) % 2 == 1, fnmatch)
1

最简单的解决办法就是把你的通配符扩展一下,让它能匹配更多的东西。

为此,我可能会这样做:

for filename in fnmatch.filter(filenames, '??????[13579]*.txt'):

这样做的话,它会匹配你值前面的任何字符,然后会匹配你通配符类中的任何奇怪值,最后会接受后面的任何东西。

这样有点麻烦,因为像aaaaaaa3alkjfdhalkjfshglkjzsdhfgs.txt这样的文件名也会被匹配到,这样就很糟糕。如果你知道你要处理的目录里的数据是比较规范的,那可能还可以接受。更好的办法是稍微具体一点。你可以用下面的表达式来实现:

'[a-z][0-0][a-z][0-9][0-9][0-9][13579][a-z][0-9][0-9][0-9].txt'

fnmatch.filter方法使用的是Unix风格的通配符。这意味着你可以使用以下符号:

? - 匹配任何单个字符
* - 匹配从零到所有的任何东西
[] - 这匹配一类东西,使用 - 来表示范围,使用 ! 来表示排除

撰写回答