Skript pro frekvenční analýzu textu

Není nic nepříjemnějšího, než pocit zbytečné a promarněné práce. Když na FITu byly k programování libovolné projekty, své programy jsem dával veřejně k dispozici. Teď už, můžu říci díky bohu, nestuduji. Ovšem mí přátelé se, bohužel, této hrůzy nezbavili a i když odešli a studují na jiné fakultě, přesto některé předměty na FITu mají. Jeden z projektů se týkal šifrování a za bonusový bod bylo naprogramování nástroje pro frekvenční analýzu textu. A protože mě programování baví, pustil jsem se do tohoto úkolu. Script nebyl nikým použit, uveřejňuji až po termínu odevzdání.

Chceme jednoduchý skript, kterému předložíme textový soubor (.txt, nikoli .doc) a skript spočte počet písmen v souboru, načež vše vypíše. OK, pojďme do toho.

Jako všechny skripty i tento začne obligátním

  1. #!/bin/bash

a funkcí pro tisk nápovědy:

  1. function print_help () {
  2. echo "Tool for freq. analyse
  3.  
  4. USAGE: fra.sh -f <foo.txt>
  5. -f foo.txt - file foo.txt contained hashed text
  6.  
  7. note: foo.txt MUST be ASCII text
  8. note2: script ignores non-English-alphabet letters
  9.  
  10. by Petos.cz; under GPLv3+"
  11. exit 0
  12. }

Jsem zvyklý, že funkce či skript musí vždy a všude podporovat aspoň dva parametry. -h a --help. -h vyřešíme později, –-help jednoduchým ifem.

  1. if [ "$1" = "--help" ]; then
  2. print_help
  3. fi

A zbytek parametrů se postará
  1. while getopts "hf:" optname
  2. do
  3. case "$optname" in
  4. "h")
  5. print_help;
  6. ;;
  7. "f")
  8. FILE="$OPTARG"
  9. ;;
  10. "?")
  11. echo "Unknown option $OPTARG"
  12. ;;
  13. ":")
  14. echo "No argument value for option $OPTARG"
  15. ;;
  16. *)
  17. # Should not occur
  18. echo "Unknown error while processing options"
  19. ;;
  20. esac
  21. done

Je zřejmé, že skript bude potřebovat parametr -f SOUBOR, takže překontrolujeme, že parametr -f byl zadán i se souborem a že soubor existuje.

  1. if [ -z "$FILE" ]; then
  2. echo "Error, -f parametr is mandatory!"
  3. exit 1
  4. fi
  5.  
  6. if [ ! -f $FILE ]; then
  7. echo "Error, file $FILE does not exist"
  8. exit 1
  9. fi

Moc jsem se nezdržoval převody textů, formátů a kontrolami, co vše je podporováno a co ne. Přesto jsem se rozhodl, že aspoň jednu rychlou kontrolu provádím. Ale je možné vše zakomentovat, případně upravit tak, aby to vyhovovalo:

  1. FILETYPE=$(file -b $FILE)
  2. if [ "$FILETYPE" != "ASCII text" ]; then
  3. echo "Error, file is not ASCII"
  4. exit 1
  5. fi

Vím, že další řádek je… nepěkný, ale protože BASH, ve kterém jsem celý tento prográmek dělal, má problémy s exportováním promenných v případě, že se použije pipe, je tento krok nutný:
  1. SHIFRA=$(cat $FILE)

Do pole CHARMAP uložím všechny písmena abecedy tak, aby „a“ bylo na pozici nula a „z“ na pozici 25.

  1. CHARMAP=(a b c d e f g h i j k l m n o p q r s t u v w x y z)

Vedle pole CHARMAP budu používat ještě pole CHARFIELD, ve kterém budou uloženy počty jednotlivých znaků. Toto pole zpočátku vynuluji, aby bylo prázdné.

  1. for i in `seq 0 25`; do
  2. CHARFIELD[i]=0
  3. done

Nyní postupně každý řádek vstupního souboru (uloženého v proměnné $SHIFRA) zbavím všech prázdných znaků, z čehož mi vznikne „jednoslovný řádek“. Tento jednoslovný řádek uložím do pole zvaného ONELINE, které bude mít jen jeden člen:

  1. while read LINE; do
  2. ONELINE=$(echo "$LINE" | tr -d [[:space:]] )

