├── .gitattributes ├── .dockerignore ├── publishMain.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── test │ ├── resources │ │ ├── clean │ │ │ ├── hyphen.txt │ │ │ ├── enc_cp1251.txt │ │ │ ├── enc_utf-16.txt │ │ │ ├── spacing1.txt │ │ │ ├── enc2.txt │ │ │ ├── spacing.txt │ │ │ ├── umlauts.txt │ │ │ ├── enc1.txt │ │ │ └── test.rtf │ │ └── tag │ │ │ ├── zheleh1.txt │ │ │ ├── zheleh2.txt │ │ │ ├── unknown.txt │ │ │ └── simple.txt │ ├── java │ │ └── ua │ │ │ └── net │ │ │ └── nlp │ │ │ └── tools │ │ │ └── TagTextWrapperTest.java │ └── groovy │ │ └── ua │ │ └── net │ │ └── nlp │ │ ├── tools │ │ ├── InflectTest.groovy │ │ ├── tag │ │ │ ├── TagTextPerfTest.groovy │ │ │ ├── TagTextSemTest.groovy │ │ │ ├── TagTextVerticalOutputTest.groovy │ │ │ ├── TagTextModLesyaTest.groovy │ │ │ └── TagTextModZhelehTest.groovy │ │ ├── TextUtilTest.groovy │ │ ├── StressTextTest.groovy │ │ └── tokenize │ │ │ └── TokenizeTextTest.groovy │ │ ├── other │ │ ├── EvaluateTextTest.groovy │ │ └── clean │ │ │ ├── CleanTextBigFileTest.groovy │ │ │ ├── SplitLongLinesTest.groovy │ │ │ ├── MarkLanguageTest.groovy │ │ │ ├── CleanLatCyrTest.groovy │ │ │ ├── CleanSpacingTest.groovy │ │ │ └── CleanHyphenTest.groovy │ │ └── bruk │ │ └── ContextTokenTest.groovy ├── main │ ├── resources │ │ ├── ua │ │ │ └── net │ │ │ │ └── nlp │ │ │ │ └── tools │ │ │ │ └── ud │ │ │ │ ├── negatives.txt │ │ │ │ └── vesum-ud.csv │ │ └── schema.xsd │ ├── groovy │ │ └── ua │ │ │ └── net │ │ │ └── nlp │ │ │ ├── tools │ │ │ ├── tag │ │ │ │ ├── TTR.groovy │ │ │ │ ├── TokenInfo.groovy │ │ │ │ ├── VerticalModule.groovy │ │ │ │ ├── ModLesya.groovy │ │ │ │ ├── SemTags.groovy │ │ │ │ ├── TagUnknown.groovy │ │ │ │ └── TagOptions.groovy │ │ │ ├── OutputFormat.groovy │ │ │ ├── logback.xml │ │ │ ├── OptionsBase.groovy │ │ │ ├── tokenize │ │ │ │ └── TokenizeOptions.groovy │ │ │ ├── TokenizeText.groovy │ │ │ └── TagText.groovy │ │ │ ├── other │ │ │ ├── clean │ │ │ │ ├── CleanRequest.groovy │ │ │ │ ├── CleanUtils.groovy │ │ │ │ ├── ApostropheModule.groovy │ │ │ │ ├── OutputTrait.groovy │ │ │ │ ├── SplitLongLines.groovy │ │ │ │ ├── GracModule.groovy │ │ │ │ ├── ControlCharModule.groovy │ │ │ │ ├── CleanOptions.groovy │ │ │ │ ├── LtModule.groovy │ │ │ │ ├── CleanTextNanu.groovy │ │ │ │ └── EncodingModule.groovy │ │ │ ├── Inflect.groovy │ │ │ ├── ExtractText.groovy │ │ │ ├── CleanText.groovy │ │ │ ├── CheckText.groovy │ │ │ └── EvaluateText.groovy │ │ │ └── bruk │ │ │ ├── WordReading.groovy │ │ │ ├── WordContext.groovy │ │ │ └── ContextToken.groovy │ ├── python │ │ ├── README.md │ │ ├── tag_text_recursive.py │ │ ├── tokenize_text.py │ │ └── tag_text.py │ └── java │ │ └── ua │ │ └── net │ │ └── nlp │ │ └── tools │ │ └── TagTextWrapper.java └── example │ └── java │ └── ua │ └── net │ └── nlp │ └── tools │ └── TagTextWrapperRun.java ├── publishStats.sh ├── .gitignore ├── gradle.properties ├── Dockerfile ├── .github └── workflows │ └── gradle.yml ├── doc ├── disambig.md ├── README_disambig.md ├── development.md ├── README_tools.md └── README_other.md ├── README.md └── gradlew.bat /.gitattributes: -------------------------------------------------------------------------------- 1 | gradlew text eol=lf 2 | *.bat text eol=crlf 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | bin/ 3 | tmp 4 | *.sh 5 | *dbg* 6 | stress 7 | build 8 | -------------------------------------------------------------------------------- /publishMain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./gradlew jar publishMainJarPublicationToMavenRepository 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brown-uk/nlp_uk/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/clean/hyphen.txt: -------------------------------------------------------------------------------- 1 | військо- 2 | во-політичний 3 | 4 | теста- 5 | ментів 6 | 7 | Дмитра Со- 8 | лунського 9 | -------------------------------------------------------------------------------- /src/test/resources/clean/enc_cp1251.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brown-uk/nlp_uk/HEAD/src/test/resources/clean/enc_cp1251.txt -------------------------------------------------------------------------------- /src/test/resources/clean/enc_utf-16.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brown-uk/nlp_uk/HEAD/src/test/resources/clean/enc_utf-16.txt -------------------------------------------------------------------------------- /src/test/resources/clean/spacing1.txt: -------------------------------------------------------------------------------- 1 | м и й м е н і в і д с і ч н я п р о ф . В . Д е р ж а в и н : н е п о к а з н а і в ж е л і т н я 2 | -------------------------------------------------------------------------------- /src/test/resources/tag/zheleh1.txt: -------------------------------------------------------------------------------- 1 | Добреє Дїло Діло. 2 | В тонкім флюїдї миготїнь 3 | Купаєть ся земля і море, 4 | Розчинюєть житє і смерть 5 | І родить ся добро та горе. 6 | -------------------------------------------------------------------------------- /publishStats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #./gradlew statsJar publishStatsJarPublicationToMavenLocalRepository 4 | 5 | ./gradlew statsJar publishStatsJarPublicationPublicationToMavenRepository 6 | -------------------------------------------------------------------------------- /src/main/resources/ua/net/nlp/tools/ud/negatives.txt: -------------------------------------------------------------------------------- 1 | не 2 | ні 3 | ані 4 | немає 5 | нема 6 | катма 7 | чортма 8 | чорт-ма 9 | трясцяма 10 | ма 11 | незважаючи 12 | нічичирк 13 | нітелень 14 | -------------------------------------------------------------------------------- /src/test/resources/clean/enc2.txt: -------------------------------------------------------------------------------- 1 | Ñòàí äîñë³äæåííÿ ìàã³÷íî-ñàêðàëüíîãî 2 | ôîëüêëîðó òà éîãî ì³ñöå â æàíðîâî- 3 | ðîäîâ³é ñèñòåì³ óêðà¿íñüêî¿ óñíî¿ 4 | ñëîâåñíîñò³ 5 | 6 | ÎÔ²Ö²ÉÍÀ ²ÍÔÎÐÌÀÖ²ß -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /build/ 3 | .gradle 4 | .classpath 5 | .project 6 | .settings 7 | .launch 8 | _bin 9 | .settings 10 | .gradle 11 | lib/ 12 | tmp/ 13 | *.bak 14 | *.jfr 15 | **/semtags/*.csv 16 | **/*freqs*.txt 17 | stats_dbg* 18 | stress 19 | settings.gradle 20 | -------------------------------------------------------------------------------- /src/test/resources/clean/spacing.txt: -------------------------------------------------------------------------------- 1 | — Г м . . . — м у р к н у в в і н . — Т о й т р е т і й , м а б у т ь , б у в н а й м у д-р і ш и й з в а с у с і х . — У с і т р о є л о т и ш і в о г л у ш л и в о з а р е г о т а л и . 2 | 3 | Л у н а є Д е р ж а в н и й Г і м н У к р а ї н и 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # for LT snapshots we can use https://repo.languagetool.org/artifactory/languagetool-os-snapshot/ 2 | ltBaseVersion=6.7-SNAPSHOT 3 | ltDevVersion=6.7-SNAPSHOT 4 | #morfologik_ukrainian_lt_version=6.5.3 5 | groovyVersion=4.0.28 6 | # nlp_uk version 7 | statsVersion=3.3.9 8 | version=3.3.10 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tag/TTR.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag 2 | 3 | import groovy.transform.Canonical 4 | import groovy.transform.CompileStatic 5 | import groovy.transform.PackageScope 6 | import ua.net.nlp.tools.tag.TagTextCore.TaggedToken 7 | 8 | @CompileStatic 9 | @Canonical 10 | @PackageScope 11 | class TTR { 12 | List tokens 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/OutputFormat.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | public enum OutputFormat { txt, xml, json, vertical, conllu 7 | 8 | String getExtension() { 9 | return this == vertical ? "vertical.txt" 10 | : this == conllu ? "conllu.txt" 11 | : this.name() 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tag/TokenInfo.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag 2 | 3 | import org.languagetool.AnalyzedTokenReadings 4 | 5 | import groovy.transform.CompileStatic 6 | import groovy.transform.PackageScope 7 | 8 | @CompileStatic 9 | @PackageScope 10 | class TokenInfo { 11 | String cleanToken 12 | String cleanToken2 13 | AnalyzedTokenReadings[] tokens 14 | int idx 15 | List taggedTokens 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/ua/net/nlp/tools/TagTextWrapperTest.java: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools; 2 | 3 | import java.io.File; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class TagTextWrapperTest { 8 | 9 | @Test 10 | public void testTagTextWrapper() throws Exception { 11 | TagTextWrapper wrapper = new TagTextWrapper(); 12 | new File("build/tmp").mkdirs(); 13 | wrapper.tag("src/test/resources/tag/simple.txt", "build/tmp/simple.tagged.txt"); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/CleanRequest.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | class CleanRequest { 7 | public String text 8 | public File file 9 | public File outFile 10 | public boolean dosNl 11 | 12 | String getLineBreak() { dosNl ? ".\r\n" : ".\n" } 13 | CleanRequest forText(String text) { 14 | new CleanRequest(text: text, file: file, outFile: outFile, dosNl: dosNl) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/bruk/WordReading.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.bruk 2 | 3 | import groovy.transform.Canonical 4 | import groovy.transform.CompileStatic 5 | 6 | @CompileStatic 7 | @Canonical 8 | class WordReading { 9 | String lemma 10 | String postag 11 | 12 | @CompileStatic 13 | WordReading(String lemma, String postag) { 14 | this.lemma = lemma 15 | this.postag = postag 16 | } 17 | 18 | @CompileStatic 19 | String toString() { 20 | "$lemma\t$postag" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/python/README.md: -------------------------------------------------------------------------------- 1 | # LanguageTool API NLP UK 2 | 3 | 4 | ## У цьому каталозі наданий код на python3, що використовує утиліти для токенізації, лемування та тегування українських текстів 5 | 6 | * Обгортка для тегування: `tag_text.py [-v] [-f] [-o <виходовий файл>] <входовий файл>` 7 | * Обгортка для лексемування: `tokenize_text.py [-v] [-o <виходовий файл>] <входовий файл>` 8 | 9 | 10 | ## Ліцензія 11 | 12 | Проект LanguageTool API NLP UK розповсюджується за умов ліцензії [GPL версії 3](https://www.gnu.org/licenses/gpl.html) 13 | 14 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/InflectTest.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools 2 | 3 | import static org.junit.jupiter.api.Assertions.* 4 | 5 | import org.junit.jupiter.api.Test 6 | 7 | import ua.net.nlp.other.Inflect 8 | 9 | 10 | class InflectTest { 11 | 12 | @Test 13 | void test() { 14 | Inflect inflectText = new Inflect() 15 | def res = inflectText.inflectWord("місто", "noun:inanim:n:v_rod.*", true) 16 | def expected = ["міста"] 17 | assertEquals expected, Arrays.asList(res) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/clean/umlauts.txt: -------------------------------------------------------------------------------- 1 | утóмою косáрською 2 | рипить глухá ворíтниця, 3 | у надвечíр’я цáрина 4 | росóю завагíтніла; 5 | 6 | і прóхолодь галýзиться, 7 | ставки лежать верцáдлами, 8 | троя́ндною пелю́сткою 9 | на землю сонце падає; 10 | 11 | вмостúвся сонний óболок 12 | на чéпчик Богорóдицький, 13 | дощами і корóвою 14 | солодка пахне прóзелень; 15 | 16 | і позіхає вúярок, 17 | туман кошлáтий здóбрився, 18 | а клен в сорóчці вúпраній 19 | збирається на дóсвітки; 20 | 21 | і вікна насупóнились 22 | жалóбою вільхóвою, 23 | мені у душу соняшник 24 | занурив жовту голову… 25 | 26 | військо- 27 | во-політичний 28 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/bruk/WordContext.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.bruk 2 | 3 | import groovy.transform.Canonical 4 | import groovy.transform.CompileStatic 5 | 6 | @CompileStatic 7 | @Canonical 8 | class WordContext { 9 | ContextToken contextToken 10 | int offset 11 | 12 | @CompileStatic 13 | WordContext(ContextToken contexToken, int offset) { 14 | this.contextToken = contexToken 15 | this.offset = offset 16 | } 17 | 18 | @CompileStatic 19 | String toString() { 20 | def offs = offset > 0 ? "+$offset" : "$offset" 21 | "$offs\t$contextToken" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | RUN apt update 4 | RUN apt install -y openjdk-17-jdk-headless unzip 5 | ADD . /opt/nlp_uk 6 | # ADD ./gradle-8.14.3-bin.tgz /opt/ 7 | ADD https://services.gradle.org/distributions/gradle-8.14.3-bin.zip /opt/gradle/ 8 | RUN unzip -q /opt/gradle/gradle-8.14.3-bin.zip -d /opt/gradle && \ 9 | ln -s /opt/gradle/gradle-8.14.3 /opt/gradle/latest 10 | WORKDIR /opt/nlp_uk 11 | RUN rm -f /opt/nlp_uk/src/main/resources/ua/net/nlp/tools/stats/* 12 | #RUN /opt/gradle-8.14.3/bin/gradle --no-daemon --offline -PlocalLib compileGroovy 13 | RUN /opt/gradle/latest/bin/gradle --no-daemon --offline tagText -PlocalLib -Pargs="-g README.md" 14 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/CleanUtils.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | abstract class CleanUtils { 7 | 8 | static String escapeNl(String text) { 9 | text.replace("\r", "\\r").replace("\n", "\\n") 10 | } 11 | 12 | static String getContext(String text, String str) { 13 | int idx = text.indexOf(str) 14 | if( idx == -1 ) 15 | return "" 16 | 17 | int from = Math.max(idx - 10, 0) 18 | int to = Math.min(idx + 10, text.length()-1) 19 | return escapeNl(text.substring(from, to)) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %-5level %logger{36} %msg%n 7 | 8 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/resources/clean/enc1.txt: -------------------------------------------------------------------------------- 1 | ­¾ÊͽÃÉÈÅȼÂǍˆÏs¼ˆÏÊÈĺϯŒŒŒË̾¿ÊÀº¼ºÄÇÙÁ¼žºÇÂź̺ 2 | œºËÂÅÖĺ ªÈƺÇȼÂэ¼ ÉÊȼº¾ÂÌÖ ºÄ̼ÇÍ ÁȼǍÒÇØ ÉÈōÌÂÄÍ  ËÉÊÙÆÈ¼ºÇÍ 3 | ÙÄÍǺÉÊÙÆÄÍÁº»¿ÁɿѿÇÇÙ»¿Áɿĩ¼¾¿ÇÇȈ¡ºÏ¾ÇșªÍˍ ̺čɍ¾ËÂÅ¿ÇÇÙ 4 | ™™ ÉÈÁÂЍÃ Í ÌȾÒÇÖÈÆÍ ˿ʿ¾ÇÖȼÑÇÈÆÍ ˼Ì ¡ÇºÄÈ¼ÈØ ÉȾ”Ø Í ÐÖÈÆÍ Ë̺ˆ 5 | źÄÈÊÈǺЍÙÄÇÙÁÙžºÇÂźªÈƺÇȼÂѺ¼½Ê;ǍÊÍžÈÊȽÂÑÂǍ¡ºÏ¾ÇȈ 6 | ÊÍËÖÄÂüÈÅȾºÊ¼ÌÈÃѺËÉʺ½ÇͼÇ¿̍ÅÖÄÂɍ¾ÇÙÌÂÉÊ¿ËÌÂÀ˼Ȕ™¼Åº¾ÂǺ 7 | ƍÀǺÊȾǍà ºÊ¿Ç  Á»¿Ê¿½Ì Ëͼ¿Ê¿ÇÌ¿Ì ˼Ȕ™ ¾¿ÊÀº¼Â  º  ¼ÂËͼº¼ Éʿ̿ÇÁ™ 8 | Ǻō¾¿ÊË̼ÈͼˍêÍˍ 9 | £Ä×ÐÇ»Œ ÊÄÇ»¹ ¼½È¾¸º¸ ¨ÆÄ¸ÅƺÀϋº  ÈÀÄÉÔ¸ ÂËÈ‹×  ÂÆÈÆÅ¸Î‹×  ċ¾¼½È† 10 | ¾¸ºÅ‹ÉÊÆÉËÅÂÀ 11 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Java CI with Gradle 9 | 10 | on: 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up JDK 17 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: '17' 30 | distribution: 'temurin' 31 | - name: Build with Gradle 32 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 33 | with: 34 | arguments: --info test 35 | -------------------------------------------------------------------------------- /doc/disambig.md: -------------------------------------------------------------------------------- 1 | # Disambiguation in TagText 2 | 3 | There two layers where the disambiguation is happening: 4 | 5 | 1. LanguageTool - this disambiguation is used in LanguageTool rules and thus need to be more precise: 6 | - Simple disard: [the list](https://github.com/languagetool-org/languagetool/blob/master/languagetool-language-modules/uk/src/main/resources/org/languagetool/resource/uk/disambig_remove.txt) (approximately 600 rules) 7 | - Disambiguation based on rules. [the rule file](https://github.com/languagetool-org/languagetool/blob/master/languagetool-language-modules/uk/src/main/resources/org/languagetool/resource/uk/disambiguation.xml) (approximately 470 rules) 8 | - For most complicated disambiguation rules the logic is implemented in Java. [source code](https://github.com/languagetool-org/languagetool/blob/master/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/disambiguation/uk/UkrainianHybridDisambiguator.java) (approximately 10 rules) 9 | 10 | 2. Statistical disambiguation in TagText. This approach is based on statistics collected from [BRUK](https://github.com/brown-uk/corpus) corpus. 11 | -------------------------------------------------------------------------------- /doc/README_disambig.md: -------------------------------------------------------------------------------- 1 | # Disambiguation in TagText 2 | 3 | There two layers where the disambiguation is happening: 4 | 5 | 1. LanguageTool - this disambiguation is used in LanguageTool rules and thus need to be more precise: 6 | - Simple disard: [the list](https://github.com/languagetool-org/languagetool/blob/master/languagetool-language-modules/uk/src/main/resources/org/languagetool/resource/uk/disambig_remove.txt) (approximately 600 rules) 7 | - Disambiguation based on rules. [the rule file](https://github.com/languagetool-org/languagetool/blob/master/languagetool-language-modules/uk/src/main/resources/org/languagetool/resource/uk/disambiguation.xml) (approximately 470 rules) 8 | - For most complicated disambiguation rules the logic is implemented in Java. [source code](https://github.com/languagetool-org/languagetool/blob/master/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/disambiguation/uk/UkrainianHybridDisambiguator.java) (approximately 10 rules) 9 | 10 | 2. Statistical disambiguation in TagText. This approach is based on statistics collected from [BRUK](https://github.com/brown-uk/corpus) corpus. 11 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/ApostropheModule.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import groovy.transform.CompileStatic 4 | import groovy.transform.PackageScope 5 | 6 | @PackageScope 7 | @CompileStatic 8 | class ApostropheModule { 9 | OutputTrait out 10 | LtModule ltModule 11 | 12 | String fixWeirdApostrophes(String t01) { 13 | // fix weird apostrophes 14 | t01 = t01.replaceAll(/(?iu)([бвгґдзкмнпрстфхш])[\"\u201D\u201F\u0022\u2018\u2032\u0313\u0384\u0092´`?*]([єїюя])/, /$1'$2/) // " 15 | t01 = t01.replaceAll(/(?iu)[´`]([аеєиіїоуюя])/, '\u0301$1') 16 | // t0 = t0.replaceAll(/(?iu)([а-яіїєґ'\u2019\u02BC\u2013-]*)[´`]([а-яіїєґ'\u2019\u02BC\u2013-]+)/, { all, w1, w2 17 | // def fix = "$w1'$w2" 18 | // knownWord(fix) ? fix : all 19 | // } 20 | 21 | return t01 22 | } 23 | 24 | String fixSpacedApostrophes(String t01) { 25 | t01 = t01.replaceAll(/([а-яїієґА-ЯІЇЄҐ]+) (['\u2019\u02BC][яєюїЯЄЮЇ][а-яіїєґА-ЯІЇЄҐ]+)/, { all, w1, w2 -> 26 | String fix = "${w1}${w2}" 27 | ltModule.knownWord(fix) ? fix : all 28 | }) 29 | 30 | return t01 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/OptionsBase.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools 2 | 3 | import groovy.transform.CompileStatic 4 | import picocli.CommandLine.Option 5 | 6 | @CompileStatic 7 | public class OptionsBase { 8 | @Option(names = ["-i", "--input"], arity="1", description = ["Input file. Default: stdin"], defaultValue = "-") 9 | public String input 10 | @Option(names = ["-o", "--output"], arity="1", description = ["Output file"]) 11 | public String output 12 | @Option(names = ["-q", "--quiet"], description = ["Less output"]) 13 | public boolean quiet 14 | @Option(names= ["-h", "--help"], usageHelp= true, description= "Show this help message and exit.") 15 | public boolean helpRequested 16 | @Option(names = ["-n", "--outputFormat"], arity="1", description = "Output format: {xml (default), json, txt, vertical}", defaultValue = "xml") 17 | public OutputFormat outputFormat = OutputFormat.xml 18 | public boolean singleThread = true 19 | 20 | @Option(names = ["--splitHypenParts"], description = "If true parts in words like \"якби-то\" etc will be separate tokens.", defaultValue = "true") 21 | public boolean splitHyphenParts = true 22 | 23 | // internal 24 | public String xmlSchema 25 | 26 | public boolean isNoTag() { 27 | return false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/other/EvaluateTextTest.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | 7 | import org.junit.jupiter.api.Disabled 8 | import org.junit.jupiter.api.Test 9 | 10 | 11 | class EvaluateTextTest { 12 | 13 | EvaluateText evalText = new EvaluateText( ) 14 | 15 | @Disabled 16 | @Test 17 | public void test1() { 18 | def errLines = [] 19 | def ruleMatches = evalText.check("десь я тии", true, errLines) 20 | println ruleMatches 21 | assertEquals 2, ruleMatches.size() 22 | assertEquals 7, ruleMatches[1].getFromPos() 23 | assertEquals 10, ruleMatches[1].getToPos() 24 | 25 | evalText.setSentLimit(1) 26 | ruleMatches = evalText.check("десь я тии. Десь тии я", true, errLines) 27 | println ruleMatches 28 | assertEquals 1, ruleMatches.size() 29 | assertEquals 5, ruleMatches[0].getFromPos() 30 | assertEquals 8, ruleMatches[0].getToPos() 31 | } 32 | 33 | @Disabled 34 | @Test 35 | public void test2() { 36 | def text = 37 | """черевичками, лаками й закордонними поїзд""" 38 | 39 | text += (char)0xAD 40 | 41 | def errLines = [] 42 | def ruleMatch = evalText.check(text, true, errLines) 43 | println ruleMatch 44 | assertEquals 1, ruleMatch.size() 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tag/VerticalModule.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag 2 | 3 | import groovy.transform.CompileStatic 4 | import groovy.transform.PackageScope 5 | import ua.net.nlp.tools.tag.TagTextCore.TaggedSentence 6 | 7 | @CompileStatic 8 | @PackageScope 9 | class VerticalModule { 10 | 11 | TagOptions options 12 | 13 | void printSentence(TaggedSentence taggedSent, StringBuilder sb, int sentId) { 14 | if( ! taggedSent.tokens ) //

