文件名字符串过滤器

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个回答

186

对Tor Valamo的解决方案进行小调整以解决Dominic Rodger注意到的问题,您可以使用以下方法:

// Remove anything which isn't a word, whitespace, number
// or any of the following caracters -_~,;[]().
// If you don't need to handle multi-byte characters
// you can use preg_replace rather than mb_ereg_replace
// Thanks @Łukasz Rysiak!
$file = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $file);
// Remove any runs of periods (thanks falstro!)
$file = mb_ereg_replace("([\.]{2,})", '', $file);

2
@iim.hlk - 是的,缺少了括号。我现在已经添加了。谢谢! - Sean Vieira
2
这里有一个漏洞,你应该将其分成两部分,并在之后运行对“..”的检查。例如,“.?. ”最终会变成“..”。虽然由于你过滤了“/”,我现在看不出你如何进一步利用它,但这说明了为什么在这里检查“..”是无效的。更好的方法可能是,如果不符合条件,则拒绝替换。 - falstro
2
因为这些值都不是Windows文件系统上非法的,为什么要失去更多信息呢?你可以将正则表达式更改为简单的[^a-z0-9_-],如果你想要更加严格 - 或者只需使用生成的名称,丢弃给定的名称,避免所有这些问题。 :-) - Sean Vieira
3
请注意:冒号是非法的。 - JasonXA
1
我会添加 trim() 函数来去除前后的空格,这样复制粘贴的 filename.txt 就会被清理成 filename.txt - Slava
显示剩余20条评论

86
这是如何对文件系统进行文件名清理的方法。
function filter_filename($name) {
    // remove illegal file system characters https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
    $name = str_replace(array_merge(
        array_map('chr', range(0, 31)),
        array('<', '>', ':', '"', '/', '\\', '|', '?', '*')
    ), '', $name);
    // maximise filename length to 255 bytes http://serverfault.com/a/9548/44086
    $ext = pathinfo($name, PATHINFO_EXTENSION);
    $name= mb_strcut(pathinfo($name, PATHINFO_FILENAME), 0, 255 - ($ext ? strlen($ext) + 1 : 0), mb_detect_encoding($name)) . ($ext ? '.' . $ext : '');
    return $name;
}

除了文件系统,其他任何东西都是允许的,所以问题得到了完美的回答...

...但是如果您在不安全的HTML上下文中稍后使用它,例如在文件名中允许单引号'可能会很危险,因为这个绝对合法的文件名:

 ' onerror= 'alert(document.cookie).jpg

变成了一个XSS漏洞

<img src='<? echo $image ?>' />
// output:
<img src=' ' onerror= 'alert(document.cookie)' />

因此,流行的 CMS 软件 WordPress 删除了它们,但只有在一些更新之后才覆盖了所有相关字符。
$special_chars = array("?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}", "%", "+", chr(0));
// ... a few rows later are whitespaces removed as well ...
preg_replace( '/[\r\n\t -]+/', '-', $filename )

最终,他们的列表现在包括了大部分 URI保留字符URL不安全字符 列表中的字符。

当然,你可以简单地在HTML输出中对所有这些字符进行编码,但大多数开发人员和我都遵循谚语 "宁愿安全也不要后悔" 并提前删除它们。

因此,我建议使用以下内容:

