从字符串中删除隐藏字符

49
我的问题:
我有一个使用.NET编写的应用程序,通过电子邮件发送新闻通讯。当在Outlook中查看新闻时,Outlook会显示一个问号,代替无法识别的隐藏字符。这些隐藏字符来自将构成新闻通讯的HTML复制并粘贴到表单中提交的终端用户。C#的trim()函数将从字符串开头或结尾处移除这些隐藏字符。当在Gmail中查看新闻时,Gmail会很好地忽略它们。将这些隐藏字符粘贴到Word文档中并打开“显示段落标记和隐藏符号”选项后,这些符号会显示为一个矩形内部的另一个矩形。此外,构成新闻通讯的文本可以是任何语言,因此必须接受Unicode字符。我已经尝试循环遍历字符串以检测字符,但循环无法识别它并跳过它。要求终端用户先将HTML粘贴到记事本中再提交是行不通的。
我的问题:
如何使用C#检测和消除这些隐藏字符?

在这里放一个例子。 - Soner Gönül
示例无效值会很好。我猜测这是ASCII文本中的Unicode字符串,但这只是猜测。 - Jake H
正则表达式,只允许字母和数字。 - Scott Selby
2
可能是在.NET中如何检测非打印字符?的重复问题。 - IAbstract
我不知道隐藏字符是什么。它只在Outlook或Word中显示一次。如果我在SharePoint列表(存储位置)中查看文本,它就会被隐藏。 - bradley4
已经有一段时间了,但这个问题还没有得到解答。如何在发送代码中包含HTML内容?如果你是从文件中读取的,请检查文件编码。如果你使用带有签名的UTF-8(编辑器名称略有不同),这可能会导致邮件开头出现奇怪的字符。 - SimSimY
10个回答

102
您可以使用以下代码从输入字符串中删除所有控制字符:
string input; // this is your input string
string output = new string(input.Where(c => !char.IsControl(c)).ToArray());

这里是关于IsControl()方法的文档

或者如果你只想保留字母和数字,你也可以使用IsLetterIsDigit函数:

string output = new string(input.Where(c => char.IsLetter(c) || char.IsDigit(c)).ToArray());

HtmlEncode/Decode 不会删除任何字符,不确定您建议如何使用它。 - Alexei Levenkov
@AlexeiLevenkov 是的,抱歉,我看错了问题... 我会相应地更新我的答案。 - Yannick Blondeau
6
我不知道为什么,但是Char.IsControl对于从左到右的标记(Left-to-right mark)会返回false。 - Igor Meszaros
2
@YannickBlondeau,这也会删除标点符号和特殊字符“£$%^”等,因此我认为最好的解决方案是两者结合,或者是我添加的答案。 - Igor Meszaros
3
LRM是一个“格式”字符,但幸运的是C#有一个GetUnicodeCategory(char c)方法,可以识别任何字符的类别。string clean = new string(e.Value.Where(c => char.GetUnicodeCategory(c) != UnicodeCategory.Format).ToArray());这段代码可以很好地去除LRM。 - Hinrich
显示剩余4条评论

29

我通常使用这个正则表达式来替换所有不可打印的字符。

顺便说一下,大多数人认为制表符、换行符和回车符是不可打印的字符,但对我而言它们不是。

所以这就是表达式:

string output = Regex.Replace(input, @"[^\u0009\u000A\u000D\u0020-\u007E]", "*");
  • ^的意思是如果它是以下任何一个:
  • \u0009是制表符
  • \u000A是换行符
  • \u000D是回车符
  • \u0020-\u007E表示从空格到~之间的所有内容 - 也就是说,ASCII中的所有内容。

如果您想进行更改,请参见ASCII表。请记住,它将剥离每个非 ASCII 字符。

要测试上述内容,您可以按照以下方式创建字符串:

    string input = string.Empty;

    for (int i = 0; i < 255; i++)
    {
        input += (char)(i);
    }

3
我认为第一个 ^ 是用来反转集合的,而其他的 ^ 不应该存在(会从输出中排除 ^)。 - Matt

9
我最有效的做法是:

string result = new string(value.Where(c =>  char.IsLetterOrDigit(c) || (c >= ' ' && c <= byte.MaxValue)).ToArray());

我需要确保字符是字母或数字,这样我就不会忽略任何非英文字母。如果不是字母,我会检查它是否为ASCII字符,且大于或等于空格,以确保忽略一些控制字符,从而避免忽略标点符号。

有人建议使用IsControl检查字符是否可打印,但这会忽略例如从左到右标记之类的字符。


7
new string(input.Where(c => !char.IsControl(c)).ToArray());

IsControl函数无法检测到一些控制字符,如左至右标记(LRM)(该字符通常在复制粘贴字符串时隐藏)。如果您确定字符串仅包含数字和字母,则可以使用IsLetterOrDigit函数。

