This is an old revision of the document!


Curs 11 - Prelucrarea datelor

  • Cuvinte cheie: date, prelucrare, means, ends, parsare, prezentare, date tabelare, separator de câmpuri, cut, awk, IFS, while read, expresii regulate, metacaractere, grep, shell scripting, for, if, rezultate numerice, grafice
  • Suport de curs
    • Suport (Introducere în sisteme de operare)
      • Capitolul 12 – Shell scripting
        • Secțiunile 12.4, 12.5, 12.6, 12.9
      • Puteți descărca fișierul PDF aferent de aici.
      • Capitolul 1 – Introduction to Regular Expressions
      • Capitolul 2 – Basic Regular Expression Skills

Demo-uri

Pentru rularea demo-urilor de mai jos folosim mașina virtuală USO Demo. Mașina virtuală (în format OVA) poate fi importată în VirtualBox. Comenzile le vom rula în cadrul mașinii virtuale.

Mașina virtuală deține două interfețe de rețea:

  • eth0 pentru accesul la Internet (interfață de tipul NAT)
  • eth1 pentru comunicarea cu sistemul fizic (gazdă, host) (interfață de tipul Host-only Adapter)

Pentru a rula demo-ul avem două opțiuni:

  1. Folosim direct consola mașinii virtuale.
  2. Aflăm adresa IP de pe interfața eth1 a mașinii virtuale și ne conectăm prin SSH, de pe sistemul fizic, folosind comanda
    ssh student@<adresa-IP-vm-eth1>

    unde <adresa-IP-vm-eth1> este adresa IP a interfeței eth1 din cadrul mașinii virtuale.

Pentru conectarea la mașina virtuală folosim numele de utilizator student cu parola student. Contul student are permsiuni de sudo. Folosind comanda

sudo su -

obținem permisiuni privilegiate (de root) în shell.

Dacă dorim să ne conectăm pe SSH iar mașina virtuală nu are adresă IP configurată pe interfața eth1 atunci folosim comanda

sudo dhclient eth1

pentru a obține o adresă IP.

Dacă optăm pentru rularea prin SSH iar sistemul fizic rulează Windows, putem folosi Putty pe post de client SSH pe sistemul fizic.

Comenzile folosite sunt de uz general. Actualizând adresele IP cu adrese potrivite, putem rula cu succes comenzile pe orice sistem sau mașină virtuală Linux.

Obținere arhivă

Pentru parcurgerea demo-urilor, folosim arhiva aferentă. Descărcăm arhiva în mașina virtuală (sau în orice alt mediu Linux) folosind comanda

student@uso-demo:~$ wget http://elf.cs.pub.ro/uso/res/cursuri/curs-11/curs-11-demo.zip
[...]

și apoi dezarhivăm arhiva

student@uso-demo:~$ unzip curs-11-demo.zip 
[...]

și accesăm directorul rezultat în urma dezarhivării

student@uso-demo:~$ cd curs-11-demo/
student@uso-demo:~/curs-11-demo$ ls
user-results.csv

Acum putem parcurge secțiunile cu demo-uri de mai jos. Vom folosi resursele din directorul rezultat în urma dezarhivării.

Splitting folosind cut

Pentru splitting simplu de date tabelare putem folosi utilitarul cut. Acesta permite precizarea unui delimitator și a unui câmp sau a mai multor câmpuri (coloane) care să fie selectate din tabel.

Din fișierul user-results.csv (format CSV – Comma Separated Values) dorim să selectăm doar numele grupurilor. Pentru aceasta selectăm doar a doua coloană și folosim separatorul , (virgulă) folosind comanda

student@uso-demo:~/curs-11-demo$ cut -d ',' -f 2 < user-results.csv
Liceul Teoretic Ștefan Odobleja
Colegiul Național Ion Maiorescu
Colegiul Tehnic Toma Socolescu
Colegiul Național Nichita Stănescu
Liceul Teoretic Benjamin Franklin
Colegiul Național I.L. Caragiale
Colegiul Național Zinca Golescu
[...]

