Przetwarzaj XML jak bazę danych w Pythonie
W dobie nowoczesnych języków programowania i świetnych bibliotek zadania programistyczne stają się coraz łatwiejsze. A także - i to może nie jest oczywiste - pozwalają rozwiązywać problemy w bardziej elegancji sposób.
Rozważmy typowy scenariusz programisty przetwarzającego dane z pliku XML. Niech będą to artykuły w sklepie:
<shop>
<stock name="basement">
<item name="bicycle" price="100" qty="3"/>
<item name="ball" price="10" qty="5"/>
<item name="paint" price="3" qty="20"/>
</stock>
<stock name="attic">
<item name="sock" price="1" qty="100"/>
<item name="ambrella" price="5" qty="10"/>
<item name="tv" price="0" qty="2"/>
</stock>
</shop>
Niech naszym zadaniem będzie znalezienie nazwy magazynu, gdzie znajdują się piłki. Można to zrobić na wiele sposobów - iterowanie po węzłach XML bądź używanie XPath. Jednak pierwsze wymaga pisania brzydkiego kodu a drugie wymaga sporej biegłości zwłaszcza przy nieco bardziej skomplikowanych zadaniach.
Użytkownicy baz danych stwierdzą, że mogą to zrobić łatwo za pomocą prostego zapytania bazodanowego. Ale wymaga to użycia innej reprezentacji danych - tabularycznej.
Rozważmy tego typu transformację (w tym przykładzie korzystamy z Pythona, biblioteki lxml do operacji na plikach XML oraz biblioteki py_linq do operacji typu Linq - Language Integrated Query):
import pprint
from collections import namedtuple
from lxml import objectify
from py_linq import Enumerable as en
def iterate_items(file):
t = namedtuple("item", ["stock_name", "item_name", "item_qty", "item_price"])
shop = objectify.parse(file).getroot()
for stock in shop.stock:
for item in stock.item:
print("Yielding", item.get("name"))
yield t(stock.get("name"), item.get("name"), item.get("qty"), item.get("price"))
q = en(iterate_items("shop.xml"))
result = q.where(
lambda x: x.item_name == "ball").select(
lambda x: x.stock_name).to_list()
pprint.pprint(result)
W wyniku otrzymujemy:
['basement']
Co się tutaj dzieje:
- "drzewiaste" dane XML zostają sprowadzone do "płaskiej" (tabularycznej) postaci za pomocą iterate_items();
- iterator służy do stworzenia obiektu umożliwiającego wyliczanie (Enumerable);
- na tym obiekcie wywołujemy metodę kwalifikującą (where()) a na wyniku kwalifikacji metodę selekcji (select());
- wynik konsumujemy i przekształcamy na listę.
Uwaga: kwalifikacja i selekcja odbywa się jednocześnie z generowaniem kolejnych elementów item; oznacza to, że jeśli funkcja iterate_items() jest czasochłonna, jesteśmy w stanie zacząć uzyskiwać wyniki zapytania zanim zakończy się przeglądanie danych:
q = en(iterate_items("shop.xml"))
for i in q.where(
lambda x: int(x.item_qty) > 5).select(
lambda x: x.item_name):
print("Consuming", i)
otrzymujemy:
Yielding bicycle
Yielding ball
Yielding paint
Consuming paint
Yielding sock
Consuming sock
Yielding ambrella
Consuming ambrella
Yielding tv
Jak widać operacje przebiegają współbieżnie.
Kolejny przykład:
q = en(iterate_items("shop.xml"))
q.order_by(
lambda x: int(x.item_price)).select(
lambda x: print(f"{x.item_name}: {x.item_price}")).to_list()
i wynik:
tv: 0
sock: 1
paint: 3
ambrella: 5
ball: 10
bicycle: 100
Widać tutaj ciekawą właściwość zapytań: mogą one posiadać efekty uboczne (tutaj wywołanie print()). Co ciekawe, bez wywołania to_list() nic nie zostanie wypisane - wynik wyrażenia nie jest wtedy skonsumowany.
Stosując podejście z Linq możemy tworzyć zaawansowane konstrukty które w klasycznych algorytmach wymagały by pętli, tworzenia wyników pośrednich itd.
Na temat Linq:
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/index
Podobna technika dotycząca XML i Linq:
https://docs.microsoft.com/pl-pl/dotnet/csharp/programming-guide/concepts/linq/linq-to-xml-overview
Biblioteka Linq dla Python:
https://pypi.org/project/py_linq/