Home Dokumentacje Efektywne programowanie w AWK - Podręcznik użytkownika GNU awk - Tablice w awk
10 | 12 | 2019
Efektywne programowanie w AWK - Podręcznik użytkownika GNU awk - Tablice w awk Drukuj

Przejdź do pierwszej, poprzedniej, następnej, ostatniej sekcji, spisu treści.

 


 

11. Tablice w awk

Tablica (array) jest tabelą wartości, zwanych elementami. Elementy tablicy rozróżniane są poprzez swoje indeksy. Indeksy mogą być liczbami lub łańcuchami. awk przechowuje jeden zbiór nazw, które mogą być wykorzystywane przy nazywaniu zmiennych, tablic i funkcji (zob. 13. Funkcje definiowane przez użytkownika). Zatem, w jednym programie awk nie można mieć zmiennej i tablicy o tej samej nazwie.

11.1. Wprowadzenie do tablic

Język awk do przechowywania grup powiązanych ze sobą łańcuchów czy liczb zapewnia jednowymiarowe tablice.

Każda tablica w awk musi mieć nazwę. Nazwy tablic mają tę samą składnię, co nazwy zmiennych; każda poprawna nazwa zmiennej byłaby także poprawną nazwą tablicy. Nie można jednak w jednym programie awk stosować jednej nazwy na oba sposoby (jako tablicy i jako zmiennej).

Tablice w awk pozornie przypominają tablice w innych językach programowania. Istnieją jednak zasadnicze różnice. W awk nie ma konieczności podawania rozmiaru tablicy przed rozpoczęciem korzystania z niej. Dodatkowo, jako indeks tablicy może być wykorzystana dowolna liczba czy łańcuch, a nie wyłącznie kolejne liczby całkowite.

