Специальный символ в XPATH Query - PullRequest
39 голосов
/ 27 августа 2009

Я использую следующие XPATH Query, чтобы перечислить объект под сайтом. ListObject[@Title='SomeValue']. SomeValue является динамическим. Этот запрос работает до тех пор, пока у SomeValue нет апострофа ('). Пробовал также использовать escape-последовательность. Не сработало

Что я делаю не так?

Ответы [ 10 ]

57 голосов
/ 30 августа 2009

Это удивительно сложно сделать.

Взгляните на Рекомендацию XPath , и вы увидите, что она определяет литерал как:

Literal ::=   '"' [^"]* '"' 
            | "'" [^']* "'"

То есть строковые литералы в выражениях XPath могут содержать апострофы или двойные кавычки, но не оба одновременно.

Вы не можете использовать побег, чтобы обойти это. Буквально такой:

'Some'Value'

будет соответствовать этому XML-тексту:

Some'Value

Это означает, что может существовать фрагмент текста XML, который вы не можете сгенерировать для литерала XPath, например ::

<elm att="&quot;&apos"/>

Но это не значит, что невозможно сопоставить этот текст с XPath, это просто сложно. В любом случае, когда значение, которое вы пытаетесь сопоставить, содержит как одинарные, так и двойные кавычки, вы можете создать выражение, которое использует concat для получения текста, который будет соответствовать:

elm[@att=concat('"', "'")]

Итак, это подводит нас к этому, что намного сложнее, чем хотелось бы:

/// <summary>
/// Produce an XPath literal equal to the value if possible; if not, produce
/// an XPath expression that will match the value.
/// 
/// Note that this function will produce very long XPath expressions if a value
/// contains a long run of double quotes.
/// </summary>
/// <param name="value">The value to match.</param>
/// <returns>If the value contains only single or double quotes, an XPath
/// literal equal to the value.  If it contains both, an XPath expression,
/// using concat(), that evaluates to the value.</returns>
static string XPathLiteral(string value)
{
    // if the value contains only single or double quotes, construct
    // an XPath literal
    if (!value.Contains("\""))
    {
        return "\"" + value + "\"";
    }
    if (!value.Contains("'"))
    {
        return "'" + value + "'";
    }

    // if the value contains both single and double quotes, construct an
    // expression that concatenates all non-double-quote substrings with
    // the quotes, e.g.:
    //
    //    concat("foo", '"', "bar")
    StringBuilder sb = new StringBuilder();
    sb.Append("concat(");
    string[] substrings = value.Split('\"');
    for (int i = 0; i < substrings.Length; i++ )
    {
        bool needComma = (i>0);
        if (substrings[i] != "")
        {
            if (i > 0)
            {
                sb.Append(", ");
            }
            sb.Append("\"");
            sb.Append(substrings[i]);
            sb.Append("\"");
            needComma = true;
        }
        if (i < substrings.Length - 1)
        {
            if (needComma)
            {
                sb.Append(", ");                    
            }
            sb.Append("'\"'");
        }

    }
    sb.Append(")");
    return sb.ToString();
}

И да, я проверял это со всеми крайними случаями. Вот почему логика настолько тупо сложна:

    foreach (string s in new[]
    {
        "foo",              // no quotes
        "\"foo",            // double quotes only
        "'foo",             // single quotes only
        "'foo\"bar",        // both; double quotes in mid-string
        "'foo\"bar\"baz",   // multiple double quotes in mid-string
        "'foo\"",           // string ends with double quotes
        "'foo\"\"",         // string ends with run of double quotes
        "\"'foo",           // string begins with double quotes
        "\"\"'foo",         // string begins with run of double quotes
        "'foo\"\"bar"       // run of double quotes in mid-string
    })
    {
        Console.Write(s);
        Console.Write(" = ");
        Console.WriteLine(XPathLiteral(s));
        XmlElement elm = d.CreateElement("test");
        d.DocumentElement.AppendChild(elm);
        elm.SetAttribute("value", s);

        string xpath = "/root/test[@value = " + XPathLiteral(s) + "]";
        if (d.SelectSingleNode(xpath) == elm)
        {
            Console.WriteLine("OK");
        }
        else
        {
            Console.WriteLine("Should have found a match for {0}, and didn't.", s);
        }
    }
    Console.ReadKey();
}
6 голосов
/ 27 августа 2009

РЕДАКТИРОВАТЬ: После тяжелого юнит-тестирования и проверки Стандартов XPath я изменил свою функцию следующим образом:

public static string ToXPath(string value) {

    const string apostrophe = "'";
    const string quote = "\"";

    if(value.Contains(quote)) {
        if(value.Contains(apostrophe)) {
            throw new XPathException("Illegal XPath string literal.");
        } else {
            return apostrophe + value + apostrophe;
        }
    } else {
        return quote + value + quote;
    }
}

Похоже, что XPath вообще не имеет системы выхода из строя персонажа, на самом деле она довольно примитивна. Очевидно, мой оригинальный код работал только по стечению обстоятельств. Приношу свои извинения за введение в заблуждение кого-либо!

Оригинальный ответ ниже только для справки - пожалуйста, игнорируйте

В целях безопасности убедитесь, что все 5 предопределенных объектов XML в вашей строке XPath экранированы, например,

public static string ToXPath(string value) {
    return "'" + XmlEncode(value) + "'";
}

public static string XmlEncode(string value) {
    StringBuilder text = new StringBuilder(value);
    text.Replace("&", "&amp;");
    text.Replace("'", "&apos;");
    text.Replace(@"""", "&quot;");
    text.Replace("<", "&lt;");
    text.Replace(">", "&gt;");
    return text.ToString();
}

Я делал это раньше, и он отлично работает. Если это не работает для вас, возможно, есть какой-то дополнительный контекст проблемы, о котором вы должны сообщить нам.

5 голосов
/ 22 апреля 2014

Безусловно, лучший подход к этой проблеме - использовать средства, предоставляемые вашей библиотекой XPath, для объявления переменной уровня XPath, на которую вы можете ссылаться в выражении. Значением переменной может быть любая строка в языке программирования хоста, и она не подпадает под ограничения строковых литералов XPath. Например, в Java с javax.xml.xpath:

XPathFactory xpf = XPathFactory.newInstance();
final Map<String, Object> variables = new HashMap<>();
xpf.setXPathVariableResolver(new XPathVariableResolver() {
  public Object resolveVariable(QName name) {
    return variables.get(name.getLocalPart());
  }
});

XPath xpath = xpf.newXPath();
XPathExpression expr = xpath.compile("ListObject[@Title=$val]");
variables.put("val", someValue);
NodeList nodes = (NodeList)expr.evaluate(someNode, XPathConstants.NODESET);

Для C # XPathNavigator вы должны определить пользовательский XsltContext , как описано в этой статье MSDN (вам понадобятся только части, связанные с переменными в этом примере, а не функции расширения). 1009 *

5 голосов
/ 20 июля 2012

Я портировал ответ Роберта на Java (проверено в 1.6):

/// <summary>
/// Produce an XPath literal equal to the value if possible; if not, produce
/// an XPath expression that will match the value.
///
/// Note that this function will produce very long XPath expressions if a value
/// contains a long run of double quotes.
/// </summary>
/// <param name="value">The value to match.</param>
/// <returns>If the value contains only single or double quotes, an XPath
/// literal equal to the value.  If it contains both, an XPath expression,
/// using concat(), that evaluates to the value.</returns>
public static String XPathLiteral(String value) {
    if(!value.contains("\"") && !value.contains("'")) {
        return "'" + value + "'";
    }
    // if the value contains only single or double quotes, construct
    // an XPath literal
    if (!value.contains("\"")) {
        System.out.println("Doesn't contain Quotes");
        String s = "\"" + value + "\"";
        System.out.println(s);
        return s;
    }
    if (!value.contains("'")) {
        System.out.println("Doesn't contain apostophes");
        String s =  "'" + value + "'";
        System.out.println(s);
        return s;
    }

    // if the value contains both single and double quotes, construct an
    // expression that concatenates all non-double-quote substrings with
    // the quotes, e.g.:
    //
    //    concat("foo", '"', "bar")
    StringBuilder sb = new StringBuilder();
    sb.append("concat(");
    String[] substrings = value.split("\"");
    for (int i = 0; i < substrings.length; i++) {
        boolean needComma = (i > 0);
        if (!substrings[i].equals("")) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append("\"");
            sb.append(substrings[i]);
            sb.append("\"");
            needComma = true;
        }
        if (i < substrings.length - 1) {
            if (needComma) {
                sb.append(", ");
            }
            sb.append("'\"'");
        }
        System.out.println("Step " + i + ": " + sb.toString());
    }
    //This stuff is because Java is being stupid about splitting strings
    if(value.endsWith("\"")) {
        sb.append(", '\"'");
    }
    //The code works if the string ends in a apos
    /*else if(value.endsWith("'")) {
        sb.append(", \"'\"");
    }*/
    sb.append(")");
    String s = sb.toString();
    System.out.println(s);
    return s;
}

Надеюсь, это кому-нибудь поможет!

3 голосов
/ 29 декабря 2014

Большинство ответов здесь сосредоточены на том, как использовать манипуляции со строками для объединения XPath, который корректно использует разделители строк.

Я бы сказал, что лучшая практика - не полагаться на такие сложные и потенциально хрупкие методы.

Следующее относится к .NET, так как этот вопрос помечен C #. Ян Робертс предоставил, как мне кажется, лучшее решение для использования XPath в Java.

В настоящее время вы можете использовать Linq-to-Xml для запроса XML-документов таким образом, чтобы вы могли напрямую использовать переменные в запросе. Это не XPath, но цель та же.

Для примера, приведенного в OP, вы можете запросить нужные узлы следующим образом:

var value = "Some value with 'apostrophes' and \"quotes\"";

// doc is an instance of XElement or XDocument
IEnumerable<XElement> nodes = 
                      doc.Descendants("ListObject")
                         .Where(lo => (string)lo.Attribute("Title") == value);

или использовать синтаксис понимания запроса:

IEnumerable<XElement> nodes = from lo in doc.Descendants("ListObject")
                              where (string)lo.Attribute("Title") == value
                              select lo;

.NET также предоставляет способ использования переменных XPath в ваших запросах XPath. К сожалению, это не легко сделать из коробки, но с помощью простого вспомогательного класса, который я предоставляю в этом другом ответе SO , это довольно просто.

Вы можете использовать это так:

var value = "Some value with 'apostrophes' and \"quotes\"";

var variableContext = new VariableContext { { "matchValue", value } };
// ixn is an instance of IXPathNavigable
XPathNodeIterator nodes = ixn.CreateNavigator()
                             .SelectNodes("ListObject[@Title = $matchValue]", 
                                          variableContext);
2 голосов
/ 13 июня 2014

Вы можете заключить строку XPath в кавычки, используя поиск и замену.

В F #

let quoteString (s : string) =
    if      not (s.Contains "'" ) then sprintf "'%s'"   s
    else if not (s.Contains "\"") then sprintf "\"%s\"" s
    else "concat('" + s.Replace ("'", "', \"'\", '") + "')"

Я не тестировал его широко, но, похоже, работает.

2 голосов
/ 28 сентября 2012

Вот альтернатива подходу Роберта Росни StringBuilder, возможно, более интуитивно понятный:

    /// <summary>
    /// Produce an XPath literal equal to the value if possible; if not, produce
    /// an XPath expression that will match the value.
    /// 
    /// Note that this function will produce very long XPath expressions if a value
    /// contains a long run of double quotes.
    /// 
    /// From: /1208585/spetsialnyi-simvol-v-xpath-query
    /// </summary>
    /// <param name="value">The value to match.</param>
    /// <returns>If the value contains only single or double quotes, an XPath
    /// literal equal to the value.  If it contains both, an XPath expression,
    /// using concat(), that evaluates to the value.</returns>
    public static string XPathLiteral(string value)
    {
        // If the value contains only single or double quotes, construct
        // an XPath literal
        if (!value.Contains("\""))
            return "\"" + value + "\"";

        if (!value.Contains("'"))
            return "'" + value + "'";

        // If the value contains both single and double quotes, construct an
        // expression that concatenates all non-double-quote substrings with
        // the quotes, e.g.:
        //
        //    concat("foo",'"',"bar")

        List<string> parts = new List<string>();

        // First, put a '"' after each component in the string.
        foreach (var str in value.Split('"'))
        {
            if (!string.IsNullOrEmpty(str))
                parts.Add('"' + str + '"'); // (edited -- thanks Daniel :-)

            parts.Add("'\"'");
        }

        // Then remove the extra '"' after the last component.
        parts.RemoveAt(parts.Count - 1);

        // Finally, put it together into a concat() function call.
        return "concat(" + string.Join(",", parts) + ")";
    }
0 голосов
/ 24 апреля 2014

Вы можете решить эту проблему, используя double quotes вместо single quotes в выражении XPath.

Например:

element.XPathSelectElements(String.Format("//group[@title=\"{0}\"]", "Man's"));
0 голосов
/ 24 ноября 2009

У меня была эта проблема некоторое время назад, и, казалось бы, самое простое, но не самое быстрое решение, заключается в том, что вы добавляете в документ XML новый узел, имеющий атрибут со значением «SomeValue», а затем ищите значение этого атрибута, используя простой поиск по xpath. По завершении операции вы можете удалить «временный узел» из документа XML.

Таким образом, все сравнение происходит "изнутри", поэтому вам не нужно создавать странный запрос XPath.

Кажется, я помню, что для ускорения процесса вы должны добавить временное значение к корневому узлу.

Удачи ...

0 голосов
/ 27 августа 2009

Если вы не собираетесь использовать двойные кавычки в SomeValue, вы можете использовать экранированные двойные кавычки, чтобы указать значение, которое вы ищете в строке поиска XPath.

ListObject[@Title=\"SomeValue\"]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...