检测XML的更好方法是什么?

25

目前,我拥有以下C#代码来从文本中提取值。如果是XML格式,我想要其中的值 - 否则,如果不是XML格式,它可以返回文本本身。

String data = "..."
try
{
    return XElement.Parse(data).Value;
}
catch (System.Xml.XmlException)
{
    return data;
}

我知道在C#中,异常处理是比较耗费性能的。因此我想知道是否有更好的方法来确定我所处理的文本是否是XML格式的?

我考虑过使用正则表达式来测试,但我并不认为那是一种更便宜的替代方案。请注意,我正在寻求一种更少消耗资源的解决方案。


2
异常是免费的,我经常将它们抛弃。除非你能证明有问题,否则上面的代码没有任何问题,这只是一种代码异味。有人测试过下面的方法是否真的更快,而且速度是否必要吗? - JustEngland
@JustEngland 实际上,在大多数 C++ 实现中,异常处理是很慢的。但是 C# 可能会有所不同。我没有使用过 C#,因此无法评论在 C# 中异常处理的性能。在 C++ 中,我可以每秒循环 4 亿次迭代,但如果每次迭代都抛出异常,则每秒迭代次数不到一百万次。 - juhist
哇,这是一个好帖子,我甚至不再编写C#代码了 :)今天我能给出的最好建议是,比较一下框架中的所有不同解析器。你也可以通过一些基本检查来作弊。还有第三方XML解析器可用,性能更好。 - JustEngland
即使性能不是问题,最好在非异常情况下避免抛出异常。我们的工具是为查找异常而构建的,当调试其他内容时,它们可能会妨碍我们。我认为这是一个“令人烦恼的异常”(vexing exception)的例子,尽管比Eric提供的int.Parse示例要轻微一些。 - eisenpony
14个回答

19
你可以进行初步检查,看看是否有一个 <,因为所有的 XML 都必须以这个为开头,而大多数非 XML 不会以这个为开头。
// Has to have length to be XML
if (!string.IsNullOrEmpty(data))
{
    // If it starts with a < after trimming then it probably is XML
    // Need to do an empty check again in case the string is all white space.
    var trimmedData = data.TrimStart();
    if (string.IsNullOrEmpty(trimmedData))
    {
       return data;
    }

    if (trimmedData[0] == '<')
    {
        try
        {
            return XElement.Parse(data).Value;
        }
        catch (System.Xml.XmlException)
        {
            return data;
        }
    }
}
else
{
    return data;
}

我最初使用了正则表达式,但 Trim()[0] 与该正则表达式的作用完全相同。


3
赞同这个概念,因为它可以筛选出99%的例外情况,但我认为这里并不需要正则表达式。使用StartsWith或IndexOf会更好且更快。 - annakata
1
Um,StartsWith不起作用,因为允许空格,而IndexOf需要知道索引之前的所有内容都是空格。虽然IndexOf 可以使用,我会修改我的答案。 - Colin Burnett

7
下面给出的代码将匹配所有以下XML格式:
<text />                             
<text/>                              
<text   />                           
<text>xml data1</text>               
<text attr='2'>data2</text>");
<text attr='2' attr='4' >data3 </text>
<text attr>data4</text>              
<text attr1 attr2>data5</text>

以下是代码:

public class XmlExpresssion
{
    // EXPLANATION OF EXPRESSION
    // <        :   \<{1}
    // text     :   (?<xmlTag>\w+)  : xmlTag is a backreference so that the start and end tags match
    // >        :   >{1}
    // xml data :   (?<data>.*)     : data is a backreference used for the regex to return the element data      
    // </       :   <{1}/{1}
    // text     :   \k<xmlTag>
    // >        :   >{1}
    // (\w|\W)* :   Matches attributes if any

    // Sample match and pattern egs
    // Just to show how I incrementally made the patterns so that the final pattern is well-understood
    // <text>data</text>
    // @"^\<{1}(?<xmlTag>\w+)\>{1}.*\<{1}/{1}\k<xmlTag>\>{1}$";

