Selles praktikumis vaatame, kuidas Pythoni vahenditega oma eestikeelsele tekstile süntaktilist analüüsi lisada saab, ning tutvume kahe omavahel üsna sarnase andmeformaadiga: XML ja HTML.
Süntaktilise analüüsi (sünonüümina kasutatakse sõna "parsima") ülesandeks on lause struktuuri ehk sõnadevaheliste seoste leidmine. Kui me teostame tekstil morfoloogilise analüüsi ja leiame sõnaliigid, siis saame üldjoontes teada, milliseid tegevusi selle lausega kirjeldatakse (tegusõnad), millistest olenditest/objektidest/nähtustest/jne seal räägitakse (nimisõnad), milliseid omadusi seal mainitakse (omadussõnad) jne. Sõnaliigid ei anna aga meile infot nendevahelistest seostest. Näiteks, kui teame, et lauses esinevad nimisõnad "ema" ja "isa" ning tegusõna "hirmutama", siis ei ole meil siiski teadmist selle kohta, kes keda ehmatab. Kui teame aga, et "ema" on lauses alus (subjekt), "isa" on sihitis (objekt) ning "hirmutama" on selle lause öeldis, siis on selge, et ema on hirmutajaks ja isa selleks, keda hirmutatakse. Seda teadmist saame esitada puu kujul:
Eesti keele süntaktiliseks märgendamiseks on kaks enamkasutatavat töövahendit statistiline MaltParser ja reeglipõhine kitsenduste grammatika formalismil baseeruv VISLCG3 analüsaator. Mõlemat on võimalik kasutada läbi EstNLTK (vt ka juhend) ning mõlema väljundit aitab vajadusel mõista see dokument.
Vaikimisi rakendatakse süntaktiliseks märgendamiseks MaltParserit, see käib meetodi tag_syntax() abil:
from estnltk import Text
text = Text('Ema hirmutas isa.')
text.tag_syntax()
{'conll_syntax': [{'end': 3, 'parser_out': [['@SUBJ', 1]], 'sent_id': 0, 'start': 0}, {'end': 12, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 4}, {'end': 16, 'parser_out': [['@OBJ', 1]], 'sent_id': 0, 'start': 13}, {'end': 17, 'parser_out': [['xxx', 2]], 'sent_id': 0, 'start': 16}], 'paragraphs': [{'end': 17, 'start': 0}], 'sentences': [{'end': 17, 'start': 0}], 'text': 'Ema hirmutas isa.', 'words': [{'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'ema', 'partofspeech': 'S', 'root': 'ema', 'root_tokens': ['ema']}], 'end': 3, 'start': 0, 'text': 'Ema'}, {'analysis': [{'clitic': '', 'ending': 's', 'form': 's', 'lemma': 'hirmutama', 'partofspeech': 'V', 'root': 'hirmuta', 'root_tokens': ['hirmuta']}], 'end': 12, 'start': 4, 'text': 'hirmutas'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'isa', 'partofspeech': 'S', 'root': 'isa', 'root_tokens': ['isa']}], 'end': 16, 'start': 13, 'text': 'isa'}, {'analysis': [{'clitic': '', 'ending': '', 'form': '', 'lemma': '.', 'partofspeech': 'Z', 'root': '.', 'root_tokens': ['.']}], 'end': 17, 'start': 16, 'text': '.'}]}
text['conll_syntax']
[{'end': 3, 'parser_out': [['@SUBJ', 1]], 'sent_id': 0, 'start': 0}, {'end': 12, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 4}, {'end': 16, 'parser_out': [['@OBJ', 1]], 'sent_id': 0, 'start': 13}, {'end': 17, 'parser_out': [['xxx', 2]], 'sent_id': 0, 'start': 16}]
Nagu näeme, lisatakse tekstile süntaktilise analüüsi käigus nii juba tuttav 'words' kiht koos morfoloogilise analüüsiga kui ka uus kiht nimega 'conll_syntax', kus paiknevad nii kaarte nimed ehk süntaksi märgendid (@SUBJ, @OBJ, ROOT - tähistab lause juurt ehk üldjuhul öeldist) kui ka sõltuvussuhet (sõna ülemust puus) tähistavad numbrid.
Vaatame huvi pärast ka pisut keerulisemat näidet:
text = Text('Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.')
text.tag_syntax()
{'conll_syntax': [{'end': 3, 'parser_out': [['@P>', 1]], 'sent_id': 0, 'start': 0}, {'end': 7, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 4}, {'end': 12, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 8}, {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13}, {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20}, {'end': 34, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 26}, {'end': 44, 'parser_out': [['@PRD', 5]], 'sent_id': 0, 'start': 35}, {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44}, {'end': 49, 'parser_out': [['@J', 12]], 'sent_id': 0, 'start': 46}, {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50}, {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60}, {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69}, {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72}, {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79}, {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85}], 'paragraphs': [{'end': 86, 'start': 0}], 'sentences': [{'end': 86, 'start': 0}], 'text': 'Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.', 'words': [{'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg g', 'lemma': 'pood', 'partofspeech': 'S', 'root': 'pood', 'root_tokens': ['pood']}], 'end': 3, 'start': 0, 'text': 'Poe'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': '', 'lemma': 'ees', 'partofspeech': 'K', 'root': 'ees', 'root_tokens': ['ees']}], 'end': 7, 'start': 4, 'text': 'ees'}, {'analysis': [{'clitic': '', 'ending': 't', 'form': 'sg p', 'lemma': 'õlu', 'partofspeech': 'S', 'root': 'õlu', 'root_tokens': ['õlu']}], 'end': 12, 'start': 8, 'text': 'õlut'}, {'analysis': [{'clitic': '', 'ending': 'nud', 'form': 'nud', 'lemma': 'jooma', 'partofspeech': 'V', 'root': 'joo', 'root_tokens': ['joo']}, {'clitic': '', 'ending': '0', 'form': '', 'lemma': 'joonud', 'partofspeech': 'A', 'root': 'joo=nud', 'root_tokens': ['joonud']}, {'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'joonud', 'partofspeech': 'A', 'root': 'joo=nud', 'root_tokens': ['joonud']}, {'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'joonud', 'partofspeech': 'A', 'root': 'joo=nud', 'root_tokens': ['joonud']}], 'end': 19, 'start': 13, 'text': 'joonud'}, {'analysis': [{'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'mees', 'partofspeech': 'S', 'root': 'mees', 'root_tokens': ['mees']}], 'end': 25, 'start': 20, 'text': 'mehed'}, {'analysis': [{'clitic': '', 'ending': 'sid', 'form': 'sid', 'lemma': 'häirima', 'partofspeech': 'V', 'root': 'häiri', 'root_tokens': ['häiri']}], 'end': 34, 'start': 26, 'text': 'häirisid'}, {'analysis': [{'clitic': '', 'ending': 'e', 'form': 'pl p', 'lemma': 'kohalik', 'partofspeech': 'A', 'root': 'kohalik', 'root_tokens': ['kohalik']}], 'end': 44, 'start': 35, 'text': 'kohalikke'}, {'analysis': [{'clitic': '', 'ending': '', 'form': '', 'lemma': ',', 'partofspeech': 'Z', 'root': ',', 'root_tokens': [',']}], 'end': 45, 'start': 44, 'text': ','}, {'analysis': [{'clitic': '', 'ending': '0', 'form': '', 'lemma': 'aga', 'partofspeech': 'J', 'root': 'aga', 'root_tokens': ['aga']}], 'end': 49, 'start': 46, 'text': 'aga'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'ükskõikne', 'partofspeech': 'A', 'root': 'üks_kõikne', 'root_tokens': ['üks', 'kõikne']}], 'end': 59, 'start': 50, 'text': 'ükskõikne'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'politsei', 'partofspeech': 'S', 'root': 'politsei', 'root_tokens': ['politsei']}], 'end': 68, 'start': 60, 'text': 'politsei'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': 'neg', 'lemma': 'ei', 'partofspeech': 'V', 'root': 'ei', 'root_tokens': ['ei']}], 'end': 71, 'start': 69, 'text': 'ei'}, {'analysis': [{'clitic': '', 'ending': 'nud', 'form': 'nud', 'lemma': 'tegema', 'partofspeech': 'V', 'root': 'tege', 'root_tokens': ['tege']}, {'clitic': '', 'ending': '0', 'form': '', 'lemma': 'teinud', 'partofspeech': 'A', 'root': 'tei=nud', 'root_tokens': ['teinud']}, {'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'teinud', 'partofspeech': 'A', 'root': 'tei=nud', 'root_tokens': ['teinud']}, {'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'teinud', 'partofspeech': 'A', 'root': 'tei=nud', 'root_tokens': ['teinud']}], 'end': 78, 'start': 72, 'text': 'teinud'}, {'analysis': [{'clitic': '', 'ending': 'dagi', 'form': 'sg p', 'lemma': 'miski', 'partofspeech': 'P', 'root': 'miski', 'root_tokens': ['miski']}], 'end': 85, 'start': 79, 'text': 'midagi'}, {'analysis': [{'clitic': '', 'ending': '', 'form': '', 'lemma': '.', 'partofspeech': 'Z', 'root': '.', 'root_tokens': ['.']}], 'end': 86, 'start': 85, 'text': '.'}]}
Et paremini aru saada, milline analüüs millise sõna juurde käib, võime väljastada koos süntaktilise analüüsi kihiga ka sõnad:
list(zip(text.word_texts, text['conll_syntax']))
[('Poe', {'end': 3, 'parser_out': [['@P>', 1]], 'sent_id': 0, 'start': 0}), ('ees', {'end': 7, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 4}), ('õlut', {'end': 12, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 8}), ('joonud', {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13}), ('mehed', {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20}), ('häirisid', {'end': 34, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 26}), ('kohalikke', {'end': 44, 'parser_out': [['@PRD', 5]], 'sent_id': 0, 'start': 35}), (',', {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44}), ('aga', {'end': 49, 'parser_out': [['@J', 12]], 'sent_id': 0, 'start': 46}), ('ükskõikne', {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50}), ('politsei', {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60}), ('ei', {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69}), ('teinud', {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72}), ('midagi', {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79}), ('.', {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85})]
VISLCG3 parseri kasutamiseks on vaja VISLCG3 esmalt installida oma arvutisse (vt juhendit siit). Selle kasutamiseks läbi EstNLTK tuleb luua VISLCG3 parseri isend, andes ette vislcg3 asukoha oma arvutis (kui lisate vislcg3 parseri kataloogi oma PATH süsteemimuutujasse, siis ei ole vaja asukohta parseri isendi loomisel ette anda).
from estnltk.syntax.parsers import VISLCG3Parser
# See käsk niisugusel kujul tõenäoliselt ei tööta - peate vislcg3 asukoha määrama vastavalt sellele,
# kuhu oma arvutis selle installisite
parser = VISLCG3Parser(vislcg_cmd='C:\\cg3\\bin\\vislcg3.exe')
# Kui VISLCG3 kataloog on PATHi lisatud, saab isendi tekitada teed ette andmata
parser = VISLCG3Parser()
VislCG3-ga saame samuti teksti märgendada meetodi tag_syntax() abil. Et meetod kasutaks MaltParseri asemel VISLCG3, tuleb see juba Text objekti loomise käigus ära määrata - parameetriks syntactic_parser tuleb ette anda oma VISLCG3 parser (mille tekitasime eelmisel real):
text = Text('Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.', syntactic_parser = parser)
text.tag_syntax()
{'paragraphs': [{'end': 86, 'start': 0}], 'sentences': [{'end': 86, 'start': 0}], 'text': 'Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.', 'vislcg3_syntax': [{'end': 3, 'parser_out': [['@P>', 1]], 'sent_id': 0, 'start': 0}, {'end': 7, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 4}, {'end': 12, 'parser_out': [['@OBJ', 3]], 'sent_id': 0, 'start': 8}, {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13}, {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20}, {'end': 34, 'parser_out': [['@FMV', -1]], 'sent_id': 0, 'start': 26}, {'end': 44, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 35}, {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44}, {'end': 49, 'parser_out': [['@J', 10]], 'sent_id': 0, 'start': 46}, {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50}, {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60}, {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69}, {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72}, {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79}, {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85}], 'words': [{'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg g', 'lemma': 'Pood', 'partofspeech': 'H', 'root': 'Pood', 'root_tokens': ['Pood']}, {'clitic': '', 'ending': '0', 'form': 'sg g', 'lemma': 'Poe', 'partofspeech': 'H', 'root': 'Poe', 'root_tokens': ['Poe']}, {'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'Poe', 'partofspeech': 'H', 'root': 'Poe', 'root_tokens': ['Poe']}, {'clitic': '', 'ending': '0', 'form': 'sg g', 'lemma': 'pood', 'partofspeech': 'S', 'root': 'pood', 'root_tokens': ['pood']}, {'clitic': '', 'ending': '0', 'form': 'o', 'lemma': 'pugema', 'partofspeech': 'V', 'root': 'puge', 'root_tokens': ['puge']}], 'end': 3, 'start': 0, 'text': 'Poe'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': '', 'lemma': 'ees', 'partofspeech': 'D', 'root': 'ees', 'root_tokens': ['ees']}, {'clitic': '', 'ending': '0', 'form': '', 'lemma': 'ees', 'partofspeech': 'K', 'root': 'ees', 'root_tokens': ['ees']}, {'clitic': '', 'ending': 's', 'form': 'sg in', 'lemma': 'esi', 'partofspeech': 'S', 'root': 'esi', 'root_tokens': ['esi']}], 'end': 7, 'start': 4, 'text': 'ees'}, {'analysis': [{'clitic': '', 'ending': 't', 'form': 'sg p', 'lemma': 'õlu', 'partofspeech': 'S', 'root': 'õlu', 'root_tokens': ['õlu']}], 'end': 12, 'start': 8, 'text': 'õlut'}, {'analysis': [{'clitic': '', 'ending': 'nud', 'form': 'nud', 'lemma': 'jooma', 'partofspeech': 'V', 'root': 'joo', 'root_tokens': ['joo']}, {'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'joonu', 'partofspeech': 'S', 'root': 'joo=nu', 'root_tokens': ['joonu']}, {'clitic': '', 'ending': '0', 'form': '', 'lemma': 'joonud', 'partofspeech': 'A', 'root': 'joo=nud', 'root_tokens': ['joonud']}, {'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'joonud', 'partofspeech': 'A', 'root': 'joo=nud', 'root_tokens': ['joonud']}, {'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'joonud', 'partofspeech': 'A', 'root': 'joo=nud', 'root_tokens': ['joonud']}], 'end': 19, 'start': 13, 'text': 'joonud'}, {'analysis': [{'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'mees', 'partofspeech': 'S', 'root': 'mees', 'root_tokens': ['mees']}], 'end': 25, 'start': 20, 'text': 'mehed'}, {'analysis': [{'clitic': '', 'ending': 'sid', 'form': 'sid', 'lemma': 'häirima', 'partofspeech': 'V', 'root': 'häiri', 'root_tokens': ['häiri']}], 'end': 34, 'start': 26, 'text': 'häirisid'}, {'analysis': [{'clitic': '', 'ending': 'e', 'form': 'pl p', 'lemma': 'kohalik', 'partofspeech': 'A', 'root': 'kohalik', 'root_tokens': ['kohalik']}], 'end': 44, 'start': 35, 'text': 'kohalikke'}, {'analysis': [{'clitic': '', 'ending': '', 'form': '', 'lemma': ',', 'partofspeech': 'Z', 'root': ',', 'root_tokens': [',']}], 'end': 45, 'start': 44, 'text': ','}, {'analysis': [{'clitic': '', 'ending': '0', 'form': '', 'lemma': 'aga', 'partofspeech': 'J', 'root': 'aga', 'root_tokens': ['aga']}], 'end': 49, 'start': 46, 'text': 'aga'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'ükskõikne', 'partofspeech': 'A', 'root': 'üks_kõikne', 'root_tokens': ['üks', 'kõikne']}], 'end': 59, 'start': 50, 'text': 'ükskõikne'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': 'sg g', 'lemma': 'politsei', 'partofspeech': 'S', 'root': 'politsei', 'root_tokens': ['politsei']}, {'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'politsei', 'partofspeech': 'S', 'root': 'politsei', 'root_tokens': ['politsei']}], 'end': 68, 'start': 60, 'text': 'politsei'}, {'analysis': [{'clitic': '', 'ending': '0', 'form': '', 'lemma': 'ei', 'partofspeech': 'D', 'root': 'ei', 'root_tokens': ['ei']}, {'clitic': '', 'ending': '0', 'form': 'neg', 'lemma': 'ei', 'partofspeech': 'V', 'root': 'ei', 'root_tokens': ['ei']}], 'end': 71, 'start': 69, 'text': 'ei'}, {'analysis': [{'clitic': '', 'ending': 'nud', 'form': 'nud', 'lemma': 'tegema', 'partofspeech': 'V', 'root': 'tege', 'root_tokens': ['tege']}, {'clitic': '', 'ending': '0', 'form': '', 'lemma': 'teinud', 'partofspeech': 'A', 'root': 'tei=nud', 'root_tokens': ['teinud']}, {'clitic': '', 'ending': '0', 'form': 'sg n', 'lemma': 'teinud', 'partofspeech': 'A', 'root': 'tei=nud', 'root_tokens': ['teinud']}, {'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'teinud', 'partofspeech': 'A', 'root': 'tei=nud', 'root_tokens': ['teinud']}, {'clitic': '', 'ending': 'd', 'form': 'pl n', 'lemma': 'teinu', 'partofspeech': 'S', 'root': 'teinu', 'root_tokens': ['teinu']}], 'end': 78, 'start': 72, 'text': 'teinud'}, {'analysis': [{'clitic': '', 'ending': 'dagi', 'form': 'sg p', 'lemma': 'miski', 'partofspeech': 'P', 'root': 'miski', 'root_tokens': ['miski']}], 'end': 85, 'start': 79, 'text': 'midagi'}, {'analysis': [{'clitic': '', 'ending': '', 'form': '', 'lemma': '.', 'partofspeech': 'Z', 'root': '.', 'root_tokens': ['.']}], 'end': 86, 'start': 85, 'text': '.'}]}
Nagu näeme, siis kui tekst on analüüsitud VISLCG3 parseriga, lisatakse süntaktiline analüüs kihti 'vislcg3_syntax':
text['vislcg3_syntax']
[{'end': 3, 'parser_out': [['@P>', 1]], 'sent_id': 0, 'start': 0}, {'end': 7, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 4}, {'end': 12, 'parser_out': [['@OBJ', 3]], 'sent_id': 0, 'start': 8}, {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13}, {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20}, {'end': 34, 'parser_out': [['@FMV', -1]], 'sent_id': 0, 'start': 26}, {'end': 44, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 35}, {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44}, {'end': 49, 'parser_out': [['@J', 10]], 'sent_id': 0, 'start': 46}, {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50}, {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60}, {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69}, {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72}, {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79}, {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85}]
Süntaksis ei huvitu me enamasti vaid ühest sõnast korraga, vaid ka sõnadevahelistest seostest. Seetõttu ei piisa enam võimalusest sõna kaupa üle teksti itereerida, vaid soovime teha päringuid vastavalt leitud süntaksipuu struktuurile. Siinkohal tulebki appi meetod syntax_trees(), mis loob tekstist puuobjektid, millele omakorda on võimalik vastavaid päringuid teha. Vaatame näidet kahelauselise teksti puhul:
text = Text("Pisike poiss kimbutas kirpe. Need kirbud ei julgenud kiusata karusid.")
# Loome tekstist puuobjektid ehk leiame kõik tekstis esinevad juurtipud
# Kui me parserit ei täpsusta, tehakse analüüs MaltParseriga
trees = text.syntax_trees()
len(trees)
2
trees
[<estnltk.syntax.utils.Tree at 0x23a849fe198>, <estnltk.syntax.utils.Tree at 0x23a849fedd8>]
trees[0].text
'kimbutas'
Kui puud on loodud, võime nende peal teha erinevaid päringuid. Abiks on kindlasti meetodid get_children() tipu (sõna) alluvate leidmiseks ning get_parent() ülema leidmiseks:
# Leiame subjektid ja nendele alluvad sõnad
for tree in trees:
subject_nodes = tree.get_children( label="@SUBJ" )
for subj_node in subject_nodes:
subject_and_children = subj_node.get_children( include_self=True, sorted=True )
# Prindime nii sõna kui funktsioonimärgendi
print( [(node.text, node.labels) for node in subject_and_children] )
[('Pisike', ['@AN>']), ('poiss', ['@SUBJ'])] [('Need', ['@NN>']), ('kirbud', ['@SUBJ'])]
Lisaks eelmises näites kasutatud funktsioonimärgendile saame seada päringule ka muid kitsendusi, selleks vajame WordTemplate moodulit:
from estnltk.mw_verbs.utils import WordTemplate
from estnltk.names import POSTAG, FORM, LEMMA
Loome sõnamalli, mis otsiks nimetavas käändes nimisõnu - nii ainsuses kui mitmuses. Sõnamallides saab kasutada regulaaravaldisi:
noun_in_nominative = WordTemplate({POSTAG: 'S', FORM: '^(sg n|pl n)$'})
Leiame esimesest puust mallile vastavad sõnad:
nominative_subj = trees[0].get_children(word_template=noun_in_nominative, label = '@SUBJ' )
[node.text for node in nominative_subj]
['poiss']
Loome sõnamalli, mis otsib lemmat "kirp":
kirp = WordTemplate({LEMMA: 'kirp'})
Leiame sõnad, mis on sõna "kirp" ülemusteks (parent):
for tree in trees:
kirbud = tree.get_children(word_template = kirp)
for element in kirbud:
print(element.parent.text)
kimbutas julgenud
Kui tahaksime aga kätte saada sõnavormide asemel lemmasid, peaksime süvenema token klassimuutujasse:
for tree in trees:
kirbud = tree.get_children(word_template = kirp)
for element in kirbud:
print(element.parent.token)
for i in element.parent.token['analysis']:
print('LEMMA: ' + i['lemma'])
{'analysis': [{'clitic': '', 'lemma': 'kimbutama', 'root': 'kimbuta', 'form': 's', 'root_tokens': ['kimbuta'], 'partofspeech': 'V', 'ending': 's'}], 'text': 'kimbutas', 'end': 21, 'start': 13} LEMMA: kimbutama {'analysis': [{'clitic': '', 'lemma': 'julgema', 'root': 'julge', 'form': 'nud', 'root_tokens': ['julge'], 'partofspeech': 'V', 'ending': 'nud'}, {'clitic': '', 'lemma': 'julgenud', 'root': 'julge=nud', 'form': '', 'root_tokens': ['julgenud'], 'partofspeech': 'A', 'ending': '0'}, {'clitic': '', 'lemma': 'julgenud', 'root': 'julge=nud', 'form': 'sg n', 'root_tokens': ['julgenud'], 'partofspeech': 'A', 'ending': '0'}, {'clitic': '', 'lemma': 'julgenud', 'root': 'julge=nud', 'form': 'pl n', 'root_tokens': ['julgenud'], 'partofspeech': 'A', 'ending': 'd'}], 'text': 'julgenud', 'end': 52, 'start': 44} LEMMA: julgema LEMMA: julgenud LEMMA: julgenud LEMMA: julgenud
NB! Eelnevates näites rakendati analüsaatoreid üksikutel lausetel. Kuigi see praktikas töötab, siis kiiruse poolest on oluline vahe, kas analüüsida tekste ühe lause kaupa või lasta kogu tekst korraga läbi süntaksianalüsaatori -- viimane variant peaks enamikul juhtudest olema oluliselt kiirem. Selle põhjus on tehniline: süntaksianalüsaator salvestab analüüsitava teksti faili, rakendab sellel Pythoni-välist programmi ning loeb uued analüüsi tulemused failist. Selline failidega opereerimine -- failide avamine/lugemine/sulgemine -- on aga üksjagu aeganõudev protsess. Seega on parema kiiruse huvides soovitav "failidega majandamist" minimeerida ehk siis lasta suur tekst läbi korraga, mitte lausete kaupa.
Parsige lause "Sir Kenneth Robinson on rahvusvaheline nõustaja loovushariduse teemal valitsustele, MTÜ-dele ja kunstikoolidele." nii MaltParseri kui VislCG3 parseriga (selleks installige esmalt vislcg3 - juhend ülalpool). Võrrelge väljundeid.
*Paberi ja pliiatsi kasutamisel palun esitage puu järgmises praktikumis või pildistatuna/skannituna.
**Puu esitamine Latexis tehtuna annab 0,5 boonuspunkti.
Teostage süntaktiline analüüs Indrek Hargla lühijuttudel, mille leiate kataloogist hargla. Kas kasutada MaltParserit või VislCG3, võite ise otsustada.
A. Leidke, kummale jääb juttudes sagedamini subjekti, kummale objekti roll - kas mehele või naisele? Milliseid tegevusi tegemas kujutatakse meest, milliseid naist? Selleks:
B. Kuidas aga jagunevad tegevused elusate ja elutute subjektide vahel? Kasutage elususe-elutuse määramiseks:
Ehk:
Millised on sagedasemad elus ja millised elutute subjektide tegevused? Kas leidub tegevusi, mida teevad ainult elus või ainult elutud subjektid?
XML andmeformaadi eesmärgiks pakkuda andmete salvestamise ja levitamise viisi, mis oleks ühtviisi loetav nii inimesele kui ka arvutile. Ajalooliselt on XML olnud standardina juba kaua ning seetõttu on salvestatud paljud varasemad tekstikogud ja korpused sellesse formaati. XML-i "sugulaskeel" on HTML (mõlemad pärinevad ühisest esivanemast: SGML standardist), mis on sisuliselt veebilehekülgede "lähtekoodi" keel -- seega enamik veebis olevast (pool)tekstilisest materjalist on salvestatud just selles formaadis.
Alustuseks vaatame viise, kuidas XML-i parsida* ehk automaatselt analüüsida XML struktuuri ja otsida sealt mingeid alamosi. Pythonis on üheks väga heaks ja sageli kasutatavaks lahenduseks teek BeautifulSoup.
*Parsimise all mõistetakse süntaktilise analüüsi teostamist nii loomuliku keele kui ka programmeerimis- või märgenduskeele puhul. MaltParser ja VISLCG tegelevad arusaadavalt loomuliku keele parsimisega, beautifulsoup aga märgenduskeeltega. Seega, XML-ist ja HTML-ist rääkides mõistame parsimise all seda, kuidas märgendite vahelt oma tekst välja korjata.
Kuna BeautifulSoup (moodul bs4
) on üheks estnltk
sõltuvuseks, peaks see teil conda
keskkonda olema juba varasemalt installitud. Kui moodul bs4
on mingil veidral põhjusel siiski puudu, tuleks see käsurealt installida:
conda install bs4
Et praktikumi näited ilusti töötaksid, tuleb lisaks installida ka teek lxml
. Seda saab teha käsuga:
conda install lxml
Järgnevas näites on XML kujul lõik ühest märgendatud ilukirjandustekstist, BeautifulSoup abil parsime selle struktuuri ja kuvame "ilustatud" kujul, nii et taanded toovad XML-i struktuuri esile:
from bs4 import BeautifulSoup
# Näite-XML ( lõigud pärinevad Tasakaalus korpuse ilukirjanduse failist 'tanav_jutt4.tei' )
xml_content = '''
<?xml version="1.0"?>
<text> <body> <div0 type='tervikteos'> <head> Taadeldus </head>
<p> <bibl> <author> <s> Maniakkide Tänav </s> </author> </bibl> </p> <p> /---/ </p>
<p> <s> " Lähme Widgeti juurde , " ütlesin . </s> <s> Widget oli kursaõde , kes elas siinsamas Annelinnas . </s> <s> Tee peal üritas Ults mulle asja selgitada . </s> </p> <p> /---/ </p>
</div0> </body> </text>
'''
# Parsime XML-sisu lxml parseri abil
soup = BeautifulSoup(xml_content,'lxml')
# Kuvame XML elementide sisud ilusti joondatult:
print(soup.prettify())
<?xml version="1.0"?> <html> <body> <text> <div0 type="tervikteos"> Taadeldus <p> <bibl> <author> <s> Maniakkide Tänav </s> </author> </bibl> </p> <p> /---/ </p> <p> <s> " Lähme Widgeti juurde , " ütlesin . </s> <s> Widget oli kursaõde , kes elas siinsamas Annelinnas . </s> <s> Tee peal üritas Ults mulle asja selgitada . </s> </p> <p> /---/ </p> </div0> </text> </body> </html>
ü
(tähistab tähte 'ü') ja õ
(tähistab tähte 'õ'). Kui kasutada BeautifulSoup'i XML (või HTML) failide parsimiseks, ei pea olemite konverteerimise pärast muretsema: konverteerimine viiakse teegi poolt läbi automaatselt, nagu on näha ka "ilustatud" väljundis;BeautfifulSoup lubab otsida XML-ist, näiteks saab leida kõikide "lauseliste"-elementide sisud:
soup.find_all('s')
[<s> Maniakkide Tänav </s>, <s> " Lähme Widgeti juurde , " ütlesin . </s>, <s> Widget oli kursaõde , kes elas siinsamas Annelinnas . </s>, <s> Tee peal üritas Ults mulle asja selgitada . </s>]
Lisaks on võimalik otsida kindlate atribuutide väärtuste järgi.
# Leiame elemendi, mille type='tervikteos'
soup.find(type='tervikteos')
<div0 type="tervikteos"> Taadeldus <p> <bibl> <author> <s> Maniakkide Tänav </s> </author> </bibl> </p> <p> /---/ </p> <p> <s> " Lähme Widgeti juurde , " ütlesin . </s> <s> Widget oli kursaõde , kes elas siinsamas Annelinnas . </s> <s> Tee peal üritas Ults mulle asja selgitada . </s> </p> <p> /---/ </p> </div0>
# Leiame vastava elemendi tekstilise sisu (ilma XML elementideta)
soup.find(type='tervikteos').text
' Taadeldus \n Maniakkide Tänav /---/ \n " Lähme Widgeti juurde , " ütlesin . Widget oli kursaõde , kes elas siinsamas Annelinnas . Tee peal üritas Ults mulle asja selgitada . /---/ \n'
Positiivsetel otsingutulemustel saab omakorda rakendada alamotsinguid.
Näide: kitsendame otsingut kuni saame kätte autori nime sõne kujul:
soup.find(type='tervikteos').author
<author> <s> Maniakkide Tänav </s> </author>
soup.find(type='tervikteos').author.s.text
' Maniakkide Tänav '
Näide: kitsendame, kuni saame kätte lauseliste elementide tekstilise sisu. Kõigepealt esimese "lause" korral:
soup.find_all('s')[0].text
' Maniakkide Tänav '
Seejärel kõigil lausetel:
[ s.text for s in soup.find_all('s') ]
[' Maniakkide Tänav ', ' " Lähme Widgeti juurde , " ütlesin . ', ' Widget oli kursaõde , kes elas siinsamas Annelinnas . ', ' Tee peal üritas Ults mulle asja selgitada . ']
Tartu Ülikooli koondkorpuse XML formaadis tekstide sisselugemiseks on EstNLTK-s olemas ka eraldiseisvad funktsioonid. Need leiab moodulist estnltk.teicorpus
:
parse_tei_corpus(path, target=['artikkel'], encoding=None)
-- loeb ja parsib ühe XML faili (antud kataloogiteega path
) sisu ning tagastab faili põhjal loodud Text
objektide järjendi;
parse_tei_corpora(root, prefix='', suffix='.xml', target=['artikkel'], encoding=None)
-- loeb ja parsib kataloogist root
faile, mille prefiks on prefix
ja sufiks suffix
ning tagastab failide põhjal loodud Text
objektide järjendi.
Mõlema funktsiooni puhul: tekstilist sisu otsitakse div
elementidest, mille type
väärtused on kirjeldatud järjendis target
. Vaikeväärtus target=['artikkel']
sobib enamiku ajakirjandustekstide parsimiseks; ilukirjanduse puhul peaks sobima väärtus target=['tervikteos']
;
Kuigi funktsioonid lubavad vaikimisi kodeeringu täpsustamata jätta (encoding=None
), tuleks koondkorpuse XML failide lugemisel siiski kodeering alati määrata ("utf-8").
Kogu koondkorpuse XML failide automaatset konverteerimist kirjeldab juhend ning koondkorpus ise on saadaval siin veebilehel (linke järgides jõuab sealt koondkorpuse kokkupakitud XML failideni).
Mooduli estnltk.teicorpus
funktsioonide abil tekste sisse lugedes ja lausestades võite avastada, et tekstide lausestus ei tule päris selline, nagu see oli algses XML failis <s>
-märgendite abil esile toodud.
Kuna koondkorpuse XML failis puudub päris algne (ilma märgendusteta) tekst, peavad meetodid parse_tei_corpora()
ja parse_tei_corpus()
teksti rekonstrueerima.
Rekonstrueerimise käigus paigutatakse iga lause (tekstisisu <s>
-märgendite vahel) eraldi reale (ehk siis: laused on eraldatud reavahetustega).
EstNLTK tavapärane lausestaja aga sellist lausestust ei tunne, vaid otsib lausepiire ikka punktuatsioonisümbolite järgi.
Mõnikord EstNLTK tavalausestaja eksib ning arvab, et XML failis lause keskel olev punktuatsioon on lausepiiriks, ning seetõttu tekivadki sisseloetud tekste lausestades teistsugused lausepiirid, kui olid algses XML failis.
Selleks, et saada täpselt samasugune lausestus nagu oli algses XML failis, tuleb sisseloetud Text
objektid uuesti luua, kasutades tavalise lausestaja asemel lausestajat, mis märgibki lausepiirid reavahetuste järele.
Kuidas seda teha, vaatame järgmise näite varal detailsemalt.
from estnltk.teicorpus import parse_tei_corpus
# Loeme sisse ilukirjandustekstid XML-failist
# (fail pärineb TÜ koondkorpuse ilukirjanduse osast)
texts = parse_tei_corpus('ilu_habicht_wcnt.tasak.xml', target=['tervikteos'], encoding='utf-8')
# Uurime esimese teksti esimest 700 sümbolit: paistab, et laused on eraldatud reavahetustega
texts[0]['text'][:700]
'Tegelikult oli see igati loogiline , et survaivalid virtuaalsusse kolisid .\nLinnaäärne võserik jäi kanofiilidele , kes nagu nende lemmikudki ei pidanud enam juhuslike laserikiirte või värvilirakate pärast muretsema .\nMitte et see mulle kuidagi korda oleks läinud .\nVõserik tuli mulle ainult seepärast meelde , et ma siin ise ühe säärasega pean tegelema .\nKirves käes .\nSiin , muideks , on kusagil Dalaranis , peoonile ei hakka keegi üksikasju täpsustama .\nKakskümmend korda kirvega vastu puud .\nSiis kukub puu maha .\nMa võtan selle otsapidi õlale ja punun saeveskisse .\nVõi longin .\nKui grunt ei näe .\nKõik see on täiesti illusoorne , kuid väga väsitav .\nJa vasak õlg valutab .\nÜsna realistlikult .\nK'
Text
objekti luues on võimalik argumentide abil määrata, milliseid tekstitöötlusvahendeid analüüsimisel kasutatakse.
Seega võib vaikimisi kasutatava lausestaja (argument "sentence_tokenizer"
) asendada NLTK LineTokenizer
'iga, mis paneb lausepiirid ainult reavahetuste kohale:
# Kirjutame tekstid ümber nii, et tavalausestaja asemel kasutatakse LineTokenizer'it
from estnltk import Text
from nltk.tokenize import LineTokenizer
# Sisendargumentide sõnastik, kus on määratud uus lausestaja
kwargs = {
"sentence_tokenizer": LineTokenizer()
}
sentence_tokenized_texts = []
for text in texts:
# Teeme vana teksti põhjal uue teksti
text = Text(text['text'], **kwargs)
# Lausestame teksti
text = text.tokenize_sentences()
sentence_tokenized_texts.append(text)
# Uurime lausestamise tulemusi (esimesed 15 lauset)
sentence_tokenized_texts[0].sentence_texts[:15]
['Tegelikult oli see igati loogiline , et survaivalid virtuaalsusse kolisid .', 'Linnaäärne võserik jäi kanofiilidele , kes nagu nende lemmikudki ei pidanud enam juhuslike laserikiirte või värvilirakate pärast muretsema .', 'Mitte et see mulle kuidagi korda oleks läinud .', 'Võserik tuli mulle ainult seepärast meelde , et ma siin ise ühe säärasega pean tegelema .', 'Kirves käes .', 'Siin , muideks , on kusagil Dalaranis , peoonile ei hakka keegi üksikasju täpsustama .', 'Kakskümmend korda kirvega vastu puud .', 'Siis kukub puu maha .', 'Ma võtan selle otsapidi õlale ja punun saeveskisse .', 'Või longin .', 'Kui grunt ei näe .', 'Kõik see on täiesti illusoorne , kuid väga väsitav .', 'Ja vasak õlg valutab .', 'Üsna realistlikult .', 'Kui me siia saarele jõudsime , valvas seda metsatukka griffin .']
Text
objekti sisendargumentide abil on võimalik ka sõnestaja välja vahetada (selle kohta kirjutatakse täpsemalt siin). Mõningatel juhtudel leiab sobiva uue sõnestaja NLTK sõnestusmoodulist, aga kui on tarvis teha vähegi keerukamaid sõnestuse parandusi, on soovitatav hoopis ise uus sõnestaja klass kirjutada. Sama soovitus kehtib ka lausestaja kohta.Sisuliselt peab endakirjutatud klass järgima NLTK StringTokenizer
'i liidest ehk siis: olema StringTokenizer
'i alamklass ning pakkuma välja meetodid span_tokenize
ja tokenize
. Endakirjutatud sõnestamise loogika lähebki siis nendesse meetoditesse.
Ühe koodinäite, kuidas teha järelparandustega lausestaja, leiab siit. Tegemist on lausestajaga, mis teostab kõigepealt tavapärase EstNLTK lausestuse, ning rakendab seejärel regulaaravaldisi, et tuvastada ja parandada kõige sagedasemaid lausestusvigu.
Text
on loetud sisse estnltk.teicorpus
funktsioonide abil, siis sageli (olenevalt alamkorpusest) on seal ka meta-andmed teksti kohta. Kui on oluline säilitada nii lausestus kui ka meta-andmed, siis tuleks eelmises näites luua uus tekst teistsuguse nimega ning kanda sinna meta-andmed vanast tekstist üle.Tõenäoliselt oleks huvitav näiteks eelnenud ülesannetega sarnaseid analüüse teha ka muudel tekstidel. Kust aga tekste saada, kui soovime vaadata kaugemale juba olemasolevatest standardsesse formaati viidud korpustest? Kõige lihtsam on neid hankida muidugi veebist. Kuidas seda teha, käsitletakse mõni praktikum hiljem, praegu aga teeme väikse harjutuse selle kohta, kuidas juba omale salvestatud HTML-failist meid huvitav info kätte saada.
Kõige lihtsam on selleks kasutada vahendit beautifulsoup
, mida tutvustati eelnevalt juba XML formaadi juures. Sarnaselt XML-ile võimaldab beautifulsoup
parsida ka HTML-i, aga lxml-i asemel aitab meid html.parser.
from bs4 import BeautifulSoup
html = '''<!DOCTYPE html>
<html>
<body>
<h1>Pole probleemi</h1>
<p>Oli õhtune tipptund, tuli sõita läbi linna.</p>
<p>Tuled fooris vahetusid, kuid üle veel ei saanud.</p>
</body>
</html>'''
soup = BeautifulSoup(html, 'html.parser')
for i in soup.find_all('p'):
print(i.get_text())
Oli õhtune tipptund, tuli sõita läbi linna. Tuled fooris vahetusid, kuid üle veel ei saanud.
Lugege ja parsige (kahes tähenduses ja kolme parseriga) uudis failist kenneth_robinson.html. Leidke, kui palju erinevad omavahel MaltParseri ja VislCG3 väljund. Seejuures ärge lugege erinevuste hulka juhte, kus MaltParser on väljastanud pindsüntaktiliseks funktsiooniks ROOT. Vastake küsimustele: