从路径中提取基本文件名的正则表达式

0 投票
2 回答
4057 浏览
提问于 2025-04-19 11:14

我想从一些文件路径中提取出字母部分。

files = ['data/Conversion/201406/MM_CLD_Conversion_Advertiser_96337_Daily_140606.zip', 
         'data/Match/201406/MM_CLD_Match_Advertiser_111423_Daily_140608.csv.zip', 
         'data/AQlog/201406/orx140605.csv.zip',
         'data/AQlog/201406/orx140605.csv.zip/']

目前我这样做:

  1. 去掉路径末尾的斜杠
  2. os.path.split()[1] 获取文件名
  3. 用两次 os.path.splitext() 去掉可能的两个文件扩展名
  4. 去掉数字
  5. 去掉下划线

代码:

for f in files:
    a = os.path.splitext(os.path.splitext(os.path.split(f.rstrip('/\\'))[1])[0])[0]
    b = re.sub('\d+', '', a).replace('_','')

结果:

'MMCLDConversionAdvertiserDaily'
'MMCLDMatchAdvertiserDaily'
'orx'
'orx'

有没有更快或者更符合Python风格的方法,可以使用编译好的正则表达式函数?或者使用 os.path() 这个库的做法是否很合理?我也不需要做超过100次,所以这不是速度问题,主要是为了清晰。

2 个回答

2

不使用正则表达式:

import os
import string
trans = string.maketrans('_', ' ')
def get_filename(path):
    # If you need to keep the directory, use os.path.split
    filename = os.path.basename(path.rstrip('/'))
    try:
        # If the extension starts at the last period, use
        # os.path.splitext
        # If the extension starts at the 2nd to last period,
        # use os.path.splitext twice
        # Continuing this pattern (since it sounds like you
        # don't know how many extensions a filename may have,
        # it may be safer to assume the file extension starts
        # at the first period. In which case, use
        # filename.split('.', 1).
        filename_without_ext, extension = filename.split('.', 1)
    except ValueError:
        filename_without_ext = filename
        extension = ''
    filename_cleaned = filename_without_ext.translate(trans, string.digits)
    return filename_cleaned

>>> path = 'data/Match/201406/MM_CLD_Match_Advertiser_111423_Daily_140608.csv.zip/'
>>> get_filename(path)
'MM CLD Match Advertiser  Daily '

你可以用更容易理解的方法来解决问题。我通常会避免使用正则表达式,除非问题特别需要。在这种情况下,普通的字符串操作就能完成你想做的事情。

如果你想去掉多余的空格(就像你结果中显示的那样),可以用 filename.replace(' ', '')。如果你可能会遇到其他类型的空白字符,可以用 ''.join(filename.split()) 来去掉它们。

注意:如果你在使用 Python 3,把 trans=string.maketrans('_', ' ') 替换成 trans=str.maketrans('_', ' ', string.digits),同时把 filename_without_ext.translate(trans, string.digits) 替换成 filename_without_ext.translate(trans)这个变化是为了改善对 Unicode 语言的支持。想了解更多,可以查看:为什么 string.maketrans 在 Python 3.1 中不工作?

以下是 Python 3 的代码:

import os
import string
trans = string.maketrans('_', ' ', string,digits)
def get_filename(path):
    filename = os.path.basename(path.rstrip('/'))
    filename_without_ext = filename.split('.', 1)[0]
    filename_cleaned = filename_without_ext.translate(trans)
    return filename_cleaned
2

你可以通过使用 os.path 中的合适函数来简化这个过程。

首先,如果你调用 normpath,那么你就不需要担心两种路径分隔符了,只需要关注 os.sep(注意,如果你是在 Linux 上处理 Windows 路径,这可能会有问题……但如果你是在处理某个平台的本地路径,这正是你想要的)。而且它还会去掉路径末尾的斜杠。

接下来,如果你使用 basename 而不是 split,那么你就不需要再加那些末尾的 [1] 了。

可惜的是,splitext 没有类似于 basenamesplit 的替代方法……但你可以很容易地写一个,这样可以让你的代码在可读性上和使用 basename 一样好。

至于其他的……用正则表达式来去掉数字是个明显的办法(虽然其实你不需要 +)。而且,既然你已经有了正则表达式,可能把 _ 也放进去会更简单,而不是单独处理。

所以:

def stripext(p):
    return os.path.splitext(p)[0]

for f in files:
    a = stripext(stripext(os.path.basename(os.path.normpath(f))))
    b = re.sub(r'[\d_]', '', a)

当然,如果你把整个过程封装成一个函数,可能会更易读:

def process_path(p):
    a = stripext(stripext(os.path.basename(os.path.normpath(f))))
    return re.sub(r'[\d_]', '', a)

for f in files:
    b = process_path(f)

尤其是现在你可以把循环变成列表推导式、生成器表达式或者 map 调用:

processed_files = map(process_path, files)

我只是好奇速度,因为我觉得编译后的正则表达式函数非常快。

是的,通常来说是这样。不过,未编译的字符串模式也很快。

当你使用字符串模式而不是编译后的正则表达式对象时,会发生以下情况:

  • re 模块会在编译好的正则表达式缓存中查找这个模式。
  • 如果没有找到,就会编译这个字符串,并把结果添加到缓存中。

所以,假设你在应用中没有使用很多几十个正则表达式,无论哪种方式,你的模式都会被编译一次,并作为编译后的表达式重复运行。使用未编译表达式的唯一额外成本是查找缓存字典,这个成本非常低——尤其是当它是一个字符串字面量时,它每次都是同一个字符串对象,所以它的哈希值也会被缓存,因此在第一次查找后,字典查找就变成了简单的 mod 和数组查找。

对于大多数应用,你可以假设 re 缓存已经足够好,所以决定是否预编译正则表达式的主要原因是可读性。例如,当你有一个函数运行一堆复杂的正则表达式,而这些表达式的目的很难理解时,给每个表达式起个名字会很有帮助,这样你可以写 for r in (r_phone_numbers, r_addresses, r_names): …,在这种情况下,不编译它们几乎显得有些傻。

撰写回答