function filter_filename($filename, $beautify=true) {
    // sanitize filename
    $filename = preg_replace(
        '~
        [<>:"/\\\|?*]|            # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
        [\x00-\x1F]|             # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
        [\x7F\xA0\xAD]|          # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
        [#\[\]@!$&\'()+,;=]|     # URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2
        [{}^\~`]                 # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
        ~x',
        '-', $filename);
    // avoids ".", ".." or ".hiddenFiles"
    $filename = ltrim($filename, '.-');
    // optional beautification
    if ($beautify) $filename = beautify_filename($filename);
    // maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
    $ext = pathinfo($filename, PATHINFO_EXTENSION);
    $filename = mb_strcut(pathinfo($filename, PATHINFO_FILENAME), 0, 255 - ($ext ? strlen($ext) + 1 : 0), mb_detect_encoding($filename)) . ($ext ? '.' . $ext : '');
    return $filename;
}

除了会对文件系统造成问题的内容,其他应该作为额外功能的一部分:

function beautify_filename($filename) {
    // reduce consecutive characters
    $filename = preg_replace(array(
        // "file   name.zip" becomes "file-name.zip"
        '/ +/',
        // "file___name.zip" becomes "file-name.zip"
        '/_+/',
        // "file---name.zip" becomes "file-name.zip"
        '/-+/'
    ), '-', $filename);
    $filename = preg_replace(array(
        // "file--.--.-.--name.zip" becomes "file.name.zip"
        '/-*\.-*/',
        // "file...name..zip" becomes "file.name.zip"
        '/\.{2,}/'
    ), '.', $filename);
    // lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625
    $filename = mb_strtolower($filename, mb_detect_encoding($filename));
    // ".file-name.-" becomes "file-name"
    $filename = trim($filename, '.-');
    return $filename;
}

在这一点上,如果结果为空,您需要生成一个文件名,并且您可以决定是否要编码UTF-8字符。但是,在用于Web托管上下文的所有文件系统中,都允许使用UTF-8,因此您不需要进行编码。

您需要做的唯一一件事就是使用urlencode()(希望您对所有URL都这样做),以便文件名საბეჭდი_მანქანა.jpg变成此URL作为您的<img src><a href>http://www.maxrev.de/html/img/%E1%83%A1%E1%83%90%E1%83%91%E1%83%94%E1%83%AD%E1%83%93%E1%83%98_%E1%83%9B%E1%83%90%E1%83%9C%E1%83%A5%E1%83%90%E1%83%9C%E1%83%90.jpg

Stackoverflow可以这样做,所以我可以像用户一样发布此链接:
http://www.maxrev.de/html/img/საბეჭდი_მანქანა.jpg 因此,这是一个完全合法的文件名,而不是问题,如@SequenceDigitale.com在他的答案中提到的那样

4
干得好。对我来说最有帮助的答案。+1 - user5147563
1
哦,好吧...刚刚进行了调试,发现问题出现在filter_filename()函数中的preg_replace之后。 - user5147563
你删除了哪些注释?如果这样更容易,请给我发送电子邮件:http://gutt.it/contact.htm - mgutt
4
注意:正则表达式中的双反斜杠需要再加上第三个斜杠以适用于 PHP 字符串。否则,preg_replace('~[<>:"/\\|?*]~x','-', $filename) 会允许 Hello\World.txt 正常通过!将 [<>:"/\\|?*] 改为 [<>:"/\\\|?*] 即可修复该问题。 - spackmat
非常好的写作。我以为PHP会有内置的东西来处理这个问题,但惊讶地发现它没有。但是这比我自己写的要更符合我的需求。 - rolinger
显示剩余9条评论

56

解决方案1 - 简单而有效

$file_name = preg_replace( '/[^a-z0-9]+/', '-', strtolower( $url ) );

  • strtolower()保证文件名为小写(因为URL内部大小写不重要,但在NTFS文件名中区分大小写)
  • [^a-z0-9]+将确保文件名仅包含字母和数字
  • '-'替换无效字符可使文件名可读性更好

示例:

URL:  https://dev59.com/eHI-5IYBdhLWcg3wFkOO
File: http-stackoverflow-com-questions-2021624-string-sanitizer-for-filename

解决方案2 - 适用于非常长的URL

您希望缓存URL内容并且只需要具有唯一文件名。 我建议使用以下函数:

$file_name = md5( strtolower( $url ) )

这将创建一个固定长度的文件名。在大多数情况下,MD5哈希对这种用途是足够独特的。

示例:

URL:  https://www.amazon.com/Interstellar-Matthew-McConaughey/dp/B00TU9UFTS/ref=s9_nwrsa_gw_g318_i10_r?_encoding=UTF8&fpl=fresh&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=desktop-1&pf_rd_r=BS5M1H560SMAR2JDKYX3&pf_rd_r=BS5M1H560SMAR2JDKYX3&pf_rd_t=36701&pf_rd_p=6822bacc-d4f0-466d-83a8-2c5e1d703f8e&pf_rd_p=6822bacc-d4f0-466d-83a8-2c5e1d703f8e&pf_rd_i=desktop
File: 51301f3edb513f6543779c3a5433b01c

4
可能MD5有问题:在使用哈希和URL时要小心。虽然URL数量的平方根仍然比当前网络规模大得多,但如果发生冲突,您将得到关于Britney Spears的页面,而不是您预期的Bugzilla页面。在我们的情况下,这可能不是问题,但对于数十亿个页面,我会选择更大的哈希算法,例如SHA 256,或者完全避免使用哈希。 - adilbo
解决方案 1 ❤️ 。这就是我在简单的下载方法中所需要的全部。 - Gianpaolo Scrigna

43

使用rawurlencode()怎么样?http://www.php.net/manual/en/function.rawurlencode.php

这里有一个可以过滤中文字符的函数:

public static function normalizeString ($str = '')
{
    $str = strip_tags($str); 
    $str = preg_replace('/[\r\n\t ]+/', ' ', $str);
    $str = preg_replace('/[\"\*\/\:\<\>\?\'\|]+/', ' ', $str);
    $str = strtolower($str);
    $str = html_entity_decode( $str, ENT_QUOTES, "utf-8" );
    $str = htmlentities($str, ENT_QUOTES, "utf-8");
    $str = preg_replace("/(&)([a-z])([a-z]+;)/i", '$2', $str);
    $str = str_replace(' ', '-', $str);
    $str = rawurlencode($str);
    $str = str_replace('%', '-', $str);
    return $str;
}
  1. 去除HTML标签
  2. 删除换行符/制表符/回车符
  3. 删除文件夹和文件名中的非法字符
  4. 将字符串转换为小写
  5. 将含有外语重音符号(如Éàû)的字符转换为HTML实体,然后移除代码并保留字母。
  6. 用短横线代替空格
  7. 编码特殊字符,以避免与服务器上的文件名冲突。例如:“中文百强网”
  8. 将“%”替换为短横线,以确保查询文件时浏览器不会重写文件链接。

有些文件名可能不相关,但在大多数情况下它将起作用。

例如:

原始文件名:“საბეჭდი-და-ტიპოგრაფიული.jpg”

输出文件名:“-E1-83-A1-E1-83-90-E1-83-91-E1-83-94-E1-83-AD-E1-83-93-E1-83-98--E1-83-93-E1-83-90--E1-83-A2-E1-83-98-E1-83-9E-E1-83-9D-E1-83-92-E1-83-A0-E1-83-90-E1-83-A4-E1-83-98-E1-83-A3-E1-83-9A-E1-83-98.jpg”

这样比404错误更好。

希望对您有所帮助。

卡尔。


1
您没有移除空字符和控制字符。ASCII码从0到32的字符都应该从字符串中移除。 - Basil Musa
UTF-8在文件系统和URL中都是允许的,那么为什么会产生404错误呢?你需要做的唯一一件事就是将URL http://www.maxrev.de/html/img/საბეჭდი_მანქანა.jpg 编码为 http://www.maxrev.de/html/img/%E1%83%A1%E1%83%90%E1%83%91%E1%83%94%E1%83%AD%E1%83%93%E1%83%98_%E1%83%9B%E1%83%90%E1%83%9C%E1%83%A5%E1%83%90%E1%83%9C%E1%83%90.jpg,就像你希望对所有URL进行的那样,在HTML源代码中。 - mgutt
1
一些其他的要点:你可以通过 strip_tags() 去除 HTML 标签,然后再去除 [<>]。这样 strip_tags() 实际上就不是必需的了。同样的道理也适用于引号。当你使用 ENT_QUOTES 解码时,就没有引号了。而且 str_replace() 不会删除连续的空格,然后你又使用 strtolower() 处理多字节字符串。为什么你要转换成小写呢?最后,你没有像 @BasilMusa 提到的那样捕获任何保留字符。更多细节请参见我的答案:https://dev59.com/eHI-5IYBdhLWcg3wFkOO#42058764。 - mgutt
爱上了它! - Yash Kumar Verma
为什么要创建从未在替换中使用的捕获组?为什么不用\s替换[\r\n\t ]?在[\"\*\/\:\<\>\?\'\|]中有太多不必要的转义。 - mickmackusa

42

不必担心遗漏字符 - 为什么不使用一个白名单来限制可以使用的字符呢?例如,您可以只允许使用a-z0-9_和一个点(.)的实例。这显然比大多数文件系统更具限制性,但可以保障您的安全。


46
对于带有变音符号的语言来说,这种翻译并不理想。这会导致将“Québec”翻译成“Qubec”,“Düsseldorf”翻译成“Dsseldorf”等等。 - Pekka
17
确实如此,但就像我所说的:“例如”。 - Dominic Rodger
5
可能对于原帖作者来说这是完全可以接受的。否则,可以使用类似于http://php.net/manual/en/class.normalizer.php的东西。 - Blair McMillan
4
实际上这并不是被问到的内容。原帖要求一个函数来清理字符串,而不是提供一个替代方法。 - i.am.michiel
4
@i.am.michiel,也许是这样,但鉴于发布者接受了它,我会假设他们认为它很有帮助。 - Dominic Rodger
显示剩余9条评论

22
嗯,tempnam()会为你完成这个任务。

https://www.php.net/manual/en/function.tempnam.php

但这会创建一个全新的名称。
要对现有字符串进行清理,只需限制用户输入的内容,并将其限制为字母、数字、句点、连字符和下划线,然后使用简单的正则表达式进行清理。检查哪些字符需要转义,否则可能会出现错误的结果。
$sanitized = preg_replace('/[^a-zA-Z0-9\-\._]/','', $filename);

15

安全性:将每个非 "a-zA-Z0-9_-" 字符序列替换为破折号;自行添加扩展名。

$name = preg_replace('/[^a-zA-Z0-9_-]+/', '-', strtolower($name)).'.'.$extension;

所以有一个名为PDF的文件

"This is a grüte test_service +/-30 thing"

变成

"This-is-a-gr-te-test_service-30-thing.pdf"

1
你需要加上文件扩展名并用“.”隔开:$name = preg_replace('/[^a-zA-Z0-9_-]+/', '-', strtolower($name)).'.'.$extension; - Edmunds22
“[^\w-]+”有什么问题?如果您要对输入进行无条件调用“strtolower()”,则在字符类中包含“A-Z”的意义何在?您不应该使用“mb_strtolower()”并添加“u”模式修饰符以确保始终将输入文本解析为单个字节吗?我不知道那些不支持多字节的技术如何分割(任何)多字节字符--是否会产生意外的有效字符? - mickmackusa

14
preg_replace("[^\w\s\d\.\-_~,;:\[\]\(\]]", '', $file)

根据你的系统允许的内容,添加/删除更多有效字符。

或者,您可以尝试创建文件,如果文件不正确则返回错误。


5
那样会允许像 .. 这样的文件名通过,这可能是一个问题也可能不是。 - Dominic Rodger
1
@Dom - 请单独检查该值,因为它是一个固定值。 - Tor Valamo
此页面上所有将\d_写在同一字符类中的答案都表明缺乏正则表达式模式基础。 - mickmackusa

12

PHP提供了一个函数来将文本转换为不同的格式

filter_var()与第二个参数FILTER_SANITIZE_URL一起使用

如何使用:

echo filter_var(
   "Lorem Ipsum has been the industry's", FILTER_SANITIZE_URL
); 

示例输出:

LoremIpsum已成为该行业的


3
好的,但它不能去除斜杠,这可能会成为一个问题:目录遍历。 - func0der
在Windows系统中,文件名的非法字符列表包括\ / : * ? " < > |。 然而,这些字符在使用FILTER_SANITIZE_URL规则时都是被允许的。 - thelr
作为变量 - FILTER_SANITIZE_EMAIL。删除除字母、数字和 !#$%&'*+-=?^_\{|}~@.[]` 之外的所有字符。 - dobs

7
对于Sean Vieira的解决方案,如果要允许单个点的微调,您可以使用以下方法:
preg_replace("([^\w\s\d\.\-_~,;:\[\]\(\)]|[\.]{2,})", '', $file)

字符类中的字面点不受转义反斜杠的影响。我不建议使用“(”和“)”作为模式分隔符,因为这可能会使新手对正则表达式感到困惑——他们可能会认为它是一个捕获组,并且根本没有分隔符。 - mickmackusa

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