Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Przetwarzanie danych JSON i zarządzanie złożonymi strukturami danych

W poprzednich ćwiczeniach nauczyliśmy się pobierać dane z serwerów API za pomocą Fetch i tworzyć zaawansowane klienty HTTP dla naszego Parku Jurajskiego. Teraz przyszedł czas, aby nauczyć się efektywnie przetwarzać, transformować i zarządzać złożonymi strukturami danych JSON, które otrzymujemy z naszych systemów.

JSON (JavaScript Object Notation) to standardowy format wymiany danych, który jest lekki, łatwy do odczytania przez ludzi i prosty do przetwarzania przez maszyny. W Parku Jurajskim używamy JSON do reprezentowania wszystkiego - od danych dinozaurów po statusy systemów bezpieczeństwa i dane odwiedzających.

Podstawy JSON w JavaScript

JSON w JavaScript jest reprezentowany przez dwa główne typy struktur:

  • Obiekty (obiekty JavaScript / hashmapy)
  • Tablice (listy elementów)

Przykładowa struktura JSON reprezentująca dinozaura w naszym parku:

1const trex = {
2  "id": "dino-001",
3  "nazwa": "Rexy",
4  "gatunek": "Tyrannosaurus Rex",
5  "wiek": 25,
6  "statystyki": {
7    "waga": 8000,
8    "dlugosc": 12.3,
9    "wysokosc": 3.6
10  },
11  "diety": ["mięso", "padlina"],
12  "zagrożenie": 5,
13  "lokalizacja": {
14    "wybieg": "T-Rex Kingdom",
15    "koordynaty": {
16      "x": 34.5,
17      "y": 67.2
18    }
19  },
20  "zachowania": [
21    {
22      "typ": "polowanie",
23      "czestosc": "wysoka",
24      "pory": ["rano", "wieczór"]
25    },
26    {
27      "typ": "odpoczynek",
28      "czestosc": "średnia",
29      "pory": ["południe"]
30    }
31  ],
32  "dataDodania": "2023-06-15T08:30:00Z",
33  "aktywny": true
34}

Parsowanie i serializacja JSON

JavaScript oferuje dwie podstawowe metody do pracy z JSON:

  1. JSON.parse()
    - konwertuje string JSON na obiekt JavaScript
  2. JSON.stringify()
    - konwertuje obiekt JavaScript na string JSON
1// Serializacja obiektu do stringa JSON
2const trexString = JSON.stringify(trex);
3console.log("Zserializowany T-Rex:", trexString);
4
5// Parsowanie stringa JSON z powrotem do obiektu
6const parsedTrex = JSON.parse(trexString);
7console.log("Sparsowany T-Rex:", parsedTrex);

Dodatkowe parametry JSON.stringify() i JSON.parse()

JSON.stringify()
ma dodatkowe parametry, które mogą być przydatne:

1// Formatowanie JSON z wcięciami dla czytelności
2const prettyTrex = JSON.stringify(trex, null, 2);
3console.log("Sformatowany T-Rex:");
4console.log(prettyTrex);
5
6// Selektywne serializowanie tylko wybranych pól
7const simpleTrex = JSON.stringify(trex, ['id', 'nazwa', 'gatunek', 'wiek']);
8console.log("Uproszczony T-Rex:", simpleTrex);
9
10// Własna funkcja zastępcza (replacer)
11const customTrex = JSON.stringify(trex, (key, value) => {
12  // Ukrywamy dokładne współrzędne z powodów bezpieczeństwa
13  if (key === 'koordynaty') return "UKRYTE";
14  // Kody poziomu zagrożenia konwertujemy na opis tekstowy
15  if (key === 'zagrożenie') {
16    const opisy = ["niegroźny", "niski", "umiarkowany", "wysoki", "ekstremalny"];
17    return opisy[value - 1] || value;
18  }
19  return value;
20});
21console.log("T-Rex z niestandardową serializacją:", customTrex);

Techniki przetwarzania złożonych struktur JSON

Załóżmy, że otrzymaliśmy złożoną strukturę JSON zawierającą dane o wszystkich dinozaurach w parku. Jak możemy efektywnie przetwarzać takie dane?

1. Filtrowanie danych

