momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Sintetizarea etapelor de înregistrare a datelor dintr-o colecţie PGN

AWK | Bash | Crafty | Django | MySQL | Python | grep | sed
2014 aug

În [1] am constituit modelele Python-Django potrivite unei colecţii de partide de şah (colecţie reprezentată iniţial de un fişier PGN obţinut de la [*]) şi am constatat că extragerea şi gruparea adecvată a datelor specifice din fişierul iniţial implică o gamă variată de "instrumente" - încât în principiu, înregistrarea acestor date devine o etapă de sine stătătoare, în cursul realizării unei aplicaţii Web de prezentare a acestor partide. Doar că până la urmă… această "etapă" a devenit ea însăşi, o serie de etape (mai mult sau mai puţin, "de sine stătătoare").

Am folosit numeroase comenzi directe şi scripturi independente (pentru câte o "sub-etapă"), creând şi utilizând de la o etapă la alta, diverse fişiere intermediare. Nu-i de mirare că înregistrarea unei a doua colecţii nu prea reuşeşte de prima dată: am uitat să tastez una dintre comenzile intermediare asumate într-o "subetapă"…

Trebuie să fie posibilă sintetizarea în cel mult două scripturi, a acestor "subetape" şi încă, fără a produce fişiere intermediare: primul script trebuie să conducă la înregistrarea în baza de date a partenerilor şi la separarea partidelor din fişierul iniţial în fişiere PGN individuale - acoperind toate sub-etapele arătate în [1] pentru aceasta; al doilea script va trebui rulat după ce vom fi obţinut fişierele adnotate de Crafty, pentru partidele respective - asigurând înregistrarea în baza de date a partidelor adnotate (finalizând astfel, înregistrarea colecţiei).

Revederea cadrului de lucru

O "colecţie de partide" înseamnă la început, un fişier conţinând partide de şah (în format PGN) ale unui aceluiaşi jucător - numit "selector", sau "coach" - cu diverşi parteneri, din diverse ţări. Astfel de fişiere avem de exemplu, de la [*] - site care include şi o secţiune de partide uşoare: timpul de gândire individual este de 15 minute, permiţând un joc "închegat" (spre deosebire de cazul a două, sau "5 minute"), rezultând uneori partide instructive; jucătorii sunt clasificaţi ("class A", "expert", "master") şi sunt ierarhizaţi prin coeficienţi care variază în funcţie de rezultatul fiecărui joc.

De obicei, într-o sesiune de joc pe [*] (de două-trei ore) nu-ţi faci timp să analizezi partida tocmai încheiată, ci abordezi imediat un alt partener; întrebările de genul "unde puteam să joc mai bine" se cumulează şi rămân nerezolvate. Dacă vrei să-ţi îmbunătăţeşti jocul este necesar efortul de a lămuri nuanţele anumitor poziţii şi de a dezvălui greşelile de gândire şi scăpările tactice, într-o partidă sau alta; neavând dispoziţia necesară pentru aceasta, poţi totuşi să apelezi la un program competent (aici angajăm Crafty), pentru analiza automată a partidelor respective - rămânând să "vizualizezi" cumva partidele deja adnotate, furnizate de programul respectiv.

Am ajuns astfel la ideea unei aplicaţii Web, pe care am început să o dezvoltăm în [1] folosind Python şi Django. Modelul de date Selector înregistrează fiecare "coach"; ca user autorizat, acesta va putea să şteargă din colecţia sa partidele care nu prezintă interes, va putea să trunchieze o partidă la o poziţie care este interesantă şi va putea să adauge propriile comentarii finale, asupra partidei.

Presupunem că deja am înregistrat ţările (folosind modelul Land), prin nume şi codul de două litere aferent (vezi [1]). Modelul Partner serveşte pentru înregistrarea partenerilor tuturor celor existenţi în Selector, indicând pentru fiecare numele sub care joacă şi indexul ţării lui (din Land).

În sfârşit, modelul Game este destinat să conţină textul PGN pentru fiecare partidă adnotată de Crafty, împreună cu "link"-urile corespunzătoare către Selector şi respectiv, Partner.

Modelele Selector şi Partner trebuie "hrănite" cu date extrase din fişierul iniţial (aşa cum este furnizat de [*]); Game va putea fi completat numai după obţinerea partidelor adnotate, de la Crafty.

Înregistrarea selectorului şi noilor parteneri, detaşarea partidelor

În fişierul descărcat de la [*] sfârşitul de linie este marcat prin două caractere: \r\n (CR şi LF), cum este specific sistemelor DOS (dar şi unor protocoale de comunicaţie, de exemplu HTTP). Ne va conveni (folosind Linux) să eliminăm caracterul \r, din finalul fiecărei linii a fişierului.

