XPATH 查询
列出站点下的对象:ListObject[@Title='SomeValue']
。 SomeValue 是动态的。只要 SomeValue 没有单引号('),这个查询就能够工作。尝试使用转义序列,但是并没有起作用。我做错了什么?
这实际上很难做到。
看一下XPath Recommendation,你会发现它定义了字面量为:
Literal ::= '"' [^"]* '"'
| "'" [^']* "'"
也就是说,在XPath表达式中,字符串文字可以包含单引号或双引号,但不能同时包含两者。
你不能使用转义字符来解决这个问题。例如以下的文字:
'Some'Value'
将匹配此XML文本:
Some'Value
这意味着存在一些XML文本,你无法生成一个XPath文本来匹配它,例如:
<elm att=""&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();
}
function xpath_quote($str){ return 'concat(\''.implode('\', "\'", \'', explode('\'', $value)).'\')'; }
- 在大多数情况下生成效率较低、长度较长的xpath,但代码很短<strike>&甜美</strike>:https://3v4l.org/rDa1L - hanshenrik我将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;
}
编辑: 经过大量单元测试和查阅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("&", "&");
text.Replace("'", "'");
text.Replace(@"""", """);
text.Replace("<", "<");
text.Replace(">", ">");
return text.ToString();
}
我以前做过这个,它可以正常工作。如果对你不起作用,可能是因为有一些额外的上下文问题,你需要让我们知道。
System.Security.SecurityElement.Escape(value)
?(在C#中) - Flynn1179目前解决这个问题最好的方法是使用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
(你只需要使用此示例中与变量相关的部分,而不是扩展函数)。
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);
这里有一种替代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) + ")";
}
if (split[i].Length > 0)
parts.Add('"' + split[i] + '"');
} - Elmuelet quoteString (s : string) =
if not (s.Contains "'" ) then sprintf "'%s'" s
else if not (s.Contains "\"") then sprintf "\"%s\"" s
else "concat('" + s.Replace ("'", "', \"'\", '") + "')"
我非常喜欢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}");
}
}
}
如果在 SomeValue 中不会出现任何双引号,您可以使用转义的双引号来指定您在 XPath 搜索字符串中要搜索的值。
ListObject[@Title=\"SomeValue\"]
您可以通过在XPath表达式中使用双引号
而不是单引号
来解决此问题。
例如:
element.XPathSelectElements(String.Format("//group[@title=\"{0}\"]", "Man's"));