XPath 查询中的撇号 (')

Posted

技术标签:

【中文标题】XPath 查询中的撇号 (\')【英文标题】:Apostrophe (') in XPath queryXPath 查询中的撇号 (') 【发布时间】:2010-11-23 10:23:48 【问题描述】:

我使用以下XPATH Query 列出站点下的对象。 ListObject[@Title='SomeValue']。 SomeValue 是动态的。只要 SomeValue 没有撇号 ('),此查询就可以工作。也尝试使用转义序列。没用。

我做错了什么?

【问题讨论】:

SomeValue 是 C# 变量吗? 是的。它是一个 C# 变量。"ListObject[@Title='" + SomeValue +"']"。这就是我写表达式的方式 【参考方案1】:

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

ListObject[@Title=\"SomeValue\"]

【讨论】:

这不是您在 XML 中转义字符的方式。 确实如此。但是 XPath 查询不是 XML 文本,无论如何,他并没有为 XPath 转义引号,而是为 C# 转义它们。实际的文字 XPath 是 ListObject[@Title="SomeValue"] 您没有理解问题。 XPath 语法不允许反斜杠字符转义。【参考方案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 似乎根本没有字符转义系统,真的很原始。显然我的原始代码只是巧合。我很抱歉误导任何人!

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

为了安全起见,请确保 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("&", "&");
    text.Replace("'", "'");
    text.Replace(@"""", """);
    text.Replace("<", "&lt;");
    text.Replace(">", "&gt;");
    return text.ToString();

我以前做过,效果很好。如果它对您不起作用,也许您需要让我们意识到问题的一些额外背景。

【讨论】:

您甚至不必将 XML 视为纯字符串。内置 XML 库为您抽象了转义和取消转义之类的内容。你在这里重新发明***。 如果您能指出一个抽象出构建 XPath 查询字符串过程的 BCL 类,我很乐意放弃这些功能。 System.Security.SecurityElement.Escape(value)? (在 C# 中) @ChristianHayter 我在这里聚会很晚了,但你错过的一点(我认为 Welbog 试图说明)是 XPath 有变量的概念,它是免疫的这些字符串分隔符问题。所以最好的做法是利用它们。 .NET 确实提供了一种在 XPath 中使用变量的机制,我提供了一个如何使用的示例here。 @JLRishe:我已经好几年没看这个问题了;自从 LINQ to XML 出现以来,我根本没有编写任何 XPath 查询。 :-) 参数化数据值始终是任何字符串注入问题的最佳解决方案,因此我赞成您的两个答案。非常感谢。【参考方案3】:

这很难做到。

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

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

也就是说,XPath 表达式中的字符串文字可以包含撇号或双引号,但不能同时包含两者。

你不能使用转义来解决这个问题。像这样的文字:

'Some&apos;Value'

将匹配此 XML 文本:

Some&amp;apos;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();

【讨论】:

请做。我自己实际上没有用它;我这样做只是因为起初我发现这个问题很有趣,然后当我深入研究它的困难时,我开始烦恼。我的多动症是你的收获。 “\n”怎么样?我怀疑新线路也会导致问题。 @kan 不,XPath 中的字符串文字包含换行符是完全可以的。 only 限制是单引号字面量不能包含单引号,双引号字面量不能包含双引号。 “这很难做到。” 如果你以错误的方式去做(通过尝试将字符串拼凑在一起),它只会令人惊讶地难以做到。如果您使用正确的方法之一,那就很简单了。 嗨@RobertRossney 我面临同样的问题,我的搜索字符串为“???”和“+”。除了这两个,我都能找到。你能推荐一些相同的东西吗?【参考方案4】:

前段时间我遇到过这个问题,似乎最简单但不是最快的解决方案是在 XML 文档中添加一个新节点,该节点的属性值为“SomeValue”,然后使用简单的 xpath 搜索。操作完成后,您可以从 XML 文档中删除“临时节点”。

这样,整个比较发生在“内部”,因此您不必构造奇怪的 XPath 查询。

我似乎记得为了加快速度,您应该将 temp 值添加到根节点。

祝你好运……

【讨论】:

顺便说一句,这个解决方案也可能解决您的问题,这与您所做的几乎相同:***.com/questions/642125/… 你没看懂问题。【参考方案5】:

我将 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】:

这是 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://***.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) + ")";
    

【讨论】:

仅供参考,您的测试并没有通过他的所有测试。 更改添加到部件以引用字符串。 谢谢,不知道我是怎么错过的。固定的。 :-) 您好,您的代码比原来的代码好 1000 倍,但仍然比要求的要笨拙。而不是首先添加一个稍后删除的字符串,它会更容易: String[] split = value.Split('"'); for (int i=0; i0 ) 部分.Add("'\"'"); if (split[i].Length > 0) parts.Add('"' + split[i] + '"'); @Elmue 我想这是个人喜好问题。我发现这比删除最后一个字符串更笨重。当然,性能没有显着差异。实现它的另一种方法可能是在每个条目之前添加一个 '"' before,然后使用内联 LINQ 表达式而不是单独的 .Remove 语句:foreach (var str in value.Split('"')) parts.Add("'\"'"); if (!string.IsNullOrEmpty(str)) parts.Add('"' + str + '"'); return "concat(" + string.Join(",", parts.Skip(1)) + ")";【参考方案7】:

到目前为止,解决此问题的最佳方法是使用 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 as described in this MSDN article(您只需要此示例中与变量相关的部分,而不需要扩展函数)。

【讨论】:

迄今为止最好的方法。 +1【参考方案8】:

您可以通过在 XPath 表达式中使用 double quotes 而不是 single quotes 来解决此问题。

例如:

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

【讨论】:

【参考方案9】:

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

我没有对它进行广泛的测试,但似乎可以工作。

【讨论】:

【参考方案10】:

这里的大部分答案都集中在如何使用字符串操作来拼凑一个以有效方式使用字符串分隔符的 XPath。

我想说最好的做法是不要依赖这种复杂且可能很脆弱的方法。

以下内容适用于 .NET,因为此问题使用 C# 标记。当您在 Java 中使用 XPath 时,Ian Roberts 提供了我认为最好的解决方案。

现在,您可以使用 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 变量的方法。遗憾的是,开箱即用并不容易,但使用我在this other SO answer 中提供的一个简单的帮助类,这很容易。

你可以这样使用它:

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

【讨论】:

以上是关于XPath 查询中的撇号 (')的主要内容,如果未能解决你的问题,请参考以下文章

将 ' 转换为 PHP 中的撇号

忽略正则表达式中的撇号[重复]

忽略包含中的撇号

XML 字符串文件中的撇号

任何允许 Pivot 中的撇号 SQL 列别名的安全问题?

Discord:命令参数中的撇号在 C# 中不起作用