În fişierul respectiv lipsesc tagurile PGN pentru data desfăşurării partidei şi pentru coeficienţii de ierarhizare; în schimb, apare uneori un tag "...ITeam", pentru "echipa" în care este înregimentat jucătorul. Vom elimina din start liniile cu "ITeam", fiind nerelevante pentru aplicaţia noastră.

Cele două operaţii de eliminare precizate mai sus sunt foarte simplu de "dictat" cu sed. Apoi, scriptul Bash "supply.sh" de mai jos prevede funcţia find_coach() care înlănţuie două comenzi AWK (preluând liniile de program 102-121, referite deja în [1]) - funcţie care determină "selectorul" colecţiei al cărei nume de fişier îi este transmis ca argument. Valoarea "coach" astfel determinată este transferată programului executabil test_coach.py; acesta consultă Selector şi înscrie eventual, noul "coach".

Mai departe, supply.sh extrage datele partenerilor (preluând liniile de program 151-162, referite deja în [1]) şi le transferă direct - fără să creeze un fişier JSON intermediar, ca în [1] - programului executabil partners.py, iar acesta le înscrie în baza de date (conform modelului Partner).

Scriptul supply.sh se încheie cu o linie AWK (documentată în [2]) prin care se constituie un director conţinând câte un fişier PGN pentru fiecare dintre partidele existente în fişierul indicat ca argument; pe acest director de PGN-uri urmează să invocăm Crafty, pentru a obţine partidele adnotate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/bin/bash
<<HELP
    vb@vb:~/slightchess/Doc/SUPPLY$  ls
        GA_08-03-2014.pgn  partners.py  supply.sh  test_coach.py
    vb@vb:~/slightchess/Doc/SUPPLY$  ./supply.sh  GA_08-03-2014.pgn
HELP

sed -i -e 's/\r$//' $1     # Delete "carriage return" character.
sed -i -e "/ITeam/d" $1    # Delete /ITeam/ lines.

# Find "coach" from collection (appears in all games).
find_coach() {
    awk '/White|Black/ && ! /IFlag/  \
        {   nume = $2;
            for(i=3; i <= NF; i++) { 
                nume = nume " " $i 
            }
            print substr(nume, 2, length(nume) - 4)
        }
    ' $1 | awk '{count[$0]++}   
                END { nr_games = 1;    
                      for(i in count) {   
                          if(count[i] > nr_games) {    
                              nr_games = count[i];
                              selector = i
                          }   
                      }
                      print selector
                }'
}
coach=$(find_coach $1)

# Insert (or ignore) the coach in DB (data base).
echo $coach | ./test_coach.py  2>/dev/null

# Extract partners (country, name) and insert/ignore in DB.
for player in "White" "Black"
do
    awk  -v PLY="$player" '$0 ~ PLY' $1    \
     | 
    awk -v selector="${coach}"    \
        '{    
              if(match($2, selector)) {
                  getline; 
                  next
              } 
              print $0
        }'
done | ./partners.py

# Detach games in own files (deleting /IFlag/ lines).
sed -i -e "/IFlag/d" $1
awk '/\[Event \"InstantChess\"\]/{x="Can/G_"++i".pgn";}{print > x;}' $1  
echo $coach > Can/G_0.txt  # Stores coach's name.

În linia 8, expresia regulată /\r$/ vizează caracterul '\r' de la sfârşitul şirului; este prudent să vizăm locul de final de linie, fiindcă - în funcţie de cum deschidem şi accesăm fişierul - putem întâlni codul 13 ('\r') şi între octeţii de reprezentare Unicode a vreunui caracter interior liniei.

În linia 34, echo $coach emite o linie conţinând valoarea încărcată (la linia 31) în variabila coach; linia respectivă nu este afişată pe ecran, ci - folosind | (operatorul "pipe", prin care se conectează "ieşirea" unui program cu "intrarea" altuia) - ea este oferită spre citire programului test_coach.py; acesta citeşte linia respectivă folosind o metodă din sys.stdin şi obţinând numele "coach" (prin eliminarea terminatorului '\n' al liniei) - îl înscrie (dacă nu există deja) în baza de date, anume în tabelul games_selector aferent modelului Selector:

#!/usr/bin/python
'''         ~/slightchess/Doc/SUPPLY/test_coach.py
            Make it executable (chmod +x test_coach.py).    '''
import sys, MySQLdb as mydb
conn = mydb.connect('localhost', 'vb', '123456', 'slightchess', 
                    charset='utf8', use_unicode=True);
cursor = conn.cursor()

coach = sys.stdin.readlines()[0].strip('\n')

