Przetwarzanie dużych JSONów w Pythonie

Jak w Pythonie odczytać dane z pliku w formacie JSON? Użyć modułu json! – odpowie zapewne spora część pythonistów. I chociaż bardzo często odpowiedź taka jest wystarczająca, niekiedy na horyzoncie pojawiają się problemy, w których zdecydowanie lepiej skorzystać z innych rozwiązań.

Jednym z nich jest ijson, czyli iteratywny (ang. iterative) parser plików JSON. Na czym polega różnica? Otóż zamiast od razu ładować do pamięci operacyjnej całą zawartość pliku, możemy po kolei przechodzić po znajdujących się w nim elementach (iterować). Przyda nam się to przede wszystkim w dwóch sytuacjach:

  • Plik, który chcemy przetwarzać jest tak duży (np. rozmiar rzędu gigabajtów), że niemożliwym jest wczytanie go w całości.
  • Przetwarzany plik jest duży i chociaż można załadować go do pamięci, to jest to sposób mało wydajny i powolny.

Pierwszy z przypadków jest dosyć oczywisty. Jeśli spróbujemy wczytać jakiś spory plik (przekraczający ilość dostępnej pamięci operacyjnej) przy pomocy modułu json, proces po prostu zostanie w pewnym momencie automatycznie unicestwiony.  Skorzystanie z ijson, zapewniającego iteracyjny dostęp do zawartości pliku, po prostu pozwoli nam po prostu dobrać się do tych danych.

A jak wygląda przyspieszenie działania aplikacji za sprawą wymiany json na ijson? Wyobraźmy sobie, że dysponujemy danymi w takim oto formacie:

W celu wygenerowania nieco większego (np. składającego się z miliona wierszy) pliku o podanej powyżej strukturze, możesz skorzystać ze skryptu, który w tym celu przygotowałem – KLIK.

Załóżmy teraz, że naszym celem jest znalezienie takiego wpisu konfiguracyjnego, który zawiera nieznaną usługę działającą na porcie w zakresie [5000, 6000). Oczywiście taki program poradzi sobie z tym bez problemu:

…tyle, że trochę to potrwa (u mnie – około 6 sekund). Alternatywą będzie wersja z wykorzystaniem modułu ijson:

Nawet jeżeli szukany przez nas wpis będzie wymagał przeiterowania prawie całego pliku, to czas wykonania i tak powinien być sporo krótszy (w moim środowisku wariant pesymistyczny to około 4 sekund, a optymistyczny, w którym poszukiwana usługa jest bliżej niż dalej –  to zaledwie ułamki sekundy).

Co więcej, kod ma szansę stać się bardziej przejrzysty, dzięki możliwości umieszczenia poszukiwanego elementu jako argumentu funkcji ijson.items. Nie musimy definiować dwóch pętli – pierwszej po dostępnych kontach oraz wewnętrznej – po przypisanych dla danego konta konfiguracjach. Zamiast tego elegancko podajemy data.item.configs.item i cieszymy się jedną prostą pętlą bezpośrednio operujących na configach. 

Jeśli zastanawiasz się, skąd wziął się taki ciąg znaków, wskazujący na szukany element, to w odkryciu tego i zrozumieniu zasady działania parsera ijson, pomoże Ci zapewne wywołanie nieco bardziej niskopoziomowej funkcji ijson.parse(file), które da następujący rezultat:

Widać tu w szczegółach na czym polega dokonywane przez ijson parsowanie, którego rezultatem są informacje zbierane o kolejnych elementach JSON-a zamiast jednego wielkiego słownika wynikowego. A czy Ty miałeś już kiedyś okazję skorzystać z tej biblioteki? Może napotkałeś jakieś inne problemy z przetwarzaniem JSON-ów, o których chciałbyś opowiedzieć?

.