.NET能否将Unicode转换为ASCII以去除“智能引号”等字符?

18

我们的一些用户使用不支持Unicode的电子邮件客户端,即使编码等在邮件头中正确设置。

我想要“标准化”他们收到的内容。我们最大的问题是用户从Microsoft Word复制粘贴内容到我们的Web应用程序中,然后通过电子邮件转发该内容 - 包括分数、智能引号和Word为您插入的所有其他扩展Unicode字符。

我猜这方面可能没有绝对的解决方案,但在我坐下来开始写大型查找表之前,有没有一些内置方法可以让我开始?

基本上涉及三个阶段。

首先,从否则正常的字母中去除重音符号 - 解决方案在这里

This paragraph contains “smart quotes” and áccénts and ½ of the problem is fractions

前往

This paragraph contains “smart quotes” and accents and ½ of the problem is fractions

其次,将单个Unicode字符替换为它们的ASCII等效字符,得到:

This paragraph contains "smart quotes" and accents and ½ of the problem is fractions
这是我希望在实现自己的解决方案之前能有一个解决方法的部分。最后,将特定字符替换为适当的ASCII序列—— ½ 替换为 1/2 等等—— 我很确定这种操作不受任何Unicode魔法本地支持,但是也许有人写了一个合适的查找表可以重复使用。 有什么想法吗?

你如何检测目标邮件客户端无法理解utf-8编码? - CodesInChaos
@CodeInChaos - 我们通过等待用户打电话投诉我们的软件向他们发送垃圾邮件来检测不兼容Unicode的邮件客户端。这种情况经常发生。我们无法重现故障;它似乎影响Outlook 2003,但我们的测试虚拟机使用Outlook 2003可以正常显示Unicode。 - Dylan Beattie
@CodeInChaos - 关于 Ä --> Ae - 没错。此外,还可以将Unicode省略号替换为“...”,°替换为“度”,©替换为(C)以及许多其他替换,尽可能保留原始Unicode文本的含义。 - Dylan Beattie
如果您将Unicode引号替换为",请注意分隔符冲突攻击。 - dan04
@CodeInChaos - 将 Ä 替换为 Ae 对于德语来说是有意义的,但对于其他使用 Ä 字符的语言来说则不一定如此。 - dan04
显示剩余2条评论
4个回答

25

非常感谢你们提供的有用答案。我意识到实际问题不是“如何将任何Unicode字符转换为其ASCII回退”,而是“如何将我的客户抱怨的Unicode字符转换为它们的ASCII回退”?

换句话说,我们不需要通用解决方案;我们需要一种在英语客户从Word和其他网站复制英语内容粘贴到我们的应用程序时99%正常工作的解决方案。为此,我分析了八年来通过我们系统发送的消息,寻找不能用ASCII编码表示的字符,使用以下测试:

///<summary>Determine whether the supplied character is 
///using ASCII encoding.</summary>
bool IsAscii(char inputChar) {
    var ascii = new ASCIIEncoding();
    var asciiChar = (char)(ascii.GetBytes(inputChar.ToString())[0]);
    return(asciiChar == inputChar);
}

我已经对无法表示的字符集进行了处理,并手动分配了适当的替换字符串。整个过程被打包在一个扩展方法中,因此您可以调用myString.Asciify()将您的字符串转换为合理的ASCII编码近似值。

public static class StringExtensions {
    private static readonly Dictionary<char, string> Replacements = new Dictionary<char, string>();
    /// <summary>Returns the specified string with characters not representable in ASCII codepage 437 converted to a suitable representative equivalent.  Yes, this is lossy.</summary>
    /// <param name="s">A string.</param>
    /// <returns>The supplied string, with smart quotes, fractions, accents and punctuation marks 'normalized' to ASCII equivalents.</returns>
    /// <remarks>This method is lossy. It's a bit of a hack that we use to get clean ASCII text for sending to downlevel e-mail clients.</remarks>
    public static string Asciify(this string s) {
        return (String.Join(String.Empty, s.Select(c => Asciify(c)).ToArray()));
    }

    private static string Asciify(char x) {
        return Replacements.ContainsKey(x) ? (Replacements[x]) : (x.ToString());
    }

    static StringExtensions() {
        Replacements['’'] = "'"; // 75151 occurrences
        Replacements['–'] = "-"; // 23018 occurrences
        Replacements['‘'] = "'"; // 9783 occurrences
        Replacements['”'] = "\""; // 6938 occurrences
        Replacements['“'] = "\""; // 6165 occurrences
        Replacements['…'] = "..."; // 5547 occurrences
        Replacements['£'] = "GBP"; // 3993 occurrences
        Replacements['•'] = "*"; // 2371 occurrences
        Replacements[' '] = " "; // 1529 occurrences
        Replacements['é'] = "e"; // 878 occurrences
        Replacements['ï'] = "i"; // 328 occurrences
        Replacements['´'] = "'"; // 226 occurrences
        Replacements['—'] = "-"; // 133 occurrences
        Replacements['·'] = "*"; // 132 occurrences
        Replacements['„'] = "\""; // 102 occurrences
        Replacements['€'] = "EUR"; // 95 occurrences
        Replacements['®'] = "(R)"; // 91 occurrences
        Replacements['¹'] = "(1)"; // 80 occurrences
        Replacements['«'] = "\""; // 79 occurrences
        Replacements['è'] = "e"; // 79 occurrences
        Replacements['á'] = "a"; // 55 occurrences
        Replacements['™'] = "TM"; // 54 occurrences
        Replacements['»'] = "\""; // 52 occurrences
        Replacements['ç'] = "c"; // 52 occurrences
        Replacements['½'] = "1/2"; // 48 occurrences
        Replacements['­'] = "-"; // 39 occurrences
        Replacements['°'] = " degrees "; // 33 occurrences
        Replacements['ä'] = "a"; // 33 occurrences
        Replacements['É'] = "E"; // 31 occurrences
        Replacements['‚'] = ","; // 31 occurrences
        Replacements['ü'] = "u"; // 30 occurrences
        Replacements['í'] = "i"; // 28 occurrences
        Replacements['ë'] = "e"; // 26 occurrences
        Replacements['ö'] = "o"; // 19 occurrences
        Replacements['à'] = "a"; // 19 occurrences
        Replacements['¬'] = " "; // 17 occurrences
        Replacements['ó'] = "o"; // 15 occurrences
        Replacements['â'] = "a"; // 13 occurrences
        Replacements['ñ'] = "n"; // 13 occurrences
        Replacements['ô'] = "o"; // 10 occurrences
        Replacements['¨'] = ""; // 10 occurrences
        Replacements['å'] = "a"; // 8 occurrences
        Replacements['ã'] = "a"; // 8 occurrences
        Replacements['ˆ'] = ""; // 8 occurrences
        Replacements['©'] = "(c)"; // 6 occurrences
        Replacements['Ä'] = "A"; // 6 occurrences
        Replacements['Ï'] = "I"; // 5 occurrences
        Replacements['ò'] = "o"; // 5 occurrences
        Replacements['ê'] = "e"; // 5 occurrences
        Replacements['î'] = "i"; // 5 occurrences
        Replacements['Ü'] = "U"; // 5 occurrences
        Replacements['Á'] = "A"; // 5 occurrences
        Replacements['ß'] = "ss"; // 4 occurrences
        Replacements['¾'] = "3/4"; // 4 occurrences
        Replacements['È'] = "E"; // 4 occurrences
        Replacements['¼'] = "1/4"; // 3 occurrences
        Replacements['†'] = "+"; // 3 occurrences
        Replacements['³'] = "'"; // 3 occurrences
        Replacements['²'] = "'"; // 3 occurrences
        Replacements['Ø'] = "O"; // 2 occurrences
        Replacements['¸'] = ","; // 2 occurrences
        Replacements['Ë'] = "E"; // 2 occurrences
        Replacements['ú'] = "u"; // 2 occurrences
        Replacements['Ö'] = "O"; // 2 occurrences
        Replacements['û'] = "u"; // 2 occurrences
        Replacements['Ú'] = "U"; // 2 occurrences
        Replacements['Œ'] = "Oe"; // 2 occurrences
        Replacements['º'] = "?"; // 1 occurrences
        Replacements['‰'] = "0/00"; // 1 occurrences
        Replacements['Å'] = "A"; // 1 occurrences
        Replacements['ø'] = "o"; // 1 occurrences
        Replacements['˜'] = "~"; // 1 occurrences
        Replacements['æ'] = "ae"; // 1 occurrences
        Replacements['ù'] = "u"; // 1 occurrences
        Replacements['‹'] = "<"; // 1 occurrences
        Replacements['±'] = "+/-"; // 1 occurrences
    }
}

