Распознаем числа в тексте

от автора

Кому может быть полезна эта статья?

Извращенцам делающим NLP на Java? Или может быть для обучения?

Хотя зачем эти оправдания? Весь код был написан because we can.

Под катом мы рассмотрим как превращать числа вида «Двенадцать тысяч шестьсот пятьдесят девять целых четыре миллионных» в форму вроде 12 659, 000 004.

Русский язык обладает встроенными алиасами для некоторых чисел. Их мы будем с переводить в последовательность обычных чисел. Для этого составим словарь псевдонимов:

0 ноль нуль 1 один 2 два 3 три 4 четыре 5 пять 6 шесть 7 семь 8 восемь 9 девять 11 одиннадцать 12 двенадцать дюжина 13 тринадцать 14 четырнадцать 15 пятнадцать 16 шестнадцать 17 семнадцать 18 восемнадцать 19 девятнадцать 20 двадцать 30 тридцать 40 сорок 50 пятьдесят 60 шестьдесят 70 семьдесят 80 восемьдесят 90 девяносто 200 двести 300 триста 400 четыреста 500 пятьсот 600 шестьсот 700 семьсот 800 восемьсот 900 девятьсот 0.00000000001 стомиллиардный 0.0000000001 десятимиллиардный 0.000000001 миллиардный 0.00000001 стомиллионный 0.0000001 десятимиллионный 0.000001 миллионный 0.00001 стотысячный 0.0001 десятитысячный 0.001 тысячный 0.01 сотый 0.1 десятый 10 десять 100 сто 1000 тысяча 1000000 миллион 1000000000 миллиард 1000000000000 триллион 1000000000000000 квадриллион 1000000000000000000 квинтиллион 1000000000000000000000 секстиллион 1000000000000000000000000 септиллион 1000000000000000000000000000 октиллион

Чтобы прочитать словарь из ресурсов в память, нам потребуется такой код на Kotlin:

{}.javaClass.getResourceAsStream("/dictionary")!!   .bufferedReader()   .readLines()   .flatMap { line ->     val aliases = line.split(' ')     val number = aliases.first().toDouble()     aliases.drop(1).map { Pair(it, number) }   }.toMap()

Некоторая сложность этого кода обусловлена теоретической возможностью наличия двух и более псеводонимов для одного числа.

Теперь настало время выхода на сцену токенайзера и морфологического словаря.
Подключив их, мы можем вытащить из любой строки последовательность наших чисел в любых разрешенных русским языком склонениях:

val integerPart = mutableListOf<Double>() val fractionalPart = mutableListOf<Double>() var currentPart = integerPart for (token in words) {   if (integerPart.isNotEmpty() && token.lowercase() in separators) {     currentPart = fractionalPart     continue   }   val number =     lookupForMeanings(token)       .run {         firstOrNull { it.partOfSpeech == Numeral || it.partOfSpeech == OrdinalNumber }           ?: getOrNull(0)       }       ?.lemma       ?.toString()       ?.let(numbers::get)   if (number != null) {     currentPart += number     continue   }   if (currentPart.isNotEmpty()) {     break   } }

Код ужасно мутабельный, но как сделать лучше пока не придумал. После этого нам остается только склеить последовательность обычных чисел в одно. Это самое простое, пока число в последовательности меньше следующего, то умножаем, а когда следующее становится меньше предыдущего, то складываем островки умножений.

private fun List<Double>.join(): Double {   var tokensSum = 0.0   var previousToken = first()   for (currToken in drop(1)) {     if (currToken > previousToken) {       previousToken *= currToken     } else {       tokensSum += previousToken       previousToken = currToken     }   }   return tokensSum + previousToken }

Пришло время тестов нашей чудо-библиотеки!

@Test fun parseRussianDouble() {   assertThat("Двенадцать тысяч шестьсот пятьдесят девять целых четыре миллионных".parseRussianDouble())     .isEqualTo(12659.000004)    assertThat("Десять тысяч четыреста тридцать четыре".parseRussianDouble())     .isEqualTo(10434.0)    assertThat("Двенадцать целых шестьсот пятьдесят девять тысячных".parseRussianDouble())     .isEqualTo(12.659)    assertThat("Ноль целых пятьдесят восемь сотых".parseRussianDouble())     .isEqualTo(0.58)    assertThat("Сто тридцать пять".parseRussianDouble())     .isEqualTo(135.0) }

Если вам интересно, как сделать, чтобы метод .parseToRussianDouble появился для всех строк в вашем Kotlin (или Java) проекте, то вам нужно просто подключить пару строчек в вашей системе сборки:
https://jitpack.io/#demidko/chisla/2021.10.30

В качестве демонстрации еще одной возможности библиотеки приведу кусочек кода:

"Я хотел передать ему сто тридцать пять яблок".parseRussianDouble() // 135

Исходный код библиотеки доступен на GitHub.

Критика, вопросы, пожелания, принимаются в issues или в комментариях под статьей.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Полезно?

50% Да, может заиспользую в своем проекте13
50% Нет, буду пользоваться ICU / другим велосипедом13

Проголосовали 26 пользователей. Воздержались 20 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/586466/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *