From 4df3aec34b74a8021140b288c1527c97ce345e6d Mon Sep 17 00:00:00 2001 From: Kirill Malakhov Date: Fri, 16 Apr 2021 13:07:35 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20spaCy=20updates=20up=20to=203.0.5?= =?UTF-8?q?=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ⚠️ Cautions and Deprecations! - update spaCy up to `3.0.5`; - update spaCy model `en_core_web_sm` up to `3.0.0`; - update `pdfminer.six` up to `20201018`; - update `flask-cors` up to `3.0.10`; - added new documents for testing services for English language; - changed API endpoint URL to get XML structure ** allterms.xml ** for English language: old: `host[:port]/ken/api/en/file/allterm` new: `host[:port]/ken/api/en/allterms` From now on, the same API endpoint is used for processing/analyzing English texts in the form of messages and files: `host[:port]/ken/api/en/allterms`. ### 🏭 New features - аdded API for text processing / analysis in English (namely, receiving `allterms.xml`) in the form of messages. Example of input data: ```json { "message": "After the vision of the Semantic Web was broadcasted at the turn of the millennium, ontology became a synonym for the solution to many problems concerning the fact that computers do not understand human language: if there were an ontology and every document were marked up with it and we had agents that would understand the mark-up, then computers would finally be able to process our queries in a really sophisticated way. Some years later, the success of Google shows us that the vision has not come true, being hampered by the incredible amount of extra work required for the intellectual encoding of semantic mark-up – as compared to simply uploading an HTML page." } ``` HTTP method: POST API Endpoint: `host[:port]/ken/api/en/allterms` ### 👍 Improvements - improved processing of complex English terms, in particular, from three words; - updated API description for text processing / analysis in the form of messages and files for English and Ukrainian languages in the file `HELP.md`. ### 🔴 Bug fixes - fixed errors in displaying / visualizing dependencies for terms in the `# depparse_tab` element, namely in` # displacy`; - various bugfixes. --- CHANGELOG.md | 37 + Dockerfile | 2 +- HELP.md | 85 +- README.md | 4 +- deploy/requirements.txt | 15 +- srvr.py | 1657 ++++++++++++++------ static/javascripts/recap-en.js | 3 +- static/stylesheets/style-en.css | 4 +- templates/changelog.html | 32 +- templates/en.html | 2 +- templates/index.html | 4 +- tests/load-tests/Ridge CV - Jon Herud.docx | Bin 0 -> 66138 bytes tests/load-tests/about-ontology-bigger.txt | 43 + 13 files changed, 1381 insertions(+), 507 deletions(-) create mode 100644 tests/load-tests/Ridge CV - Jon Herud.docx create mode 100644 tests/load-tests/about-ontology-bigger.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index baf2cc1..bad787a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## v3.2.0, 2021-04-16 + +### ⚠️ Зауваження + +- ENG🇬🇧 оновлено бібліотеку spaCy до версії `3.0.5`; +- ENG🇬🇧 оновлено модель бібліотеки spaCy для англійської мови `en_core_web_sm` до версії `3.0.0`; +- оновлено бібліотеку `pdfminer.six` до версії `20201018`; +- оновлено бібліотеку `flask-cors` до версії `3.0.10`; +- ENG🇬🇧 додано нові документи для тестування сервісів для Англійської мови; +- ENG🇬🇧 Змінено URL кінцевої точки API для отримання XML-структуру **allterms.xml**: + було: + `host[:port]/ken/api/en/file/allterm` + стало: + `host[:port]/ken/api/en/allterms` + Відтепер для обробки/аналізу текстів у вигляді повідомлень та файлів діє одна й та сама кінцева точка API: `host[:port]/ken/api/en/allterms`. + +### 🏭 Нові можливості + +- ENG🇬🇧 Додано API для обробки/аналізу текстів (а саме, отримання `allterms.xml`) у вигляді повідомлень. Приклад вхідних даних: + ```json + { + "message": "After the vision of the Semantic Web was broadcasted at the turn of the millennium, ontology became a synonym for the solution to many problems concerning the fact that computers do not understand human language: if there were an ontology and every document were marked up with it and we had agents that would understand the mark-up, then computers would finally be able to process our queries in a really sophisticated way. Some years later, the success of Google shows us that the vision has not come true, being hampered by the incredible amount of extra work required for the intellectual encoding of semantic mark-up – as compared to simply uploading an HTML page." + } + ``` + HTTP method: POST + Кінцева точка: `host[:port]/ken/api/en/allterms` + +### 👍 Покращення + +- ENG🇬🇧 покращено обробку складних термінів, зокрема, з трьох слів; +- UKR🇺🇦 ENG🇬🇧 Оновлено опис API для обробки/аналізу текстів у вигляді повідомлень та файлів для Англійської та Української мов у файлі `HELP.md`. + +### 🔴 Виправлення помилок + +- UKR🇺🇦 ENG🇬🇧 виправлені помилки відображення/візуалізації залежностей для термінів в елементі `#depparse_tab`, а саме в `#displacy`; +- UKR🇺🇦 ENG🇬🇧 дрібні виправлення. + ## v3.1.0, 2021-03-02 ### 👍 Покращення diff --git a/Dockerfile b/Dockerfile index fdb4e05..0b965d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.9-slim-stretch +FROM python:3.7.10-slim-stretch # FROM python:3.7.7-slim-stretch # FROM python:2.7-slim-stretch diff --git a/HELP.md b/HELP.md index 0ee61f9..ee1a8f6 100644 --- a/HELP.md +++ b/HELP.md @@ -1,4 +1,4 @@ -# Конспект для української мови (API для обробки/аналізу текстів у вигляді повідомлень) +# Конспект для Української мови (API для обробки/аналізу текстів у вигляді повідомлень та файлів) ### API endpoints @@ -6,8 +6,8 @@ | :----: | :------------------------------------------: | :----------------------------------------------------------- | :---------: | | **E1** | Черга для обробки повідомлень | `host[:port]/kua/api/task/message/queued`
`http://194.44.28.250:45100/kua/api/task/message/queued` | POST | | **E2** | Статус виконання обробки | `host[:port]/kua/api/task/status`
`http://194.44.28.250:45100/kua/api/task/status` | GET | -| **E3** | Отримати XML-структуру **allterms** | `host[:port]/kua/api/task/allterms/result`
`http://194.44.28.250:45100/kua/api/task/allterms/result` | GET | -| **E4** | Отримати XML-структуру **parce** | `host[:port]/kua/api/task/parce/result`
`http://194.44.28.250:45100/kua/api/task/parce/result` | GET | +| **E3** | Отримати XML-структуру **allterms.xml** | `host[:port]/kua/api/task/allterms/result`
`http://194.44.28.250:45100/kua/api/task/allterms/result` | GET | +| **E4** | Отримати XML-структуру **parce.xml** | `host[:port]/kua/api/task/parce/result`
`http://194.44.28.250:45100/kua/api/task/parce/result` | GET | ##### E1 - Input data @@ -86,4 +86,81 @@ curl "http://194.44.28.250:45100/kua/api/task/parce/result?id=uwsgi_spoolfile_on або -`Return 200` та XML-структура \ No newline at end of file +`Return 200` та XML-структура + +# Конспект для Англійської мови (API для обробки/аналізу текстів у вигляді повідомлень та файлів) + +### API endpoints + +| | Service | API endpoint | HTTP method | +| :----: | :------------------------------------------: | :----------------------------------------------------------- | :---------: | +| **E1** | Отримати XML-структуру **allterms.xml** з файлу | `host[:port]/ken/api/en/allterms` | POST | +| **E2** | Отримати XML-структуру **allterms.xml** з повідомлення | `host[:port]/ken/api/en/allterms` | POST | +| **E3** | Отримати XML-структуру **parce.xml** з файлу | `host[:port]/ken/api/en/file/parcexml` | POST | + +##### E1 - Input data + +Вхідними даними можуть бути файли форматів `.txt`, `.docx`, `.pdf`, які містять текстові дані англійською мовою. + +Використовуючи метод `http`-запиту `POST` можна відправити тільки один файл (доступних форматів) для опрацювання службою формування спеціалізованої `XML`-структури тексту. + +Приклад `POST` запиту до кінцевої точки служби **S1** на мові програмування `JavaScript` з використанням [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API): + +```javascript +# Детальний опис Fetch API за посиланням https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API + +# Файли можна завантажувати за допомогою елемента вводу HTML , FormData() та fetch(). +var formData = new FormData(); +var fileField = document.querySelector('input[type="file"]'); + +# https://developer.mozilla.org/en-US/docs/Web/API/FormData/append +# formData.append(name, value); +formData.append('file', fileField.files[0]); + +fetch("file", 'host[:port]/ken/api/en/allterms', { + method: 'post', + body: formData + }) +.then(response => response.text()) +.catch(error => console.error('Error:', error)) +.then(response => console.log('Success:', response)); +``` +Процес формування спеціалізованої `XML`-структури тексту може зайняти деякий час (в залежності від обсягу тексту), але в загальному випадку вихідні дані формуються миттєво. + +##### E2 - Input data + +```JSON +{ + "message": "After the vision of the Semantic Web was broadcasted at the turn of the millennium, ontology became a synonym for the solution to many problems concerning the fact that computers do not understand human language: if there were an ontology and every document were marked up with it and we had agents that would understand the mark-up, then computers would finally be able to process our queries in a really sophisticated way.." +} +``` + +##### E3 - Input data + +Вхідними даними можуть бути файли форматів `.txt`, `.docx`, `.pdf`. + +Використовуючи метод `http`-запиту `POST` можна відправити тільки один файл (доступних форматів) для опрацювання службою формування спеціалізованої `XML`-структури тексту. + +Приклад `POST` запиту до кінцевої точки служби **S2** на мові програмування `JavaScript` з використанням [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API): + +```javascript +# Детальний опис Fetch API за посиланням https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API + +# Файли можна завантажувати за допомогою елемента вводу HTML , FormData() та fetch(). +var formData = new FormData(); +var fileField = document.querySelector('input[type="file"]'); + +# https://developer.mozilla.org/en-US/docs/Web/API/FormData/append +# formData.append(name, value); +formData.append('file', fileField.files[0]); + +fetch("file", 'host[:port]/ken/api/en/file/parcexml', { + method: 'post', + body: formData + }) +.then(response => response.text()) +.catch(error => console.error('Error:', error)) +.then(response => console.log('Success:', response)); +``` + +Процес формування спеціалізованої `XML`-структури тексту може зайняти деякий час (в залежності від обсягу тексту), але в загальному випадку вихідні дані формуються миттєво. diff --git a/README.md b/README.md index 0e5cd93..293371d 100644 --- a/README.md +++ b/README.md @@ -632,7 +632,7 @@ $ docker run --restart always --name ken -d -p 80:80 ken_image | Позначення |Служба|Кінцева точка API|Метод http-запиту| | :--------: | :---------------------: | :--------- | :--------: | -| **S1** | формування спеціалізованої `XML`-структури тексту *allterms.xml* |`host[:port]/ken/api/en/file/allterms`|POST| +| **S1** | формування спеціалізованої `XML`-структури тексту *allterms.xml* |`host[:port]/ken/api/en/allterms`|POST| | **S2** | формування спеціалізованої `XML`-структури тексту *parce.xml* |`host[:port]/ken/api/en/file/parcexml`|POST| | **S3** | візуалізації залежностей термінів |`host[:port]/ken/api/en/html/depparse/nounchunk`|POST| | **S4** | візуалізації іменованих сутностей тексту |`host[:port]/ken/api/en/html/ner`|POST| @@ -660,7 +660,7 @@ var fileField = document.querySelector('input[type="file"]'); # formData.append(name, value); formData.append('file', fileField.files[0]); -fetch("file", 'host[:port]/ken/api/en/file/allterms', { +fetch("file", 'host[:port]/ken/api/en/allterms', { method: 'post', body: formData }) diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 3ba48d6..534437e 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -1,13 +1,14 @@ Flask==1.1.2 -flask-cors==3.0.8 +# flask-cors==3.0.8 +flask-cors==3.0.10 # spacy>=2.2.0,<3.0.0 -# spacy==2.3.2 -spacy==2.3.5 -# https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.2.5/en_core_web_sm-2.2.5.tar.gz#egg=en_core_web_sm -# https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.0/en_core_web_sm-2.3.0.tar.gz#egg=en_core_web_sm -https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz#egg=en_core_web_sm +spacy==3.0.5 +https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.0.0/en_core_web_sm-3.0.0.tar.gz#egg=en_core_web_sm +# https://github.com/explosion/spacy-models/releases/download/en_core_web_trf-3.0.0/en_core_web_trf-3.0.0.tar.gz#egg=en_core_web_trf +# https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.0.0/en_core_web_lg-3.0.0.tar.gz#egg=en_core_web_lg # pdfminer.six==20200402 -pdfminer.six==20200517 +# pdfminer.six==20200517 +pdfminer.six==20201018 uWSGI==2.0.18 nltk==3.5 chardet==3.0.4 diff --git a/srvr.py b/srvr.py index 085af38..25e891a 100644 --- a/srvr.py +++ b/srvr.py @@ -30,7 +30,7 @@ # logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.DEBUG) logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.ERROR) -# for spooler +# ! for spooler DO NOT FORGET TURN IT ON!!!!! import uwsgi from tasks import konspekt_task_ua @@ -64,6 +64,7 @@ # Load globally spaCy model via package name NLP_EN = spacy.load('en_core_web_sm') +# NLP_EN_TRF = spacy.load('en_core_web_trf') __author__ = "Kyrylo Malakhov and Vitalii Velychko " __copyright__ = "Copyright (C) 2020 Kyrylo Malakhov and Vitalii Velychko " @@ -92,7 +93,7 @@ class XMLResponse(Response): """ # ------------------------------------------------------------------------------------------------------ -# DEBUG functions +# ! DEBUG functions # ------------------------------------------------------------------------------------------------------ # """ @@ -127,7 +128,7 @@ def get_size(obj, seen=None): """ # ------------------------------------------------------------------------------------------------------ -# SECONDARY functions +# ! SECONDARY functions # ------------------------------------------------------------------------------------------------------ # """ @@ -284,7 +285,7 @@ def get_bytes_from_pdfminer(pdf_path): """ # ------------------------------------------------------------------------------------------------------ -# Web app GUI +# ! Web app GUI # ------------------------------------------------------------------------------------------------------ # """ @@ -319,7 +320,7 @@ def get_changelog(): # """ """ -# Visualizers service +# ! Visualizers service # ------------------------------------------------------------------------------------------------------ # """ @@ -338,6 +339,13 @@ def get_dep_parse(): r_t = displacy.parse_deps(doc) return Response(json.dumps(r_t), mimetype='text/plain') +# Noun chunks "base noun phrases" deps visualization with preview +@app.route('/ken/api/en/html/depparse/nounchunk/preview', methods=['POST']) +def get_dep_parse_preview(): + rec = json.loads(request.get_data(as_text=True)) + doc = NLP_EN(rec['text']) + return Response(displacy.render(doc, style="dep", page=True, minify=True), mimetype='text/html') + # NER in text visualization @app.route('/ken/api/en/html/ner', methods=['POST']) def get_ner(): @@ -357,7 +365,7 @@ def get_ner(): # """ """ -# UKRAINIAN PART +# ! UKRAINIAN PART # ------------------------------------------------------------------------------------------------------ # """ @app.route('/kua/api/task/message/queued', methods=['POST']) @@ -579,16 +587,16 @@ def merge_alltermsxml_structurexml(): return result """ -# ENGLISH PART +# ! ENGLISH PART # ------------------------------------------------------------------------------------------------------ # """ """ -# parce.xml service +# ! parce.xml service # ------------------------------------------------------------------------------------------------------ # """ @app.route('/ken/api/en/file/parcexml', methods=['POST']) -def generate_parcexml(): +def get_parce_xml_en(): # check if the post request has the file part if 'file' not in request.files: flash('No file part') @@ -787,304 +795,723 @@ def generate_parcexml(): # """ """ -# allterms.xml service +# ! allterms.xml service # ------------------------------------------------------------------------------------------------------ # """ -@app.route('/ken/api/en/file/allterms', methods=['POST']) -def generate_allterms(): - # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') - return abort(400) - - file = request.files['file'] +# TODO word [e.g.] - is one token !!! +# @app.route('/ken/api/en/file/allterms', methods=['POST']) +@app.route('/ken/api/en/allterms', methods=['POST']) +def get_allterms_xml_en(): + req_data = request.get_json() + logging.debug(req_data) + + if req_data is not None and 'message' in req_data: + raw_text = req_data['message'] + file_name = 'message' + else: + # check if the post request has the file part + if 'file' not in request.files: + flash('No file part') + return abort(400) - # if user does not select file, browser also submit an empty part without filename - if file.filename == '': - flash('No selected file') - return abort(400) + file = request.files['file'] - if file and allowed_file(file.filename): - # TODO doc/docx processing - # pdf processing - if file.filename.rsplit('.', 1)[1].lower() == 'pdf': - pdf_file = secure_filename(file.filename) - destination = "/".join([tempfile.mkdtemp(),pdf_file]) - file.save(destination) - file.close() - if os.path.isfile(destination): - # raw_text = get_text_from_pdfminer(destination) - raw_text = get_text_from_pdf(destination) - # docx processing - if file.filename.rsplit('.', 1)[1].lower() == 'docx': - docx_file = secure_filename(file.filename) - destination = "/".join([tempfile.mkdtemp(),docx_file]) - file.save(destination) - file.close() - if os.path.isfile(destination): - raw_text = get_text_from_docx(destination) - # txt processing - if file.filename.rsplit('.', 1)[1].lower() == 'txt': - # decode the file as UTF-8 ignoring any errors - raw_text = file.read().decode('utf-8', errors='replace') - file.close() - try: - # spaCy doc init + default sentence normalization - doc = NLP_EN(text_normalization_default(raw_text)) - # Measure the Size of doc Python Object - logging.info("%s byte", get_size(doc)) - - """ - # create the file structure - """ - # create root element - root_termsintext_element = ET.Element("termsintext") - # create element - sentences_element = ET.Element("sentences") - # create element - filepath_element = ET.Element("filepath") - filepath_element.text = file.filename - # create element - exporterms_element = ET.Element("exporterms") - - # Helper list for one-word terms - one_word_terms_help_list = [] - # Helper list for two-word terms - two_word_terms_help_list = [] - # Helper list for multiple-word terms (from 4-word terms) - multiple_word_terms_help_list = [] - - noun_chunks = [] + # if user does not select file, browser also submit an empty part without filename + if file.filename == '': + flash('No selected file') + return abort(400) - ''' - # Main text parsing cycle for sentences - ''' - for sentence_index, sentence in enumerate(doc.sents): + if file and allowed_file(file.filename): + file_name = file.filename + # TODO doc/docx processing + # pdf processing + if file.filename.rsplit('.', 1)[1].lower() == 'pdf': + pdf_file = secure_filename(file.filename) + destination = "/".join([tempfile.mkdtemp(),pdf_file]) + file.save(destination) + file.close() + if os.path.isfile(destination): + # raw_text = get_text_from_pdfminer(destination) + raw_text = get_text_from_pdf(destination) + # docx processing + if file.filename.rsplit('.', 1)[1].lower() == 'docx': + docx_file = secure_filename(file.filename) + destination = "/".join([tempfile.mkdtemp(),docx_file]) + file.save(destination) + file.close() + if os.path.isfile(destination): + raw_text = get_text_from_docx(destination) + # txt processing + if file.filename.rsplit('.', 1)[1].lower() == 'txt': + # decode the file as UTF-8 ignoring any errors + raw_text = file.read().decode('utf-8', errors='replace') + file.close() + try: + # ! spaCy proccessing + # spaCy doc init + default sentence normalization + doc = NLP_EN(text_normalization_default(raw_text)) + # Measure the Size of doc Python Object + logging.info("%s byte", get_size(doc)) + + """ + # create the file structure + """ + # create root element + root_termsintext_element = ET.Element("termsintext") + # create element + sentences_element = ET.Element("sentences") + # create element + filepath_element = ET.Element("filepath") + # filepath_element.text = file.filename + filepath_element.text = file_name + # create element + exporterms_element = ET.Element("exporterms") + + # Helper list for one-word terms + one_word_terms_help_list = [] + # Helper list for two-word terms + two_word_terms_help_list = [] + # Helper list for three-word terms + three_word_terms_help_list = [] + # Helper list for multiple-word terms (from 4-word terms) + multiple_word_terms_help_list = [] - # sentence counter --> sentence_index + noun_chunks = [] - # default sentence normalization - sentence_clean = sentence_normalization_default(sentence.text) - # create and append - new_sent_element = ET.Element('sent') - new_sent_element.text = sentence_clean #.encode('ascii', 'ignore') errors='replace' - sentences_element.append(new_sent_element) + ''' + # ! MAIN PARSE CYCLE for sentences + ''' + for sentence_index, sentence in enumerate(doc.sents): - """ - NP shallow parsing - Noun chunks are "base noun phrases" – flat phrases that have a noun as their head. You can think of noun chunks as a noun plus the words describing the noun – for example, “the lavish green grass” or “the world’s largest tech fund”. - https://spacy.io/usage/linguistic-features/#dependency-parse - """ + # sentence counter --> sentence_index - # for processing specific sentence - doc_for_chunks = NLP_EN(sentence_clean) - # Measure the Size of doc_for_chunks Python Object - logging.info("%s byte", get_size(doc_for_chunks)) + # default sentence normalization + sentence_clean = sentence_normalization_default(sentence.text) + # create and append + new_sent_element = ET.Element('sent') + new_sent_element.text = sentence_clean #.encode('ascii', 'ignore') errors='replace' + sentences_element.append(new_sent_element) - # sentence NP shallow parsing cycle - for chunk in doc_for_chunks.noun_chunks: + """ + NP shallow parsing + Noun chunks are "base noun phrases" – flat phrases that have a noun as their head. You can think of noun chunks as a noun plus the words describing the noun – for example, “the lavish green grass” or “the world’s largest tech fund”. + https://spacy.io/usage/linguistic-features/#dependency-parse + """ - doc_for_tokens = NLP_EN(chunk.text) - # Measure the Size of doc_for_tokens Python Object - logging.info("%s byte", get_size(doc_for_tokens)) + # for processing specific sentence + doc_for_chunks = NLP_EN(sentence_clean) + # Measure the Size of doc_for_chunks Python Object + logging.info("%s byte", get_size(doc_for_chunks)) + + # ! EXTRACT ALL SINGLE NOUNs and PROPNs / ONE-WORD TERMS + for token in doc_for_chunks: + if token.pos_ in ['NOUN', 'PROPN']: + if token.lemma_ in one_word_terms_help_list: + for term in exporterms_element.findall('term'): + if term.find('tname').text == token.lemma_: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(token.i+1) + term.append(new_sentpos_element) + if token.lemma_ not in one_word_terms_help_list: + one_word_terms_help_list.append(token.lemma_) + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = '1' + # create and append + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = token.pos_ + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + new_tname_element.text = token.lemma_ + # create and append + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(token.text) + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(token.i+1) + new_term_element.append(new_sentpos_element) + # append to + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_osn_element) + new_term_element.append(new_wcount_element) + # append to + exporterms_element.append(new_term_element) + + # * sentence NP shallow parsing cycle + for chunk in doc_for_chunks.noun_chunks: + + # doc_for_tokens = NLP_EN(chunk.text) + doc_for_tokens = chunk.as_doc() + # Measure the Size of doc_for_tokens Python Object + logging.info("%s byte", get_size(doc_for_tokens)) + + ''' + # ! EXTRACT ONE-WORD TERMS ---------------------------------------------------------------------- + ''' + # if len(doc_for_tokens) == 1: + + # if doc_for_tokens[0].pos_ in ['NOUN', 'PROPN']: + + # if doc_for_tokens[0].lemma_ in one_word_terms_help_list: + # for term in exporterms_element.findall('term'): + # if term.find('tname').text == doc_for_tokens[0].lemma_: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + # term.append(new_sentpos_element) + + # if doc_for_tokens[0].lemma_ not in one_word_terms_help_list: + + # one_word_terms_help_list.append(doc_for_tokens[0].lemma_) + # # create and append + # new_wcount_element = ET.Element('wcount') + # new_wcount_element.text = '1' + # # create and append + # new_ttype_element = ET.Element('ttype') + # new_ttype_element.text = doc_for_tokens[0].pos_ + # # create + # new_term_element = ET.Element('term') + # # create and append + # new_tname_element = ET.Element('tname') + # new_tname_element.text = doc_for_tokens[0].lemma_ + # # create and append + # new_osn_element = ET.Element('osn') + # new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[0].text) + + # # create and append + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + # new_term_element.append(new_sentpos_element) + + # # append to + # new_term_element.append(new_ttype_element) + # new_term_element.append(new_tname_element) + # new_term_element.append(new_osn_element) + # new_term_element.append(new_wcount_element) + + # # append to + # exporterms_element.append(new_term_element) + + if len(doc_for_tokens) == 2: ''' - # EXTRACT ONE-WORD TERMS ---------------------------------------------------------------------- + # * Extract 1-word terms from 2-words statements (excluding articles DET) ''' - if len(doc_for_tokens) == 1: + if doc_for_tokens[0].pos_ in ['DET', 'PUNCT']: + + if doc_for_tokens[1].lemma_ in one_word_terms_help_list: + for term in exporterms_element.findall('term'): + if term.find('tname').text == doc_for_tokens[1].lemma_: + for sentpos in exporterms_element.findall('sentpos'): + if sentpos.find('sentpos').text != str(sentence_index) + '/' + str(chunk.start+2): + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + term.append(new_sentpos_element) + + if doc_for_tokens[1].lemma_ not in one_word_terms_help_list: + + one_word_terms_help_list.append(doc_for_tokens[1].lemma_) + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = '1' + # create and append + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = doc_for_tokens[1].pos_ + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + new_tname_element.text = doc_for_tokens[1].lemma_ + # create and append + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[1].text) + + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + new_term_element.append(new_sentpos_element) + + # append to + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_osn_element) + new_term_element.append(new_wcount_element) + + # append to + exporterms_element.append(new_term_element) - if doc_for_tokens[0].pos_ in ['NOUN', 'PROPN']: + ''' + # ! EXTRACT TWO-WORD TERMS --------------------------------------------------------------- + ''' + if doc_for_tokens[0].pos_ not in ['DET', 'PUNCT']: - if doc_for_tokens[0].lemma_ in one_word_terms_help_list: - for term in exporterms_element.findall('term'): - if term.find('tname').text == doc_for_tokens[0].lemma_: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) - term.append(new_sentpos_element) + # print('two-word term lemma ---> ' + chunk.lemma_ +' POS[0]:'+ doc_for_tokens[0].pos_ + ' POS[0]:'+ doc_for_tokens[0].tag_ + ' HEAD[0]:' + doc_for_tokens[0].head.lower_ +' POS[1]:' + doc_for_tokens[1].pos_ + ' POS[1]:'+ doc_for_tokens[1].tag_ + ' HEAD[1]:' + doc_for_tokens[1].head.lower_) - if doc_for_tokens[0].lemma_ not in one_word_terms_help_list: + # If two-word term already exists in two_word_terms_help_list + if chunk.lemma_ in two_word_terms_help_list: - one_word_terms_help_list.append(doc_for_tokens[0].lemma_) + # add new for existing two-word term + for term in exporterms_element.findall('term'): + if term.find('tname').text == chunk.lemma_: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + term.append(new_sentpos_element) + + # Check If root (root of Noun chunks always is a NOUN) of the two-word term + # already exists in one_word_terms_help_list + if chunk.root.lemma_ in one_word_terms_help_list: + + sent_pos_helper = [] + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == chunk.root.lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + # create and append new + # check if new already exist, if no then add new + if (str(sentence_index) + '/' + str(chunk.root.i+1)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) + one_term.append(new_sentpos_element) + # if chunk.root.lower_ == doc_for_tokens[0].lower_: + # if (str(sentence_index) + '/' + str(chunk.start+1)) not in sent_pos_helper: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + # one_term.append(new_sentpos_element) + # else: + # if (str(sentence_index) + '/' + str(chunk.start+2)) not in sent_pos_helper: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + # one_term.append(new_sentpos_element) + + # Check If child of the root (Child not always be a NOUN, so not always be a term) of the two-word term + # already exists in one_word_terms_help_list + for t in doc_for_tokens: + if t.lemma_ != chunk.root.lemma_: + # if child of the root is NOUN, so it is a term + if t.pos_ in ['NOUN', 'PROPN']: + if t.lemma_ in one_word_terms_help_list: + + sent_pos_helper = [] + + if t.i == 0: + index_helper = chunk.start+1 + else: + index_helper = chunk.start+2 + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == t.lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) + one_term.append(new_sentpos_element) + + + # If two-word term not exists in two_word_terms_help_list + if chunk.lemma_ not in two_word_terms_help_list: + + # update two_word_terms_help_list with the new two-word term + # two_word_terms_help_list.append(chunk.lower_) + two_word_terms_help_list.append(chunk.lemma_) + + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = '2' + # create and append + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = doc_for_tokens[0].pos_ + '_' + doc_for_tokens[1].pos_ + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + # new_tname_element.text = chunk.lower_ + new_tname_element.text = chunk.lemma_ + # create and append + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[0].text) + new_term_element.append(new_osn_element) + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[1].text) + new_term_element.append(new_osn_element) + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + new_term_element.append(new_sentpos_element) + # append to + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_wcount_element) + # append to + exporterms_element.append(new_term_element) + + # Check If root (root of Noun chunks always is a NOUN) of the two-word term + # already exists in one_word_terms_help_list + # add relup/reldown + if chunk.root.lemma_ in one_word_terms_help_list: + + sent_pos_helper = [] + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == chunk.root.lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(chunk.root.i+1)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) + one_term.append(new_sentpos_element) + # if chunk.root.lower_ == doc_for_tokens[0].lower_: + # if (str(sentence_index) + '/' + str(chunk.start+1)) not in sent_pos_helper: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + # one_term.append(new_sentpos_element) + # else: + # if (str(sentence_index) + '/' + str(chunk.start+2)) not in sent_pos_helper: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + # one_term.append(new_sentpos_element) + + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): + + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == chunk.lemma_: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + + # Check If root NOUN not exists in one_word_terms_help_list + # add root NOUN to one_word_terms_help_list + # add relup/reldown + if chunk.root.lemma_ not in one_word_terms_help_list: + + # print('root NOUN not exists in one_word_terms_help_list --->> ' + chunk.root.lemma_) + + one_word_terms_help_list.append(chunk.root.lemma_) # create and append new_wcount_element = ET.Element('wcount') new_wcount_element.text = '1' # create and append new_ttype_element = ET.Element('ttype') - new_ttype_element.text = doc_for_tokens[0].pos_ + new_ttype_element.text = chunk.root.pos_ # create new_term_element = ET.Element('term') # create and append new_tname_element = ET.Element('tname') - new_tname_element.text = doc_for_tokens[0].lemma_ + new_tname_element.text = chunk.root.lemma_ # create and append new_osn_element = ET.Element('osn') - new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[0].text) - + new_osn_element.text = ENGLISH_STEMMER.stem(chunk.root.lower_) # create and append new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) + # if chunk.root.lower_ == doc_for_tokens[0].lower_: + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + # else: + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) new_term_element.append(new_sentpos_element) - # append to new_term_element.append(new_ttype_element) new_term_element.append(new_tname_element) - new_term_element.append(new_osn_element) new_term_element.append(new_wcount_element) # append to exporterms_element.append(new_term_element) - if len(doc_for_tokens) == 2: + for relup_index, one_term in enumerate(exporterms_element.findall('term')): - ''' - # Extract one-word terms from 2-words statements (excluding articles DET) - ''' - if doc_for_tokens[0].pos_ in ['DET', 'PUNCT']: + if one_term.find('tname').text == chunk.root.lemma_: + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): - if doc_for_tokens[1].lemma_ in one_word_terms_help_list: - for term in exporterms_element.findall('term'): - if term.find('tname').text == doc_for_tokens[1].lemma_: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) - term.append(new_sentpos_element) + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == chunk.lemma_: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) - if doc_for_tokens[1].lemma_ not in one_word_terms_help_list: + for t in doc_for_tokens: + if t.lemma_ != chunk.root.lemma_: + if t.pos_ in ['NOUN', 'PROPN']: - one_word_terms_help_list.append(doc_for_tokens[1].lemma_) - # create and append - new_wcount_element = ET.Element('wcount') - new_wcount_element.text = '1' - # create and append - new_ttype_element = ET.Element('ttype') - new_ttype_element.text = doc_for_tokens[1].pos_ - # create - new_term_element = ET.Element('term') - # create and append - new_tname_element = ET.Element('tname') - new_tname_element.text = doc_for_tokens[1].lemma_ - # create and append - new_osn_element = ET.Element('osn') - new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[1].text) - - # create and append - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) - new_term_element.append(new_sentpos_element) + # print('-------->>>>>>' + t.lemma_) - # append to - new_term_element.append(new_ttype_element) - new_term_element.append(new_tname_element) - new_term_element.append(new_osn_element) - new_term_element.append(new_wcount_element) + if t.lemma_ in one_word_terms_help_list: - # append to - exporterms_element.append(new_term_element) + sent_pos_helper = [] - ''' - # EXTRACT TWO-WORD TERMS --------------------------------------------------------------- - ''' - if doc_for_tokens[0].pos_ not in ['DET', 'PUNCT']: + if t.i == 0: + index_helper = chunk.start+1 + else: + index_helper = chunk.start+2 - # print('two-word term lemma ---> ' + chunk.lemma_ +' POS[0]:'+ doc_for_tokens[0].pos_ + ' POS[0]:'+ doc_for_tokens[0].tag_ + ' HEAD[0]:' + doc_for_tokens[0].head.lower_ +' POS[1]:' + doc_for_tokens[1].pos_ + ' POS[1]:'+ doc_for_tokens[1].tag_ + ' HEAD[1]:' + doc_for_tokens[1].head.lower_) + for relup_index, one_term in enumerate(exporterms_element.findall('term')): - # print('--------------------') + if one_term.find('tname').text == t.lemma_: + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): - # If two-word term already exists in two_word_terms_help_list - # if chunk.lower_ in two_word_terms_help_list: - if chunk.lemma_ in two_word_terms_help_list: + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == chunk.lemma_: - # add new for existing two-word term - for term in exporterms_element.findall('term'): - # if term.find('tname').text == chunk.lower_: - if term.find('tname').text == chunk.lemma_: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) - term.append(new_sentpos_element) + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) - # Check If root (root of Noun chunks always is a NOUN) of the two-word term - # already exists in one_word_terms_help_list - if chunk.root.lemma_ in one_word_terms_help_list: - - sent_pos_helper = [] - - for relup_index, one_term in enumerate(exporterms_element.findall('term')): - - if one_term.find('tname').text == chunk.root.lemma_: - - for sent_pos in one_term.findall('sentpos'): - sent_pos_helper.append(sent_pos.text) - - # create and append new - # check if new already exist, if no then add new - if chunk.root.lower_ == doc_for_tokens[0].lower_: - if (str(sentence_index) + '/' + str(chunk.start+1)) not in sent_pos_helper: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) - one_term.append(new_sentpos_element) - else: - if (str(sentence_index) + '/' + str(chunk.start+2)) not in sent_pos_helper: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) - one_term.append(new_sentpos_element) - - # Check If child of the root (Child not always be a NOUN, so not always be a term) of the two-word term - # already exists in one_word_terms_help_list - for t in doc_for_tokens: - if t.lemma_ != chunk.root.lemma_: - # if child of the root is NOUN, so it is a term - if t.pos_ in ['NOUN']: - if t.lemma_ in one_word_terms_help_list: - - sent_pos_helper = [] - if t.i == 0: - index_helper = chunk.start+1 - else: - index_helper = chunk.start+2 - - for relup_index, one_term in enumerate(exporterms_element.findall('term')): - - if one_term.find('tname').text == t.lemma_: - - for sent_pos in one_term.findall('sentpos'): - sent_pos_helper.append(sent_pos.text) - - if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: + if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: new_sentpos_element = ET.Element('sentpos') new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) one_term.append(new_sentpos_element) + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + + if t.lemma_ not in one_word_terms_help_list: + + # print('if t.lemma_ not in one_word_terms_help_list ----->>>>>>' + t.lemma_) + + sent_pos_helper = [] + + if t.i == 0: + index_helper = chunk.start+1 + else: + index_helper = chunk.start+2 + + one_word_terms_help_list.append(t.lemma_) + + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = '1' + # create and append + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = t.pos_ + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + new_tname_element.text = t.lemma_ + # create and append + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(t.lower_) + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) + # append to + new_term_element.append(new_sentpos_element) + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_wcount_element) + + # append to + exporterms_element.append(new_term_element) + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == t.lemma_: + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): + + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == chunk.lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) + one_term.append(new_sentpos_element) - # If two-word term not exists in two_word_terms_help_list - if chunk.lemma_ not in two_word_terms_help_list: - - # update two_word_terms_help_list with the new two-word term - # two_word_terms_help_list.append(chunk.lower_) - two_word_terms_help_list.append(chunk.lemma_) + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + + ''' + # ! EXTRACT THREE-WORD TERMS + ''' + if len(doc_for_tokens) == 3: + + logging.debug('Three-words [ALL terms] lemma --> ' + chunk.lemma_ +' POS[0]: '+ doc_for_tokens[0].pos_ + ' POS[1]: ' + doc_for_tokens[1].pos_ + ' POS[2]: ' + doc_for_tokens[2].pos_) + logging.debug('--------------------') + + # * If FIRST WORD is DET then extract only TWO-WORDS term --> [the language processing] --> [language processing] + if doc_for_tokens[0].pos_ in ['DET']: + # If two-word term already exists in two_word_terms_help_list + # if chunk.lower_ in two_word_terms_help_list: + two_words_term_lemma = chunk.lemma_.split(" ", 1)[1] + logging.debug('Two-words term lemma: ' + two_words_term_lemma) + # doc_for_two_words_chunks = NLP(two_words_term_lemma) + # for two_words_chunk in doc_for_two_words_chunks.noun_chunks + if two_words_term_lemma in two_word_terms_help_list: + # add new for existing two-word term + for term in exporterms_element.findall('term'): + # if term.find('tname').text == chunk.lower_: + if term.find('tname').text == two_words_term_lemma: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + term.append(new_sentpos_element) + + # Check If root (root of Noun chunks always is a NOUN) of the two-word term + # already exists in one_word_terms_help_list + if chunk.root.lemma_ in one_word_terms_help_list: + + sent_pos_helper = [] + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == chunk.root.lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + # create and append new + # check if new already exist, if no then add new + if (str(sentence_index) + '/' + str(chunk.root.i+1)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) + one_term.append(new_sentpos_element) + # Check If child of the root (Child not always be a NOUN, so not always be a term) of the two-word term + # already exists in one_word_terms_help_list + for t in doc_for_tokens: + if ((t.lemma_ != chunk.root.lemma_) and (t.pos_ not in ['DET'])): + # if child of the root is NOUN, so it is a term + if t.pos_ in ['NOUN', 'PROPN']: + if t.lemma_ in one_word_terms_help_list: + + sent_pos_helper = [] + if t.i == 1: + index_helper = chunk.start+2 + else: + index_helper = chunk.start+3 + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == t.lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) + one_term.append(new_sentpos_element) + + # If two-word term not exists in two_word_terms_help_list + if two_words_term_lemma not in two_word_terms_help_list: + + # update two_word_terms_help_list with the new two-word term + # two_word_terms_help_list.append(chunk.lower_) + two_word_terms_help_list.append(two_words_term_lemma) + + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = '2' + # create and append + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = doc_for_tokens[1].pos_ + '_' + doc_for_tokens[2].pos_ + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + # new_tname_element.text = chunk.lower_ + new_tname_element.text = two_words_term_lemma + # create and append + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[1].text) + new_term_element.append(new_osn_element) + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[2].text) + new_term_element.append(new_osn_element) + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + new_term_element.append(new_sentpos_element) + + # append to + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_wcount_element) + + # append to + exporterms_element.append(new_term_element) + + # Check If root (root of Noun chunks always is a NOUN) of the two-word term + # already exists in one_word_terms_help_list + # add relup/reldown + if chunk.root.lemma_ in one_word_terms_help_list: + + sent_pos_helper = [] + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == chunk.root.lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(chunk.root.i+1)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) + one_term.append(new_sentpos_element) + + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == two_words_term_lemma: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + + # Check If root NOUN not exists in one_word_terms_help_list + # add root NOUN to one_word_terms_help_list + # add relup/reldown + if chunk.root.lemma_ not in one_word_terms_help_list: + + # print('root NOUN not exists in one_word_terms_help_list --->> ' + chunk.root.lemma_) + # print('--------------------') + + one_word_terms_help_list.append(chunk.root.lemma_) # create and append new_wcount_element = ET.Element('wcount') - new_wcount_element.text = '2' + new_wcount_element.text = '1' # create and append new_ttype_element = ET.Element('ttype') - new_ttype_element.text = doc_for_tokens[0].pos_ + '_' + doc_for_tokens[1].pos_ + new_ttype_element.text = chunk.root.pos_ # create new_term_element = ET.Element('term') # create and append new_tname_element = ET.Element('tname') - # new_tname_element.text = chunk.lower_ - new_tname_element.text = chunk.lemma_ + new_tname_element.text = chunk.root.lemma_ # create and append new_osn_element = ET.Element('osn') - new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[0].text) - new_term_element.append(new_osn_element) - new_osn_element = ET.Element('osn') - new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[1].text) - new_term_element.append(new_osn_element) + new_osn_element.text = ENGLISH_STEMMER.stem(chunk.root.lower_) # create and append new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) new_term_element.append(new_sentpos_element) - # append to new_term_element.append(new_ttype_element) new_term_element.append(new_tname_element) @@ -1093,273 +1520,485 @@ def generate_allterms(): # append to exporterms_element.append(new_term_element) - # Check If root (root of Noun chunks always is a NOUN) of the two-word term - # already exists in one_word_terms_help_list - # add relup/reldown - if chunk.root.lemma_ in one_word_terms_help_list: - - sent_pos_helper = [] - - for relup_index, one_term in enumerate(exporterms_element.findall('term')): - - if one_term.find('tname').text == chunk.root.lemma_: + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == chunk.root.lemma_: + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): + + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == two_words_term_lemma: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + + for t in doc_for_tokens: + if ((t.lemma_ != chunk.root.lemma_) and (t.pos_ not in ['DET'])): + if t.pos_ in ['NOUN', 'PROPN']: + # print('-------->>>>>>' + t.lemma_) + if t.lemma_ in one_word_terms_help_list: + + sent_pos_helper = [] + if t.i == 1: + index_helper = chunk.start+2 + else: + index_helper = chunk.start+3 - for sent_pos in one_term.findall('sentpos'): - sent_pos_helper.append(sent_pos.text) + for relup_index, one_term in enumerate(exporterms_element.findall('term')): - if chunk.root.lower_ == doc_for_tokens[0].lower_: - if (str(sentence_index) + '/' + str(chunk.start+1)) not in sent_pos_helper: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) - one_term.append(new_sentpos_element) - else: - if (str(sentence_index) + '/' + str(chunk.start+2)) not in sent_pos_helper: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) - one_term.append(new_sentpos_element) - - for reldown_index, two_term in enumerate(exporterms_element.findall('term')): - - # if two_term.find('tname').text == chunk.lower_: - if two_term.find('tname').text == chunk.lemma_: - new_relup_element = ET.Element('relup') - new_relup_element.text = str(relup_index) - two_term.append(new_relup_element) - new_reldown_element = ET.Element('reldown') - new_reldown_element.text = str(reldown_index) - one_term.append(new_reldown_element) - - # Check If root NOUN not exists in one_word_terms_help_list - # add root NOUN to one_word_terms_help_list - # add relup/reldown - if chunk.root.lemma_ not in one_word_terms_help_list: - - # print('root NOUN not exists in one_word_terms_help_list --->> ' + chunk.root.lemma_) - # print('--------------------') - - one_word_terms_help_list.append(chunk.root.lemma_) - - # create and append - new_wcount_element = ET.Element('wcount') - new_wcount_element.text = '1' - # create and append - new_ttype_element = ET.Element('ttype') - new_ttype_element.text = 'NOUN' - # create - new_term_element = ET.Element('term') - # create and append - new_tname_element = ET.Element('tname') - new_tname_element.text = chunk.root.lemma_ - # create and append - new_osn_element = ET.Element('osn') - new_osn_element.text = ENGLISH_STEMMER.stem(chunk.root.lower_) - # create and append - new_sentpos_element = ET.Element('sentpos') - if chunk.root.lower_ == doc_for_tokens[0].lower_: - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) - else: - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) - new_term_element.append(new_sentpos_element) - # append to - new_term_element.append(new_ttype_element) - new_term_element.append(new_tname_element) - new_term_element.append(new_wcount_element) - - # append to - exporterms_element.append(new_term_element) - - for relup_index, one_term in enumerate(exporterms_element.findall('term')): - - if one_term.find('tname').text == chunk.root.lemma_: - for reldown_index, two_term in enumerate(exporterms_element.findall('term')): - - # if two_term.find('tname').text == chunk.lower_: - if two_term.find('tname').text == chunk.lemma_: - new_relup_element = ET.Element('relup') - new_relup_element.text = str(relup_index) - two_term.append(new_relup_element) - new_reldown_element = ET.Element('reldown') - new_reldown_element.text = str(reldown_index) - one_term.append(new_reldown_element) - - for t in doc_for_tokens: - if t.lemma_ != chunk.root.lemma_: - if t.pos_ in ['NOUN']: - - # print('-------->>>>>>' + t.lemma_) - - if t.lemma_ in one_word_terms_help_list: - - sent_pos_helper = [] - if t.i == 0: - index_helper = chunk.start+1 - else: - index_helper = chunk.start+2 - - - for relup_index, one_term in enumerate(exporterms_element.findall('term')): - - if one_term.find('tname').text == t.lemma_: - for reldown_index, two_term in enumerate(exporterms_element.findall('term')): - - # if two_term.find('tname').text == chunk.lower_: - if two_term.find('tname').text == chunk.lemma_: - - for sent_pos in one_term.findall('sentpos'): - sent_pos_helper.append(sent_pos.text) - - if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) - one_term.append(new_sentpos_element) - - new_relup_element = ET.Element('relup') - new_relup_element.text = str(relup_index) - two_term.append(new_relup_element) - new_reldown_element = ET.Element('reldown') - new_reldown_element.text = str(reldown_index) - one_term.append(new_reldown_element) - - if t.lemma_ not in one_word_terms_help_list: - - # print('if t.lemma_ not in one_word_terms_help_list ----->>>>>>' + t.lemma_) - - sent_pos_helper = [] - - if t.i == 0: - index_helper = chunk.start+1 - else: - index_helper = chunk.start+2 - - one_word_terms_help_list.append(t.lemma_) - - # create and append - new_wcount_element = ET.Element('wcount') - new_wcount_element.text = '1' - # create and append - new_ttype_element = ET.Element('ttype') - new_ttype_element.text = 'NOUN' - # create - new_term_element = ET.Element('term') - # create and append - new_tname_element = ET.Element('tname') - new_tname_element.text = t.lemma_ - # create and append - new_osn_element = ET.Element('osn') - new_osn_element.text = ENGLISH_STEMMER.stem(t.lower_) - # create and append - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) - # append to - new_term_element.append(new_sentpos_element) - new_term_element.append(new_ttype_element) - new_term_element.append(new_tname_element) - new_term_element.append(new_wcount_element) - - # append to - exporterms_element.append(new_term_element) - - for relup_index, one_term in enumerate(exporterms_element.findall('term')): - - if one_term.find('tname').text == t.lemma_: - for reldown_index, two_term in enumerate(exporterms_element.findall('term')): + if one_term.find('tname').text == t.lemma_: + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): - # if two_term.find('tname').text == chunk.lower_: - if two_term.find('tname').text == chunk.lemma_: - - for sent_pos in one_term.findall('sentpos'): - sent_pos_helper.append(sent_pos.text) - - if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) - one_term.append(new_sentpos_element) - - new_relup_element = ET.Element('relup') - new_relup_element.text = str(relup_index) - two_term.append(new_relup_element) - new_reldown_element = ET.Element('reldown') - new_reldown_element.text = str(reldown_index) - one_term.append(new_reldown_element) + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == two_words_term_lemma: - ''' - # EXTRACT THREE-WORD TERMS - ''' - if len(doc_for_tokens) == 3: + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) - logging.debug('three-word term lemma ---> ' + chunk.lemma_ +' POS[0]:'+ doc_for_tokens[0].pos_ + ' POS[1]:' + doc_for_tokens[1].pos_ + ' POS[2]:' + doc_for_tokens[2].pos_) - logging.debug('--------------------') + if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) + one_term.append(new_sentpos_element) - if len(doc_for_tokens) > 3: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) - logging.debug('multi-word term lemma ---> ' + chunk.lemma_) - logging.debug('--------------------') + if t.lemma_ not in one_word_terms_help_list: - if doc_for_tokens[0].pos_ not in ['DET', 'PUNCT']: + # print('if t.lemma_ not in one_word_terms_help_list ----->>>>>>' + t.lemma_) - # If multiple-word term already exists in multiple_word_terms_help_list - if chunk.lemma_ in multiple_word_terms_help_list: + sent_pos_helper = [] + if t.i == 1: + index_helper = chunk.start+2 + else: + index_helper = chunk.start+3 + + one_word_terms_help_list.append(t.lemma_) + + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = '1' + # create and append + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = t.pos_ + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + new_tname_element.text = t.lemma_ + # create and append + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(t.lower_) + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) + # append to + new_term_element.append(new_sentpos_element) + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_wcount_element) + + # append to + exporterms_element.append(new_term_element) + + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == t.lemma_: + for reldown_index, two_term in enumerate(exporterms_element.findall('term')): + + # if two_term.find('tname').text == chunk.lower_: + if two_term.find('tname').text == two_words_term_lemma: - # add new for existing two-word term - for term in exporterms_element.findall('term'): - if term.find('tname').text == chunk.lemma_: - new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) - term.append(new_sentpos_element) - - # If multiple-word term not exists in multiple_word_terms_help_list - if chunk.lemma_ not in multiple_word_terms_help_list: - # update multiple_word_terms_help_list with the new multiple-word term - multiple_word_terms_help_list.append(chunk.lemma_) + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + if (str(sentence_index) + '/' + str(index_helper)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(index_helper) + one_term.append(new_sentpos_element) + + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + two_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + # ! EXTRACT FULL THREE-WORDS terms ---------------------------------------------------------------- + if doc_for_tokens[0].pos_ not in ['DET', 'PUNCT']: + logging.debug('Three-words [FULL terms] lemma --> ' + chunk.lemma_ +' POS[0]: '+ doc_for_tokens[0].pos_ + ' POS[1]: ' + doc_for_tokens[1].pos_ + ' POS[2]: ' + doc_for_tokens[2].pos_) + logging.debug('--------------------') + # * Check If three-word term already exists in three_word_terms_help_list + if chunk.lemma_ in three_word_terms_help_list: + # add new for existing two-word term + for term in exporterms_element.findall('term'): + if term.find('tname').text == chunk.lemma_: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + term.append(new_sentpos_element) + # Check If root (root of Noun chunks always is a NOUN) of the three-word term already exists in one_word_terms_help_list + # if chunk.root.lemma_ in one_word_terms_help_list: + # sent_pos_helper = [] + # for relup_index, one_term in enumerate(exporterms_element.findall('term')): + # if one_term.find('tname').text == chunk.root.lemma_: + # for sent_pos in one_term.findall('sentpos'): + # sent_pos_helper.append(sent_pos.text) + # # create and append new + # # check if new already exist, if no then add new + # if (str(sentence_index) + '/' + str(chunk.root.i+1)) not in sent_pos_helper: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) + # one_term.append(new_sentpos_element) + # * Check if FIRST word (NOUN or PRONOUN) of the three-word term already exists in one_word_terms_help_list + if ((doc_for_tokens[0].pos_ in ['NOUN', 'PROPN']) and (doc_for_tokens[0].lemma_ in one_word_terms_help_list)): + sent_pos_helper = [] + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + if one_term.find('tname').text == doc_for_tokens[0].lemma_: + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + # create and append new + # check if new already exist, if no then add new + if (str(sentence_index) + '/' + str(chunk.start+1)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + one_term.append(new_sentpos_element) + # * Check if SECOND word (NOUN or PRONOUN) of the three-word term already exists in one_word_terms_help_list + if ((doc_for_tokens[1].pos_ in ['NOUN', 'PROPN']) and (doc_for_tokens[1].lemma_ in one_word_terms_help_list)): + sent_pos_helper = [] + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + if one_term.find('tname').text == doc_for_tokens[1].lemma_: + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + # create and append new + # check if new already exist, if no then add new + if (str(sentence_index) + '/' + str(chunk.start+2)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + one_term.append(new_sentpos_element) + # * Check if THIRD word (NOUN or PRONOUN) of the three-word term already exists in one_word_terms_help_list + if ((doc_for_tokens[2].pos_ in ['NOUN', 'PROPN']) and (doc_for_tokens[2].lemma_ in one_word_terms_help_list)): + sent_pos_helper = [] + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + if one_term.find('tname').text == doc_for_tokens[2].lemma_: + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + # create and append new + # check if new already exist, if no then add new + if (str(sentence_index) + '/' + str(chunk.start+3)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+3) + one_term.append(new_sentpos_element) + # * Check if three-word term not exists in three_word_terms_help_list + if chunk.lemma_ not in three_word_terms_help_list: + + # update three_word_terms_help_list with the new three-word term + three_word_terms_help_list.append(chunk.lemma_) + + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = '3' + # create and append + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = doc_for_tokens[0].pos_ + '_' + doc_for_tokens[1].pos_ + '_' + doc_for_tokens[2].pos_ + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + new_tname_element.text = chunk.lemma_ + # create and append + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[0].text) + new_term_element.append(new_osn_element) + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[1].text) + new_term_element.append(new_osn_element) + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[2].text) + new_term_element.append(new_osn_element) + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + new_term_element.append(new_sentpos_element) + # append to + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_wcount_element) + # append to + exporterms_element.append(new_term_element) + # * Check if FIRST word (NOUN or PROPN) of the three-word term already exists in one_word_terms_help_list + # * add relup/reldown + # Check if ROOT word (NOUN or PROPN) of the three-word term already exists in one_word_terms_help_list + # add relup/reldown + # if chunk.root.lemma_ in one_word_terms_help_list: + # sent_pos_helper = [] + # for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + # if one_term.find('tname').text == chunk.root.lemma_: + + # for sent_pos in one_term.findall('sentpos'): + # sent_pos_helper.append(sent_pos.text) + + # if (str(sentence_index) + '/' + str(chunk.root.i+1)) not in sent_pos_helper: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) + # one_term.append(new_sentpos_element) + + # for reldown_index, three_term in enumerate(exporterms_element.findall('term')): + # if three_term.find('tname').text == chunk.lemma_: + # new_relup_element = ET.Element('relup') + # new_relup_element.text = str(relup_index) + # three_term.append(new_relup_element) + # new_reldown_element = ET.Element('reldown') + # new_reldown_element.text = str(reldown_index) + # one_term.append(new_reldown_element) + if ((doc_for_tokens[0].pos_ in ['NOUN', 'PROPN']) and (doc_for_tokens[0].lemma_ in one_word_terms_help_list)): + sent_pos_helper = [] + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == doc_for_tokens[0].lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(chunk.start+1)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + one_term.append(new_sentpos_element) + + for reldown_index, three_term in enumerate(exporterms_element.findall('term')): + if three_term.find('tname').text == chunk.lemma_: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + three_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + # * Check if SECOND word (NOUN or PROPN) of the three-word term already exists in one_word_terms_help_list + # * add relup/reldown + if ((doc_for_tokens[1].pos_ in ['NOUN', 'PROPN']) and (doc_for_tokens[1].lemma_ in one_word_terms_help_list)): + sent_pos_helper = [] + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == doc_for_tokens[1].lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(chunk.start+2)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+2) + one_term.append(new_sentpos_element) + + for reldown_index, three_term in enumerate(exporterms_element.findall('term')): + if three_term.find('tname').text == chunk.lemma_: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + three_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + # * Check if THIRD word (NOUN or PROPN) of the three-word term already exists in one_word_terms_help_list + # * add relup/reldown + if ((doc_for_tokens[2].pos_ in ['NOUN', 'PROPN']) and (doc_for_tokens[2].lemma_ in one_word_terms_help_list)): + sent_pos_helper = [] + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + + if one_term.find('tname').text == doc_for_tokens[2].lemma_: + + for sent_pos in one_term.findall('sentpos'): + sent_pos_helper.append(sent_pos.text) + + if (str(sentence_index) + '/' + str(chunk.start+3)) not in sent_pos_helper: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+3) + one_term.append(new_sentpos_element) + + for reldown_index, three_term in enumerate(exporterms_element.findall('term')): + if three_term.find('tname').text == chunk.lemma_: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + three_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + # * Check If root NOUN not exists in one_word_terms_help_list, add root NOUN to one_word_terms_help_list + # add relup/reldown + if chunk.root.lemma_ not in one_word_terms_help_list: + one_word_terms_help_list.append(chunk.root.lemma_) # create and append new_wcount_element = ET.Element('wcount') - new_wcount_element.text = str(len(chunk)) + new_wcount_element.text = '1' # create and append - multiple_pos_helper = [] - for multiple_pos in doc_for_tokens: - multiple_pos_helper.append(multiple_pos.pos_) new_ttype_element = ET.Element('ttype') - new_ttype_element.text = '_'.join(multiple_pos_helper) + new_ttype_element.text = chunk.root.pos_ # create new_term_element = ET.Element('term') # create and append new_tname_element = ET.Element('tname') - # new_tname_element.text = chunk.lower_ - new_tname_element.text = chunk.lemma_ + new_tname_element.text = chunk.root.lemma_ # create and append - multiple_osn_helper = [] - for multiple_osn in doc_for_tokens: - new_osn_element = ET.Element('osn') - new_osn_element.text = ENGLISH_STEMMER.stem(multiple_osn.text) - new_term_element.append(new_osn_element) + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(chunk.root.lower_) # create and append new_sentpos_element = ET.Element('sentpos') - new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.root.i+1) new_term_element.append(new_sentpos_element) - # append to new_term_element.append(new_ttype_element) new_term_element.append(new_tname_element) new_term_element.append(new_wcount_element) - # append to exporterms_element.append(new_term_element) - # create full file structure - root_termsintext_element.append(filepath_element) - root_termsintext_element.append(exporterms_element) - root_termsintext_element.append(sentences_element) + for relup_index, one_term in enumerate(exporterms_element.findall('term')): + if one_term.find('tname').text == chunk.root.lemma_: + for reldown_index, three_term in enumerate(exporterms_element.findall('term')): + # if two_term.find('tname').text == chunk.lower_: + if three_term.find('tname').text == chunk.lemma_: + new_relup_element = ET.Element('relup') + new_relup_element.text = str(relup_index) + three_term.append(new_relup_element) + new_reldown_element = ET.Element('reldown') + new_reldown_element.text = str(reldown_index) + one_term.append(new_reldown_element) + # TODO Extract two-words terms from the first part of three-words terms --> [natural language processing] --> [natural language] + # * ------------------------------------------------------------------------------------------------------------------------ + # * Extract two-words terms from the first part of three-words terms --> [natural language processing] --> [natural language] + if doc_for_tokens[0].pos_ and doc_for_tokens[1].pos_ not in ['ADJ', 'PUNCT', 'DET']: + first_two_words_term_lemma = " ".join(chunk.lemma_.split(" ", 2)[:2]) + logging.debug('1 two-words term lemma: ' + first_two_words_term_lemma) + # if first_two_words_term_lemma in two_word_terms_help_list: + # sent_pos_helper = [] + # # add new for existing two-word term + # for term in exporterms_element.findall('term'): + # if term.find('tname').text == first_two_words_term_lemma: + # for sent_pos in term.findall('sentpos'): + # sent_pos_helper.append(sent_pos.text) + # if (str(sentence_index) + '/' + str(chunk.start+1)) not in sent_pos_helper: + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + # term.append(new_sentpos_element) + # if first_two_words_term_lemma not in two_word_terms_help_list: + # # update two_word_terms_help_list with the new two-word term + # # two_word_terms_help_list.append(chunk.lower_) + # two_word_terms_help_list.append(first_two_words_term_lemma) + + # # create and append + # new_wcount_element = ET.Element('wcount') + # new_wcount_element.text = '2' + # # create and append + # new_ttype_element = ET.Element('ttype') + # new_ttype_element.text = doc_for_tokens[0].pos_ + '_' + doc_for_tokens[1].pos_ + # # create + # new_term_element = ET.Element('term') + # # create and append + # new_tname_element = ET.Element('tname') + # # new_tname_element.text = chunk.lower_ + # new_tname_element.text = first_two_words_term_lemma + # # create and append + # new_osn_element = ET.Element('osn') + # new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[0].text) + # new_term_element.append(new_osn_element) + # new_osn_element = ET.Element('osn') + # new_osn_element.text = ENGLISH_STEMMER.stem(doc_for_tokens[1].text) + # new_term_element.append(new_osn_element) + # # create and append + # new_sentpos_element = ET.Element('sentpos') + # new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + # new_term_element.append(new_sentpos_element) + + # # append to + # new_term_element.append(new_ttype_element) + # new_term_element.append(new_tname_element) + # new_term_element.append(new_wcount_element) + + # # append to + # exporterms_element.append(new_term_element) + # * Extract two-words terms from the first part of three-words terms --> [natural language processing] --> [language processing] + if doc_for_tokens[1].pos_ and doc_for_tokens[2].pos_ not in ['ADJ', 'PUNCT', 'DET']: + second_two_words_term_lemma = chunk.lemma_.split(" ", 1)[1] + logging.debug('2 two-words term lemma: ' + second_two_words_term_lemma) + # * ------------------------------------------------------------------------------------------------------------------------ + ''' + # ! EXTRACT FOUR-WORD TERMS ----------------------------------------------------------------------------- + ''' + if len(doc_for_tokens) > 3: + + logging.debug('Multi-word term lemma --> ' + chunk.lemma_) + logging.debug(['< ' + token.lemma_ +' > '+ 'POS: ' + token.pos_ for token in doc_for_tokens]) + logging.debug('--------------------') + + # if doc_for_tokens[0].pos_ not in ['DET','PUNCT']: + if doc_for_tokens[0].pos_ not in ['PUNCT']: + + # If multiple-word term already exists in multiple_word_terms_help_list + if chunk.lemma_ in multiple_word_terms_help_list: + + # add new for existing two-word term + for term in exporterms_element.findall('term'): + if term.find('tname').text == chunk.lemma_: + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + term.append(new_sentpos_element) + + # If multiple-word term not exists in multiple_word_terms_help_list + if chunk.lemma_ not in multiple_word_terms_help_list: + # update multiple_word_terms_help_list with the new multiple-word term + multiple_word_terms_help_list.append(chunk.lemma_) + + # create and append + new_wcount_element = ET.Element('wcount') + new_wcount_element.text = str(len(chunk)) + # create and append + multiple_pos_helper = [] + for multiple_pos in doc_for_tokens: + multiple_pos_helper.append(multiple_pos.pos_) + new_ttype_element = ET.Element('ttype') + new_ttype_element.text = '_'.join(multiple_pos_helper) + # create + new_term_element = ET.Element('term') + # create and append + new_tname_element = ET.Element('tname') + # new_tname_element.text = chunk.lower_ + new_tname_element.text = chunk.lemma_ + # create and append + multiple_osn_helper = [] + for multiple_osn in doc_for_tokens: + new_osn_element = ET.Element('osn') + new_osn_element.text = ENGLISH_STEMMER.stem(multiple_osn.text) + new_term_element.append(new_osn_element) + # create and append + new_sentpos_element = ET.Element('sentpos') + new_sentpos_element.text = str(sentence_index) + '/' + str(chunk.start+1) + new_term_element.append(new_sentpos_element) - return ET.tostring(root_termsintext_element, encoding='utf8', method='xml') - except Exception as e: - logging.error(e, exc_info=True) - return abort(500) + # append to + new_term_element.append(new_ttype_element) + new_term_element.append(new_tname_element) + new_term_element.append(new_wcount_element) + + # append to + exporterms_element.append(new_term_element) + + # create full file structure + root_termsintext_element.append(filepath_element) + root_termsintext_element.append(exporterms_element) + root_termsintext_element.append(sentences_element) + + return ET.tostring(root_termsintext_element, encoding='utf8', method='xml') + except Exception as e: + logging.error(e, exc_info=True) + return abort(500) file.close() return abort(400) @app.route('/ken/api/en/file/json/allterms', methods=['POST']) -def get_allterms_json(): +def get_allterms_json_en(): # check if the post request has the file part if 'file' not in request.files: flash('No file part') @@ -1427,16 +2066,19 @@ def get_allterms_json(): # 1 term [{'POS': {'IN':['NOUN', 'PROPN']}}], # 2 terms - [{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}], + [{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN','PROPN']}}], + [{'POS': {'IN':['NOUN', 'PROPN']}}, {'POS': {'IN':['NOUN','ADJ','PROPN']}}], # 3 terms [{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}], - # 4 terms - [{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}},{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}], - # 3 terms with APD in the middle - [{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN', 'ADP']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}] + [{'POS': {'IN':['NOUN','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}], + # # 4 terms + # [{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}},{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}], + # # 3 terms with APD in the middle + # [{'POS': {'IN':['NOUN', 'ADJ','PROPN']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN', 'ADP']}}, {'POS': {'IN':['NOUN', 'ADJ','PROPN']}}] ] matcher = Matcher(NLP_EN.vocab) - matcher.add("NOUN/PROPN", None, patterns[0]) + # matcher.add("NOUN/PROPN", [patterns[0]]) + matcher.add("NOUN/PROPN", patterns) # matcher.add("NOUN/ADJ/PROPN+NOUN/ADJ/PROPN", None, patterns[1]) # matcher.add("NOUN/ADJ/PROPN+NOUN/ADJ/PROPN+NOUN/ADJ/PROPN", None, patterns[2]) # matcher.add("NOUN/ADJ/PROPN+NOUN/ADJ/PROPN+NOUN/ADJ/PROPN+NOUN/ADJ/PROPN", None, patterns[3]) @@ -1490,6 +2132,17 @@ def get_allterms_json(): if str(sentence_index) + '/' + str(span.start+1) not in terms_element[span.lemma_]['sentpos']: terms_element[span.lemma_]['sentpos'].append(str(sentence_index) + '/' + str(span.start+1)) + if len(span) == 2: + logging.debug('MATCHER | Matched span: ' + span.text + ' | Span lenght: ' + str(len(span))) + doc_for_span = NLP_EN(span.text) + if doc_for_span[0].lemma_ == doc_for_span[0].head.lemma_: + logging.debug('ROOT: ' + doc_for_span[0].lemma_ + ' POS: ' + doc_for_span[0].pos_ + ' | ' + 'Child: ' + doc_for_span[1].lemma_ + ' POS: ' + doc_for_span[1].pos_) + else: + logging.debug('ROOT: ' + doc_for_span[1].lemma_ + ' POS: ' + doc_for_span[1].pos_ + ' | ' + 'Child: ' + doc_for_span[0].lemma_ + ' POS: ' + doc_for_span[0].pos_) + # logging.debug([token.lemma_ for token in doc_for_span]) + # logging.debug('MATCHER | Matched span: ' + span.text + ' | Span lenght: ' + str(len(span)) + ' | Span root POS/Lemma: ' + span.root.pos_ + '/' + span.root.lemma_) + # logging.debug([child for child in span.root.children]) + # NP shallow # sentence NP shallow parsing cycle for chunk in doc_sentence.noun_chunks: @@ -1528,6 +2181,62 @@ def get_allterms_json(): # ------------------------------------------------------------------------------------------------------ # """ +""" @app.route('/ken/api/en/file/trf', methods=['POST']) +def get_trf(): + # check if the post request has the file part + if 'file' not in request.files: + flash('No file part') + return abort(400) + + file = request.files['file'] + + # if user does not select file, browser also submit an empty part without filename + if file.filename == '': + flash('No selected file') + return abort(400) + + if file and allowed_file(file.filename): + # pdf processing + if file.filename.rsplit('.', 1)[1].lower() == 'pdf': + pdf_file = secure_filename(file.filename) + destination = "/".join([tempfile.mkdtemp(),pdf_file]) + file.save(destination) + file.close() + if os.path.isfile(destination): + # raw_text = get_text_from_pdfminer(destination) + raw_text = get_text_from_pdf(destination) + # docx processing + if file.filename.rsplit('.', 1)[1].lower() == 'docx': + docx_file = secure_filename(file.filename) + destination = "/".join([tempfile.mkdtemp(),docx_file]) + file.save(destination) + file.close() + if os.path.isfile(destination): + raw_text = get_text_from_docx(destination) + # txt processing + if file.filename.rsplit('.', 1)[1].lower() == 'txt': + # decode the file as UTF-8 ignoring any errors + raw_text = file.read().decode('utf-8', errors='replace') + file.close() + try: + sentences = [] + text_normalized = text_normalization_default(raw_text) + # default sentence normalization + spaCy doc init + doc = NLP_EN_TRF(text_normalized) + # Measure the Size of doc Python Object + logging.info("%s byte", get_size(doc)) + for sentence in doc.sents: + # default sentence normalization + sentence_clean = sentence_normalization_default(sentence.text) + logging.debug(sentence_clean) + sentences.append(sentence_clean) + return Response(jsonify(sentences), mimetype='application/json') + except Exception as e: + logging.error(e, exc_info=True) + return abort(500) + file.close() + return abort(400) """ + if __name__ == '__main__': # default port = 5000 app.run(host = '0.0.0.0') diff --git a/static/javascripts/recap-en.js b/static/javascripts/recap-en.js index 609e2b1..e574883 100644 --- a/static/javascripts/recap-en.js +++ b/static/javascripts/recap-en.js @@ -723,7 +723,8 @@ function fetchFileToRecapService() { if (self.fetch) { - fetch('/ken/api/en/file/allterms', { + // fetch('/ken/api/en/file/allterms', { + fetch('/ken/api/en/allterms', { method: 'post', body: form }) diff --git a/static/stylesheets/style-en.css b/static/stylesheets/style-en.css index e952c3f..9187265 100644 --- a/static/stylesheets/style-en.css +++ b/static/stylesheets/style-en.css @@ -413,13 +413,13 @@ select { /*LOADER_________________________________________________________________________________________________________*/ /*displacy_________________________________________________________________________________________________________*/ -#displacy { +/* #displacy { overflow: auto; } #displacy-ner { overflow: auto; -} +} */ #displacy-label { position: sticky; diff --git a/templates/changelog.html b/templates/changelog.html index 48dd10e..4b0beac 100644 --- a/templates/changelog.html +++ b/templates/changelog.html @@ -823,27 +823,33 @@ -