new string(input.Where(c => char.IsLetterOrDigit(c)).ToArray())

如果您的字符串包含特殊字符,则:
new string(input.Where(c => c < 128).ToArray())

2
很遗憾,从我的单元测试来看,最后一个建议( new string(input.Where(c => c < 128).ToArray()) )也会剥离掉重音字符。例如,“Siñalizacíon” 将变成 “Sializacon”。 - Jan V.

4
你可以这样做:
var hChars = new char[] {...};
var result = new string(yourString.Where(c => !hChars.Contains(c)).ToArray());

2

简短回答

使用这个正则表达式...

\P{Cc}\P{Cn}\P{Cs}

像这样...

var regex = new Regex(@"![\P{Cc}\P{Cn}\P{Cs}]");

简短解释

  • \P{Cc} : 不匹配控制字符。
  • \P{Cn} : 不匹配未分配的字符。
  • \P{Cs} : 不匹配UTF-8无效的字符。

演示示例

在这个演示中,我使用这个正则表达式来搜索字符串"Hello, World!"。末尾的奇怪字符是(char)4 - 这是结束传输的字符。

using System;
using System.Text.RegularExpressions;

public class Test {
    public static void Main() {
        var regex = new Regex(@"![\P{Cc}\P{Cn}\P{Cs}]");
        var matches = regex.Matches("Hello, World!" + (char)4);
        Console.WriteLine("Results: " + matches.Count);
        foreach (Match match in matches) {
            Console.WriteLine("Result: " + match);
        }
    }
}

在IDEOne.com上查看完整演示

以上代码的输出结果:

Results: 1
Result: !

替代方案

  • \P{C}:仅匹配可见字符。不匹配任何不可见字符。
  • \P{Cc}:仅匹配非控制字符。不匹配任何控制字符。
  • \P{Cc}\P{Cn}:仅匹配已分配的非控制字符。不匹配任何控制或未分配字符。
  • \P{Cc}\P{Cn}\P{Cs}:仅匹配已分配且UTF-8有效的非控制字符。不匹配任何控制、未分配或UTF-8无效字符。
  • \P{Cc}\P{Cn}\P{Cs}\P{Cf}:仅匹配已分配且UTF-8有效的非控制、非格式化字符。不匹配任何控制、未分配、格式化或UTF-8无效字符。

来源和解释

查看可用于在正则表达式中测试的Unicode字符属性。您应该能够在Microsoft .NETJavaScriptPythonJavaPHPRubyPerlGolang,甚至Adobe中使用这些正则表达式。了解Unicode字符类是非常可转移的知识,因此我建议您使用它!


1
如果您需要速度,请创建一个类似于以下的静态方法:
private static string RemoveControlCharacters(ReadOnly<char> input)
{
    Span<char> output = stackalloc char[input.Length];
    int j = 0;

    foreach (char c in input)
    {
        if (!char.IsControl(c))
        {
            output[j++] = c;
        }
    }

    return new string(output.Slice(0, j));
}

它使用stackalloc在堆栈上分配输出字符串的内存,这比堆分配更快。

0
string output = new string(input.Where(c => !char.IsControl(c)).ToArray());

这肯定会解决问题。我在一个字符串中有一个非可打印的替代字符(ASCII 26),导致我的应用程序崩溃,而这行代码删除了这些字符。


2
这与得票最高且被接受的答案完全相同。 - Lance U. Matthews

0

我在AWS S3 SDK中遇到了一个错误:“目标资源路径[name - ‎3.‎30.‎2022 - ‎15‎.‎27.‎00.pdf]具有双向字符,这些字符不受System.Uri支持,因此无法由.NET SDK处理”

我的文件名包含Unicode字符'LEFT-TO-RIGHT MARK'(U+200E),而这些字符在html或Notepad++中是不可见的。当文本被粘贴到Visual Studio 2019编辑器中时,Unicode文本变得可见,我成功解决了这个问题。

U+200E Left to Right Mark

问题通过使用以下脚本替换文件名中的所有控制和其他不可打印字符来解决。
var input = Regex.Replace(s, @"\p{C}+", string.Empty);

引用来源:https://dev59.com/31kR5IYBdhLWcg3w9Ryt#40568888


0
我使用了这个快速而简单的一行代码来清理一些输入,这些输入是由于破损的Windows 10计算器应用程序留下的LTR / RTL标记。它可能远非完美,但足以进行快速修复:
string cleaned = new string(input.Where(c => !char.IsControl(c) && (char.IsLetterOrDigit(c) || char.IsPunctuation(c) || char.IsSeparator(c) || char.IsSymbol(c) || char.IsWhiteSpace(c))).ToArray());

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