    //<text />
    // @"^\<{1}(?<xmlTag>\w+)\s*/{1}\>{1}$";

    //<text>data</text> or <text />
    // @"^\<{1}(?<xmlTag>\w+)((\>{1}.*\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

    //<text>data</text> or <text /> or <text attr='2'>xml data</text> or <text attr='2' attr2 >data</text>
    // @"^\<{1}(?<xmlTag>\w+)(((\w|\W)*\>{1}(?<data>.*)\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

    private const string XML_PATTERN = @"^\<{1}(?<xmlTag>\w+)(((\w|\W)*\>{1}(?<data>.*)\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

    // Checks if the string is in xml format
    private static bool IsXml(string value)
    {
        return Regex.IsMatch(value, XML_PATTERN);
    }

    /// <summary>
    /// Assigns the element value to result if the string is xml
    /// </summary>
    /// <returns>true if success, false otherwise</returns>
    public static bool TryParse(string s, out string result)
    {
        if (XmlExpresssion.IsXml(s))
        {
            Regex r = new Regex(XML_PATTERN, RegexOptions.Compiled);
            result = r.Match(s).Result("${data}");
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }

}

调用代码:

if (!XmlExpresssion.TryParse(s, out result)) 
    result = s;
Console.WriteLine(result);

我对此有些怀疑,因为XML不是一种正则语言,因此您无法使用正则表达式解析XML:https://dev59.com/questions/rGw15IYBdhLWcg3wO5IH ...但是,对于识别XML,如果您不进行完全解析,也许它可以起作用。 - juhist
如果字符串以XML声明开头,即 <?xml version="1.0" encoding="UTF-8" ?>,该怎么办? - Drew Noakes

5
更新:(原帖如下)Colin有一个绝妙的想法,即将正则表达式实例化移出调用,以便只创建一次。以下是新程序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace ConsoleApplication3
{
    delegate String xmltestFunc(String data);

    class Program
    {
        static readonly int iterations = 1000000;

        private static void benchmark(xmltestFunc func, String data, String expectedResult)
        {
            if (!func(data).Equals(expectedResult))
            {
                Console.WriteLine(data + ": fail");
                return;
            }
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < iterations; ++i)
                func(data);
            sw.Stop();
            Console.WriteLine(data + ": " + (float)((float)sw.ElapsedMilliseconds / 1000));
        }

        static void Main(string[] args)
        {
            benchmark(xmltest1, "<tag>base</tag>", "base");
            benchmark(xmltest1, " <tag>base</tag> ", "base");
            benchmark(xmltest1, "base", "base");
            benchmark(xmltest2, "<tag>ColinBurnett</tag>", "ColinBurnett");
            benchmark(xmltest2, " <tag>ColinBurnett</tag> ", "ColinBurnett");
            benchmark(xmltest2, "ColinBurnett", "ColinBurnett");
            benchmark(xmltest3, "<tag>Si</tag>", "Si");
            benchmark(xmltest3, " <tag>Si</tag> ", "Si" );
            benchmark(xmltest3, "Si", "Si");
            benchmark(xmltest4, "<tag>RashmiPandit</tag>", "RashmiPandit");
            benchmark(xmltest4, " <tag>RashmiPandit</tag> ", "RashmiPandit");
            benchmark(xmltest4, "RashmiPandit", "RashmiPandit");
            benchmark(xmltest5, "<tag>Custom</tag>", "Custom");
            benchmark(xmltest5, " <tag>Custom</tag> ", "Custom");
            benchmark(xmltest5, "Custom", "Custom");

            // "press any key to continue"
            Console.WriteLine("Done.");
            Console.ReadLine();
        }

        public static String xmltest1(String data)
        {
            try
            {
                return XElement.Parse(data).Value;
            }
            catch (System.Xml.XmlException)
            {
                return data;
            }
        }

        static Regex xmltest2regex = new Regex("^[ \t\r\n]*<");
        public static String xmltest2(String data)
        {
            // Has to have length to be XML
            if (!string.IsNullOrEmpty(data))
            {
                // If it starts with a < then it probably is XML
                // But also cover the case where there is indeterminate whitespace before the <
                if (data[0] == '<' || xmltest2regex.Match(data).Success)
                {
                    try
                    {
                        return XElement.Parse(data).Value;
                    }
                    catch (System.Xml.XmlException)
                    {
                        return data;
                    }
                }
            }
           return data;
        }

        static Regex xmltest3regex = new Regex(@"<(?<tag>\w*)>(?<text>.*)</\k<tag>>");
        public static String xmltest3(String data)
        {
            Match m = xmltest3regex.Match(data);
            if (m.Success)
            {
                GroupCollection gc = m.Groups;
                if (gc.Count > 0)
                {
                    return gc["text"].Value;
                }
            }
            return data;
        }

        public static String xmltest4(String data)
        {
            String result;
            if (!XmlExpresssion.TryParse(data, out result))
                result = data;

            return result;
        }

        static Regex xmltest5regex = new Regex("^[ \t\r\n]*<");
        public static String xmltest5(String data)
        {
            // Has to have length to be XML
            if (!string.IsNullOrEmpty(data))
            {
                // If it starts with a < then it probably is XML
                // But also cover the case where there is indeterminate whitespace before the <
                if (data[0] == '<' || data.Trim()[0] == '<' || xmltest5regex.Match(data).Success)
                {
                    try
                    {
                        return XElement.Parse(data).Value;
                    }
                    catch (System.Xml.XmlException)
                    {
                        return data;
                    }
                }
            }
            return data;
        }
    }

    public class XmlExpresssion
    {
        // EXPLANATION OF EXPRESSION
        // <        :   \<{1}
        // text     :   (?<xmlTag>\w+)  : xmlTag is a backreference so that the start and end tags match
        // >        :   >{1}
        // xml data :   (?<data>.*)     : data is a backreference used for the regex to return the element data      
        // </       :   <{1}/{1}
        // text     :   \k<xmlTag>
        // >        :   >{1}
        // (\w|\W)* :   Matches attributes if any

        // Sample match and pattern egs
        // Just to show how I incrementally made the patterns so that the final pattern is well-understood
        // <text>data</text>
        // @"^\<{1}(?<xmlTag>\w+)\>{1}.*\<{1}/{1}\k<xmlTag>\>{1}$";

        //<text />
        // @"^\<{1}(?<xmlTag>\w+)\s*/{1}\>{1}$";

        //<text>data</text> or <text />
        // @"^\<{1}(?<xmlTag>\w+)((\>{1}.*\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

        //<text>data</text> or <text /> or <text attr='2'>xml data</text> or <text attr='2' attr2 >data</text>
        // @"^\<{1}(?<xmlTag>\w+)(((\w|\W)*\>{1}(?<data>.*)\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

        private static string XML_PATTERN = @"^\<{1}(?<xmlTag>\w+)(((\w|\W)*\>{1}(?<data>.*)\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";
        private static Regex regex = new Regex(XML_PATTERN, RegexOptions.Compiled);

        // Checks if the string is in xml format
        private static bool IsXml(string value)
        {
            return regex.IsMatch(value);
        }