Filtrowanie to jedna z najczęstszych operacji podczas pracy z kolekcjami danych:

1// Załóżmy, że mamy tablicę dinozaurów
2const dinozaury = await api.get('/dinozaury');
3
4// Filtrowanie tylko drapieżników
5const drapiezniki = dinozaury.filter(dino => dino.diety.includes('mięso'));
6
7// Filtrowanie dinozaurów w określonym wybiegu
8const dinoWWybiegu = dinozaury.filter(dino =>
9  dino.lokalizacja.wybieg === 'T-Rex Kingdom'
10);
11
12// Filtrowanie niebezpiecznych dinozaurów (poziom zagrożenia > 3)
13const niebezpieczneDino = dinozaury.filter(dino => dino.zagrożenie > 3);
14
15// Złożone filtrowanie z wieloma warunkami
16const niebezpieczneDrapieznikiWZonie = dinozaury.filter(dino =>
17  dino.zagrożenie > 3 &&
18  dino.diety.includes('mięso') &&
19  dino.lokalizacja.wybieg === 'Zone A'
20);

2. Mapowanie i transformacja danych

Często potrzebujemy przekształcić dane z jednego formatu na inny, np. do wyświetlenia na interfejsie użytkownika:

1// Tworzymy uproszczoną listę dinozaurów dla UI
2const prosteDino = dinozaury.map(dino => ({
3  id: dino.id,
4  nazwa: dino.nazwa,
5  gatunek: dino.gatunek,
6  zagrożenie: dino.zagrożenie
7}));
8
9// Dodajemy nowe pola na podstawie istniejących danych
10const dinoZDodatkowymiInformacjami = dinozaury.map(dino => ({
11  ...dino, // kopiujemy wszystkie istniejące pola
12  wiekLat: dino.wiek, // przemianowanie pola
13  zagrozenieOpis: ['niegroźny', 'niski', 'umiarkowany', 'wysoki', 'ekstremalny'][dino.zagrożenie - 1],
14  waga_tony: dino.statystyki.waga / 1000,
15  wymaga_ochrony: dino.zagrożenie > 3
16}));
17
18// Transformacja do specyficznego formatu dla wykresu
19const daneDoWykresu = dinozaury.map(dino => ({
20  x: dino.statystyki.dlugosc,
21  y: dino.statystyki.waga,
22  r: dino.zagrożenie * 5, // rozmiar punktu zależny od zagrożenia
23  label: dino.nazwa,
24  color: dino.diety.includes('mięso') ? 'red' : 'green'
25}));

3. Agregacja i redukcja danych

Redukcja pozwala na agregowanie danych w celu uzyskania podsumowań i statystyk:

1// Obliczenie średniej wagi wszystkich dinozaurów
2const sredniWaga = dinozaury.reduce((suma, dino) => suma + dino.statystyki.waga, 0) / dinozaury.length;
3
4// Znalezienie najdłuższego dinozaura
5const najdluzszy = dinozaury.reduce((najdluzszy, dino) =>
6  dino.statystyki.dlugosc > najdluzszy.statystyki.dlugosc ? dino : najdluzszy
7, dinozaury[0]);
8
9// Grupowanie dinozaurów według gatunków
10const dinoPoGatunkach = dinozaury.reduce((grupy, dino) => {
11  // Jeśli grupa dla tego gatunku nie istnieje, tworzymy ją
12  if (!grupy[dino.gatunek]) {
13    grupy[dino.gatunek] = [];
14  }
15  // Dodajemy dinozaura do odpowiedniej grupy
16  grupy[dino.gatunek].push(dino);
17  return grupy;
18}, {});
19
20// Liczenie dinozaurów według poziomu zagrożenia
21const liczbaPoZagrozeniu = dinozaury.reduce((licznik, dino) => {
22  licznik[dino.zagrożenie] = (licznik[dino.zagrożenie] || 0) + 1;
23  return licznik;
24}, {});

4. Łączenie operacji z użyciem łańcuchów metod

Metody tablicowe można łączyć w łańcuchy, co pozwala na pisanie zwięzłego i czytelnego kodu:

1// Znajdź 3 najniebezpieczniejsze dinozaury w Zone A
2const top3NiebezpieczneWZoneA = dinozaury
3  .filter(dino => dino.lokalizacja.wybieg === 'Zone A')
4  .sort((a, b) => b.zagrożenie - a.zagrożenie) // sortowanie malejąco
5  .slice(0, 3) // weź tylko pierwsze 3
6  .map(dino => ({
7    nazwa: dino.nazwa,
8    gatunek: dino.gatunek,
9    zagrożenie: dino.zagrożenie
10  }));
11
12// Oblicz średnią wagę dla każdego gatunku
13const sredniaWagaPoGatunkach = Object.entries(
14  dinozaury.reduce((grupy, dino) => {
15    if (!grupy[dino.gatunek]) {
16      grupy[dino.gatunek] = { suma: 0, liczba: 0 };
17    }
18    grupy[dino.gatunek].suma += dino.statystyki.waga;
19    grupy[dino.gatunek].liczba += 1;
20    return grupy;
21  }, {})
22).map(([gatunek, { suma, liczba }]) => ({
23  gatunek,
24  sredniaWaga: suma / liczba
25}));

5. Głębokie znajdowanie i aktualizowanie danych

W złożonych strukturach często musimy znaleźć lub zaktualizować głęboko zagnieżdżone dane. Oto kilka podejść:

1// Znajdowanie dinozaura na podstawie głęboko zagnieżdżonego warunku
2const dinoWPobliżuKoordynatów = dinozaury.find(dino => {
3  const { x, y } = dino.lokalizacja.koordynaty;
4  // Szukamy dinozaura w promieniu 5 jednostek od punktu (30, 70)
5  return Math.sqrt(Math.pow(x - 30, 2) + Math.pow(y - 70, 2)) < 5;
6});
7
8// Aktualizacja głęboko zagnieżdżonej właściwości (immutability)
9function updateDinoLocation(dino, newX, newY) {
10  return {
11    ...dino,
12    lokalizacja: {
13      ...dino.lokalizacja,
14      koordynaty: {
15        ...dino.lokalizacja.koordynaty,
16        x: newX,
17        y: newY
18      }
19    }
20  };
21}
22
23// Użycie
24const updatedDino = updateDinoLocation(trex, 35.0, 68.0);

6. Rekurencyjne przetwarzanie danych

Niektóre struktury danych są hierarchiczne lub mają nieznany poziom zagnieżdżenia. W takich przypadkach przydaje się przetwarzanie rekurencyjne:

1// Przykładowa hierarchiczna struktura kategorii dinozaurów
2const kategorieDinozaurów = {
3  "nazwa": "Dinozaury",
4  "podkategorie": [
5    {
6      "nazwa": "Teropody",
7      "podkategorie": [
8        {
9          "nazwa": "Tyranozauroidy",
10          "dinozaury": [
11            { "id": "dino-001", "nazwa": "Rexy" },
12            { "id": "dino-002", "nazwa": "Tara" }
13          ]
14        },
15        {
16          "nazwa": "Deinonychoidy",
17          "dinozaury": [
18            { "id": "dino-003", "nazwa": "Blue" },
19            { "id": "dino-004", "nazwa": "Delta" }
20          ]
21        }
22      ]
23    },
24    {
25      "nazwa": "Zauropody",
26      "dinozaury": [
27        { "id": "dino-005", "nazwa": "Długi" },
28        { "id": "dino-006", "nazwa": "Wysoki" }
29      ]
30    }
31  ]
32};
33
34// Funkcja rekurencyjna do znajdowania dinozaura według ID w hierarchii
35function znajdzDinozauraPoId(wezel, id) {
36  // Sprawdź, czy obecny węzeł ma tablicę dinozaurów
37  if (wezel.dinozaury) {
38    const znaleziony = wezel.dinozaury.find(dino => dino.id === id);
39    if (znaleziony) return znaleziony;
40  }
41
42  // Jeśli węzeł ma podkategorie, przeszukaj je rekurencyjnie
43  if (wezel.podkategorie) {
44    for (const podkategoria of wezel.podkategorie) {
45      const znaleziony = znajdzDinozauraPoId(podkategoria, id);
46      if (znaleziony) return znaleziony;
47    }
48  }
49
50  // Nic nie znaleziono
51  return null;
52}
53
54// Funkcja rekurencyjna do spłaszczania hierarchii i tworzenia listy wszystkich dinozaurów
55function pobierzWszystkieDinozaury(wezel, lista = []) {
56  if (wezel.dinozaury) {
57    lista.push(...wezel.dinozaury);
58  }
59
60  if (wezel.podkategorie) {
61    for (const podkategoria of wezel.podkategorie) {
62      pobierzWszystkieDinozaury(podkategoria, lista);
63    }
64  }
65
66  return lista;
67}
68
69// Użycie
70const blue = znajdzDinozauraPoId(kategorieDinozaurów, "dino-003");
71console.log("Znaleziony dinozaur:", blue);
72
73const wszystkieDino = pobierzWszystkieDinozaury(kategorieDinozaurów);
74console.log("Wszystkie dinozaury:", wszystkieDino);

