在构造使用该数据的XmlReader或XPathDocument之前,如何从基于XML的数据源中删除无效的十六进制字符?


76

在XmlReader中使用它之前,是否有任何简单/通用的方法来清理基于XML的数据源,以便我可以正常使用与XML上的十六进制字符限制不一致的XML数据?

注意:

  • 该解决方案需要处理使用UTF-8以外的字符编码的XML数据源,例如,通过在XML文档声明中指定字符编码。剥离无效的十六进制字符时不破坏源代码的字符编码已成为主要问题。
  • 删除无效的十六进制字符仅应删除十六进制编码的值,因为您经常会在数据中恰好包含一个与十六进制字符匹配的字符串的数据中找到href值。

背景:

我需要使用符合特定格式(例如Atom或RSS feed)的基于XML的数据源,但希望能够使用已发布的数据源(根据XML规范包含无效的十六进制字符)。

在.NET中,如果您有一个表示XML数据源的Stream,然后尝试使用XmlReader和/或XPathDocument对其进行解析,则会由于XML数据中包含无效的十六进制字符而引发异常。我当前解决此问题的尝试是将Stream解析为字符串,并使用正则表达式删除和/或替换无效的十六进制字符,但是我正在寻找一种性能更高的解决方案。

Answers:


76

可能并不完美(由于人们错过了此免责声明,因此添加了重点),但是在此情况下,我的工作如下。您可以调整以与流一起使用。

/// <summary>
/// Removes control characters and other non-UTF-8 characters
/// </summary>
/// <param name="inString">The string to process</param>
/// <returns>A string with no control characters or entities above 0x00FD</returns>
public static string RemoveTroublesomeCharacters(string inString)
{
    if (inString == null) return null;

    StringBuilder newString = new StringBuilder();
    char ch;

    for (int i = 0; i < inString.Length; i++)
    {

        ch = inString[i];
        // remove any characters outside the valid UTF-8 range as well as all control characters
        // except tabs and new lines
        //if ((ch < 0x00FD && ch > 0x001F) || ch == '\t' || ch == '\n' || ch == '\r')
        //if using .NET version prior to 4, use above logic
        if (XmlConvert.IsXmlChar(ch)) //this method is new in .NET 4
        {
            newString.Append(ch);
        }
    }
    return newString.ToString();

}

1
请在下面尝试dnewcome的解决方案。
尤金·卡兹

2
-1此答案具有误导性,因为它会删除XML中有效的字符,非控制字符以及有效的UTF-8字符。
Daniel Cassidy

2
如果您想使用更好的过滤器范围来更新答案,请随时进行。如我的回答所述,它可能并不完美,但可以满足我的需求。
尤金·卡兹

3
我将XmlConvert.IsXmlChar(ch)用于过滤器。
布拉德J

1
@BradJ,非常好。该方法似乎已在.NET 4中添加,因此切换代码仅在示例中使用它。谢谢!
尤金·卡兹

60

我喜欢尤金的白名单概念。我需要做与原始海报相似的操作,但是我需要支持所有Unicode字符,而不仅仅是0x00FD。XML规范是:

字符=#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]

在.NET中,Unicode字符的内部表示形式只有16位,因此我们不能明确地“允许” 0x10000-0x10FFFF。XML规范明确禁止出现从0xD800开始的替代代码点。但是,如果我们允许在白名单中使用这些替代代码点,则只要对字符串中的utf-16字符替代对产生了正确的utf-8编码,最后对字符串进行utf-8编码就可能产生有效的XML。 .NET字符串。不过,我还没有对此进行探讨,因此我选择了比较安全的选择,也不允许将替代产品列入我的白名单。

但是,尤金解决方案中的注释具有误导性,问题在于我们排除的字符在XML中无效……它们是完全有效的Unicode代码点。我们不会删除“非UTF-8字符”。我们将删除格式不正确的XML文档中可能不会出现的utf-8字符。