cursor.execute(
    'INSERT IGNORE INTO games_selector SET username="%s", instant_username="%s"'
                                           % (coach, coach) )
conn.commit()
cursor.close()
conn.close()

Selector subclasează modelul User din aplicaţia django.contrib.auth (vezi [1]), încât tabelul asociat games_selector conţine şi câmpuri ca password, email, is_staff, etc. - câmpuri a căror setare este normal să o amânăm. Dar aceste câmpuri sunt atributate deja prin NOT NULL, rezultând mesaje de atenţionare ("Field ... doesn't have a default value"); pentru a evita apariţia pe ecran a acestor mesaje, am încheiat linia 34 cu 2 > /dev/null (redirecţionează stderr la "dispozitivul nul").

Scriptul test_coach.py poate fi invocat şi direct, dacă va fi cazul de făcut vreo verificare, sau chiar de a înscrie sumar un nou "coach":

vb@vb:~/slightchess/Doc/SUPPLY$ ./test_coach.py
joe666      # tastăm numele, ENTER şi combinaţia CTRL+D ("End Of File") 
./test_coach.py:15: Warning: Field 'password' doesn't have a default value
...

Scriptul partners.py invocat în linia 49 simplifică programele din [1] pentru înscrierea partenerilor: nu mai este necesară crearea unui fişier JSON (şi nici importarea şi folosirea modulelor codecs şi json), fiindcă liniile de date necesare sunt preluate prin operatorul | de la secvenţa 37 - 49 care le constituie (cum am explicat deja în [1]), fiind citite în program prin sys.stdin.

#!/usr/bin/python
'''         ~/slightchess/Doc/SUPPLY/partners.py
            Make it "executable" (chmod +x partners.py).  '''
import re, sys, MySQLdb as mydb
prog = re.compile(r"^.*\"(.+?)\"\]\n.*\"(.+?)\"\]\n", re.MULTILINE)

partners = ''.join(sys.stdin.readlines())

d1_part = {nume: cod for nume, cod in prog.findall(partners)}
d_part = {}
for nume, cod in d1_part.items():
    if cod in d_part:
        d_part[cod].append(nume)
    else:
        d_part[cod] = [nume]

conn = mydb.connect('localhost', 'vb', '123456', 'slightchess', 
                    charset='utf8', use_unicode=True);
cursor = conn.cursor()
for cod in d_part:
    cursor.execute('SELECT id from games_land where cod="%s"' % cod)
    land_id = cursor.fetchone()[0]
    for nume in d_part[cod]:
        cursor.execute('INSERT IGNORE INTO games_partner SET nume="%s", land_id="%s"'
                        %(nume, land_id))    
conn.commit()
cursor.close()
conn.close()

La linia 52 ştergem liniile care conţin "IFlag" (nemaiavând nevoie de-acum, de codul de ţară). Linia 53 din scriptul Bash de mai sus presupune existenţa unui subdirector Can/, în care creează prin splitarea fişierului iniţial, câte un fişier PGN pentru fiecare partidă; iar ultima linie adaugă în Can/ fişierul G_0.txt, conţinând numele "selectorului".

Înregistrarea partidelor adnotate

Am avut grijă în scriptul Bash de mai sus, să ştergem liniile care conţin "IFlag" nu atât pentru că "nu mai avem nevoie" de ele, cum am zis mai sus; am arătat în [2] că dacă întâlneşte tagul "White" şi apoi tagul "WhiteIFlag", funcţia Annotate() din Crafty va suprascrie tagul (necesar) "White" (şi pe de altă parte, suportă numai tagurile PGN standard).

În [2] am constituit scriptul annotate.sh care, copiat în directorul Can/ şi lansat, apelează crafty pentru fiecare fişier PGN din acest director, înlocuindu-l cu fişierul ".can" în care Crafty a înscris partida respectivă împreună cu adnotările sale. Adnotarea durează: am prevăzut 10 secunde pentru analiza fiecărei mutări, încât pentru o partidă de 30 de mutări sunt necesare 5-6 minute (fiind 30 de mutări pentru alb şi 30 pentru negru) pentru a obţine fişierul ".can" corespunzător. Dar scriptul poate fi întrerupt, iar la relansarea ulterioară va continua cu partidele rămase neadnotate.

Folosind periodic annotate.sh, vom obţine la un moment dat un număr mulţumitor de fişiere ".can"; următorul script Bash - împreună cu scriptul Python pe care îl invocă, transmiţându-i anumiţi parametri - simplifică faţă de [1], înregistrarea în baza de date a acestor partide adnotate:

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/bin/bash
<<HELP
    vb@vb:~/slightchess/Doc/SUPPLY$  ls
       Can/  supply_.sh  st_games.py   GA_08-03-2014.pgn supply.sh ... 
    vb@vb:~/slightchess/Doc/SUPPLY$  ./supply_.sh
