有没有更优雅的方法将Unicode转换为ASCII?

4

我经常遇到这样的问题,就是有一些很难以辨认的Unicode字符,它们与某些ASCII字符相似,需要在运行时进行转换。

在这种情况下,我正在尝试导出到CSV。除了已经使用过的修复破折号、长破折号、短破折号和水平线外,我刚刚收到了一个新的请求,即“ ` ”。除了使用另一种不好的修复方法之外,还有其他更好的方法吗?

以下是我目前所拥有的内容...

        formattedString = formattedString.Replace(char.ConvertFromUtf32(8211), "-");
        formattedString = formattedString.Replace(char.ConvertFromUtf32(8212), "-");
        formattedString = formattedString.Replace(char.ConvertFromUtf32(8213), "-");

有什么想法吗?

听起来类似于https://dev59.com/InE85IYBdhLWcg3wwWat - Daniel Gehriger
我只需将其存储到文本文件中,然后使用Perl正则表达式进行处理,无需使用.NET框架。 - David Heffernan
4个回答

7

这是一个相当不够优雅的问题,因此没有一种方法会真正地优雅。

不过,我们肯定可以改进。哪种方法最好取决于需要进行的更改数量(以及要更改的字符串大小,尽管通常最好假设它可能很大)。

在一个替换字符的情况下,到目前为止使用.Replace的方法是优越的,但我会用"\u2013"替换char.ConvertFromUtf32(8211)。对性能的影响微不足道,但它更易读,因为通常用十六进制引用该字符而不是用十进制表示法(当然,char.ConvertFromUtf32(0x2013)在这方面也有同样的优势,但在只使用字符符号时没有优势)。 (在某些情况下,也可以直接将'–'放入代码中-在某些情况下更易读,但在这种情况下,它看起来与‒、—或-几乎相同,对读者来说不太容易分辨)。

我还会用稍快的字符替换(在这种情况下至少是这样,因为你要用单个字符替换另一个单个字符)来代替字符串替换。

采用这种方法改进您的代码如下:

formattedString = formattedString.Replace('\u2013', '-');
formattedString = formattedString.Replace('\u2014', '-');
formattedString = formattedString.Replace('\u2015', '-');

即使只有三个替换,这种方法的效率可能仍然不如一次性完成所有这些替换(我不会测试需要多长的formattedString才能达到此效果,在某个数量级以上,即使对于只有几个字符的字符串,一次性处理也更有效)。一种方法是:
StringBuilder sb = new StringBuilder(formattedString.length);//we know this is the capacity so we initialise with it:
foreach(char c in formattedString)
  switch(c)
  {
    case '\u2013': case '\u2014': case '\u2015':
      sb.Append('-');
    default:
      sb.Append(c)
  }
formattedString = sb.ToString();

另一个可能性是检查 (int)c >= 0x2013 && (int)c <= 0x2015 是否成立,但是分支数量的减少很小,并且如果你要查找的大多数字符在数值上不接近,则此方法不相关。

对于各种变体(例如,如果 formattedString 最终要输出到流中,则最好在获取每个最终字符时进行输出,而不是再次缓冲)。

请注意,此方法无法处理您搜索的多个字符字符串,但可以用于输出字符串,例如我们可以包括:

case 'ß':
  sb.Append("ss");

现在,这种方法比之前更有效,但在一定数量的替换案例后仍然变得难以控制。它还涉及许多分支,这些分支有自己的性能问题。
让我们考虑相反的问题。假设您想要将字符从仅在US-ASCII范围内的源转换。您只有128个可能的字符,因此您的方法可能是:
char[] replacements = {/*list of replacement characters*/}
StringBuilder sb = new StringBuilder(formattedString.length);
foreach(char c in formattedString)
  sb.Append(replacements[(int)c]);
formattedString = sb.ToString();

现在,对于Unicode来说这是不切实际的。它有超过109,000个字符,范围从0到1114111。然而,你关心的字符很可能不仅比那小得多(如果你真的关心那么多情况,你会想要上面给出的方法),而且还在一个相对受限制的块中。
同时考虑一下,如果你并不特别关心任何代理项(我们稍后会介绍)。嗯,大多数字符你其实并不关心,所以让我们考虑这个:
char[] unchanged = new char[128];
for(int i = 0; i != 128; ++i)
  unchanged[i] = (char)i;
char[] error = new string('\uFFFD', 128).ToCharArray();
char[] block0 = (new string('\uFFFD', 13) + "---" + new string('\uFFFD', 112)).ToCharArray();

char[][] blocks = new char[8704][];
for(int i = 1; i != 8704; ++i)
  blocks[i] = error;
blocks[0] = unchanged;
blocks[64] = block0;

/* the above need only happen once, so it could be done with static members of a helper class that are initialised in a static constructor*/

StringBuilder sb = new StringBuilder(formattedString.Length);
foreach(char c in formattedString)
{
  int cAsI = (int)c;
  sb.Append(blocks[i / 128][i % 128]);
}
string ret = sb.ToString();
if(ret.IndexOf('\uFFFD') != -1)
    throw new ArgumentException("Unconvertable character");
formattedString = ret;

无法转换字符测试是在最后一次进行还是每次转换时进行的平衡取决于其发生的可能性。如果您能确信(由于数据知识)不会发生这种情况并且可以删除该检查,那当然更好 - 但您必须非常确定。优点在于,虽然我们使用了查找方法,但我们仅占用了384个字符的内存来保存查找(以及一些数组开销),而不是109,000个字符的内存。此中块的最佳大小因数据而异(即,您要进行哪些替换),但假设将有相互相同的块通常保持。现在,最后,如果您关心“天体平面”中的字符,它们在.NET内部使用的UTF-16中表示为代理对,或者如果您关心以特定方式替换某些多字符字符串呢?在这种情况下,您可能至少需要在switch中读取一个或多个字符(如果使用块方法处理大多数情况,则可以使用无法转换的情况来表示需要这样的操作)。在这种情况下,最好将其转换为US-ASCII,然后再使用System.Text.Encoding和EncoderFallback和EncoderFallbackBuffer的自定义实现进行处理。这意味着大多数转换(明显情况)将为您完成,而您的实现仅需处理特殊情况。

6
你可以维护一个查找表,将你的问题字符映射到替换字符。为了提高效率,你可以使用字符数组来避免大量的中间字符串操作,这是使用 string.Replace 会产生的结果。
例如:
var lookup = new Dictionary<char, char>
{
    { '`',  '-' },
    { 'இ', '-' },
    //next pair, etc, etc
};

var input = "blah இ blah ` blah";

var r;

var result = input.Select(c => lookup.TryGetValue(c, out r) ? r : c);

string output = new string(result.ToArray());

或者,如果您需要对非ASCII字符进行全面处理:

string output = new string(input.Select(c => c <= 127 ? c : '-').ToArray());

3

很遗憾,由于你正在进行一些特定的数据转换,因此你可能需要通过替换来完成这些操作。

尽管如此,你可以做出一些改进:

  1. 如果这是常见的,并且字符串很长,将其存储在StringBuilder而不是字符串中,可以允许值的原地替换,从而可能改善事情。
  2. 您可以将转换字符(包括“from”和“to”)存储在字典或其他结构中,并在简单的循环中执行这些操作。
  3. 您可以在运行时从配置文件中加载“from”和“to”字符,而不必硬编码每个转换操作。稍后,当需要更多此类操作时,您无需更改代码-可以通过配置完成。

1
如果它们都被替换为相同的字符串:
formattedString = string.Join("-", formattedString.Split('\u2013', '\u2014', '\u2015'));

或者

foreach (char c in "\u2013\u2014\u2015") 
    formattedString = formattedString.Replace(c, '-');

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