使用C#检测文件名字符是否被视为国际字符

3
我写了一个小型控制台应用程序(下面是源代码),用于查找并可选地重命名包含国际字符的文件,因为它们是大多数源代码控制系统的不断痛点(有关此问题的背景请参见下文)。我使用的代码具有一个简单的字典,其中包含要查找和替换的字符(并消除每个使用多个存储字节的其他字符),但感觉非常hackish。 (a)找出字符是否为国际字符的正确方法是什么?(b)最佳ASCII替换字符是什么?
让我提供一些背景信息,解释为什么需要这样做。恰好,丹麦的Å字符在UTF-8中有两种不同的编码,都代表相同的符号。这些被称为NFC和NFD编码。 Windows和Linux默认创建NFC编码,但尊重任何给定的编码。 Mac将所有名称(保存到HFS +分区时)转换为NFD,因此为Windows创建的文件的名称返回不同的字节流。这实际上破坏了Subversion,Git和许多其他未能妥善处理此方案的实用程序。
我目前正在评估Mercurial,结果发现它处理国际字符的能力甚至更差...由于这些问题而感到相当疲倦,无论是源代码控制还是国际字符都必须消失,因此我们在这里。
我的当前实现:
public class Checker
{
    private Dictionary<char, string> internationals = new Dictionary<char, string>();
    private List<char> keep = new List<char>();
    private List<char> seen = new List<char>();

    public Checker()
    {
        internationals.Add( 'æ', "ae" );
        internationals.Add( 'ø', "oe" );
        internationals.Add( 'å', "aa" );
        internationals.Add( 'Æ', "Ae" );
        internationals.Add( 'Ø', "Oe" );
        internationals.Add( 'Å', "Aa" );

        internationals.Add( 'ö', "o" );
        internationals.Add( 'ü', "u" );
        internationals.Add( 'ä', "a" );
        internationals.Add( 'é', "e" );
        internationals.Add( 'è', "e" );
        internationals.Add( 'ê', "e" );

        internationals.Add( '¦', "" );
        internationals.Add( 'Ã', "" );
        internationals.Add( '©', "" );
        internationals.Add( ' ', "" );
        internationals.Add( '§', "" );
        internationals.Add( '¡', "" );
        internationals.Add( '³', "" );
        internationals.Add( '­', "" );
        internationals.Add( 'º', "" );

        internationals.Add( '«', "-" );
        internationals.Add( '»', "-" );
        internationals.Add( '´', "'" );
        internationals.Add( '`', "'" );
        internationals.Add( '"', "'" );
        internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 147 } )[ 0 ], "-" );
        internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 148 } )[ 0 ], "-" );
        internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 153 } )[ 0 ], "'" );
        internationals.Add( Encoding.UTF8.GetString( new byte[] { 226, 128, 166 } )[ 0 ], "." );

        keep.Add( '-' );
        keep.Add( '=' );
        keep.Add( '\'' );
        keep.Add( '.' );
    }

    public bool IsInternationalCharacter( char c )
    {
        var s = c.ToString();
        byte[] bytes = Encoding.UTF8.GetBytes( s );
        if( bytes.Length > 1 && ! internationals.ContainsKey( c ) && ! seen.Contains( c ) )
        {
            Console.WriteLine( "X '{0}' ({1})", c, string.Join( ",", bytes ) );
            seen.Add( c );
            if( ! keep.Contains( c ) )
            {
                internationals[ c ] = "";
            }
        }
        return internationals.ContainsKey( c );
    }

    public bool HasInternationalCharactersInName( string name, out string safeName )
    {
        StringBuilder sb = new StringBuilder();
        Array.ForEach( name.ToCharArray(), c => sb.Append( IsInternationalCharacter( c ) ? internationals[ c ] : c.ToString() ) );
        int length = sb.Length;
        sb.Replace( "  ", " " );
        while( sb.Length != length )
        {
            sb.Replace( "  ", " " );
        }
        safeName = sb.ToString().Trim();
        string namePart = Path.GetFileNameWithoutExtension( safeName );
        if( namePart.EndsWith( "." ) )
            safeName = namePart.Substring( 0, namePart.Length - 1 ) + Path.GetExtension( safeName );
        return name != safeName;
    }
}