Dacă dorim să “unicizăm” rezultatele și să afișăm doar numele liceelor putem conecta comanda de mai sus la o comandă sort:

student@uso-demo:~/curs-11-demo$ cut -d ',' -f 2 < user-results.csv | sort -u

Colegiul Economic Virgil Madgearu
Colegiul Național Alexandru Odobescu
Colegiul Național Barbu Știrbei
Colegiul Național Cantemir Vodă
Colegiul Național Carol I
Colegiul Național Gheorghe Lazăr
[...]

În urma rulării comenzii de mai sus ne sunt afișate doar numele liceelor.

Dacă ne interesează să afișăm doar identificatorii utilizatorilor și punctajele obținute, atunci folosim comanda

student@uso-demo:~/curs-11-demo$ cut -d ',' -f 1,4 < user-results.csv | head
ionut.asimionesei,0
laura.matei,66
alin.dascalu,285
dragos.konnerth,42
alexandru.corneanu,247
alexandru.tittes,154
[...]

Utilitarul cut are două opțiuni frecvent folosite:

  • opțiunea -d care precizează delimitatorul de câmpuri (field delimiter sau field separator)
  • opțiunea -f care precizează ce câmpuri/coloane dorim să extragem

Splitting și prelucrare folosind while read

Utilitarul cut are dezavantajul că face doar splitting și extrage câmpuri/coloane. Nu putem condiționa extragerea unor câmpuri. De exemplu, dacă dorim extragerea conturilor care au punctaj mai mare ca 500, nu vom putea folosi cut. Putem însă folosi construcția while read într-un script shell.

Pentru a extrage conturile care au punctaj mai mare ca 500, vom folosi scriptul extract-points-500:

student@uso-demo:~/curs-11-demo$ cat extract-points-500 
#!/bin/bash

IFS=','
while read uid school date points; do
    if test "$points" -ge 500; then
        echo "$uid"
    fi
done < user-results.csv

student@uso-demo:~/curs-11-demo$ ./extract-points-500 
mihaela.croitoru
andreea.cismas
elvis.titirca
mihaela.serbana
anjie.teodorescu
[...]

În scriptul extract-points-500 am folosit construcția while read pentru a face split la cele patru coloane din fișierul user-results.csv. Separatorul (delimitatorul) l-am definit cu ajutorul variabilei IFS (Input Field Separator) pe care am inițializat-o la , (virgulă). După split am folosit construcția if pentru a afișa doar conturile utilizatorilor cu punctaj peste 500.

Dacă dorim să afișăm și punctajul obținut (nu doar contul) atunci trebuie doar să modificăm linia de afișare (care folosește comanda echo). Rezultatul va fi scriptul actualizat și cu rularea de mai jos:

student@uso-demo:~/curs-11-demo$ cat extract-points-500 
#!/bin/bash

IFS=','
while read uid school date points; do
    if test "$points" -ge 500; then
        echo "$uid,$points"
    fi
done < user-results.csv

student@uso-demo:~/curs-11-demo$ ./extract-points-500
mihaela.croitoru,516
andreea.cismas,803
elvis.titirca,501
mihaela.serbana,526
anjie.teodorescu,666
georgiana.ciobanica,1047
[...]

Dacă în output-ul de mai sus dorim să avem sortare în ordine descrescătoare a punctajului, înlănțuim o comandă sort care să sorteze numeric, descrescător după a doua coloană

student@uso-demo:~/curs-11-demo$ ./extract-points-500 | sort -t ',' -k 2,2rn
radu.dumitru5227,21433
mihaela.catai,13623
stefania.oprea,9547
alexandra.calinescu,5266
george.ungureanu,3846
dragos.totu,2040
monica.cirisanu,1815