请注意,其中有一些相当奇怪的后备方案 - 就像这个:

Replacements['³'] = "'"; // 3 occurrences
Replacements['²'] = "'"; // 3 occurrences

这是因为我们的一位用户使用了一个程序,将开/闭智能引号转换为²和³(例如:他说“²你好³”),而且从未有人使用它们来表示指数运算,所以对于我们来说,这可能非常有效,但您的情况可能会有所不同。


6

我自己遇到了一些问题,当我们使用最初在Word中构建的字符串列表时。我发现使用一个简单的"String".replace(current char/string, new char/string)命令就可以完美地解决问题。我用于智能引号的确切代码,或者更确切地说是左“”,右“”,左‘’和右‘’如下:

StringName = StringName.Replace(ChrW(8216), "'")     ' Replaces any left ' with a normal '
StringName = StringName.Replace(ChrW(8217), "'")     ' Replaces any right ' with a normal '
StringName = StringName.Replace(ChrW(8220), """")    ' Replace any left " with a normal "
StringName = StringName.Replace(ChrW(8221), """")    ' Replace any right " with a normal "

我希望这能帮助那些仍然遇到这个问题的人!

1
有没有一些内置的方法可以让我开始?我建议首先尝试使用字符串规范化方法Normalize将文本转换为NFKDnormalization form。虽然在你提供的问题的答案中提到了这个建议,但我建议使用NFKD而不是NFD,因为NFKD会删除不需要的排版区别(例如,NBSP → 空格,或ℂ → C)。
你也可以通过Unicode category进行通用替换。例如,Pd可以被替换为-,Nd可以被替换为相应的0-9数字,Mn可以被替换为空字符串(以去除重音符号)。
但如果有人编写了一个适合你的查找表,你也可以重复使用它。
你可以尝试使用Unidecode程序的数据,或者CLDR编辑:这里有一个巨大的替换图表

链接的替换图表已移动:http://unicode.org/cldr/charts/32/supplemental/character_fallback_substitutions.html - Jeremy Murray

-1

你永远不应该尝试将Unicode转换为ASCII,因为这样做会带来更多问题而不是解决问题。

这就像试图将1,114,112个代码点(Unicode 6.0)适配到只有128个字符的范围内。

你认为你会成功吗?

顺便说一下,Unicode中有很多引用,不仅仅是你提到的那些。如果你仍然想要进行转换,请记住转换将取决于区域设置。

请查看ICU - 它包含了最完整的Unicode转换例程。


4
我们的市场在地理和文化上都比较特定,我们的客户并非故意使用Unicode。每周我们会接到10-15通关于“电子邮件中出现乱码”的电话,而从未有人抱怨无法发送阿拉伯语或希伯来语的电子邮件。所以,这是一个很愚蠢的问题,但它确实存在 :) - Dylan Beattie
@Dylan 没有什么问题是不好的 :) 顺便问一下,你能否更具体地说明为什么不能使用Unicode?我真的很想知道为什么以及是否有任何解决方法。另外,我更新了我的答案,包括一个检查的地方。 - sorin
ICU不仅适用于C++,而且不适用于.NET。 - Uwe Keim
没有针对.NET的这样的声明,如http://blogs.msdn.com/b/michkap/archive/2008/12/18/9234330.aspx中所述 - 但是,如果您编写服务器应用程序,则可以添加C组件并调用它们。 - sorin

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