XPath查询中的撇号(')

42
我使用以下 XPATH 查询 列出站点下的对象:ListObject[@Title='SomeValue']。 SomeValue 是动态的。只要 SomeValue 没有单引号('),这个查询就能够工作。尝试使用转义序列,但是并没有起作用。
我做错了什么?

那么SomeValue是C#变量吗? - Jon W
是的,这是一个C#变量。"ListObject[@Title='" + SomeValue +"']"。这就是我编写表达式的方式。 - Prabhu
11个回答

59

这实际上很难做到。

看一下XPath Recommendation,你会发现它定义了字面量为:

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

10
好的。其实我自己对这没什么用处,只是因为一开始觉得这个问题很有趣,然后随着深入挖掘,它的复杂度开始让我感到烦恼。我的注意力缺陷过动症就成了你的收获。 - Robert Rossney
"\n" 是怎样的呢?我有疑问换行符也可能会导致问题。 - kan
1
"这个做起来出奇地困难。" 只有在错误的方式下(尝试拼凑字符串)才会出奇地困难。如果你使用正确的方法之一,它就相当简单。 - JLRishe
做得好极了,@RobertRossney!顺便说一句,我对这段代码进行了 PHP 移植,如果有人感兴趣:https://gist.github.com/divinity76/64b0c12bcafc2150efa8ca87d2ccee52 - hanshenrik
我相信这可以缩短为一个漂亮的一行代码,例如PHP:function xpath_quote($str){ return 'concat(\''.implode('\', "\'", \'', explode('\'', $value)).'\')'; } - 在大多数情况下生成效率较低、长度较长的xpath,但代码很短<strike>&甜美</strike>:https://3v4l.org/rDa1L - hanshenrik
显示剩余2条评论

7

我将Robert的回答移植到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;
}

希望这能帮助到一些人!

6

编辑: 经过大量单元测试和查阅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根本没有字符转义系统,它确实非常原始。显然我的原始代码只是偶然起作用的。我很抱歉误导了任何人!

以下是仅供参考的原始答案 - 请忽略

为了安全起见,请确保您XPath字符串中所有5个预定义的XML实体的出现都已转义,例如:

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

我以前做过这个,它可以正常工作。如果对你不起作用,可能是因为有一些额外的上下文问题,你需要让我们知道。


你甚至不必把XML当作纯字符串来处理。像转义和反转义这样的操作已经被内置的XML库抽象出来了。你在这里是在重复造轮子。 - Welbog
6
如果你能指出一个BCL类,可以将构建XPath查询字符串的过程抽象化,我很乐意放弃这些函数。 - Christian Hayter
比如 System.Security.SecurityElement.Escape(value)?(在C#中) - Flynn1179
1
@ChristianHayter 我来晚了,但你错过的一点(我认为Welbog试图表达的)是XPath有变量的概念,这些变量不受字符串分隔符问题的影响。因此最佳实践是利用它们。.NET提供了在XPath中使用变量的机制,我在这里提供了如何使用的示例(https://dev59.com/rnjZa4cB1Zd3GeqPbTcl#19704008)。 - JLRishe
1
@JLRishe:我已经多年没有看过这个问题了;自从LINQ to XML出现以来,我就再也没有写过任何XPath查询。 :-) 对于任何字符串注入问题,参数化数据值始终是最好的解决方案,因此我已经为您两个答案点赞。非常感谢。 - Christian Hayter
例外并不是必须的,您可以通过使用concat()来解决它[请参见Robert Rossney的答案] (https://dev59.com/pXM_5IYBdhLWcg3whznx#1352556)。 - hanshenrik

5

目前解决这个问题最好的方法是使用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,你需要按照这篇MSDN文章所述定义一个自定义的XsltContext(你只需要使用此示例中与变量相关的部分,而不是扩展函数)。


这绝对是最好的方法。+1 - JLRishe

4
大多数答案都集中在如何使用字符串操作来拼凑一个使用字符串分隔符的有效XPath上。我认为最佳实践是不要依赖这种复杂且可能脆弱的方法。以下内容适用于.NET,因为此问题标记了C#。Ian Roberts提供了我认为是在Java中使用XPath时的最佳解决方案。现在,您可以使用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);

我认为你说得很有道理,这是一个非常好的选择。 - Jtbs

2

这里有一种替代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: https://dev59.com/pXM_5IYBdhLWcg3whznx
    /// </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) + ")";
    }

你的代码没有通过他所有的测试。 - Daniel A. White
将您的“add to the parts”更改为引用字符串。 - Daniel A. White
谢谢,不确定我怎么会错过那个。已修复。 :-) - Jonathan Gilbert
你好,你的代码比原来的好了1000倍,但仍然比所需的笨拙。不要先添加一个稍后再删除的字符串,这样会更容易:String[] split = value.Split('"'); for (int i=0; i<split.length; i++) { if (i>0) parts.Add("'"'");if (split[i].Length > 0) parts.Add('"' + split[i] + '"');} - Elmue
@Elmue 这可能是个人口味的问题。我认为这比删除最后一个字符串更加笨重。当然,性能上没有显著的区别。另一种实现方式可能是在每个条目之前添加一个 '"',然后使用内联 LINQ 表达式,而不是单独使用 .Remove 语句:`foreach (var str in value.Split('"'))
{
parts.Add("'"'");
if (!string.IsNullOrEmpty(str))
parts.Add('"' + str + '"');
} return "concat(" + string.Join(",", parts.Skip(1)) + ")";`
- Jonathan Gilbert
@Elmue,顺便说一句,谢谢你的赞美 :-) - Jonathan Gilbert

2
你可以使用搜索和替换来引用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 ("'", "', \"'\", '") + "')"

我还没有进行过全面测试,但似乎可以工作。

1

我非常喜欢Robert的答案,但我觉得代码可以更紧凑。

using System.Linq;

namespace Humig.Csp.Common
{
    public static class XpathHelpers
    {
        public static string XpathLiteralEncode(string literalValue)
        {
            return string.IsNullOrEmpty(literalValue)
                ? "''"
                : !literalValue.Contains("\"")
                ? $"\"{literalValue}\""
                : !literalValue.Contains("'")
                ? $"'{literalValue}'"
                : $"concat({string.Join(",'\"',", literalValue.Split('"').Select(k => $"\"{k}\""))})";
        }
    }
}

我还创建了一个包含所有测试用例的单元测试:

using HtmlAgilityPack;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Humig.Csp.Common.Tests
{
    [TestClass()]
    public class XpathHelpersTests
    {

        [DataRow("foo")]   // no quotes
        [DataRow("\"foo")]   // double quotes only
        [DataRow("'foo")]   // single quotes only
        [DataRow("'foo\"bar")]   // both; double quotes in mid-string
        [DataRow("'foo\"bar\"baz")]   // multiple double quotes in mid-string
        [DataRow("'foo\"")]   // string ends with double quotes
        [DataRow("'foo\"\"")]   // string ends with run of double quotes
        [DataRow("\"'foo")]   // string begins with double quotes
        [DataRow("\"\"'foo")]   // string begins with run of double quotes
        [DataRow("'foo\"\"bar")]   // run of double quotes in mid-string
        [TestMethod()]
        public void XpathLiteralEncodeTest(string attrValue)
        {
            var doc = new HtmlDocument();
            var hnode = doc.CreateElement("html");
            var body = doc.CreateElement("body");
            var div = doc.CreateElement("div");
            div.Attributes.Add("data-test", attrValue);
            doc.DocumentNode.AppendChild(hnode);
            hnode.AppendChild(body);
            body.AppendChild(div);
            var literalOut = XpathHelpers.XpathLiteralEncode(attrValue);
            string xpath = $"/html/body/div[@data-test = {literalOut}]";
            var result = doc.DocumentNode.SelectSingleNode(xpath);
            Assert.AreEqual(div, result, $"did not find a match for {attrValue}");

        }
    }
}

0

如果在 SomeValue 中不会出现任何双引号,您可以使用转义的双引号来指定您在 XPath 搜索字符串中要搜索的值。

ListObject[@Title=\"SomeValue\"]

这不是在XML中转义字符的正确方式。 - Welbog
2
没错。但XPath查询不是XML文本,而且无论如何他都没有为XPath转义引号,他是为C#转义它们。实际的、字面的XPath是ListObject[@Title="SomeValue"]。 - Robert Rossney
你没有理解问题。XPath语法不允许使用反斜杠字符进行转义。 - Elmue

0

您可以通过在XPath表达式中使用双引号而不是单引号来解决此问题。

例如:

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

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