如何对字符串进行清洗以用作文件名?

31

我有一个例程,可以将文件转换为不同的格式并保存。原始数据文件是按编号命名的,但我的例程会根据原始文件中的内部名称给输出文件命名。

我尝试批量运行整个目录,它在遇到一个内部名称带有斜杠的文件时出错。糟糕!如果在这里出现这种情况,它很容易在其他文件上发生。是否有RTL(或WinAPI)例程可以对字符串进行清理,并删除无效符号,使其可安全用作文件名?

8个回答

24
你可以使用 PathGetCharType functionPathCleanupSpec function 或以下技巧:
  function IsValidFilePath(const FileName: String): Boolean;
  var
    S: String;
    I: Integer;
  begin
    Result := False;
    S := FileName;
    repeat
      I := LastDelimiter('\/', S);
      MoveFile(nil, PChar(S));
      if (GetLastError = ERROR_ALREADY_EXISTS) or
         (
           (GetFileAttributes(PChar(Copy(S, I + 1, MaxInt))) = INVALID_FILE_ATTRIBUTES)
           and
           (GetLastError=ERROR_INVALID_NAME)
         ) then
        Exit;
      if I>0 then
        S := Copy(S,1,I-1);
    until I = 0;
    Result := True;
  end;

这段代码将字符串分成几部分,并使用MoveFile验证每个部分。对于无效字符或保留的文件名(如“COM”),MoveFile将失败并为有效文件名返回成功或ERROR_ALREADY_EXISTS。


PathCleanupSpec位于Win32API/JwaShlObj.pas下的Jedi Windows API中。


谢谢!PathCleanupSpec 看起来正是我所需要的。 - Mason Wheeler
2
MoveFile()函数的第一个参数中使用nil是未记录的行为。此外,只有在MoveFile()首次返回FALSE时才检查GetLastError(),而此代码没有进行检查。 - Remy Lebeau
有人在 Delphi 中成功使用 PathCleanupSpec 吗?我总是得到一个空字符串。从这里开始 https://narkive.com/l2Um5mzw:3.379.300,现在已经尝试了几个小时,但仍无法使其正常工作。 - Reversed Engineer

12

关于是否有任何API函数可以清理文件名(甚至检查其有效性)的问题 - 似乎没有。引用自PathSearchAndQualify()函数的评论:

没有出现任何Windows API可以验证用户输入的路径;每个应用程序都需要自行解决此问题。

所以,您只能查看来自文件名、路径和命名空间(Windows)中的文件名合法性规则:

  • 使用当前代码页中几乎所有字符作为名称(包括Unicode字符和扩展字符集中的字符(128-255)),除了以下内容:

    • 不允许使用以下保留字符:
      < > : " / \ | ? *
    • 不允许使用整数表示在零到31之间的字符。
    • 目标文件系统不允许的任何其他字符。
  • 不要将以下保留设备名称用于文件名称:CONPRNAUXNULCOM1..COM9LPT1..LPT9
    还应避免紧接着这些名称的扩展名;例如,不建议使用NUL.txt

如果您知道您的程序只会写入NTFS文件系统,那么您可能可以确信该文件系统没有其他不允许的字符,因此您只需要检查文件名是否过长(使用MAX_PATH常量),在删除所有无效字符之后(或者替换为下划线等)。

程序还应确保文件名清理不会导致文件名冲突,并且它会自动覆盖以相同名称结束的其他文件。


9
{
  CleanFileName
  ---------------------------------------------------------------------------

  Given an input string strip any chars that would result
  in an invalid file name.  This should just be passed the
  filename not the entire path because the slashes will be
  stripped.  The function ensures that the resulting string
  does not hae multiple spaces together and does not start
  or end with a space.  If the entire string is removed the
  result would not be a valid file name so an error is raised.

}

function CleanFileName(const InputString: string): string;
var
  i: integer;
  ResultWithSpaces: string;
begin

  ResultWithSpaces := InputString;

  for i := 1 to Length(ResultWithSpaces) do
  begin
    // These chars are invalid in file names.
    case ResultWithSpaces[i] of 
      '/', '\', ':', '*', '?', '"', '<', '>', '|', ' ', #$D, #$A, #9:
        // Use a * to indicate a duplicate space so we can remove
        // them at the end.
        {$WARNINGS OFF} // W1047 Unsafe code 'String index to var param'
        if (i > 1) and
          ((ResultWithSpaces[i - 1] = ' ') or (ResultWithSpaces[i - 1] = '*')) then
          ResultWithSpaces[i] := '*'
        else
          ResultWithSpaces[i] := ' ';

        {$WARNINGS ON}
    end;
  end;

  // A * indicates duplicate spaces.  Remove them.
  result := ReplaceStr(ResultWithSpaces, '*', '');

  // Also trim any leading or trailing spaces
  result := Trim(Result);

  if result = '' then
  begin
    raise(Exception.Create('Resulting FileName was empty Input string was: '
      + InputString));
  end;
end;

6
// for all platforms (Windows\Unix), uses IOUtils.
function ReplaceInvalidFileNameChars(const aFileName: string; const aReplaceWith: Char = '_'): string;
var
  i: integer;
begin
  Result := aFileName;
  for i := Low(Result) to High(Result) do
  begin
    if not TPath.IsValidFileNameChar(Result[i]) then
      Result[i] := aReplaceWith;
  end;
end.

1
这是最有价值的答案,因为它仅依赖于专为此目的而创建的Delphi函数。 - undefined

4

对于其他想使用PathCleanupSpec的读者,我编写了这个测试程序,似乎可以正常工作......在网络上缺乏明确的示例。 您需要包含ShlObj.pas(不确定何时添加了PathCleanupSpec,但我在Delphi 2010中进行了测试) 您还需要检查XP sp2或更高版本。

procedure TMainForm.btnTestClick(Sender: TObject);
var
  Path: array [0..MAX_PATH - 1] of WideChar;
  Filename: array[0..MAX_PATH - 1] of WideChar;
  ReturnValue: integer;
  DebugString: string;

begin
  StringToWideChar('a*dodgy%\filename.$&^abc',FileName, MAX_PATH);
  StringToWideChar('C:\',Path, MAX_PATH);
  ReturnValue:= PathCleanupSpec(Path,Filename);
  DebugString:= ('Cleaned up filename:'+Filename+#13+#10);
  if (ReturnValue and $80000000)=$80000000 then
    DebugString:= DebugString+'Fatal result. The cleaned path is not a valid file name'+#13+#10;
  if (ReturnValue and $00000001)=$00000001 then
    DebugString:= DebugString+'Replaced one or more invalid characters'+#13+#10;
  if (ReturnValue and $00000002)=$00000002 then
    DebugString:= DebugString+'Removed one or more invalid characters'+#13+#10;
  if (ReturnValue and $00000004)=$00000004 then
    DebugString:= DebugString+'The returned path is truncated'+#13+#10;
  if (ReturnValue and $00000008)=$00000008 then
    DebugString:= DebugString+'The input path specified at pszDir is too long to allow the formation of a valid file name from pszSpec'+#13;
  ShowMessage(DebugString);
end;

谢谢!在你的回答之前,我无法让PathCleanupSpec正常工作。 - Reversed Engineer

2
好的,简单的方法是使用正则表达式和您喜欢的语言版本的gsub来替换任何不是“单词字符”的内容。在大多数具有类Perl的正则表达式的语言中,这个字符类将是“\w”,或者作为一个简单选项,否则为“[A-Za-z0-9]”。
特别是与其他答案中的一些示例相比,您不希望寻找无效的字符以删除,而是要寻找有效的字符以保留。如果您正在寻找无效的字符,您总是容易受到新字符的影响,但如果您只寻找有效的字符,您可能会稍微低效(因为您替换了一个您实际上不需要的字符),但至少您永远不会错。
现在,如果您想让新版本尽可能像旧版本一样,您可以考虑替换。而不是删除,您可以替换一个或多个您知道是可以的字符。但是这样做是一个有趣的问题,可能是另一个问题的好题目。

1
不行。考虑到最近版本的Windows支持完整的Unicode文件名,而像Ä£̆Ώۑ≥♣.txt这样的文件名是有效的,你绝对需要一个黑名单来执行此操作,而不是白名单。 - Mason Wheeler
不是我理解问题的方式。你不是在寻找任意字符串是否为有效文件名,而是要从任意字符串的转换中保证一个有效的文件名。这些(或许微妙地)不同。例如,如果你可以将任何字符串转换成唯一的8位数字,那么它可能与原始字符串没有明显的关系,但仍然保证你可以将该字符串保存到磁盘上。 - cjs
是的。我正在寻求从任意字符串的转换中保证有效文件名,同时尽可能保留原始字符串所传达的信息。 - Mason Wheeler
您是否也希望文件名尽可能地与原始字符串相似? - cjs
1
是的,那正是我想要的。 - Mason Wheeler
2
+1 消毒必须始终使用白名单。否则,一旦新的输入变得可能,或者以前可以接受的输入变得危险(解释代码中的代码更改),您就会变得脆弱。 - sleske

0

使用这个函数。对我来说很好用。 目的是获取一个目录级别的名称

使用shelobj...

function  CleanDirName(DirFileName : String) : String;
var
  CheckStr : String;
  Path: array [0..MAX_PATH - 1] of WideChar;
  Filename: array[0..MAX_PATH - 1] of WideChar;
  ReturnValue: integer;

begin
  //--     The following are considered invalid characters in all names.
  //--     \ / : * ? " < > |

  CheckStr := Trim(DirFileName);
  CheckStr := StringReplace(CheckStr,'/','-',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'\','-',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'.','-',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,':',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'?',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'<',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'>',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'|',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'!',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'~',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'+',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'=',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,')',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'(',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'*',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'&',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'^',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'%',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'$',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'#',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'@',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'{',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'}',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,'"',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,';',' ',[rfReplaceAll, rfIgnoreCase]);
  CheckStr := StringReplace(CheckStr,',',' ',[rfReplaceAll, rfIgnoreCase]);

  // '' become - nil
  CheckStr := StringReplace(CheckStr,'''','',[rfReplaceAll, rfIgnoreCase]);

  StringToWideChar(CheckStr,FileName, MAX_PATH);
  StringToWideChar('C:\',Path, MAX_PATH);
  ReturnValue:= PathCleanupSpec(Path,Filename);

  Filename := StringReplace(Filename,'  ',' ',[rfReplaceAll, rfIgnoreCase]);
  Result := String(Filename);
end;

这是给那些想要避免循环的人。请参见DRY - AmigoJack

0

在现代 Delphi 上尝试一下:

 use System.IOUtils;
 ...
 result := TPath.HasValidFileNameChars(FileName, False)

我也允许在文件名中使用德语umlauts或其他字符,如-,_等。


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