Praktyczny przykład: System monitorowania dinozaurów

Teraz połączmy wszystkie omówione techniki w jednym praktycznym przykładzie. Załóżmy, że tworzymy panel kontrolny dla kierownika Parku Jurajskiego, który pokazuje status wszystkich dinozaurów w parku.

1// Funkcja pobierająca dane z API
2async function pobierzDaneDinozaurów() {
3  try {
4    const odpowiedź = await fetch('https://api.jurassic-park.com/dinozaury');
5    if (!odpowiedź.ok) throw new Error(`Błąd HTTP: ${odpowiedź.status}`);
6    return await odpowiedź.json();
7  } catch (błąd) {
8    console.error('Błąd pobierania danych:', błąd);
9    // W przypadku błędu zwracamy puste dane
10    return { dinozaury: [] };
11  }
12}
13
14// Funkcja do przetwarzania i przygotowania danych do wyświetlenia
15function przetwórzDaneDoWyświetlenia(dane) {
16  // Pobieranie głównej tablicy dinozaurów
17  const { dinozaury } = dane;
18
19  // Statystyki zbiorcze
20  const statystyki = {
21    liczba: dinozaury.length,
22    liczbaGatunków: new Set(dinozaury.map(d => d.gatunek)).size,
23    średniaWaga: dinozaury.reduce((suma, d) => suma + d.statystyki.waga, 0) / dinozaury.length,
24    maksymalneZagrożenie: Math.max(...dinozaury.map(d => d.zagrożenie))
25  };
26
27  // Grupowanie według wybiegów
28  const dinozauryPoWybiegach = dinozaury.reduce((grupy, dino) => {
29    const wybieg = dino.lokalizacja.wybieg;
30    if (!grupy[wybieg]) grupy[wybieg] = [];
31    grupy[wybieg].push(dino);
32    return grupy;
33  }, {});
34
35  // Liczenie dinozaurów według poziomów zagrożenia
36  const liczbaPoZagrożeniu = [1, 2, 3, 4, 5].map(poziom => ({
37    poziom,
38    liczba: dinozaury.filter(d => d.zagrożenie === poziom).length,
39    procent: (dinozaury.filter(d => d.zagrożenie === poziom).length / dinozaury.length) * 100
40  }));
41
42  // Identyfikacja potencjalnych zagrożeń
43  const zagrożenia = dinozaury
44    .filter(d => d.zagrożenie >= 4)
45    .map(d => ({
46      id: d.id,
47      nazwa: d.nazwa,
48      gatunek: d.gatunek,
49      wybieg: d.lokalizacja.wybieg,
50      zagrożenie: d.zagrożenie,
51      ostatnieZachowanie: d.zachowania[0]?.typ || 'nieznane',
52      koordynaty: d.lokalizacja.koordynaty
53    }))
54    .sort((a, b) => b.zagrożenie - a.zagrożenie);
55
56  // Przygotowanie danych do wykresu wagowego
57  const daneDoWykresuWagi = dinozaury
58    .filter(d => d.statystyki.waga > 0) // Odfiltruj brakujące dane
59    .map(d => ({
60      id: d.id,
61      nazwa: d.nazwa,
62      gatunek: d.gatunek,
63      waga: d.statystyki.waga,
64      drapieżnik: d.diety.includes('mięso'),
65      rozmiarPunktu: Math.sqrt(d.statystyki.waga) / 5
66    }));
67
68  // Ostatnie aktywności (sortowane według daty)
69  const ostatnieAktywności = dinozaury
70    .filter(d => d.zachowania && d.zachowania.length > 0)
71    .flatMap(d => d.zachowania.map(z => ({
72      dinoId: d.id,
73      dinoNazwa: d.nazwa,
74      typ: z.typ,
75      czestosc: z.czestosc,
76      pory: z.pory
77    })))
78    .sort((a, b) => a.dinoId.localeCompare(b.dinoId));
79
80  // Zwróć przetworzone dane
81  return {
82    statystyki,
83    dinozauryPoWybiegach,
84    liczbaPoZagrożeniu,
85    zagrożenia,
86    daneDoWykresuWagi,
87    ostatnieAktywności,
88    surowe: dinozaury // Oryginalne dane, gdyby były potrzebne
89  };
90}
91
92// Funkcja do renderowania danych na interfejsie użytkownika (przykład)
93function wyrenderujPanelKontrolny(przetworzoneDane) {
94  // Tutaj kod aktualizujący widoki UI, wykresy, tabele, mapy, itp.
95  console.log("Przetworzono dane do panelu kontrolnego:", przetworzoneDane);
96
97  // Przykład renderowania tabeli z zagrożeniami
98  const tabelaZagrożeń = document.getElementById('tabela-zagrozen');
99  if (tabelaZagrożeń) {
100    tabelaZagrożeń.innerHTML = przetworzoneDane.zagrożenia
101      .map(z => `
102        <tr>
103          <td>${z.nazwa}</td>
104          <td>${z.gatunek}</td>
105          <td>${z.wybieg}</td>
106          <td class="zagrozenie-${z.zagrożenie}">${z.zagrożenie}</td>
107          <td>${z.ostatnieZachowanie}</td>
108          <td>
109            <button onclick="pokazNaMapie(${z.koordynaty.x}, ${z.koordynaty.y})">
110              Pokaż na mapie
111            </button>
112          </td>
113        </tr>
114      `)
115      .join('');
116  }
117
118  // ... pozostałe elementy UI
119}
120
121// Główna funkcja aktualizacji danych
122async function aktualizujPanel() {
123  // Pobranie danych z API
124  const dane = await pobierzDaneDinozaurów();
125
126  // Przetworzenie danych
127  const przetworzoneDane = przetwórzDaneDoWyświetlenia(dane);
128
129  // Wyrenderowanie panelu
130  wyrenderujPanelKontrolny(przetworzoneDane);
131}
132
133// Uruchomienie aktualizacji co 5 minut
134aktualizujPanel();
135setInterval(aktualizujPanel, 5 * 60 * 1000);

Podsumowanie

W tym ćwiczeniu nauczyliśmy się:

  1. Podstaw serializacji i deserializacji JSON - jak konwertować dane między formatem tekstowym a obiektami JavaScript

  2. Zaawansowanych technik przetwarzania danych:

    • Filtrowanie danych za pomocą
      .filter()
    • Transformacja danych za pomocą
      .map()
    • Agregacja danych za pomocą
      .reduce()
    • Łączenie operacji w łańcuchy
    • Praca z głęboko zagnieżdżonymi strukturami
    • Rekurencyjne przetwarzanie hierarchicznych danych
  3. Praktycznego zastosowania tych technik w kontekście systemu zarządzania parkiem pełnym dinozaurów

Umiejętność efektywnego przetwarzania i analizy danych JSON jest kluczowa dla każdego współczesnego projektu JavaScript - od prostych stron internetowych po złożone aplikacje jak system zarządzania Parkiem Jurajskim. Dzięki technikom, które poznałeś, będziesz mógł przekształcać surowe dane w użyteczne informacje, które pomogą utrzymać park funkcjonujący sprawnie i bezpiecznie... przynajmniej do czasu, gdy raptor znajdzie lukę w systemie!

W następnym ćwiczeniu przyjrzymy się jak tworzyć i korzystać z Web Workers, aby przenieść intensywne obliczenia (jak analizy zachowań dinozaurów) do oddzielnych wątków, nie blokując głównego interfejsu użytkownika.

Przejdź do CodeWorlds