W większości innych języków istnieje obowiązek zadeklarowania tablicy i określenia, ile zawiera ona elementów czy składowych. W takich językach deklaracja powoduje zaalokowanie ciągłego bloku pamięci dla wskazanej liczby elementów. Indeks tablicy zwykle musi być dodatnią liczbą całkowitą, na przykład, indeks zero określa pierwszy element tablicy, który faktycznie jest przechowywany na samym początku bloku pamięci. Indeks jeden określa drugi element, przechowywany w pamięci zaraz za pierwszym, i tak dalej. Dodanie kolejnych elementów do tablicy jest niemożliwe gdyż ma ona miejsce tylko na tyle elementów, ile zadeklarowano. (Niektóre języki zezwalają na dowolne indeksy początkowe i końcowe, np. `15 .. 27', ale rozmiar tablicy jest nadal wyznaczany na stałe przy jej deklaracji.)

Ciągła tablica czterech elementów mogłaby, pojęciowo, wyglądać jak niżej, jeśli wartościami elementów są osiem, "foo", "" i 30:

Przechowywane są tylko wartości, indeksy wynikają z kolejności tych wartości. Osiem jest wartością o indeksie zero, gdyż osiem stoi na pozycji, przed którą jest zero elementów.

Tablice w awk są inne: są one asocjacyjne (przyporządkowujące). Oznacza to, że każda tablica jest zbiorem par: indeks, i odpowiadająca mu wartość elementu tablicy:

Element 4     Wartość 30
Element 2     Wartość "foo"
Element 1     Wartość 8
Element 3     Wartość ""

Pokazaliśmy pary w wymieszanej kolejności, gdyż ich kolejność jest nieistotna.

Jedną z zalet tablic asocjacyjnych jest to, że w dowolnym momencie można dodawać nowe pary. Na przykład, załóżmy, że do powyższej tablicy dodamy dziesiąty element, o wartości "numer dziesięć". Wynikiem jest:

Element 10    Wartość "numer dziesięć"
Element 4     Wartość 30
Element 2     Wartość "foo"
Element 1     Wartość 8
Element 3     Wartość ""

Teraz tablica jest rzadka (sparse), co oznacza po prostu, że brakuje niektórych indeksów: ma elementy 1--4 i 10, ale nie ma elementów 5, 6, 7, 8, czy 9.

Innym skutkiem tablic asocjacyjnych jest fakt, że indeksy nie muszą być dodatnimi liczbami całkowitymi. Indeksem może być dowolna liczba, a nawet łańcuch. Na przykład, oto tablica tłumacząca słowa z polskiego na angielski:

Element "pies"  Wartość "dog"
Element "kot"   Wartość "cat"
Element "jeden" Wartość "one"
Element 1       Wartość "one"

Zdecydowaliśmy się tu tłumaczyć liczbę jeden zarówno napisaną słownie jak i w postaci numerycznej -- pokazując w ten sposób, że pojedyncza tablica może zawierać jako indeksy jednocześnie liczby i łańcuchy. (W rzeczywistości indeksy tablicy są zawsze łańcuchami; omówiono to szczegółowo w 11.7. Stosowanie liczb do indeksowania tablic.)

Wartość IGNORECASE nie wpływa na indeksowanie tablic. Do pobrania elementu tablicy musimy użyć dokładnie tej samej wartości łańcuchowej, która posłużyła do jego zapamiętania.

Gdy tablicę tworzy dla nas sam awk, np. funkcją wbudowaną split, to jej indeksy są kolejnymi liczbami całkowitymi poczynając od jeden. (Zob. 12.3. Funkcje wbudowane działające na łańcuchach.)

11.2. Odwoływanie się do elementów tablicy

Głównym sposobem korzystania z tablic jest odwoływanie się do któregoś z jej elementów. Odwołanie do tablicy jest wyrażeniem wyglądającym tak:

tablica[indeks]

Gdzie tablica jest nazwą tablicy. Wyrażenie indeks jest indeksem elementu tablicy, o który nam chodzi.

Wartością odwołania do tablicy jest bieżąca wartość tego elementu tablicy. Na przykład, foo[4.3] jest wyrażeniem dla elementu tablicy foo o indeksie `4.3'.

Jeżeli odwołamy się do elementu tablicy, który nie ma wpisanej wartości, to wartością odwołania będzie "", łańcuch pusty. Dotyczy to elementów, którym jeszcze nie przypisano wartości, jak i elementów, które zostały usunięte (zob. 11.6. Instrukcja delete). Odwołanie takie automatycznie tworzy element tablicy, z łańcuchem pustym jako jego wartością. (W niektórych przypadkach nie jest to zbyt szczęśliwe, gdyż może marnować pamięć wewnątrz awk.)

Czy w tablicy istnieje element o danym indeksie można sprawdzić za pomocą wyrażenia:

indeks in tablica

Powyższe wyrażenie testuje czy istnieje konkretny indeks, czy nie, bez skutku ubocznego w postaci tworzenia elementu jeśli ma indeksu. Ma ono wartość jeden (prawda) jeśli tablica[indeks] istnieje, a zero (fałsz) jeśli nie istnieje.

Na przykład, do sprawdzenia czy tablica czestosci zawiera indeks `2', moglibyśmy napisać tę instrukcję:

if (2 in czestosci)
    print "Indeks 2 istnieje."

Zwróć uwagę, że powyższe nie sprawdza, czy tablica czestosci zawiera element, którego wartością jest dwa. (Nie ma innej metody, by to zrobić, niż zbadanie wszystkich elementów.) Nie tworzy to także czestosci[2], choć poniższa (niepoprawna) alternatywa by to robiła:

if (czestosci[2] != "")
    print "Indeks 2 istnieje."

11.3. Przypisanie do elementów tablicy

Elementy tablic są lwartościami: można im przypisywać wartości dokładnie tak samo jak zmiennym awk:

tablica[indeks] = wartość

Gdzie tablica jest nazwą naszej tablicy. Wyrażenie indeks jest indeksem elementu tablicy, do któremu chcemy przypisać wartość. Wyrażenie wartość jest wartością, którą przypisujemy temu elementowi tablicy.

11.4. Prosty przykład tablicy

Poniższy program pobiera listę wierszy, każdy zaczynający się numerem wiersza, i wypisuje je w kolejności tych numerów. Numery wierszy nie są jednak uporządkowane podczas odczytu -- są wymieszane. Program sortuje wiersze tworząc tablicę z wykorzystującą numery wierszy jako indeksy. Następnie wypisuje wiersze w kolejności ich posortowanych numerów. To bardzo prosty program, i wprawia go w zakłopotanie napotkanie powtórzonych numerów, luk, czy wierszy, które nie rozpoczynają się numerem.

{
  if ($1 > max)
    max = $1
  arr[$1] = $0
}

END {
  for (x = 1; x <= max; x++)
    print arr[x]
}

Pierwsza reguła śledzi największy numer wiersza, jaki do tej pory napotkano. Zapisuje też każdy wiersz do tablicy arr, pod indeksem będącym numerem wiersza.

Druga reguła pracuje po przeczytaniu całości wejścia, wypisując wszystkie wiersze.

Gdy uruchomimy ten program z poniższymi danymi wejściowymi:

5  I am the Five man
2  Who are you?  The new number two!
4  . . . And four on the floor
1  Who is number one?
3  I three you.

jego wyjściem będzie:

1  Who is number one?
2  Who are you?  The new number two!
3  I three you.
4  . . . And four on the floor
5  I am the Five man

Jeśli jakiś numer wiersza powtarza się, to ostatni wiersz z tym numerem zastępuje poprzednie.

Luki w numerach wierszy można łatwo obsłużyć usprawnieniem reguły END:

END {
  for (x = 1; x <= max; x++)
    if (x in arr)
      print arr[x]
}

11.5. Przeglądanie wszystkich elementów tablicy

W programach używającym tablic często potrzebujemy pętli, która wykonuje się po jednym razie dla każdego elementu tablicy. W innych językach, gdzie tablice są ciągłe a indeksy ograniczone do dodatnich liczb całkowitych, jest to łatwe: Można znaleźć wszystkie poprawne indeksy zliczając od najniższego indeksu do najwyższego. Ta technika nie sprawdzi się w awk, gdyż indeksem tablicy może być dowolna liczba lub łańcuch. Stąd też awk posiada specjalny rodzaj instrukcji for służący do przeglądania tablic:

for (zmn in tablica)
  ciało

Pętla ta wykonuje ciało jeden raz dla każdego indeksu tablicy, którą uprzednio stworzył program, z przypisaniem tego indeksu do zmiennej zmn.

Oto program korzystający z tej postaci instrukcji for. Pierwsza jego reguła bada rekordy wejściowe i odnotowuje, jakie słowa (co najmniej jedno) pojawiły się w wejściu, zapisując jedynkę w tablicy used z danym słowem jako indeksem. Druga reguła przeszukuje elementy tablicy used, w celu znalezienia wszystkich różnych słów, jakie pojawiły się w danych wejściowych. Wypisuje każde słowo dłuższe niż 10 znaków, wypisuje też liczbę takich słów. Zob. 12.3. Funkcje wbudowane działające na łańcuchach, gdzie szerzej opisano funkcję wbudowaną length.

# zapisz 1 dla każdego choć raz użytego słowa.
{
    for (i = 1; i <= NF; i++)
        used[$i] = 1
}

# znajdź liczbę różnych słów dłuższych niż 10 znaków.
END {
    for (x in used)
        if (length(x) > 10) {
            ++num_long_words
            print x
        }
    print num_long_words, "słów dłuższych niż 10 znaków"
}

Zob. 16.2.5. Tworzenie statystyk użycia wyrazów, gdzie podano bardziej szczegółowy przykład tego typu.

Kolejność, w jakiej instrukcja for sięga do elementów tablicy, wyznaczona jest wewnętrznym układem tych elementów w awk i nie daje się sterować czy zmieniać. Może to prowadzić do kłopotów jeśli instrukcje w ciele pętli dodadzą do tablicy nowe elementy. Nie da się przewidzieć, czy pętla for sięgnie po nie, czy też nie. Podobnie, zmiana zmn wewnątrz pętli może dać dziwne skutki. Najlepiej unikać takich rzeczy.

11.6. Instrukcja delete

Pojedynczy element tablicy usuwa się instrukcją delete:

delete tablica[indeks]

Po usunięciu elementu tablicy, nie można już otrzymać wartości, jaką miał on uprzednio. Jest tak, jakby nigdy nie odwoływano się do tego elementu i nigdy nie nadawano mu żadnej wartości.

Oto przykład usuwania elementów tablicy:

for (i in czestosci)
  delete czestosci[i]

Ten przykład usuwa wszystkie elementy z tablicy czestosci.

Jeśli usuniemy element, to następująca potem instrukcja for przeglądająca tablicę nie zgłosi go, a operator in sprawdzający istnienie tego elementu zwróci zero (tj. fałsz):

delete foo[4]
if (4 in foo)
    print "To nigdy nie zostanie wypisane"

Należy pamiętać, że usunięcie elementu nie jest tym samym, co przypisanie mu pustej wartości (łańcucha pustego, "").

foo[4] = ""
if (4 in foo)
  print "To jest wypisane, mimo że foo[4] jest puste"

Nie jest błędem usuwanie elementu, który nie istnieje.

Można usunąć wszystkie elementy tablicy jedną instrukcją, pomijając indeks w instrukcji delete.

delete tablica

Możliwość ta stanowi rozszerzenie gawk. Nie jest dostępna w trybie zgodności (zob. 14.1. Opcje wiersza poleceń).

Stosowanie tej wersji instrukcji delete jest około trzech razy bardziej efektywne niż równoważna jej pętla usuwająca po jednym elemencie naraz.

Poniższa instrukcja zapewnia przenośną, ale nie oczywistą metodę wyczyszczenia tablicy.

# dzięki Michaelowi Brennananowi za wskazanie tego
split("", tablica)

Funkcja split (zob. 12.3. Funkcje wbudowane działające na łańcuchach) czyści najpierw tablicę docelową. Wywołanie to żąda od niej rozbicia na części łańcucha pustego. Ponieważ brak jest danych do podziału, funkcja po prostu czyści tablicę a następnie wraca.

Uwaga! Usunięcie tablicy nie zmienia jej typu; nie można usunąć tablicy a następnie wykorzystać jej nazwy jako skalara. Na przykład, to nie zadziała:

a[1] = 3; delete a; a = 3

11.7. Stosowanie liczb do indeksowania tablic

Istotnym aspektem tablic, o którym należy pamiętać, jest to, że indeksy tablicy są zawsze łańcuchami. Jeśli jako indeks zastosujemy wartość numeryczną, zostanie ona przekształcona na wartość łańcuchową przed wykorzystaniem do indeksowania (zob. 7.4. Konwersja łańcuchów i liczb).

Oznacza to, że wartość zmiennej wbudowanej CONVFMT może potencjalnie wpływać na sposób, w jaki program sięga do elementów tablicy. Na przykład:

xyz = 12.153
dane[xyz] = 1
CONVFMT = "%2.2f"
if (xyz in dane)
    printf "%s jest w tabl. dane\n", xyz
else
    printf "%s nie ma w tabl. dane\n", xyz

Powyższe wypisze `12.15 nie ma w tabl. dane'. Pierwsza instrukcja nadaje xyz wartość numeryczną. Przypisanie do dane[xyz] indeksuje dane wartością łańcuchową "12.153" (wykorzystując domyślną wartość konwersji CONVFMT, "%.6g"), i nadaje data["12.153"] wartość jeden. Następnie program zmienia wartość CONVFMT. Test `(xyz in dane)' tworzy nową wartość łańcuchową z xyz, tym razem "12.15", gdyż wartość CONVFMT pozwala tylko na dwie cyfry znaczące. Test zwraca porażkę, gdyż "12.15" jest łańcuchem różnym od "12.153".

Zgodnie z regułami konwersji (zob. 7.4. Konwersja łańcuchów i liczb), wartości całkowite są zawsze przekształcane na łańcuchy jako całkowite, bez względu na to, jaka jest wartość CONVFMT. Zatem zwyczajny przypadek:

for (i = 1; i <= maxind; i++)
    zrób coś z tablica[i]

będzie działał, bez względu na wartość CONVFMT.

Jak wiele rzeczy w awk, przeważnie rzeczy działają tak, jak się tego spodziewamy. Przydaje się jednak dokładna wiedzę o faktycznych zasadach, gdyż mogą one czasem mieć trudno uchwytny wpływ na nasze programy.

11.8. Stosowanie niezainicjowanych zmiennych jako indeksów

Załóżmy, że chcemy wypisać jakieś dane wejściowe w odwróconej kolejności. Sensowny program próbny, który by to robił (na pewnych danych testowych) wygląda tak:

$ echo 'wiersz 1
> wiersz 2
> wiersz 3' | awk '{ w[wiersze] = $0; ++wiersze }
> END {
>     for (i = wiersze-1; i >= 0; --i)
>        print w[i]

> }'
-| wiersz 3
-| wiersz 2

Niestety, pierwszy wiersz danych wejściowych nie pojawił się na wyjściu!

Na pierwszy rzut oka, program powinien działać. Zmienna wiersze jest niezainicjowana, a niezainicjowane zmienne mają numeryczną wartość zero. Zatem, awk powinien wypisać wartość elementu w[0].

Problemem jest tu fakt, że indeksy tablic awkzawsze łańcuchami. A niezainicjowane zmienne, użyte jako łańcuchy, mają wartość "", a nie zero. Zatem, `wiersz 1' został ostatecznie zapisany w w[""].

Poniższa wersja programu działa poprawnie:

{ w[wiersze++] = $0 }
END {
    for (i = wiersze - 1; i >= 0; --i)
       print w[i]
}

Tutaj, `++' wymusza, by wiersze było numeryczne, w ten sposób czyniąc "starą wartość" numerycznym zerem, które z kolei jest przekształcane na "0" jako indeks tablicy.

Jak właśnie widzieliśmy, mimo tego, że jest to nieco niezwykłe, łańcuch pusty ("") jest poprawnym indeksem tablicy (c.k.). Jeśli w wierszu poleceń podano opcję `--lint' (zob. 14.1. Opcje wiersza poleceń), gawk będzie ostrzegał o użyciu łańcucha pustego jako indeksu.

11.9. Tablice wielowymiarowe

Tablica wielowymiarowa jest tablicą, w której element jest identyfikowany przez ciąg indeksów, a nie przez pojedynczy indeks. Na przykład, tablica dwuwymiarowa wymaga dwu indeksów. Zwyczajowym sposobem (w większości języków, łącznie z awk) odwoływania się do elementu tablicy dwuwymiarowej o nazwie siatka jest siatka[x,y].

Tablice wielowymiarowe obsługiwane są w awk dzięki konkatenacji indeksów w jeden łańcuch. Działa to tak, że awk przekształca indeksy na łańcuchy (zob. 7.4. Konwersja łańcuchów i liczb) i skleja je razem, z separatorem między nimi. Tworzy to pojedynczy łańcuch opisujący wartości poszczególnych indeksów. Połączony łańcuch służy jako pojedynczy indeks zwykłej, jednowymiarowej tablicy. Wykorzystywanym separatorem jest wartość zmiennej wbudowanej SUBSEP.

Na przykład, załóżmy, że obliczamy wyrażenie `foo[5,12] = "wartość"' gdy wartością SUBSEP jest "@". Liczby pięć i 12 są przekształcane na łańcuchy i łączone ze sobą z `@' między nimi, dając "5@12". Zatem, element foo["5@12"] otrzymuje wartość "wartość".

Po zapamiętaniu wartości elementu awk nie ma żadnego zapisu o tym, czy był on zapamiętany z pojedynczym indeksem, czy ciągiem indeksów. Wyrażenia `foo[5,12]' i `foo[5 SUBSEP 12]' są zawsze równoważne.

Domyślną wartością SUBSEP jest łańcuch "\034", zawierający znak niedrukowalny, którego pojawienie się w programie awk lub większości danych wejściowych jest mało prawdopodobne.

Użyteczność wyboru nieprawdopodobnego znaku pochodzi stąd, iż wartości indeksów zawierające łańcuch pasujący do SUBSEP prowadzą do łączonych łańcuchów, które są niejednoznaczne. Załóżmy, że SUBSEP byłoby "@". Wówczas `foo["a@b", "c"]' i `foo["a", "b@c"]' byłyby nieodróżnialne, gdyż oba zostały by w rzeczywistości zapamiętane jako `foo["a@b@c"]'.

Możemy sprawdzać czy w tablicy "wielowymiarowej" istnieje konkretna sekwencja-indeksów za pomocą tego samego operatora `in', używanego dla tablic jednowymiarowych. Zamiast pojedynczego indeksu jako lewostronnego operatora, piszemy w nawiasach cały ciąg indeksów rozdzielonych przecinkami:

(indeks1, indeks2, ...) in tablica

Poniższy przykład traktuje swoje wejście jak dwuwymiarową tablicę pól. Obraca tę tablicę o 90 stopni zgodnie z ruchem wskazówek zegara i wypisuje wynik. Zakłada, że wszystkie wiersze mają tę samą ilość elementów.

awk '{
     if (max_nf < NF)
          max_nf = NF
     max_nr = NR
     for (x = 1; x <= NF; x++)
          wektor[x, NR] = $x
}

END {
     for (x = 1; x <= max_nf; x++) {
          for (y = max_nr; y >= 1; --y)
               printf("%s ", wektor[x, y])
          printf("\n")
     }
}'

Przy podaniu mu jako wejścia:

1 2 3 4 5 6
2 3 4 5 6 1
3 4 5 6 1 2
4 5 6 1 2 3

daje:

4 3 2 1
5 4 3 2
6 5 4 3
1 6 5 4
2 1 6 5
3 2 1 6

11.10. Przeglądanie tablic wielowymiarowych

Nie istnieje żadna specjalna instrukcja for do przeglądania tablic "wielowymiarowych". Nie może istnieć, gdyż naprawdę nie ma wielowymiarowych tablic czy elementów. Jest tylko wielowymiarowa metoda dostępu do tablicy.

Jeśli jednak w naszym programie jest tablica, do której zawsze odwołujemy się wielowymiarowo, można uzyskać efekt jej przeglądania dzięki połączeniu instrukcji for (zob. 11.5. Przeglądanie wszystkich elementów tablicy) z funkcją wbudowaną split (zob. 12.3. Funkcje wbudowane działające na łańcuchach). Działa to tak:

for (laczony in tablica) {
  split(laczony, poszczegolne, SUBSEP)
  ...
}

Nadaje to zmiennej laczony kolejne wartości skonkatenowanych, połączonych indeksów tablicy, i dzieli ją na poszczególne indeksy przez cięcia w miejscach, gdzie występuje wartość SUBSEP. Powstałe z podziału indeksy stają się elementami tablicy poszczegolne.

Zatem załóżmy, że w tablica[1, "foo"] mamy uprzednio zapamiętaną wartość. Wówczas w tablicy array istnieje element o indeksie "1\034foo". (Przypominamy, że domyślną wartością SUBSEP jest znak o kodzie 034.) Wcześniej czy później instrukcja for znajdzie ten indeks i wykona iterację ze zmienną laczony o nadanej wartości "1\034foo". Następnie wywoływana jest funkcja split jak niżej:

split("1\034foo", poszczegolne, "\034")

Wynikiem jest przypisanie poszczegolne[1] wartości "1", a poszczegolne[2] wartości "foo". Presto, została odtworzona pierwotna sekwencja indeksów.

11.11. Efektywne wykorzystywanie tablic

Ta sekcja odnosi się tylko do gawk.

Często przydaje się stosować tę samą daną jako indeks w wielu tablicach. Z powodu metody implementacji tablic asocjacyjnych przez gawk, gdy potrzebujemy użyć danych wejściowych jako indeksu w wielu tablicach, znacznie efektywniej jest przypisać pole wejściowe do osobnej zmiennej, a następnie zastosować jako indeks tę zmienną.

{
      imie = $1
      skladka = $2
      ndz = $3
      ...
      starsi[imie]++       # lepiej niż starsi[$1]++
      dzieci[imie] = ndz   # lepiej niż dzieci[$1] = ndz
}

W każdym razie, stosowanie do pól wejściowych odrębnych zmiennych o mnemonicznych nazwach czyni program czytelniejszym. Ostatecznym celem jest uzyskanie możliwie dużej efektywności indeksowania tablic gawk, bez względu na źródło wartości indeksu.

 


Przejdź do pierwszej, poprzedniej, następnej, ostatniej sekcji, spisu treści.

 
Linki sponsorowane

W celu realizacji usług i funkcji na witrynach internetowych ZUI "ELPRO" stosujemy pliki cookies. Korzystanie z witryny bez zmiany ustawień dotyczących plików cookies oznacza, że będą one zapisywane w urządzeniu wyświetlającym stronę internetową. Więcej szczegółów w Polityce plików cookies.

Akceptuję pliki cookies z tej witryny.