        /// <summary>
        /// Assigns the element value to result if the string is xml
        /// </summary>
        /// <returns>true if success, false otherwise</returns>
        public static bool TryParse(string s, out string result)
        {
            if (XmlExpresssion.IsXml(s))
            {
                result = regex.Match(s).Result("${data}");
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

    }


}

以下是最新的结果:

<tag>base</tag>: 3.667
 <tag>base</tag> : 3.707
base: 40.737
<tag>ColinBurnett</tag>: 3.707
 <tag>ColinBurnett</tag> : 4.784
ColinBurnett: 0.413
<tag>Si</tag>: 2.016
 <tag>Si</tag> : 2.141
Si: 0.087
<tag>RashmiPandit</tag>: 12.305
 <tag>RashmiPandit</tag> : fail
RashmiPandit: 0.131
<tag>Custom</tag>: 3.761
 <tag>Custom</tag> : 3.866
Custom: 0.329
Done.

看到这里,我们可以得出结论:预编译的正则表达式是最佳选择,而且效率也非常高。




(原始帖子)

我编写了以下程序来对提供给这个答案的代码示例进行基准测试,以展示我的观点并评估所提供答案的速度。

话不多说,下面是程序。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace ConsoleApplication3
{
    delegate String xmltestFunc(String data);

    class Program
    {
        static readonly int iterations = 1000000;

        private static void benchmark(xmltestFunc func, String data, String expectedResult)
        {
            if (!func(data).Equals(expectedResult))
            {
                Console.WriteLine(data + ": fail");
                return;
            }
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < iterations; ++i)
                func(data);
            sw.Stop();
            Console.WriteLine(data + ": " + (float)((float)sw.ElapsedMilliseconds / 1000));
        }

        static void Main(string[] args)
        {
            benchmark(xmltest1, "<tag>base</tag>", "base");
            benchmark(xmltest1, " <tag>base</tag> ", "base");
            benchmark(xmltest1, "base", "base");
            benchmark(xmltest2, "<tag>ColinBurnett</tag>", "ColinBurnett");
            benchmark(xmltest2, " <tag>ColinBurnett</tag> ", "ColinBurnett");
            benchmark(xmltest2, "ColinBurnett", "ColinBurnett");
            benchmark(xmltest3, "<tag>Si</tag>", "Si");
            benchmark(xmltest3, " <tag>Si</tag> ", "Si" );
            benchmark(xmltest3, "Si", "Si");
            benchmark(xmltest4, "<tag>RashmiPandit</tag>", "RashmiPandit");
            benchmark(xmltest4, " <tag>RashmiPandit</tag> ", "RashmiPandit");
            benchmark(xmltest4, "RashmiPandit", "RashmiPandit");

            // "press any key to continue"
            Console.WriteLine("Done.");
            Console.ReadLine();
        }

        public static String xmltest1(String data)
        {
            try
            {
                return XElement.Parse(data).Value;
            }
            catch (System.Xml.XmlException)
            {
                return data;
            }
        }

        public static String xmltest2(String data)
        {
            // Has to have length to be XML
            if (!string.IsNullOrEmpty(data))
            {
                // If it starts with a < then it probably is XML
                // But also cover the case where there is indeterminate whitespace before the <
                if (data[0] == '<' || new Regex("^[ \t\r\n]*<").Match(data).Success)
                {
                    try
                    {
                        return XElement.Parse(data).Value;
                    }
                    catch (System.Xml.XmlException)
                    {
                        return data;
                    }
                }
            }
           return data;
        }

        public static String xmltest3(String data)
        {
            Regex regex = new Regex(@"<(?<tag>\w*)>(?<text>.*)</\k<tag>>");
            Match m = regex.Match(data);
            if (m.Success)
            {
                GroupCollection gc = m.Groups;
                if (gc.Count > 0)
                {
                    return gc["text"].Value;
                }
            }
            return data;
        }

        public static String xmltest4(String data)
        {
            String result;
            if (!XmlExpresssion.TryParse(data, out result))
                result = data;

            return result;
        }

    }

    public class XmlExpresssion
    {
        // EXPLANATION OF EXPRESSION
        // <        :   \<{1}
        // text     :   (?<xmlTag>\w+)  : xmlTag is a backreference so that the start and end tags match
        // >        :   >{1}
        // xml data :   (?<data>.*)     : data is a backreference used for the regex to return the element data      
        // </       :   <{1}/{1}
        // text     :   \k<xmlTag>
        // >        :   >{1}
        // (\w|\W)* :   Matches attributes if any