Do proměnné CHARSsi uložím počet znaků, které pole ONELINE obsahuje:
  1. CHARS=${#ONELINE[0]}

A spustím smyčku, kde každý znak porovnám a v poli s počtem výzkytů znaků zvednu patřičný člen o jedničku. Bohužel BASH není při práci s poli a matematikou zrovna nejšikovnější, takže to musím řešit ne uplně elegantně. Hezčí by bylo, kdyby fungovalo ((${CHARFIELD[0]}++)), ale to bohužel není možné. Tedy:
  1. for wrd in `seq 0 $CHARS`;
  2. do
  3. CASECHAR=${ONELINE[0]:$wrd:1}
  4. case $CASECHAR in
  5. [aA]) CHARFIELD[0]=${CHARFIELD[0]}+1;;
  6. [bB]) CHARFIELD[1]=${CHARFIELD[1]}+1;;
  7. [cC]) CHARFIELD[2]=${CHARFIELD[2]}+1;;
  8. [dD]) CHARFIELD[3]=${CHARFIELD[3]}+1;;
  9. [eE]) CHARFIELD[4]=${CHARFIELD[4]}+1;;
  10. [fF]) CHARFIELD[5]=${CHARFIELD[5]}+1;;
  11. [gG]) CHARFIELD[6]=${CHARFIELD[6]}+1;;
  12. [hH]) CHARFIELD[7]=${CHARFIELD[7]}+1;;
  13. [iI]) CHARFIELD[8]=${CHARFIELD[8]}+1;;
  14. [jJ]) CHARFIELD[9]=${CHARFIELD[9]}+1;;
  15. [kK]) CHARFIELD[10]=${CHARFIELD[10]}+1;;
  16. [lL]) CHARFIELD[11]=${CHARFIELD[11]}+1;;
  17. [mM]) CHARFIELD[12]=${CHARFIELD[12]}+1;;
  18. [nN]) CHARFIELD[13]=${CHARFIELD[13]}+1;;
  19. [oO]) CHARFIELD[14]=${CHARFIELD[14]}+1;;
  20. [pP]) CHARFIELD[15]=${CHARFIELD[15]}+1;;
  21. [qQ]) CHARFIELD[16]=${CHARFIELD[16]}+1;;
  22. [rR]) CHARFIELD[17]=${CHARFIELD[17]}+1;;
  23. [sS]) CHARFIELD[18]=${CHARFIELD[18]}+1;;
  24. [tT]) CHARFIELD[19]=${CHARFIELD[19]}+1;;
  25. [uU]) CHARFIELD[20]=${CHARFIELD[20]}+1;;
  26. [vV]) CHARFIELD[21]=${CHARFIELD[21]}+1;;
  27. [wW]) CHARFIELD[22]=${CHARFIELD[22]}+1;;
  28. [xX]) CHARFIELD[23]=${CHARFIELD[23]}+1;;
  29. [yY]) CHARFIELD[24]=${CHARFIELD[24]}+1;;
  30. [zZ]) CHARFIELD[25]=${CHARFIELD[25]}+1;;
  31. esac
  32. done
  33. done < <(echo "$SHIFRA")

Jediný výstup budou dva sloupečky: „písmeno četnost“.

  1. for i in `seq 0 25`; do
  2. echo ${CHARMAP[i]} ${CHARFIELD[i]}
  3. done

Ještě několik poznámek k tomu, co se děje v případě přiřazování:

  • na řádku 58 se postupně generuje proměnná wrd a to v hodnotách od nuly až po počet znaků v poli ONELINE.
  • O prográmku seq A B C jsem už psal – generuje postupně čísla od A do B s krokem C. Výchozí nastavení seq A generuje jednotlivá následující čísla do A od jedničky
  • CASECHAR=${ONELINE[0]:$wrd:1} je trochu složitější na vysvětlení. Do proměné CASECHAR se uloží požadovaný $wrd-tý znak, který se získá jako položka na pozici nula v poli ONELINE, offset $wrt a jeden vypsaný znak.

Je mi jasné, že se najdou tací, kteří v tomto skriptu uvidí mnoho nedodělků, či by to udělali lépe. Souhlasím. Na druhou stranu jsem celý skript delal jako bych byl studentem — na poslední chvíli před odevzdáním a skript byl funkční během hodiny práce 🙂 Na což jsem celkem pyšný, neb skript dělal to, co dělat měl a má. Není to žádná bomba, ale neurazí 🙂

4 komentáře u „Skript pro frekvenční analýzu textu“

  1. Pominu-li, že bash je na toto extrémě nevhodný (je výborný na jiné věci), tak spawnování procesu /usr/bin/echo pro _každý řádek i znak_ je teda „brutál“, toto bych nechtěl spouštět na větším souboru. Je to úplně zbytečné, bash samotný umí aritmetické operace přímo sám.

    Pro informaci Bash 4.0+ podporuje jak asociativní pole, takže by to šlo napsat podstatně efektivněji bez toho switche, a to i za hodinu 😉

    1. Jak jsem psal nekde ve clanku, nepovedlo se mi donutit bash, aby udelal
      $(( VAR++ )) tam, kde VAR je, stejne jak v tomto pripade, nekde v poli. Toho, co jsi napsal, si jsem sam vedom a nekolikrat jsem to i v zapisku uvedl, ale nepovedlo se mi to udelat lepe. Budu rad, kdyz mi to napises, jak to udelat.

        1. Kvuli problemum s exportem promennych ven z
          cat $FILE | while read LINE…
          loopu (kvuli pipe) mi nefungoval pristup s VAR=VAR+1, proto jsem zacal pouzivat tu vec s echo a uz se k VAR=VAR+1 nevratil.
          Jinak pro veci mimo pole $ (( ++VAR)) funguje…

          [safarikp@AcisionWork ~]$ VAR=1
          [safarikp@AcisionWork ~]$ (( ++VAR))
          [safarikp@AcisionWork ~]$ echo $VAR
          2
          [safarikp@AcisionWork ~]$

          Pole hodnot uz je asi prilis slozite… Kazdopadne jsem to upravil a uhladil. Ale jak rikam, cilem nebyl dokonaly program, ale abych si zase oprasil BASH a praci s nim…

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *