-
Notifications
You must be signed in to change notification settings - Fork 27
XPath: Functions with Strings
We expect XPath functions to look like this: functionName(), but we're aware that they can sit in different places in an XPath expression depending on what you're trying to find out, and whether or not the function is expected to process multiple nodes (walking the XML tree and lots of nodes and calculating a count() or distinct-values() for example). Some functions, like name() and normalize-space() are only allowed to process one node at a time and so we append them at the end of an XPath, with nothing inside. These functions are written in different ways for different purposes!
The "things" that an XPath function is designed to process are called arguments, and some functions take just one. The distinct-values() function just takes a single argument, which is an XPath expression that walks the tree and collects all the nodes you designate: distinct-values(//body//persName) has one argument, and the argument portion is //body//persName. The name() and normalize-space() that have to land on each node and process it independently--those effectively take a single argument, too, and that argument is the individual node you've landed on to process.
The functions we're introducing today take MULTIPLE arguments, so this will help call your attention to special syntax required for processing. When you look up a new XPath function to use, you want to pay attention to how many arguments it takes and what order those arguments are expected to have.
We're introducing some functions that help you locate pieces of strings inside XML tree nodes. Rather than evaluate whether a whole node is = or eq to a string using a comparison operator, the XPath functions contains() and matches() can help you locate a portion or "snip" of something in your XML nodes. The functions contains() and matches() are like twins--they're structured just the same, only matches() lets you hunt for regular expression patterns, while contains() is limited to searching for literal strings.
Both of these have to process one node at a time, and they take TWO arguments, which we think of as "haystack" and "needle" (because we use them when we are trying to "find a needle in a haystack"):
contains(XPath_node, "literal_string")
matches(XPath_node, "regexPattern")
The XPath node is your haystack, and the string is the "needle" you're hunting for. The arguments have to come in this order for XPath to understand first where it's looking and then what it's trying to find.
With the Forster file open, let's take a look at some of the matches() expression. You can tell your friends that in DH class, we like to play with matches()! ;-)
//p[matches(., '[0-9]')]
(We get 589 items, and notice that these match all the way deep down, into descendant elements!)
//p[matches(., '[0-9]$')] (Now that we put a `$` at the end of the regex expression, we get 33 results here. Look at the results and let's talk about the regex `^` and `$` here and what they mean in the XPath context! When we were working with plain text files, these regex symbols referred to the start or end of a line of text, but here in XPath they more precisely refer to the start or end of an XML node.)
Notice the use of the dot . in the above expressions. The dot is our indicator of the self:: axis, where the XPath expression has taken us right now, and we typically use it in XPath expressions that need to process each node one by one.
Notice that matches() does a "deep dive" and if there's ANY match that's a deep descendant of the node you're checking, it'll return its match. But sometimes you have a general sense of where to look, but you want to find out more specific information about where that information turns up.
What if we knew we wanted to find a sequence of two or more numbers, and we want to a) try to find it only when it is in the text() node child of the body paragraph (and no deeper than that)? We'd need to specify that we want to search in the text() node, remembering that this is sitting in a child relationship (on the child:: axis) just below the <p>:
//p/text()[matches(., '[0-9]+')]
Okay, but what if ONLY know this string of one or more numbers could appear somewhere inside the chapter div elements? We still want to find out WHERE it is, though. Let's take this in stages:
First, let's find the text() node that holds our match!
//div[@type="chapter"]//*/text()[matches(., '[0-9]+')]
Now, let's take a look up at the parent:: element, whatever it is!
//div[@type="chapter"]//*/text()[matches(., '[0-9]+')]/parent::*
And we can even ask its name:
//div[@type="chapter"]//*/text()[matches(., '[0-9]+')]/parent::*/name()
And say you wanted to get distinct values of those element names, for those elements that hold digits in their text node children:
distinct-values(//div[@type="chapter"]//*/text()[matches(., '[0-9]+')]/parent::*/name())
We've built a pretty complex expression now, but we did it by building up in small stages.
These take two arguments, the same haystack-and-needle configuration we discussed above, only these return a piece of string before or after something you designate. substring-after() can be used to return a bit of text after a # that you know is always in front of it, for example.
The translate() function actually takes three arguments! That's because it's used to make micro-edits in a string, which we might think of as "string surgery"! Let's say (for some odd reason) you wanted to remove the letter "e" from each of the persNames in the teiHeader of the Forster file, and replace it with a % sign?
//teiHeader//persName/translate(., 'e', '%')
This will also do things like remove and replace letters out of sequence with one another. See the example on The XPath Functions we use most.
Splicing strings together from different nodes: concat() strings that are equivalent: in a one-to-one relationship
The concat() function takes the text contents of nodes and splices them together. It can take two or more arguments, but those arguments need to be expressed in a one-to-one relationship with each other. We typically concatenate strings from the point of view of one XPath navigation down the tree. So, let's say we want to connect the @type from each <div> element in the Forster file with the text of the <head> element that is immediately below it. These exist in a one-to-one relationship, so I can always expect to find one @xml:id and one <head> element in every <div> in the Forster file. So can do a concat() function to join their strings together like so:
//div[@type]/concat(./@type, ./head)
Let's take a close look at what I did: First I went down the tree to find the div elements with an @type attribute. Then, one by one, I asked the concatenate function to stop on each one and output the value of @type together with the contents of <head>. See how that works? concat() will give you errors if you try to concatenate one thing with multiple things.
We use the string-join() function in a more macro way, to take all of a collection of nodes and glue them together in a single string, with a separator like a comma and a little white space. This is a string function that we run in a global way so that it walks the whole tree, collects its results, and stitches them together using a text separator that we designate. It's how we can output a comma-separated listing of the results of a node.
Example: Let's make a string-joined list of the persName elements in the teiHeader of our Forster file. Our separator will be a comma and a white space. The first argument is all the nodes we want to process, and the second is the little separator we'll place in between each one.
string-join(//teiHeader//persName/normalize-space(), ', ')
Just for fun, you can play around with your separators!
string-join(//teiHeader//persName/normalize-space(), '?! ')