XML Abfragen

  Ralf Hersel   Lesezeit: 13 Minuten  🗪 12 Kommentare Auf Mastodon ansehen

Es gibt viele Möglichkeiten, um XML-Dateien auszulesen und abzufragen. Hier findet ihr eine davon.

xml abfragen

Das ist die Art von Artikeln, die ich sehr gerne schreibe. Oft entsteht die Idee dazu, weil mir nichts Besseres einfällt. Dann greife ich gerne auf eigene Herausforderungen oder Anwendungsfälle zurück. Eigentlich wollte ich die nächste Podcastfolge mit Joël über die Alpha 5 des COSMIC-Desktops aufnehmen. Doch dann fiel mir auf, dass wir bereits im Juli 2024 in CIW095 über COSMIC gesprochen haben. Wir haben uns dann schnell auf ein anderes Thema geeinigt; ihr werdet es am Mittwoch in CIW121 hören.

Bei den vielen Artikeln und Podcastfolgen wird es immer schwerer, den Überblick zu behalten. Über was haben wir schon geschrieben? Worüber haben wir bereits gesprochen? Und so ist die Idee für diesen Artikel entstanden. Ich brauche eine Liste aller Podcastfolgen, in der ich schnell nachschauen kann, was wir wann behandelt haben. Während ich diese Einleitung schreibe, habe ich noch keine Ahnung, wie die Lösung aussehen wird.

Wie könnte es gehen?

Zuerst wollte ich ein kleines Python-Skript schreiben, welches sich per FTP mit unserem Server verbindet und dort das Artikelverzeichnis ausliest. Daraus hätte ich die Podcastfolgen extrahiert und die gewünschten Daten aus den Shownote-Artikeln geparst. Unser CMS (Bludit) legt für jeden Artikel ein Verzeichnis an, in dem sich eine Datei mit dem Inhalt befindet.

Dann kam mir in den Sinn, dass wir XML-Dateien für die RSS-Feeder haben. Einen für alle Artikel und einen anderen für den Podcast. Auf unserer Webseite findet man beide ganz unten auf der Seite beim RSS- und dem Mikrofon-Symbol. Bei der Verwendung des Podcast-XMLs würde ich mir das Filtern auf Podcastartikel sparen. Also habe ich diese Datei lokal heruntergeladen; einen Ausschnitt seht ihr im Titelbild.

Mit XML umgehen

Nun stellt sich die Frage, welche Inhalte der XML-Datei ich für meine Liste brauche. Ein Podcast-Eintrag aus der Datei sieht so aus:

<item>
    <title>CIW119 - Ditana</title>
    <link>https://gnulinux.ch/ciw119-podcast</link>
    <description><![CDATA[Wir werfen einen Blick auf die neue Linux Distribution Ditana]]></description>
    <content:encoded><![CDATA[<h2>CIW - Folge 119 - 15.01.2025 - Ditana</h2><ul><li>Wir begrüssen alle Distro-Hopper zur Folge 119 von ...</li></ul>]]></content:encoded>
    <author>GNU/Linux.ch</author>
    <pubDate>Wed, 15 Jan 2025 11:45:35 +0000</pubDate>
    <guid>https://gnulinux.ch/ciw119-podcast</guid>
    <enclosure url="https://gnulinux.ch/podcast/CIW119.mp3" length="97219675" type="audio/mpeg" />
    <itunes:summary>Wir werfen einen Blick auf die neue Linux Distribution Ditana</itunes:summary>
    <itunes:author>GNU/Linux.ch</itunes:author>
    <itunes:duration>4856</itunes:duration>
    <itunes:explicit>no</itunes:explicit>
    <itunes:title>CIW119 - Ditana</itunes:title>
</item>

Die Tags <title>, <pubDate> und <itunes:summary> würden mir genügen. Doch wie geht das? Nach kurzer Suche im Internet bin ich auf das CLI-Werkzeug xmllint gestossen. Dabei handelt es sich um einen XML-Linter. Ein Linter ist ein Werkzeug zur statischen Code-Analyse. Xmllint kann XML formatieren, validieren und via Xpath abfragen. So wie ich das einschätze, ist xmllint bei den meisten Distributionen vorinstalliert.

Schritt für Schritt

Im ersten Schritt habe ich den Titel aus der RSS-XML-Datei abgefragt. Das geht so:

xmllint --xpath '//item/title' gnulinux_newscast_rss.xml

<title>CIW120 - Asocial Media Flucht</title>
<title>CIW119 - Ditana</title>
<title>CIW118 - Happy GNU Year</title>
<title>CIW117 - Steuererklärung</title>
...

Den Xpath kann man absolut (/rss/channel/item/title) oder relativ (//item/title) angeben, was nichts am Ergebnis ändert. Dummerweise ist der Text vom Title-Tag umrandet. Zwar gibt es bei xmllint dieses Kommando, welches die Tags entfernt:

xmllint --xpath 'string(//item/title)' xml-Datei

… was leider nur die erste Instanz zurückgibt. Daher habe ich die Ausgabe in eine Datei geschrieben:

xmllint --xpath '//item/title' gnulinux_newscast_rss.xml > title.txt 

Um die Titel-Klammer zu entfernen, kam der Stream-Editor sed zum Einsatz; einmal für das vordere Tag und noch einmal für den hinteren Teil. Beim vorderen <title> geht das so:

sed -i 's/<title>//g' title.txt

Der Parameter -i sorgt dafür, dass das Ersetzen direkt in der angegebenen Datei title.txt stattfindet. Mit dem s sagt man, dass etwas ersetzt werden soll. Die Slashes sind Trennzeichen zwischen den Befehlsparametern. Dann gibt man an, was ersetzt werden soll (<title>) und wodurch es ersetzt werden soll (//), also gar nichts. Zum Schluss sagt man mit dem g, dass sich die Ersetzung auf alle Vorkommnisse beziehen soll (global). Danach sieht die Liste so aus:

CIW120 - Asocial Media Flucht</title>
CIW119 - Ditana</title>
CIW118 - Happy GNU Year</title>
...

Der vordere Tag wurde entfernt. Nun könnte man denken, dass sich der hintere Tag genauso einfach entfernen lässt. Theoretisch ja, praktisch steht jedoch der Slash als Trennzeichen im Weg. Der Befehl:

sed -i 's/</title>//g' title.txt

… funktioniert nicht, weil der Slash vor title zur Verwirrung bei sed führt. Ist das jetzt ein Trennzeichen, oder soll das ersetzt werden? Um das zu lösen, gibt es verschiedene Möglichkeiten, mit denen ich euch nicht langweilen möchte. Am einfachsten sagt ihr sed, dass ein anderes Trennzeichen verwendet werden soll, z. B. ein Semikolon:

sed -i 's;</title>;;g' title.txt

Nun sieht das Ergebnis so aus:

CIW120 - Asocial Media Flucht
CIW119 - Ditana
CIW118 - Happy GNU Year
...

… und das ist, was ich haben wollte. Um das Datum herauszufischen, mache ich das Gleiche in Grün:

xmllint --xpath '//item/pubDate' gnulinux_newscast_rss.xml > date.txt
sed -i 's/<pubDate>//g' date.txt
sed -i 's;</pubDate>;;g' date.txt

Doch beim Auslesen der Summary ergeben sich unerwartete Schwierigkeiten:

xmllint --xpath '//item/itunes:summary' gnulinux_newscast_rss.xml > summary.txt

XPath error : Undefined namespace prefix
XPath evaluation failure

Da bockt xmllint wegen des Namespaces in itunes:summary. Man kann in xmllint den Namespace angeben; leider habe ich nicht herausgefunden, wie man das macht. Das ist eine Frage an die Kommentatoren. Daher belasse ich es bei den beiden Feldern title und pubDate. Diese Listen stehen jetzt in den beiden Dateien title.txt und date.txt.

Zusammenführen

Jetzt stellt sich die Frage, wie man diese beiden Dateien zeilenweise zusammenführt. Dafür gibt es eine naheliegende Antwort: Ich kopiere beide Dateien als Spalten in LibreOffice Calc:

Doch geht das auch im Terminal? Nichts einfacher als das:

paste date.txt title.txt > ciw_folgen.txt

Wed, 22 Jan 2025 11:34:04 +0000 CIW120 - Asocial Media Flucht
Wed, 15 Jan 2025 11:45:35 +0000 CIW119 - Ditana
Wed, 08 Jan 2025 11:33:57 +0000 CIW118 - Happy GNU Year
Wed, 18 Dec 2024 11:31:36 +0000 CIW117 - Steuererklärung
Wed, 11 Dec 2024 11:32:34 +0000 CIW116 - Aufmerksamkeitsökonomie

Das Datum könnte man noch auf 22 Jan 2025 reduzieren. Auch das geht mit einem Befehl im Terminal:

cut -b 6-16 date.txt > date_cut.txt

Mit cut schneide ich den Text von Position 6 bis 16 aus und schreibe das Ergebnis in die Datei date_cut.txt.

Zusammenfassung

Was ich hier mit vielen Worten beschrieben habe, lässt sich in einem Shell-Skript vereinen. Das manuelle Herunterladen der XML-Datei entfällt; das Skript erledigt das mit dem wget Befehl.

#!/bin/bash
# Create a list of podcast episodes with date and title

wget -q https://gnulinux.ch/podcast/gnulinux_newscast_rss.xml -O rss.xml

xmllint --xpath '//item/title' rss.xml > title.txt 
sed -i 's/<title>//g' title.txt
sed -i 's;</title>;;g' title.txt

xmllint --xpath '//item/pubDate' rss.xml > date.txt
sed -i 's/<pubDate>//g' date.txt
sed -i 's;</pubDate>;;g' date.txt

cut -b 6-16 date.txt > date_cut.txt
paste date_cut.txt title.txt > ciw_folgen.txt
echo 'Suche in: ciw_folgen.txt'

Das Ergebnis in der Datei ciw_folgen.txt sieht so aus:

22 Jan 2025  CIW120 - Asocial Media Flucht
15 Jan 2025 CIW119 - Ditana
08 Jan 2025 CIW118 - Happy GNU Year
18 Dec 2024 CIW117 - Steuererklärung
...

Es geht noch einfacher

Falls ihr euch erinnert, war die Frage, ob ein Thema (z. B.: COSMIC) in einer Podcastfolge bereits behandelt wurde. Obwohl die Ausgabe nicht schön ist, beantwortet dieser Einzeiler die Frage ebenfalls:

curl -s https://gnulinux.ch/podcast/gnulinux_newscast_rss.xml | grep cosmic

Statt dem Gebastel mit xmllint, sed, cut und paste, holt curl die XML-Datei im Silent-Modus und lässt die Suche mit grep darauf los. Vorteil: die vollständigen Shownotes werden durchsucht. Nachteil: das Ergebnis ist unübersichtlich.

Fazit

Es hält sich der hartnäckige Mythos, dass man für Linux die Kommandozeile bedienen und beherrschen muss. Dieser Mythos ist negativ belegt. Fakt ist, dass niemand die Kommandozeile bedienen muss, wenn er oder sie es nicht möchte.

Die Wahrheit ist: Wer das Terminal für sich entdeckt, hat Freude daran und erledigt viele Aufgaben in Null-Komma-Nichts. Traut euch, die Kommandozeile für euch zu entdecken. Die Community hilft gerne dabei.

Die oben beschriebene Aufgabenstellung ist ein anschauliches Beispiel dafür. Nun kann ich eine aktuelle Podcast-Liste in ein paar Sekunden erstellen und weiss immer, welche Themen wir schon besprochen haben. Ohne das Terminal würde ich viel länger brauchen, um diese Liste zu erzeugen.

Titelbild: selbst erstellt

Quellen: stehen im Text

Tags

XML, Query, Abfrage, Xpath, Xmllint

Claudia
Geschrieben von Claudia am 27. Januar 2025 um 12:06

Ich würde es folgender maßen formatieren:

awk -F '' '// {split($2, parts, ˝ ˝); printf ˝;%s %s %s\n˝, parts[2], parts[3], parts[4]} // {printf ˝%s˝, $2}' rss.xml | tr -s ˝ ˝ > ciw_folgen.txt

Zur Erklärung:

  • -F ' : Filtert nach Tags oder
  • Wenn eine Zeile mit // auftaucht, dann split(content, variable, delimiter), wer schonmal C Programmiert hat kennt printf, um eine neue Zeile zu bekommen benötigt man Back-Slash + n: \n
  • //, wenn Title Tag gefunden wird, dann wird nur der Titel ohne NewLine ausgeben, dank printf
  • tr -s delimiter wird verwendet, um ein Zeichen nur einmalig vorkommen zu lassen. Oder mit tr search replace um ein bestimmtes Zeichen zu tauschen (ALLE) Bsp.: `tr 's' 'S' - Dies würde jedes kleine s ins Große S verwandeln

So, hätte ich es gemacht. Hoffentlich wird es ordentlich angezeigt. Danke für den netten Artikel, der war wieder etwas erfrischend und hat meine Stimmung gehoben, obwohl ich mich heute etwas Kränklich fühle.

Claudia
Geschrieben von Claudia am 27. Januar 2025 um 12:13

LESSTHAN SLASH ( title PIPE dubDate ) GREATERTHAN hätte ich dies mit Backslash machen müssen?

Test:

\-F '\'
\/\\/
\/\\/

Gibt es irgendwo ein FAQ für Zeichen, die erlaubt sind. Meine Antwort macht ja so dann keinen Sinn mehr, die ich zu erst geschrieben habe.

Maik
Geschrieben von Maik am 27. Januar 2025 um 13:42

Hallo,

zumindest die Aufrufe von sed kann man sich sparen, wenn man gleich den textuellen Inhalt des Elemenknotens selektiert, also mittels xmllint --xpath '//item/title/text()' gnulinux_newscast_rss.xml bzw. xmllint --xpath '//item/pubDate/text()' gnulinux_newscast_rss.xml Würde man statt xmllint einen voll ausgewachsenen XSLT-Prozessor nutzen, wäre dann auch eine xsl:for-each-Schleife zur Iteration über die jeweiligen Kindelemente (hier: Titel und Datum) jedes item-Elements möglich -- aber das wäre dann vermutlich mit Kanonen auf Spatzen geschossen.

Maik
Geschrieben von Maik am 27. Januar 2025 um 13:46

Und die summary-Elemente kann man mittels local-name() auslesen: xmllint --xpath "//item/*[local-name()='summary']/text()" gnulinux_newscast_rss.xml

Nick
Geschrieben von Nick am 27. Januar 2025 um 13:56

Ich werfe meinen "HOLZHAMMER" in den Ring, der -falls er geht- sicherlich eleganter ginge 😉️

In Debian 12 ist übrigens xmllint augenscheinlich gar nicht installiert (bei mir), es stünde aber im Paket libxml2utils zur Verfügung.

...
wget -q https://gnulinux.ch/podcast/gnulinux_newscast_rss.xml -O rss.xml
sed -i 's///g'  rss.xml
...
sed -i 's///g'  summary.txt
...

Ferner gibt es wohl noch ein Tool namens xmlstarlet (neben wahrscheinlich noch etlichen anderen, von denen vmtl. einige hier schon vorgestellt wurden).

Nick
Geschrieben von Nick am 27. Januar 2025 um 14:00

...HUCH!... Was ist da denn passiert? – In der "Vorschau" wurde meine Idee glatt "ausgeblendet". Gemeint war, dass man in rsss.xml die folgende Ersetzung vornimmt:

itunes:summary durch itunes_summary

Trivial, und nicht sonderlich elegant. Ja, ich weiß.

Ralf Hersel Admin
Geschrieben von Ralf Hersel am 28. Januar 2025 um 19:02

Vielen Dank für eure tollen Hinweise. Ich mache es jetzt so:

#!/bin/bash
# Create a list of podcast episodes with date, title and summary
wget -q https://gnulinux.ch/podcast/gnulinux_newscast_rss.xml -O rss.xml        # get rss.xml from server
sed -i 's/itunes:summary/summary/g' rss.xml                                     # replace namespace itunes:summary
xmllint --xpath '//item/title/text()' rss.xml > title.txt                       # extract Title
xmllint --xpath '//item/pubDate/text()' rss.xml > date.txt                      # extract Date
xmllint --xpath '//item/summary/text()' rss.xml > summary.txt                   # extract Summary
cut -b 6-16 date.txt > date_cut.txt                                             # reformat Date
paste -d "|" date_cut.txt title.txt summary.txt > ciw_folgen.txt                # combine Date, Title and Summary
sed -i 's/|/ - /g' ciw_folgen.txt                                               # replace delimiter | by ' - '
echo 'Suche in: ciw_folgen.txt'                                                 # notify about result
Frank
Geschrieben von Frank am 28. Januar 2025 um 20:10

ohne xmllint, ohne paste , nur mit SED:

wget -q https://gnulinux.ch/podcast/gnulinux_newscast_rss.xml -O rss.txt
sed -n '//,// { // { s/^.(.)/\1/ ; p } // { s/^...., (.) ..:./\1/ ; p } // { s/^.(.*)/\1\n/ ; p } }' < rss.txt | sed -n '1~2!G;h;2~2{s/\n/ /g;p}' > Ergebnis.txt

Ralf Hersel Admin
Geschrieben von Ralf Hersel am 28. Januar 2025 um 21:50

Hallo Frank. Bitte klammere den Code in drei Gravis: U+0060 https://symbl.cc/de/0060/

Dann sieht es so aus:

Mein Super-Code
Naja
Geschrieben von Naja am 28. Januar 2025 um 22:36

Sehr geil! Von mir erhältst du auf jeden Fall den ersten Preis für die nerdigste Lösung!

Frank
Geschrieben von Frank am 30. Januar 2025 um 12:47

ich versuche es noch einmal , und vermeide im skript die kombination von schrägstrichen und kleiner und größer zeichen.


wget -q https://gnulinux.ch/podcast/gnulinux_newscast_rss.xml -O rss.txt  
sed   's;;\nvon ; ;  s;;\nbis ; ; s;;\n1 ;  ; s;;\n2 ;  ;  s;;\n3 ;' &lt; rss.txt &gt; temp.txt
sed -n '/^von /,/^bis / {
/^1 /   { s;^1 \(.*\)
Frank
Geschrieben von Frank am 30. Januar 2025 um 12:48

funktioniert leider nicht .. auch hier werden die texte mit kleiner und größer zeichen zerstört ..