v3.1.0, 2021-03-02

👍 Покращення

  • оновлено бібліотеку spaCy до версії 2.3.5;
  • оновлено модель бібліотеки spaCy en_core_web_sm до версії 2.3.1;
  • elementTree API оновлено до версії python 3.7.9;

🔴 Виправлення помилок

  • виправлено xml declaration (elementTree API оновлено до версії python 3.7.9);
  • дрібні виправлення.

v3.0.3, 2020-10-16

🔴 Виправлення помилок

  • Дрібні виправлення.

v3.0.2, 2020-10-16

👍 Покращення

  • Оновлено платформу Python до версії Python 3.7.9.

🔴 Виправлення помилок

  • Дрібні виправлення.

v3.0.1, 2020-10-16

🏭 Нові можливості

  • UKR🇺🇦 Додано функцию заповнення контекстів в онтологічному шаблоні (вхідні структури: allterms.xml та structure.xml).

🔴 Виправлення помилок

  • Дрібні виправлення.

v3.0.0-beta-6, 2020-08-21

📚 Документація

  • UKR🇺🇦 Створено окрему документацію по API та використанню API-ендпоентів - HELP.md.

🔴 Виправлення помилок

  • Дрібні виправлення.

v3.0.0-beta-5, 2020-08-20

🏭 Нові можливості

  • UKR🇺🇦 Додано API для обробки/аналізу текстів у вигляді повідомлень. Приклад вхідних даних:

    Кінцева точка: http://IP:PORT/kua/api/task/message/queued

v3.0.0-beta-4, 2020-05-19

🏭 Нові можливості

  • UKR🇺🇦 ENG🇬🇧 Додано можливість завантаження таблиці з зовнішнього XLSX документу.

v3.0.0-beta-3, 2020-05-14

🔴 Виправлення помилок

  • UKR🇺🇦 ENG🇬🇧 Виправлено помилку при збереженні таблиці в формат EXCEL (встановлено формат файлу xlsx замість xls) з використанням FileSaver.js та xlsx.full.min.js.

v3.0.0-beta-2, 2020-05-14

🔴 Виправлення помилок

  • UKR🇺🇦 ENG🇬🇧 Виправлено помилку при збереженні таблиці в формат EXCEL (встановлено розширення файлу xls замість xlsx).

v3.0.0-beta-1, 2020-05-07

🏭 Нові можливості

  • UKR🇺🇦 ENG🇬🇧 Здійснено перехід проекту на актуальну версію Python 3 та відповідне оновлення початкового коду.

👍 Покращення

  • ENG🇬🇧 Здійснено перехід на актуальну версію бібліотеки spaCy та її моделей.

v2.0.0-beta-73, 2020-04-16

⚠️ Зауваження

  • UKR🇺🇦 ENG🇬🇧 Встановлені постійні залежності в requirements.txt (для підтримки Python 2.7.17).