public static string XmlCharacterWhitelist( string in_string ) {
    if( in_string == null ) return null;

    StringBuilder sbOutput = new StringBuilder();
    char ch;

    for( int i = 0; i < in_string.Length; i++ ) {
        ch = in_string[i];
        if( ( ch >= 0x0020 && ch <= 0xD7FF ) || 
            ( ch >= 0xE000 && ch <= 0xFFFD ) ||
            ch == 0x0009 ||
            ch == 0x000A || 
            ch == 0x000D ) {
            sbOutput.Append( ch );
        }
    }
    return sbOutput.ToString();
}

它会附加,这会导致doc = XDocument.Load(@strXMLPath);产生异常
CODError 2014年

1
您好,您认为XmlConvert.IsXmlChar()会更准确吗?自您上次发表评论以来,尤金的答案已更改。谢谢
DaFi4

30

作为删除无效XML字符的方法,建议您使用XmlConvert.IsXmlChar方法。它是从.NET Framework 4开始添加的,并且也在Silverlight中提供。这是小样本:

void Main() {
    string content = "\v\f\0";
    Console.WriteLine(IsValidXmlString(content)); // False

    content = RemoveInvalidXmlChars(content);
    Console.WriteLine(IsValidXmlString(content)); // True
}

static string RemoveInvalidXmlChars(string text) {
    char[] validXmlChars = text.Where(ch => XmlConvert.IsXmlChar(ch)).ToArray();
    return new string(validXmlChars);
}

static bool IsValidXmlString(string text) {
    try {
        XmlConvert.VerifyXmlChars(text);
        return true;
    } catch {
        return false;
    }
}

13

此答案的解决方案的DRY实现(使用其他构造函数-随时在应用程序中使用所需的构造函数):

public class InvalidXmlCharacterReplacingStreamReader : StreamReader
{
    private readonly char _replacementCharacter;

    public InvalidXmlCharacterReplacingStreamReader(string fileName, char replacementCharacter) : base(fileName)
    {
        this._replacementCharacter = replacementCharacter;
    }

    public override int Peek()
    {
        int ch = base.Peek();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return this._replacementCharacter;
        }
        return ch;
    }

    public override int Read()
    {
        int ch = base.Read();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return this._replacementCharacter;
        }
        return ch;
    }

    public override int Read(char[] buffer, int index, int count)
    {
        int readCount = base.Read(buffer, index, count);
        for (int i = index; i < readCount + index; i++)
        {
            char ch = buffer[i];
            if (IsInvalidChar(ch))
            {
                buffer[i] = this._replacementCharacter;
            }
        }
        return readCount;
    }

    private static bool IsInvalidChar(int ch)
    {
        return (ch < 0x0020 || ch > 0xD7FF) &&
               (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D;
    }
}

也许最好在ch范围检查中使用XmlConvert.IsXmlChar()?你怎么看?
DaFi4

@montewhizdoh:IsXmlChar是.NET 4中的新增功能。如果您可以使用,请随时使用。此解决方案是.NET 2.0+。
Neolisk

1
我为自己实现了相同的方法,但是我继承自Stream并不是一个好主意,因为Stream.Read()使用字节数组而不是char进行操作,并且检查字符不是那么优雅。通过从StreamReader继承的解决方案更好,谢谢!
三月

1
+1,因为它允许读取非常大的XML文件(已成功测试100MB文件)。在滤除不良字符之前将所有内容加载到String中的解决方案因OutOfMemory异常而失败。
布拉德·奥斯特赖克

9

现代化dnewcombe的答案,您可以采用稍微简单一些的方法

public static string RemoveInvalidXmlChars(string input)
{
    var isValid = new Predicate<char>(value =>
        (value >= 0x0020 && value <= 0xD7FF) ||
        (value >= 0xE000 && value <= 0xFFFD) ||
        value == 0x0009 ||
        value == 0x000A ||
        value == 0x000D);

    return new string(Array.FindAll(input.ToCharArray(), isValid));
}

或者,与Linq

public static string RemoveInvalidXmlChars(string input)
{
    return new string(input.Where(value =>
        (value >= 0x0020 && value <= 0xD7FF) ||
        (value >= 0xE000 && value <= 0xFFFD) ||
        value == 0x0009 ||
        value == 0x000A ||
        value == 0x000D).ToArray());
}

我很想知道这些方法的性能如何比较,以及它们与使用的黑名单方法相比如何Buffer.BlockCopy


当较大的XML文件上的XML字符串出现Linq方法引发System.OutOfMemoryException时,我遇到了问题。
布拉德J

@BradJ大概在这种情况下传入的字符串很长?
Jodrell 2015年

@BradJ最终,某种形式的流转换会更好,您可以将其直接传递给XmlReader.Create而不是将整个文件加载到内存中的字符串中。
Jodrell 2015年

2
与dnewcombe的答案相比,它只是做了一次速度测试,并且您的两个解决方案的速度都快了3-4倍,其中Linq版本仅比非linq版本慢一点。我没想到会有这种区别。使用长字符串和带有秒表的100k迭代来确定计时。
2015年

@Seer我正在使用〜60k长度的字符流,并且此解决方案的运行速度比StringBuilder方法要慢一些,不确定我做了什么不同的事情。
2016年

5

这是dnewcome在自定义StreamReader中的答案。它只是包装了一个真正的流读取器,并在读取字符时替换了它们。

我只采用了几种方法来节省时间。我将其与XDocument.Load和文件流结合使用,并且仅调用了Read(char []缓冲区,int索引,int计数)方法,因此它可以这样工作。您可能需要实现其他方法才能使此方法适用于您的应用程序。我使用这种方法是因为它似乎比其他答案更有效。我也只实现了一个构造函数,显然,您可以实现所需的任何StreamReader构造函数,因为它只是一个传递。

我选择替换字符而不是删除字符,因为它极大地简化了解决方案。这样,文本的长度保持不变,因此无需跟踪单独的索引。

public class InvalidXmlCharacterReplacingStreamReader : TextReader
{
    private StreamReader implementingStreamReader;
    private char replacementCharacter;

    public InvalidXmlCharacterReplacingStreamReader(Stream stream, char replacementCharacter)
    {
        implementingStreamReader = new StreamReader(stream);
        this.replacementCharacter = replacementCharacter;
    }

    public override void Close()
    {
        implementingStreamReader.Close();
    }

    public override ObjRef CreateObjRef(Type requestedType)
    {
        return implementingStreamReader.CreateObjRef(requestedType);
    }

    public void Dispose()
    {
        implementingStreamReader.Dispose();
    }

    public override bool Equals(object obj)
    {
        return implementingStreamReader.Equals(obj);
    }

    public override int GetHashCode()
    {
        return implementingStreamReader.GetHashCode();
    }

    public override object InitializeLifetimeService()
    {
        return implementingStreamReader.InitializeLifetimeService();
    }

    public override int Peek()
    {
        int ch = implementingStreamReader.Peek();
        if (ch != -1)
        {
            if (
                (ch < 0x0020 || ch > 0xD7FF) &&
                (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D
                )
            {
                return replacementCharacter;
            }
        }
        return ch;
    }

    public override int Read()
    {
        int ch = implementingStreamReader.Read();
        if (ch != -1)
        {
            if (
                (ch < 0x0020 || ch > 0xD7FF) &&
                (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D
                )
            {
                return replacementCharacter;
            }
        }
        return ch;
    }

    public override int Read(char[] buffer, int index, int count)
    {
        int readCount = implementingStreamReader.Read(buffer, index, count);
        for (int i = index; i < readCount+index; i++)
        {
            char ch = buffer[i];
            if (
                (ch < 0x0020 || ch > 0xD7FF) &&
                (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D
                )
            {
                buffer[i] = replacementCharacter;
            }
        }
        return readCount;
    }

    public override Task<int> ReadAsync(char[] buffer, int index, int count)
    {
        throw new NotImplementedException();
    }

    public override int ReadBlock(char[] buffer, int index, int count)
    {
        throw new NotImplementedException();
    }

    public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
    {
        throw new NotImplementedException();
    }

    public override string ReadLine()
    {
        throw new NotImplementedException();
    }

    public override Task<string> ReadLineAsync()
    {
        throw new NotImplementedException();
    }

    public override string ReadToEnd()
    {
        throw new NotImplementedException();
    }

    public override Task<string> ReadToEndAsync()
    {
        throw new NotImplementedException();
    }

    public override string ToString()
    {
        return implementingStreamReader.ToString();
    }
}

最终,这是正确的主意,但您的实现可以是DRYer。
Jodrell 2015年

@Jodrell:在此处添加了DRY版本。
Neolisk 2015年

1
@Neolisk:谢谢!在发布之前,我可能应该已经清理过了:)
Ryan Adams

4

基于正则表达式的方法

public static string StripInvalidXmlCharacters(string str)
{
    var invalidXmlCharactersRegex = new Regex("[^\u0009\u000a\u000d\u0020-\ud7ff\ue000-\ufffd]|([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])");
    return invalidXmlCharactersRegex.Replace(str, "");

}

有关更多详细信息,请参见我的博客文章


1
这比我计算机上dnewcome的解决方案慢50倍。
2016年

2

上述解决方案似乎是为了在转换为XML之前删除无效字符。

使用此代码从XML字符串中删除无效的XML字符。例如。&x1A;

    public static string CleanInvalidXmlChars( string Xml, string XMLVersion )
    {
        string pattern = String.Empty;
        switch( XMLVersion )
        {
            case "1.0":
                pattern = @"&#x((10?|[2-F])FFF[EF]|FDD[0-9A-F]|7F|8[0-46-9A-F]9[0-9A-F]);";
                break;
            case "1.1":
                pattern = @"&#x((10?|[2-F])FFF[EF]|FDD[0-9A-F]|[19][0-9A-F]|7F|8[0-46-9A-F]|0?[1-8BCEF]);";
                break;
            default:
                throw new Exception( "Error: Invalid XML Version!" );
        }

        Regex regex = new Regex( pattern, RegexOptions.IgnoreCase );
        if( regex.IsMatch( Xml ) )
            Xml = regex.Replace( Xml, String.Empty );
        return Xml;
    }

http://balajiramesh.wordpress.com/2008/05/30/strip-illegal-xml-characters-based-on-w3c-standard/


1
-1这个答案不能解决所问的问题,并且在任何情况下都是错误和误导的,因为它只删除无效的XML字符实体引用,而不删除无效的XML字符。
Daniel Cassidy

1

上面Neolisk修改后的答案或原始答案。
更改:传递\ 0个字符,删除已完成,而不是替换。也使用XmlConvert.IsXmlChar(char)方法

    /// <summary>
    /// Replaces invalid Xml characters from input file, NOTE: if replacement character is \0, then invalid Xml character is removed, instead of 1-for-1 replacement
    /// </summary>
    public class InvalidXmlCharacterReplacingStreamReader : StreamReader
    {
        private readonly char _replacementCharacter;

        public InvalidXmlCharacterReplacingStreamReader(string fileName, char replacementCharacter)
            : base(fileName)
        {
            _replacementCharacter = replacementCharacter;
        }

        public override int Peek()
        {
            int ch = base.Peek();
            if (ch != -1 && IsInvalidChar(ch))
            {
                if ('\0' == _replacementCharacter)
                    return Peek(); // peek at the next one

                return _replacementCharacter;
            }
            return ch;
        }

        public override int Read()
        {
            int ch = base.Read();
            if (ch != -1 && IsInvalidChar(ch))
            {
                if ('\0' == _replacementCharacter)
                    return Read(); // read next one

                return _replacementCharacter;
            }
            return ch;
        }

        public override int Read(char[] buffer, int index, int count)
        {
            int readCount= 0, ch;

            for (int i = 0; i < count && (ch = Read()) != -1; i++)
            {
                readCount++;
                buffer[index + i] = (char)ch;
            }

            return readCount;
        }


        private static bool IsInvalidChar(int ch)
        {
            return !XmlConvert.IsXmlChar((char)ch);
        }
    }

1

我创建了一个稍微更新版本@ Neolisk的答案,它支持的*Async功能和使用.NET 4.0XmlConvert.IsXmlChar的功能。

public class InvalidXmlCharacterReplacingStreamReader : StreamReader
{
    private readonly char _replacementCharacter;

    public InvalidXmlCharacterReplacingStreamReader(string fileName, char replacementCharacter) : base(fileName)
    {
        _replacementCharacter = replacementCharacter;
    }

    public InvalidXmlCharacterReplacingStreamReader(Stream stream, char replacementCharacter) : base(stream)
    {
        _replacementCharacter = replacementCharacter;
    }

    public override int Peek()
    {
        var ch = base.Peek();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return _replacementCharacter;
        }
        return ch;
    }

    public override int Read()
    {
        var ch = base.Read();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return _replacementCharacter;
        }
        return ch;
    }

