{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "<h1 style=\"color:blue\">Praktikum 7. </h1>\n", "<h3 style=\"color:blue\">Süntaktiline analüüs. XML ja HTML sisendi lugemine</h3>" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Süntaktiline analüüs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<img src=\"puu1.png\">" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Eesti keele süntaktiliseks märgendamiseks on kaks enamkasutatavat töövahendit statistiline [MaltParser](http://www.maltparser.org/) ja reeglipõhine kitsenduste grammatika formalismil baseeruv [VISLCG3](https://github.com/EstSyntax/EstCG) analüsaator. Mõlemat on võimalik kasutada läbi EstNLTK (vt ka [juhend](https://estnltk.github.io/estnltk/1.4.1/tutorials/dependency_syntax.html)) ning mõlema väljundit aitab vajadusel mõista [see dokument](https://korpused.keeleressursid.ee/syntaks/dokumendid/syntaksiliides_ee.pdf)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### MaltParser" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vaikimisi rakendatakse süntaktiliseks märgendamiseks MaltParserit, see käib meetodi tag_syntax() abil:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'conll_syntax': [{'end': 3,\n", " 'parser_out': [['@SUBJ', 1]],\n", " 'sent_id': 0,\n", " 'start': 0},\n", " {'end': 12, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 4},\n", " {'end': 16, 'parser_out': [['@OBJ', 1]], 'sent_id': 0, 'start': 13},\n", " {'end': 17, 'parser_out': [['xxx', 2]], 'sent_id': 0, 'start': 16}],\n", " 'paragraphs': [{'end': 17, 'start': 0}],\n", " 'sentences': [{'end': 17, 'start': 0}],\n", " 'text': 'Ema hirmutas isa.',\n", " 'words': [{'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'ema',\n", " 'partofspeech': 'S',\n", " 'root': 'ema',\n", " 'root_tokens': ['ema']}],\n", " 'end': 3,\n", " 'start': 0,\n", " 'text': 'Ema'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 's',\n", " 'form': 's',\n", " 'lemma': 'hirmutama',\n", " 'partofspeech': 'V',\n", " 'root': 'hirmuta',\n", " 'root_tokens': ['hirmuta']}],\n", " 'end': 12,\n", " 'start': 4,\n", " 'text': 'hirmutas'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'isa',\n", " 'partofspeech': 'S',\n", " 'root': 'isa',\n", " 'root_tokens': ['isa']}],\n", " 'end': 16,\n", " 'start': 13,\n", " 'text': 'isa'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '',\n", " 'form': '',\n", " 'lemma': '.',\n", " 'partofspeech': 'Z',\n", " 'root': '.',\n", " 'root_tokens': ['.']}],\n", " 'end': 17,\n", " 'start': 16,\n", " 'text': '.'}]}" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from estnltk import Text\n", "text = Text('Ema hirmutas isa.')\n", "text.tag_syntax()" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/plain": [ "[{'end': 3, 'parser_out': [['@SUBJ', 1]], 'sent_id': 0, 'start': 0},\n", " {'end': 12, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 4},\n", " {'end': 16, 'parser_out': [['@OBJ', 1]], 'sent_id': 0, 'start': 13},\n", " {'end': 17, 'parser_out': [['xxx', 2]], 'sent_id': 0, 'start': 16}]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "text['conll_syntax']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vaatame huvi pärast ka pisut keerulisemat näidet:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'conll_syntax': [{'end': 3,\n", " 'parser_out': [['@P>', 1]],\n", " 'sent_id': 0,\n", " 'start': 0},\n", " {'end': 7, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 4},\n", " {'end': 12, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 8},\n", " {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13},\n", " {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20},\n", " {'end': 34, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 26},\n", " {'end': 44, 'parser_out': [['@PRD', 5]], 'sent_id': 0, 'start': 35},\n", " {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44},\n", " {'end': 49, 'parser_out': [['@J', 12]], 'sent_id': 0, 'start': 46},\n", " {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50},\n", " {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60},\n", " {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69},\n", " {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72},\n", " {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79},\n", " {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85}],\n", " 'paragraphs': [{'end': 86, 'start': 0}],\n", " 'sentences': [{'end': 86, 'start': 0}],\n", " 'text': 'Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.',\n", " 'words': [{'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg g',\n", " 'lemma': 'pood',\n", " 'partofspeech': 'S',\n", " 'root': 'pood',\n", " 'root_tokens': ['pood']}],\n", " 'end': 3,\n", " 'start': 0,\n", " 'text': 'Poe'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'ees',\n", " 'partofspeech': 'K',\n", " 'root': 'ees',\n", " 'root_tokens': ['ees']}],\n", " 'end': 7,\n", " 'start': 4,\n", " 'text': 'ees'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 't',\n", " 'form': 'sg p',\n", " 'lemma': 'õlu',\n", " 'partofspeech': 'S',\n", " 'root': 'õlu',\n", " 'root_tokens': ['õlu']}],\n", " 'end': 12,\n", " 'start': 8,\n", " 'text': 'õlut'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'nud',\n", " 'form': 'nud',\n", " 'lemma': 'jooma',\n", " 'partofspeech': 'V',\n", " 'root': 'joo',\n", " 'root_tokens': ['joo']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'joonud',\n", " 'partofspeech': 'A',\n", " 'root': 'joo=nud',\n", " 'root_tokens': ['joonud']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'joonud',\n", " 'partofspeech': 'A',\n", " 'root': 'joo=nud',\n", " 'root_tokens': ['joonud']},\n", " {'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'joonud',\n", " 'partofspeech': 'A',\n", " 'root': 'joo=nud',\n", " 'root_tokens': ['joonud']}],\n", " 'end': 19,\n", " 'start': 13,\n", " 'text': 'joonud'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'mees',\n", " 'partofspeech': 'S',\n", " 'root': 'mees',\n", " 'root_tokens': ['mees']}],\n", " 'end': 25,\n", " 'start': 20,\n", " 'text': 'mehed'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'sid',\n", " 'form': 'sid',\n", " 'lemma': 'häirima',\n", " 'partofspeech': 'V',\n", " 'root': 'häiri',\n", " 'root_tokens': ['häiri']}],\n", " 'end': 34,\n", " 'start': 26,\n", " 'text': 'häirisid'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'e',\n", " 'form': 'pl p',\n", " 'lemma': 'kohalik',\n", " 'partofspeech': 'A',\n", " 'root': 'kohalik',\n", " 'root_tokens': ['kohalik']}],\n", " 'end': 44,\n", " 'start': 35,\n", " 'text': 'kohalikke'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '',\n", " 'form': '',\n", " 'lemma': ',',\n", " 'partofspeech': 'Z',\n", " 'root': ',',\n", " 'root_tokens': [',']}],\n", " 'end': 45,\n", " 'start': 44,\n", " 'text': ','},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'aga',\n", " 'partofspeech': 'J',\n", " 'root': 'aga',\n", " 'root_tokens': ['aga']}],\n", " 'end': 49,\n", " 'start': 46,\n", " 'text': 'aga'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'ükskõikne',\n", " 'partofspeech': 'A',\n", " 'root': 'üks_kõikne',\n", " 'root_tokens': ['üks', 'kõikne']}],\n", " 'end': 59,\n", " 'start': 50,\n", " 'text': 'ükskõikne'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'politsei',\n", " 'partofspeech': 'S',\n", " 'root': 'politsei',\n", " 'root_tokens': ['politsei']}],\n", " 'end': 68,\n", " 'start': 60,\n", " 'text': 'politsei'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'neg',\n", " 'lemma': 'ei',\n", " 'partofspeech': 'V',\n", " 'root': 'ei',\n", " 'root_tokens': ['ei']}],\n", " 'end': 71,\n", " 'start': 69,\n", " 'text': 'ei'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'nud',\n", " 'form': 'nud',\n", " 'lemma': 'tegema',\n", " 'partofspeech': 'V',\n", " 'root': 'tege',\n", " 'root_tokens': ['tege']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'teinud',\n", " 'partofspeech': 'A',\n", " 'root': 'tei=nud',\n", " 'root_tokens': ['teinud']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'teinud',\n", " 'partofspeech': 'A',\n", " 'root': 'tei=nud',\n", " 'root_tokens': ['teinud']},\n", " {'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'teinud',\n", " 'partofspeech': 'A',\n", " 'root': 'tei=nud',\n", " 'root_tokens': ['teinud']}],\n", " 'end': 78,\n", " 'start': 72,\n", " 'text': 'teinud'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'dagi',\n", " 'form': 'sg p',\n", " 'lemma': 'miski',\n", " 'partofspeech': 'P',\n", " 'root': 'miski',\n", " 'root_tokens': ['miski']}],\n", " 'end': 85,\n", " 'start': 79,\n", " 'text': 'midagi'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '',\n", " 'form': '',\n", " 'lemma': '.',\n", " 'partofspeech': 'Z',\n", " 'root': '.',\n", " 'root_tokens': ['.']}],\n", " 'end': 86,\n", " 'start': 85,\n", " 'text': '.'}]}" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "text = Text('Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.')\n", "text.tag_syntax()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('Poe', {'end': 3, 'parser_out': [['@P>', 1]], 'sent_id': 0, 'start': 0}),\n", " ('ees', {'end': 7, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 4}),\n", " ('õlut', {'end': 12, 'parser_out': [['@ADVL', 3]], 'sent_id': 0, 'start': 8}),\n", " ('joonud',\n", " {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13}),\n", " ('mehed',\n", " {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20}),\n", " ('häirisid',\n", " {'end': 34, 'parser_out': [['ROOT', -1]], 'sent_id': 0, 'start': 26}),\n", " ('kohalikke',\n", " {'end': 44, 'parser_out': [['@PRD', 5]], 'sent_id': 0, 'start': 35}),\n", " (',', {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44}),\n", " ('aga', {'end': 49, 'parser_out': [['@J', 12]], 'sent_id': 0, 'start': 46}),\n", " ('ükskõikne',\n", " {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50}),\n", " ('politsei',\n", " {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60}),\n", " ('ei', {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69}),\n", " ('teinud',\n", " {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72}),\n", " ('midagi',\n", " {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79}),\n", " ('.', {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85})]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(zip(text.word_texts, text['conll_syntax']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### VISLCG3" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "VISLCG3 parseri kasutamiseks on vaja VISLCG3 esmalt installida oma arvutisse (vt juhendit [siit](http://visl.sdu.dk/cg3/single/#installation)). 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)." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "from estnltk.syntax.parsers import VISLCG3Parser" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# See käsk niisugusel kujul tõenäoliselt ei tööta - peate vislcg3 asukoha määrama vastavalt sellele,\n", "# kuhu oma arvutis selle installisite\n", "parser = VISLCG3Parser(vislcg_cmd='C:\\\\cg3\\\\bin\\\\vislcg3.exe')" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# Kui VISLCG3 kataloog on PATHi lisatud, saab isendi tekitada teed ette andmata\n", "parser = VISLCG3Parser()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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):" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "text = Text('Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.', syntactic_parser = parser)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'paragraphs': [{'end': 86, 'start': 0}],\n", " 'sentences': [{'end': 86, 'start': 0}],\n", " 'text': 'Poe ees õlut joonud mehed häirisid kohalikke, aga ükskõikne politsei ei teinud midagi.',\n", " 'vislcg3_syntax': [{'end': 3,\n", " 'parser_out': [['@P>', 1]],\n", " 'sent_id': 0,\n", " 'start': 0},\n", " {'end': 7, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 4},\n", " {'end': 12, 'parser_out': [['@OBJ', 3]], 'sent_id': 0, 'start': 8},\n", " {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13},\n", " {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20},\n", " {'end': 34, 'parser_out': [['@FMV', -1]], 'sent_id': 0, 'start': 26},\n", " {'end': 44, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 35},\n", " {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44},\n", " {'end': 49, 'parser_out': [['@J', 10]], 'sent_id': 0, 'start': 46},\n", " {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50},\n", " {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60},\n", " {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69},\n", " {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72},\n", " {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79},\n", " {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85}],\n", " 'words': [{'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg g',\n", " 'lemma': 'Pood',\n", " 'partofspeech': 'H',\n", " 'root': 'Pood',\n", " 'root_tokens': ['Pood']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg g',\n", " 'lemma': 'Poe',\n", " 'partofspeech': 'H',\n", " 'root': 'Poe',\n", " 'root_tokens': ['Poe']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'Poe',\n", " 'partofspeech': 'H',\n", " 'root': 'Poe',\n", " 'root_tokens': ['Poe']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg g',\n", " 'lemma': 'pood',\n", " 'partofspeech': 'S',\n", " 'root': 'pood',\n", " 'root_tokens': ['pood']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'o',\n", " 'lemma': 'pugema',\n", " 'partofspeech': 'V',\n", " 'root': 'puge',\n", " 'root_tokens': ['puge']}],\n", " 'end': 3,\n", " 'start': 0,\n", " 'text': 'Poe'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'ees',\n", " 'partofspeech': 'D',\n", " 'root': 'ees',\n", " 'root_tokens': ['ees']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'ees',\n", " 'partofspeech': 'K',\n", " 'root': 'ees',\n", " 'root_tokens': ['ees']},\n", " {'clitic': '',\n", " 'ending': 's',\n", " 'form': 'sg in',\n", " 'lemma': 'esi',\n", " 'partofspeech': 'S',\n", " 'root': 'esi',\n", " 'root_tokens': ['esi']}],\n", " 'end': 7,\n", " 'start': 4,\n", " 'text': 'ees'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 't',\n", " 'form': 'sg p',\n", " 'lemma': 'õlu',\n", " 'partofspeech': 'S',\n", " 'root': 'õlu',\n", " 'root_tokens': ['õlu']}],\n", " 'end': 12,\n", " 'start': 8,\n", " 'text': 'õlut'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'nud',\n", " 'form': 'nud',\n", " 'lemma': 'jooma',\n", " 'partofspeech': 'V',\n", " 'root': 'joo',\n", " 'root_tokens': ['joo']},\n", " {'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'joonu',\n", " 'partofspeech': 'S',\n", " 'root': 'joo=nu',\n", " 'root_tokens': ['joonu']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'joonud',\n", " 'partofspeech': 'A',\n", " 'root': 'joo=nud',\n", " 'root_tokens': ['joonud']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'joonud',\n", " 'partofspeech': 'A',\n", " 'root': 'joo=nud',\n", " 'root_tokens': ['joonud']},\n", " {'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'joonud',\n", " 'partofspeech': 'A',\n", " 'root': 'joo=nud',\n", " 'root_tokens': ['joonud']}],\n", " 'end': 19,\n", " 'start': 13,\n", " 'text': 'joonud'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'mees',\n", " 'partofspeech': 'S',\n", " 'root': 'mees',\n", " 'root_tokens': ['mees']}],\n", " 'end': 25,\n", " 'start': 20,\n", " 'text': 'mehed'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'sid',\n", " 'form': 'sid',\n", " 'lemma': 'häirima',\n", " 'partofspeech': 'V',\n", " 'root': 'häiri',\n", " 'root_tokens': ['häiri']}],\n", " 'end': 34,\n", " 'start': 26,\n", " 'text': 'häirisid'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'e',\n", " 'form': 'pl p',\n", " 'lemma': 'kohalik',\n", " 'partofspeech': 'A',\n", " 'root': 'kohalik',\n", " 'root_tokens': ['kohalik']}],\n", " 'end': 44,\n", " 'start': 35,\n", " 'text': 'kohalikke'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '',\n", " 'form': '',\n", " 'lemma': ',',\n", " 'partofspeech': 'Z',\n", " 'root': ',',\n", " 'root_tokens': [',']}],\n", " 'end': 45,\n", " 'start': 44,\n", " 'text': ','},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'aga',\n", " 'partofspeech': 'J',\n", " 'root': 'aga',\n", " 'root_tokens': ['aga']}],\n", " 'end': 49,\n", " 'start': 46,\n", " 'text': 'aga'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'ükskõikne',\n", " 'partofspeech': 'A',\n", " 'root': 'üks_kõikne',\n", " 'root_tokens': ['üks', 'kõikne']}],\n", " 'end': 59,\n", " 'start': 50,\n", " 'text': 'ükskõikne'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg g',\n", " 'lemma': 'politsei',\n", " 'partofspeech': 'S',\n", " 'root': 'politsei',\n", " 'root_tokens': ['politsei']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'politsei',\n", " 'partofspeech': 'S',\n", " 'root': 'politsei',\n", " 'root_tokens': ['politsei']}],\n", " 'end': 68,\n", " 'start': 60,\n", " 'text': 'politsei'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'ei',\n", " 'partofspeech': 'D',\n", " 'root': 'ei',\n", " 'root_tokens': ['ei']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'neg',\n", " 'lemma': 'ei',\n", " 'partofspeech': 'V',\n", " 'root': 'ei',\n", " 'root_tokens': ['ei']}],\n", " 'end': 71,\n", " 'start': 69,\n", " 'text': 'ei'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'nud',\n", " 'form': 'nud',\n", " 'lemma': 'tegema',\n", " 'partofspeech': 'V',\n", " 'root': 'tege',\n", " 'root_tokens': ['tege']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': '',\n", " 'lemma': 'teinud',\n", " 'partofspeech': 'A',\n", " 'root': 'tei=nud',\n", " 'root_tokens': ['teinud']},\n", " {'clitic': '',\n", " 'ending': '0',\n", " 'form': 'sg n',\n", " 'lemma': 'teinud',\n", " 'partofspeech': 'A',\n", " 'root': 'tei=nud',\n", " 'root_tokens': ['teinud']},\n", " {'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'teinud',\n", " 'partofspeech': 'A',\n", " 'root': 'tei=nud',\n", " 'root_tokens': ['teinud']},\n", " {'clitic': '',\n", " 'ending': 'd',\n", " 'form': 'pl n',\n", " 'lemma': 'teinu',\n", " 'partofspeech': 'S',\n", " 'root': 'teinu',\n", " 'root_tokens': ['teinu']}],\n", " 'end': 78,\n", " 'start': 72,\n", " 'text': 'teinud'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': 'dagi',\n", " 'form': 'sg p',\n", " 'lemma': 'miski',\n", " 'partofspeech': 'P',\n", " 'root': 'miski',\n", " 'root_tokens': ['miski']}],\n", " 'end': 85,\n", " 'start': 79,\n", " 'text': 'midagi'},\n", " {'analysis': [{'clitic': '',\n", " 'ending': '',\n", " 'form': '',\n", " 'lemma': '.',\n", " 'partofspeech': 'Z',\n", " 'root': '.',\n", " 'root_tokens': ['.']}],\n", " 'end': 86,\n", " 'start': 85,\n", " 'text': '.'}]}" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "text.tag_syntax()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nagu näeme, siis kui tekst on analüüsitud VISLCG3 parseriga, lisatakse süntaktiline analüüs kihti 'vislcg3_syntax':" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[{'end': 3, 'parser_out': [['@P>', 1]], 'sent_id': 0, 'start': 0},\n", " {'end': 7, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 4},\n", " {'end': 12, 'parser_out': [['@OBJ', 3]], 'sent_id': 0, 'start': 8},\n", " {'end': 19, 'parser_out': [['@AN>', 4]], 'sent_id': 0, 'start': 13},\n", " {'end': 25, 'parser_out': [['@SUBJ', 5]], 'sent_id': 0, 'start': 20},\n", " {'end': 34, 'parser_out': [['@FMV', -1]], 'sent_id': 0, 'start': 26},\n", " {'end': 44, 'parser_out': [['@ADVL', 5]], 'sent_id': 0, 'start': 35},\n", " {'end': 45, 'parser_out': [['xxx', 6]], 'sent_id': 0, 'start': 44},\n", " {'end': 49, 'parser_out': [['@J', 10]], 'sent_id': 0, 'start': 46},\n", " {'end': 59, 'parser_out': [['@AN>', 10]], 'sent_id': 0, 'start': 50},\n", " {'end': 68, 'parser_out': [['@SUBJ', 12]], 'sent_id': 0, 'start': 60},\n", " {'end': 71, 'parser_out': [['@NEG', 12]], 'sent_id': 0, 'start': 69},\n", " {'end': 78, 'parser_out': [['@FMV', 5]], 'sent_id': 0, 'start': 72},\n", " {'end': 85, 'parser_out': [['@OBJ', 12]], 'sent_id': 0, 'start': 79},\n", " {'end': 86, 'parser_out': [['xxx', 13]], 'sent_id': 0, 'start': 85}]" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "text['vislcg3_syntax']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Puustruktuur" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "text = Text(\"Pisike poiss kimbutas kirpe. Need kirbud ei julgenud kiusata karusid.\")" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "# Loome tekstist puuobjektid ehk leiame kõik tekstis esinevad juurtipud\n", "# Kui me parserit ei täpsusta, tehakse analüüs MaltParseriga\n", "trees = text.syntax_trees()" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(trees)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[<estnltk.syntax.utils.Tree at 0x23a849fe198>,\n", " <estnltk.syntax.utils.Tree at 0x23a849fedd8>]" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "trees" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'kimbutas'" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "trees[0].text" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[('Pisike', ['@AN>']), ('poiss', ['@SUBJ'])]\n", "[('Need', ['@NN>']), ('kirbud', ['@SUBJ'])]\n" ] } ], "source": [ "# Leiame subjektid ja nendele alluvad sõnad\n", "for tree in trees:\n", " subject_nodes = tree.get_children( label=\"@SUBJ\" )\n", " for subj_node in subject_nodes:\n", " subject_and_children = subj_node.get_children( include_self=True, sorted=True )\n", " # Prindime nii sõna kui funktsioonimärgendi\n", " print( [(node.text, node.labels) for node in subject_and_children] )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lisaks eelmises näites kasutatud funktsioonimärgendile saame seada päringule ka muid kitsendusi, selleks vajame WordTemplate moodulit:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "from estnltk.mw_verbs.utils import WordTemplate\n", "from estnltk.names import POSTAG, FORM, LEMMA" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Loome sõnamalli, mis otsiks nimetavas käändes nimisõnu - nii ainsuses kui mitmuses. Sõnamallides saab kasutada regulaaravaldisi:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "noun_in_nominative = WordTemplate({POSTAG: 'S', FORM: '^(sg n|pl n)$'})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Leiame esimesest puust mallile vastavad sõnad:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "nominative_subj = trees[0].get_children(word_template=noun_in_nominative, label = '@SUBJ' )" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['poiss']" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[node.text for node in nominative_subj]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Loome sõnamalli, mis otsib lemmat \"kirp\":" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "kirp = WordTemplate({LEMMA: 'kirp'})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Leiame sõnad, mis on sõna \"kirp\" ülemusteks (parent):" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "kimbutas\n", "julgenud\n" ] } ], "source": [ "for tree in trees:\n", " kirbud = tree.get_children(word_template = kirp)\n", " for element in kirbud:\n", " print(element.parent.text)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Kui tahaksime aga kätte saada sõnavormide asemel lemmasid, peaksime süvenema token klassimuutujasse:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'analysis': [{'clitic': '', 'lemma': 'kimbutama', 'root': 'kimbuta', 'form': 's', 'root_tokens': ['kimbuta'], 'partofspeech': 'V', 'ending': 's'}], 'text': 'kimbutas', 'end': 21, 'start': 13}\n", "LEMMA: kimbutama\n", "{'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}\n", "LEMMA: julgema\n", "LEMMA: julgenud\n", "LEMMA: julgenud\n", "LEMMA: julgenud\n" ] } ], "source": [ "for tree in trees:\n", " kirbud = tree.get_children(word_template = kirp)\n", " for element in kirbud:\n", " print(element.parent.token)\n", " for i in element.parent.token['analysis']:\n", " print('LEMMA: ' + i['lemma'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ülesanne 1. Süntaksianalüsaatorite väljundite lahtimõtestamine (1p + 0,5 boonuspunkti)\n", "\n", "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.\n", "* Mille poolest erineb parserite poolt väljastatud pindsüntaktiline märgendus? Seletage erinevused inimkeeli lahti (st kasutades keeleteaduslikke termineid, mitte programmi väljastatavaid märgendeid). (0,5p)\n", "* Milline näeb välja kummagi parseri poolt väljastatud sõltuvuspuu selle lause jaoks? Visualiseerige mõlema parseri poolt väljastatud puud, kasutades omale meelepäraseid töövahendeid (võimalikud variandid: paber ja pliiats\\*, Paint, Word, Latex\\*\\*, ...). Kui analüüs on jäänud mitmeseks, kasutage variantidest esimest. (0,5p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\\*Paberi ja pliiatsi kasutamisel palun esitage puu järgmises praktikumis või pildistatuna/skannituna.\n", "\n", "\\*\\*Puu esitamine Latexis tehtuna annab **0,5 boonuspunkti**." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ülesanne 2. Naine, mees ja teised Hargla lühijuttudes (3p)\n", "Teostage süntaktiline analüüs Indrek Hargla lühijuttudel, mille leiate kataloogist *hargla*. Kas kasutada MaltParserit või VislCG3, võite ise otsustada.\n", "\n", "A. Leidke, kummale jääb juttudes sagedamini subjekti, kummale objekti roll - kas mehele või naisele? Milliseid tegevusi tegemas kujutatakse meest, milliseid naist?\n", "Selleks:\n", "* leidke, mitmes lauses esineb sõna \"naine\" subjektina, mitmes \"mees\"? Mitmes objektina? Milline on subjektina vs objektina käsitlemise suhe naiste puhul, milline meestel?\n", "* millised on 5 sagedasemat tegevust, mida teeb subjektina \"naine\", millised \"mees\" puhul? Tegevuse puhul leidke kõigepealt vastava subjekti otsene ülemus, kui see aga pole tegusõna (nt koordinatsiooni puhul), siis leidke lause kõrgeim ülemus.\n", "\n", "\n", "B. Kuidas aga jagunevad tegevused elusate ja elutute subjektide vahel? Kasutage elususe-elutuse määramiseks:\n", "* eelmises praktikumis Wordnetist leitud olendite listi - need on elus\n", "* EstNLTK pärisnimede tuvastajat (arvestades, et isikud on elus, organisatsioonid ja lokatsioonid mitte) ning \n", "* eeldust, et isikulised asesõnad tähistavad samuti elusolendeid\n", "* eeldust, et kõik, mis pole elus, on elutu\n", "\n", "Ehk:\n", "* leidke lausest subjekt\n", "* otsustage, kas subjekt on elus või mitte\n", "* leidke tegevus, mida subjekt teeb, samal põhimõttel, nagu ülesande A-osas\n", "\n", "Millised on sagedasemad elus ja millised elutute subjektide tegevused? Kas leidub tegevusi, mida teevad ainult elus või ainult elutud subjektid?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## XML andmeformaat\n", "\n", "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](https://et.wikipedia.org/wiki/%C3%9Chtlustatud_%C3%9Cldine_Markeerimise_Keel)), mis on sisuliselt veebilehekülgede \"lähtekoodi\" keel -- seega enamik veebis olevast (pool)tekstilisest materjalist on salvestatud just selles formaadis." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### _BeautifulSoup_\n", "\n", "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_](https://www.crummy.com/software/BeautifulSoup/). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "_\\*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._" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:\n", "\n", " conda install bs4\n", "\n", "Et praktikumi näited ilusti töötaksid, tuleb lisaks installida ka teek `lxml`. Seda saab teha käsuga:\n", "\n", " conda install lxml\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<?xml version=\"1.0\"?>\n", "<html>\n", " <body>\n", " <text>\n", " <div0 type=\"tervikteos\">\n", " Taadeldus\n", " <p>\n", " <bibl>\n", " <author>\n", " <s>\n", " Maniakkide Tänav\n", " </s>\n", " </author>\n", " </bibl>\n", " </p>\n", " <p>\n", " /---/\n", " </p>\n", " <p>\n", " <s>\n", " \" Lähme Widgeti juurde , \" ütlesin .\n", " </s>\n", " <s>\n", " Widget oli kursaõde , kes elas siinsamas Annelinnas .\n", " </s>\n", " <s>\n", " Tee peal üritas Ults mulle asja selgitada .\n", " </s>\n", " </p>\n", " <p>\n", " /---/\n", " </p>\n", " </div0>\n", " </text>\n", " </body>\n", "</html>\n" ] } ], "source": [ "from bs4 import BeautifulSoup\n", "\n", "# Näite-XML ( lõigud pärinevad Tasakaalus korpuse ilukirjanduse failist 'tanav_jutt4.tei' )\n", "xml_content = '''\n", "<?xml version=\"1.0\"?>\n", "<text> <body> <div0 type='tervikteos'> <head> Taadeldus </head>\n", "<p> <bibl> <author> <s> Maniakkide Tänav </s> </author> </bibl> </p> <p> /---/ </p>\n", "<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>\n", "</div0> </body> </text>\n", "'''\n", "\n", "# Parsime XML-sisu lxml parseri abil\n", "soup = BeautifulSoup(xml_content,'lxml')\n", "\n", "# Kuvame XML elementide sisud ilusti joondatult:\n", "print(soup.prettify())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* **Märkus HTML olemite kohta: ** Varasemates XML versioonides polnud kodeeringu _'utf-8'_ kasutamine otseselt toetatud ning seetõttu tuli tekstide salvestamisel _'utf-8'_ sümbolid, sealhulgas ka eesti täpitähed, konverteerida nn _HTML olemiteks_. Seetõttu võimegi eelmises näites täheldada olemeid `ü` (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;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### XML-ist otsimine\n", "\n", "_BeautfifulSoup_ lubab otsida XML-ist, näiteks saab leida kõikide \"lauseliste\"-elementide sisud:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[<s> Maniakkide Tänav </s>,\n", " <s> \" Lähme Widgeti juurde , \" ütlesin . </s>,\n", " <s> Widget oli kursaõde , kes elas siinsamas Annelinnas . </s>,\n", " <s> Tee peal üritas Ults mulle asja selgitada . </s>]" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "soup.find_all('s')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lisaks on võimalik otsida kindlate atribuutide väärtuste järgi." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "<div0 type=\"tervikteos\"> Taadeldus \n", "<p> <bibl> <author> <s> Maniakkide Tänav </s> </author> </bibl> </p> <p> /---/ </p>\n", "<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>\n", "</div0>" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Leiame elemendi, mille type='tervikteos'\n", "soup.find(type='tervikteos')" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "' 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'" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Leiame vastava elemendi tekstilise sisu (ilma XML elementideta)\n", "soup.find(type='tervikteos').text" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Positiivsetel otsingutulemustel saab omakorda rakendada alamotsinguid.\n", "\n", "Näide: kitsendame otsingut kuni saame kätte autori nime sõne kujul:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "<author> <s> Maniakkide Tänav </s> </author>" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "soup.find(type='tervikteos').author" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "' Maniakkide Tänav '" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "soup.find(type='tervikteos').author.s.text" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Näide: kitsendame, kuni saame kätte lauseliste elementide tekstilise sisu. Kõigepealt esimese \"lause\" korral:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "' Maniakkide Tänav '" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "soup.find_all('s')[0].text" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Seejärel kõigil lausetel:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[' Maniakkide Tänav ',\n", " ' \" Lähme Widgeti juurde , \" ütlesin . ',\n", " ' Widget oli kursaõde , kes elas siinsamas Annelinnas . ',\n", " ' Tee peal üritas Ults mulle asja selgitada . ']" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[ s.text for s in soup.find_all('s') ]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Lisamaterjal: EstNLTK funktsioonid koondkorpuse XML failide lugemiseks \n", "### (NB! Allpool järgneb veel HTML-andmeformaadi tutvustus ja üks ülesanne!)\n", "\n", "Tartu Ülikooli [koondkorpuse](http://www.cl.ut.ee/korpused/segakorpus/index.php?lang=et) XML formaadis tekstide sisselugemiseks on EstNLTK-s olemas ka eraldiseisvad funktsioonid. Need leiab moodulist `estnltk.teicorpus`:\n", "\n", " * `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; \n", "\n", "\n", " * `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.\n", "\n", "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']`;\n", "\n", "Kuigi funktsioonid lubavad vaikimisi kodeeringu täpsustamata jätta (`encoding=None`), tuleks koondkorpuse XML failide lugemisel siiski kodeering alati määrata (\"utf-8\").\n", "\n", "Kogu koondkorpuse XML failide automaatset konverteerimist kirjeldab [juhend](https://estnltk.github.io/estnltk/1.4.1/tutorials/tei.html) ning koondkorpus ise on saadaval [siin veebilehel](http://www.cl.ut.ee/korpused/segakorpus/index.php?lang=et) (linke järgides jõuab sealt koondkorpuse kokkupakitud XML failideni).\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Lausestuse säilitamine sisselugemisfunktsioonide kasutamisel\n", "\n", "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. \n", "\n", "Kuna koondkorpuse XML failis puudub päris algne (ilma märgendusteta) tekst, peavad meetodid `parse_tei_corpora()` ja `parse_tei_corpus()` teksti rekonstrueerima. \n", "Rekonstrueerimise käigus paigutatakse iga lause (tekstisisu `<s>`-märgendite vahel) eraldi reale (ehk siis: laused on eraldatud reavahetustega). \n", "EstNLTK tavapärane lausestaja aga sellist lausestust ei tunne, vaid otsib lausepiire ikka punktuatsioonisümbolite järgi. \n", "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.\n", "\n", "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.\n", "Kuidas seda teha, vaatame järgmise näite varal detailsemalt." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'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'" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from estnltk.teicorpus import parse_tei_corpus\n", "\n", "# Loeme sisse ilukirjandustekstid XML-failist \n", "# (fail pärineb TÜ koondkorpuse ilukirjanduse osast)\n", "texts = parse_tei_corpus('ilu_habicht_wcnt.tasak.xml', target=['tervikteos'], encoding='utf-8')\n", "# Uurime esimese teksti esimest 700 sümbolit: paistab, et laused on eraldatud reavahetustega\n", "texts[0]['text'][:700]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Text` objekti luues on võimalik argumentide abil määrata, milliseid tekstitöötlusvahendeid analüüsimisel kasutatakse. \n", "Seega võib vaikimisi kasutatava lausestaja (argument `\"sentence_tokenizer\"`) asendada NLTK [`LineTokenizer`](http://www.nltk.org/api/nltk.tokenize.html#nltk.tokenize.simple.LineTokenizer)'iga, mis paneb lausepiirid ainult reavahetuste kohale:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "# Kirjutame tekstid ümber nii, et tavalausestaja asemel kasutatakse LineTokenizer'it\n", "\n", "from estnltk import Text\n", "from nltk.tokenize import LineTokenizer\n", "\n", "# Sisendargumentide sõnastik, kus on määratud uus lausestaja\n", "kwargs = {\n", " \"sentence_tokenizer\": LineTokenizer()\n", "}\n", "sentence_tokenized_texts = []\n", "for text in texts:\n", " # Teeme vana teksti põhjal uue teksti\n", " text = Text(text['text'], **kwargs)\n", " # Lausestame teksti\n", " text = text.tokenize_sentences()\n", " sentence_tokenized_texts.append(text)" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['Tegelikult oli see igati loogiline , et survaivalid virtuaalsusse kolisid .',\n", " 'Linnaäärne võserik jäi kanofiilidele , kes nagu nende lemmikudki ei pidanud enam juhuslike laserikiirte või värvilirakate pärast muretsema .',\n", " 'Mitte et see mulle kuidagi korda oleks läinud .',\n", " 'Võserik tuli mulle ainult seepärast meelde , et ma siin ise ühe säärasega pean tegelema .',\n", " 'Kirves käes .',\n", " 'Siin , muideks , on kusagil Dalaranis , peoonile ei hakka keegi üksikasju täpsustama .',\n", " 'Kakskümmend korda kirvega vastu puud .',\n", " 'Siis kukub puu maha .',\n", " 'Ma võtan selle otsapidi õlale ja punun saeveskisse .',\n", " 'Või longin .',\n", " 'Kui grunt ei näe .',\n", " 'Kõik see on täiesti illusoorne , kuid väga väsitav .',\n", " 'Ja vasak õlg valutab .',\n", " 'Üsna realistlikult .',\n", " 'Kui me siia saarele jõudsime , valvas seda metsatukka griffin .']" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Uurime lausestamise tulemusi (esimesed 15 lauset)\n", "sentence_tokenized_texts[0].sentence_texts[:15]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " * _Veel tehnilist juttu_: `Text` objekti sisendargumentide abil on võimalik ka sõnestaja välja vahetada (selle kohta kirjutatakse täpsemalt [siin](https://estnltk.github.io/estnltk/1.4.1/tutorials/text.html#tokenization)). Mõningatel juhtudel leiab sobiva uue sõnestaja [NLTK sõnestusmoodulist](http://www.nltk.org/api/nltk.tokenize.html#nltk-tokenize-package), 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. \n", " \n", " Sisuliselt peab endakirjutatud klass järgima NLTK [`StringTokenizer`]( http://www.nltk.org/api/nltk.tokenize.html#nltk.tokenize.api.StringTokenizer)'i liidest ehk siis: olema `StringTokenizer`'i alamklass ning pakkuma välja meetodid [`span_tokenize`](http://www.nltk.org/api/nltk.tokenize.html#nltk.tokenize.api.StringTokenizer.span_tokenize) ja [`tokenize`]( http://www.nltk.org/api/nltk.tokenize.html#nltk.tokenize.api.StringTokenizer.tokenize). Endakirjutatud sõnestamise loogika lähebki siis nendesse meetoditesse.\n", " \n", " Ühe koodinäite, kuidas teha järelparandustega lausestaja, leiab [siit](https://github.com/estnltk/estnltk/blob/version_1.4/estnltk/tokenizers/sent_tokenizer_for_koond.py). Tegemist on lausestajaga, mis teostab kõigepealt tavapärase EstNLTK lausestuse, ning rakendab seejärel regulaaravaldisi, et tuvastada ja parandada kõige sagedasemaid lausestusvigu." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " * _Märkus_: Kui `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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## HTML" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "from bs4 import BeautifulSoup" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "html = '''<!DOCTYPE html>\n", "<html>\n", "<body>\n", "\n", "<h1>Pole probleemi</h1>\n", "\n", "<p>Oli õhtune tipptund, tuli sõita läbi linna.</p>\n", "<p>Tuled fooris vahetusid, kuid üle veel ei saanud.</p>\n", "\n", "</body>\n", "</html>'''" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": [ "soup = BeautifulSoup(html, 'html.parser')" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Oli õhtune tipptund, tuli sõita läbi linna.\n", "Tuled fooris vahetusid, kuid üle veel ei saanud.\n" ] } ], "source": [ "for i in soup.find_all('p'):\n", " print(i.get_text())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ülesanne 3. HTML sisendi lugemine ja parserite võrdlus (2p)\n", "\n", "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:\n", "* Kui suur osa sõnadest saab nii sama pindsüntaksi märgendi kui sõltuvusmärgendi mõlemalt parserilt? Lugege siia hulka ka juhud, kus VISLCG3 väljund on jäänud mitmeseks, aga siiski sisaldab MaltParseri väljundit ka.\n", "* Kui suur osa sõnadest saab erineva pindsüntaksi märgendi?\n", "* Kui suur osa sõnadest saab erineva sõltuvussüntaksi märgendi?" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.3" } }, "nbformat": 4, "nbformat_minor": 1 }