HELP

cd Can  # Operates in Can/ subdirectory.
can_arr=(*.can)  # (G_1.pgn.can  G_2.pgn.can  ...)

for file in ${can_arr[@]}
do  # Remove unwanted lines from file.
    grep -Fv '""
"?"
"???
annotating
using
search
' $file > temp && mv temp $file
done

# Proceed ./st_games.py, with coach's name and number of games.
coach=`cat G_0.txt`
param=($coach ${#can_arr[@]})
cd ..  # Returns to SUPPLY/ directory.
./st_games.py ${param[@]}

Pentru a elimina din fişierele "*.can" toate tagurile nesemnificative (cu valori ca "?" sau "") şi cele trei comentarii adăugate de Crafty - în [1] invocam `ls -v G_*.can` pentru a obţine lista numelor acestor fişiere şi foloseam grep -Fvf patterns.txt $file > "$file.1", unde în prealabil "patterns.txt" conţine şabloanele de linii care trebuie "şterse" din fişier.

De data aceasta operăm direct: (*.can) din linia 68 construieşte un tablou având ca elemente numele fişierelor ".can" din directorul curent; şabloanele liniilor pe care vrem să le ştergem sunt indicate (fiecare pe câte o linie) în liniile 72..77 (ambalând apoi cu apostrof). Este drept că puteam înlocui liniile 72-78 cu o singură linie, folosind sed (şi scutind invocările mv):

sed --in-place '/(""\|"?"\|"???\|annotating\|using\|search\|)/d' $file

Probabil era ceva mai eficient astfel; am preferat grep, pentru claritate: şabloanele apar ca atare (fără "metacaractere") pe câte o linie proprie, fiind uşor de adăugat sau eliminat unul.

În linia 82 se obţine în variabila coach numele "selectorului" (vezi şi linia 54, din primul script Bash), constituind apoi tabloul param în care, al doilea element ${#can_arr[@]} este numărul de valori conţinute de tabloul can_arr. În final este lansat scriptul (executabil) st_games.py, transmiţându-i aceşti doi parametri (numele selectorului şi numărul fişierelor ".can" din directorul Can/).

#!/usr/bin/python
''' ~/slightchess/Doc/SUPPLY/st_games.py  Make it "executable" (chmod +x ).
    ./st_games.py  coach_name  number_of_can_files '''

import sys, codecs, re, MySQLdb as mdb

coach = re.escape(sys.argv[1])
can_files = ["Can/G_%s.pgn.can" %nr for nr in xrange(1, int(sys.argv[2]) + 1)] 

prog = re.compile(r"\[White \"%s\"\]" % coach)

def get_partner(pgn):
    if re.search(prog, pgn):
        return re.search("\[Black \"(.*?)\"", pgn).group(1)
    return re.search("\[White \"(.*?)\"", pgn).group(1)

conn = mdb.connect('localhost', 'vb', '123456', 'slightchess', 
                   charset='utf8', use_unicode=True);
cursor = conn.cursor()

cursor.execute('SELECT id FROM games_selector WHERE instant_username="%s"' 
                % coach)
selector_id = cursor.fetchone()[0]

for f_name in can_files:
    fn = codecs.open(f_name, 'r', 'utf-8')
    can = fn.read()
    fn.close()
    part = get_partner(can)
    cursor.execute('SELECT id FROM games_partner WHERE nume="%s"' % part)
    part_id = cursor.fetchone()[0]
    cursor.execute(
        'INSERT INTO games_game SET coach_id="%s", pgn="%s", partner_id="%s"' 
         %(selector_id, re.escape(can), part_id))
conn.commit()
cursor.close()
conn.close()

Ştiind numărul de fişiere ".can" (preluat prin sys.argv[2] din lista parametrilor de apel) şi mai ştiind şablonul numelor acestora G_%s.pgn.can (unde %s va fi înlocuit cu numărul de ordine al fişierului) - am putut constitui lista can_files, conţinând ca şiruri numele fişierelor respective. În [1], această listă era produsă inutil de complicat - importând os şi folosind metoda next(os.walk('Can')).

Epilog

Spre deosebire de [1], acum avem de tastat numai două comenzi: ./supply.sh nume_PGN_iniţial şi respectiv (după ce vom fi obţinut de la Crafty fişierele adnotate ale partidelor) ./supply_.sh - pentru a înregistra colecţia respectivă în baza de date.

vezi Cărţile mele (de programare)

docerpro | Prev | Next