net - leer nodos xml c#
Crear Nodos XML basados en XPath? (12)
¿Alguien sabe de un medio existente para crear una jerarquía XML programáticamente a partir de una expresión XPath?
Por ejemplo, si tengo un fragmento de XML como, por ejemplo:
<feed>
<entry>
<data></data>
<content></content>
</entry>
</feed>
Dada la expresión XPath / feed / entry / content / @ source tendría:
<feed>
<entry>
<data></data>
<content @source=""></content>
</entry>
</feed>
Me doy cuenta de que esto es posible usando XSLT, pero debido a la naturaleza dinámica de lo que intento lograr, una transformación fija no funcionará.
Estoy trabajando en C #, pero si alguien tiene una solución usando otro idioma, por favor, toque.
¡Gracias por la ayuda!
Aquí está mi versión. Espero que esto también ayude a alguien.
public static void Main(string[] args)
{
XmlDocument doc = new XmlDocument();
XmlNode rootNode = GenerateXPathXmlElements(doc, "/RootNode/FirstChild/SecondChild/ThirdChild");
Console.Write(rootNode.OuterXml);
}
private static XmlDocument GenerateXPathXmlElements(XmlDocument xmlDocument, string xpath)
{
XmlNode parentNode = xmlDocument;
if (xmlDocument != null && !string.IsNullOrEmpty(xpath))
{
string[] partsOfXPath = xpath.Split(''/'');
string xPathSoFar = string.Empty;
foreach (string xPathElement in partsOfXPath)
{
if(string.IsNullOrEmpty(xPathElement))
continue;
xPathSoFar += "/" + xPathElement.Trim();
XmlNode childNode = xmlDocument.SelectSingleNode(xPathSoFar);
if(childNode == null)
{
childNode = xmlDocument.CreateElement(xPathElement);
}
parentNode.AppendChild(childNode);
parentNode = childNode;
}
}
return xmlDocument;
}
Aquí hay un RegEx mejorado basado en Mark Miller:
/([/w]+)(?:(?:[/[])(@|)([/w]+)(?:([!=<>]+)(?:(?:(?:'')([^'']+)(?:''))|([^'']+))|)(?:[]])|)|([.]+))
Group 1: Node name
Group 2: @ (or Empty, for non attributes)
Group 3: Attribute Key
Group 4: Attribute Value (if string)
Group 5: Attribute Value (if number)
Group 6: .. (dots, one or more)
En el ejemplo que presenta, lo único que se crea es el atributo ...
XmlElement element = (XmlElement)doc.SelectSingleNode("/feed/entry/content");
if (element != null)
element.SetAttribute("source", "");
Si lo que realmente quieres es poder crear la jerarquía donde no existe, entonces podrías hacer tu propio analizador simple de xpath. Sin embargo, no sé cómo mantener el atributo en xpath. Prefiero lanzar el nodo como elemento y fijarlo en .SetAttribute como lo he hecho aquí:
static private XmlNode makeXPath(XmlDocument doc, string xpath)
{
return makeXPath(doc, doc as XmlNode, xpath);
}
static private XmlNode makeXPath(XmlDocument doc, XmlNode parent, string xpath)
{
// grab the next node name in the xpath; or return parent if empty
string[] partsOfXPath = xpath.Trim(''/'').Split(''/'');
string nextNodeInXPath = partsOfXPath.First();
if (string.IsNullOrEmpty(nextNodeInXPath))
return parent;
// get or create the node from the name
XmlNode node = parent.SelectSingleNode(nextNodeInXPath);
if (node == null)
node = parent.AppendChild(doc.CreateElement(nextNodeInXPath));
// rejoin the remainder of the array as an xpath expression and recurse
string rest = String.Join("/", partsOfXPath.Skip(1).ToArray());
return makeXPath(doc, node, rest);
}
static void Main(string[] args)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml("<feed />");
makeXPath(doc, "/feed/entry/data");
XmlElement contentElement = (XmlElement)makeXPath(doc, "/feed/entry/content");
contentElement.SetAttribute("source", "");
Console.WriteLine(doc.OuterXml);
}
Esta es una versión mejorada de la solución Christian Peeters que admite espacios de nombres en la expresión xpath.
public static XNode CreateNodeFromXPath(XElement elem, string xpath)
{
// Create a new Regex object
Regex r = new Regex(@"/*([a-zA-Z0-9_/./-/:]+)(/[@([a-zA-Z0-9_/./-]+)=''([^'']*)''/])?|/@([a-zA-Z0-9_/./-]+)");
xpath = xpath.Replace("/"", "''");
// Find matches
Match m = r.Match(xpath);
XNode currentNode = elem;
StringBuilder currentPath = new StringBuilder();
XPathNavigator XNav = elem.CreateNavigator();
while (m.Success)
{
String currentXPath = m.Groups[0].Value; // "/ns:configuration" or "/appSettings" or "/add"
String NamespaceAndElementName = m.Groups[1].Value; // "ns:configuration" or "appSettings" or "add"
String filterName = m.Groups[3].Value; // "" or "key"
String filterValue = m.Groups[4].Value; // "" or "name"
String attributeName = m.Groups[5].Value; // "" or "value"
XNamespace nspace = "";
string elementName;
int p = NamespaceAndElementName.IndexOf('':'');
if (p >= 0)
{
string ns = NamespaceAndElementName.Substring(0, p);
elementName = NamespaceAndElementName.Substring(p + 1);
nspace = XNav.GetNamespace(ns);
}
else
elementName = NamespaceAndElementName;
StringBuilder builder = currentPath.Append(currentXPath);
String relativePath = builder.ToString();
XNode newNode = (XNode)elem.XPathSelectElement(relativePath, XNav);
if (newNode == null)
{
if (!string.IsNullOrEmpty(attributeName))
{
((XElement)currentNode).Attribute(attributeName).Value = "";
newNode = (XNode)elem.XPathEvaluate(relativePath, XNav);
}
else if (!string.IsNullOrEmpty(elementName))
{
XElement newElem = new XElement(nspace + elementName);
if (!string.IsNullOrEmpty(filterName))
{
newElem.Add(new XAttribute(filterName, filterValue));
}
((XElement)currentNode).Add(newElem);
newNode = newElem;
}
else
{
throw new FormatException("The given xPath is not supported " + relativePath);
}
}
currentNode = newNode;
m = m.NextMatch();
}
// Assure that the node is found or created
if (elem.XPathEvaluate(xpath, XNav) == null)
{
throw new FormatException("The given xPath cannot be created " + xpath);
}
return currentNode;
}
Este es mi truco rápido que también puede crear atributos siempre que use un formato como /configuration/appSettings/add[@key=''name'']/@value
.
static XmlNode createXPath(XmlDocument doc, string xpath)
{
XmlNode node=doc;
foreach (string part in xpath.Substring(1).Split(''/''))
{
XmlNodeList nodes=node.SelectNodes(part);
if (nodes.Count>1) throw new ComponentException("Xpath ''"+xpath+"'' was not found multiple times!");
else if (nodes.Count==1) { node=nodes[0]; continue; }
if (part.StartsWith("@"))
{
var anode=doc.CreateAttribute(part.Substring(1));
node.Attributes.Append(anode);
node=anode;
}
else
{
string elName, attrib=null;
if (part.Contains("["))
{
part.SplitOnce("[", out elName, out attrib);
if (!attrib.EndsWith("]")) throw new ComponentException("Unsupported XPath (missing ]): "+part);
attrib=attrib.Substring(0, attrib.Length-1);
}
else elName=part;
XmlNode next=doc.CreateElement(elName);
node.AppendChild(next);
node=next;
if (attrib!=null)
{
if (!attrib.StartsWith("@")) throw new ComponentException("Unsupported XPath attrib (missing @): "+part);
string name, value;
attrib.Substring(1).SplitOnce("=''", out name, out value);
if (string.IsNullOrEmpty(value) || !value.EndsWith("''")) throw new ComponentException("Unsupported XPath attrib: "+part);
value=value.Substring(0, value.Length-1);
var anode=doc.CreateAttribute(name);
anode.Value=value;
node.Attributes.Append(anode);
}
}
}
return node;
}
SplitOnce es un método de extensión:
public static void SplitOnce(this string value, string separator, out string part1, out string part2)
{
if (value!=null)
{
int idx=value.IndexOf(separator);
if (idx>=0)
{
part1=value.Substring(0, idx);
part2=value.Substring(idx+separator.Length);
}
else
{
part1=value;
part2=null;
}
}
else
{
part1="";
part2=null;
}
}
Muestra:
public static void Set(XmlDocument doc, string xpath, string value)
{
if (doc==null) throw new ArgumentNullException("doc");
if (string.IsNullOrEmpty(xpath)) throw new ArgumentNullException("xpath");
XmlNodeList nodes=doc.SelectNodes(xpath);
if (nodes.Count>1) throw new ComponentException("Xpath ''"+xpath+"'' was not found multiple times!");
else if (nodes.Count==0) createXPath(doc, xpath).InnerText=value;
else nodes[0].InnerText=value;
}
p.ej
Set(doc, "/configuration/appSettings/add[@key=''Server'']/@value", "foobar");
La versión C # de Mark Miller
/// <summary>
/// Makes the X path. Use a format like //configuration/appSettings/add[@key=''name'']/@value
/// </summary>
/// <param name="doc">The doc.</param>
/// <param name="xpath">The xpath.</param>
/// <returns></returns>
public static XmlNode createNodeFromXPath(XmlDocument doc, string xpath)
{
// Create a new Regex object
Regex r = new Regex(@"/+([/w]+)(/[@([/w]+)=''([^'']*)''/])?|/@([/w]+)");
// Find matches
Match m = r.Match(xpath);
XmlNode currentNode = doc.FirstChild;
StringBuilder currentPath = new StringBuilder();
while (m.Success)
{
String currentXPath = m.Groups[0].Value; // "/configuration" or "/appSettings" or "/add"
String elementName = m.Groups[1].Value; // "configuration" or "appSettings" or "add"
String filterName = m.Groups[3].Value; // "" or "key"
String filterValue = m.Groups[4].Value; // "" or "name"
String attributeName = m.Groups[5].Value; // "" or "value"
StringBuilder builder = currentPath.Append(currentXPath);
String relativePath = builder.ToString();
XmlNode newNode = doc.SelectSingleNode(relativePath);
if (newNode == null)
{
if (!string.IsNullOrEmpty(attributeName))
{
((XmlElement)currentNode).SetAttribute(attributeName, "");
newNode = doc.SelectSingleNode(relativePath);
}
else if (!string.IsNullOrEmpty(elementName))
{
XmlElement element = doc.CreateElement(elementName);
if (!string.IsNullOrEmpty(filterName))
{
element.SetAttribute(filterName, filterValue);
}
currentNode.AppendChild(element);
newNode = element;
}
else
{
throw new FormatException("The given xPath is not supported " + relativePath);
}
}
currentNode = newNode;
m = m.NextMatch();
}
// Assure that the node is found or created
if (doc.SelectSingleNode(xpath) == null)
{
throw new FormatException("The given xPath cannot be created " + xpath);
}
return currentNode;
}
Me gustó la versión de Chris porque manejaba atributos en xpaths y las otras soluciones no (aunque no maneja "text ()" en la ruta que he corregido). Desafortunadamente tuve que usar esto en una aplicación VB, así que aquí está la conversión para eso:
Private Sub SplitOnce(ByVal value As String, ByVal separator As String, ByRef part1 As String, ByRef part2 As String)
If (value IsNot Nothing) Then
Dim idx As Integer = value.IndexOf(separator)
If (idx >= 0) Then
part1 = value.Substring(0, idx)
part2 = value.Substring(idx + separator.Length)
Else
part1 = value
part2 = Nothing
End If
Else
part1 = ""
part2 = Nothing
End If
End Sub
Private Function createXPath(ByVal doc As XmlDocument, ByVal xpath As String) As XmlNode
Dim node As XmlNode = doc
Dim part As String
For Each part In xpath.Substring(1).Split("/")
Dim nodes As XmlNodeList = node.SelectNodes(part)
If (nodes.Count > 1) Then
Throw New Exception("Xpath ''" + xpath + "'' was not found multiple times!")
ElseIf (nodes.Count = 1) Then
node = nodes(0)
Continue For
End If
If (part.EndsWith("text()")) Then
'' treat this the same as previous node since this is really innertext
Exit For
ElseIf (part.StartsWith("@")) Then
Dim anode As XmlAttribute = doc.CreateAttribute(part.Substring(1))
node.Attributes.Append(anode)
node = anode
Else
Dim elName As String = Nothing
Dim attrib As String = Nothing
If (part.Contains("[")) Then
SplitOnce(part, "[", elName, attrib)
If (Not attrib.EndsWith("]")) Then
Throw New Exception("Unsupported XPath (missing ]): " + part)
End If
attrib = attrib.Substring(0, attrib.Length - 1)
Else
elName = part
End If
Dim nextnode As XmlNode = doc.CreateElement(elName)
node.AppendChild(nextnode)
node = nextnode
If (attrib IsNot Nothing) Then
If (Not attrib.StartsWith("@")) Then
Throw New Exception("Unsupported XPath attrib (missing @): " + part)
End If
Dim name As String = ""
Dim value As String = ""
SplitOnce(attrib.Substring(1), "=''", name, value)
If (String.IsNullOrEmpty(value) Or Not value.EndsWith("''")) Then
Throw New Exception("Unsupported XPath attrib: " + part)
End If
value = value.Substring(0, value.Length - 1)
Dim anode As XmlAttribute = doc.CreateAttribute(name)
anode.Value = value
node.Attributes.Append(anode)
End If
End If
Next
Return node
End Function
Necesitaba un XNode en lugar de una implementación de XmlNode, y el RegEx no me funcionaba (porque los nombres de los elementos con o - no funcionan)
Entonces, esto es lo que funcionó para mí:
public static XNode createNodeFromXPath(XElement elem, string xpath)
{
// Create a new Regex object
Regex r = new Regex(@"/*([a-zA-Z0-9_/./-]+)(/[@([a-zA-Z0-9_/./-]+)=''([^'']*)''/])?|/@([a-zA-Z0-9_/./-]+)");
xpath = xpath.Replace("/"", "''");
// Find matches
Match m = r.Match(xpath);
XNode currentNode = elem;
StringBuilder currentPath = new StringBuilder();
while (m.Success)
{
String currentXPath = m.Groups[0].Value; // "/configuration" or "/appSettings" or "/add"
String elementName = m.Groups[1].Value; // "configuration" or "appSettings" or "add"
String filterName = m.Groups[3].Value; // "" or "key"
String filterValue = m.Groups[4].Value; // "" or "name"
String attributeName = m.Groups[5].Value; // "" or "value"
StringBuilder builder = currentPath.Append(currentXPath);
String relativePath = builder.ToString();
XNode newNode = (XNode)elem.XPathSelectElement(relativePath);
if (newNode == null)
{
if (!string.IsNullOrEmpty(attributeName))
{
((XElement)currentNode).Attribute(attributeName).Value = "";
newNode = (XNode)elem.XPathEvaluate(relativePath);
}
else if (!string.IsNullOrEmpty(elementName))
{
XElement newElem = new XElement(elementName);
if (!string.IsNullOrEmpty(filterName))
{
newElem.Add(new XAttribute(filterName, filterValue));
}
((XElement)currentNode).Add(newElem);
newNode = newElem;
}
else
{
throw new FormatException("The given xPath is not supported " + relativePath);
}
}
currentNode = newNode;
m = m.NextMatch();
}
// Assure that the node is found or created
if (elem.XPathEvaluate(xpath) == null)
{
throw new FormatException("The given xPath cannot be created " + xpath);
}
return currentNode;
}
Sé que este es un hilo muy viejo ... pero he estado intentando lo mismo y se me ocurrió la siguiente expresión regular, que no es perfecta, pero encuentro que es más genérica.
/+([/w]+)(/[@([/w]+)=''([^'']*)''/])?|/@([/w]+)
El valor string / configuration / appSettings / add [@ key = ''name''] / @
debe ser analizado para
Encontrado 14 partido (s):
start = 0, end = 14 Group (0) = / configuration Group (1) = configuración Group (2) = null Group (3) = null Group (4) = null Group (5) = null
start = 14, end = 26 Group (0) = / appSettings Group (1) = appSettings Group (2) = null Group (3) = null Group (4) = null Group (5) = null
start = 26, end = 43 Group (0) = / add [@ key = ''name''] Group (1) = add Grupo (2) = [@ clave = ''nombre''] Grupo (3) = clave Grupo (4 ) = nombre Grupo (5) = nulo
start = 43, end = 50 Group (0) = / @ value Group (1) = null Group (2) = null Group (3) = null Group (4) = null Group (5) = value
Lo que significa que tenemos
Grupo (0) = Grupo ignorado (1) = El nombre del elemento Grupo (2) = Grupo ignorado (3) = Nombre del atributo del filtro Grupo (4) = Valor del atributo del filtro
Aquí hay un método de Java que puede usar el patrón
public static Node createNodeFromXPath(Document doc, String expression) throws XPathExpressionException {
StringBuilder currentPath = new StringBuilder();
Matcher matcher = xpathParserPattern.matcher(expression);
Node currentNode = doc.getFirstChild();
while (matcher.find()) {
String currentXPath = matcher.group(0);
String elementName = matcher.group(1);
String filterName = matcher.group(3);
String filterValue = matcher.group(4);
String attributeName = matcher.group(5);
StringBuilder builder = currentPath.append(currentXPath);
String relativePath = builder.toString();
Node newNode = selectSingleNode(doc, relativePath);
if (newNode == null) {
if (attributeName != null) {
((Element) currentNode).setAttribute(attributeName, "");
newNode = selectSingleNode(doc, relativePath);
} else if (elementName != null) {
Element element = doc.createElement(elementName);
if (filterName != null) {
element.setAttribute(filterName, filterValue);
}
currentNode.appendChild(element);
newNode = element;
} else {
throw new UnsupportedOperationException("The given xPath is not supported " + relativePath);
}
}
currentNode = newNode;
}
if (selectSingleNode(doc, expression) == null) {
throw new IllegalArgumentException("The given xPath cannot be created " + expression);
}
return currentNode;
}
Si la cadena XPath se procesa de atrás hacia adelante, es más fácil procesar XPaths no rooteados, por ej. // a / b / c ... También debería soportar la sintaxis de XPath de Gordon aunque no lo he intentado ...
static private XmlNode makeXPath(XmlDocument doc, string xpath)
{
string[] partsOfXPath = xpath.Split(''/'');
XmlNode node = null;
for (int xpathPos = partsOfXPath.Length; xpathPos > 0; xpathPos--)
{
string subXpath = string.Join("/", partsOfXPath, 0, xpathPos);
node = doc.SelectSingleNode(subXpath);
if (node != null)
{
// append new descendants
for (int newXpathPos = xpathPos; newXpathPos < partsOfXPath.Length; newXpathPos++)
{
node = node.AppendChild(doc.CreateElement(partsOfXPath[newXpathPos]));
}
break;
}
}
return node;
}
Un problema con esta idea es que xpath "destruye" la información.
Hay un número infinito de árboles xml que pueden coincidir con muchos xpaths. Ahora, en algunos casos, al igual que en el ejemplo que das, existe un árbol xml mínimo obvio que coincide con tu xpath, donde tienes un predicado que usa "=".
Pero, por ejemplo, si el predicado no utiliza igual o cualquier otro operador aritmético que no sea igual, existe un número infinito de posibilidades. Podría tratar de elegir un árbol xml "canónico" que requiera, por ejemplo, la menor cantidad de bits para representar.
Supongamos, por ejemplo, que tiene xpath /feed/entry/content[@source > 0]
. Ahora cualquier árbol xml de la estructura apropiada en la que el contenido del nodo tuviera un origen de atributo cuyo valor fuera> 0 coincidiría, pero hay un número infinito de números mayor que cero. Al elegir el valor "mínimo", presumiblemente 1, podría intentar canonicalizar su xml.
Los predicados de Xpath pueden contener expresiones aritméticas bastante arbitrarias, por lo que la solución general a esto es bastante difícil, si no imposible. Se podría imaginar una ecuación enorme allí, y tendría que resolverse a la inversa para encontrar valores que coincidieran con la ecuación; pero dado que puede haber un número infinito de valores coincidentes (siempre que en realidad sea una desigualdad, no una ecuación), sería necesario encontrar una solución canónica.
Muchas expresiones de otras formas también destruyen la información. Por ejemplo, un operador como "o" siempre destruye la información. Si sabes que (X or Y) == 1
, no sabes si X es 1, Y es 1, o ambos son 1; ¡todo lo que sabes con certeza es que uno de ellos es 1! Por lo tanto, si tiene una expresión que usa O, no puede decir cuál de los nodos o valores que son entradas para el OR debe ser 1 (puede hacer una elección arbitraria y establecer ambos 1, ya que eso satisfará la expresión, como lo hará las dos opciones en las que solo una de ellas es 1).
Ahora supongamos que hay varias expresiones en xpath que se refieren al mismo conjunto de valores. Luego terminas con un sistema de ecuaciones o desigualdades simultáneas que pueden ser virtualmente imposibles de resolver. Nuevamente, si restringe el xpath permitido a un pequeño subconjunto de su potencia máxima, puede resolver este problema. Sospecho que el caso completamente general es similar al problema de detención de Turing, sin embargo; en este caso, dado un programa arbitrario (el xpath), descubra un conjunto de datos consistentes que coincida con el programa, y en algún sentido es mínimo.
- Para XDocument
- Admite la creación de atributos
Utilizar
var xDoc = new XDocument(new XElement("root",
new XElement("child1"),
new XElement("child2")));
CreateElement(xDoc, "/root/child3");
CreateElement(xDoc, "/root/child4[@year=32][@month=44]");
CreateElement(xDoc, "/root/child4[@year=32][@month=44]/subchild1");
CreateElement(xDoc, "/root/child4[@year=32][@month=44]/subchild1/subchild[@name=''jon'']");
CreateElement(xDoc, "/root/child1");
definir
public static XDocument CreateElement(XDocument document, string xpath)
{
if (string.IsNullOrEmpty(xpath))
throw new InvalidOperationException("Xpath must not be empty");
var xNodes = Regex.Matches(xpath, @"//[^//]+").Cast<Match>().Select(it => it.Value).ToList();
if (!xNodes.Any())
throw new InvalidOperationException("Invalid xPath");
var parent = document.Root;
var currentNodeXPath = "";
foreach (var xNode in xNodes)
{
currentNodeXPath += xNode;
var nodeName = Regex.Match(xNode, @"(?<=//)[^/[]+").Value;
var existingNode = parent.XPathSelectElement(currentNodeXPath);
if (existingNode != null)
{
parent = existingNode;
continue;
}
var attributeNames =
Regex.Matches(xNode, @"(?<=@)([^=]+)/=([^]]+)")
.Cast<Match>()
.Select(it =>
{
var groups = it.Groups.Cast<Group>().ToList();
return new { AttributeName = groups[1].Value, AttributeValue = groups[2].Value };
});
parent.Add(new XElement(nodeName, attributeNames.Select(it => new XAttribute(it.AttributeName, it.AttributeValue)).ToArray()));
parent = parent.Descendants().Last();
}
return document;
}