是否有更优雅的方式将Unicode更改为Ascii?

时间:2011-01-21 16:24:46

标签: c# .net unicode ascii

我看到了很多问题,你有一些模糊的unicode字符,有点像某个ascii字符,需要在运行时因任何原因进行转换。

在这种情况下,我正在尝试导出到csv。已经对dash,emdash,endash和hbar使用了一个讨厌的修复程序,我刚收到了一个新的'''请求。除了另一个讨厌的修复方法还有另一种更好的方法吗?

这是我现在所拥有的......

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

任何想法?

4 个答案:

答案 0 :(得分:7)

这是一个相当不优雅的问题,所以没有任何方法会真正优雅。

尽管如此,我们当然可以改进。哪种方法最有效将取决于需要进行的更改的数量(以及要更改的字符串的大小,尽管通常最好假设这可能是或者可能非常大)。

在一个替换字符处,您目前使用的方法 - 使用.Replace是优越的,但我会将char.ConvertFromUtf32(8211)替换为"\u2013"。对性能的影响可以忽略不计,但它更具可读性,因为在U + 2013中引用十六进制的字符比在十进制表示法中更常见(当然char.ConvertFromUtf32(0x2013)在那里具有相同的优势,但没有优势只是使用char表示法)。 (也可以将'–'直接放入代码中 - 在某些情况下更具可读性,但在这种情况下,它看起来与 - , - 或 - 对读者来说差不多。

我还会用更快的字符替换替换字符串替换(至少在这种情况下,使用单个字符替换单个字符)。

将这种方法应用于您的代码:

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

即使替换次数少于3,这可能比在一次传递中完成所有此类替换的效率低(我不打算进行测试以查找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但是分支数量的减少很小,如果你寻找的大多数字符在数字上彼此不相近,那就无关紧要了。

使用各种变体(例如,如果formatString将在某个时刻输出到流,最好在获得每个最终字符时这样做,而不是再次缓冲)。

请注意,此方法不会处理搜索中的多字符串字符串,但可以在输出中使用字符串,例如我们可以包括:

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来说是不实用的,它在0到1114111的范围内已经超过了109,000个字符。但是,你关心的字符不仅可能比那个小得多(如果你真的这样做了)关心那么多案例,你需要上面给出的方法),但也需要相对有限的块。

如果你不特别关心任何代理人,我们也会考虑(我们将在稍后介绍)。好吧,大多数你不关心的角色,所以,让我们考虑一下:

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中表示为代理对,或者如果你关心更换一些多字符串特别的方式?

在这种情况下,您可能必须至少在开关中读取一个或更多字符(如果在大多数情况下使用块方法,您可以使用不可转换的情况来指示此类工作是必需的)。在这种情况下,使用System.Text.Encoding以及EncoderFallbackEncoderFallbackBuffer的自定义实现进行转换,然后从US-ASCII转换回来然后处理它。这意味着大部分转换(明显的情况)都将为您完成,而您的实现只能处理特殊情况。

答案 1 :(得分:5)

您可以维护一个查找表,将问题字符映射到替换字符。为了提高效率,您可以处理字符数组,以防止使用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());

答案 2 :(得分:3)

不幸的是,鉴于您在数据中进行了大量特定转换,您可能需要通过替换来完成这些转换。

话虽如此,你可以做一些改进。

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

答案 3 :(得分:1)

如果它们都被相同的字符串替换:

formattedString = string.Join("-", formattedString.Split('\u2013', '\u2014', '\u2015'));

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