文件名字符串过滤器

160
我正在寻找一个能够清理字符串并使其可以用于文件名的PHP函数。有人知道一个方便的函数吗?
(我可以自己写一个,但是我担心会忽略某个字符!)
编辑:用于在Windows NTFS文件系统上保存文件。

1
你能更具体一些吗:Umlauts 应该怎么处理(删除还是转换为基本字符?)特殊字符应该怎么处理? - Pekka
1
针对哪个文件系统?它们是不同的。请参阅http://en.wikipedia.org/wiki/Filename#Comparison_of_file_name_limitations。 - Gordon
Windows :) 需要15个字符。 - user151841
1
我想指出,一些答案中提出的“黑名单”解决方案并不足够,因为检查每个可能的不良字符是不可行的(除了特殊字符外,还有带重音和变音符号的字符、整个非英语/拉丁字母表、控制字符等需要处理)。因此,我认为“白名单”方法总是更好的,并且规范化字符串(如Blair McMillan在Dominic Rodger的答案中建议的那样)将允许自然处理任何带有重音、变音符号等字母。 - Sean the Bean
一个好的方法可能是使用正则表达式,可以看看我写的这个Python脚本:https://github.com/gsscoder/normalize-fn - gsscoder
19个回答

6
以下表达式可创建一个漂亮、干净且易于使用的字符串:
/[^a-z0-9\._-]+/gi

将今天的财务账单转换成today-s-financial-billing


那么文件名不能有句号、下划线或类似的字符,对吗? - Tor Valamo
2
@Jonathan - 斜体字是怎么回事? - Dominic Rodger
@Tor,是的,抱歉。已更新。@Dominic,只是强调文本。 - Sampson
什么是gism?我得到了“警告:preg_replace()[function.preg-replace]:未知修饰符'g'” - user151841
g - 全局匹配,i - 不区分大小写,s - 匹配任意字符(包括换行符),m - 多行匹配。在这个例子中,您可以不使用sm - Sampson
1
@user151841 对于 preg_replace 函数,全局标志是隐式的。因此,在使用 preg_replace 时不需要使用 g。当我们想要控制替换的数量时,preg_replace 有一个 limit 参数可以用来设置。请阅读 preg_replace 文档以获取更多信息。 - rineez

2
这些可能有点繁重,但它们足够灵活,可以将任何字符串转换为“安全”的en风格的文件名或文件夹名(或者甚至是去掉了空格的短语)。
1)构建完整的文件名(如果输入被完全截断,则使用备用名称):
str_file($raw_string, $word_separator, $file_extension, $fallback_name, $length);

2) 或者仅使用过滤器工具而不构建完整的文件名(严格模式 true 不允许在文件名中使用 [] 或 ()):

str_file_filter($string, $separator, $strict, $length);

3) 这里是这些函数:

// Returns filesystem-safe string after cleaning, filtering, and trimming input
function str_file_filter(
    $str,
    $sep = '_',
    $strict = false,
    $trim = 248) {

    $str = strip_tags(htmlspecialchars_decode(strtolower($str))); // lowercase -> decode -> strip tags
    $str = str_replace("%20", ' ', $str); // convert rogue %20s into spaces
    $str = preg_replace("/%[a-z0-9]{1,2}/i", '', $str); // remove hexy things
    $str = str_replace(" ", ' ', $str); // convert all nbsp into space
    $str = preg_replace("/&#?[a-z0-9]{2,8};/i", '', $str); // remove the other non-tag things
    $str = preg_replace("/\s+/", $sep, $str); // filter multiple spaces
    $str = preg_replace("/\.+/", '.', $str); // filter multiple periods
    $str = preg_replace("/^\.+/", '', $str); // trim leading period

    if ($strict) {
        $str = preg_replace("/([^\w\d\\" . $sep . ".])/", '', $str); // only allow words and digits
    } else {
        $str = preg_replace("/([^\w\d\\" . $sep . "\[\]\(\).])/", '', $str); // allow words, digits, [], and ()
    }

    $str = preg_replace("/\\" . $sep . "+/", $sep, $str); // filter multiple separators
    $str = substr($str, 0, $trim); // trim filename to desired length, note 255 char limit on windows

    return $str;
}


