Лучший способ обнаружить XML?
-
23-08-2019 - |
Вопрос
В настоящее время у меня есть следующий код на c # для извлечения значения из текста.Если это XML, я хочу указать значение внутри него - в противном случае, если это не XML, он может просто вернуть сам текст.
String data = "..."
try
{
return XElement.Parse(data).Value;
}
catch (System.Xml.XmlException)
{
return data;
}
Я знаю, что исключения в C # обходятся дорого, поэтому мне было интересно, есть ли лучший способ определить, является ли текст, с которым я имею дело, xml или нет?
Я думал о тестировании регулярных выражений, но я не рассматриваю это как более дешевую альтернативу.Обратите внимание, я прошу о менее дорогой способ сделать это.
Решение
Вы могли бы провести предварительную проверку на < поскольку весь 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] идентичен тому, что будет делать это регулярное выражение.
Другие советы
Приведенный ниже код будет соответствовать всем следующим форматам 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);
Обновить: (оригинал поста ниже) У Колина есть блестящая идея перенести создание экземпляра regex за пределы вызовов, чтобы они создавались только один раз.Вот новая программа:
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;
}
}
}
}
И вот результаты.Каждый из них был выполнен 1 миллион раз.
<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
Тест 4 занял слишком много времени, так как через 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 минут.
Это хороший пример того, почему сложные регулярные выражения - плохая идея, если вы ищете эффективный код.Однако это показало, что простое регулярное выражение все еще может быть хорошим ответом в некоторых случаях, т. е.небольшое "предварительное тестирование" xml в ответе Колинбернетта создало потенциально более дорогой базовый вариант (регулярное выражение было создано в случае 2), но также и гораздо более короткий вариант else, избегая исключения.
Я нахожу это вполне приемлемым способом справиться с вашей ситуацией (вероятно, именно так я бы и поступил).Я не смог найти какой-либо "XElement.Попробуйте проанализировать (строку)" в MSDN, поэтому то, как у вас это есть, будет работать просто отлично.
Нет никакого способа проверить, что текст является XML, кроме как выполнить что-то вроде XElement.Разобрать.Если, например, в текстовом поле отсутствует самая последняя скобка с закрытым углом, то это недопустимый XML, и очень маловероятно, что вы обнаружите это с помощью регулярного выражения или синтаксического анализа текста.Существует ряд недопустимых символов, недопустимых последовательностей и т.д., которые, скорее всего, будут пропущены при анализе регулярных выражений.
Все, на что вы можете надеяться, - это сократить количество неудачных случаев.
Итак, если вы ожидаете увидеть много данных, отличных от XML, и наименее ожидаемым вариантом является XML, то использование поиска по регулярным выражениям или подстрокам для обнаружения угловых скобок может сэкономить вам немного времени, но я бы предположил, что это Только полезно, если вы выполняете пакетную обработку большого количества данных в тесном цикле.
Если вместо этого выполняется синтаксический анализ введенных пользователем данных из веб-формы или приложения winforms, то я думаю, что оплата стоимости Исключения может быть лучше, чем затраты на разработку и тестирование, гарантирующие, что ваш сокращенный код не генерирует ложноположительных / отрицательных результатов.
Неясно, откуда вы получаете свой XML (файл, поток, текстовое поле или где-то еще), но помните, что пробелы, комментарии, метки порядка байтов и другие вещи могут помешать простым правилам, таким как "это должно начинаться с <".
Почему регулярное выражение такое дорогое?Разве это не убивает 2 зайцев одним выстрелом (сопоставляет и анализирует)?
Простой пример разбора всех элементов, еще проще, если это всего лишь один элемент!
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;
...
}
Как отметил @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 , но вы не можете создать статический метод, вызываемый для типа, а не для экземпляра.
Подсказка - все допустимые XML-файлы должны начинаться с "<?xml
"
Возможно, вам придется иметь дело с различиями в наборе символов, но проверка обычного ASCII, utf-8 и unicode охватит 99,5% существующего xml.
Предлагаемый вами способ будет дорогостоящим, если вы будете использовать его в цикле, где большая часть xml-файлов не проверена, В случае проверенного xml ваш код будет работать так, как будто нет обработки исключений...таким образом, если в большинстве случаев ваш xml проверен или вы не используете его в цикле, ваш код будет работать нормально
Если вы хотите знать, действительно ли это, почему бы не использовать встроенный объект .NetFX вместо того, чтобы писать его с нуля?
Надеюсь, это поможет,
Билл
Вариация на тему техники Колина Бернетта:вы могли бы выполнить простое регулярное выражение в начале, чтобы увидеть, начинается ли текст с тега, а затем попытаться разобрать его.Вероятно, >99% строк, с которыми вы будете иметь дело, которые начинаются с допустимого элемента, являются XML.Таким образом, вы могли бы пропустить обработку регулярных выражений для полномасштабного допустимого XML, а также пропустить обработку на основе исключений почти в каждом случае.
Что - то вроде ^<[^>]+>
вероятно, это сработало бы.
Я не совсем уверен, учитывает ли ваше требование формат файла, и поскольку этот вопрос был задан давным-давно, и мне довелось искать нечто подобное, я хотел бы, чтобы вы знали, что сработало у меня, поэтому, если кто-нибудь придет сюда, это может помочь :)
Мы можем использовать Path.getExtension(filePath) и проверить, является ли это XML, а затем использовать его другим способом, делая то, что когда-либо требовалось
Как насчет этого, возьмите свою строку или объект и вставьте в новый XDocument или XElement.Все решается с помощью toString().