而这个将会被这样调用:

FileInfo file = new File( "Århus.txt" );
string safeName;    
if( checker.HasInternationalCharactersInName( file.Name, out safeName ) )
{
    // rename file 
}

1
请注意,从''到""的映射实际上在单引号之间包含一个字符。 我真的很惊讶我可以将它们从控制台窗口复制到Visual Studio,然后通过Chrome到StackOverflow,而且它们仍然看起来完全正确。但是一旦我们谈论文件名而不是内容,那么我们就回到了20世纪80年代。 - Morten Mertner
3个回答

2

(a) 简单。检查是否存在大于127的代码点。

(b) 尝试使用NKFD规范化和/或uni2ascii工具。


哪一个字节是代码点?我可以调查一下,但如果你知道就请给我一个提示。尽管提供了 C 源代码,但似乎 Windows 上没有 uni2ascii 工具可用,所以我可以查看源代码。不想自己实现规范化而重复造轮子——难道没有 C# 库或 Windows API 可用吗? - Morten Mertner
Unicode代码点是一个21位的数字。它可以被编码为UTF-8中的1-4个字节,1-2个UTF-16代码单元或1个UTF-32代码单元。这三种编码方式都使用0-127范围内的单一代码单元来表示ASCII字符。Windows API有一个名为NormalizeString的函数。 - dan04

1
如果你不介意使用蛮力的话,可以尝试类似这样的方法:
string name = "Århus.txt";
string kd = name.Normalize(NormalizationForm.FormKD);
byte[] kd_bytes = Encoding.Unicode.GetBytes(kd);
byte[] ascii_bytes = Encoding.Convert(Encoding.Unicode, Encoding.ASCII, kd_bytes);
string flattened = Encoding.ASCII.GetString(ascii_bytes);

这将把Århus.txt转换为A?rhus.txt,因为KD表单会将Å分开,而转换为7位ASCII将丢失发音符号。如何处理剩余的小?由你决定。

其他字符可能有所不同,但我想KD归一化应该可以解决问题。我已经很多年没有做过代码页转换,但我觉得这个问题很有趣。

编辑:

我刚才尝试了æÆØ,它们都转换为了?,所以这可能对你来说太丢失了。但仍然可能会给你一些线索,带领你找到答案。


谢谢,我会尝试用这种方法进行实验。 - Morten Mertner

1
在这个时代,出现这种问题确实很糟糕。显然,MAC 使用的 NFD 表单让你头疼不已。你可以考虑的一件事是从字形中删除导致 NFD 与 NFC 不同的变音符号。
我不能百分之百确定这完全准确(尤其是对于亚洲文字),但应该很接近。
public static string RemoveDiacriticals(string txt) {
  string nfd = txt.Normalize(NormalizationForm.FormD);
  StringBuilder retval = new StringBuilder(nfd.Length);
  foreach (char ch in nfd) {
    if (ch >= '\u0300' && ch <= '\u036f') continue;
    if (ch >= '\u1dc0' && ch <= '\u1de6') continue;
    if (ch >= '\ufe20' && ch <= '\ufe26') continue;
    if (ch >= '\u20d0' && ch <= '\u20f0') continue;
    retval.Append(ch);
  }
  return retval.ToString();
}

这看起来就是我一直在寻找的。我想我会采用一种方法,将字符串使用不同的规范化方式进行归一化,并比较结果。这个方法结合了dan04的答案,应该可以解决谜题的第一部分。我仍然需要弄清楚最好的ASCII替换字符是什么,最好有一个不需要表格或字典的代码解决方案。一旦我有一些更新的代码可以展示,我会发布一个新问题来询问。 - Morten Mertner

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