// Returns full file name including fallback and extension
function str_file(
    $str,
    $sep = '_',
    $ext = '',
    $default = '',
    $trim = 248) {

    // Run $str and/or $ext through filters to clean up strings
    $str = str_file_filter($str, $sep);
    $ext = '.' . str_file_filter($ext, '', true);

    // Default file name in case all chars are trimmed from $str, then ensure there is an id at tail
    if (empty($str) && empty($default)) {
        $str = 'no_name__' . date('Y-m-d_H-m_A') . '__' . uniqid();
    } elseif (empty($str)) {
        $str = $default;
    }

    // Return completed string
    if (!empty($ext)) {
        return $str . $ext;
    } else {
        return $str;
    }
}

假设有一些用户输入如下:.....&lt;div&gt;&lt;/div&gt;<script></script>&amp; Weiß Göbel 中文百强网File name %20 %20 %21 %2C Décor \/. /. . z \... y \...... x ./ “This name” is & 462^^ not &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = that grrrreat -][09]()1234747) საბეჭდი-და-ტიპოგრაფიული

我们想将其转换为更友好的格式,以便使用文件名长度为255个字符的tar.gz。以下是一个示例用法。注意:此示例包括一个格式错误的tar.gz扩展名作为概念证明,您仍应根据白名单过滤字符串构建后的扩展名。

$raw_str = '.....&lt;div&gt;&lt;/div&gt;<script></script>&amp; Weiß Göbel 中文百强网File name  %20   %20 %21 %2C Décor  \/.  /. .  z \... y \...... x ./  “This name” is & 462^^ not &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = that grrrreat -][09]()1234747) საბეჭდი-და-ტიპოგრაფიული';
$fallback_str = 'generated_' . date('Y-m-d_H-m_A');
$bad_extension = '....t&+++a()r.gz[]';

echo str_file($raw_str, '_', $bad_extension, $fallback_str);

输出结果将是:_wei_gbel_file_name_dcor_._._._z_._y_._x_._this_name_is_462_not_that_grrrreat_][09]()1234747)_.tar.gz 可以在这里进行测试:https://3v4l.org/iSgi8 或者在Gist上查看:https://gist.github.com/dhaupin/b109d3a8464239b7754a 编辑:将脚本过滤器更新为&nbsp;而不是空格,更新了3v4l链接。

在包含\w的字符类中加入\d是没有意义的。 - mickmackusa

2
我今天所知道的最好方法是 Nette 框架中的静态方法 Strings::webalize
顺便说一下,这个方法将所有变音符号转换为它们的基本形式.. š=>s ü=>u ß=>ss 等等。
对于文件名,您需要将点 "." 添加到允许字符参数中。
/**
 * Converts to ASCII.
 * @param  string  UTF-8 encoding
 * @return string  ASCII
 */
public static function toAscii($s)
{
    static $transliterator = NULL;
    if ($transliterator === NULL && class_exists('Transliterator', FALSE)) {
        $transliterator = \Transliterator::create('Any-Latin; Latin-ASCII');
    }

    $s = preg_replace('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s);
    $s = strtr($s, '`\'"^~?', "\x01\x02\x03\x04\x05\x06");
    $s = str_replace(
        array("\xE2\x80\x9E", "\xE2\x80\x9C", "\xE2\x80\x9D", "\xE2\x80\x9A", "\xE2\x80\x98", "\xE2\x80\x99", "\xC2\xB0"),
        array("\x03", "\x03", "\x03", "\x02", "\x02", "\x02", "\x04"), $s
    );
    if ($transliterator !== NULL) {
        $s = $transliterator->transliterate($s);
    }
    if (ICONV_IMPL === 'glibc') {
        $s = str_replace(
            array("\xC2\xBB", "\xC2\xAB", "\xE2\x80\xA6", "\xE2\x84\xA2", "\xC2\xA9", "\xC2\xAE"),
            array('>>', '<<', '...', 'TM', '(c)', '(R)'), $s
        );
        $s = @iconv('UTF-8', 'WINDOWS-1250//TRANSLIT//IGNORE', $s); // intentionally @
        $s = strtr($s, "\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e"
            . "\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3"
            . "\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8"
            . "\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe"
            . "\x96\xa0\x8b\x97\x9b\xa6\xad\xb7",
            'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.');
        $s = preg_replace('#[^\x00-\x7F]++#', '', $s);
    } else {
        $s = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); // intentionally @
    }
    $s = str_replace(array('`', "'", '"', '^', '~', '?'), '', $s);
    return strtr($s, "\x01\x02\x03\x04\x05\x06", '`\'"^~?');
}