15 | return 16 | 17 | if( sb.length() > 0 ) { 18 | sb.append("\n") 19 | } 20 | sb.append("\n") 21 | 22 | taggedSent.tokens.eachWithIndex { TTR token, int i -> 23 | def tkn = token.tokens[0] 24 | if( i > 0 && tkn.tags == 'punct' && ! tkn.whitespaceBefore ) { 25 | sb.append('\n') 26 | } 27 | sb.append("${tkn.value}\t${tkn.tags}\t${tkn.lemma}") 28 | if( token.tokens.size() > 1 ) { 29 | token.tokens[1..-1].each { t -> 30 | sb.append(" || ${t.tags} ${t.lemma}") 31 | } 32 | } 33 | if( options.semanticTags ) { 34 | if( tkn.semtags ) { 35 | sb.append("\tsemTags=${tkn.semtags}") 36 | } 37 | else { 38 | sb.append("\t_") 39 | } 40 | } 41 | sb.append('\n') 42 | } 43 | 44 | sb.append("\n") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/OutputTrait.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import java.nio.charset.StandardCharsets 4 | 5 | import org.slf4j.Logger 6 | 7 | import groovy.transform.CompileStatic 8 | import groovy.transform.PackageScope 9 | 10 | @CompileStatic 11 | class OutputTrait { 12 | Logger logger 13 | CleanOptions options 14 | ByteArrayOutputStream byteStream 15 | PrintStream out 16 | 17 | OutputTrait() { 18 | init() 19 | } 20 | 21 | void init() { 22 | byteStream = new ByteArrayOutputStream(1024) 23 | out = new PrintStream(byteStream) 24 | } 25 | 26 | public void setLogger(Logger logger) { 27 | this.logger = logger 28 | } 29 | 30 | synchronized void flushAndPrint() { 31 | out.flush() 32 | System.out.println(byteStream.toString(StandardCharsets.UTF_8)) 33 | init() 34 | } 35 | 36 | void debug(str) { 37 | if( logger ) { 38 | logger.debug str.toString() 39 | } 40 | else { 41 | if( options.debug ) { 42 | out.println "\tDEBUG: $str" 43 | } 44 | } 45 | } 46 | 47 | // making println thread-safe 48 | void println(Object str) { 49 | if( logger ) { 50 | logger.info str.toString() 51 | } 52 | else { 53 | out.println(str) 54 | } 55 | } 56 | 57 | void append(OutputTrait localOut) { 58 | localOut.out.flush() 59 | out.append(localOut.byteStream.toString(StandardCharsets.UTF_8)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/SplitLongLines.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | class SplitLongLines { 7 | int maxLength = 1024 8 | int minLength = 64 9 | 10 | String split(String text) { 11 | def lines = text.readLines() 12 | def lineCnt = lines.size() 13 | if( lineCnt > 2 ) 14 | return null 15 | 16 | if( ! lines.find{ l -> l.length() > maxLength} ) 17 | return null 18 | 19 | if( lineCnt > 1 ) { 20 | def p = ~/^([А-ЯІЇЄҐA-Z ,'0-9«»".:!?()-]+)\h+\n?([А-ЯІЇЄҐ]'?[а-яіїєґ])/ 21 | def m = p.matcher(text) 22 | if( m.find() ) { 23 | text = m.replaceFirst('$1\n\n$2') 24 | } 25 | else if( lines[0] =~ /[а-яіїєґ]\h*$/ ) { 26 | text = text.replaceFirst(/\n/, '\n\n') 27 | } 28 | } 29 | 30 | text = text.replaceAll(/(.{/+minLength+/}[а-яіїєґА-ЯІЇЄҐ0-9]{3}\.) ([А-ЯІЇЄҐ])/, '$1\n$2') 31 | 32 | // println "-------------------------------" 33 | // println text.readLines().take(4).join("\n") 34 | 35 | text 36 | } 37 | 38 | static void main(String[] args) { 39 | def splitLongLines = new SplitLongLines() 40 | 41 | new File(args[0]).eachFileRecurse { f -> 42 | if( ! f.name.toLowerCase().endsWith(".txt") ) 43 | return 44 | 45 | def txt = f.getText('utf-8') 46 | txt = splitLongLines.split(txt) 47 | 48 | if( txt != null ) { 49 | f.setText(txt, 'utf-8') 50 | println "Updated: ${f.path}" 51 | } 52 | } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/other/clean/CleanTextBigFileTest.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other.clean 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | import static org.junit.jupiter.api.Assertions.assertFalse 7 | import static org.junit.jupiter.api.Assertions.assertTrue 8 | import static org.junit.jupiter.api.Assumptions.assumeTrue 9 | 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Disabled 12 | import org.junit.jupiter.api.Test 13 | 14 | import groovy.transform.CompileStatic 15 | import ua.net.nlp.other.clean.CleanOptions 16 | import ua.net.nlp.other.clean.CleanOptions.MarkOption 17 | import ua.net.nlp.other.clean.CleanOptions.ParagraphDelimiter 18 | import ua.net.nlp.other.clean.CleanTextCore 19 | 20 | 21 | @CompileStatic 22 | class CleanTextBigFileTest { 23 | CleanOptions options = new CleanOptions("wordCount": 0) 24 | 25 | CleanTextCore cleanText = new CleanTextCore( options ) 26 | CleanTextCore2 cleanTextCore2 27 | 28 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream() 29 | 30 | @BeforeEach 31 | public void init() { 32 | cleanText.out.out = new PrintStream(outputStream) 33 | cleanTextCore2 = new CleanTextCore2(cleanText.out, options, cleanText.ltModule) 34 | } 35 | 36 | @CompileStatic 37 | String clean(String str) { 38 | str = str.replace('_', '') 39 | cleanText.cleanText(str, null, null, cleanText.out) 40 | } 41 | 42 | @Test 43 | public void testSplitBigFile() { 44 | String w = "abc de\n" 45 | int cnt = (int)(CleanTextCore.CHUNK_LIMIT * 3 / 2 / w.length()) 46 | String text = w.repeat(cnt); 47 | 48 | assertEquals text.hashCode(), clean(text).hashCode() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/other/clean/SplitLongLinesTest.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.Test; 7 | 8 | class SplitLongLinesTest { 9 | SplitLongLines splitLongLines = new SplitLongLines(); 10 | 11 | @BeforeEach 12 | void init() { 13 | splitLongLines.maxLength = 30 14 | splitLongLines.minLength = 20 15 | } 16 | 17 | @Test 18 | void test() { 19 | def txt = "ЛЮБИЙ ДРУЖЕ! Героїв цієї книжки ти часто бачив у мультиках. Так-так. Усі вони постійно потрапляють у скрутне становище\nі от" 20 | def result = splitLongLines.split(txt) 21 | 22 | def expected = 23 | """ЛЮБИЙ ДРУЖЕ! 24 | 25 | Героїв цієї книжки ти часто бачив у мультиках. 26 | Так-так. Усі вони постійно потрапляють у скрутне становище 27 | і от""" 28 | 29 | assertEquals expected, result 30 | 31 | txt = """ГЕНЕЗИС ШПИГУНСЬКОГО РОМАНУ: ВІД Р. КІПЛІНГА ДО Я. ФЛЕМІНГА 32 | Постановка проблеми. У цей час спостерігається стійкий інтерес до вивчення проблеми. Ідеологічного впливу на взаємні""" 33 | result = splitLongLines.split(txt) 34 | 35 | expected = 36 | """ГЕНЕЗИС ШПИГУНСЬКОГО РОМАНУ: ВІД Р. КІПЛІНГА ДО Я. ФЛЕМІНГА 37 | 38 | Постановка проблеми. У цей час спостерігається стійкий інтерес до вивчення проблеми. 39 | Ідеологічного впливу на взаємні""" 40 | 41 | assertEquals expected, result 42 | } 43 | 44 | 45 | @Test 46 | void testNoTouch() { 47 | def txt = "ЛЮБИЙ ДРУЖЕ!\nГероїв цієї книжки ти часто бачив у мультиках.\nУсі вони постійно потрапляють у скрутне становище" 48 | def result = splitLongLines.split(txt) 49 | 50 | assertEquals null, result 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tokenize/TokenizeOptions.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tokenize 2 | 3 | import java.util.regex.Pattern 4 | 5 | import groovy.transform.AutoClone 6 | import groovy.transform.PackageScope 7 | import picocli.CommandLine.Option 8 | import picocli.CommandLine.Parameters 9 | import ua.net.nlp.tools.OptionsBase 10 | import ua.net.nlp.tools.OutputFormat 11 | 12 | @PackageScope 13 | @AutoClone 14 | class TokenizeOptions extends OptionsBase { 15 | @Parameters(index = "0", description = "Input files. Default: stdin", arity="0..") 16 | public List inputFiles 17 | @Option(names = ["-r", "--recursive"], description = "Tag all files recursively in the given directories") 18 | public boolean recursive 19 | @Option(names = ["--list-file"], description = "Read files to tag from the file") 20 | public String listFile 21 | 22 | @Option(names = ["-w", "--words"], description = ["Tokenize into words"]) 23 | public boolean words 24 | @Option(names = ["-u", "--onlyWords"], description = ["Remove non-words (assumes \"-w\")"]) 25 | public boolean onlyWords 26 | @Option(names = ["--preserveWhitespace"], description = "Preserve whitepsace tokens") 27 | public boolean preserveWhitespace 28 | @Option(names = ["-s", "--sentences"], description = "Tokenize into sentences (default)") 29 | public boolean sentences 30 | @Option(names = ["--additionalSentenceSeparator"], description = "Additional pattern to split sentences by (regular expression). Note: this separator will be removed from the output.") 31 | public String additionalSentenceSeparator 32 | public Pattern additionalSentenceSeparatorPattern 33 | 34 | // internal for now 35 | public String newLine = ' ' 36 | 37 | public TokenizeOptions() { 38 | outputFormat = OutputFormat.txt 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | ## Підхоплення змін в *languagetool* в скриптах *nlp_uk* 2 | 3 | Потрібно мати встановленим: 4 | 5 | * git 6 | * java (>=11) 7 | * maven 8 | * gradle 9 | 10 | 11 | ### Як вносити зміни в *LanguageTool* і аналізувати їх в *nlp_uk*: 12 | 13 | УВАГА: ці кроки потрібні лише, якщо ви хочете вносити зміни у LanguageTool (напр. правила зняття омонімії) і тегувати тексти з цими змінами. 14 | 15 | * Витягнути проекти: 16 | * git clone https://github.com/brown-uk/nlp_uk 17 | * git clone https://github.com/languagetool-org/languagetool 18 | * Зібрати та встановити ядро та український модуль LT (в теці languagetool): 19 | * ./build.sh languagetool-core install 20 | * ./build.sh languagetool-language-modules/uk install 21 | * Поставити останню версію languagetool в скрипті, напр. в TagTextCore.groovy: 22 | `@Grab(group='org.languagetool', module='language-uk', version='5.9-SNAPSHOT')` 23 | * Стерти кеш groovy grapes: `rm -rf $HOME/.groovy/grapes/org.languagetool` 24 | * Запустити скрипт TagText.groovy в модулі nlp_uk 25 | 26 | 27 | ### Інтеграція *nlp_uk* в іншому проєкті: 28 | 29 | УВАГА: цей варіант найкращий, якщо ви постійно вносите зміни у LanguageTool (напр. працюєте над правилами зняття омонімії) і перевіряєте результати тегування. 30 | 31 | * Витягнути проєкт (git clone) 32 | * git clone https://github.com/brown-uk/nlp_uk 33 | * Побудувати й встановити пакунок nlp_uk: 34 | * cd nlp_uk 35 | * ./gradlew publish 36 | * В своєму проєкті додати залежність, напр. для gradle: 37 | 38 | repositories { 39 | mavenLocal() 40 | mavenCentral() 41 | } 42 | dependencies { 43 | implementation 'ua.net.nlp:nlp_uk:3.0-SNAPSHOT' 44 | } 45 | 46 | 47 | * Приклад коду Java з використанням тегування через nlp_uk: 48 | * [TagTextWrapperRun.java](../src/example/java/ua/net/nlp/tools/TagTextWrapperRun.java) 49 | 50 | -------------------------------------------------------------------------------- /src/example/java/ua/net/nlp/tools/TagTextWrapperRun.java: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import groovy.lang.GroovyShell; 6 | import groovy.lang.Script; 7 | import groovy.util.GroovyScriptEngine; 8 | 9 | /** 10 | * This class shows how to call TagText.groovy 11 | * If you have multiple text you process in java 12 | * this way you can avoid starting new jdk for each TagText.groovy invocation 13 | */ 14 | public class TagTextWrapperRun { 15 | 16 | public void tag(String filename, String outFilename) throws Exception { 17 | 18 | GroovyShell shell = new GroovyShell(); //this.class.classLoader, new Binding(), config) 19 | Object script = shell.evaluate("ua.net.nlp.tools.TagText.class"); 20 | 21 | Class clazz = (Class) script; 22 | 23 | // Example 2: for multiple files 24 | // Note: for multi-threaded approach create separate tagText instance for each thread 25 | java.lang.reflect.Constructor constructor = clazz.getDeclaredConstructor(); 26 | Object tagText = constructor.newInstance(); 27 | Method parseOptionsMethod = clazz.getDeclaredMethod("parseOptions", String[].class); 28 | Method processMethod = clazz.getDeclaredMethod("process"); 29 | 30 | for(int i=0; i<4; i++) { 31 | Object options = parseOptionsMethod.invoke(null, (Object)new String[]{"-i", filename, "-o", outFilename, "-e", "-x"}); 32 | Method setOptionsMethod = clazz.getDeclaredMethod("setOptions", options.getClass()); 33 | setOptionsMethod.invoke(tagText, options); 34 | processMethod.invoke(tagText); 35 | System.out.println("Done tagging: " + i); 36 | } 37 | } 38 | 39 | public static void main(String[] args) throws Exception { 40 | new TagTextWrapper().tag(args[0], args[1]); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /doc/README_tools.md: -------------------------------------------------------------------------------- 1 | # LanguageTool API NLP UK 2 | 3 | 4 | ## Утиліта аналізу тексту: 5 | `groovy TagText.groovy -i -o ` 6 | 7 | Аналізує текст і записує результат у виходовий файл: 8 | 9 | * розбиває на речення 10 | * розбиває на лексеми 11 | * проставляє теги для лексем 12 | * робить базове зняття омонімії (наразі алгоритм розомонімізації знімає лише близько тисячі найпростіших випадків омонімії) 13 | 14 | 15 | Головні опції: 16 | 17 | - `--semanticTags` - додає семантичні теги; цей тип тегування базується на Українському семантичному лексиконі (УСЛ), дані якого лежать [тут](https://github.com/brown-uk/dict_uk/tree/master/data/sem) 18 | - `--tokenFormat` - формат `...` замість `...` 19 | - `--disambiguate=frequency|context` зняття омонімії за статистикою 20 | 21 | 22 | Для тегування лексем використовується словник української мови з проекту [ВЕСУМ](https://github.com/brown-uk/dict_uk) 23 | 24 | УВАГА: в онлайнових українських текстах дуже часто вживають латинські літери замість українських, різні символи апострофів тощо. 25 | Для якісного аналізу текстів дуже важливо очистити на «нормалізувати» тексти. 26 | Тому майже завжди перед аналізом текстів варто опрацювати їх утилітою [CleanText.groovy](../src/main/groovy/org/nlp_uk/other/CleanText.groovy) 27 | 28 | 29 | 30 | ## Утиліта розбиття тексту: 31 | `groovy TokenizeText.groovy -w -u -i -o ` 32 | 33 | Аналізує текст і записує результат у виходовий файл: 34 | 35 | - розбиває на речення (`-s`) 36 | - розбиває на токени (`-w`) (результати включають пунктуацію тому всі токени розділяються вертикальними рисками) 37 | - розбиває на слова (`-u`) 38 | 39 | 40 | 41 | ## Ліцензія 42 | 43 | Проект LanguageTool API NLP UK розповсюджується за умов ліцензії [GPL версії 3](https://www.gnu.org/licenses/gpl.html) 44 | 45 | -------------------------------------------------------------------------------- /src/main/python/tag_text_recursive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # This script allows to tag Ukrinian text 4 | # by invoking TagText.groovy that uses LanguageTool API 5 | # JDK > 17 and groovy >= 4.0 (http://www.groovy-lang.org) needs to be installed and in the path 6 | # Usage: tag_text.py 7 | 8 | import os 9 | import sys 10 | import subprocess 11 | import threading 12 | import argparse 13 | 14 | 15 | ENCODING='utf-8' 16 | SCRIPT_PATH=os.path.dirname(__file__) + '/../groovy/ua/net/nlp/tools' 17 | 18 | in_txt = None 19 | 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument("-v", help="Verbose", action="store_true") 22 | parser.add_argument("-g", help="Disambiguate and print first token only", action="store_true") 23 | parser.add_argument("dir", default=".", type=str, help="Directory to look for txt files in") 24 | 25 | args = parser.parse_args() 26 | 27 | def print_output(p): 28 | 29 | print("output: ", p.stdout.read().decode(ENCODING)) 30 | 31 | def print_error(p): 32 | 33 | error_txt = p.stderr.read().decode(ENCODING) 34 | if error_txt: 35 | print("stderr: ", error_txt, "\n", file=sys.stderr) 36 | 37 | 38 | # technically only needed on Windows 39 | my_env = os.environ.copy() 40 | my_env["JAVA_TOOL_OPTIONS"] = "-Dfile.encoding=UTF-8" 41 | 42 | 43 | groovy_cmd = 'groovy.bat' if sys.platform == "win32" else 'groovy' 44 | cmd = [groovy_cmd, SCRIPT_PATH + '/TagText.groovy'] 45 | 46 | if args.g: 47 | cmd.append('-g') 48 | cmd.append('-t1') 49 | 50 | cmd.append('-r') 51 | cmd.append(args.dir) 52 | 53 | if args.v: 54 | print('Running: ' + str(cmd)) 55 | else: 56 | cmd.append('-q') 57 | 58 | 59 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env) 60 | 61 | threading.Thread(target=print_output, args=(p,)).start() 62 | threading.Thread(target=print_error, args=(p,)).start() 63 | 64 | 65 | p.stdin.close() 66 | 67 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/tag/TagTextPerfTest.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag 2 | 3 | import static org.junit.jupiter.api.Assertions.* 4 | import static org.junit.jupiter.api.Assumptions.assumeTrue 5 | 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.Test 8 | 9 | import ua.net.nlp.tools.OutputFormat 10 | import ua.net.nlp.tools.tag.TagTextCore.TagResult 11 | 12 | 13 | public class TagTextPerfTest { 14 | 15 | static String text 16 | static tagText = new TagTextCore() 17 | 18 | @BeforeEach 19 | void before() { 20 | assumeTrue(Boolean.getBoolean("performance.tests")) 21 | 22 | if( text == null ) { 23 | text = new File("text1.txt").getText('UTF-8') 24 | 25 | // warm it up and make sure it works 26 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.xml)) 27 | 28 | TagResult tagged = tagText.tagText('текст') 29 | assertTrue tagged.tagged.length() > 0 30 | } 31 | } 32 | 33 | 34 | @Test 35 | void testXmlPerf() { 36 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.xml)) 37 | 38 | bench(16756) 39 | } 40 | 41 | @Test 42 | void testJsonPerf() { 43 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.json)) 44 | 45 | bench(19640) 46 | } 47 | 48 | void bench(int baseline) { 49 | println "baseline: $baseline" 50 | 51 | long tm1 = System.currentTimeMillis() 52 | 53 | TagResult tagged = tagText.tagText(text) 54 | 55 | long tm2 = System.currentTimeMillis() 56 | def tm = tm2-tm1 57 | println "time: $tm, d: ${(tm-baseline)*100/baseline}%" 58 | 59 | assertTrue tagged.tagged.length() > 0 60 | 61 | new File("text1." + tagText.options.outputFormat ).setText(tagged.tagged, 'UTF-8') 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/resources/tag/zheleh2.txt: -------------------------------------------------------------------------------- 1 | За наших часів кобзу вважають за один і той самий струмент, що й бандуру. 2 | Кобзар та бандурист, цї два назвиска не розрізняють більше, однаково називаючи сьпівцїв дум і інших історичнїх пісень на Вкраїнї. 3 | Але-ж подвійне назвище одного й того самого струмента мимохіть надоумляє, шо був час, коли цї два струменти відрізняли ся; з якої-б інак речі надано було два назвища одному струментови? Обидва ті назвища не наші, вони чужоземні. 4 | Одно зі сходу азийського, друге з заходу европейського. 5 | З'явили ся вони на Вкраїнї не рівночасно, для того одно назвище старіще, а друге новіще. 6 | Подвійність назвища струмента п. Фамінцин*, в своїх високоцїнних історичних вислїдах струментів народнїх ясує тими словами. 7 | Він каже: стародавнїй в народї струмент міг з часом замінити ся новим, схожим на старого струмента, але більш обладженим, перфекцийнїшим. 8 | Нове назвище не затерло в памяти народній старого і ось народ, каже, перенїс старе назвище на новий, схожий струмент. 9 | Таким чином новий струмент обняв два назвища, своє власне, та ще й чуже старіще. 10 | В народї слова кобза уживаєть ся частїще і любіще нїж бандура. 11 | І слово „кобзар“, що́ грає на кобзї, лунає якось народнїще. 12 | Не для чого-ж і наш народній поет Тарас Шевченко* надав своїй збірцї творів назвище „Кобзар“, а не „бандурист“, бо назва „Кобзар“ і стариннїйша і любійша ухови українському. 13 | В вісїмдесятих роках минулого столїття український історик Ріґельман* писав про народ український, що між їми, каже, вельми багато буває добрих, ручих музиків. 14 | Кажучи, що вони грають на скрипках, цимбалах, сурмах, гуслях, згадує й про бандуру; а сїльські люди, каже, грають те-ж на скрипках, на кобзї і на сопілках. 15 | Отож постерігати треба, що вишче він розумів міських музиків, що́ грають на бандурі, а сїльські по селах, грали на кобзї. 16 | Значить ся, була якась відміна межи бандурою міською, перфекцийнїйшим струментом, а сїльською кобзою, простїшим струментом. 17 | 18 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/bruk/ContextTokenTest.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.bruk 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals 4 | 5 | import org.junit.jupiter.api.Test 6 | 7 | 8 | public class ContextTokenTest { 9 | 10 | @Test 11 | public void testNumbers() { 12 | def r = ContextToken.normalizeContextString("89", "89", "number") 13 | assertEquals "0", r 14 | 15 | r = ContextToken.normalizeContextString("4", "4", "number") 16 | assertEquals "2", r 17 | 18 | r = ContextToken.normalizeContextString("15", "15", "number") 19 | assertEquals "0", r 20 | 21 | r = ContextToken.normalizeContextString("13", "13", "number") 22 | assertEquals "0", r 23 | 24 | r = ContextToken.normalizeContextString("23", "23", "number") 25 | assertEquals "2", r 26 | 27 | r = ContextToken.normalizeContextString("21", "21", "number") 28 | assertEquals "1", r 29 | 30 | r = ContextToken.normalizeContextString("0,2-0,3", "0,0", "number") 31 | assertEquals "0,0", r 32 | 33 | r = ContextToken.normalizeContextString("1999", "1999", "number") 34 | assertEquals "YY99", r 35 | 36 | r = ContextToken.normalizeContextString("1999-2020", "1999-2020", "number") 37 | assertEquals "YY20", r 38 | 39 | r = ContextToken.normalizeContextString("2999", "3999", "number") 40 | assertEquals "0", r 41 | } 42 | 43 | @Test 44 | public void testSymbols() { 45 | def r = ContextToken.normalizeContextString("один\u2013два", "один-два", "numr") 46 | assertEquals "один-два", r 47 | 48 | r = ContextToken.normalizeContextString("\u2014", "\u2014", "punct") 49 | assertEquals "-", r 50 | 51 | r = ContextToken.normalizeContextString("?..", "?..", "punct") 52 | assertEquals "?", r 53 | } 54 | 55 | @Test 56 | public void testNormalizePostag() { 57 | def t = new ContextToken("аліменти", "аліменти", "noun:inanim:p:v_zna:ns") 58 | assertEquals "noun:inanim:p:v_zna", t.postag 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/python/tokenize_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # This script allows to tokenize Ukrinian text by sentences or words 4 | # by invoking TokenizeText.groovy that uses LanguageTool API 5 | # groovy >= 3.0 (http://www.groovy-lang.org) needs to be installed and in the path 6 | # Usage: tokenize_text.py 7 | 8 | import os 9 | import sys 10 | import subprocess 11 | import threading 12 | import argparse 13 | 14 | 15 | SCRIPT_PATH=os.path.dirname(__file__) + '/../groovy/ua/net/nlp/tools' 16 | ENCODING='utf-8' 17 | 18 | 19 | in_txt = None 20 | 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument("-v", help="Verbose", action="store_true") 23 | parser.add_argument("input_file", default=None, type=str, help="Input file") 24 | parser.add_argument("-o", "--output_file", default=None, type=str, help="Output file") 25 | 26 | args = parser.parse_args() 27 | 28 | 29 | with open(args.input_file, encoding=ENCODING) as a_file: 30 | in_txt = a_file.read() 31 | 32 | 33 | def print_output(p): 34 | print("output: ", p.stdout.read().decode(ENCODING)) 35 | 36 | def print_error(p): 37 | error_txt = p.stderr.read().decode(ENCODING) 38 | if error_txt: 39 | print("stderr: ", error_txt, "\n", file=sys.stderr) 40 | 41 | 42 | # technically only needed on Windows 43 | my_env = os.environ.copy() 44 | my_env["JAVA_TOOL_OPTIONS"] = "-Dfile.encoding=UTF-8" 45 | 46 | 47 | groovy_cmd = 'groovy.bat' if sys.platform == "win32" else 'groovy' 48 | cmd = [groovy_cmd, SCRIPT_PATH + '/TokenizeText.groovy', '-i', '-', '-w', '-u'] 49 | 50 | 51 | if args.output_file: 52 | cmd.append('-o') 53 | cmd.append(args.output_file) 54 | else: 55 | cmd.append('-o') 56 | cmd.append('-') 57 | 58 | if args.v: 59 | print('Running: ' + str(cmd)) 60 | else: 61 | cmd.append('-q') 62 | 63 | 64 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env) 65 | 66 | threading.Thread(target=print_output, args=(p,)).start() 67 | threading.Thread(target=print_error, args=(p,)).start() 68 | 69 | 70 | p.stdin.write(in_txt.encode(ENCODING)) 71 | p.stdin.close() 72 | 73 | -------------------------------------------------------------------------------- /src/test/resources/tag/unknown.txt: -------------------------------------------------------------------------------- 1 | Пр 2 | спр 3 | Пит 4 | ої 5 | кол 6 | ни 7 | ін 8 | Арк 9 | как 10 | неспр 11 | Тз 12 | гл 13 | Ред 14 | Інф 15 | КУп 16 | пр 17 | Укр 18 | што 19 | Мал 20 | вул 21 | ва 22 | заг 23 | ра 24 | іл 25 | меня 26 | ий 27 | ім 28 | #русского 29 | ка 30 | Спр 31 | Івано 32 | ня 33 | со 34 | Тов 35 | #народа 36 | Вип 37 | России 38 | річчя 39 | нет 40 | кор 41 | Цит 42 | Как 43 | Ол 44 | ли 45 | под 46 | из 47 | Ана 48 | Кор 49 | Кн 50 | ст 51 | ред 52 | ій 53 | СОКУРЧАК 54 | Тел 55 | Со 56 | года 57 | Пс 58 | много 59 | Мокр 60 | Са 61 | Спасибо 62 | Россия 63 | су 64 | Зб 65 | аб 66 | есть 67 | время 68 | #Сегодня 69 | их 70 | Зап 71 | іі 72 | мой 73 | За-1 74 | тел 75 | Сл 76 | Поч 77 | Зафод 78 | Известия 79 | сл 80 | Табл 81 | Пейссу 82 | іх 83 | ку 84 | кв 85 | Стор 86 | Ор 87 | #вечером 88 | лі 89 | лет 90 | пе 91 | дес 92 | стр 93 | см-1 94 | Н'Гума 95 | Бі 96 | #небудь 97 | За-2 98 | История 99 | ліття 100 | ду 101 | или 102 | #Кароскі 103 | ки 104 | Дт 105 | Загор 106 | Хорошо 107 | иа 108 | #Александровська 109 | Барбікен 110 | За-0 111 | кто 112 | будет 113 | #только 114 | также 115 | она 116 | #Бобрьонок 117 | пох 118 | тебя 119 | можно 120 | Ном 121 | #Камамурі 122 | ський 123 | Аомаме 124 | #Електроник 125 | відм 126 | дом 127 | хорошо 128 | году 129 | тот 130 | ДОЛЖЕНКОВ 131 | побр 132 | #ського 133 | За-3 134 | они 135 | пов 136 | зап 137 | #племени 138 | куда 139 | #Русского 140 | #русский 141 | ро 142 | Іка 143 | вопрос 144 | Кто 145 | одн 146 | Рябчин 147 | такой 148 | Ро 149 | зв 150 | Ни 151 | ВАШІНҐТОН 152 | Бленкінсоп 153 | ілі 154 | Др 155 | Інеж 156 | очи 157 | ак 158 | Вол 159 | пять 160 | война 161 | марта 162 | зл 163 | Дизма 164 | ру 165 | Нет 166 | Морт 167 | За-4 168 | оо 169 | ЛТчС 170 | Ніх 171 | истории 172 | Ан 173 | род 174 | Нунке 175 | СЕМЕНУХА 176 | Нао 177 | Кросс 178 | Непран 179 | Наб 180 | Кр 181 | сел 182 | #українсько 183 | еще 184 | ії 185 | туда 186 | Ровно 187 | Ерве 188 | Нісс 189 | Ермантьє 190 | #Русский 191 | мнє 192 | Чу 193 | хп 194 | #правительство 195 | пам 196 | #оркестра 197 | вв 198 | др 199 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/Inflect.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other 4 | 5 | import org.languagetool.AnalyzedToken 6 | import org.languagetool.language.Ukrainian 7 | import org.languagetool.synthesis.uk.UkrainianSynthesizer 8 | 9 | @GrabConfig(systemClassLoader=true) 10 | @Grab(group='org.languagetool', module='languagetool-core', version='6.5') 11 | @Grab(group='org.languagetool', module='language-uk', version='6.6') 12 | @Grab(group='ch.qos.logback', module='logback-classic', version='1.4.+') 13 | @Grab(group='info.picocli', module='picocli', version='4.6.+') 14 | 15 | import groovy.util.Eval 16 | 17 | 18 | class Inflect { 19 | @groovy.transform.SourceURI 20 | static SOURCE_URI 21 | static SCRIPT_DIR=new File(SOURCE_URI).parent 22 | 23 | // easy way to include a class without forcing classpath to be set 24 | static textUtils = Eval.me(new File("$SCRIPT_DIR/../tools/TextUtils.groovy").text + "\n new TextUtils()") 25 | 26 | Ukrainian ukLanguage = new Ukrainian() { 27 | @Override 28 | protected synchronized List getPatternRules() { return [] } 29 | } 30 | 31 | UkrainianSynthesizer synth = new UkrainianSynthesizer(ukLanguage); 32 | 33 | 34 | def inflectWord(String word, String tag, boolean regexp) { 35 | def token = new AnalyzedToken("", "", word); 36 | return synth.synthesize(token, tag, regexp); 37 | } 38 | 39 | static void main(String[] argv) { 40 | textUtils.warnOnWindows() 41 | 42 | 43 | if( argv.length != 2 ) { 44 | System.err.println("Використання: Inflect.groovy ") 45 | System.err.println("Повертає всі словоформи зі словника, що відповідають заданій лемі та виразу тегів") 46 | System.err.println("Опис тегів: https://github.com/brown-uk/dict_uk/blob/master/doc/tags.txt") 47 | System.err.println("Напр.: Inflect.groovy місто noun:inanim:n:v_rod") 48 | System.err.println("або: Inflect.groovy місто noun:inanim:n:v_.*") 49 | System.exit(1) 50 | } 51 | 52 | def nlpUk = new Inflect() 53 | 54 | println nlpUk.inflectWord(argv[0], argv[1], true) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /doc/README_other.md: -------------------------------------------------------------------------------- 1 | # Допоміжні утиліти для опрацьовування тестів 2 | 3 | УВАГА: в онлайнових українських текстах дуже часто вживають латинські літери замість українських, різні символи апострофів тощо. 4 | Для якісного аналізу текстів дуже важливо очистити на «нормалізувати» тексти. 5 | Тому майже завжди перед аналізом текстів варто прогнати їх утилітою CleanText.groovy 6 | 7 | 8 | Скрипти в цій теці дозволяють опрацьовувати українські тексти в пакетному режимі: 9 | 10 | * `ExtractText.groovy [ []]` (типово: `pdf/` та `txt/`) 11 | 12 | Витягує тексти з різних форматів у теці і складає в теку . Підтримувані формати: 13 | 14 | * .pdf (вимагає pdftotext) 15 | * .djvu (вимагає djvutxt) 16 | * .fb2 (вимагає unoconv) 17 | * .epub (вимагає ebook-convert) 18 | * .doc, .docx, .rtf (вимагає unoconv) 19 | 20 | Також відсіює файли, в яких визначено два ствопчики в теку `/multicol/` (в майбутньому плануємо об'єднувати стовпчики в один) 21 | 22 | * `CleanText.groovy [] {-wc }` (типово: `txt/`) 23 | 24 | Читає всі файли .txt в теці й намагається знайти ті, що відповідають крітеріям українського тексту 25 | (напр. задана мінімальна кількість українських слів, наразі 80), вивід йде в `/good/` 26 | 27 | Перед застосуванням критерію намагається почистити і виправити типові проблеми: 28 | 29 | * поламані/мішані кодування (лише при читанні з файлу) 30 | * мішанину латиниці та кирилиці 31 | * цифри замість літер (напр. 3а з цифрою замість літери З) 32 | * виправляє «ї» та «й» записані через комбіновані символи 33 | * нетипові апострофи (міняє на прямий — ') 34 | * вилучає м'який дефіс (00AD) 35 | * об'єднує перенесення слів на новий рядок (використовуючи орфографічний словник) 36 | * розділяє зліплені тире/дефіси на початку (перев. прямої мови) 37 | * може позначати абзаци російською 38 | 39 | * `EvaluateText []` (типово: ./) 40 | 41 | Читає всі файли .txt в теці й генерує файли `*.err.txt` для кожного з описом помилок знайдених перевіркою граматики LanguageTool (деякі правила вимкнені) 42 | 43 | Також генерує файл `/err/ratings.txt` зі статистикою для кожного файлу 44 | -------------------------------------------------------------------------------- /src/test/resources/tag/simple.txt: -------------------------------------------------------------------------------- 1 | ДОВГИЙ ШЛЯХ ДО СВОБОДИ 2 | Автобіографія Нельсона Мандели 3 | 4 | 5 | 6 | Від перекладача 7 | 8 | У перекладі загалом дотримано нині чинного правопису й враховано кілька рекомендацій правописного характеру, як-от написання разом слів на прес- (наприклад, пресконференція) згідно з ухвалою погоджувальної комісії Інституту української мови НАНУ (Українська мова, 2010, №1, с. 148-149), вживання форми магнет (паралельно з магніт) згідно з ухвалою Бюро Відділення фізики та астрономії НАНУ про написання термінів, пов’язаних із поняттям магнетизму (лист №59 від 19.03.2009), проєкт (за аналогією зі словами, що походять від того самого латинського кореня). У родовому відмінку однини назви міст на -бург, а також Лондон дістали закінчення -у згідно з «Правописним словником» Г. Голоскевича. 9 | Текст автобіографії Мандели рясніє власними назвами, що походять з африкаанс, голландської та африканських мов. З огляду на брак узгоджених принципів віддання таких назв українською було вирішено спиратися, по змозі, на автентичну вимову й транскрипцію знаками Міжнародного фонетичного алфавіту. Наприклад, прізвище південноафриканського прем’єр-міністра віддано як Фервурд (Verwoerd), а ім’я Мандели, дане йому при народженні, — Холілала попри те, що латинкою пишуть Rolihlahla, а мовою коса воно звучить, на українське вухо, майже як [холісаса]. Як часто буває, в низці випадків можна обстоювати дещо відмінні написання тих самих слів. Вже давно наспіла потреба узгодити правила відтворення чужомовних назв, охопивши якомога ширше коло мов. 10 | Варто також згадати про незвичні конструкції зі словами заборона й забороняти: хтось потрапив під заборону, заборонені активісти тощо. Тут ідеться про специфічний винахід каральної системи Південної Африки часів апартеїду, коли влада офіційно позбавляла людину низки свобод: вона не мала права залишати межі одного округу, зустрічатися більше ніж із однією людиною, бувати в різних громадських місцях, обіймати посади в організаціях, а преса не могла цитувати її слів. За словами Нельсона Мандели, заборона була подібна до «ув’язнення без ґрат». 11 | Чернетковий варіант перекладу критично прочитали рідні й колеги перекладача, за що їм велика подяка. Спільними зусиллями текст значно покращено, а всі хиби, що залишилися, — на совісті перекладача. 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/clean/test.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\deff3\adeflang1025 2 | {\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\froman\fprq2\fcharset2 Symbol;}{\f2\fswiss\fprq2\fcharset0 Arial;}{\f3\froman\fprq2\fcharset0 Liberation Serif{\*\falt Times New Roman};}{\f4\fswiss\fprq2\fcharset0 Liberation Sans{\*\falt Arial};}{\f5\fnil\fprq2\fcharset0 Noto Sans CJK SC;}{\f6\fnil\fprq2\fcharset0 Lohit Devanagari;}{\f7\fnil\fprq0\fcharset128 Lohit Devanagari;}} 3 | {\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;} 4 | {\stylesheet{\s0\snext0\rtlch\af6\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af5\langfe2052 Normal;} 5 | {\s15\sbasedon0\snext16\rtlch\af6\afs28 \ltrch\hich\af4\loch\sb240\sa120\keepn\f4\fs28\dbch\af5 Heading;} 6 | {\s16\sbasedon0\snext16\loch\sl276\slmult1\sb0\sa140 Text Body;} 7 | {\s17\sbasedon16\snext17\rtlch\af7 \ltrch\loch\sl276\slmult1\sb0\sa140 List;} 8 | {\s18\sbasedon0\snext18\rtlch\af7\afs24\ai \ltrch\loch\sb120\sa120\noline\fs24\i Caption;} 9 | {\s19\sbasedon0\snext19\rtlch\af7 \ltrch\loch\noline Index;} 10 | }{\*\generator LibreOffice/7.4.5.1$Linux_X86_64 LibreOffice_project/40$Build-1}{\info{\creatim\yr2023\mo2\dy10\hr14\min30}{\revtim\yr2023\mo2\dy10\hr14\min30}{\printim\yr0\mo0\dy0\hr0\min0}}{\*\userprops}\deftab709 11 | \hyphauto1\viewscale100 12 | {\*\pgdsctbl 13 | {\pgdsc0\pgdscuse451\pgwsxn12240\pghsxn15840\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\pgdscnxt0 Default Page Style;}} 14 | \formshade\paperh15840\paperw12240\margl1134\margr1134\margt1134\margb1134\sectd\sbknone\pgndec\sftnnar\saftnnrlc\sectunlocked1\pgwsxn12240\pghsxn15840\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\ftnbj\ftnstart1\ftnrstcont\ftnnar\aenddoc\aftnrstcont\aftnstart1\aftnnrlc 15 | {\*\ftnsep\chftnsep}\pgndec\pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af5\langfe2052\ql\ltrpar{\loch 16 | \u1058\'3f\u1077\'3f\u1089\'3f\u1090\'3f} 17 | \par \pard\plain \s0\rtlch\af6\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af5\langfe2052\ql\ltrpar\loch 18 | 19 | \par } -------------------------------------------------------------------------------- /src/main/python/tag_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # This script allows to tag Ukrinian text 4 | # by invoking TagText.groovy that uses LanguageTool API 5 | # JDK > 17 and groovy >= 4.0 (http://www.groovy-lang.org) needs to be installed and in the path 6 | # Usage: tag_text.py 7 | 8 | import os 9 | import sys 10 | import subprocess 11 | import threading 12 | import argparse 13 | 14 | 15 | ENCODING='utf-8' 16 | SCRIPT_PATH=os.path.dirname(__file__) + '/../groovy/ua/net/nlp/tools' 17 | 18 | in_txt = None 19 | 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument("-v", help="Verbose", action="store_true") 22 | parser.add_argument("-g", help="Disambiguate and print first token only", action="store_true") 23 | parser.add_argument("input_file", default=None, type=str, help="Input file") 24 | parser.add_argument("-o", "--output_file", default=None, type=str, help="Output file") 25 | #parser.add_argument("-gr", "--disambiguationRate", default=None, type=str, help="Show a disambiguated token ratings") 26 | #parser.add_argument("-t", "--tokenFormat", default=None, type=str, help="Use format (instead of )") 27 | #parser.add_argument("-t1", "--singleTokenOnly", default=None, type=str, help="rint only one token per reading (-g is recommended with this option)") 28 | 29 | args = parser.parse_args() 30 | 31 | 32 | with open(args.input_file, encoding=ENCODING) as a_file: 33 | in_txt = a_file.read() 34 | 35 | 36 | def print_output(p): 37 | 38 | print("output: ", p.stdout.read().decode(ENCODING)) 39 | 40 | def print_error(p): 41 | 42 | error_txt = p.stderr.read().decode(ENCODING) 43 | if error_txt: 44 | print("stderr: ", error_txt, "\n", file=sys.stderr) 45 | 46 | 47 | # technically only needed on Windows 48 | my_env = os.environ.copy() 49 | my_env["JAVA_TOOL_OPTIONS"] = "-Dfile.encoding=UTF-8" 50 | 51 | 52 | groovy_cmd = 'groovy.bat' if sys.platform == "win32" else 'groovy' 53 | cmd = [groovy_cmd, SCRIPT_PATH + '/TagText.groovy', '-i', '-'] 54 | 55 | if args.g: 56 | cmd.append('-g') 57 | cmd.append('-t1') 58 | 59 | if args.output_file: 60 | cmd.append('-o') 61 | cmd.append(args.output_file) 62 | else: 63 | cmd.append('-o') 64 | cmd.append('-') 65 | 66 | if args.v: 67 | print('Running: ' + str(cmd)) 68 | else: 69 | cmd.append('-q') 70 | 71 | 72 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env) 73 | 74 | threading.Thread(target=print_output, args=(p,)).start() 75 | threading.Thread(target=print_error, args=(p,)).start() 76 | 77 | 78 | p.stdin.write(in_txt.encode(ENCODING)) 79 | p.stdin.close() 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LanguageTool API NLP UK 2 | 3 | This is a project to demonstrate NLP API from LanguageTool for Ukrainian language. 4 | 5 | Це — проект демонстрації API для обробляння природної мови в LanguageTool для української мови. 6 | 7 | Використовує мову [groovy](http://www.groovy-lang.org/), засоби для токенізації та тегування також мають скрипти-обгортки для python3 та java. 8 | Рекомендована версія groovy - 4.0.22 або новіше. 9 | 10 | Для запуску скриптів потрібно встановити мову [groovy](http://www.groovy-lang.org/) 11 | 12 | УВАГА: при першому запуску потрібно мережеве з'єднання, щоб скрипти могли звантажити потрібні модулі 13 | 14 | ПРИМІТКА: файли gradle потрібен лише для розробників 15 | 16 | Основні скрити аналізу текстів знаходяться в каталозі [src/main/groovy/ua/net/nlp/tools](src/main/groovy/ua/net/nlp/tools) 17 | 18 | Тегувальник підтримує розмітку UD (Universal Dependencies). 19 | 20 | ## Використання 21 | 22 | 23 | ### Утиліта розбиття тексту: TokenizeText.groovy 24 | ### Утиліта аналізу тексту: TagText.groovy 25 | 26 | [докладніше про утиліти аналізу](doc/README_tools.md) 27 | 28 | 29 | ## Допоміжні утиліти: 30 | [докладніше про допоміжні утиліти](doc/README_other.md) 31 | 32 | 33 | ## Використання (найпростіший шлях) 34 | 35 | Встановити JDK 17 (https://www.oracle.com/java/technologies/downloads/#jdk17-windows) 36 | 37 | ### Чистити файл 38 | UNIX:
39 | `./gradlew -q cleanText -Pargs="-i <мій-файл.txt>"`
40 | Windows:
41 | `gradlew.bat -q cleanText -Pargs="-i <мій-файл.txt>"` 42 | 43 | Буде створено файл <мій-файл.good.txt> в якому виправлено знайдені проблеми зі словами. 44 | 45 | ### Тегувати файл 46 | UNIX:
47 | `./gradlew -q tagText -Pargs="-i <мій-файл.txt> -su"`
48 | Windows:
49 | `gradlew.bat -q tagText -Pargs="-i <мій-файл.txt> -su"` 50 | 51 | Буде створено файл <мій-файл.tagged.xml>. Прапорець "-su" генерує файл невідомих слів. 52 | 53 | ## Робота офлайн (без доступу до інтернету) 54 | 55 | Локально: 56 | `./gradlew copyRuntimeLibs` 57 | це стягне потрібні залежності у build/lib 58 | потім скопіювати все на потрібну систему і запускати: 59 | `./gradlew --offline tagText -PlocalLib -Pargs="-g "` 60 | 61 | ## Використовувані програмні засоби 62 | 63 | Для аналізу текстів використовується український модуль [LanguageTool](https://languagetool.org) 64 | 65 | Для тегування лексем використовується словник української мови з проекту [ВЕСУМ](https://github.com/brown-uk/dict_uk) 66 | 67 | 68 | ## Ліцензія 69 | 70 | Проект LanguageTool API NLP UK розповсюджується за умов ліцензії [GPL версії 3](https://www.gnu.org/licenses/gpl.html) 71 | 72 | Copyright (c) 2022 Андрій Рисін (arysin@gmail.com) 73 | -------------------------------------------------------------------------------- /src/main/resources/schema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/ExtractText.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other 4 | 5 | // this script tries to extract text from multiple document formats 6 | 7 | //package ua.net.nlp.other 8 | 9 | 10 | def srcDir = args.length > 0 ? args[0] : "pdf" 11 | def targetDir = args.length > 1 ? args[1] : "txt" 12 | 13 | 14 | println "Source dir: $srcDir, target dir: $targetDir" 15 | 16 | 17 | exec("rm $targetDir/*.txt") 18 | exec("rm $targetDir/multicol/*.txt") 19 | 20 | 21 | new File(srcDir).eachFile { file -> 22 | 23 | if( file.isDirectory() ) 24 | return 25 | 26 | def filename = file.name 27 | def lowercaseName = filename.toLowerCase() 28 | 29 | def txtFilename = filename.replaceFirst(/\.[^.]+$/, ".txt") 30 | 31 | 32 | def convertCmd 33 | 34 | if( lowercaseName.endsWith(".txt") ) { 35 | if( srcDir != targetDir ) { 36 | convertCmd = "cp $srcDir/$filename ." 37 | } 38 | //else noop 39 | } 40 | else if( lowercaseName.endsWith(".pdf") ) { 41 | convertCmd = "pdftotext -layout -nopgbrk -enc UTF-8 $srcDir/$filename $txtFilename" 42 | } 43 | else if( lowercaseName.endsWith(".djvu") ) { 44 | convertCmd = "djvutxt $srcDir/$filename $txtFilename" 45 | } 46 | else if( lowercaseName.endsWith(".fb2") ) { 47 | convertCmd = "unoconv -f txt -o $txtFilename $srcDir/$filename" 48 | } 49 | else if( lowercaseName.endsWith(".epub") ) { 50 | convertCmd = "ebook-convert $srcDir/$filename $txtFilename" 51 | } 52 | else if( lowercaseName.endsWith(".doc") || lowercaseName.endsWith(".docx") || lowercaseName.endsWith(".rtf") ) { 53 | convertCmd = "unoconv -f txt -o $txtFilename $srcDir/$filename" 54 | } 55 | else { 56 | System.err.println("Skipping not supported extension: $filename") 57 | return 58 | } 59 | 60 | println "Extracting from $filename..." 61 | 62 | 63 | if( convertCmd ) 64 | if( exec(convertCmd) ) 65 | return 66 | 67 | 68 | if( new File(txtFilename).text =~ /[‑-] +[а-яіїєґ]/ ) { 69 | println "\tmultiple col" 70 | exec("mv $txtFilename $targetDir/multicol") 71 | } 72 | else { 73 | println "\tsingle col" 74 | exec("mv $txtFilename $targetDir/") 75 | } 76 | 77 | } 78 | 79 | def exec(String cmd) { 80 | // println "Executing: $cmd" 81 | def proc = cmd.execute() 82 | 83 | def b = new StringBuffer() 84 | proc.consumeProcessErrorStream(b) 85 | 86 | def exit = proc.waitFor() 87 | 88 | try { 89 | if( proc.text ) 90 | print proc.text 91 | } 92 | catch(Exception e) { 93 | System.err.println("Failed to read stream: " + e.getMessage()) 94 | } 95 | 96 | if( b ) 97 | print b.toString() 98 | 99 | return exit 100 | } 101 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/TextUtilTest.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | package ua.net.nlp.tools 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | 10 | import ua.net.nlp.tools.tag.TagOptions 11 | import ua.net.nlp.tools.tag.TagTextCore 12 | import ua.net.nlp.tools.tag.TagTextCore.TagResult 13 | 14 | 15 | class TextUtilTest { 16 | TagOptions options = new TagOptions( 17 | input: "-", 18 | unknownStats: true 19 | ) 20 | 21 | TextUtils tagUtils = new TextUtils() 22 | TagTextCore tagText = new TagTextCore() 23 | 24 | @BeforeEach 25 | void before() { 26 | tagText.setOptions(options) 27 | } 28 | 29 | 30 | @Test 31 | public void testParallel() { 32 | def ret = [] 33 | 34 | int cores = Runtime.getRuntime().availableProcessors() 35 | System.err.println("$cores cores detected") 36 | if( cores < 2 ) { 37 | System.err.println("this test won't test parallel tagging") 38 | } 39 | 40 | def byteOS = new ByteArrayOutputStream(10000) 41 | def out = new PrintStream(byteOS) 42 | def count = 16 43 | def input = new ByteArrayInputStream(("борода кікука.\n\n".repeat(count) + "А").getBytes("UTF-8")) 44 | 45 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.txt, unknownStats: true)) 46 | 47 | tagUtils.processFileParallel(input, out, options, 48 | { buffer -> 49 | return tagText.tagText(buffer) 50 | }, 51 | cores, 52 | { TagResult result -> 53 | tagText.stats.add(result.stats) 54 | }) 55 | 56 | def expected = "борода[борода/noun:inanim:f:v_naz] кікука[кікука/unknown].[./punct]\n".repeat(count) + "А[а/conj:coord,а/intj,а/part]\n\n" 57 | assertEquals(expected, byteOS.toString("UTF-8") + "\n") 58 | assertEquals(new HashMap<>(["кікука": count]), new HashMap<>(tagText.stats.unknownMap)) 59 | } 60 | 61 | @Test 62 | public void testSingleThread() { 63 | def ret = [] 64 | 65 | def byteOS = new ByteArrayOutputStream(10000) 66 | def out = new PrintStream(byteOS) 67 | def count = 8 68 | def input = new ByteArrayInputStream(("борода кікука.\n\n".repeat(count) + "А").getBytes("UTF-8")) 69 | 70 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.txt, unknownStats: true)) 71 | 72 | tagUtils.processFile(input, out, options, 73 | { buffer -> 74 | return tagText.tagText(buffer) 75 | }, 76 | { TagResult result -> 77 | tagText.stats.add(result.stats) 78 | }) 79 | 80 | String expected = "борода[борода/noun:inanim:f:v_naz] кікука[кікука/unknown].[./punct]\n".repeat(count) + "А[а/conj:coord,а/intj,а/part]\n\n" 81 | assertEquals(expected, byteOS.toString("UTF-8") +"\n") 82 | assertEquals(new HashMap<>(["кікука": count]), new HashMap<>(tagText.stats.unknownMap)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/GracModule.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import java.util.regex.Pattern 4 | 5 | import groovy.transform.CompileStatic 6 | import groovy.transform.PackageScope 7 | 8 | @PackageScope 9 | @CompileStatic 10 | class GracModule { 11 | private static final Pattern BROKEN_I = Pattern.compile(/\b([А-ЯІЇЄҐ]?[а-яіїєґ'\u2019\u02bc-]+) і ([а-яіїєґ'\u2019\u02bc-]+)\b/, Pattern.UNICODE_CHARACTER_CLASS) 12 | 13 | OutputTrait out 14 | LtModule ltModule 15 | 16 | String fix(String text) { 17 | 18 | // 1-time GRAC 19 | // t10 = t10.replaceAll(/(чоло|Людо)[ -](в[іі]к)/, '$1$2') 20 | 21 | // TODO: remove later - temp fix 22 | text = text.replace(/Крим SOS/, 'КримSOS') 23 | text = text.replace(/Євромайдан SOS/, 'ЄвромайданSOS') 24 | text = text.replace(/Чорнобиль Art/, 'ЧорнобильArt') 25 | text = text.replace(/Золотоноша City/, 'ЗолотоношаCity') 26 | text = text.replace(/Умань News/, 'УманьNews') 27 | text = text.replace('Хутро OFF', 'ХутроOFF') 28 | text = text.replaceAll(/(?iu)(Армія) (Inform)/, '$1$2') 29 | text = text.replaceAll(/(?iu)(Гоголь|Пуленк) (FEST|Train)/, '$1$2') 30 | text = text.replaceAll(/([a-z])(Переглянути)/, '$1$2') 31 | text = text.replace('Ник Life', 'НикLife') 32 | 33 | text = text.replace("пагороджен", "нагороджен") 34 | text = text.replace("голсоування", "голосування") 35 | text = text.replace("річ-чя", "річчя") 36 | text = text.replaceAll(/(Івано)\h+(Франківськ)/, '$1-$2') 37 | text = text.replaceAll(/(Івано)\h+([\u2013\u2011-])\h+(Франківськ)/, '$1$2$3') 38 | 39 | text = text.replaceAll(/([дД]епутата?)([А-ЯІЇЄҐ])/, '$1 $2') 40 | text = text.replace("каналуМнения", "каналу Мнения") 41 | text = text.replace("номеріжурналу", "номері журналу") 42 | text = text.replace(" зорутут", " зору тут") 43 | 44 | // text = t01.replaceAll(/йдетсь?я/, 'йдетьсья') 45 | // байдужосте, відповідальносте, відсутносте, досконалосте, діяльносте, промисловосте 46 | // > 640 ліття 47 | 48 | return text 49 | } 50 | 51 | 52 | String firtka(String text) { 53 | 54 | def m2 = BROKEN_I.matcher(text) 55 | text = m2.replaceAll{ mr -> 56 | def w1 = mr.group(1) 57 | def w2 = mr.group(2) 58 | 59 | if( ( ltModule.knownWord(w1) && ! (w1 ==~ /[гґдзклмнпрстфхцчшщ]/) ) 60 | || ltModule.knownWord(w2) && ! (w2 ==~ /[гґдзклмнпрстфхцчшщ]/) ) 61 | return mr.group(0) 62 | 63 | def newWord = "${w1}і${w2}" 64 | if( ltModule.knownWord(newWord) ) 65 | return newWord 66 | 67 | return mr.group(0) 68 | } 69 | 70 | return text 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/StressTextTest.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | package ua.net.nlp.tools 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | import static org.junit.jupiter.api.Assumptions.assumeTrue 7 | 8 | import org.junit.jupiter.api.AfterEach 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | 12 | import ua.net.nlp.other.StressText 13 | import ua.net.nlp.other.StressText.StressResult 14 | 15 | 16 | class StressTextTest { 17 | final boolean enabled = false 18 | 19 | def options = [ : ] 20 | 21 | static StressText stressText 22 | StressResult result 23 | 24 | @BeforeEach 25 | void before() { 26 | assumeTrue(enabled) 27 | if( stressText == null ) { 28 | stressText = new StressText() 29 | } 30 | stressText.setOptions(options) 31 | } 32 | 33 | @AfterEach 34 | void after() { 35 | assumeTrue(enabled) 36 | println "Unknown: " + result.stats.unknownCnt 37 | println "Homonym: " + result.stats.homonymCnt 38 | } 39 | 40 | 41 | @Test 42 | public void testStress() { 43 | def expected = "Аби́ те про́сте́ сло́во загуло́ розмі́щеним якнайщирі́шим мабобом." 44 | def text = "Аби те просте слово загуло розміщеним якнайщирішим мабобом." 45 | // def text = TagText.stripAccent(expected + ambig).trim() 46 | 47 | result = stressText.stressText(text) 48 | assertEquals expected.trim(), result.tagged 49 | assertEquals 1, result.stats.unknownCnt 50 | assertEquals 0, result.stats.homonymCnt 51 | } 52 | 53 | @Test 54 | public void testStressDualTags() { 55 | def expected = "аналізу́є/аналізує абонува́ти ага́кало/агакало докла́дніше" 56 | def text = "аналізує абонувати агакало докладніше" 57 | 58 | result = stressText.stressText(text) 59 | assertEquals expected.trim(), result.tagged 60 | assertEquals 2, result.stats.unknownCnt 61 | assertEquals 0, result.stats.homonymCnt 62 | } 63 | 64 | @Test 65 | public void testStressProp() { 66 | def expected = "Байден з Берлі́на до Ки́єва" 67 | def text = "Байден з Берліна до Києва" 68 | 69 | result = stressText.stressText(text) 70 | assertEquals expected.trim(), result.tagged 71 | assertEquals 1, result.stats.unknownCnt 72 | assertEquals 0, result.stats.homonymCnt 73 | } 74 | 75 | @Test 76 | public void testStress2() { 77 | def expected = "Готу́є ре́чення мене́/ме́не біржовика́ архівника" 78 | def text = "Готує речення мене біржовика архівника" 79 | 80 | result = stressText.stressText(text) 81 | assertEquals expected.trim(), result.tagged 82 | } 83 | 84 | @Test 85 | public void testStressHomonym() { 86 | def expected = "Я пасу́/па́су по́ки ове́ць, ні́чим/нічи́м не гі́рше за будь-кого́." 87 | def text = "Я пасу поки овець, нічим не гірше за будь-кого." 88 | // def text = TagText.stripAccent(expected + ambig).trim() 89 | 90 | result = stressText.stressText(text) 91 | assertEquals expected.trim(), result.tagged 92 | } 93 | } 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/ControlCharModule.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import java.util.function.Function 4 | import java.util.regex.MatchResult 5 | import java.util.regex.Matcher 6 | import java.util.regex.Pattern 7 | 8 | import org.apache.commons.lang3.StringUtils 9 | 10 | import groovy.transform.CompileDynamic 11 | import groovy.transform.CompileStatic 12 | import groovy.transform.PackageScope 13 | 14 | @PackageScope 15 | @CompileStatic 16 | class ControlCharModule { 17 | 18 | private final Pattern CONTROL_CHAR_PATTERN_R = Pattern.compile(/[\u0000-\u0008\u000B-\u0012\u0014-\u001F\u0A0D]/, Pattern.CASE_INSENSITIVE|Pattern.UNICODE_CASE) 19 | private final Pattern CONTROL_CHAR_PATTERN_W = Pattern.compile(/([а-яіїєґ'\u2019\u02BC\u0301-]+)[\u0000-\u0008\u000B-\u0012\u0014-\u001F\u0A0D]\n?([а-яіїєґ'\u2019\u02BC\u0301-]+)/, Pattern.MULTILINE|Pattern.CASE_INSENSITIVE|Pattern.UNICODE_CASE) 20 | private final Pattern PRIVATE_BLOCK_CHARS = Pattern.compile(/[\uE000-\uF8FF]/) 21 | 22 | OutputTrait out 23 | LtModule ltModule 24 | 25 | String removeControlChars(String text) { 26 | text = text.replace("\u200E", "") 27 | 28 | text = remove001DHyphens(text) 29 | 30 | def m = text =~ CONTROL_CHAR_PATTERN_W 31 | 32 | if( m ) { 33 | out.println "\treplacing control characters inside words" 34 | text = m.replaceAll{ mr -> 35 | def fix = "${mr.group(1)}-${mr.group(2)}" 36 | if( ltModule.knownWord(fix) ) return fix 37 | fix = "${mr.group(1)}${mr.group(2)}" 38 | if( ltModule.knownWord(fix) ) return fix 39 | return mr.group(0) 40 | } 41 | } 42 | 43 | def m2 = text =~ CONTROL_CHAR_PATTERN_R 44 | 45 | if( m2 ) { 46 | out.println "\tremoving standalone control characters" 47 | text = m2.replaceAll('') 48 | } 49 | 50 | def ctrlM = CONTROL_CHAR_PATTERN_R.matcher(text) 51 | if( ctrlM ) { 52 | def ctx = CleanUtils.getContext(text, ctrlM.group(0)) 53 | out.println "\tWARNING: still control characters present: $ctx" 54 | } 55 | 56 | def privM = PRIVATE_BLOCK_CHARS.matcher(text) 57 | if( privM ) { 58 | def ctx = CleanUtils.getContext(text, privM.group(0)) 59 | out.println "\tWARNING: private area characters - needs manual analysis: $ctx" 60 | } 61 | 62 | return text 63 | } 64 | 65 | private String remove001DHyphens(String text) { 66 | if( text.contains("\u001D") ) { 67 | out.println "\tremoving U+001D" 68 | // out.println "\treplacing U+001D with U+00AC" 69 | // text = text.replace("\u001D", "\u00AC") 70 | text = text.replaceAll(/(?iu)([а-яіїєґ])\u001D\n(\h*)([а-яіїєґ'\u2019\u20BC-]+)/, '$1$3\n$2') 71 | text = text.replaceAll(/(?iu)([а-яіїєґ])\u001D([а-яіїєґ])/, '$1-$2') 72 | } 73 | return text 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/ua/net/nlp/tools/TagTextWrapper.java: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import groovy.lang.GroovyShell; 6 | //import groovy.lang.Script; 7 | //import groovy.util.GroovyScriptEngine; 8 | 9 | /** 10 | * This class shows how to call TagText.groovy 11 | * If you have multiple text you process in java 12 | * this way you can avoid starting new jdk for each TagText.groovy invocation 13 | * 14 | * compile: javac -cp ".:$GROOVY_PATH/lib/*" TagTextWrapper.java 15 | * run: java -cp ".:$GROOVY_PATH/lib/*" TagTextWrapper -i <filename> 16 | * 17 | * Note: both TagText.groovy and TextUtils.groovy must be in current directory 18 | * Note: you may want to comment out @Grab...logback-classic in TagText.groovy if you have your own logging framework 19 | */ 20 | public class TagTextWrapper { 21 | // private static final String SCRIPT_DIR = "src/main/groovy/ua/net/nlp/tools"; 22 | // private static final String SCRIPT_DIR = "classpath://src/main/groovy/ua/net/nlp/tools"; 23 | 24 | /** 25 | * Tags the file 26 | * @param filename The name of the file to tag 27 | * @param outFilename Output file name 28 | * @throws Exception if problems happen 29 | */ 30 | public void tag(String filename, String outFilename) throws Exception { 31 | 32 | // Binding binding = new Binding(); 33 | // GroovyScriptEngine engine = new GroovyScriptEngine(SCRIPT_DIR); 34 | 35 | GroovyShell shell = new GroovyShell(); //this.class.classLoader, new Binding(), config) 36 | Object script = shell.evaluate("ua.net.nlp.tools.tag.TagTextCore.class"); 37 | // engine.loadScriptByName("TextUtils.groovy"); 38 | // Class clazz = engine.loadScriptByName("TagText.groovy"); 39 | 40 | Class clazz = (Class) script; 41 | 42 | // Example 1: for simple 1-time invocation 43 | // clazz.getDeclaredMethod("main", String[].class).invoke(null, (Object)args); 44 | 45 | // Example 2: for multiple files 46 | // Note: for multi-threaded approach create separate tagText instance for each thread 47 | java.lang.reflect.Constructor constructor = clazz.getDeclaredConstructor(); 48 | Object tagText = constructor.newInstance(); 49 | Method parseOptionsMethod = clazz.getDeclaredMethod("parseOptions", String[].class); 50 | Method processMethod = clazz.getDeclaredMethod("process"); 51 | 52 | for(int i=0; i<4; i++) { 53 | Object options = parseOptionsMethod.invoke(null, (Object)new String[]{"-i", filename, "-o", outFilename, "-e", "--outputFormat", "xml"}); 54 | Method setOptionsMethod = clazz.getDeclaredMethod("setOptions", options.getClass()); 55 | setOptionsMethod.invoke(tagText, options); 56 | processMethod.invoke(tagText); 57 | System.err.println("Done tagging: " + i); 58 | } 59 | } 60 | 61 | /** 62 | * main method to test 63 | * @param args Arguments 64 | * @throws Exception If problems happen 65 | */ 66 | public static void main(String[] args) throws Exception { 67 | new TagTextWrapper().tag(args[0], args[1]); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/TokenizeText.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.tools 4 | 5 | @GrabConfig(systemClassLoader=true) 6 | @GrabResolver(name="ltSnapshots", root="https://repo.languagetool.org/artifactory/languagetool-os-snapshot/") 7 | @Grab(group='org.languagetool', module='languagetool-core', version='6.7-SNAPSHOT') 8 | @Grab(group='org.languagetool', module='language-uk', version='6.7-SNAPSHOT') 9 | @Grab(group='ch.qos.logback', module='logback-classic', version='1.4.+') 10 | @Grab(group='info.picocli', module='picocli', version='4.6.+') 11 | 12 | import java.nio.charset.StandardCharsets 13 | 14 | // A wrapper to load tag/TagTextCore.groovy with all related classes and resources without complicating CLI 15 | 16 | class TokenizeText { 17 | 18 | @groovy.transform.SourceURI 19 | static SOURCE_URI 20 | static SCRIPT_DIR=new File(SOURCE_URI).parent 21 | 22 | static void main(String[] args) { 23 | warnForEncoding() 24 | 25 | long tm1 = System.currentTimeMillis() 26 | 27 | def cl = new GroovyClassLoader() 28 | cl.addClasspath("$SCRIPT_DIR/../../../../") 29 | 30 | // def resourceDir = SCRIPT_DIR + "/../../../../../resources" 31 | // if( ! new File(resourceDir).isDirectory() ) { 32 | // new File(resourceDir).mkdirs() 33 | // } 34 | // cl.addClasspath(resourceDir) 35 | 36 | def basePkg = TokenizeText.class.getPackageName() 37 | def tagTextClass = cl.loadClass("${basePkg}.tokenize.TokenizeTextCore") 38 | def m = tagTextClass.getMethod("main", String[].class) 39 | def mArgs = [args].toArray() // new Object[]{args} - Eclips chokes on this 40 | 41 | long tm2 = System.currentTimeMillis() 42 | 43 | if( "--timing" in args ) { 44 | System.err.println("Loaded classes in ${tm2-tm1} ms") 45 | } 46 | m.invoke(null, mArgs) 47 | } 48 | 49 | private static void warnForEncoding() { 50 | String osName = System.getProperty("os.name").toLowerCase(); 51 | if ( osName.contains("windows")) { 52 | if( ! "UTF-8".equals(System.getProperty("file.encoding")) 53 | || ! StandardCharsets.UTF_8.equals(java.nio.charset.Charset.defaultCharset()) ) { 54 | System.setOut(new PrintStream(System.out,true,"UTF-8")) 55 | 56 | println "file.encoding: " + System.getProperty("file.encoding") 57 | println "defaultCharset: " + java.nio.charset.Charset.defaultCharset() 58 | 59 | println "On Windows to get unicode handled correctly you need to set environment variable before running expand:" 60 | println "\tbash:" 61 | println "\t\texport JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8" 62 | println "\tPowerShell:" 63 | println "\t\t\$env:JAVA_TOOL_OPTIONS=\"-Dfile.encoding=UTF-8\"" 64 | println "\tcmd:" 65 | println "\t\t(change Font to 'Lucida Console' in cmd window properties)" 66 | println "\t\tchcp 65001" 67 | println "\t\tset JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8" 68 | } 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/CleanOptions.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import groovy.transform.CompileStatic 4 | import picocli.CommandLine.Option 5 | import picocli.CommandLine.Parameters 6 | 7 | @CompileStatic 8 | public class CleanOptions { 9 | @Parameters(index = "0", description = "Directory to process. Default: current directory", arity="0..1") 10 | List inputDirs 11 | @Option(names = ["-i", "--input"], arity="1", description = ["Input file"]) 12 | String input 13 | @Option(names = ["-o", "--output"], arity="1", description = ["Output file ((default: input file/dir with \"-good\" added)"]) 14 | String output 15 | @Option(names = ["--dir"], arity="1", description = ["Directory to process *.txt in (default is current directory)"]) 16 | String dir 17 | @Option(names = ["-k", "--keepInvalidFiles"], description = ["Do not discard invalid files"]) 18 | boolean keepInvalidFiles 19 | @Option(names = ["-n", "--allowTwoColumns"], description = ["do not discard two-column text"]) 20 | boolean allowTwoColumns 21 | @Option(names = ["-w", "--wordCount"], description = "Minimum Ukrainian word count") 22 | int wordCount 23 | @Option(names = ["-c", "--clean"], description = ["Clean old files in

-good/ directory"]) 24 | boolean clean 25 | @Option(names = ["-r", "--recursive"], description = ["Process directories recursively"]) 26 | boolean recursive 27 | @Option(names = ["--exclude-file"], description = ["Don't clean files listed in this file (just copy them)"]) 28 | String excludeFromFile 29 | @Option(names = ["-d", "--debug"], description = ["Debug output"]) 30 | boolean debug 31 | @Option(names = ["-z", "--markLanguages"], description = ["Mark text in another language, modes: none, mark, cut (supported language: Russian)"], defaultValue="none") 32 | MarkOption markLanguages = MarkOption.none 33 | @Option(names = ["--paragraph"], description = ["Tells if to split paragraph by single or double new line (for marking other languages)"], defaultValue = "double_nl") 34 | ParagraphDelimiter paragraphDelimiter = ParagraphDelimiter.double_nl 35 | @Option(names = ["-p", "--parallel"], description = ["Process files in parallel"]) 36 | boolean parallel 37 | // @Option(names = ["-m", "--modules"], description = ["Extra cleanup: remove footnotes, page numbers etc. (supported modules: nanu)"]) 38 | List modules 39 | @Option(names = ["-x", "--disable-rules"], description = ["Rules to disable (supported: oi)"]) 40 | List disabledRules = [] 41 | // @Option(names = ["--singleThread"], description = ["Always use single thread (default is to use multithreading if > 2 cpus are found)"]) 42 | // boolean singleThread 43 | @Option(names = ["--simple"], description = ["Simple pass"], hidden = true) 44 | boolean simple 45 | @Option(names = ["-q", "--quiet"], description = ["Less output"]) 46 | boolean quiet 47 | @Option(names= ["-h", "--help"], usageHelp= true, description= "Show this help message and exit.") 48 | boolean helpRequested 49 | 50 | List arguments() { [] } 51 | 52 | 53 | public enum MarkOption { 54 | none, mark, cut 55 | } 56 | 57 | public enum ParagraphDelimiter { 58 | double_nl, 59 | single_nl, 60 | auto 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/TagText.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.tools 4 | 5 | @GrabConfig(systemClassLoader=true) 6 | @GrabResolver(name="ltSnapshots", root="https://repo.languagetool.org/artifactory/languagetool-os-snapshot/") 7 | @Grab(group='org.languagetool', module='languagetool-core', version='6.7-SNAPSHOT') 8 | @Grab(group='org.languagetool', module='language-uk', version='6.7-SNAPSHOT') 9 | @Grab(group='org.languagetool', module='language-ru', version='6.6') 10 | //@Grab(group='ua.net.nlp', module='morfologik-ukrainian-lt', version='6.6.9') 11 | @Grab(group='ua.net.nlp', module='nlp_uk-stats', version='3.3.9') 12 | 13 | @Grab(group='ch.qos.logback', module='logback-classic', version='1.5.18') 14 | @Grab(group='info.picocli', module='picocli', version='4.7.7') 15 | @Grab(group='org.apache.commons', module='commons-csv', version='1.14.0') 16 | 17 | import java.nio.charset.StandardCharsets 18 | 19 | import groovy.transform.CompileStatic 20 | 21 | // A wrapper to load tag/TagTextCore.groovy with all related classes and resources without complicating CLI 22 | 23 | @CompileStatic 24 | class TagText { 25 | 26 | @groovy.transform.SourceURI 27 | static URI SOURCE_URI 28 | static String SCRIPT_DIR=new File(SOURCE_URI).parent 29 | 30 | static void main(String[] args) { 31 | warnForEncoding() 32 | 33 | long tm1 = System.currentTimeMillis() 34 | 35 | def cl = new GroovyClassLoader() 36 | cl.addClasspath(SCRIPT_DIR + "/../../../../") 37 | 38 | def resourceDir = SCRIPT_DIR + "/../../../../../resources" 39 | if( ! new File(resourceDir).isDirectory() ) { 40 | // println "making missing dir: $resourceDir" 41 | new File(resourceDir).mkdirs() 42 | } 43 | cl.addClasspath(resourceDir) 44 | 45 | def basePkg = TagText.class.getPackageName() 46 | def tagTextClass = cl.loadClass("${basePkg}.tag.TagTextCore") 47 | def m = tagTextClass.getMethod("main", String[].class) 48 | def mArgs = [args].toArray() // new Object[]{args} - Eclips chokes on this 49 | 50 | long tm2 = System.currentTimeMillis() 51 | 52 | if( "--timing" in args ) { 53 | System.err.println("Loaded classes in ${tm2-tm1} ms") 54 | } 55 | m.invoke(null, mArgs) 56 | } 57 | 58 | private static void warnForEncoding() { 59 | String osName = System.getProperty("os.name").toLowerCase(); 60 | if ( osName.contains("windows")) { 61 | if( ! "UTF-8".equals(System.getProperty("file.encoding")) 62 | || ! StandardCharsets.UTF_8.equals(java.nio.charset.Charset.defaultCharset()) ) { 63 | System.setOut(new PrintStream(System.out,true,"UTF-8")) 64 | 65 | println "file.encoding: " + System.getProperty("file.encoding") 66 | println "defaultCharset: " + java.nio.charset.Charset.defaultCharset() 67 | 68 | println "On Windows to get unicode handled correctly you need to set environment variable before running expand:" 69 | println "\tbash:" 70 | println "\t\texport JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8" 71 | println "\tPowerShell:" 72 | println "\t\t\$env:JAVA_TOOL_OPTIONS=\"-Dfile.encoding=UTF-8\"" 73 | println "\tcmd:" 74 | println "\t\t(change Font to 'Lucida Console' in cmd window properties)" 75 | println "\t\tchcp 65001" 76 | println "\t\tset JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8" 77 | } 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/CleanText.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | package ua.net.nlp.other 4 | 5 | // This script reads all .txt files in given directory (default is "txt/") 6 | // and tries to clean up the text ot make it more suitable for NLP 7 | // The output files go into -good 8 | // Cleanups: 9 | // fix broken encoding (broken cp1251 etc) 10 | // remove soft hyphen 11 | // replace weird apostrophe characters with correct one (') 12 | // merge some simple word wraps 13 | // remove backslash from escaped quotes 14 | // weird ї and й via combining characters (U+0308) 15 | // і instead of ї: промисловоі, нацполіціі 16 | // clean up latin/cyrillic character mix 17 | // CO/CO2 with cyr/lat mix 18 | // degree Celcius with cyr 19 | // digit 3 instead of letter З 20 | // try to detect and skip two-column texts 21 | // separate leading hyphen (e.g. -Алло! - проричав він в слухавку) 22 | // fix dangling hyphen (at the end of the line) 23 | // check and warn for spaced words (e.g. Н А Т А Л К А) 24 | // mark/rate or remove Russian paragraphs 25 | 26 | @GrabConfig(systemClassLoader=true) 27 | @Grab(group='org.languagetool', module='languagetool-core', version='6.6') 28 | @Grab(group='org.languagetool', module='language-uk', version='6.6') 29 | @Grab(group='org.languagetool', module='language-ru', version='6.6') 30 | @Grab(group='org.languagetool', module='language-en', version='6.6') 31 | 32 | @Grab(group='ch.qos.logback', module='logback-classic', version='1.5.18') 33 | @Grab(group='info.picocli', module='picocli', version='4.7.7') 34 | @Grab(group='org.apache.commons', module='commons-csv', version='1.14.0') 35 | 36 | import java.nio.charset.StandardCharsets 37 | import java.nio.file.Files 38 | import java.nio.file.Path 39 | import java.nio.file.Paths 40 | import java.util.function.Function 41 | import java.util.regex.MatchResult 42 | import java.util.regex.Matcher 43 | import java.util.regex.Pattern 44 | import picocli.CommandLine 45 | import picocli.CommandLine.Option 46 | import picocli.CommandLine.ParameterException 47 | import groovy.io.FileVisitResult 48 | import groovy.transform.CompileStatic 49 | 50 | import org.languagetool.tokenizers.SRXSentenceTokenizer 51 | import org.languagetool.tokenizers.uk.UkrainianWordTokenizer 52 | import org.languagetool.tagging.Tagger 53 | import org.languagetool.tagging.ru.RussianTagger 54 | import org.languagetool.AnalyzedToken 55 | import org.languagetool.language.Ukrainian 56 | import org.slf4j.Logger 57 | 58 | @CompileStatic 59 | class CleanText { 60 | 61 | @groovy.transform.SourceURI 62 | static URI SOURCE_URI 63 | static String SCRIPT_DIR=new File(SOURCE_URI).parent 64 | 65 | static void main(String[] args) { 66 | // warnForEncoding() 67 | 68 | long tm1 = System.currentTimeMillis() 69 | 70 | def cl = new GroovyClassLoader() 71 | cl.addClasspath(SCRIPT_DIR + "/../../../../") 72 | 73 | def resourceDir = SCRIPT_DIR + "/../../../../../resources" 74 | if( ! new File(resourceDir).isDirectory() ) { 75 | // println "making missing dir: $resourceDir" 76 | new File(resourceDir).mkdirs() 77 | } 78 | cl.addClasspath(resourceDir) 79 | 80 | def basePkg = CleanText.class.getPackageName() 81 | def tagTextClass = cl.loadClass("${basePkg}.clean.CleanTextCore") 82 | def m = tagTextClass.getMethod("main", String[].class) 83 | def mArgs = [args].toArray() // new Object[]{args} - Eclipse chokes on this 84 | 85 | long tm2 = System.currentTimeMillis() 86 | 87 | if( "--timing" in args ) { 88 | System.err.println("Loaded classes in ${tm2-tm1} ms") 89 | } 90 | m.invoke(null, mArgs) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/LtModule.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import org.languagetool.AnalyzedToken 4 | import org.languagetool.AnalyzedTokenReadings 5 | import org.languagetool.Languages 6 | import org.languagetool.language.Ukrainian 7 | import org.languagetool.tagging.Tagger 8 | import org.languagetool.tagging.en.EnglishTagger 9 | import org.languagetool.tagging.ru.RussianTagger 10 | import org.languagetool.tagging.uk.UkrainianTagger 11 | import org.languagetool.tokenizers.SRXSentenceTokenizer 12 | import org.languagetool.tokenizers.uk.UkrainianWordTokenizer 13 | 14 | import groovy.transform.CompileStatic 15 | import groovy.transform.PackageScope 16 | 17 | @PackageScope 18 | //@CompileStatic 19 | class LtModule { 20 | @Lazy 21 | RussianTagger ruTagger = { Languages.getLanguageForShortCode("ru").getTagger() }() 22 | @Lazy 23 | EnglishTagger enTagger = { Languages.getLanguageForShortCode("en").getTagger() }() 24 | 25 | Ukrainian ukLanguage = Languages.getLanguageForShortCode("uk") 26 | UkrainianTagger ukTagger = ukLanguage.getTagger() 27 | // SRXSentenceTokenizer ukSentTokenizer = ukLanguage.getSentenceTokenizer() 28 | UkrainianWordTokenizer ukWordTokenizer = ukLanguage.getWordTokenizer() 29 | 30 | 31 | @CompileStatic 32 | boolean knownWordUk(String word) { 33 | if( ! tag(ukTagger, normalize(word))[0].hasNoTag() ) { 34 | return true 35 | } 36 | 37 | return false 38 | } 39 | 40 | @CompileStatic 41 | List tagWord(String word) { 42 | return tag(ukTagger, normalize(word)) 43 | } 44 | 45 | @CompileStatic 46 | boolean knownWord(String word) { 47 | try { 48 | return knownWordUk(word) 49 | } 50 | catch (Exception e) { 51 | System.err.println("Failed on word: $word") 52 | throw e 53 | } 54 | } 55 | 56 | @CompileStatic 57 | boolean knownWordTwoLang(String word) { 58 | try { 59 | return knownWordUk(word) \ 60 | || ! tag(ruTagger, word)[0].hasNoTag() 61 | } 62 | catch (Exception e) { 63 | System.err.println("Failed dual lang on word: $word") 64 | throw e 65 | } 66 | } 67 | 68 | @CompileStatic 69 | boolean knownWordRu(String word) { 70 | try { 71 | return ! ruTagger.tag(Arrays.asList(word))[0][0].hasNoTag() 72 | } 73 | catch (Exception e) { 74 | System.err.println("Failed on word: $word") 75 | throw e 76 | } 77 | } 78 | 79 | @CompileStatic 80 | boolean knownWordEn(String word) { 81 | try { 82 | return ! enTagger.tag(Arrays.asList(word))[0][0].hasNoTag() 83 | } 84 | catch (Exception e) { 85 | System.err.println("Failed on word: $word") 86 | throw e 87 | } 88 | } 89 | 90 | @CompileStatic 91 | static String normalize(String word) { 92 | word.replace('\u2019', '\'') 93 | .replace('\u02BC', '\'') 94 | .replace('\u2018', '\'') 95 | .replace('\u0301', '') 96 | } 97 | 98 | @CompileStatic 99 | private List tag(Tagger tagger, String word) { 100 | tagger.tag(Arrays.asList(word)).get(0).getReadings() 101 | } 102 | 103 | @CompileStatic 104 | List getLemmas(String word) { 105 | tag(ukTagger, normalize(word))*.getLemma() 106 | } 107 | 108 | @CompileStatic 109 | List tagSent(String sent) { 110 | def tk = ukWordTokenizer.tokenize(sent) 111 | return ukTagger.tag(tk) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tag/ModLesya.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag 2 | 3 | import org.languagetool.AnalyzedToken 4 | import org.languagetool.AnalyzedTokenReadings 5 | import org.languagetool.JLanguageTool 6 | import org.languagetool.tagging.uk.PosTagHelper 7 | 8 | import groovy.transform.CompileStatic 9 | 10 | class ModLesya { 11 | JLanguageTool langTool 12 | 13 | ModLesya(JLanguageTool langTool) { 14 | this.langTool = langTool 15 | } 16 | 17 | @CompileStatic 18 | AnalyzedTokenReadings tagWord(String word) { 19 | langTool.getLanguage().getTagger().tag([word]).get(0) 20 | } 21 | 22 | @CompileStatic 23 | AnalyzedTokenReadings adjustTokens(AnalyzedTokenReadings tokenReadings, AnalyzedTokenReadings[] tokens, int idx) { 24 | AnalyzedToken origAnalyzedToken = tokenReadings.getReadings().get(0) 25 | // boolean syaIsNext = idx < tokens.size()-1 && tokens[idx+1].getToken() == 'ся' 26 | 27 | if( ! tokenReadings.isPosTagUnknown() ) 28 | return tokenReadings 29 | 30 | String originalToken = tokenReadings.cleanToken 31 | 32 | String adjustedToken = originalToken 33 | 34 | tokenReadings = tagWord(adjustedToken) 35 | 36 | if( tokenReadings.isPosTagUnknown() ) { 37 | adjustedToken = adjustedToken.replaceAll(/(?ui)йі/, 'ї') 38 | adjustedToken = adjustedToken.replaceAll(/(?Ui)([іо])і\b/, '$1ї') 39 | tokenReadings = tagWord(adjustedToken) 40 | } 41 | if( tokenReadings.isPosTagUnknown() ) { 42 | adjustedToken = originalToken.replaceAll(/(?ui)(.)і/, '$1и') 43 | tokenReadings = tagWord(adjustedToken) 44 | } 45 | if( tokenReadings.isPosTagUnknown() ) { 46 | adjustedToken = originalToken.replaceAll(/(?Ui)\bи/, 'і') 47 | tokenReadings = tagWord(adjustedToken) 48 | } 49 | if( tokenReadings.isPosTagUnknown() ) { 50 | adjustedToken = originalToken.replaceAll(/(?Ui)рь\b/, 'р') 51 | tokenReadings = tagWord(adjustedToken) 52 | } 53 | if( tokenReadings.isPosTagUnknown() ) { 54 | adjustedToken = originalToken.replaceAll(/(?ui)ійш/, 'іш') 55 | tokenReadings = tagWord(adjustedToken) 56 | } 57 | if( tokenReadings.isPosTagUnknown() ) { 58 | adjustedToken = originalToken.replaceAll(/(?Ui)лько\b/, 'льки') 59 | tokenReadings = tagWord(adjustedToken) 60 | } 61 | if( tokenReadings.isPosTagUnknown() ) { 62 | adjustedToken = originalToken.replaceAll(/(?Ui)иї\b/, 'ії') 63 | tokenReadings = tagWord(adjustedToken) 64 | } 65 | if( tokenReadings.isPosTagUnknown() ) { 66 | adjustedToken = originalToken.replaceAll(/(?Ui)([яа][нр])е\b/, '$1и') 67 | tokenReadings = tagWord(adjustedToken) 68 | } 69 | if( tokenReadings.isPosTagUnknown() ) { 70 | adjustedToken = originalToken.replaceAll(/(?Ui)ів\b/, 'ей') 71 | tokenReadings = tagWord(adjustedToken) 72 | } 73 | if( tokenReadings.isPosTagUnknown() ) { 74 | adjustedToken = originalToken.replaceAll(/(?Ui)([ндтч])\1ів\b/, '$1ь') 75 | tokenReadings = tagWord(adjustedToken) 76 | } 77 | if( tokenReadings.isPosTagUnknown() ) { 78 | adjustedToken = originalToken.replaceAll(/(?Ui)жу\b/, 'джу') 79 | tokenReadings = tagWord(adjustedToken) 80 | } 81 | if( tokenReadings.isPosTagUnknown() ) { 82 | adjustedToken = originalToken.replaceAll(/(?Ui)ови\b/, 'ові') 83 | tokenReadings = tagWord(adjustedToken) 84 | } 85 | if( tokenReadings.isPosTagUnknown() ) { 86 | adjustedToken = originalToken.replaceAll(/(?Ui)ов\b/, 'ів') 87 | tokenReadings = tagWord(adjustedToken) 88 | } 89 | if( tokenReadings.isPosTagUnknown() ) { 90 | adjustedToken = originalToken.replaceAll(/(?Ui)а\b/, 'у') 91 | def tokenReadings2 = tagWord(adjustedToken) 92 | if( PosTagHelper.hasPosTagPart(tokenReadings2, ":m:v_rod")) { 93 | tokenReadings = tokenReadings2 94 | } 95 | } 96 | 97 | // put back original word 98 | if( ! tokenReadings.isPosTagUnknown() ) { 99 | for(int i=0; iВыйдя из развозки, Дима остановился у кафе, раздумывая, а не посидеть ли ему тут с полчасика?.\r 54 | \r 55 | удерживала его теперь на месте, не позволяя голове принимать какие–либо резкие решения.\r 56 | Да еще.\r 57 | \r 58 | Да да, ему дали все, как положено все дали под розпись.\r 59 | ''' 60 | 61 | String text = 62 | """Выйдя из развозки, Дима остановился у кафе, раздумывая, а не посидеть ли ему тут с полчасика?.\r 63 | 64 | удерживала его теперь на месте, не позволяя голове принимать какие–либо резкие решения.\r 65 | Да еще.\r 66 | 67 | Да да, ему дали все, как положено все дали под розпись.\r 68 | """ 69 | 70 | assertEquals expected, clean(text) 71 | } 72 | 73 | @Test 74 | public void testMarkLanguageCut() { 75 | options.markLanguages = MarkOption.cut 76 | 77 | String expected= 78 | '''Десь там за горою. 79 | 80 | --- 81 | 82 | --- 83 | 84 | 85 | --- 86 | ''' 87 | 88 | String text = 89 | """Десь там за горою. 90 | 91 | Выйдя из развозки, Дима остановился у кафе, раздумывая, а не посидеть ли ему тут с полчасика?. 92 | 93 | Да да, ему дали все, как положено все дали под розпись. 94 | 95 | 96 | --- 97 | """ 98 | 99 | assertEquals expected, clean(text) 100 | 101 | options.markLanguages = MarkOption.mark 102 | } 103 | 104 | 105 | @Test 106 | public void testMarkLanguageCutSingleNlPara() { 107 | options.markLanguages = MarkOption.cut 108 | options.paragraphDelimiter = ParagraphDelimiter.single_nl 109 | 110 | String expected= 111 | '''Десь там за горою. 112 | --- 113 | --- 114 | --- 115 | ''' 116 | 117 | String text = 118 | """Десь там за горою. 119 | Выйдя из развозки, Дима остановился у кафе, раздумывая, а не посидеть ли ему тут с полчасика?. 120 | Да да, ему дали все, как положено все дали под розпись. 121 | --- 122 | """ 123 | 124 | assertEquals expected, clean(text) 125 | 126 | assumeTrue(NEW_TESTS) 127 | 128 | text = "ГОЛОВА. Борис Райков, будь ласка." 129 | assertEquals text, clean(text) 130 | } 131 | 132 | @Test 133 | public void testGetText() { 134 | def txt = cleanTextCore2.spacingModule.getText(new Node(word: "голова"), "") 135 | assertEquals( ["голова"], txt ) 136 | 137 | txt = cleanTextCore2.spacingModule.getText(new Node(word: "голова", 138 | children: ([new Node(word:"його"), new Node(word:"її", 139 | children: ([new Node(word: "кудлата")]))])), "") 140 | assertEquals (["голова його", "голова її кудлата"], txt) 141 | } 142 | 143 | 144 | @CompileStatic 145 | void assertUntouched(String txt) { 146 | assertEquals txt, clean(txt) 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/resources/ua/net/nlp/tools/ud/vesum-ud.csv: -------------------------------------------------------------------------------- 1 | UD,VESUM,Comments 2 | Upos=NOUN,noun,іменник 3 | Animacy=Anim,anim,істота 4 | NameType=Giv,fname,ім'я 5 | NameType=Sur,lname,прізвище 6 | NameType=Pat,pname,по батькові 7 | Animacy=Inan,inanim,неістота 8 | "Animacy=Anim,Inan",unanim,"невизначена категорія істота/неістота (бактерія тощо, також деякі займ.: він, вони...)" 9 | Upos=PROPN,prop,власна назва 10 | NameType=Geo,geo, 11 | Upos=VERB,verb,дієслово 12 | Aspect=Imp,imperf,недоконаний вид 13 | Aspect=Perf,perf,доконаний вид 14 | Reflex=Yes,rev,"зворотна форма (дієслова) (тег є неявним ключем, оскільки лема на #NAME? завжди відрізняється від прямого дієслова)" 15 | VerbForm=Inf,inf,інфінітив 16 | Tense=Fut; Mood=Ind,futr,майбутній час 17 | Tense=Past; Mood=Ind,past,минулий час 18 | Tense=Pres; Mood=Ind,pres,теперішній час 19 | Mood=Imp,impr,наказова форма 20 | VerbForm=Fin; Person=0; Mood=Ind,impers,безособова форма 21 | VerbForm=Fin; Person=1,1,1-а особа 22 | VerbForm=Fin; Person=2,2,2-а особа 23 | VerbForm=Fin; Person=3,3,3-а особа 24 | Upos=ADJ,adj,прикметник 25 | Degree=Pos,compb,базова форма 26 | Degree=Cmp,compc,порівняльна форма 27 | Degree=Sup,comps,найвища форма 28 | Variant=Short,short,"короткі форми прикметників, короткі форми дієслів 3-ї особи, інфінітиви на #NAME?" 29 | Variant=Uncontr ,long,"нестягнені форми прикметників, наказові форми на #NAME?" 30 | VerbForm=Part,adjp,дієприкметник: 31 | Voice=Act,actv,активний 32 | Voice=Pass,pasv,пасивний 33 | Animacy=Inan,v_zna:rinanim,знахідний для неістот (лише ч.р. та мн.) 34 | Animacy=Anim,v_zna:ranim,знахідний для істот (лише ч.р. та мн.) 35 | Upos=ADV,adv,прислівник 36 | Upos=VERB; VerbForm=Conv,advp,дієприслівник 37 | Aspect=Perf,perf, 38 | Aspect=Imp,imperf, 39 | Upos=ADP,prep,прийменник 40 | -,conj,сполучник 41 | Upos=SCONJ,conj:subord,сполучник підрядний 42 | Upos=CCONJ,conj:coord,сполучник сурядний 43 | Upos=PART,part,частка 44 | Upos=INTJ,intj,вигук 45 | Upos=NUM,numr,числівник 46 | Uninflect=Yes,noninfl,"невідмінювані частини (най-най, брутто, екстра...)" 47 | Upos=ADV; Uninflect=Yes,noninfl:predic,"можна, треба, немає" 48 | Foreign=Yes,foreign,"запозичені слова невизначеної частини мови (Альгемайне, Юнайтед, ла (Ла Страда) тощо)" 49 | -,onomat,"(клас звуконаслідувальних слів) Але може бути |intj:onomat|, тоді не дублювати" 50 | Case=Nom,v_naz,називний 51 | Case=Gen,v_rod,родовий 52 | Case=Dat,v_dav,давальний 53 | Case=Acc,v_zna,знахідний 54 | Case=Ins,v_oru,орудний 55 | Case=Loc,v_mis,місцевий 56 | Case=Voc,v_kly,кличний 57 | InflClass=Ind,nv,не відмінюється 58 | Number=Ptan,ns,множинний іменник 59 | Number=Plur,p,множина 60 | Number=Sing,s,однина 61 | Gender=Masc,m,чоловічий 62 | Gender=Fem,f,жіночий 63 | Gender=Neut,n,середній 64 | Abbr=Yes,abbr,абревіатура 65 | BadStyle=Yes,bad,покруч/помилкове написання 66 | -,subst,нестандартні форма 67 | Style=Rare,rare,рідковживана форма (також другий зн. в. для істот - в президенти) 68 | -,coll,розмовне слово/розмовна форма (наразі не генерується на виході) 69 | Style=Arch,arch,застаріле/архаїчне/(інколи) діалектне. 70 | -,slang,сленг та (проф)жаргонізми 71 | Orth=Alt,alt,альтернативне написання (не за чинним правописом) 72 | -,vulg,вульгарне 73 | -,ua_1992,за правописом 1992 74 | -,ua_2019,за правописом 2019 75 | Animacy[gram]=Anim,var,варіативний знах. відм. 76 | -,:xp[1-9],"омоніми, що відрізняються парадигмою відмінювання (напр. бар - р.в. бару, бар - р.в. бара)" 77 | -,#,"в коментарях також :xv[1-9] омоніми, що відрізняються семантично (напр. глупий (дурний, має вищий ступінь глупіший) і глупий - глупа ніч," 78 | -,v-u,"паралельні форми на в-/у- (для правил милозвучності, не генерується за уставою)" 79 | -,pron,"- наразі всі займенники мають теги відповідних частин мови (noun/adj/adv), але всі мають додатковий тег pron (тег pron разом з наступним класифікатором стає ключем леми)" 80 | NumType=Ord,numr,"- слова, що є порядковими числівниками" 81 | NumType=Card,numr,"- слова, що є і іменниками і кількісними числівниками" 82 | -,insert,- може бути вставним словом 83 | -,predic,- може бути предикативом 84 | PronType=Prs,pers,особовий 85 | Poss=Yes|PronType=Prs|Reflex=Yes,refl,зворотний 86 | Poss=Yes|PronType=Prs,pos,присвійний 87 | PronType=Dem,dem,вказівний 88 | PronType=Rel,def,означальний 89 | PronType=Int,int,питальний 90 | PronType=Rel,rel,відносний 91 | PronType=Neg,neg,заперечний 92 | PronType=Ind,ind,неозначений 93 | PronType=Tot,gen,узагальнювальний 94 | PronType=Emp,emph,підсилювальний 95 | Upos=NUM,number,- число 96 | -,latin,- число латинськими цифрами 97 | Upos=NUM,date,- дата 98 | Upos=NUM,time,- час 99 | Upos=NUM,hashtag,- хештег 100 | Upos=PUNCT,punct,- пунктуація (лише TagText.groovy) 101 | Upos=SYM,symb,- символ (лише TagText.groovy) 102 | Upos=X,unknown,- невідомі українські слова (лише TagText.groovy) 103 | Upos=X,unclass,- неукраїнські слова (лише TagText.groovy) 104 | Upos=AUX,,"у ВЕСУМі part, verb (в укр. м. AUX це ""б, би"" і ""бути, бувати"", але не у всіх випадках)" 105 | Mood=Cnd,,дієслово в формі минулого часу з б/би 106 | Upos=PRON,noun.*pron, 107 | Upos=ADV,adv.*pron, 108 | Upos=DET,numr.*pron, 109 | Upos=DET,adj.*pron, 110 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/CleanTextNanu.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | 4 | import groovy.transform.Field 5 | import groovy.transform.PackageScope 6 | import groovy.transform.TupleConstructor 7 | import groovy.transform.CompileStatic 8 | import java.util.regex.Pattern 9 | 10 | @PackageScope 11 | class CleanTextNanu { 12 | 13 | Pattern SPECIAL_REGEX_CHARS = Pattern.compile(/[{}()\[\].+*?^$\\|]/) 14 | String pntf1 = /[ ]*((?:[12][0-9]{3} +)?[“"«]?[а-яіїєґА-ЯІЇЄҐ][^\n]*)[ ][0-9]{1,3}$/ 15 | String pntf2 = /^[ ]*[0-9]{1,3}[ ]*([“"«]?[а-яіїєґА-ЯІЇЄҐ][^\n]*)/ 16 | String pntf3 = /(?:[ ]*([А-ЯІЇЄҐ][а-яіїєґ'-]+ (?:[А-ЯІЇЄҐ]\.)(?: ?[А-ЯІЇЄҐ]\.)?(?:, [А-ЯІЇЄҐ][а-яіїєґ'-]+ (?:[А-ЯІЇЄҐ]\.)(?: ?[А-ЯІЇЄҐ]\.)?)*) *)/ 17 | String pntf4 = /(?:[ ]*((?:[А-ЯІЇЄҐ]\.)(?: ?[А-ЯІЇЄҐ]\.)? [А-ЯІЇЄҐ][а-яіїєґ'-]+(?:, (?:[А-ЯІЇЄҐ]\.)(?: ?[А-ЯІЇЄҐ]\.)? [А-ЯІЇЄҐ][а-яіїєґ'-]+)*) *)/ 18 | Pattern pageNumTitleFooter = Pattern.compile(/^(?:/ + pntf1 + /|/ + pntf2 + /|/ + pntf3 + /|/ + pntf4 + /)$/, Pattern.MULTILINE) 19 | 20 | 21 | PrintStream out 22 | 23 | CleanTextNanu(PrintStream out) { 24 | this.out = out 25 | } 26 | 27 | 28 | void println(String txt) { 29 | out.println(txt) 30 | } 31 | 32 | 33 | String removeMeta(String text, File file, def options) { 34 | def footers = findFooter(text) 35 | 36 | def footerMatchers = null 37 | if( footers ) { 38 | // println "\tfooters: " + footers 39 | footerMatchers = footers.collect { footer -> 40 | // Pattern.compile("^(?:[ ]*(${footer.key})[ ]+[0-9]+\$|^[ ]*[0-9]+[ ]*(${footer.key}))\$", Pattern.MULTILINE) 41 | def footerKey = SPECIAL_REGEX_CHARS.matcher(footer.key).replaceAll('\\\\$0') 42 | footerKey = footerKey.replaceAll(/ {8,}/, ' {8,}') 43 | // footerKey = footerKey.replaceAll(/ {10,}/, ' {10,}') 44 | // println"\tregex: " + footerKey 45 | Pattern.compile("^(?:[ ]*(${footerKey})([ ]+[0-9]+| *)\$|^([ ]*[0-9]+[ ]*| *)(${footerKey}))\$", Pattern.MULTILINE) 46 | } 47 | // println "match: " + footerMatcher.matcher(text).find() 48 | 49 | // println"\tfooterMatchers: " + footerMatchers 50 | } 51 | 52 | text = removeFooters(text, file, options, footerMatchers) 53 | text = removeAbstracts(text) 54 | } 55 | 56 | def removeAbstracts(String text) { 57 | return text.replaceAll(/(?si)\n *(abstract|resume|Literatura|(Джерела та|Використана|Цитована) Література|СПИСОК ЛІТЕРАТУРИ)\n.+?(\n\n|\n?$)/, '\n') 58 | .replaceAll(/(?si)\n *(abstract|resume|Literatura|(Джерела та|Використана|Цитована) Література|СПИСОК ЛІТЕРАТУРИ)\n.+?(\n\n|\n?$)/, '\n') 59 | } 60 | 61 | 62 | def findNonNullGroup(m) { 63 | for(int i=1;i<=m.groupCount(); i++) { 64 | if( m.group(i) ) 65 | return m.group(i) 66 | } 67 | assert null, "No group in $m (group count ${m.groupCount()}" 68 | } 69 | 70 | def findFooter(String text) { 71 | def m = pageNumTitleFooter.matcher(text) 72 | def candidates = [:].withDefault { 0 } 73 | while( m.find() ) { 74 | // println "found group: " + m.group() 75 | def key = findNonNullGroup(m) 76 | key = key.trim() 77 | if( ! (key =~ /Таблиця|група|ЗМІСТ №|Рис\.|.*?\.{6,}/) ) { 78 | key = key.replaceAll(/ {8,}/, ' ') 79 | candidates[ key ] += 1 80 | } 81 | } 82 | // println "\tcandidates: " + candidates 83 | return candidates.findAll { it.value > 2 } 84 | } 85 | 86 | String removeFooters(String text, File file, def options, def footerMatchers) { 87 | 88 | String[] lines = text.split(/\n\r?/) 89 | 90 | List newLines = [] 91 | 92 | List suspectWeak = [] 93 | boolean justCut = false 94 | 95 | for(String line: lines) { 96 | // println ":: " + line 97 | 98 | if( line.trim().isEmpty() 99 | || line.matches(/ \s{30,}?[0-9]+/) ) { 100 | if( ! justCut ) { 101 | suspectWeak << line 102 | continue 103 | } 104 | 105 | // justCut = false 106 | continue 107 | } 108 | else { 109 | justCut = false 110 | } 111 | 112 | if( line.matches(/([0-9]*\s{10,})?© .*/) 113 | || line.matches(/([0-9]*\s{10,})?ISSN [0-9]+.*/) 114 | || (footerMatchers && footerMatchers.find{ it.matcher(line).matches() } ) ) { 115 | 116 | // println "\tfound footer: $line" 117 | suspectWeak.clear() 118 | justCut = true 119 | continue 120 | } 121 | 122 | if( suspectWeak ) { 123 | // println "adding weak: $suspectWeak" 124 | newLines += suspectWeak 125 | } 126 | 127 | if( justCut ) { 128 | if( line.matches(/(Рис\. |Таблиця ).*/) ) { 129 | newLines << "\n" 130 | } 131 | } 132 | 133 | // println "adding: $line" 134 | newLines << line 135 | 136 | suspectWeak.clear() 137 | justCut = false 138 | } 139 | 140 | if( suspectWeak ) { 141 | // println "adding weak2: $suspectWeak" 142 | newLines += suspectWeak 143 | } 144 | 145 | if( text.endsWith("\n") ) { 146 | // println "adding new line" 147 | newLines << "" 148 | } 149 | 150 | // println "=$newLines=" 151 | text = newLines.join("\n") 152 | 153 | 154 | // def m = pageNumFooter.matcher(text) 155 | // 156 | // if( m.find() ) { 157 | // println "\tremoving page-num footers..." 158 | // println "\t" + m 159 | // text = m.replaceAll("\n") 160 | // } 161 | 162 | return text 163 | } 164 | 165 | 166 | } -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/other/clean/CleanLatCyrTest.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other.clean 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | import static org.junit.jupiter.api.Assertions.assertFalse 7 | import static org.junit.jupiter.api.Assertions.assertTrue 8 | import static org.junit.jupiter.api.Assumptions.assumeTrue 9 | 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Disabled 12 | import org.junit.jupiter.api.Test 13 | 14 | import groovy.transform.CompileStatic 15 | import ua.net.nlp.other.clean.CleanOptions 16 | import ua.net.nlp.other.clean.CleanOptions.MarkOption 17 | import ua.net.nlp.other.clean.CleanOptions.ParagraphDelimiter 18 | import ua.net.nlp.other.clean.CleanTextCore 19 | 20 | 21 | @CompileStatic 22 | class CleanLatCyrTest { 23 | 24 | CleanOptions options = new CleanOptions("wordCount": 0, "debug": true) 25 | 26 | CleanTextCore cleanText = new CleanTextCore( options ) 27 | 28 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream() 29 | 30 | @BeforeEach 31 | public void init() { 32 | cleanText.out.out = new PrintStream(outputStream) 33 | } 34 | 35 | @CompileStatic 36 | String clean(String str) { 37 | str = str.replace('_', '') 38 | cleanText.cleanText(str, null, null, cleanText.out) 39 | } 40 | 41 | 42 | @Test 43 | public void testLatDigits() { 44 | assertEquals "XXI", clean("XХІ") 45 | assertEquals "XVII", clean("XVП") 46 | assertEquals "XVIII", clean("XVШ") 47 | } 48 | 49 | @Test 50 | public void testLatCyrcMix() { 51 | assertEquals "брат", clean("б_p_ат") 52 | 53 | assertEquals "труба", clean("тр_y_ба") 54 | 55 | assertEquals "baby", clean("b_а_b_у_") 56 | 57 | assertEquals "Abby", clean("А_bb_у_") 58 | 59 | assertEquals "сіс", clean("с_і_с") 60 | 61 | assertEquals "Corporation", clean("С_orporation") 62 | 63 | assertEquals "нашій Twitter", clean("нашійTwitter") 64 | 65 | assertEquals "ДонорUA", clean("ДонорUA") 66 | 67 | // do not touch 68 | assertEquals "Renault Kangoo", clean("Renault Kangoo") 69 | assertEquals "FREЕДОМ", clean("FREЕДОМ") 70 | // Mo - Latin 71 | assertEquals "Moстиск", clean("Moстиск") 72 | // pe - Latin 73 | assertEquals "пеpeсвідчив", clean("пеpeсвідчив") 74 | // cap - Latin 75 | assertEquals "цїcapска", clean("цїcapска") 76 | assertEquals "Pianoбой", clean("Pianoбой") 77 | 78 | assertEquals " IІвана", clean(" IІвана") 79 | 80 | // do not touch 81 | assertEquals "квадрокоптери Aquila16-fpv-kit.", clean("квадрокоптери Aquila16-fpv-kit.") 82 | 83 | assertEquals "Insider розповів", clean("Insiderрозповів") 84 | 85 | // latin i 86 | def orig = "чоловіка i жінки" 87 | def result = clean(orig) 88 | assert result != orig 89 | assert result == "чоловіка і жінки" 90 | 91 | // latin y 92 | orig = "з полком y поміч" 93 | result = clean(orig) 94 | assert result != orig 95 | assert result == "з полком у поміч" 96 | 97 | assertEquals "концентрація CO та CO2", clean("концентрація СO та CО2") 98 | 99 | assertEquals "не всі в", clean("не всi в") 100 | assertEquals "ДАІ", clean("ДАI") 101 | 102 | // assertEquals "Бі–Бі–Сi", clean("Бі–Бі–Сi") 103 | 104 | assertEquals "розвиток ІТ", clean("розвиток IТ") 105 | assertEquals "На лаві", clean("Hа лаві") 106 | 107 | // Пальчикова і Кo 108 | 109 | assertEquals "о\u0301ргани", clean("óргани") 110 | 111 | assertEquals "агітаторів", clean("ariтаторів") 112 | 113 | // old spelling 114 | assertEquals "роздїлив", clean("p_оздїлив") 115 | 116 | assertEquals "Ł. Op. cit.", clean("Ł. Оp. cit.") // Cyrillic "Ор" 117 | } 118 | 119 | @Test 120 | public void testUntouched() { 121 | // leave as is 122 | outputStream.reset() 123 | assertEquals "margin'ом", clean("margin'ом") 124 | assertFalse(new String(outputStream.toByteArray()).contains("mix")) 125 | 126 | assertUntouched("Kurjeр-ї") 127 | 128 | outputStream.reset() 129 | assertEquals "ГогольFest", clean("ГогольFest") 130 | assertFalse(new String(outputStream.toByteArray()).contains("mix")) 131 | 132 | outputStream.reset() 133 | assertEquals "Narodow-ої", clean("Narodow-ої") 134 | assertFalse(new String(outputStream.toByteArray()).contains("mix")) 135 | 136 | assertEquals "скорhйше", clean("скорhйше") 137 | // mark but don't fix as most probably it's "ѣ" 138 | // assertFalse(new String(outputStream.toByteArray()).contains("mix")) 139 | 140 | def orig = "da y Вoreckomu" 141 | assertEquals orig, clean(orig) 142 | 143 | // don't touch 144 | assertUntouched "senior'и" 145 | assertUntouched "Велесової rниги" 146 | assertUntouched "дівчинrи." 147 | assertUntouched "ГогольTRAIN" 148 | 149 | assertUntouched "Xі" 150 | assertUntouched "Рi0" 151 | assertUntouched "OАО" 152 | } 153 | 154 | @CompileStatic 155 | void assertUntouched(String txt) { 156 | assertEquals txt, clean(txt) 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/tag/TagTextSemTest.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | package ua.net.nlp.tools.tag 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | 7 | import org.junit.jupiter.api.BeforeAll 8 | import org.junit.jupiter.api.Test 9 | 10 | import ua.net.nlp.tools.tag.TagOptions 11 | import ua.net.nlp.tools.tag.TagTextCore 12 | import ua.net.nlp.tools.tag.TagTextCore.TagResult 13 | 14 | 15 | 16 | class TagTextSemTest { 17 | def options = new TagOptions() 18 | 19 | static TagTextCore tagText = new TagTextCore() 20 | 21 | @BeforeAll 22 | static void before() { 23 | tagText.disambigStats.writeDerivedStats = true 24 | } 25 | 26 | 27 | @Test 28 | public void testSemantic() { 29 | def expected= 30 | """ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | """ 71 | 72 | tagText.setOptions(new TagOptions(semanticTags: true)) 73 | TagResult tagged = tagText.tagText("Слово усе голова аахенська Вашингтон акту один-другий.") 74 | assertEquals expected, tagged.tagged 75 | } 76 | 77 | 78 | @Test 79 | public void testSemanticOrg() { 80 | def expected= 81 | """ 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | """ 91 | 92 | tagText.setOptions(new TagOptions(semanticTags: true)) 93 | TagResult tagged = tagText.tagText("хімвиробник півча") 94 | assertEquals expected, tagged.tagged 95 | } 96 | 97 | 98 | @Test 99 | public void testSemanticAlt() { 100 | def expected= 101 | """ 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | """ 111 | 112 | tagText.setOptions(new TagOptions(semanticTags: true)) 113 | TagResult tagged = tagText.tagText("колеґа по\u2013турецьки") 114 | assertEquals expected, tagged.tagged 115 | } 116 | 117 | 118 | @Test 119 | public void testSemanticDerivat() { 120 | def expected= 121 | """ 122 | 123 | 124 | 125 | """ 126 | 127 | tagText.setOptions(new TagOptions(semanticTags: true, tokenFormat: true)) 128 | TagResult tagged = tagText.tagText("стверджуючи") 129 | assertEquals expected, tagged.tagged 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/bruk/ContextToken.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.bruk 2 | 3 | import java.util.regex.Matcher 4 | import java.util.regex.Pattern 5 | import groovy.transform.Canonical 6 | import groovy.transform.CompileStatic 7 | import org.languagetool.rules.uk.LemmaHelper 8 | 9 | @CompileStatic 10 | @Canonical 11 | class ContextToken { 12 | // static final Pattern POSTAG_KEY_PATTERN = Pattern.compile("^(noun:(anim|[iu]nanim)|verb(:rev)?:(perf|imperf)|adj|adv(p:(imperf:perf))?|part|prep|numr|conj:(coord|subord)|intj|onomat|punct|symb|noninfl|unclass|number|unknown|time|date|hashtag|BEG|END)") 13 | static final Pattern POSTAG_CORE_REMOVE_PATTERN = Pattern.compile(/:(arch|coll|slang|bad|vulg|up[0-9]{2}|ns)/) 14 | static final ContextToken BEG = new ContextToken('__BEG', '', 'BEG') 15 | static final ContextToken END = new ContextToken('__END', '', 'END') 16 | static final String[] IGNORE_TOKENS = [] //['б', 'би', 'ж', 'же', 'бодай'] 17 | 18 | String word 19 | String lemma 20 | String postag 21 | 22 | @CompileStatic 23 | ContextToken(String word, String lemma, String postag) { 24 | this.word = word 25 | this.lemma = lemma 26 | // assert postag, "Empty postag for $word/$lemma" 27 | this.postag = getPostagCore(postag) 28 | } 29 | 30 | @CompileStatic 31 | static ContextToken normalized(String word, String lemma, String postag) { 32 | new ContextToken(normalizeContextString(word, lemma, postag), 33 | normalizeContextString(lemma, '', postag), 34 | postag) 35 | } 36 | 37 | @CompileStatic 38 | String toString() { 39 | def w = safeguard(word) 40 | def l = safeguard(lemma) 41 | "$w\t$l\t$postag" 42 | } 43 | 44 | @CompileStatic 45 | static String getPostagCore(String postag) { 46 | postag != null ? POSTAG_CORE_REMOVE_PATTERN.matcher(postag).replaceAll('') : postag 47 | } 48 | 49 | @CompileStatic 50 | static String safeguard(String w) { 51 | if( w == '' ) return '^' 52 | 53 | w //w.indexOf(' ') >= 0 ? w.replace(' ', '\u2009') : w 54 | } 55 | 56 | @CompileStatic 57 | static String unsafeguard(String w) { 58 | w //w = w.indexOf('\u2009') >= 0 ? w.replace('\u2009', ' ') : w 59 | } 60 | 61 | @CompileStatic 62 | static String normalizeContextString(String w, String lemma, String postag) { 63 | if( ! w ) // possible for lemmas from AnalyzedToken 64 | return w 65 | 66 | if( postag == "number" ) { 67 | def m0 = Pattern.compile(/((1[6789]|20)[0-9]{2}[-–—])?(1[6789]|20)([0-9]{2})/).matcher(w) // preserve a year - often works as adj 68 | if( m0.matches() ) 69 | return "YY" + m0.group(4) 70 | 71 | // normalize 10 000 72 | w = w.replace(" ", "") 73 | 74 | def m1 = Pattern.compile(/([0-9]+[-—–])?([0-9]+)/).matcher(w) // we only care about last two digits 75 | if( m1.matches() ) { 76 | if( w =~ /[05-9]$/ || w =~ /1[0-9]$/ ) 77 | return 0 78 | if( w =~ /[234]$/ ) 79 | return 2 80 | if( w =~ /1$/ ) 81 | return 1 82 | // should not happen 83 | return m1.group(2) 84 | } 85 | 86 | def m2 = Pattern.compile(/([0-9,]+[–—-])?[0-9]+([,.])[0-9]+/).matcher(w) // we only care that it's decimal 87 | if( m2.matches() ) 88 | return '0,0' 89 | } 90 | 91 | String w1 = normalizeWord(w, lemma, postag) 92 | if( w1 != w ) 93 | return w1 94 | 95 | if( postag == "punct" ) { 96 | if( w == "..." ) 97 | return '…' 98 | 99 | if( w.length() == 1 ) 100 | return w.replaceAll(/^[\u2013\u2014]$/, '-') 101 | .replace('„', '«') 102 | .replace('“', '»') 103 | 104 | if( w.indexOf(".") > 0 ) 105 | return w.replaceAll(/^([?!])\.+$/, '$1') 106 | } 107 | 108 | boolean hasLowerCaseLemma = lemma && lemma =~ /^[а-яіїєґ]/ 109 | w = hasLowerCaseLemma ? w.toLowerCase() : w 110 | 111 | // if( postag == "prep" ) { 112 | // if( w=="із" || w=="зо" ) 113 | // return "з" 114 | // if( w=="у" ) 115 | // return "в" 116 | // } 117 | // else if( postag == "conj:coord" ) { 118 | // if( w=="й" ) 119 | // return "і" 120 | // } 121 | 122 | return w 123 | } 124 | 125 | @CompileStatic 126 | static String normalizeWord(String w, String lemma, String postag) { 127 | w = w.replace('\u2013', '-') 128 | // 2000-го -> 0-го 129 | // 101-річчя -> 101-річчя 130 | if( w.indexOf('-') > 0 && postag =~ /^(adj|noun)/ ) { 131 | def m1 = Pattern.compile(/[0-9-]*([0-9])-([а-яіїєґ]+)/).matcher(w) 132 | if( m1.matches() ) 133 | return m1.replaceFirst('$1-$2') 134 | } 135 | return w 136 | } 137 | 138 | // його|що 139 | private static final USE_RIGHT_CTX_PATTERN = ~(/є|її|це|саме|[ву]с[еі]|за/ 140 | + /|всередині|відповідно|перед|протягом|близько|навколо|довкола|наприкінці|неподалік|[ву]глиб|поза/ 141 | + /|брати|(українськ|англійськ)(а|у|ою|ій)|рівні|доросл.*|майбутн(є|ього|ім|ому)/ 142 | + /|більше|добре|геть|тільки/) 143 | 144 | static boolean useRightContext(String token) { 145 | token.toLowerCase() ==~ USE_RIGHT_CTX_PATTERN 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/clean/EncodingModule.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.other.clean 2 | 3 | import java.nio.charset.StandardCharsets 4 | 5 | import groovy.transform.CompileStatic 6 | import groovy.transform.PackageScope 7 | 8 | @PackageScope 9 | @CompileStatic 10 | class EncodingModule { 11 | private static final String UTF8 = StandardCharsets.UTF_8.name() 12 | 13 | OutputTrait out 14 | 15 | String getText(File file) { 16 | String text = null 17 | 18 | if( file.length() > 10 ) { 19 | FileInputStream fis = new FileInputStream(file) 20 | byte[] bytes = new byte[1024] 21 | fis.read(bytes); 22 | fis.close() 23 | 24 | if( file.length() > 100 ) { 25 | if( file.bytes[0..3] == [0x50, 0x4B, 0x03, 0x04] ) { 26 | out.println "\tERROR: found zip file, perhaps it's a Word document?" 27 | return null 28 | } 29 | 30 | if( new String(bytes).startsWith("{\\rtf") ) { 31 | out.println "\tERROR: found \"{\\rtf\", perhaps it's an RTF document?" 32 | return null 33 | } 34 | } 35 | 36 | if( ! isUTF8(bytes) ) { 37 | out.println "\tNOTE: file is not in UTF-8 encoding" 38 | 39 | text = file.getText(UTF8) 40 | text = fixEncoding(text, file) 41 | if( text == null ) 42 | return null 43 | } 44 | } 45 | 46 | if( text == null ) { 47 | text = file.getText(UTF8) 48 | } 49 | text 50 | } 51 | 52 | String tryEncoding(File file, String encoding) { 53 | def cp1251Text = file.getText(encoding) 54 | if( cp1251Text =~ /(?iu)[сц]ьк|ння|від|[іи]й|ої|ти| [ійвузао] | н[еі] | що / ) { 55 | return cp1251Text 56 | } 57 | return null 58 | } 59 | 60 | @CompileStatic 61 | String fixEncoding(String text, File file) { 62 | if( text.contains("\u008D\u00C3") ) { // completely broken encoding for «ій» 63 | out.println "\tWARNING: nonfixable broken encoding found, garbage will be left in!" 64 | return null 65 | } 66 | 67 | if( text.contains("éîãî") ) { 68 | out.println "\tWARNING: broken encoding" 69 | 70 | // some text (esp. converted from pdf) have broken encoding in some lines and good one in others 71 | 72 | int convertedLines = 0 73 | int goodLines = 0 74 | text = text.split(/\n/).collect { String line-> 75 | if( line.trim() && ! (line =~ /(?iu)[а-яіїєґ]/) ) { 76 | line = new String(line.getBytes("cp1252"), "cp1251") 77 | convertedLines += 1 78 | } 79 | else { 80 | goodLines += 1 81 | } 82 | line 83 | } 84 | .join('\n') 85 | 86 | 87 | if( text.contains("éîãî") ) { 88 | out.println "\tERROR: still broken: encoding mixed with good one" 89 | return null 90 | } 91 | 92 | // text = text.replaceAll(/([бвгґдзклмнпрстфхцшщ])\?([єїюя])/, '$1\'$2') 93 | 94 | out.println "\tEncoding fixed (good lines: $goodLines, convertedLines: $convertedLines, text: " + CleanTextCore.getSample(text) 95 | } 96 | else { 97 | ["cp1251", "utf-16"].each { encoding -> 98 | String decodedText = tryEncoding(file, encoding) 99 | if( decodedText ) { 100 | out.println "\tencoding found: $encoding" 101 | 102 | text = decodedText 103 | 104 | if( text.size() < 10 ) { 105 | out.println "\tFile size < 10 chars, probaby $encoding conversion didn't work, skipping" 106 | return null 107 | } 108 | 109 | out.println "\tEncoding converted: " + CleanTextCore.getSample(text) 110 | } 111 | } 112 | } 113 | 114 | if( text.contains("\uFFFD") ) { 115 | out.println "\tERROR: File contains Unicode 'REPLACEMENT CHARACTER' (U+FFFD)" 116 | return null 117 | } 118 | 119 | return text 120 | } 121 | 122 | @CompileStatic 123 | private static boolean isUTF8(byte[] pText) { 124 | 125 | int expectedLength = 0; 126 | 127 | for (int i = 0; i < pText.length && i < 300; i++) { 128 | if ((pText[i] & 0b10000000) == 0b00000000) { 129 | expectedLength = 1; 130 | } else if ((pText[i] & 0b11100000) == 0b11000000) { 131 | expectedLength = 2; 132 | } else if ((pText[i] & 0b11110000) == 0b11100000) { 133 | expectedLength = 3; 134 | } else if ((pText[i] & 0b11111000) == 0b11110000) { 135 | expectedLength = 4; 136 | } else if ((pText[i] & 0b11111100) == 0b11111000) { 137 | expectedLength = 5; 138 | } else if ((pText[i] & 0b11111110) == 0b11111100) { 139 | expectedLength = 6; 140 | } else { 141 | return false; 142 | } 143 | 144 | while (--expectedLength > 0) { 145 | if (++i >= pText.length) { 146 | return false; 147 | } 148 | if ((pText[i] & 0b11000000) != 0b10000000) { 149 | return false; 150 | } 151 | } 152 | } 153 | 154 | return true; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/CheckText.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other 4 | 5 | // This script checks the text with LanguageTool 6 | // NOTE: it disables some rules, like spelling, double whitespace etc 7 | 8 | @Grab(group='org.languagetool', module='languagetool-core', version='6.6') 9 | @Grab(group='org.languagetool', module='language-uk', version='6.6') 10 | @Grab(group='ch.qos.logback', module='logback-classic', version='1.4.+') 11 | @Grab(group='info.picocli', module='picocli', version='4.6.+') 12 | 13 | 14 | import groovy.transform.CompileStatic 15 | 16 | import org.languagetool.JLanguageTool 17 | import org.languagetool.MultiThreadedJLanguageTool 18 | import org.languagetool.language.Ukrainian 19 | import org.languagetool.rules.Rule 20 | import org.languagetool.rules.RuleMatch 21 | import org.languagetool.tokenizers.SRXSentenceTokenizer 22 | 23 | import picocli.CommandLine 24 | import picocli.CommandLine.Option 25 | import picocli.CommandLine.ParameterException 26 | 27 | 28 | class CheckText { 29 | private static final String RULES_TO_IGNORE="MORFOLOGIK_RULE_UK_UA,COMMA_PARENTHESIS_WHITESPACE,WHITESPACE_RULE," \ 30 | + "UK_MIXED_ALPHABETS,UK_SIMPLE_REPLACE,UK_SIMPLE_REPLACE_SOFT,EUPHONY_OTHER,EUPHONY_PREP_V_U,INVALID_DATE,YEAR_20001," \ 31 | + "DATE_WEEKDAY1,DASH,UK_HIDDEN_CHARS,UPPER_INDEX_FOR_M,DEGREES_FOR_C,OVKA_FOR_PROCESS" 32 | 33 | 34 | final JLanguageTool langTool = new MultiThreadedJLanguageTool(new Ukrainian()); 35 | final SRXSentenceTokenizer stokenizer = new SRXSentenceTokenizer(new Ukrainian()); 36 | final List allRules = langTool.getAllRules() 37 | 38 | 39 | List check(String text, boolean force, List errorLines) { 40 | if( ! force && text.trim().isEmpty() ) 41 | return 42 | 43 | List matches = langTool.check(text); 44 | 45 | if( matches.size() > 0 ) { 46 | printMatches(matches, text, errorLines) 47 | } 48 | 49 | return matches 50 | } 51 | 52 | 53 | @CompileStatic 54 | void printMatches(List matches, String text, List errorLines) { 55 | 56 | def i = 0 57 | def total = 0 58 | 59 | def lines = text.split("\n") 60 | 61 | for (RuleMatch match : matches) { 62 | errorLines << "Rule ID: ${match.getRule().getId()}".toString() 63 | errorLines << "Message: " + match.getMessage().replace("", "«").replace("", "»") 64 | 65 | def chunkOffset = 0 66 | def leftOff = 40 67 | def rightOff = 40 68 | def posInSent = match.getFromPos() - leftOff 69 | def posToInSent = match.getToPos() + rightOff 70 | 71 | def prefix = "" 72 | def suffix = "" 73 | if( posInSent <= 0 ) { 74 | posInSent = 0 75 | } 76 | else { 77 | prefix = "…" 78 | chunkOffset = 1 79 | } 80 | if( posToInSent >= text.length() ) { 81 | posToInSent = text.length() 82 | } 83 | else { 84 | suffix = "…" 85 | } 86 | 87 | def sample = text[posInSent.. - .txt + .tagged.txt/.xml)"]) 106 | // String output 107 | boolean quiet 108 | @Option(names= ["-h", "--help"], usageHelp= true, description= "Show this help message and exit.") 109 | boolean helpRequested 110 | } 111 | 112 | @CompileStatic 113 | static CheckOptions parseOptions(String[] argv) { 114 | CheckOptions options = new CheckOptions() 115 | CommandLine commandLine = new CommandLine(options) 116 | try { 117 | commandLine.parseArgs(argv) 118 | if (options.helpRequested) { 119 | commandLine.usage(System.out) 120 | System.exit 0 121 | } 122 | } catch (ParameterException ex) { 123 | println ex.message 124 | commandLine.usage(System.out) 125 | System.exit 1 126 | } 127 | 128 | options 129 | } 130 | 131 | 132 | static void main(String[] argv) { 133 | 134 | CheckOptions options = parseOptions(argv) 135 | 136 | 137 | def nlpUk = new CheckText() 138 | nlpUk.langTool.disableRules(Arrays.asList(RULES_TO_IGNORE.split(","))) 139 | 140 | def textToAnalyze = new File(options.input).text 141 | 142 | def paragraphs = textToAnalyze.split("\n\n") 143 | 144 | long tm1 = System.currentTimeMillis() 145 | 146 | 147 | paragraphs.each { para -> 148 | def errors = [] 149 | nlpUk.check(para, false, errors) 150 | errors.each { println it } 151 | } 152 | 153 | def errors = [] 154 | nlpUk.check("", false, []) 155 | errors.each { println it } 156 | 157 | long tm2 = System.currentTimeMillis() 158 | 159 | println String.format("Check time: %d ms, (%d chars/sec), %d paragraphs", 160 | tm2-tm1, (int)(textToAnalyze.length()*1000/(tm2-tm1)), paragraphs.size()) 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/other/clean/CleanSpacingTest.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other.clean 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | import static org.junit.jupiter.api.Assertions.assertFalse 7 | import static org.junit.jupiter.api.Assertions.assertTrue 8 | import static org.junit.jupiter.api.Assumptions.assumeTrue 9 | 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Disabled 12 | import org.junit.jupiter.api.Test 13 | 14 | import groovy.transform.CompileStatic 15 | import ua.net.nlp.other.clean.CleanOptions 16 | import ua.net.nlp.other.clean.CleanOptions.MarkOption 17 | import ua.net.nlp.other.clean.CleanOptions.ParagraphDelimiter 18 | import ua.net.nlp.other.clean.CleanTextCore 19 | import ua.net.nlp.other.clean.SpacingModule.Node 20 | 21 | 22 | @CompileStatic 23 | class CleanSpacingTest { 24 | 25 | CleanOptions options = new CleanOptions("wordCount": 0, "debug": true) 26 | 27 | CleanTextCore cleanText0 = new CleanTextCore( options ) 28 | CleanTextCore2 cleanText 29 | 30 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream() 31 | 32 | @BeforeEach 33 | public void init() { 34 | cleanText0.out.out = new PrintStream(outputStream) 35 | cleanText = new CleanTextCore2(cleanText0.out, options, cleanText0.ltModule) 36 | } 37 | 38 | @CompileStatic 39 | String clean(String str) { 40 | str = str.replace('_', '') 41 | cleanText0.cleanText(str, null, null, cleanText.out) 42 | } 43 | 44 | 45 | @Test 46 | public void testGetText() { 47 | def txt = cleanText.spacingModule.getText(new Node(word: "голова"), "") 48 | assertEquals( ["голова"], txt ) 49 | 50 | txt = cleanText.spacingModule.getText(new Node(word: "голова", 51 | children: ([new Node(word:"його"), new Node(word:"її", 52 | children: ([new Node(word: "кудлата")]))])), "") 53 | assertEquals (["голова його", "голова її кудлата"], txt) 54 | } 55 | 56 | @Test 57 | public void testSpacing() { 58 | // simple cases 59 | assertEquals "Сесійний зал Верховної Ради", clean("С е с і й н и й\u00A0 з а л\u00A0 В е р х о в н о ї\u00A0 Р а д и") 60 | 61 | // heuristics 62 | String text = cleanText.spacingModule.removeSpacing("м и м и л и р а м у") 63 | assertEquals("ми мили раму", text) 64 | 65 | text = cleanText.spacingModule.removeSpacing("В у г і л л я н е б у л о") 66 | assertEquals("Вугілля не було", text) 67 | 68 | text = cleanText.spacingModule.removeSpacing "Капітуляція ще п е р е д б о є м ." 69 | assertEquals("Капітуляція ще перед боєм.", text) 70 | 71 | text = cleanText.spacingModule.removeSpacing """Капітуляція ще п е р е д б о є м . 72 | В у г і л л я н е б у л о 73 | м и м и л и р а м у 74 | """ 75 | assertEquals("""Капітуляція ще перед боєм. 76 | Вугілля не було 77 | ми мили раму 78 | """ 79 | , text) 80 | 81 | 82 | text = cleanText.spacingModule.removeSpacing("с т а н ц і ю н а Д н і п р і н а н і ч н у з м і н у") 83 | // assertEquals("станцію на Дніпрі на нічну зміну", text) 84 | 85 | text = cleanText.spacingModule.removeSpacing("м и й м е н і в і д с і ч н я п р о ф . В . Д е р ж а в и н : н е п о к а з н а і в ж е л і т н я") 86 | assertEquals("мий мені від січня проф. В. Державин: непоказна і вже літня", text) 87 | // assertEquals("мий мені від січня проф. В. Державин: не показна і вже літня", text) 88 | 89 | def src = "— Г м . . . — м у р к н у в в і н ." 90 | text = cleanText.spacingModule.removeSpacing(src) 91 | assertEquals("— Гм... — муркнув він.", text) 92 | 93 | src = "с а м і з с в о ї м к о ч е г а р с ь к и м о б о в ’я з к о м" 94 | text = cleanText.spacingModule.removeSpacing(src) 95 | assertEquals("самі з своїм кочегарським обов’язком", text) 96 | 97 | def txt = getClass().getClassLoader().getResource("clean/spacing.txt").text 98 | println cleanText.spacingModule.removeSpacing(txt) 99 | 100 | // too slow 101 | if( false ) { 102 | src = "В о н и п о с к л и к а л и в с і х л о т и ш і в і л о т и ш о к і з і н ш и х к і м н а т і в с е т е ч у ж о м о в н е т о в а р и с т в о д и в и л о с я н а" 103 | text = cleanText.spacingModule.removeSpacing(src) 104 | assertEquals("Вони поскликали всіх лотишів і лотишок із інших кімнат і все те чужомовне товариство дивилося на", text) 105 | } 106 | 107 | // don't touch 108 | assertUntouched "senior'и" 109 | } 110 | 111 | @Test 112 | public void testSpacingText4() { 113 | assumeTrue Boolean.getBoolean("clean_spacing_full") 114 | 115 | cleanText.spacingModule.fullSpacing = true 116 | 117 | def txt = getClass().getClassLoader().getResource("clean/spacing4.txt").text 118 | new File("src/test/resources/clean/spacing4_done.txt").text = cleanText.spacingModule.removeSpacing(txt) 119 | } 120 | 121 | 122 | @Test 123 | public void testSpacingText() { 124 | assumeTrue Boolean.getBoolean("clean_spacing_full") 125 | 126 | cleanText.spacingModule.fullSpacing = true 127 | 128 | def txt = getClass().getClassLoader().getResource("clean/spacing0.txt").text 129 | new File("src/test/resources/clean/spacing0_done.txt").text = cleanText.spacingModule.removeSpacing(txt) 130 | } 131 | 132 | @CompileStatic 133 | void assertUntouched(String txt) { 134 | assertEquals txt, clean(txt) 135 | } 136 | 137 | @Test 138 | public void testMergeG() { 139 | assertEquals "Головний", clean("Г оловний") 140 | assertEquals "Г полова", clean("Г полова") 141 | } 142 | 143 | @Test 144 | public void testTtModule() { 145 | assertTrue cleanText.spacingModule.goodWord("лотиш") 146 | assertTrue cleanText.spacingModule.goodWord("лотишів") 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/other/EvaluateText.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | // This script checks the text with LanguageTool 4 | // and prints a error rating (along with count and total # of words) 5 | // 6 | // NOTE: it disables some rules, like spelling, double whitespace etc 7 | 8 | package ua.net.nlp.other 9 | 10 | import org.languagetool.JLanguageTool 11 | import org.languagetool.MultiThreadedJLanguageTool 12 | import org.languagetool.language.Ukrainian 13 | import org.languagetool.rules.Rule 14 | import org.languagetool.rules.RuleMatch 15 | import org.languagetool.tokenizers.SRXSentenceTokenizer 16 | 17 | import groovy.transform.CompileStatic 18 | 19 | 20 | class EvaluateText { 21 | private static final String RULES_TO_IGNORE="MORFOLOGIK_RULE_UK_UA,COMMA_PARENTHESIS_WHITESPACE,WHITESPACE_RULE," \ 22 | + "EUPHONY_PREP_V_U,EUPHONY_CONJ_I_Y,EUPHONY_PREP_Z_IZ_ZI,EUPHONY_PREP_O_OB" \ 23 | + "DATE_WEEKDAY1,DASH,UK_HIDDEN_CHARS,UPPER_INDEX_FOR_M,DEGREES_FOR_C,DIGITS_AND_LETTERS," \ 24 | + "UK_MIXED_ALPHABETS,UK_SIMPLE_REPLACE_SOFT" 25 | //,UK_SIMPLE_REPLACE,INVALID_DATE,YEAR_20001," 26 | 27 | 28 | final JLanguageTool langTool = new MultiThreadedJLanguageTool(new Ukrainian()); 29 | final SRXSentenceTokenizer stokenizer = new SRXSentenceTokenizer(new Ukrainian()); 30 | final List allRules = langTool.getAllRules() 31 | 32 | 33 | List check(String text, boolean force, List errorLines) { 34 | if( ! force && text.trim().isEmpty() ) 35 | return 36 | 37 | List matches = langTool.check(text); 38 | 39 | if( matches.size() > 0 ) { 40 | printMatches(matches, text, errorLines) 41 | } 42 | 43 | return matches 44 | } 45 | 46 | 47 | @CompileStatic 48 | void printMatches(List matches, String text, List errorLines) { 49 | 50 | def i = 0 51 | def total = 0 52 | 53 | def lines = text.split("\n") 54 | 55 | for (RuleMatch match : matches) { 56 | errorLines << "Rule ID: ${match.getRule().getId()}".toString() 57 | errorLines << "Message: " + match.getMessage().replace("", "«").replace("", "»") 58 | 59 | def chunkOffset = 0 60 | def leftOff = 40 61 | def rightOff = 40 62 | def posInSent = match.getFromPos() - leftOff 63 | def posToInSent = match.getToPos() + rightOff 64 | 65 | def prefix = "" 66 | def suffix = "" 67 | if( posInSent <= 0 ) { 68 | posInSent = 0 69 | } 70 | else { 71 | prefix = "…" 72 | chunkOffset = 1 73 | } 74 | if( posToInSent >= text.length() ) { 75 | posToInSent = text.length() 76 | } 77 | else { 78 | suffix = "…" 79 | } 80 | 81 | def sample = text[posInSent.. 0 ? args[0] : "." 103 | def outDir = "$dir/err" 104 | 105 | def outDirFile = new File(outDir) 106 | if( ! outDirFile.isDirectory() ) { 107 | System.err.println "Output dir $outDir does not exists" 108 | return 109 | } 110 | 111 | 112 | def nlpUk = new EvaluateText() 113 | nlpUk.langTool.disableRules(Arrays.asList(RULES_TO_IGNORE.split(","))) 114 | 115 | 116 | def ratings = ["коеф помил унік слів файл"] 117 | new File("$outDir/ratings.txt").text = "" 118 | 119 | new File(dir).eachFile { file-> 120 | if( ! file.name.endsWith(".txt") ) 121 | return 122 | 123 | 124 | def text = file.text 125 | List errorLines = [] 126 | 127 | 128 | println(String.format("checking $file.name, words: %d, size: %d", word_count(text), text.size())) 129 | 130 | def paragraphs = text.split("\n\n") 131 | 132 | int matchCnt = 0 133 | int uniqueRules = 0 134 | 135 | try { 136 | paragraphs.each { String para -> 137 | def matches = nlpUk.check(para, false, errorLines) 138 | if( matches ) { 139 | matchCnt += matches.size() 140 | uniqueRules += getUniqueRuleCount(matches) 141 | } 142 | } 143 | 144 | def matches = nlpUk.check("", true, errorLines) 145 | if( matches ) { 146 | matchCnt += matches.size() 147 | uniqueRules += getUniqueRuleCount(matches) 148 | } 149 | 150 | def wc = word_count(text) 151 | def rating = Math.round(matchCnt * 10000 / wc)/100 152 | ratings << String.format("%1.2f %4d %4d %6d %s", rating, matchCnt, uniqueRules, wc, file.name) 153 | 154 | new File(outDir + "/" + file.name.replace(".txt", ".err.txt")).text = errorLines.join("\n") 155 | errorLines.clear() } 156 | catch(Exception e) { 157 | e.printStackTrace(); 158 | } 159 | } 160 | 161 | new File("$outDir/ratings.txt").text = ratings.join("\n") 162 | 163 | } 164 | 165 | 166 | static getUniqueRuleCount(matches) { 167 | matches.collect{ it.rule.id == "UK_SIMPLE_REPLACE" ? it.message : it.rule.id }.unique().size() 168 | } 169 | } 170 | 171 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tag/SemTags.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.regex.Pattern 7 | 8 | import org.apache.commons.lang3.StringUtils 9 | import org.apache.commons.lang3.mutable.MutableInt; 10 | import org.languagetool.AnalyzedToken; 11 | import org.languagetool.AnalyzedTokenReadings; 12 | import org.languagetool.rules.uk.LemmaHelper 13 | import ua.net.nlp.bruk.ContextToken 14 | import ua.net.nlp.bruk.WordContext; 15 | import ua.net.nlp.bruk.WordReading; 16 | import ua.net.nlp.tools.tag.TagOptions 17 | 18 | import groovy.transform.CompileStatic; 19 | 20 | 21 | @CompileStatic 22 | public class SemTags { 23 | static final String baseDir = "/ua/net/nlp/dict_uk/semtags" 24 | static Map ADVP_DERIVATS 25 | 26 | def categories = ["noun", "adj", "adv", "verb", "numr"] 27 | 28 | TagOptions options 29 | Map>> semanticTags = new HashMap<>() 30 | 31 | 32 | def loadSemTags() { 33 | if( semanticTags.size() > 0 ) 34 | return 35 | 36 | long tm1 = System.currentTimeMillis() 37 | int semtagCount = 0 38 | 39 | categories.each { cat -> 40 | 41 | def fileName = "$baseDir/${cat}.csv" 42 | def csvFile = getClass().getResource(fileName) 43 | assert csvFile, "$fileName not found" 44 | 45 | String text = csvFile.getText('UTF-8') 46 | 47 | if( text.startsWith("\uFEFF") ) { 48 | text = text.substring(1) 49 | } 50 | 51 | text.eachLine { line -> 52 | line = line.trim().replaceFirst(/\s*#.*/, '') 53 | if( ! line ) 54 | return 55 | 56 | def parts = line.split(',') 57 | if( parts.length < 2 ) { 58 | System.err.println("skipping invalid semantic tag for: \"$line\"") 59 | return 60 | } 61 | 62 | def add = "" 63 | if( parts.length >= 3 && parts[2].trim().startsWith(':') ) { 64 | add = parts[2].trim() 65 | } 66 | 67 | def word = parts[0] 68 | def semtags = parts[1] 69 | def pos = cat 70 | def key = word + " " + pos 71 | 72 | if( ! (key in semanticTags) ) { 73 | semanticTags[key] = [:] 74 | } 75 | if( ! (add in semanticTags[key]) ) { 76 | semanticTags[key][add] = [] 77 | } 78 | 79 | // semtags sometimes have duplicate lines 80 | if( ! (semtags in semanticTags[key][add]) ) { 81 | semanticTags[key][add] << semtags 82 | semtagCount += 1 83 | } 84 | } 85 | } 86 | 87 | ADVP_DERIVATS = getClass().getResource("/org/languagetool/resource/uk/derivats.txt").readLines().collectEntries { 88 | def parts = it.split() 89 | [(parts[0]) : parts[1]] 90 | } 91 | 92 | if( ! options.quiet ) { 93 | long tm2 = System.currentTimeMillis() 94 | System.err.println("Loaded $semtagCount semantic tags for ${semanticTags.size()} lemmas in ${tm2-tm1} ms") 95 | } 96 | } 97 | 98 | 99 | String getSemTags(AnalyzedToken tkn, String posTag) { 100 | if( options.semanticTags && tkn.getLemma() != null && posTag != null ) { 101 | def lemma = tkn.getLemma() 102 | String posTagKey = posTag.replaceFirst(/:.*/, '') 103 | 104 | if( posTagKey.startsWith("advp") ) { 105 | lemma = ADVP_DERIVATS[lemma] 106 | posTagKey = "verb" 107 | if( ! lemma ) 108 | return null 109 | } 110 | 111 | String key = "$lemma $posTagKey" 112 | 113 | Map> potentialSemTags = semanticTags.get(key) 114 | 115 | if( ! potentialSemTags ) { 116 | if( key.contains("ґ") ) { 117 | potentialSemTags = semanticTags.get(key.replace("ґ", "г")) 118 | } 119 | else if( key.contains("ія") ) { 120 | potentialSemTags = semanticTags.get(key.replace("ія", "іа")) 121 | } 122 | else if( key.contains("тер") ) { 123 | potentialSemTags = semanticTags.get(key.replaceFirst(/тер$/, "тр")) 124 | } 125 | else if( key.contains("льо") ) { 126 | potentialSemTags = semanticTags.get(key.replace("льо", "ло")) 127 | } 128 | } 129 | 130 | if( potentialSemTags ) { 131 | potentialSemTags = potentialSemTags.findAll { k,v -> ! k || posTag.contains(k) } 132 | List values = (java.util.List) potentialSemTags.values().flatten() 133 | def resultSemTags = values.findAll { filterSemtag(lemma, posTag, it) } 134 | return resultSemTags ? resultSemTags.join(';') : null 135 | } 136 | } 137 | return null 138 | } 139 | 140 | 141 | private static boolean filterSemtag(String lemma, String posTag, String semtag) { 142 | if( posTag.contains("pron") ) 143 | return semtag =~ ":deictic|:quantif" 144 | 145 | if( posTag.startsWith("noun") ) { 146 | 147 | if( posTag.contains(":anim") ) { 148 | if( posTag.contains("name") ) 149 | return semtag =~ ":hum|:supernat" 150 | else 151 | return semtag =~ ":hum|:supernat|:animal|:org" 152 | } 153 | else if( posTag.contains(":unanim") ) { 154 | return semtag.contains(":animal") 155 | } 156 | else if( Character.isUpperCase(lemma.charAt(0)) && posTag.contains(":geo") ) { 157 | return semtag.contains(":loc") 158 | } 159 | 160 | return semtag =~ /:hum:group/ || ! (semtag =~ /:hum|:supernat|:animal/) 161 | } 162 | 163 | return true 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/other/clean/CleanHyphenTest.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | package ua.net.nlp.other.clean 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | import static org.junit.jupiter.api.Assertions.assertFalse 7 | import static org.junit.jupiter.api.Assertions.assertTrue 8 | import static org.junit.jupiter.api.Assumptions.assumeTrue 9 | 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Disabled 12 | import org.junit.jupiter.api.Test 13 | 14 | import groovy.transform.CompileStatic 15 | import ua.net.nlp.other.clean.CleanOptions 16 | import ua.net.nlp.other.clean.CleanOptions.MarkOption 17 | import ua.net.nlp.other.clean.CleanOptions.ParagraphDelimiter 18 | import ua.net.nlp.other.clean.CleanTextCore 19 | 20 | 21 | @CompileStatic 22 | class CleanHyphenTest { 23 | CleanOptions options = new CleanOptions("wordCount": 0, "debug": true) 24 | 25 | CleanTextCore cleanText = new CleanTextCore( options ) 26 | 27 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream() 28 | 29 | @BeforeEach 30 | public void init() { 31 | cleanText.out.out = new PrintStream(outputStream) 32 | } 33 | 34 | @CompileStatic 35 | String clean(String str) { 36 | str = str.replace('_', '') 37 | cleanText.cleanText(str, null, null, cleanText.out) 38 | } 39 | 40 | 41 | @Test 42 | public void testWordWrap() { 43 | assertEquals "урахування\n", clean("ураху-\nвання") 44 | assertEquals "Прем’єр-ліги\n", clean("Прем’єр-\nліги") 45 | // assertEquals "інформаційно\u2013звітний\n", clean("інформаційно\u2013\nзвітний") 46 | 47 | assertEquals "екс-«депутат»\n", clean("екс-\n«депутат»") 48 | 49 | assertEquals "\"депутат\" H'''", clean("''депутат'' H'''") 50 | 51 | assertEquals "Інтерфакс-Україна\n", clean("Інтерфакс-\nУкраїна") 52 | 53 | def result = clean('просто-\nрово-часового') 54 | assert result == "просторово-часового\n" 55 | //result = clean('двох-\nсторонній', file, []) 56 | //assert result == "двохсторонній\n" 57 | 58 | //TODO: 59 | result = clean("минулого-сучасного-май-\nбутнього") 60 | assert result == "минулого-сучасного-майбутнього\n" 61 | 62 | result = clean("благо-\nдійної") 63 | assert result == "благодійної\n" 64 | 65 | // with space 66 | result = clean("кудись- \nінде") 67 | assert result == "кудись-інде\n" 68 | 69 | // with 2 new lines 70 | result = clean("сукуп-\n \n ність") 71 | assertEquals "сукупність\n ", result 72 | 73 | // result = clean("сукуп-\n --- \n ність") 74 | // assertEquals "сукупність\n ", result 75 | 76 | assertEquals "сьо-годні", clean("сьо-годні") 77 | 78 | assertEquals "єдності\n", clean("єднос‑\nті") 79 | 80 | outputStream.reset() 81 | def txt = "кількістю макро-\n та мікропор.\nу таблиці 1.\n---\n Для" 82 | assertEquals txt, clean(txt) 83 | // println ":: " + new String(outputStream.toByteArray()) 84 | assertFalse(new String(outputStream.toByteArray()).contains("---")) 85 | } 86 | 87 | @Test 88 | public void testRemove00AD() { 89 | assertEquals "Залізнична", clean("За\u00ADлізнична") 90 | assertEquals "АБ", clean("А\u200BБ") 91 | assertEquals "А Б", clean("А \u200BБ") 92 | assertEquals "14-го", clean("14\u00ADго") 93 | assertEquals "необов’язковий\n", clean("необов’\u00AD\nязковий") 94 | assertEquals "Івано-франківський", clean("Івано-\u00ADфранківський") 95 | } 96 | 97 | @Test 98 | public void testRemove001D() { 99 | assertEquals "Баренцово-Карського", clean("Баренцово\u001DКарського") 100 | assertEquals "прогинання\n ", clean("прогинан\u001D\n ня") 101 | // assertEquals "Азово-Чорноморського\n", clean("Азово\u001D\nЧорноморського") 102 | assertEquals "Азово-Чорноморського\n", clean("Азово\u001DЧорно\u001D\nморського") 103 | } 104 | 105 | @Test 106 | public void testRemove00AC() { 107 | assertEquals "загальновідоме", clean("загальновідо¬ме") 108 | assertEquals "по-турецьки", clean("по¬турецьки") 109 | assertEquals "10-11", clean("10¬11") 110 | assertEquals "о¬е", clean("о¬е") 111 | assertEquals "екс-глава", clean("екс¬глава") 112 | assertEquals "конкурент", clean("конку¬ рент") 113 | // too hard for now 114 | // assertEquals "загальновідоме", clean("загально¬відо¬ме") 115 | } 116 | 117 | @Test 118 | public void testLeadingHyphen() { 119 | assertEquals "- Агов", clean("-Агов") 120 | assertEquals "-УВАТ(ИЙ)", clean("-УВАТ(ИЙ)") 121 | 122 | assertEquals "- архієпископ\n- Дитина", clean("-архієпископ\n-Дитина") 123 | assertEquals "-то ", clean("-то ") 124 | 125 | assertEquals "сказав він. - Подорожчання викликане", clean("сказав він. -Подорожчання викликане") 126 | assertEquals "люба моя,- Євген.", clean("люба моя,-Євген.") 127 | assertEquals "заперечив Денетор. - Я вже лічу", clean("заперечив Денетор. -Я вже лічу") 128 | def t = "Т. 2. -С. 212" 129 | assertEquals t, clean(t) 130 | 131 | assertEquals "будь-яким", clean("будь -яким") 132 | assertEquals "будь-що-будь", clean("будь - що - будь") 133 | 134 | // skip 135 | assertEquals("Слова на -овець", clean("Слова на -овець")) 136 | } 137 | 138 | @Disabled 139 | @Test 140 | public void testHyphenWithSpace() { 141 | assertEquals "свободи", clean("сво- боди") 142 | //FP 143 | assertEquals "теле- і радіопрограм", clean("теле- і радіопрограм") 144 | } 145 | 146 | @Test 147 | public void testHyphenWithSpace2() { 148 | assertEquals "150-річчя", clean("150- річчя") 149 | assertEquals "150-річчя", clean("150 \n- річчя") 150 | assertEquals "5-го  листопада", clean("5 -го  листопада") 151 | assertEquals "д-р", clean("д - р") 152 | // skip 153 | assertEquals "від - роб", clean("від - роб") 154 | } 155 | 156 | 157 | @Test 158 | public void testTilda() { 159 | def result = clean("по~християнськи") 160 | assertEquals "по-християнськи", result 161 | 162 | result = clean("для~мене") 163 | assertEquals "для мене", result 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tag/TagUnknown.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag; 2 | 3 | import java.util.regex.Pattern 4 | 5 | import org.languagetool.AnalyzedTokenReadings 6 | import org.languagetool.tools.StringTools 7 | 8 | import groovy.transform.CompileDynamic 9 | import groovy.transform.CompileStatic 10 | import ua.net.nlp.bruk.WordReading 11 | import ua.net.nlp.tools.tag.TagTextCore.TaggedToken 12 | 13 | 14 | @CompileStatic 15 | public class TagUnknown { 16 | private static final String statsFile = "/ua/net/nlp/tools/stats/lemma_suffix_freqs.txt" 17 | 18 | Map> lemmaSuffixStatsF = [:].withDefault { [:].withDefault { 0 } } 19 | int lemmaSuffixLenB = 4 20 | 21 | TagUnknown() { 22 | } 23 | 24 | @CompileDynamic 25 | void loadStats() { 26 | if( lemmaSuffixStatsF.size() > 0 ) 27 | return 28 | 29 | def statsFileRes = getClass().getResource(statsFile) 30 | assert statsFileRes, "Disambig stats not found :$statsFile" 31 | 32 | statsFileRes.eachLine { String line -> 33 | def (suffix, rs, postag, cnt) = line.split("\t+") 34 | def wr = new WordReading(rs, postag) 35 | lemmaSuffixStatsF[suffix] << [ (wr) : cnt as Integer] 36 | } 37 | } 38 | 39 | // private static Pattern DASHED = ~/(?iu)([а-яіїєґ']{4,})-([а-яіїєґ']{4,})/ 40 | 41 | List tag(String token, int idx, AnalyzedTokenReadings[] tokens) { 42 | // def m = DASHED.matcher(token) 43 | // m.find() 44 | // if( m ) { 45 | // String part1 = m.group(1) 46 | // String part2 = m.group(2) 47 | // 48 | // return tagInternal(part1, idx, tokens) 49 | // } 50 | try { 51 | return tagInternal(token, idx, tokens) 52 | } 53 | catch(Exception e) { 54 | System.err.println "Failed to find unknown for \"$token\"" 55 | e.printStackTrace() 56 | return null 57 | } 58 | } 59 | 60 | // НС-фільтрів 61 | static final Pattern PREFIXED = Pattern.compile(/([А-ЯІЇЄҐA-Z0-9]+[-\u2013])([а-яіїєґ].*)/) 62 | 63 | List tagInternal(String token, int idx, AnalyzedTokenReadings[] tokens) { 64 | if( token ==~ /[А-ЯІЇЄҐ]+-[0-9]+[а-яіїєґА-ЯІЇЄҐ]*/ ) // ФАТ-10 65 | return [new TaggedToken(value: token, lemma: token, tags: 'noninfl', confidence: -0.7)] 66 | if( token ==~ /[А-ЯІЇЄҐ]{2,6}/ ) 67 | return [new TaggedToken(value: token, lemma: token, tags: 'noninfl:abbr', confidence: -0.7)] 68 | 69 | def m = PREFIXED.matcher(token) 70 | if( m.matches() ) { 71 | String left = m.group(1) 72 | String right = m.group(2) 73 | 74 | def tagged = tagInternal(right, idx, tokens) 75 | tagged.each { tt -> 76 | tt.lemma = "$left${tt.lemma}" 77 | tt.value = "$left${tt.value}" 78 | } 79 | return tagged 80 | } 81 | 82 | int lemmaSuffixLen = token.endsWith("ться") ? lemmaSuffixLenB + 2 : lemmaSuffixLenB 83 | 84 | if( token.length() < lemmaSuffixLen + 2 ) 85 | return [] 86 | 87 | def last3 = token[-lemmaSuffixLen..-1] 88 | 89 | List retTokens 90 | 91 | Map opToTagMap = lemmaSuffixStatsF[last3] 92 | opToTagMap = opToTagMap.findAll { e -> getCoeff(e, token, idx, tokens) > 0 } 93 | 94 | if( opToTagMap ) { 95 | opToTagMap = opToTagMap.toSorted { e -> - getCoeff(e, token, idx, tokens) } 96 | 97 | retTokens = opToTagMap.collect { Map.Entry e -> 98 | def wr = e.key 99 | def parts = wr.lemma.split("/") 100 | int del = parts[1] as int 101 | 102 | if( del + 2 > token.length() ) 103 | return (TaggedToken)null 104 | 105 | String add = parts[0] 106 | def lemma = token[0..-del-1] + add 107 | 108 | def q = opToTagMap.size() > 1 ? -0.5 : -0.6 109 | 110 | if( ! wr.postag.contains(":prop") ) { 111 | lemma = lemma.toLowerCase() 112 | } 113 | 114 | return new TaggedToken(value: token, lemma: lemma, tags: wr.postag, confidence: q) 115 | } 116 | .findResults() as List 117 | } 118 | else { 119 | retTokens = [] 120 | } 121 | 122 | return retTokens 123 | } 124 | 125 | static Pattern mascPrefix = ~/пан|містер|гер|сеньйор|монсеньйор|добродій|князь/ 126 | static Pattern femPrefix = ~/пані|міс|місіс|княгиня|фрау|сеньора|сеньйоріта|мадам|маде?муазель|добродійка/ 127 | 128 | private static String gen(String postag) { 129 | def m = postag =~ /:[mf]:/ 130 | return m ? m[0] : null 131 | } 132 | 133 | 134 | private static int getCoeff(Map.Entry e, String token, int idx, AnalyzedTokenReadings[] tokens) { 135 | if( e.key.postag.contains("prop") ) { 136 | if( ! StringTools.isCapitalizedWord(token) ) { 137 | return 0 138 | } 139 | 140 | if( idx > 0 ) { 141 | if( e.key.postag.contains("lname") ) { 142 | def eGen = gen(e.key.postag) 143 | if( eGen ) { 144 | if( tokens[idx-1].getReadings().find { 145 | def tag = it.getPOSTag() 146 | if( tag == null ) return false 147 | if( tag.contains("fname") && gen(tag) == eGen ) return true 148 | 149 | it.getLemma() != null && 150 | ((eGen == ":m:" && mascPrefix.matcher(it.getLemma()).matches()) || 151 | (eGen == ":f:" && femPrefix.matcher(it.getLemma()).matches())) 152 | } ) { 153 | return e.value * 10000 154 | } 155 | } 156 | } 157 | return e.value * 100 158 | } 159 | // Зів-Кріспель 160 | else if( token =~ /[а-яіїєґ]-[А-ЯІЇЄҐ][а-яіїєґ']/ ) { 161 | return e.value * 100 162 | } 163 | } 164 | return e.value 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/main/groovy/ua/net/nlp/tools/tag/TagOptions.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tag; 2 | 3 | import groovy.transform.CompileStatic 4 | import picocli.CommandLine.Option 5 | import picocli.CommandLine.Parameters 6 | import ua.net.nlp.tools.OptionsBase 7 | import ua.net.nlp.tools.OutputFormat 8 | 9 | 10 | @CompileStatic 11 | public class TagOptions extends OptionsBase { 12 | 13 | @Parameters(index = "0", description = "Input files. Default: stdin", arity="0..") 14 | List inputFiles 15 | @Option(names = ["-r", "--recursive"], description = "Tag all files recursively in the given directories") 16 | boolean recursive 17 | @Option(names = ["--list-file"], description = "Read files to tag from the file") 18 | String listFile 19 | 20 | @Option(names = ["--lemmaOnly"], description = "Prints only lemmas, implies: --outputFormat=txt --disambiguate=true") 21 | boolean lemmaOnly 22 | 23 | @Option(names = ["-sh", "--homonymStats"], description = "Collect homohym statistics") 24 | boolean homonymStats 25 | @Option(names = ["-su", "--unknownStats"], description = "Collect unknown words statistics") 26 | boolean unknownStats 27 | @Option(names = ["-sfu", "--filterUnknown"], description = "Filter out unknown words with non-Ukrainian character combinations") 28 | boolean filterUnknown 29 | @Option(names = ["-sf", "--frequencyStats"], description = "Collect word frequency") 30 | boolean frequencyStats 31 | @Option(names = ["-sl", "--lemmaStats"], description = "Collect lemma frequency") 32 | boolean lemmaStats 33 | 34 | @Option(names = ["-e", "--semanticTags"], description = "Add semantic tags") 35 | boolean semanticTags 36 | @Option(names = ["-k", "--noTag"], description = "Do not write tagged text (only perform stats)") 37 | boolean noTag 38 | @Option(names = ["--setLemmaForUnknown"], description = "Fill lemma for unknown words (default: empty lemma)") 39 | boolean setLemmaForUnknown 40 | @Option(names = ["--separateDotAbbreviation"], description = "Will split the dot from abbreviation tokens into separate token") 41 | boolean separateDotAbbreviation 42 | 43 | @Option(names = ["-t", "--tokenFormat"], description = "Use format (instead of )") 44 | boolean tokenFormat 45 | @Option(names = ["-t1", "--singleTokenOnly"], description = "Print only one token per reading (default for when -g is specified)") 46 | boolean singleTokenOnly 47 | @Option(names = ["-ta", "--allTokenReadings"], description = "Print all readings of the token (default if -g is not specified)") 48 | boolean allTokenReadings 49 | @Option(names = ["--taggingLevel"], description = "Add a tagging level attribute") 50 | boolean showTaggingLevel 51 | 52 | @Option(names = ["-d", "--showDisambigRules"], description = "Show deterministic disambiguation rules applied") 53 | boolean showDisambigRules 54 | @Option(names = ["-g", "--disambiguate"], description = "Use statistics for disambiguation (implies -t1 abd -u)") 55 | boolean disambiguate 56 | @Option(names = ["-gr", "--disambiguationRate"], description = "Show a disambiguated token ratings") 57 | boolean showDisambigRate 58 | @Option(names = ["-gd", "--writeDisambiguationDebug"], description = "Write disambig debug info into a file") 59 | boolean disambiguationDebug 60 | @Option(names = ["-u", "--tagUnknown"], description = "Use statistics to tag unknown words") 61 | boolean tagUnknown 62 | @Option(names = ["-ur", "--tagUnknownWithRate"], description = "Use statistics to tag unknown words and print the rate") 63 | boolean unknownRate 64 | 65 | @Option(names = ["-m", "--module"], arity="1", description = "Alternative spelling module (only 1 at a time), supported modules: [zheleh, lesya]") 66 | List modules 67 | 68 | @Option(names = ["--singleNewLineAsParagraph"], description = "Split paragraphs by single new line instead of two") 69 | boolean singleNewLineAsParagraph 70 | @Option(names = ["--sentencePerLine"], description = "Assume each line is a sentence (don't use internal sentence tokenizer).") 71 | boolean sentencePerLine 72 | 73 | @Option(names = ["--singleThread"], description = "Always use single thread (default is to use multithreading if > 2 cpus are found)") 74 | boolean singleThread 75 | @Option(names = ["--timing"], description = "Pring timing information", hidden = true) 76 | boolean timing 77 | @Option(names = ["--no-disambig"], hidden = true) 78 | boolean noDisambig 79 | @Option(names = ["--progress"], description = "Print progress information every files", hidden = true) 80 | int progress=0 81 | @Option(names = ["--version"], description = "Print current version") 82 | boolean printVersion 83 | 84 | enum Module { zheleh, lesya } 85 | 86 | void adjust() { 87 | if( modules ) { 88 | def allowed = Module.values().collect { m -> m.name() } 89 | modules.each { module -> 90 | if( ! (module in allowed) ) { 91 | System.err.println("Error: invalid module $module") 92 | System.exit(1) 93 | } 94 | } 95 | } 96 | 97 | if( lemmaOnly ) { 98 | outputFormat = OutputFormat.txt 99 | disambiguate = true 100 | singleTokenOnly = true 101 | } 102 | 103 | if( ! outputFormat ) { 104 | outputFormat = outputFormat.xml 105 | } 106 | else if( outputFormat == OutputFormat.txt ) { 107 | setLemmaForUnknown = true 108 | } 109 | else if( outputFormat == OutputFormat.conllu || outputFormat == OutputFormat.vertical ) { 110 | setLemmaForUnknown = true 111 | if( ! noDisambig ) { 112 | disambiguate = true 113 | showDisambigRate = true 114 | } 115 | if( outputFormat == OutputFormat.conllu ) { 116 | splitHyphenParts = false 117 | } 118 | } 119 | 120 | if( showDisambigRate || disambiguationDebug ) { 121 | disambiguate = true 122 | } 123 | if( disambiguate ) { 124 | tokenFormat = true 125 | tagUnknown = true 126 | if( ! allTokenReadings ) { 127 | singleTokenOnly = true 128 | } 129 | } 130 | 131 | if( singleTokenOnly ) { 132 | tokenFormat = true 133 | } 134 | if( unknownRate ) { 135 | tagUnknown = true 136 | } 137 | 138 | // if( ! quiet ) { 139 | // System.err.println "Output format: " + outputFormat 140 | // } 141 | } 142 | 143 | boolean isSingleFile() { 144 | return ! recursive && ! listFile && inputFiles && inputFiles.size() == 1 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/tag/TagTextVerticalOutputTest.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | package ua.net.nlp.tools.tag 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | 10 | import ua.net.nlp.tools.OutputFormat 11 | import ua.net.nlp.tools.tag.TagTextCore.TagResult 12 | import ua.net.nlp.tools.tag.TagTextCore.TaggedToken 13 | 14 | 15 | class TagTextVerticalOutputTest { 16 | def options = new TagOptions() 17 | 18 | static TagTextCore tagText = new TagTextCore() 19 | 20 | @BeforeEach 21 | void before() { 22 | // tagText.setOptions(options) 23 | } 24 | 25 | def file() { return new File("/dev/null") } 26 | 27 | 28 | @Test 29 | public void testTxtFormat() { 30 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.vertical, unknownStats: true)) 31 | TagResult tagged = tagText.tagText("Десь, там за горою ходила Галя. А далі - озеро... — Де?") 32 | 33 | def expected = 34 | """ 35 | Десь adv:pron:ind десь 36 | 37 | , punct , 38 | там adv:pron:dem там 39 | за prep за 40 | горою noun:inanim:f:v_oru гора 41 | ходила verb:imperf:past:f ходити 42 | Галя noun:anim:f:v_naz:prop:fname Галя 43 | 44 | . punct . 45 | 46 | 47 | 48 | А conj:coord а 49 | далі adv:compc:predic далі 50 | - punct - 51 | озеро noun:inanim:n:v_naz озеро 52 | 53 | ... punct ... 54 | 55 | 56 | 57 | — punct — 58 | Де adv:pron:int:rel де 59 | 60 | ? punct ? 61 | 62 | """ 63 | 64 | assertEquals expected, adjustResult(tagged.tagged) 65 | } 66 | 67 | 68 | @Test 69 | public void testTxtFormatWithSemantic() { 70 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.vertical, semanticTags: true)) 71 | 72 | String text = "А далі - озеро..." 73 | TagResult tagged = tagText.tagText(text) 74 | 75 | def expected = 76 | """ 77 | А conj:coord а _ 78 | далі adv:compc:predic далі semTags=1:dist:2:time 79 | - punct - _ 80 | озеро noun:inanim:n:v_naz озеро _ 81 | 82 | ... punct ... _ 83 | 84 | """ 85 | 86 | assertEquals expected, adjustResult(tagged.tagged) 87 | } 88 | 89 | 90 | @Test 91 | public void testTxtFormatWithUD() { 92 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.conllu, semanticTags: true)) 93 | 94 | def text = "А треба далі воно - озеро Світязь де я затримався, тисяча..." 95 | TagResult tagged = tagText.tagText(text) 96 | 97 | def expected = 98 | """# sent_id = 1 99 | # text = $text 100 | 1 А а CCONJ conj:coord _ _ _ _ _ 101 | 2 треба треба ADV noninfl:predic _ _ _ _ Uninflect=Yes 102 | 3 далі далі ADV adv:compc:predic Degree=Cmp _ _ _ SemTags=1:dist:2:time 103 | 4 воно воно PRON noun:unanim:n:v_naz:pron:pers:3 Animacy=Anim,Inan|Case=Nom|Gender=Neut|Number=Sing|Person=3|PronType=Prs _ _ _ SemTags=1:conc:deictic 104 | 5 - - PUNCT punct _ _ _ _ _ 105 | 6 озеро озеро NOUN noun:inanim:n:v_naz Animacy=Inan|Case=Nom|Gender=Neut|Number=Sing _ _ _ _ 106 | 7 Світязь Світязь PROPN noun:inanim:m:v_naz:prop:geo:xp1 Animacy=Inan|Case=Nom|Gender=Masc|NameType=Geo|Number=Sing _ _ _ SemTags=1:conc:loc 107 | 8 де де ADV adv:pron:int:rel PronType=Int|PronType=Rel _ _ _ _ 108 | 9 я я PRON noun:anim:s:v_naz:pron:pers:1 Animacy=Anim|Case=Nom|Number=Sing|Person=1|PronType=Prs _ _ _ SemTags=1:conc:hum:deictic 109 | 10 затримався затриматися VERB verb:rev:perf:past:m Aspect=Perf|Gender=Masc|Mood=Ind|Number=Sing|Reflex=Yes|Tense=Past|VerbForm=Fin _ _ _ SpaceAfter=No 110 | 11 , , PUNCT punct _ _ _ _ _ 111 | 12 тисяча тисяча NOUN noun:inanim:f:v_naz:numr Animacy=Inan|Case=Nom|Gender=Fem|Number=Sing|NumType=Card _ _ _ SemTags=1:abst:quantity:absol:2:abst:quantity&max:3:conc:hum:group:part|SpaceAfter=No 112 | 13 ... ... PUNCT punct _ _ _ _ _ 113 | """.toString() 114 | 115 | assertEquals expected, adjustResult(tagged.tagged) 116 | 117 | tagText.setOptions(new TagOptions(outputFormat: OutputFormat.conllu, semanticTags: false)) 118 | 119 | def sent1 = "Шановні колеги, прошу вставте картки, зараз проведемо реєстрацію натисканням зеленої кнопки приладів." 120 | def sent2 = "Рада України." 121 | text = "$sent1 $sent2" 122 | 123 | tagged = tagText.tagText(text) 124 | 125 | expected = 126 | """# sent_id = 1 127 | # text = $sent1 128 | 1 Шановні шановний ADJ adj:p:v_kly:compb Case=Voc|Degree=Pos|Number=Plur _ _ _ _ 129 | 2 колеги колега NOUN noun:anim:p:v_kly Animacy=Anim|Case=Voc|Gender=Fem|Number=Plur _ _ _ SpaceAfter=No 130 | 3 , , PUNCT punct _ _ _ _ _ 131 | 4 прошу просити VERB verb:imperf:pres:s:1 Aspect=Imp|Mood=Ind|Number=Sing|Person=1|Tense=Pres|VerbForm=Fin _ _ _ _ 132 | 5 вставте вставити VERB verb:perf:impr:p:2 Aspect=Perf|Mood=Imp|Number=Plur|Person=2|VerbForm=Fin _ _ _ _ 133 | 6 картки картка NOUN noun:inanim:p:v_zna Animacy=Inan|Case=Acc|Gender=Fem|Number=Plur _ _ _ SpaceAfter=No 134 | 7 , , PUNCT punct _ _ _ _ _ 135 | 8 зараз зараз ADV adv:pron:dem PronType=Dem _ _ _ _ 136 | 9 проведемо провести VERB verb:perf:futr:p:1 Aspect=Perf|Mood=Ind|Number=Plur|Person=1|Tense=Fut|VerbForm=Fin _ _ _ _ 137 | 10 реєстрацію реєстрація NOUN noun:inanim:f:v_zna Animacy=Inan|Case=Acc|Gender=Fem|Number=Sing _ _ _ _ 138 | 11 натисканням натискання NOUN noun:inanim:p:v_dav Animacy=Inan|Case=Dat|Gender=Neut|Number=Plur _ _ _ _ 139 | 12 зеленої зелений ADJ adj:f:v_rod:compb Case=Gen|Degree=Pos|Gender=Fem|Number=Sing _ _ _ _ 140 | 13 кнопки кнопка NOUN noun:inanim:f:v_rod Animacy=Inan|Case=Gen|Gender=Fem|Number=Sing _ _ _ _ 141 | 14 приладів прилад NOUN noun:inanim:p:v_rod Animacy=Inan|Case=Gen|Gender=Masc|Number=Plur _ _ _ SpaceAfter=No 142 | 15 . . PUNCT punct _ _ _ _ _ 143 | 144 | # sent_id = 2 145 | # text = $sent2 146 | 1 Рада рада NOUN noun:inanim:f:v_naz Animacy=Inan|Case=Nom|Gender=Fem|Number=Sing _ _ _ _ 147 | 2 України Україна PROPN noun:inanim:f:v_rod:prop:geo Animacy=Inan|Case=Gen|Gender=Fem|NameType=Geo|Number=Sing _ _ _ SpaceAfter=No 148 | 3 . . PUNCT punct _ _ _ _ _ 149 | """.toString() 150 | 151 | // println tagged.tagged.replace(" ", '\ ') 152 | 153 | assertEquals expected, adjustResult(tagged.tagged) 154 | } 155 | 156 | 157 | @Test 158 | public void testTxtFormatWithUDPlural() { 159 | 160 | def list = [] 161 | tagText.udModule.language = tagText.language 162 | tagText.udModule.addPluralGender(new TaggedToken(value: 'труб', tags: 'noun:inanim:p:v_rod', lemma: 'труба'), list) 163 | 164 | assertEquals(['Gender=Fem'], list) 165 | 166 | list = [] 167 | tagText.udModule.addPluralGender(new TaggedToken(value: 'статтей', tags: 'noun:inanim:p:v_rod:subst', lemma: 'стаття'), list) 168 | assertEquals(['Gender=Fem'], list) 169 | } 170 | 171 | private static String adjustResult(String txt) { 172 | txt.replace('\t', ' ') 173 | .replaceAll(/\|TagConfidence=[0-9.]+/, '') 174 | .replaceAll(/TagConfidence=[0-9.]+/, '_') 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/tag/TagTextModLesyaTest.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | package ua.net.nlp.tools.tag 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | 10 | import ua.net.nlp.tools.tag.TagOptions 11 | import ua.net.nlp.tools.tag.TagTextCore 12 | import ua.net.nlp.tools.tag.TagTextCore.TagResult 13 | 14 | 15 | class TagTextModLesyaTest { 16 | def options = new TagOptions() 17 | 18 | static TagTextCore tagText = new TagTextCore() 19 | 20 | @BeforeEach 21 | void before() { 22 | tagText.setOptions(new TagOptions(unknownStats: true, modules: ["lesya"])) 23 | } 24 | 25 | def file() { return new File("/dev/null") } 26 | 27 | 28 | 29 | @Test 30 | public void testLesyaOrphograph() { 31 | 32 | TagResult tagged = tagText.tagText("звичайі нашоі націі") 33 | def expected = 34 | """ 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | """ 54 | assertEquals expected, tagged.tagged 55 | 56 | assertEquals "[:]", tagged.stats.unknownMap.toString() 57 | } 58 | 59 | @Test 60 | public void testLesyaOrphoepic() { 61 | 62 | TagResult tagged = tagText.tagText("завжді") 63 | def expected = 64 | """ 65 | 66 | 67 | 68 | 69 | """ 70 | 71 | tagged = tagText.tagText("идолянин") 72 | expected = 73 | """ 74 | 75 | 76 | 77 | 78 | """ 79 | assertEquals expected, tagged.tagged 80 | 81 | tagged = tagText.tagText("історічний") 82 | expected = 83 | """ 84 | 85 | 86 | 87 | 88 | 89 | 90 | """ 91 | assertEquals expected, tagged.tagged 92 | 93 | tagged = tagText.tagText("лікарь") 94 | expected = 95 | """ 96 | 97 | 98 | 99 | 100 | """ 101 | assertEquals expected, tagged.tagged 102 | 103 | tagged = tagText.tagText("головнійше") 104 | expected = 105 | """ 106 | 107 | 108 | 109 | 110 | 111 | 112 | """ 113 | assertEquals expected, tagged.tagged 114 | 115 | tagged = tagText.tagText("остілько") 116 | expected = 117 | """ 118 | 119 | 120 | 121 | 122 | """ 123 | assertEquals expected, tagged.tagged 124 | 125 | tagged = tagText.tagText("чорниї") 126 | expected = 127 | """ 128 | 129 | 130 | 131 | 132 | 133 | 134 | """ 135 | assertEquals expected, tagged.tagged 136 | } 137 | 138 | @Test 139 | public void testLesyaGram() { 140 | 141 | TagResult tagged = tagText.tagText("християне") 142 | def expected = 143 | """ 144 | 145 | 146 | 147 | 148 | 149 | 150 | """ 151 | assertEquals expected, tagged.tagged 152 | 153 | tagged = tagText.tagText("прикростів") 154 | expected = 155 | """ 156 | 157 | 158 | 159 | 160 | """ 161 | assertEquals expected, tagged.tagged 162 | 163 | tagged = tagText.tagText("фабрикантови") 164 | expected = 165 | """ 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | """ 175 | assertEquals expected, tagged.tagged 176 | 177 | tagged = tagText.tagText("завоюваннів") 178 | expected = 179 | """ 180 | 181 | 182 | 183 | 184 | """ 185 | assertEquals expected, tagged.tagged 186 | } 187 | 188 | 189 | @Test 190 | public void testLesyaAvRod() { 191 | 192 | TagResult tagged = tagText.tagText("всесвіта") 193 | def expected = 194 | """ 195 | 196 | 197 | 198 | 199 | 200 | 201 | """ 202 | assertEquals expected, tagged.tagged 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/tag/TagTextModZhelehTest.groovy: -------------------------------------------------------------------------------- 1 | #!/bin/env groovy 2 | 3 | package ua.net.nlp.tools.tag 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals 6 | 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | 10 | import ua.net.nlp.tools.tag.TagOptions 11 | import ua.net.nlp.tools.tag.TagTextCore 12 | import ua.net.nlp.tools.tag.TagTextCore.TagResult 13 | 14 | 15 | class TagTextModZhelehTest { 16 | 17 | static TagTextCore tagText = new TagTextCore() 18 | 19 | @BeforeEach 20 | void before() { 21 | tagText.setOptions(new TagOptions(modules: ["zheleh"])) 22 | } 23 | 24 | 25 | @Test 26 | public void testZheleh() { 27 | 28 | TagResult tagged = tagText.tagText("миготїнь купаєть ся житє і смерть") 29 | def expected = 30 | """ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | """ 55 | assertEquals expected, tagged.tagged 56 | } 57 | 58 | @Test 59 | public void testZheleh2() { 60 | 61 | TagResult tagged = tagText.tagText("називати ся") 62 | def expected = 63 | """ 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | """ 73 | assertEquals expected, tagged.tagged 74 | } 75 | 76 | 77 | @Test 78 | public void testZhelehExtraWords() { 79 | 80 | TagResult tagged = tagText.tagText("мїжь") 81 | def expected = 82 | """ 83 | 84 | 85 | 86 | 87 | """ 88 | assertEquals expected, tagged.tagged 89 | } 90 | 91 | 92 | @Test 93 | public void testZhelehSubs() { 94 | 95 | TagResult tagged = tagText.tagText("польованє") 96 | def expected = 97 | """ 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | """ 109 | assertEquals expected, tagged.tagged 110 | 111 | tagged = tagText.tagText("пізнїйше") 112 | expected = 113 | """ 114 | 115 | 116 | 117 | 118 | """ 119 | assertEquals expected, tagged.tagged 120 | 121 | tagged = tagText.tagText("йім") 122 | expected = 123 | """ 124 | 125 | 126 | 127 | 128 | 129 | """ 130 | assertEquals expected, tagged.tagged 131 | 132 | assertEquals expected, tagged.tagged 133 | 134 | tagged = tagText.tagText("Поднїпровя") 135 | expected = 136 | """ 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | """ 145 | assertEquals expected, tagged.tagged 146 | 147 | tagged = tagText.tagText("літопись") 148 | expected = 149 | """ 150 | 151 | 152 | 153 | 154 | 155 | """ 156 | assertEquals expected, tagged.tagged 157 | 158 | tagged = tagText.tagText("розпорядженєм") 159 | expected = 160 | """ 161 | 162 | 163 | 164 | 165 | 166 | """ 167 | assertEquals expected, tagged.tagged 168 | 169 | tagged = tagText.tagText("Італїйцї") 170 | expected = 171 | """ 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | """ 182 | assertEquals expected, tagged.tagged 183 | 184 | tagged = tagText.tagText("засїданя") 185 | expected = 186 | """ 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | """ 198 | assertEquals expected, tagged.tagged 199 | } 200 | 201 | @Test 202 | public void testZhelehSubsMultitag() { 203 | 204 | TagResult tagged = tagText.tagText("А-ле") 205 | def expected = 206 | """ 207 | 208 | 209 | 210 | 211 | 212 | """ 213 | assertEquals expected, tagged.tagged 214 | } 215 | } -------------------------------------------------------------------------------- /src/test/groovy/ua/net/nlp/tools/tokenize/TokenizeTextTest.groovy: -------------------------------------------------------------------------------- 1 | package ua.net.nlp.tools.tokenize 2 | 3 | import static org.junit.jupiter.api.Assertions.* 4 | 5 | import java.util.regex.Pattern 6 | 7 | import org.junit.jupiter.api.Test 8 | 9 | import groovy.json.JsonSlurper 10 | import ua.net.nlp.tools.OutputFormat 11 | 12 | 13 | class TokenizeTextTest { 14 | 15 | @Test 16 | void test() { 17 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions()) 18 | def res = TokenizeTextCore.getAnalyzed(",десь \"такі\" підходи") 19 | assertEquals ",десь \"такі\" підходи\n", res.tagged 20 | } 21 | 22 | @Test 23 | void testWords() { 24 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions(words: true)) 25 | def res = TokenizeTextCore.getAnalyzed("Автомагістраль-Південь, наш 'видатний' автобан. Став схожий на диряве корито") 26 | assertEquals "Автомагістраль-Південь|,|наш|'|видатний|'|автобан|.\nСтав|схожий|на|диряве|корито\n", res.tagged 27 | } 28 | 29 | @Test 30 | void testWordsWithSpace() { 31 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions(words: true, preserveWhitespace: true)) 32 | def res = TokenizeTextCore.getAnalyzed("Автомагістраль-Південь, наш 'видатний' автобан. Став схожий на диряве корито") 33 | assertEquals "Автомагістраль-Південь|,| |наш| |'|видатний|'| |автобан|.| \nСтав| |схожий| |на| |диряве| |корито\n", res.tagged 34 | } 35 | 36 | @Test 37 | void testWordsOnly() { 38 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions(onlyWords: true, words: true)) 39 | def res = TokenizeTextCore.getAnalyzed(",десь \"такі\" підхо\u0301ди[9]") 40 | assertEquals "десь такі підходи\n", res.tagged 41 | } 42 | 43 | @Test 44 | void testHyphenParts() { 45 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions(onlyWords: true, words: true)) 46 | def res = TokenizeTextCore.getAnalyzed("Сідай-но") 47 | assertEquals "Сідай -но\n", res.tagged 48 | 49 | res = TokenizeTextCore.getAnalyzed("десь\u2013таки") 50 | assertEquals "десь -таки\n", res.tagged 51 | } 52 | 53 | @Test 54 | void testNewLine() { 55 | def options = new TokenizeOptions() 56 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(options) 57 | def res = TokenizeTextCore.getAnalyzed("десь \"такі\"\nпідходи") 58 | assertEquals "десь \"такі\" підходи\n", res.tagged 59 | 60 | options.newLine = "
" 61 | res = TokenizeTextCore.getAnalyzed("десь такі\nпідходи") 62 | assertEquals "десь такі
підходи\n", res.tagged 63 | } 64 | 65 | @Test 66 | void testAdditionalSeparator() { 67 | def options = new TokenizeOptions(additionalSentenceSeparator: /\|+/) 68 | 69 | options.additionalSentenceSeparatorPattern = Pattern.compile(options.additionalSentenceSeparator) 70 | 71 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(options) 72 | def res = TokenizeTextCore.getAnalyzed("десь такі||підходи") 73 | assertEquals "десь такі\nпідходи\n", res.tagged 74 | } 75 | 76 | @Test 77 | void testJson() { 78 | def jsonSlurper = new JsonSlurper() 79 | 80 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions(outputFormat: OutputFormat.json)) 81 | 82 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed(",десь \"такі\" підходи").tagged + "]") 83 | 84 | // Question: should we append \n to the tokenized sentence for json as we did for txt? 85 | assertEquals ([",десь \"такі\" підходи"], object) 86 | } 87 | 88 | @Test 89 | void testWordsOnlyJson() { 90 | def jsonSlurper = new JsonSlurper() 91 | 92 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions( 93 | outputFormat: OutputFormat.json, onlyWords: true, words: true 94 | )) 95 | 96 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed(",десь \"такі\" підхо\u0301ди[9]").tagged + "]") 97 | 98 | assertEquals ([["десь", "такі", "підходи"]], object) 99 | } 100 | 101 | @Test 102 | void testHyphenPartsJson() { 103 | def jsonSlurper = new JsonSlurper() 104 | 105 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions( 106 | outputFormat: OutputFormat.json, onlyWords: true, words: true 107 | )) 108 | 109 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed("Сідай-но").tagged + "]") 110 | assertEquals ([["Сідай", "-но"]], object) 111 | 112 | object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed("десь\u2013таки").tagged + "]") 113 | assertEquals ([["десь", "-таки"]], object) 114 | } 115 | 116 | @Test 117 | void testNewLineJson() { 118 | def jsonSlurper = new JsonSlurper() 119 | 120 | def options = new TokenizeOptions(outputFormat: OutputFormat.json) 121 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(options) 122 | 123 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed("десь \"такі\"\nпідходи").tagged + "]") 124 | assertEquals (["десь \"такі\" підходи"], object) 125 | 126 | options.newLine = "
" 127 | 128 | object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed("десь такі\nпідходи").tagged + "]") 129 | assertEquals (["десь такі
підходи"], object) 130 | } 131 | 132 | @Test 133 | void testSentenceSplitJson() { 134 | def jsonSlurper = new JsonSlurper() 135 | 136 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions(outputFormat: OutputFormat.json)) 137 | 138 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed( 139 | "Автомагістраль-Південь, наш 'видатний' автобан. Став схожий на диряве корито").tagged + "]") 140 | assertEquals (["Автомагістраль-Південь, наш 'видатний' автобан.", "Став схожий на диряве корито"], object) 141 | } 142 | 143 | @Test 144 | void testWordSplitJson() { 145 | def jsonSlurper = new JsonSlurper() 146 | 147 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions( 148 | outputFormat: OutputFormat.json, words: true) 149 | ) 150 | 151 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed( 152 | "Автомагістраль-Південь, наш 'видатний' автобан. Став схожий на диряве корито").tagged + "]") 153 | assertEquals ([ 154 | ["Автомагістраль-Південь", ",", "наш", "'", "видатний", "'", "автобан", "."], 155 | ["Став", "схожий", "на", "диряве", "корито"]], object) 156 | } 157 | 158 | @Test 159 | void testQuotesSplitJson() { 160 | def jsonSlurper = new JsonSlurper() 161 | 162 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions( 163 | outputFormat: OutputFormat.json, words: true) 164 | ) 165 | 166 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed( 167 | "ТОВ «ЛАБЄАН-хісв»").tagged + "]") 168 | 169 | assertEquals ([["ТОВ", "«", "ЛАБЄАН-хісв", "»"]], object) 170 | } 171 | 172 | @Test 173 | void testQuotesSplitJsonWithWhitespace() { 174 | def jsonSlurper = new JsonSlurper() 175 | 176 | TokenizeTextCore TokenizeTextCore = new TokenizeTextCore(new TokenizeOptions( 177 | outputFormat: OutputFormat.json, words: true, preserveWhitespace: true) 178 | ) 179 | 180 | def object = jsonSlurper.parseText("[" + TokenizeTextCore.getAnalyzed( 181 | "ТОВ «ЛАБЄАН-хісв»").tagged + "]") 182 | 183 | assertEquals ([["ТОВ", " ", "«", "ЛАБЄАН-хісв", "»"]], object) 184 | } 185 | } 186 | --------------------------------------------------------------------------------