Usa awk per calcolare la frequenza delle lettere
Scrivi uno script awk per determinare le lettere più (e meno) comuni in un insieme di parole.
Recentemente ho iniziato a scrivere un gioco in cui costruisci parole usando le tessere delle lettere. Per creare il gioco, avevo bisogno di conoscere la frequenza delle lettere nelle parole regolari in lingua inglese, in modo da poter presentare un utile set di tessere con lettere. La frequenza delle lettere è discussa in vari posti, anche su Wikipedia, ma volevo calcolarla io stesso.
Linux fornisce un elenco di parole nel file /usr/share/dict/words
, quindi ho già un elenco di parole probabili da utilizzare. Il file words
contiene molte parole che desidero, ma alcune che non mi interessano. Volevo un elenco di tutte le parole che non fossero parole composte (senza trattini o spazi) o nomi propri (senza lettere maiuscole). Per ottenere tale elenco, posso eseguire il comando grep
per estrarre solo le righe costituite esclusivamente da lettere minuscole:
$ grep '^[a-z]*$' /usr/share/dict/words
Questa espressione regolare chiede a grep
di corrispondere a modelli costituiti solo da lettere minuscole. I caratteri ^
e $
nel modello rappresentano rispettivamente l'inizio e la fine della riga. Il raggruppamento [a-z]
corrisponderà solo alle lettere minuscole da a a z.
Ecco un breve esempio dell'output:
$ grep '^[a-z]*$' /usr/share/dict/words | head
a
aa
aaa
aah
aahed
aahing
aahs
aal
aalii
aaliis
E sì, sono tutte parole valide. Ad esempio, "aahed" è l'esclamazione passata di "aah", come nel rilassamento. E un "aalii" è un cespuglioso arbusto tropicale.
Ora devo solo scrivere uno script gawk
per contare le lettere in ogni parola e quindi stampare la frequenza relativa di ogni lettera trovata.
Contare le lettere
Un modo per contare le lettere in gawk
è scorrere ogni carattere in ciascuna riga di input e contare le occorrenze di ogni lettera da a a z. La funzione substr
restituirà una sottostringa di una determinata lunghezza, ad esempio una singola lettera, da una stringa più grande. Ad esempio, questo esempio di codice valuterà ogni carattere c
dall'input:
{
len = length($0); for (i = 1; i <= len; i++) {
c = substr($0, i, 1);
}
}
Se inizio con una stringa globale LETTERS
che contiene l'alfabeto, posso utilizzare la funzione index
per trovare la posizione di una singola lettera nell'alfabeto. Espanderò l'esempio di codice gawk
per valutare solo le lettere da a a z nell'input:
BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
{
len = length($0); for (i = 1; i <= len; i++) {
c = substr($0, i, 1);
ltr = index(LETTERS, c);
}
}
Tieni presente che la funzione indice restituisce la prima occorrenza della lettera dalla stringa LETTERS
, iniziando con 1 alla prima lettera o zero se non viene trovata. Se ho un array lungo 26 elementi, posso usarlo per contare le occorrenze di ciascuna lettera. Aggiungerò questo al mio esempio di codice per incrementare (usando ++
) il conteggio per ogni lettera come appare nell'input:
BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
{
len = length($0); for (i = 1; i <= len; i++) {
c = substr($0, i, 1);
ltr = index(LETTERS, c);
if (ltr > 0) {
++count[ltr];
}
}
}
Stampa della frequenza relativa
Dopo che lo script gawk
ha contato tutte le lettere, voglio stampare la frequenza di ogni lettera trovata. Non mi interessa il numero totale di ciascuna lettera dell'input, ma piuttosto la frequenza relativa di ciascuna lettera. La frequenza relativa ridimensiona i conteggi in modo che la lettera con il minor numero di occorrenze (come la lettera q) sia impostata su 1 e le altre lettere siano relative a quella.
Inizierò con il conteggio della lettera a, quindi confronterò quel valore con i conteggi di ciascuna delle altre lettere da b a z:
END {
min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
if (count[ltr] < min) {
min = count[ltr];
}
}
}
Alla fine di questo ciclo, la variabile min
contiene il conteggio minimo per qualsiasi lettera. Posso usarlo per fornire una scala per i conteggi per stampare la frequenza relativa di ciascuna lettera. Ad esempio, se la lettera con l'occorrenza più bassa è q, min
sarà uguale al conteggio q.
Quindi eseguo il ciclo di ciascuna lettera e la stampo con la sua frequenza relativa. Divido ciascun conteggio per min
per stampare la frequenza relativa, il che significa che la lettera con il conteggio più basso verrà stampata con una frequenza relativa pari a 1. Se un'altra lettera appare due volte più spesso del conteggio più basso, ciò la lettera avrà una frequenza relativa di 2. Qui mi interessano solo i valori interi, quindi 2.1 e 2.9 sono uguali a 2 per i miei scopi:
END {
min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
if (count[ltr] < min) {
min = count[ltr];
}
}
for (ltr = 1; ltr <= 26; ltr++) {
print substr(LETTERS, ltr, 1), int(count[ltr] / min);
}
}
Mettere tutto insieme
Ora ho uno script gawk
che può contare la frequenza relativa delle lettere nel suo input:
#!/usr/bin/gawk -f
# only count a-z, ignore A-Z and any other characters
BEGIN { LETTERS = "abcdefghijklmnopqrstuvwxyz" }
{
len = length($0); for (i = 1; i <= len; i++) {
c = substr($0, i, 1);
ltr = index(LETTERS, c);
if (ltr > 0) {
++count[ltr];
}
}
}
# print relative frequency of each letter
END {
min = count[1]; for (ltr = 2; ltr <= 26; ltr++) {
if (count[ltr] < min) {
min = count[ltr];
}
}
for (ltr = 1; ltr <= 26; ltr++) {
print substr(LETTERS, ltr, 1), int(count[ltr] / min);
}
}
Lo salverò in un file chiamato letter-freq.awk
in modo da poterlo utilizzare più facilmente dalla riga di comando.
Se preferisci, puoi anche usare chmod +x
per rendere il file eseguibile da solo. Il #!/usr/bin/gawk -f
sulla prima riga significa che Linux lo eseguirà come script utilizzando il programma /usr/bin/gawk
. E poiché la riga di comando gawk
utilizza -f
per indicare quale file deve utilizzare come script, è necessario che si aggiunga -f
in modo che l'esecuzione letter-freq.awk
nella shell verrà interpretato correttamente come se si eseguisse invece /usr/bin/gawk -f letter-freq.awk
.
Posso testare lo script con alcuni semplici input. Ad esempio, se inserisco l'alfabeto nel mio script gawk
, ogni lettera dovrebbe avere una frequenza relativa pari a 1:
$ echo abcdefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk
a 1
b 1
c 1
d 1
e 1
f 1
g 1
h 1
i 1
j 1
k 1
l 1
m 1
n 1
o 1
p 1
q 1
r 1
s 1
t 1
u 1
v 1
w 1
x 1
y 1
z 1
Ripetendo l'esempio ma aggiungendo un'ulteriore istanza della lettera e verrà stampata la lettera e con una frequenza relativa di 2 e ogni altra lettera come 1:
$ echo abcdeefghijklmnopqrstuvwxyz | gawk -f letter-freq.awk
a 1
b 1
c 1
d 1
e 2
f 1
g 1
h 1
i 1
j 1
k 1
l 1
m 1
n 1
o 1
p 1
q 1
r 1
s 1
t 1
u 1
v 1
w 1
x 1
y 1
z 1
E ora posso fare il grande passo! Utilizzerò il comando grep
con il file /usr/share/dict/words
e identificherò la frequenza delle lettere per tutte le parole scritte interamente in lettere minuscole:
$ grep '^[a-z]*$' /usr/share/dict/words | gawk -f letter-freq.awk
a 53
b 12
c 28
d 21
e 72
f 7
g 15
h 17
i 58
j 1
k 5
l 36
m 19
n 47
o 47
p 21
q 1
r 46
s 48
t 44
u 25
v 6
w 4
x 1
y 13
z 2
Di tutte le parole minuscole nel file /usr/share/dict/words
, le lettere j, q e x si verificano meno frequentemente. Anche la lettera z è piuttosto rara. Non sorprende che la lettera e sia quella usata più frequentemente.