v2.0.0-beta-72, 2020-04-16

🔴 Виправлення помилок

  • ENG🇬🇧 Виправлено помилку при збереженні локально файлів allterms.xml, parce.xml.

👍 Покращення

  • UKR🇺🇦 Прискорено аналіз документів розміром від 50000 до 100000 байт.

v2.0.0-beta-71, 2020-03-08

🔴 Виправлення помилок

  • UKR🇺🇦 Виправлено помилку інтерактивного підсвічування термінів (при виборі терміна в елементі #term-tree).

v2.0.0-beta-70, 2020-02-27

🔴 Виправлення помилок

  • UKR🇺🇦 Протестовано та виправлено роботу бібліотеки Konspekt.exe (для української мови версії від 03-11-2018) з файлами великого розміру.
  • UKR🇺🇦 Вимкнено режим налагодження wine (додано змінну середовища export WINEDEBUG=-all).
  • UKR🇺🇦 Виправлено помилку інтерактивного підсвічування термінів (якщо частота появи терміна в тексті перевищує 300 разів, в цьому випадку буде відключене інтерактивне підсвічування терміна задля коректного роботи інтерфейсу веб-застосунку).

v2.0.0-beta-67, 2020-02-24

👍 Покращення

  • UKR🇺🇦 ENG🇬🇧 На головну веб-сторінку додатка додано номер бета-версії.

v2.0.0-beta-66, 2020-02-22

🔴 Виправлення помилок

  • UKR🇺🇦 Виправлено список змін.

v2.0.0-beta-65, 2020-02-22

⚠️ Зауваження

  • UKR🇺🇦 Додано обмеження на розмір оброблюваного файлу, яке становить 2 мб.
  • UKR🇺🇦 Протестовано роботу бібліотеки Konspekt.exe (для української мови версії від 03-11-2018) з файлами великого розміру.
  • UKR🇺🇦 Змінено специфікацію JSON-файлу проекту (Konspekt.exe для української мови версії від 03-11-2018). +

    v3.2.0, 2021-04-16

    ⚠️ Зауваження

    • ENG🇬🇧 оновлено бібліотеку spaCy до версії 3.0.5;
    • ENG🇬🇧 оновлено модель бібліотеки spaCy для англійської мови en_core_web_sm до версії 3.0.0;
    • оновлено бібліотеку pdfminer.six до версії 20201018;
    • оновлено бібліотеку flask-cors до версії 3.0.10;
    • ENG🇬🇧 додано нові документи для тестування сервісів для Англійської мови;
    • ENG🇬🇧 Змінено URL кінцевої точки API для отримання XML-структуру allterms.xml: +було: +host[:port]/ken/api/en/file/allterm +стало: +host[:port]/ken/api/en/allterms +Відтепер для обробки/аналізу текстів у вигляді повідомлень та файлів діє одна й та сама кінцева точка API: host[:port]/ken/api/en/allterms.

    🏭 Нові можливості

    • ENG🇬🇧 Додано API для обробки/аналізу текстів (а саме, отримання allterms.xml) у вигляді повідомлень. Приклад вхідних даних:

      HTTP method: POST +Кінцева точка: host[:port]/ken/api/en/allterms

    👍 Покращення

    • ENG🇬🇧 покращено обробку складних термінів, зокрема, з трьох слів;
    • UKR🇺🇦 ENG🇬🇧 Оновлено опис API для обробки/аналізу текстів у вигляді повідомлень та файлів для Англійської та Української мов у файлі HELP.md.

    🔴 Виправлення помилок

    • UKR🇺🇦 ENG🇬🇧 виправлені помилки відображення/візуалізації залежностей для термінів в елементі #depparse_tab, а саме в #displacy;
    • UKR🇺🇦 ENG🇬🇧 дрібні виправлення.

    v3.1.0, 2021-03-02

    👍 Покращення

    • оновлено бібліотеку spaCy до версії 2.3.5;
    • оновлено модель бібліотеки spaCy en_core_web_sm до версії 2.3.1;
    • elementTree API оновлено до версії python 3.7.9;

    🔴 Виправлення помилок

    • виправлено xml declaration (elementTree API оновлено до версії python 3.7.9);
    • дрібні виправлення.

    v3.0.3, 2020-10-16

    🔴 Виправлення помилок

    • Дрібні виправлення.

    v3.0.2, 2020-10-16

    👍 Покращення

    • Оновлено платформу Python до версії Python 3.7.9.

    🔴 Виправлення помилок

    • Дрібні виправлення.

    v3.0.1, 2020-10-16

    🏭 Нові можливості

    • UKR🇺🇦 Додано функцию заповнення контекстів в онтологічному шаблоні (вхідні структури: allterms.xml та structure.xml).

    🔴 Виправлення помилок

    • Дрібні виправлення.

    v3.0.0-beta-6, 2020-08-21

    📚 Документація

    • UKR🇺🇦 Створено окрему документацію по API та використанню API-ендпоентів - HELP.md.

    🔴 Виправлення помилок

    • Дрібні виправлення.

    v3.0.0-beta-5, 2020-08-20

    🏭 Нові можливості

    • UKR🇺🇦 Додано API для обробки/аналізу текстів у вигляді повідомлень. Приклад вхідних даних:

      Кінцева точка: http://IP:PORT/kua/api/task/message/queued

    v3.0.0-beta-4, 2020-05-19

    🏭 Нові можливості

    • UKR🇺🇦 ENG🇬🇧 Додано можливість завантаження таблиці з зовнішнього XLSX документу.

    v3.0.0-beta-3, 2020-05-14

    🔴 Виправлення помилок

    • UKR🇺🇦 ENG🇬🇧 Виправлено помилку при збереженні таблиці в формат EXCEL (встановлено формат файлу xlsx замість xls) з використанням FileSaver.js та xlsx.full.min.js.

    v3.0.0-beta-2, 2020-05-14

    🔴 Виправлення помилок

    • UKR🇺🇦 ENG🇬🇧 Виправлено помилку при збереженні таблиці в формат EXCEL (встановлено розширення файлу xls замість xlsx).

    v3.0.0-beta-1, 2020-05-07

    🏭 Нові можливості

    • UKR🇺🇦 ENG🇬🇧 Здійснено перехід проекту на актуальну версію Python 3 та відповідне оновлення початкового коду.

    👍 Покращення

    • ENG🇬🇧 Здійснено перехід на актуальну версію бібліотеки spaCy та її моделей.

    v2.0.0-beta-73, 2020-04-16

    ⚠️ Зауваження

    • UKR🇺🇦 ENG🇬🇧 Встановлені постійні залежності в requirements.txt (для підтримки Python 2.7.17).

    v2.0.0-beta-72, 2020-04-16

    🔴 Виправлення помилок

    • ENG🇬🇧 Виправлено помилку при збереженні локально файлів allterms.xml, parce.xml.

    👍 Покращення

    • UKR🇺🇦 Прискорено аналіз документів розміром від 50000 до 100000 байт.

    v2.0.0-beta-71, 2020-03-08

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено помилку інтерактивного підсвічування термінів (при виборі терміна в елементі #term-tree).

    v2.0.0-beta-70, 2020-02-27

    🔴 Виправлення помилок

    • UKR🇺🇦 Протестовано та виправлено роботу бібліотеки Konspekt.exe (для української мови версії від 03-11-2018) з файлами великого розміру.
    • UKR🇺🇦 Вимкнено режим налагодження wine (додано змінну середовища export WINEDEBUG=-all).
    • UKR🇺🇦 Виправлено помилку інтерактивного підсвічування термінів (якщо частота появи терміна в тексті перевищує 300 разів, в цьому випадку буде відключене інтерактивне підсвічування терміна задля коректного роботи інтерфейсу веб-застосунку).

    v2.0.0-beta-67, 2020-02-24

    👍 Покращення

    • UKR🇺🇦 ENG🇬🇧 На головну веб-сторінку додатка додано номер бета-версії.

    v2.0.0-beta-66, 2020-02-22

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено список змін.

    v2.0.0-beta-65, 2020-02-22

    ⚠️ Зауваження

    • UKR🇺🇦 Додано обмеження на розмір оброблюваного файлу, яке становить 2 мб.
    • UKR🇺🇦 Протестовано роботу бібліотеки Konspekt.exe (для української мови версії від 03-11-2018) з файлами великого розміру.
    • UKR🇺🇦 Змінено специфікацію JSON-файлу проекту (Konspekt.exe для української мови версії від 03-11-2018). Результати роботи Konspekt.exe (для української мови версії від 03-11-2018), а саме файли allterms.xml та parce.xml відтепер зберігаються як blob в IndexedDB, з використанням бібліотеки localforage; унікальні ідентифікатори на ці blob містяться у структурі JSON-файлу проекту, відповідно "alltermsxmlAlias": "" - аліас для allterms.xml та "parcexmlAlias": "" - аліас для parce.xml.
    Специфікація `JSON`-файлу проекту (Konspekt.exe для української мови версії від 03-11-2018):

    -

    👍 Покращення

    • UKR🇺🇦 ENG🇬🇧 Додана стартова сторінка вибору мови Конспекту (для обробки документів української або англійською).
    • UKR🇺🇦 Зменшено час аналізу файлів бібліотекою Konspekt.exe (для української мови версії від 03-11-2018).
    • UKR🇺🇦 При виборі терміна в елементах <select> #uploadResultList (спиcок термінів) та <select> #term-tree (дерево термінів), якщо обчислення займає тривалий час, то інтерфейс відображає процес завантаження.

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлені функції сортування в елементі <select> #uploadResultList.
    • UKR🇺🇦 Виправлені технологія та функції підсвічування термінів (function mark(text), function markTerms(term)) в елементі #text-content.

    v2.0.0-beta-47, 2020-01-08

    ⚠️ Зауваження

    • UKR🇺🇦 Змінено специфікацію JSON-файлу проекту (Konspekt.exe для української мови версії від 03-11-2018):
    Специфікація `JSON`-файлу проекту (Konspekt.exe для української мови версії від 03-11-2018): +

    👍 Покращення

    • UKR🇺🇦 ENG🇬🇧 Додана стартова сторінка вибору мови Конспекту (для обробки документів української або англійською).
    • UKR🇺🇦 Зменшено час аналізу файлів бібліотекою Konspekt.exe (для української мови версії від 03-11-2018).
    • UKR🇺🇦 При виборі терміна в елементах <select> #uploadResultList (спиcок термінів) та <select> #term-tree (дерево термінів), якщо обчислення займає тривалий час, то інтерфейс відображає процес завантаження.

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлені функції сортування в елементі <select> #uploadResultList.
    • UKR🇺🇦 Виправлені технологія та функції підсвічування термінів (function mark(text), function markTerms(term)) в елементі #text-content.

    v2.0.0-beta-47, 2020-01-08

    ⚠️ Зауваження

    • UKR🇺🇦 Змінено специфікацію JSON-файлу проекту (Konspekt.exe для української мови версії від 03-11-2018):
    Специфікація `JSON`-файлу проекту (Konspekt.exe для української мови версії від 03-11-2018):

    -

    🔴 Виправлення помилок

    • UKR🇺🇦 ENG🇬🇧 Видалено невикористовувані JavaScript-бібліотеки, зокрема, tippy.js.

    v2.0.0-beta-41, 2019-12-25

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено проблему з попередньо визначеними об'єктами XML r'&|>|<|_|"|\.\.+|\s\s+' (Konspekt.exe для української мови версії від 03-11-2018).
    • UKR🇺🇦 Виправлені проблеми з кодиваннями utf-8/windows-1251.

    👍 Покращення

    • UKR🇺🇦 Додана функція динамічного визначення часу на тривалість роботи Konspekt.exe для української мови версії від 03-11-2018:

    v2.0.0-beta-15, 2019-12-21

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено помилки при decode()/encode().

    v2.0.0-beta-12, 2019-12-20

    👍 Покращення

    • UKR🇺🇦 Додана детекція кодування бібліотекою chardet.

    v2.0.0-beta-11, 2019-12-20

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено помилку роботи утиліти Xvfb (Xvfb or X virtual framebuffer is a display server implementing the X11 display server protocol) та програми Konspekt.exe для української мови (версії від 03-11-2018).
    • UKR🇺🇦 Виправлено помилки при кодуванні: decode the file as CP1251 ignoring any errors.

    v2.0.0-beta-5, 2019-12-19

    👍 Покращення

    • UKR🇺🇦 Оновлено Konspekt.exe для української мови до версії від 03-11-2018.
    • UKR🇺🇦 Оновлено документацію та список змін.

    v2.0.0-beta-4, 2019-12-18

    🔴 Виправлення помилок

    • ENG🇬🇧 Виправлені помилки англійської локалізації інтерфейсу користувача англомовної частини.
    • ENG🇬🇧 Дрібні виправлення JavaScript на клієнті.

    v2.0.0-beta-3, 2019-12-18

    👍 Покращення

    • ENG🇬🇧 Додана англійська локалізація інтерфейсу користувача англомовної частини.

    🔴 Виправлення помилок

    • Дрібні виправлення на серверній частині.

    v2.0.0-beta-2, 2019-12-17

    🔴 Виправлення помилок

    • Дрібні виправлення на серверній частині.

    v2.0.0-beta-1, 2019-12-17

    🏭 Нові можливості

    🌟 Багатомовна версія ENG🇬🇧, UKR🇺🇦

    🌟 Initial commit for multilingual version ENG🇬🇧, UKR🇺🇦


    v1.0.5, 2019-12-11

    🔴 Виправлення помилок

    • Виправлено помилку при нумерації в тегах <relup>/<reldown> файлу allterms.xml. -Нумерація в тегах <relup>/<reldown> файлу allterms.xml починається з 0.
    • Виправлення JavaScript на клієнті згідно нової нумерації в тегах <relup>/<reldown>, <sentpos> файлу allterms.xml.

    v1.0.4, 2019-12-10

    🔴 Виправлення помилок

    • Виправлено помилку при нумерації речень в тегу <sentpos> файлу allterms.xml. Нумерація речень в тегу <sentpos> файлу allterms.xml починається з 0. -Для індексації речень з використанням spaCy (в цій бібліотеці відсутня індексація речень) за основу використано приклад:

    • Розширення файлу при збереженні таблиці #table-main в формат Office Open XML Workbook виправлено на xlsx.

    • Дублюючий символ крапка . при обробці тексту з PDF файлів. В функцию def text_normalization_default(raw_text) додано можливість видалення дублюючого символу крапка . (а саме заміна на один символ крапка .) при нормалізації тексту.

    👍 Покращення

    • Змінено елемент <title> головної сторінки index.html (додано мовний код EN додатку ken згідно стандарту представлення назв мов ISO 639-1):

    v1.0.3, 2019-10-31

    ⚠️ Зауваження

    • Для коректної роботи клієнтської частини веб-застосунка KEn, необхідно використовувати актуальну версію браузера Google Chrome (70 та вище).
    • При оновленні KEn з версії v1.0.2 (або більш ранньої) до v.1.0.3 дані проаналізованих раніше документів будуть втрачені.
    • Змінено специфікацію JSON-файлу проекту:
    Специфікація `JSON`-файлу проекту: +

    🔴 Виправлення помилок

    • UKR🇺🇦 ENG🇬🇧 Видалено невикористовувані JavaScript-бібліотеки, зокрема, tippy.js.

    v2.0.0-beta-41, 2019-12-25

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено проблему з попередньо визначеними об'єктами XML r'&|>|<|_|"|\.\.+|\s\s+' (Konspekt.exe для української мови версії від 03-11-2018).
    • UKR🇺🇦 Виправлені проблеми з кодиваннями utf-8/windows-1251.

    👍 Покращення

    • UKR🇺🇦 Додана функція динамічного визначення часу на тривалість роботи Konspekt.exe для української мови версії від 03-11-2018:

    v2.0.0-beta-15, 2019-12-21

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено помилки при decode()/encode().

    v2.0.0-beta-12, 2019-12-20

    👍 Покращення

    • UKR🇺🇦 Додана детекція кодування бібліотекою chardet.

    v2.0.0-beta-11, 2019-12-20

    🔴 Виправлення помилок

    • UKR🇺🇦 Виправлено помилку роботи утиліти Xvfb (Xvfb or X virtual framebuffer is a display server implementing the X11 display server protocol) та програми Konspekt.exe для української мови (версії від 03-11-2018).
    • UKR🇺🇦 Виправлено помилки при кодуванні: decode the file as CP1251 ignoring any errors.

    v2.0.0-beta-5, 2019-12-19

    👍 Покращення

    • UKR🇺🇦 Оновлено Konspekt.exe для української мови до версії від 03-11-2018.
    • UKR🇺🇦 Оновлено документацію та список змін.

    v2.0.0-beta-4, 2019-12-18

    🔴 Виправлення помилок

    • ENG🇬🇧 Виправлені помилки англійської локалізації інтерфейсу користувача англомовної частини.
    • ENG🇬🇧 Дрібні виправлення JavaScript на клієнті.

    v2.0.0-beta-3, 2019-12-18

    👍 Покращення

    • ENG🇬🇧 Додана англійська локалізація інтерфейсу користувача англомовної частини.

    🔴 Виправлення помилок

    • Дрібні виправлення на серверній частині.

    v2.0.0-beta-2, 2019-12-17

    🔴 Виправлення помилок

    • Дрібні виправлення на серверній частині.

    v2.0.0-beta-1, 2019-12-17

    🏭 Нові можливості

    🌟 Багатомовна версія ENG🇬🇧, UKR🇺🇦

    🌟 Initial commit for multilingual version ENG🇬🇧, UKR🇺🇦


    v1.0.5, 2019-12-11

    🔴 Виправлення помилок

    • Виправлено помилку при нумерації в тегах <relup>/<reldown> файлу allterms.xml. +Нумерація в тегах <relup>/<reldown> файлу allterms.xml починається з 0.
    • Виправлення JavaScript на клієнті згідно нової нумерації в тегах <relup>/<reldown>, <sentpos> файлу allterms.xml.

    v1.0.4, 2019-12-10

    🔴 Виправлення помилок

    • Виправлено помилку при нумерації речень в тегу <sentpos> файлу allterms.xml. Нумерація речень в тегу <sentpos> файлу allterms.xml починається з 0. +Для індексації речень з використанням spaCy (в цій бібліотеці відсутня індексація речень) за основу використано приклад:

    • Розширення файлу при збереженні таблиці #table-main в формат Office Open XML Workbook виправлено на xlsx.

    • Дублюючий символ крапка . при обробці тексту з PDF файлів. В функцию def text_normalization_default(raw_text) додано можливість видалення дублюючого символу крапка . (а саме заміна на один символ крапка .) при нормалізації тексту.

    👍 Покращення

    • Змінено елемент <title> головної сторінки index.html (додано мовний код EN додатку ken згідно стандарту представлення назв мов ISO 639-1):

    v1.0.3, 2019-10-31

    ⚠️ Зауваження

    • Для коректної роботи клієнтської частини веб-застосунка KEn, необхідно використовувати актуальну версію браузера Google Chrome (70 та вище).
    • При оновленні KEn з версії v1.0.2 (або більш ранньої) до v.1.0.3 дані проаналізованих раніше документів будуть втрачені.
    • Змінено специфікацію JSON-файлу проекту:
    Специфікація `JSON`-файлу проекту:

    -

    👍 Покращення

    • Реалізовано функцію визначення браузера клієнта (Для коректної роботи клієнтської частини веб-застосунка KEn, необхідно використовувати актуальну версію браузера Google Chrome).

    🔴 Виправлення помилок

    • Дрібні виправлення JavaScript на клієнті.
    • Виправлено роботу елемента #notes (Блокнот), а саме додано функцію автоматичного збереження вмісту елемента #notes до головного JSON-файлу проекту в поле notes, та відповідно змінено його специфікацію.

    v1.0.2, 2019-09-27

    ⚠️ Зауваження

    • Значення параметра конфігурації app.config['MAX_CONTENT_LENGTH'] об'єкту Flask відновлено за замовчуванням (За замовчуванням об'єкт Flask прийме завантаження файлів на необмежену кількість пам'яті).

    👍 Покращення

    • Змінено елемент <title> головної сторінки index.html (видалено рік):

    v1.0.1, 2019-08-24

    ⚠️ Зауваження

    • При оновленні KEn з версії v1.0.0 (або більш ранньої) до v.1.0.1 дані проаналізованих раніше документів будуть втрачені.
    • Змінено специфікацію JSON-файлу проекту:
    Специфікація `JSON`-файлу проекту: +

    👍 Покращення

    • Реалізовано функцію визначення браузера клієнта (Для коректної роботи клієнтської частини веб-застосунка KEn, необхідно використовувати актуальну версію браузера Google Chrome).

    🔴 Виправлення помилок

    • Дрібні виправлення JavaScript на клієнті.
    • Виправлено роботу елемента #notes (Блокнот), а саме додано функцію автоматичного збереження вмісту елемента #notes до головного JSON-файлу проекту в поле notes, та відповідно змінено його специфікацію.

    v1.0.2, 2019-09-27

    ⚠️ Зауваження

    • Значення параметра конфігурації app.config['MAX_CONTENT_LENGTH'] об'єкту Flask відновлено за замовчуванням (За замовчуванням об'єкт Flask прийме завантаження файлів на необмежену кількість пам'яті).

    👍 Покращення

    • Змінено елемент <title> головної сторінки index.html (видалено рік):

    v1.0.1, 2019-08-24

    ⚠️ Зауваження

    • При оновленні KEn з версії v1.0.0 (або більш ранньої) до v.1.0.1 дані проаналізованих раніше документів будуть втрачені.
    • Змінено специфікацію JSON-файлу проекту:
    Специфікація `JSON`-файлу проекту:

    -

    👍 Покращення

    • Реалізовано функції зжимання та відновлення строк з використанням програмної бібліотеки LZ-based compression algorithm for JavaScript, а саме поцедур compressToBase64/decompressFromBase64 (що зберігають allterms.xml в alltermsxmlCompressed, та parce.xml в parcexmlCompressedJSON-файлі проекту)).

    🔴 Виправлення помилок

    • Виправлено помилку графічного інтерфейсу розташування елементу #notes відносно #displacy, #displacy-ner, #displacy-label.

    v1.0.0, 2019-08-20

    ⚠️ Зауваження

    • При оновленні KEn з версії v0.7.1 (або більш ранньої) до v.1.0.0 дані проаналізованих раніше документів будуть втрачені.

    👍 Покращення

    • Збільшено розмір nginx client_max_body_size до 500 mb (що дозволяє проводити обробку файлів розміром до 500 мб).

    • Оновлено функції та процедури роботи клієнтської частини програми з використанням IndexedDB API та програмної бібліотеки localForage, що відповідають за збереження та ініціалізацію результатів лінгвістичного аналізу документів та основного файлу проекту:

      • додавання розбору (РІС, allterms, parce) кожного нового документу до головного JSON-файл проекту;
      • збереження проекту в локальний JSON-файл, що міститеме розбори всіх документів (РІС, allterms, parce) та налаштування;
      • відкриття проекту з локального JSON-файлу.

    🔴 Виправлення помилок

    • Виправлено роботу елемента #termTree, а саме додано функцію "візуалізації залежностей термінів" з елементу #termTree в елементі #depparse_tab, а саме в #displacy.

    • Виправлено помилку DOMException QuotaExceededError / QUOTA_EXCEEDED_ERR: DOM Exception 22 (Перевищено розмір квоти для localStorage, що становить 5 Мб. Тобто є ліміт на кількість файлів, що можуть бути збережені в проекті). Збереження файлів проекту відтепер здійснюється з використанням IndexedDB API та програмної бібліотеки localForage. Довідка:

      • про розмір квот для localStorage та скрипт localStorageDB (використовує IndexedDB як key-value сховище, квота може перевищувати 5Мб) - localStorageDB скрипт;
      • максимальна квота для IndexedDB - Maximum item size in IndexedDB;
      • поліпшене керування сховищем в браузері: бібліотека localForage;

    ⚠️ Застаріле

    • Функції та процедури роботи клієнтської частини програми на основі localStorage (що відповідають за збереження та ініціалізацію результатів лінгвістичного аналізу файлів проекту), окрім таблиці.

    v0.7.1, 2019-08-14

    👍 Покращення

    • Додано функцію "копіювання по кліку" термінів з елементу #termTree (дерево термінів) в елемент #table-body (таблиця).
    • Оновлено меню "Допомога" (елемент #button-dropdown-help): додано "Журнал змін" (елемент #button-changelog) - Журнал змін проекту CHANGELOG.md.

    🔴 Виправлення помилок

    • Виправлено імена, що надаються за замовчуванням при збереженні в файл списків "Терміни" (#uploadResultList), РІС (#uploadUnknownTerms).

    v0.7.0, 2019-08-10

    ⚠️ Зауваження

    • При оновленні KEn з версії v0.6.0 до v.0.7.0 дані проаналізованих раніше файлів будуть втрачені або працюватимуть некоректно.

    👍 Покращення

    • Оновлено функції та процедури роботи з localStorage, що відповідають за збереження та ініціалізацію результатів лінгвістичного аналізу файлів проекту.
    • Вимкнено запис логів при доступі до файлів, що знаходяться в папці static.

    🔴 Виправлення помилок

    • Виправлено оновлення деяких елементів при перемиканні/вибору файлів в елементі #projectFileList ("Файли"). Оновлються елементи:

      • #displacy-ner ("Візуалізація")
      • #uploadUnknownTerms ("РІС") -відповідно до обраного файлу в елементі #projectFileList ("Файли").
    • Виправлено очищення відповідних розборів документів (-parsexml, -alltermsxml та JSON-розборів), що зберігаються в localStorage при видаленні файлів проекту зі списку Файли #projectFileList.

    • Дрібні виправлення JavaScript на клієнті (зокрема, елементів контейнеру class="col-md-6").

    v0.6.0, 2019-08-07

    🏭 Нові можливості

    • Реалізовано можливість локального збереження файлів разбору allterms.xml та parce.xml через графічний інтерфейс користувача, зокрема, через взаємодію з елементом #button-save відповідно:

      • #button-save-allterms-xml для збереження allterms.xml;
      • #button-save-parce-xml для збереження parce.xml.

    🔴 Виправлення помилок

    • Дрібні виправлення JavaScript на клієнті.

    v0.5.5, 2019-08-03

    🔴 Виправлення помилок

    • Виправлено id елементу #text-content-panel-body.

    v0.5.4, 2019-07-30

    🔴 Виправлення помилок

    • Виправлено поведінку елементу #notes (відключено можливість змінення розміру).

    v0.5.3, 2019-07-28

    🔴 Виправлення помилок

    • Виправлено випадкове виконання функцій events при взаємодії з елементами:

      • #uploadResultList
      • #projectFileList
      • #uploadUnknownTerms

    v0.5.2, 2019-07-27

    🔴 Виправлення помилок

    • Виправлено помилку роботи з файлами, що мають однакові імена але різний зміст (Реалізовано генерування унікальних імен файлів для localStorage).
    • Дрібні виправлення JavaScript на клієнті.

    v0.5.1, 2019-07-25

    🔴 Виправлення помилок

    • Виправлено дерево термінів: -Реалізовано активне дерево термінів, тобто - перехід до вибраного терміну в дереві термінів (відповідне відображення речень з терміном в елементі #term-tree та виділення речень з терміном в sents_from_text).
    • Дрібні виправлення інтерфейсу.
    • Видалено невикористовувані JavaScript-бібліотеки.

    v0.5.0, 2019-07-25

    🏭 Нові можливості

    • Реалізовано підсвічування речень з вибраним терміном в елементі #sents_from_text та вибраних термінів в #text-content з використанням бібліотеки mark.js

    🔴 Виправлення помилок

    • Дрібні виправлення інтерфейсу.
    • Дрібні виправлення серверної частини: -змінено роботу нормалізації тексту (відключено line = re.sub(r'\W', ' ', line, flags=re.I)).

    v0.4.2, 2019-07-23

    🔴 Виправлення помилок

    • Виправлено номер версії в елементі title.

    v0.4.1, 2019-07-23

    👍 Покращення

    • Додано номер поточної версії KEn до елементу title.

    v0.4.0, 2019-07-23

    🏭 Нові можливості

    • Реалізовано відображення показників частоти термінів за допомоги спливаючої підказки title для кожного терміну елементу #uploadResultList. -Реалізовано можливість сортування термінів в елементі #uploadResultList згідно:

      • частоти (за збільшенням);
      • частоти (за зменшенням);
      • за алфавітом;
      • за черговою появою в тексті;
    • Реалізовано елемент #sort-select для обрання відповідного типу сортування.

    👍 Покращення

    • Рефакторинг програмного коду.

    🔴 Виправлення помилок

    • Дрібні виправлення.

    📚 Документація

    • Оновлено розділи Системні вимоги згідно нових мінімальних системних вимог, україномовної частини README.md.

    v0.3.0, 2019-07-22

    🔴 Виправлення помилок

    • Виправлено помилку UnicodeDecodeError: 'utf8' codec can't decode byte. +

      👍 Покращення

      • Реалізовано функції зжимання та відновлення строк з використанням програмної бібліотеки LZ-based compression algorithm for JavaScript, а саме поцедур compressToBase64/decompressFromBase64 (що зберігають allterms.xml в alltermsxmlCompressed, та parce.xml в parcexmlCompressedJSON-файлі проекту)).

      🔴 Виправлення помилок

      • Виправлено помилку графічного інтерфейсу розташування елементу #notes відносно #displacy, #displacy-ner, #displacy-label.

      v1.0.0, 2019-08-20

      ⚠️ Зауваження

      • При оновленні KEn з версії v0.7.1 (або більш ранньої) до v.1.0.0 дані проаналізованих раніше документів будуть втрачені.

      👍 Покращення

      • Збільшено розмір nginx client_max_body_size до 500 mb (що дозволяє проводити обробку файлів розміром до 500 мб).

      • Оновлено функції та процедури роботи клієнтської частини програми з використанням IndexedDB API та програмної бібліотеки localForage, що відповідають за збереження та ініціалізацію результатів лінгвістичного аналізу документів та основного файлу проекту:

        • додавання розбору (РІС, allterms, parce) кожного нового документу до головного JSON-файл проекту;
        • збереження проекту в локальний JSON-файл, що міститеме розбори всіх документів (РІС, allterms, parce) та налаштування;
        • відкриття проекту з локального JSON-файлу.

      🔴 Виправлення помилок

      • Виправлено роботу елемента #termTree, а саме додано функцію "візуалізації залежностей термінів" з елементу #termTree в елементі #depparse_tab, а саме в #displacy.

      • Виправлено помилку DOMException QuotaExceededError / QUOTA_EXCEEDED_ERR: DOM Exception 22 (Перевищено розмір квоти для localStorage, що становить 5 Мб. Тобто є ліміт на кількість файлів, що можуть бути збережені в проекті). Збереження файлів проекту відтепер здійснюється з використанням IndexedDB API та програмної бібліотеки localForage. Довідка:

        • про розмір квот для localStorage та скрипт localStorageDB (використовує IndexedDB як key-value сховище, квота може перевищувати 5Мб) - localStorageDB скрипт;
        • максимальна квота для IndexedDB - Maximum item size in IndexedDB;
        • поліпшене керування сховищем в браузері: бібліотека localForage;

      ⚠️ Застаріле

      • Функції та процедури роботи клієнтської частини програми на основі localStorage (що відповідають за збереження та ініціалізацію результатів лінгвістичного аналізу файлів проекту), окрім таблиці.

      v0.7.1, 2019-08-14

      👍 Покращення

      • Додано функцію "копіювання по кліку" термінів з елементу #termTree (дерево термінів) в елемент #table-body (таблиця).
      • Оновлено меню "Допомога" (елемент #button-dropdown-help): додано "Журнал змін" (елемент #button-changelog) - Журнал змін проекту CHANGELOG.md.

      🔴 Виправлення помилок

      • Виправлено імена, що надаються за замовчуванням при збереженні в файл списків "Терміни" (#uploadResultList), РІС (#uploadUnknownTerms).

      v0.7.0, 2019-08-10

      ⚠️ Зауваження

      • При оновленні KEn з версії v0.6.0 до v.0.7.0 дані проаналізованих раніше файлів будуть втрачені або працюватимуть некоректно.

      👍 Покращення

      • Оновлено функції та процедури роботи з localStorage, що відповідають за збереження та ініціалізацію результатів лінгвістичного аналізу файлів проекту.
      • Вимкнено запис логів при доступі до файлів, що знаходяться в папці static.

      🔴 Виправлення помилок

      • Виправлено оновлення деяких елементів при перемиканні/вибору файлів в елементі #projectFileList ("Файли"). Оновлються елементи:

        • #displacy-ner ("Візуалізація")
        • #uploadUnknownTerms ("РІС") +відповідно до обраного файлу в елементі #projectFileList ("Файли").
      • Виправлено очищення відповідних розборів документів (-parsexml, -alltermsxml та JSON-розборів), що зберігаються в localStorage при видаленні файлів проекту зі списку Файли #projectFileList.

      • Дрібні виправлення JavaScript на клієнті (зокрема, елементів контейнеру class="col-md-6").

      v0.6.0, 2019-08-07

      🏭 Нові можливості

      • Реалізовано можливість локального збереження файлів разбору allterms.xml та parce.xml через графічний інтерфейс користувача, зокрема, через взаємодію з елементом #button-save відповідно:

        • #button-save-allterms-xml для збереження allterms.xml;
        • #button-save-parce-xml для збереження parce.xml.

      🔴 Виправлення помилок

      • Дрібні виправлення JavaScript на клієнті.

      v0.5.5, 2019-08-03

      🔴 Виправлення помилок

      • Виправлено id елементу #text-content-panel-body.

      v0.5.4, 2019-07-30

      🔴 Виправлення помилок

      • Виправлено поведінку елементу #notes (відключено можливість змінення розміру).

      v0.5.3, 2019-07-28

      🔴 Виправлення помилок

      • Виправлено випадкове виконання функцій events при взаємодії з елементами:

        • #uploadResultList
        • #projectFileList
        • #uploadUnknownTerms

      v0.5.2, 2019-07-27

      🔴 Виправлення помилок

      • Виправлено помилку роботи з файлами, що мають однакові імена але різний зміст (Реалізовано генерування унікальних імен файлів для localStorage).
      • Дрібні виправлення JavaScript на клієнті.

      v0.5.1, 2019-07-25

      🔴 Виправлення помилок

      • Виправлено дерево термінів: +Реалізовано активне дерево термінів, тобто - перехід до вибраного терміну в дереві термінів (відповідне відображення речень з терміном в елементі #term-tree та виділення речень з терміном в sents_from_text).
      • Дрібні виправлення інтерфейсу.
      • Видалено невикористовувані JavaScript-бібліотеки.

      v0.5.0, 2019-07-25

      🏭 Нові можливості

      • Реалізовано підсвічування речень з вибраним терміном в елементі #sents_from_text та вибраних термінів в #text-content з використанням бібліотеки mark.js

      🔴 Виправлення помилок

      • Дрібні виправлення інтерфейсу.
      • Дрібні виправлення серверної частини: +змінено роботу нормалізації тексту (відключено line = re.sub(r'\W', ' ', line, flags=re.I)).

      v0.4.2, 2019-07-23

      🔴 Виправлення помилок

      • Виправлено номер версії в елементі title.

      v0.4.1, 2019-07-23

      👍 Покращення

      • Додано номер поточної версії KEn до елементу title.

      v0.4.0, 2019-07-23

      🏭 Нові можливості

      • Реалізовано відображення показників частоти термінів за допомоги спливаючої підказки title для кожного терміну елементу #uploadResultList. +Реалізовано можливість сортування термінів в елементі #uploadResultList згідно:

        • частоти (за збільшенням);
        • частоти (за зменшенням);
        • за алфавітом;
        • за черговою появою в тексті;
      • Реалізовано елемент #sort-select для обрання відповідного типу сортування.

      👍 Покращення

      • Рефакторинг програмного коду.

      🔴 Виправлення помилок

      • Дрібні виправлення.

      📚 Документація

      • Оновлено розділи Системні вимоги згідно нових мінімальних системних вимог, україномовної частини README.md.

      v0.3.0, 2019-07-22

      🔴 Виправлення помилок

      • Виправлено помилку UnicodeDecodeError: 'utf8' codec can't decode byte. Декодовано файл як UTF-8, ігноруючи будь-які символи які закодовані в неправильному кодуванні:

      • Включено збереження макета документа, включаючи пробіли, які є лише візуальними, а не символами. -Виправлено згідно python pdfminer converts pdf file into one chunk of string with no spaces between words:

      • Виправлено роботу елементу iziToast (нотифікації про процес обробки документів).

      v0.2.8, 2019-07-20

      🔴 Виправлення помилок

      • Реалізовано видалення символу ° на етапі нормалізації тексту.
      • Реалізовано видалення всіх не словникових символів (\W non-alphanumeric characters) на етапі нормалізації тексту.
      • Реалізовано видалення всіх слів, що містять числа при нормалізації тексту.

      v0.2.7, 2019-07-19

      🔴 Виправлення помилок

      • Вимкнено появу стандартного контекстного меню на елементі #projectFileList при евенті видалення файлів проекту за кліком правої кнопки миші.
      • Збільшено максимально допустимий розмір тіла запиту клієнта до 50 мегабайт: client_max_body_size 50M.

      📚 Документація

      • Додано файл CHANGELOG.md, що містить список версій програми та список відповідних змін програмного коду, виправлень та покращень.

      v0.2.6, 2019-07-18

      🔴 Виправлення помилок

      • Виправлено скролінг в елементах класу .col-md-6.

      👍 Покращення

      • Додано вкладку "Блокнот" з елементом textarea до .col-md-6.

      v0.2.5, 2019-07-18

      🔴 Виправлення помилок

      • Дрібні виправлення.

      👍 Покращення

      • Видалено тег версій з кінцевих точок API: +Виправлено згідно python pdfminer converts pdf file into one chunk of string with no spaces between words:

      • Виправлено роботу елементу iziToast (нотифікації про процес обробки документів).

      v0.2.8, 2019-07-20

      🔴 Виправлення помилок

      • Реалізовано видалення символу ° на етапі нормалізації тексту.
      • Реалізовано видалення всіх не словникових символів (\W non-alphanumeric characters) на етапі нормалізації тексту.
      • Реалізовано видалення всіх слів, що містять числа при нормалізації тексту.

      v0.2.7, 2019-07-19

      🔴 Виправлення помилок

      • Вимкнено появу стандартного контекстного меню на елементі #projectFileList при евенті видалення файлів проекту за кліком правої кнопки миші.
      • Збільшено максимально допустимий розмір тіла запиту клієнта до 50 мегабайт: client_max_body_size 50M.

      📚 Документація

      • Додано файл CHANGELOG.md, що містить список версій програми та список відповідних змін програмного коду, виправлень та покращень.

      v0.2.6, 2019-07-18

      🔴 Виправлення помилок

      • Виправлено скролінг в елементах класу .col-md-6.

      👍 Покращення

      • Додано вкладку "Блокнот" з елементом textarea до .col-md-6.

      v0.2.5, 2019-07-18

      🔴 Виправлення помилок

      • Дрібні виправлення.

      👍 Покращення

      • Видалено тег версій з кінцевих точок API: було: host[:port]/ken/api/**v1.0**/en/file/allterms стало: -host[:port]/ken/api/en/file/allterms.
      • Додано горизонтальный скролл до елементів id="uploadResultList"; #term-tree.
      • Додано можливість видалення файлів зі списку "Файли" id="projectFileList" по кліку правої кнопки миші.
      • Оновлено структуру проекту.
      • Рефакторинг програмного коду.

      📚 Документація

      • Додана настанова користувача Як зберегти Docker image в файл (резервне копіювання) для подальшого використання на іншому сервері.
      • Виправлено виділення термінів на виокремлення термінів.
      • Оновлення україномовної частини README.md.

      v0.2.4, 2019-07-08

      🔴 Bug fixes

      • Fix sentence duplication in id="text-content".
      • Fix id="sents_from_text" area to update for a new text.
      • Fix add text from last file to id="sents_from_text" area.
      • Fix add text to id="sents_from_text" area when selecting files from id="projectFileList" select list.

      👍 Improvements

      • Update project structure.
      • Clean up source code.

      📚 Tutorial and doc improvements

      • Update UA part of README.md.

      🔴 Виправлення помилок

      • Виправлено дублювання речення в елементі id =" text-content ".
      • Виправлено оновлення елементу id="sents_from_text" згідно нового тексту.
      • Виправлено додавання тексту з останнього опрацьованого файлу в область `id =" sents_from_text ".
      • Виправлено додавання тексту в елемент id="sents_from_text" при виборі відповідного файлу зі списку елементу id="projectFileList".
      • Дрібні виправлення.

      👍 Покращення

      • Оновлено структуру проекту.
      • Рефакторинг програмного коду.

      📚 Документація

      • Оновлення україномовної частини README.md.

      v0.2.3, 2019-06-25

      👍 Improvements

      • Remove (comment) dependencies for language_check.
      • Clean up source code.

      📚 Tutorial and doc improvements

      • Update UA part of README.md.

      👍 Покращення

      • Видалено залежності для language_check.
      • Рефакторинг програмного коду.

      📚 Документація

      • Оновлення україномовної частини README.md.

      v0.2.2, 2019-06-23

      👍 Improvements

      • Fix Highlighting terms in text area id="text-content".
      • Fix loader colour.

      👍 Покращення

      • Виправлено підсвічування виокремлених термінів у id="text-content".
      • Змінено колір елементу loader.

      v0.2.1, 2019-06-23

      👍 Improvements

      • Add save to csv.
      • Add save of all lists (terms, NER, files).
      • Add new notifications.
      • Fix titles.

      🔴 Bug fixes

      • Various bug fixes.

      📚 Tutorial and doc improvements

      • Update UA part of README.md.

      👍 Покращення

      • Додано збереження в формат csv.
      • Додано можливість збереження списків (terms, NER, files).
      • Додні нотифікації про виконання процесів.
      • Виправлені елементи title.

      🔴 Виправлення помилок

      • Дрібні виправлення.

      📚 Документація

      • Оновлення україномовної частини README.md.

      v0.2.0, 2019-06-21

      👍 Improvements

      • All new table.
      • All new table controls.
      • Added saving table in Excel .xls format.

      ⚠️ Deprecations

      • Removed saving in .csv.

      🔴 Bug fixes

      • Various bug fixes.

      📚 Tutorial and doc improvements

      • Update UA part of README.md.

      👍 Покращення

      • Нова таблиця.
      • Нові елементи управління таблицею.
      • Додана можливість збереження таблиці в формат Excel .xls.

      ⚠️ Застаріле

      • Видалено збереження в формат .csv.

      🔴 Виправлення помилок

      • Різні дрібні виправлення.

      📚 Документація

      • Оновлення україномовної частини README.md.

      v0.1.1, 2019-06-02

      🌟 Початковий попередній реліз


      🌟 Initial pre-release

    +host[:port]/ken/api/en/file/allterms.
  • Додано горизонтальный скролл до елементів id="uploadResultList"; #term-tree.
  • Додано можливість видалення файлів зі списку "Файли" id="projectFileList" по кліку правої кнопки миші.
  • Оновлено структуру проекту.
  • Рефакторинг програмного коду.

📚 Документація

  • Додана настанова користувача Як зберегти Docker image в файл (резервне копіювання) для подальшого використання на іншому сервері.
  • Виправлено виділення термінів на виокремлення термінів.
  • Оновлення україномовної частини README.md.

v0.2.4, 2019-07-08

🔴 Bug fixes

  • Fix sentence duplication in id="text-content".
  • Fix id="sents_from_text" area to update for a new text.
  • Fix add text from last file to id="sents_from_text" area.
  • Fix add text to id="sents_from_text" area when selecting files from id="projectFileList" select list.

👍 Improvements

  • Update project structure.
  • Clean up source code.

📚 Tutorial and doc improvements

  • Update UA part of README.md.

🔴 Виправлення помилок

  • Виправлено дублювання речення в елементі id =" text-content ".
  • Виправлено оновлення елементу id="sents_from_text" згідно нового тексту.
  • Виправлено додавання тексту з останнього опрацьованого файлу в область `id =" sents_from_text ".
  • Виправлено додавання тексту в елемент id="sents_from_text" при виборі відповідного файлу зі списку елементу id="projectFileList".
  • Дрібні виправлення.

👍 Покращення

  • Оновлено структуру проекту.
  • Рефакторинг програмного коду.

📚 Документація

  • Оновлення україномовної частини README.md.

v0.2.3, 2019-06-25

👍 Improvements

  • Remove (comment) dependencies for language_check.
  • Clean up source code.

📚 Tutorial and doc improvements

  • Update UA part of README.md.

👍 Покращення

  • Видалено залежності для language_check.
  • Рефакторинг програмного коду.

📚 Документація

  • Оновлення україномовної частини README.md.

v0.2.2, 2019-06-23

👍 Improvements

  • Fix Highlighting terms in text area id="text-content".
  • Fix loader colour.

👍 Покращення

  • Виправлено підсвічування виокремлених термінів у id="text-content".
  • Змінено колір елементу loader.

v0.2.1, 2019-06-23

👍 Improvements

  • Add save to csv.
  • Add save of all lists (terms, NER, files).
  • Add new notifications.
  • Fix titles.

🔴 Bug fixes

  • Various bug fixes.

📚 Tutorial and doc improvements

  • Update UA part of README.md.

👍 Покращення

  • Додано збереження в формат csv.
  • Додано можливість збереження списків (terms, NER, files).
  • Додні нотифікації про виконання процесів.
  • Виправлені елементи title.

🔴 Виправлення помилок

  • Дрібні виправлення.

📚 Документація

  • Оновлення україномовної частини README.md.

v0.2.0, 2019-06-21

👍 Improvements

  • All new table.
  • All new table controls.
  • Added saving table in Excel .xls format.

⚠️ Deprecations

  • Removed saving in .csv.

🔴 Bug fixes

  • Various bug fixes.

📚 Tutorial and doc improvements

  • Update UA part of README.md.

👍 Покращення

  • Нова таблиця.
  • Нові елементи управління таблицею.
  • Додана можливість збереження таблиці в формат Excel .xls.

⚠️ Застаріле

  • Видалено збереження в формат .csv.

🔴 Виправлення помилок

  • Різні дрібні виправлення.

📚 Документація

  • Оновлення україномовної частини README.md.

v0.1.1, 2019-06-02

🌟 Початковий попередній реліз


🌟 Initial pre-release

\ No newline at end of file diff --git a/templates/en.html b/templates/en.html index ad675e6..cfe7a61 100644 --- a/templates/en.html +++ b/templates/en.html @@ -2,7 +2,7 @@ - Konspekt - v3.1.0 + Konspekt - v3.2.0 diff --git a/templates/index.html b/templates/index.html index f753cf8..a6789e6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,7 +2,7 @@ - Konspekt - v3.1.0 + Konspekt - v3.2.0 @@ -15,7 +15,7 @@

КОНСПЕКТ

-
v3.1.0
+
v3.2.0

Контекстно-семантичний аналіз природно-мовних текстів та побудова таксономії документів

Українська English diff --git a/tests/load-tests/Ridge CV - Jon Herud.docx b/tests/load-tests/Ridge CV - Jon Herud.docx new file mode 100644 index 0000000000000000000000000000000000000000..85fc91f8dc702948e77158fbe0d7b0928099751d GIT binary patch literal 66138 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 DjaZ+I literal 0 HcmV?d00001 diff --git a/tests/load-tests/about-ontology-bigger.txt b/tests/load-tests/about-ontology-bigger.txt new file mode 100644 index 0000000..8d12102 --- /dev/null +++ b/tests/load-tests/about-ontology-bigger.txt @@ -0,0 +1,43 @@ +After the vision of the Semantic Web was broadcasted at the turn of the millennium, +ontology became a synonym for the solution to many problems concerning the fact that computers do not understand human language: if there were an ontology and every document were marked up with it and we had agents that would understand the mark-up, then computers would finally be able to process our queries in a really sophisticated way. Some years later, the success of Google shows us that the vision has not come true, being hampered by the incredible amount of extra work required for the intellectual encoding of semantic mark-up – as compared to simply uploading an HTML page. To alleviate this acquisition bottleneck, the field of ontology learning has since emerged as an important sub-field of ontology engineering. +Numerous applications using lexical-semantic databases like WordNet (Miller, 1990) and its non-English counterparts, e.g. EuroWordNet (Vossen, 1997) or CoreNet (Choi and Bae, 2004) demonstrate the utility of semantic resources for natural language processing. +It is widely accepted that ontologies can facilitate text understanding and automatic +processing of textual resources. Moving from words to concepts not only mitigates data +sparseness issues, but also promises appealing solutions to polysemy and homonymy +by finding non-ambiguous concepts that may map to various realizations in – possibly +ambiguous – words. +In section 2, a variety of methods for learning ontologies from unstructured text sources are classified and explained on a conceptual level. Section 3 deals with the evaluation of automatically generated ontologies and section 4 concludes. +Numerous applications using lexical-semantic databases like WordNet (Miller, 1990) and its non-English counterparts, e.g. EuroWordNet (Vossen, 1997) or CoreNet (Choi and Bae, 2004) demonstrate the utility of semantic resources for natural language processing. +Python is a high-level programming language. +Learning semantic resources from text instead of manually creating them might be +dangerous in terms of correctness, but has undeniable advantages: Creating resources +for text processing from the texts to be processed will fit the semantic component neatly +and directly to them, which will never be possible with general-purpose resources. Further, the cost per entry is greatly reduced, giving rise to much larger resources than an advocate of a manual approach could ever afford. On the other hand, none of the methods used today are good enough for creating semantic resources of any kind in a completely unsupervised fashion, albeit automatic methods can facilitate manual construction to a large extent. +It is widely accepted that ontologies can facilitate text understanding and automatic +processing of textual resources. +In section 2, a variety of methods for learning ontologies from unstructured text sources are classified and explained on a conceptual level. Section 3 deals with the evaluation of automatically generated ontologies and section 4 concludes. +Numerous applications using lexical-semantic databases like WordNet (Miller, 1990) and its non-English counterparts, e.g. EuroWordNet (Vossen, 1997) or CoreNet (Choi and Bae, 2004) demonstrate the utility of semantic resources for natural language processing. +The term ontology is understood in a variety of ways and has been used in philosophy for many centuries. In contrast, the notion of ontology in the field of computer science is younger – but almost used as inconsistently, when it comes to the details of the definition. +Python is a high-level programming language. +The intention of this essay is to give an overview of different methods that learn +ontologies or ontology-like structures from unstructured text. Ontology learning from +other sources, issues in description languages, ontology editors, ontology merging and +ontology evolving transcend the scope of this article. Surveys on ontology learning from text and other sources can be found in Ding and Foo (2002) and Gómez-Pérez and Manzano-Macho (2003), for a survey of ontology learning from the Semantic Web perspective the reader is referred to Omelayenko (2001). +Another goal of this essay is to clarify the notion of the term ontology not by defining it once and for all, but to illustrate the correspondences and differences of its usage. +In the remainder of this section, the usage of ontology is illustrated very briefly in the field of philosophy as contrasted to computer science, where different types of ontologies can be identified. +In section 2, a variety of methods for learning ontologies from unstructured text sources are classified and explained on a conceptual level. Section 3 deals with the evaluation of automatically generated ontologies and section 4 concludes. +Numerous applications using lexical-semantic databases like WordNet (Miller, 1990) and its non-English counterparts, e.g. EuroWordNet (Vossen, 1997) or CoreNet (Choi and Bae, 2004) demonstrate the utility of semantic resources for natural language processing. +Python is a high-level programming language. +Python is a high-level programming language for the natural language processing pipeline. +In section 2, a variety of methods for learning ontologies from unstructured text sources are classified and explained on a conceptual level. Section 3 deals with the evaluation of automatically generated ontologies and section 4 concludes. +Python is a high-level programming language for natural language processing pipeline. +Numerous applications using lexical-semantic databases like WordNet (Miller, 1990) and its non-English counterparts, e.g. EuroWordNet (Vossen, 1997) or CoreNet (Choi and Bae, 2004) demonstrate the utility of semantic resources for natural language processing. +934845-593 453 4535 ---- 3 @#%#$%^&$ @#4@#%^~~~#^ +934845-593 453 4535 ---- 3 @#%#$% + +934845-593 453 4535 ---- 3 @#%#$%^ + *() (*&^ %$#( :'")) + 934845-593 453 4535 ---- 3 @#%#$%,,,,^&$ @#4@#%^~~~#^ + 934845-593 453 4535 ---- 3 @#%#$%^&$ @#4 , , , . . .@#%^~~~#^ + 934845-593 453 4535 ---- 3 @#%#$%^&$ @#4@#%^~~~#^ +Another goal of this essay is to clarify the notion of the term ontology not by defining it once and for all, but to illustrate the correspondences and differences of its usage. \ No newline at end of file