web-dev-qa-db-ja.com

XPATHクエリの特殊文字

私は以下を使用しますXPATH Queryサイトの下のオブジェクトをリストします。 ListObject[@Title='SomeValue']。 SomeValueは動的です。このクエリは、SomeValueにアポストロフィ( ')がない限り機能します。エスケープシーケンスも使用してみました。うまくいきませんでした。

何が悪いのですか?

41
Prabhu

これは驚くほど難しいことです。

XPath Recommendation を見てください。リテラルが次のように定義されていることがわかります。

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

つまり、XPath式の文字列リテラルには、アポストロフィまたは二重引用符を含めることができますが、両方を含めることはできません。

これを回避するためにエスケープを使用することはできません。このようなリテラル:

'Some'Value'

このXMLテキストに一致します:

Some'Value

これは、一致するXPathリテラルを生成できないXMLテキストの一部が存在する可能性があることを意味します。例:

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

そして、はい、すべてのEdgeケースでテストしました。これが、ロジックが非常に複雑になっている理由です。

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

EDIT:重い単体テストセッションの後で 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("&", "&amp;");
    text.Replace("'", "&apos;");
    text.Replace(@"""", "&quot;");
    text.Replace("<", "&lt;");
    text.Replace(">", "&gt;");
    return text.ToString();
}

私はこれを以前にやったことがあり、うまくいきます。それがうまくいかない場合は、問題について、追加の状況を把握しておく必要があります。

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
Cody S

この問題への最善のアプローチは、XPathライブラリが提供する機能を使用して、式で参照できるXPathレベルの変数を宣言することです。変数の値は、ホストプログラミング言語の任意の文字列にすることができ、XPath文字列リテラルの制限を受けません。たとえば、Java with 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の記事で説明されているように (この例の変数に関連する部分だけが必要で、拡張機能は必要ありません)機能)。

5
Ian Roberts

ここでの回答のほとんどは、文字列操作を使用して、有効な方法で文字列区切り文字を使用するXPathをまとめる方法に焦点を当てています。

ベストプラクティスは、そのような複雑で潜在的に壊れやすいメソッドに依存しないことです。

この質問はC#でタグ付けされているため、以下は.NETに適用されます。 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変数を使用する方法も提供します。悲しいことに、これを最初から行うのは簡単ではありませんが、私が提供する単純なヘルパークラス 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);
3
JLRishe

以下は、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) + ")";
    }
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 ("'", "', \"'\", '") + "')"

私はそれを広範囲にテストしていませんが、動作するようです。

2
Fortune

SomeValueに二重引用符を含めない場合は、エスケープされた二重引用符を使用して、XPath検索文字列で検索する値を指定できます。

ListObject[@Title=\"SomeValue\"]
0
48klocs

この問題は、XPath式でdouble quotesの代わりにsingle quotesを使用して修正できます。

例:

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