/**
 * Converts to web safe characters [a-z0-9-] text.
 * @param  string  UTF-8 encoding
 * @param  string  allowed characters
 * @param  bool
 * @return string
 */
public static function webalize($s, $charlist = NULL, $lower = TRUE)
{
    $s = self::toAscii($s);
    if ($lower) {
        $s = strtolower($s);
    }
    $s = preg_replace('#[^a-z0-9' . preg_quote($charlist, '#') . ']+#i', '-', $s);
    $s = trim($s, '-');
    return $s;
}

为什么要替换变音符号?在将文件名用作“src”或“href”之前,只需使用urlencode()即可。目前唯一具有UTF-8问题的文件系统是FATx(XBOX使用):https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits,而我不认为Web服务器会使用该文件系统。 - mgutt

2

使用此功能仅接受字符串中的单词(支持Unicode,如UTF-8)、"."、"-"和"_":

$sanitized = preg_replace('/[^\w\-\._]/u','', $filename);

下划线包含在\w中。在字符类内,.不需要转义。为了使匹配更长,替换更少,请使用+量词。 - mickmackusa

1
function sanitize_file_name($file_name) { 
 // case of multiple dots
  $explode_file_name =explode('.', $file_name);
  $extension =array_pop($explode_file_name);
  $file_name_without_ext=substr($file_name, 0, strrpos( $file_name, '.') );    
  // replace special characters
  $file_name_without_ext = preg_quote($file_name_without_ext);
  $file_name_without_ext = preg_replace('/[^a-zA-Z0-9\\_]/', '_', $file_name_without_ext);
  $file_name=$file_name_without_ext . '.' . $extension;    
  return $file_name;
}

为什么不将 [^a-zA-Z0-9\\_] 简化为 [^\w\\] 呢? - mickmackusa

1
似乎这一切都取决于一个问题,是否可能创建一个文件名来入侵服务器(或造成其他损害)。如果不行,那么试着在最终使用的操作系统中创建文件(毫无疑问,那将是首选的操作系统)。让操作系统解决。如果有问题,请将投诉作为验证错误返回给用户。
这样做的另一个好处是可靠地可移植,因为所有(我很确定)操作系统都会抱怨文件名未经妥善格式化。
如果可能使用文件名进行恶意活动,也许可以在测试驻留操作系统上的文件名之前应用某些措施 - 这些措施比完全“消毒”文件名要简单得多。

0

用户提供的文件名中的 /.. 可能会对系统造成危害。因此,您应该采取类似以下方式来消除它们:

$fname = str_replace('..', '', $fname);
$fname = str_replace('/',  '', $fname);

这是不够的!例如,文件名“./.name”仍会跳出当前目录。(在此处删除..无效,但删除/将把./.变成..,从而跳出目标目录。) - cemper93
3
不,这个答案只会将字符串转换为..name,这不会破坏任何内容。移除所有路径分隔符就足以防止任何目录遍历。(技术上说,删除..是不必要的。) - cdhowie
@cdhowie 是的,但文件名 ./. 会变成 ..。最后,这个回答漏掉了所有其他文件系统保留字符,比如 NULL。更多内容请参见我的回答:https://dev59.com/eHI-5IYBdhLWcg3wFkOO#42058764 - mgutt

0

一种方法

$bad='/[\/:*?"<>|]/';
$string = 'fi?le*';

function sanitize($str,$pat)
{
    return preg_replace($pat,"",$str);

}
echo sanitize($string,$bad);

1
非打印字符怎么办?在这种情况下最好使用白名单方法而不是黑名单方法。基本上只允许可打印的ASCII文件名,当然要排除特殊字母。但对于非英语环境来说,那就是另一个问题了。 - TheRealChx101

-4

$fname = str_replace('/','',$fname);

由于用户可能使用斜杠来分隔两个单词,最好将其替换为破折号而不是 NULL。


1
他在哪里说要用NULL替换?此外,这并不能处理所有特殊字符。 - Travis Pessetto
1
是的 - 还有其他需要处理的特殊字符。在这里,str_replace 不是最好的选择。 - Martin Kovachev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接