În comanda de mai sus, comanda sort sortează output-ul scriptului folosind ca separator virgulă (construcția -t ',' după a două coloană (construcția -k 2,2) descrescător numeric (construcția rn).

Construcția while read este folosită pentru a putea face prelucrări pe fiecare linie procesată, nu doar splitting, așa cum face comanda cut.

Splitting și prelucrare folosind awk

Construcția while read este utilă pentru realizarea de splitting și de prelucări minimale. Prelucrările pe care le poate face țin de facilitățile pe care le oferă shell-ul.

Pentru prelucrări mai avansate recomandăm folosirea utilitarului awk. Utilitarul awk are în spate un limbaj propriu, asemănător limbajului C, și are suport de expresii regulate. Este un utilitar puternic util pentru prelucrarea datelor în format text.

Pentru a obține același efect cu al scriptului extract-points-500 putem folosi oneliner-ul de mai jos:

student@uso-demo:~/curs-11-demo$ awk -F ',' '{ if ($4 >= 500) print $1;}' < user-results.csv 
mihaela.croitoru
andreea.cismas
elvis.titirca
mihaela.serbana
anjie.teodorescu
georgiana.ciobanica
[...]

Pentru a afișa și punctajul folosim one liner-ul:

student@uso-demo:~/curs-11-demo$ awk -F ',' '{ if ($4 >= 500) print $1 "," $4;}' < user-results.csv 
mihaela.croitoru,516
andreea.cismas,803
elvis.titirca,501
mihaela.serbana,526
anjie.teodorescu,666
georgiana.ciobanica,104
[...]

Observăm că pentru awk separatorul este dat de opțiunea -F, iar sintaxa este similară cu cea a limbajului C. Un câmp/coloană este indicat de construcția $N unde N este indexul câmpului; în cazul nostru am folosit $1 și $4 pentru cont și punctaj, respectiv.

După cum am precizat, awk are suport de expresii regulate. Dacă, de exemplu, din output-ul de mai sus dorim să extragem doar liniile care au un nume de cont al cărui nume de familie începe cu litera c, vom folosi construcția:

student@uso-demo:~/curs-11-demo$ awk -F ',' '$1 ~ /[^\.]+\.c/ { if ($4 >= 500) print $1 "," $4;}' < user-results.csv 
mihaela.croitoru,516
andreea.cismas,803
georgiana.ciobanica,1047
ion.camasa,502
mihaela.catai,13623
alexandra.cismaru,860
[...]

În one liner-ul de mai sus, am selectat doar acele linii pentru care primul câmp ($1) face match pe expresia regulată [^\.]+\.c (adică numele de familie începe cu litera c).

În awk, sed și alte utilitare similare expresiile regulate se plasează între slash-uri; mai sus a fost vorba de /[^\.]+\.c/.

Același rezultat ca mai sus putea fi realizat și cu ajutorul comenzii grep legată de la comanda awk, ca mai jos:

student@uso-demo:~/curs-11-demo$ awk -F ',' '{ if ($4 >= 500) print $1 "," $4;}' < user-results.csv | grep '^[^\.]\+\.c'
mihaela.croitoru,516
andreea.cismas,803
georgiana.ciobanica,1047
ion.camasa,502
mihaela.catai,13623
alexandra.cismaru,860
[...]

Rezultatul este același și poate părea mai simplu să folosim grep. Doar că awk permite match cu expresie regulată pe un câmp specific primit la intrare; se poate forța acest lucru și cu grep dar devine mai puțin clar.

În momentul în care un script awk devine mai complicat, poate fi plasat într-un script shell, așa cum este în fișierul extract-points-500-awk:

student@uso-demo:~/curs-11-demo$ cat extract-points-500-awk 
#!/bin/bash

awk -F ',' '
$1 ~ /[^\.]+\.c/ {
    if ($4 >= 500)
        print $1 "," $4;
    }
' < user-results.csv

student@uso-demo:~/curs-11-demo$ ./extract-points-500-awk
mihaela.croitoru,516
andreea.cismas,803
georgiana.ciobanica,1047
[...]

Scriptul extract-points-500-awk face același lucru ca one liner-ul anterior doar că este mai lizibil.

În general, dacă utilizatorul dorește să obțină un efect rapid și cu șanse mici de repetare, va folosi un one liner. Altfel, va folosi un script shell pe care îl poate actualiza șî rula în mod repetat.

Splitting și prelucrare folosind Python

Pentru prelucrări complexe sau pentru integrarea prelucrărilor cu alte componente ale unei aplicații, inclusiv awk poate fi insuficient. În acest caz programatorul va apela la un limbaj specific precum Python, Perl, Ruby, Lua, Java, JavaScript sau altul.

În fișierul extract-points-500.py de mai jos avem implementarea în Python a aceleiași funcționalități ca mai sus pentru awk

extract-points-500.py
#!/usr/bin/env python
 
import sys
import re
 
 
def main():
    for line in open("user-results.csv", "rt"):
        line = line.rstrip("\n")
        uid, school, date, points = line.split(",")
        if re.match("[^\.]+\.c", uid):
            if int(points) >= 500:
                print "%s,%s" % (uid,points)
 
 
if __name__ == "__main__":
    sys.exit(main())

În cadrul scriptului citim linie cu linie conținutul fișierului user-results.csv și apoi este splitted și se extrag liniile pentru care numele contului are un nume de familie care începe cu litere c și punctajul este mai mare ca 500.

Rularea scriptului Python conduce la același rezultat ca în cazul folosirii awk:

student@uso-demo:~/curs-11-demo$ ./extract-points-500.py 
mihaela.croitoru,516
andreea.cismas,803
georgiana.ciobanica,1047
ion.camasa,502
mihaela.catai,13623
alexandra.cismaru,860
[...]

Python oferă o flexibilitate superioară awk cu dezavantajul unei complexități mai mari a codului. Pentru acțiuni rapide, integrabile cu shell scripting, awk este o bună alegere (sau while read); pentru acțiuni mai complexe, un limbaj de programare dedicat, precum Python poate fi o soluție.

De exemplu, putem augmenta scriptul anterior pentru a afișa doar informații despre acele conturi care s-au autentificat prima oară în luna aprilie. Adică al treilea câmp este din luna aprilie. Pentru aceasta folosim scriptul extract-points-500-date.py din director:

extract-points-500-date.py
#!/usr/bin/env python
 
import sys
import re
from datetime import datetime
 
 
def main():
    for line in open("user-results.csv", "rt"):
        line = line.rstrip("\n")
        uid, school, date, points = line.split(",")
        date_compare = datetime.strptime("2015-04-01 00:00:00", "%Y-%m-%d %H:%M:%S")
        try:
            date_in_format = datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
        except:
            continue
        if re.match("[^\.]+\.c", uid):
            if int(points) >= 500:
                if date_in_format > date_compare:
                    print "%s,%s,%s" % (uid,date,points)
 
 
if __name__ == "__main__":
    sys.exit(main())

În scriptul de mai sus am comparat data (al treilea câmp) cu data de 1 aprilie 2015 și am afișat acele câmpuri pentru care câmpul dată era mai târziu de 1 aprilie 2015. Acest lucru ar fi putut fi realizat în awk dar ar fi complicat scriptul.

Pentru rularea scriptului, folosim construcția

student@uso-demo:~/curs-11-demo$ ./extract-points-500-date.py 
mihaela.catai,2015-04-16 12:29:26,13623
andra.cristiev,2015-04-22 13:08:57,865

Decizia de folosire cut sau while read sau awk sau Python ține atât de datele și complexitatea problemei cât și de experiența utilizatorului/dezvoltatorului. În general, recomandăm “use the best tool for the best job”; dacă un utilitar simplu poate face ce aveți nevoie, folosiți-l pe acela, chiar dacă un utilitar mai complex are mai multe caracteristici; dacă acele caracteristici nu sunt utile, nu are sens să-l folosiți.

Folosire expresii regulate în sed

Mai sus am folosit cu awk, grep și Python expresii regulate. Adesea, în linia de comandă veți folosi expresii regulate folosind comanda grep. Dar și în alte situații, fie în linie de comandă, fie în scripting, fie în limbaje de programare, expresiile regulate sunt utile.

În particular, utilitarul sed folosește expresii regulate. Unul dintre cazurile frecvente de utilizare a sed este pe post de editor neinteractiv, care să înlocuiască (substituie) elemente de pe o linie cu alte elemente.

Dacă dorim, de exemplu, să înlocuim toate prenumele din numele de cont nu șirul aaa vom folosi un one liner precum cel de mai jos:

student@uso-demo:~/curs-11-demo$ sed 's/^[^\.]\+/aaa/g' < user-results.csv
aaa.asimionesei,Liceul Teoretic Ștefan Odobleja,2015-03-31 16:18:01,0
aaa.matei,Colegiul Național Ion Maiorescu,2015-03-17 11:04:49,66
aaa.dascalu,Colegiul Tehnic Toma Socolescu,None,285
aaa.konnerth,Colegiul Național Nichita Stănescu,None,42
aaa.corneanu,Liceul Teoretic Benjamin Franklin,2015-03-18 10:26:21,247
[...]

La fel ca în cazul awk expresia regulată se plasează între slash-uri. Expresia regulată folosită ^[^\.]\+ face match pe începutul de linie și pe șiruri de cel puțin o literă care nu conțin punct (., dot). Adică exact pe prenumele din numele de cont.

Dacă dorim să eliminăm prenumele (împreună cu punctul) din numele de cont vom folosi o construcție precum

student@uso-demo:~/curs-11-demo$ sed 's/^[^\.]\+\.//g' < user-results.csv
asimionesei,Liceul Teoretic Ștefan Odobleja,2015-03-31 16:18:01,0
matei,Colegiul Național Ion Maiorescu,2015-03-17 11:04:49,66
dascalu,Colegiul Tehnic Toma Socolescu,None,285
konnerth,Colegiul Național Nichita Stănescu,None,42
corneanu,Liceul Teoretic Benjamin Franklin,2015-03-18 10:26:21,247
[...]

În one liner-ul de mai sus am lăsat ca șir care substituie șirul vid (adică două slash-uri consecutive). Adică înlocuim partea pe care face match expresia regulată (adică prenumele din cont și caracterul punct) cu nimic, adică o ștergem.

O situație poate fi să fie înlocuită construcția prenume.nume cu nume.prenume în numele contului. Pentru aceasta folosim construcția

student@uso-demo:~/curs-11-demo$ sed 's/^\([^\.]\+\)\.\([^\,]\+\),/\2.\1,/g' < user-results.csv
asimionesei.ionut,Liceul Teoretic Ștefan Odobleja,2015-03-31 16:18:01,0
matei.laura,Colegiul Național Ion Maiorescu,2015-03-17 11:04:49,66
dascalu.alin,Colegiul Tehnic Toma Socolescu,None,285
konnerth.dragos,Colegiul Național Nichita Stănescu,None,42
corneanu.alexandru,Liceul Teoretic Benjamin Franklin,2015-03-18 10:26:21,247
[...]

Expresia regulată folosită mai sus (^\([^\.]\+\)\.\([^\,]\+\),) o decodificăm în următoarele componente:

  • ^: început de linie
  • \([^\.]\+\): face match pe prenume, adică șirul format din cel puțin un caracter diferit de punct; reține acest șir într-o variabilă (variabila este referită prin construcția \1)
  • \.: face match pe caracterul punct (., dot) care separă prenumele de nume în cont
  • \([^\,]\+\): face match pe prenume, adică șirul format din cel puțin un caracter diferit de virgulă; reține acest șir într-o variabilă (variabila este referită prin construcția \2)
  • ,: face match pe caracterul virgulă (,, comma) care urmează după cont

Partea de înlocuit este \2.\1, adică numele de cont este înlocuit cu nume, urmat de punct, urmat de prenume, urmat de virgulă, așa cum ne-am dorit.

La fel ca în cazul awk, sed este un utilitar adecvat pentru one linere, acțiuni rapide și încorporare în shell scripting. Pentru scenarii de utilizare mai complexe, recomandăm folosirea unor limbaje dedicate precum Python, Perl, Ruby, Java, JavaScript.

Automatizare folosind shell scripting

Folosirea shell scripting și a one line-erelor are, de principiu, 3 scenarii de utilizare:

  • conectarea/înlănțuirea mai multor comenzi pentru a obține un rezultat nou pentru un caz punctual de utilizare
  • automatizarea unei acțiuni: acțiunile pot fi repetitive și atunci o implmentare poate fi rulată de mai multe ori
  • prelucrarea datelor: folosind comenzi și construcții de tip filtre de text (cut, while read, tr, grep, sort, awk, sed) se prelucrează date în format text

Partea de automatizare este utilă atunci când vrem să executăm o acțiune în mod repetat. Un caz de utilizare este când vrem să prelucrăm în același mod mai multe fișiere.

De exemplu, având în vedere conținutul subdirectorului horde/, vrem să creăm copii de lucru ale fișierelor de configurare de distribuție. Fișierele de distribuție au extensia .dist (de exemplu conf.php.dist); o copie de lucru este un fișier fără extensia .dist (de exemplu: conf.php).

Pentru a crea copie de lucru putem folosi următorul one liner, construit pas cu pas:

student@uso-demo:~/curs-11-demo$ find horde/ -name '*.dist'
horde/ingo/config/hooks.php.dist
horde/ingo/config/prefs.php.dist
[...]

student@uso-demo:~/curs-11-demo$ for f in $(find horde/ -name '*.dist'); do echo "$f"; done
horde/ingo/config/hooks.php.dist
horde/ingo/config/prefs.php.dist
[...]

student@uso-demo:~/curs-11-demo$ for f in $(find horde/ -name '*.dist'); do echo "${f/.dist/}"; done
horde/ingo/config/hooks.php
horde/ingo/config/prefs.php
[...]

student@uso-demo:~/curs-11-demo$ for f in $(find horde/ -name '*.dist'); do cp "$f" "${f/.dist/}"; done

În ultima comandă avem one liner-ul care creează câte o copie a fișierului de distribuție într-un fișier de lucru. Construcția ${f/.dist/} este folosită pentru a înlocui în valoarea variabilei f șirul .dist cu nimic, adică șterge acel șir, rezultând numele fișierului fără extensia .dist.

Pentru verificare folosim comanda find pentru a valida existența fișierelor de lucru:

student@uso-demo:~/curs-11-demo$ find horde/ -name '*.php'
horde/ingo/config/prefs.php
horde/ingo/config/backends.php
[...]

One liner-ul de mai sus este util o singură dată, pentru acele fișiere și de acceea nu are sens să îl trecem într-un script pe care să îl rulăm periodic, la nevoia, ca automatizare.

Un exemplu de script folosit pentru automatizare este scriptul publish-slides folosit pentru publicarea slide-urilor de cursuri de USO. Nu veți putea rula scriptul în absența fișierelor de suport, dar este reprezentantiv pentru ceea ce înseamnă automatizarea unei sarcini repetitive: publicarea slide-urilor cursului de USO intern în cadrul echipei (în Dropbox) și studenților.

publish-slides
#!/bin/bash
 
dropbox_folder=~/Downloads/Dropbox/school/uso-shared
remote_end=uso@elf.cs.pub.ro:res/current/cursuri
 
if test $# -eq 1; then
    id=$(printf "%02g" $1)
    pushd curs-$id/ > /dev/null 2>&1
    make all
    cp *.pdf "$dropbox_folder"/curs-$id/
    scp *handout*.pdf "$remote_end"/curs-$id/
    popd > /dev/null 2>&1
    exit 0
fi
 
for id in $(seq -f "%02g" 0 13); do
    pushd curs-$id/ > /dev/null 2>&1
    make all
    cp *.pdf "$dropbox_folder"/curs-$id/
    scp *handout*.pdf "$remote_end"/curs-$id/
    popd > /dev/null 2>&1
done

Scriptul compilează (folosind make) slide-urile cursului primit ca argument, sau toate slide-urile în absența argumentelor. Folosește cp pentru a copia slide-urile intern echipei, în Dropbox, și folosește scp pentru a publica slide-urile către studenți.

uso/cursuri/curs-11.1451848372.txt.gz · Last modified: 2016/01/03 21:12 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0