    public override int Read(char[] buffer, int index, int count)
    {
        var readCount = base.Read(buffer, index, count);
        ReplaceInBuffer(buffer, index, readCount);
        return readCount;
    }

    public override async Task<int> ReadAsync(char[] buffer, int index, int count)
    {
        var readCount = await base.ReadAsync(buffer, index, count).ConfigureAwait(false);
        ReplaceInBuffer(buffer, index, readCount);
        return readCount;
    }

    private void ReplaceInBuffer(char[] buffer, int index, int readCount)
    {
        for (var i = index; i < readCount + index; i++)
        {
            var ch = buffer[i];
            if (IsInvalidChar(ch))
            {
                buffer[i] = _replacementCharacter;
            }
        }
    }

    private static bool IsInvalidChar(int ch)
    {
        return IsInvalidChar((char)ch);
    }

    private static bool IsInvalidChar(char ch)
    {
        return !XmlConvert.IsXmlChar(ch);
    }
}

0

使用此功能删除无效的xml字符。

public static string CleanInvalidXmlChars(string text)   
{   
       string re = @"[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD\x10000-x10FFFF]";   
       return Regex.Replace(text, re, "");   
} 

-1
private static String removeNonUtf8CompliantCharacters( final String inString ) {
    if (null == inString ) return null;
    byte[] byteArr = inString.getBytes();
    for ( int i=0; i < byteArr.length; i++ ) {
        byte ch= byteArr[i]; 
        // remove any characters outside the valid UTF-8 range as well as all control characters
        // except tabs and new lines
        if ( !( (ch > 31 && ch < 253 ) || ch == '\t' || ch == '\n' || ch == '\r') ) {
            byteArr[i]=' ';
        }
    }
    return new String( byteArr );
}

1
-1这个答案是误导和错误的,因为它删除了同时适用于Unicode和XML的字符。
Daniel Cassidy

-1

您可以通过以下方式传递非UTF字符:

string sFinalString  = "";
string hex = "";
foreach (char ch in UTFCHAR)
{
    int tmp = ch;
   if ((ch < 0x00FD && ch > 0x001F) || ch == '\t' || ch == '\n' || ch == '\r')
    {
    sFinalString  += ch;
    }
    else
    {  
      sFinalString  += "&#" + tmp+";";
    }
}

1
-1这个答案是错误的,因为它生成无效的XML字符实体引用(例如&#1;,不是有效的XML字符实体引用)。这也是一种误导,因为它会删除同时适用于Unicode和XML的字符。
Daniel Cassidy

没错,但是上述解决方案是,如果您想在xml文件中传递无效的xml,那么它将起作用,或者您不能在xml文档中传递无效的xml字符
Murari Kumar 2012年

无论做什么,您都不能在XML文档中传递无效的XML字符。例如,在U+0001 START OF HEADING格式正确的XML文档中不允许使用该字符,即使您尝试将其转义为&#1;,在格式正确的XML文档中仍不允许使用该字符。
Daniel Cassidy 2012年

-5

试试这个PHP!

$goodUTF8 = iconv("utf-8", "utf-8//IGNORE", $badUTF8);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.