Подтвердить что ты не робот

Специальный символ в запросе XPATH

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

Что я делаю неправильно?

4b9b3361

Ответ 1

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

Взгляните на Рекомендацию 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();
}

Ответ 2

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

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();
}

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

Ответ 3

Я поместил Роберта на 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;
}

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

Ответ 4

На сегодняшний день лучшим подходом к этой проблеме является использование средств, предоставленных вашей библиотекой 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);

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

Ответ 5

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

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

Следующее относится к .NET, поскольку этот вопрос помечен С#. Ян Робертс предоставил то, что я считаю лучшим решением, когда вы используете 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);

Ответ 6

Вот альтернатива подходу Robert Rossney 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: http://stackoverflow.com/questions/1341847/special-character-in-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) + ")";
    }

Ответ 7

Вы можете указать строку 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 ("'", "', \"'\", '") + "')"

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

Ответ 8

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

ListObject[@Title=\"SomeValue\"]

Ответ 9

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

Для ex:

element.XPathSelectElements(String.Format("//group[@title=\"{0}\"]", "Man's"));

Ответ 10

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

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

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

Удачи...