        // Sample match and pattern egs
        // Just to show how I incrementally made the patterns so that the final pattern is well-understood
        // <text>data</text>
        // @"^\<{1}(?<xmlTag>\w+)\>{1}.*\<{1}/{1}\k<xmlTag>\>{1}$";

        //<text />
        // @"^\<{1}(?<xmlTag>\w+)\s*/{1}\>{1}$";

        //<text>data</text> or <text />
        // @"^\<{1}(?<xmlTag>\w+)((\>{1}.*\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

        //<text>data</text> or <text /> or <text attr='2'>xml data</text> or <text attr='2' attr2 >data</text>
        // @"^\<{1}(?<xmlTag>\w+)(((\w|\W)*\>{1}(?<data>.*)\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

        private const string XML_PATTERN = @"^\<{1}(?<xmlTag>\w+)(((\w|\W)*\>{1}(?<data>.*)\<{1}/{1}\k<xmlTag>)|(\s*/{1}))\>{1}$";

        // Checks if the string is in xml format
        private static bool IsXml(string value)
        {
            return Regex.IsMatch(value, XML_PATTERN);
        }

        /// <summary>
        /// Assigns the element value to result if the string is xml
        /// </summary>
        /// <returns>true if success, false otherwise</returns>
        public static bool TryParse(string s, out string result)
        {
            if (XmlExpresssion.IsXml(s))
            {
                Regex r = new Regex(XML_PATTERN, RegexOptions.Compiled);
                result = r.Match(s).Result("${data}");
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

    }


}

以下是结果。每个结果都执行了100万次。

<tag>base</tag>: 3.531
 <tag>base</tag> : 3.624
base: 41.422
<tag>ColinBurnett</tag>: 3.622
 <tag>ColinBurnett</tag> : 16.467
ColinBurnett: 7.995
<tag>Si</tag>: 19.014
 <tag>Si</tag> : 19.201
Si: 15.567

第四个测试花费的时间太长,30分钟后被认为速度过慢。为了展示它有多慢,这里提供同样的测试,只不过运行了1000次。

<tag>base</tag>: 0.004
 <tag>base</tag> : 0.004
base: 0.047
<tag>ColinBurnett</tag>: 0.003
 <tag>ColinBurnett</tag> : 0.016
ColinBurnett: 0.008
<tag>Si</tag>: 0.021
 <tag>Si</tag> : 0.017
Si: 0.014
<tag>RashmiPandit</tag>: 3.456
 <tag>RashmiPandit</tag> : fail
RashmiPandit: 0
Done.

将其推广到一百万次执行,需要3456秒,即略超过57分钟。
这很好地说明了为什么如果你想要高效的代码,复杂的正则表达式是一个坏主意。然而,它也表明在某些情况下简单的正则表达式仍然是一个好的答案——即在colinBurnett的答案中,小的“预测试”xml可能会创建一个潜在更昂贵的基础情况(正则表达式是在情况2中创建的),但通过避免异常,也可以创建一个更短的else情况。

通过使用静态字段来保存正则表达式实例,再次尝试。我敢说,重复实例化和编译正则表达式占用了相当大的时间。 - Colin Burnett
实际上,我的示例中的正则表达式完全是不必要的,因为 if (data[0] == '<' || data.TrimStart()[0] == '<') 就是 "^[ \t\r\n]*<" 寻找的内容。 - Colin Burnett
添加了自定义测试,使得该更改生效。 - cyberconte

3

我认为处理你的情况的方法是完全可以接受的(这可能也是我处理它的方式)。我在MSDN上找不到任何"XElement.TryParse(string)"的相关信息,所以你现在的处理方式完全没问题。


3
除了使用XElement.Parse之类的方法,没有其他有效的方法可以验证文本是否为XML。例如,如果文本字段中缺少最后一个关闭尖括号,则它就不是有效的XML,而且很难通过正则表达式或文本解析找到这种错误。有许多非法字符、非法序列等,在正则表达式解析时很可能会被忽略。
你所能做的就是缩短失败案例的时间。因此,如果您希望看到大量的非XML数据,而较少的情况是XML,那么使用正则表达式或子字符串搜索来检测尖括号可能会节省一些时间,但我认为这只有在批量处理大量数据时才有用。
相反,如果这是从Web表单或WinForms应用程序中解析用户输入的数据,那么我认为支付异常成本可能比花费开发和测试工作更好,以确保您的快捷代码不会产生错误的结果。
不清楚您从哪里获取XML(文件、流、文本框或其他位置),但请记住,空格、注释、字节顺序标记和其他内容可能会妨碍简单规则,例如“它必须以<开头”。

2
正如评论中@JustEngland所指出的,异常并不是很昂贵,调试器拦截它们可能需要时间,但通常它们表现良好且是良好的实践。请参见 C#中的异常有多昂贵?
更好的方法是编写自己的TryParse样式函数:
[System.Diagnostics.DebuggerNonUserCode]
static class MyXElement
{
    public static bool TryParse(string data, out XElement result)
    {
        try
        {
            result = XElement.Parse(data);
            return true;
        }
        catch (System.Xml.XmlException)
        {
            result = default(XElement);
            return false;
        }
    }
}

DebuggerNonUserCode属性使调试器跳过捕获的异常,以简化您的调试体验。

使用方法如下:

    static void Main()
    {
        var addressList = "line one~line two~line three~postcode";

        var address = new XElement("Address");
        var addressHtml = "<span>" + addressList.Replace("~", "<br />") + "</span>";

        XElement content;
        if (MyXElement.TryParse(addressHtml, out content))
            address.ReplaceAll(content);
        else
            address.SetValue(addressHtml);

        Console.WriteLine(address.ToString());
        Console.ReadKey();
    }
}

我本希望为TryParse创建一个扩展方法,但是你不能创建一个静态的方法,而是要调用一个类型的实例。


在框架中有一个TryParse会更好,但这是一种有用的技巧,可以改善调试体验。 - eisenpony

1
为什么正则表达式很耗费资源?难道不是一举两得(匹配和解析)吗?
简单的例子需要解析所有元素,如果只有一个元素那就更容易了!
Regex regex = new Regex(@"<(?<tag>\w*)>(?<text>.*)</\k<tag>>");
MatchCollection matches = regex.Matches(data);
foreach (Match match in matches)
{
    GroupCollection groups = match.Groups;
    string name = groups["tag"].Value;
    string value = groups["text"].Value;
    ...
}

1
只是需要注意的是,这并不验证XML是否有效(它在文本部分可能无效)。 - cyberconte
1
它也不能确定所有标签都已关闭(使其成为无效的XML)。 - Richard Szalay

0

我不确定你的要求是否考虑了文件格式,因为这个问题是很久以前问的,我碰巧搜索到了类似的东西,我想让你知道对我有用的是什么,所以如果有人来到这里,这可能会有所帮助 :)

我们可以使用Path.GetExtension(filePath)并检查它是否为XML,然后使用它,否则做必要的事情。


0

这是一个相当古老的问题和答案,但仍然是一个有效的关注点 :-)

以下是被接受的答案的稍微简化版本,同时包装成一个自定义扩展,以便在任何字符串中轻松使用:

public static bool IsDuckTypedXml(this string xmlText)
{
    if (string.IsNullOrWhiteSpace(xmlText))
        return false;

    var text = xmlText.Trim();
    return (text.StartsWith("<") && text.EndsWith(">"));
}

0
Colin Burnett技巧的变体:您可以在开头进行简单的正则表达式,以查看文本是否以标签开头,然后尝试解析它。您将处理的字符串中,可能有超过99%以有效元素开头的XML。这样,您就可以跳过完整的有效XML的正则表达式处理,并且在几乎每种情况下都可以跳过基于异常的处理。
类似^<[^>]+>的东西可能会起作用。

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