it-swarm-ru.tech

Как читать значения из чисел, написанных словами?

Как мы все знаем, числа могут быть записаны либо в цифрах, либо названы их именами. В то время как есть много примеров, которые преобразуют 123 в сто двадцать три, я не смог найти хороших примеров того, как преобразовать это наоборот.

Некоторые из предостережений:

  1. кардинальное/именное или порядковое: "один" и "первый"
  2. распространенные орфографические ошибки: "сорок"/"сорок"
  3. сотни/тысячи: 2100 -> "двадцать одна сотня", а также "две тысячи сто"
  4. разделители: "одиннадцатьсот пятьдесят два", но также "одиннадцатьсот пятьдесят два" или "одиннадцатьсот пятьдесят два" и еще много чего
  5. разговорные выражения: "тридцать с чем-то"
  6. фракции: "одна треть", "две пятых"
  7. общие названия: "дюжина", "половина"

И, возможно, есть еще возможные предостережения, которые еще не перечислены. Предположим, что алгоритм должен быть очень надежным и даже понимать орфографические ошибки.

Какие поля/документы/исследования/алгоритмы я должен прочитать, чтобы научиться писать все это? Где информация?

PS: мой последний парсер должен понимать 3 разных языка: английский, русский и иврит. И, возможно, на более позднем этапе будет добавлено больше языков. У иврита также есть мужские/женские номера, например, "один мужчина" и "одна женщина" имеют разные "один" - "эхад" и "ахат". У русских тоже есть свои сложности.

Google делает большую работу в этом. Например:

http://www.google.com/search?q=two+thousand+and+one+hundred+plus+five+dozen+and+four+fifths+in+decimal

(возможно и обратное http://www.google.com/search?q=999999999999+in+english )

49
Evgeny

Я играл с PEG-парсером, чтобы делать то, что вы хотели (и могу опубликовать это как отдельный ответ позже), когда заметил, что есть очень простой алгоритм, который замечательно справляется с обычными формами чисел на английском, испанском и других языках. Немецкий, по крайней мере.

Например, для работы с английским языком необходим словарь, который отображает слова в значения очевидным образом:

"one" -> 1, "two" -> 2, ... "twenty" -> 20,
"dozen" -> 12, "score" -> 20, ...
"hundred" -> 100, "thousand" -> 1000, "million" -> 1000000

...и так далее

Алгоритм просто:

total = 0
prior = null
for each Word w
    v <- value(w) or next if no value defined
    prior <- case
        when prior is null:       v
        when prior > v:     prior+v
        else                prior*v
        else
    if w in {thousand,million,billion,trillion...}
        total <- total + prior
        prior <- null
total = total + prior unless prior is null

Например, это прогрессирует следующим образом:

total    prior      v     unconsumed string
    0      _              four score and seven 
                    4     score and seven 
    0      4              
                   20     and seven 
    0     80      
                    _     seven 
    0     80      
                    7 
    0     87      
   87

total    prior      v     unconsumed string
    0        _            two million four hundred twelve thousand eight hundred seven
                    2     million four hundred twelve thousand eight hundred seven
    0        2
                  1000000 four hundred twelve thousand eight hundred seven
2000000      _
                    4     hundred twelve thousand eight hundred seven
2000000      4
                    100   twelve thousand eight hundred seven
2000000    400
                    12    thousand eight hundred seven
2000000    412
                    1000  eight hundred seven
2000000  412000
                    1000  eight hundred seven
2412000     _
                      8   hundred seven
2412000     8
                     100  seven
2412000   800
                     7
2412000   807
2412807

И так далее. Я не говорю, что он идеален, но для быстрого и грязного он вполне хорош.


Обращаясь к вашему конкретному списку при редактировании:

  1. кардинальные/именные или порядковые: "один" и "первый" - просто поместите их в словарь
  2. английский/британский: "сорок"/"сорок" - то же самое
  3. сотни/тысячи: 2100 -> "двадцать одна сотня", а также "две тысячи сто" - работает как есть
  4. разделители: "одиннадцатьсот пятьдесят два", но также "одиннадцатьсот пятьдесят два" или "одиннадцатьсот пятьдесят два" и еще много чего - просто определите "следующее слово" как самый длинный префикс, соответствующий определенному Слову, или до следующего не-Word, если никто не делает, для начала
  5. разговорные выражения: "тридцать с чем-то" - работает
  6. фрагменты: "одна треть", "две пятых" - э-э, еще нет ...
  7. общие названия: "дюжина", "половина" - работает; вы даже можете делать такие вещи, как "полдюжины"

Число 6 - единственное, на которое у меня нет готового ответа, и это из-за неоднозначности между порядковыми и дробными числами (по крайней мере на английском языке), добавленной к тому факту, что моя последняя чашка кофе была много часов назад.

46
MarkusQ

У меня есть код, который я написал некоторое время назад: text2num . Это делает то, что вы хотите, за исключением того, что он не обрабатывает порядковые номера. На самом деле я ни для чего не использовал этот код, поэтому он в основном не проверен!

11
Greg Hewgill

Это непростая проблема, и я не знаю ни одной библиотеки для этого. Я мог бы сесть и попытаться написать что-то подобное когда-нибудь. Я бы сделал это либо в Prolog, Java, либо в Haskell. Насколько я вижу, есть несколько вопросов:

  • Маркировка: иногда числа пишутся одиннадцатьсот пятьдесят два, но я видел одиннадцатьсот пятьдесят два или одиннадцать сто пятьдесят два и еще много чего. Нужно было бы провести опрос о том, какие формы на самом деле используются. Это может быть особенно сложно для иврита.
  • Орфографические ошибки: это не так сложно. У вас ограниченное количество слов, и немного магии на расстоянии Левенштейна должно сработать.
  • Альтернативные формы, как вы уже упоминали, существуют. Это включает в себя порядковые/кардинальные числа, а также сорок/сорок и ...
  • ... общие имена или часто используемые фразы и NE (именованные объекты). Хотите извлечь 30 из Тридцатилетней войны или 2 из Второй мировой войны?
  • Римские цифры тоже?
  • Разговорные выражения, такие как "тридцать с чем-то" и "три евро и шрапнель", которые я не знаю, как лечить.

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

Извините, это еще не реальный ответ, а просто продолжение вашего вопроса. Я дам вам знать, если найду/напишу что-нибудь.

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

11
Aleksandar Dimitrov

Используйте библиотеку Python pattern-en :

>>> from pattern.en import number
>>> number('two thousand fifty and a half') => 2050.5
7
Alex Brooks

Вы должны иметь в виду, что Европа и Америка считают по-разному.

Европейский стандарт:

One Thousand
One Million
One Thousand Millions (British also use Milliard)
One Billion
One Thousand Billions
One Trillion
One Thousand Trillions

Здесь это небольшая ссылка на него.


Простой способ увидеть разницу заключается в следующем:

(American counting Trillion) == (European counting Billion)
5
fmsf

Порядковые числа не применимы, потому что они не могут быть осмысленно объединены с другими числами на языке (... по крайней мере на английском языке)

например сто первый, одиннадцать секунд и т. д.

Тем не менее, есть еще одна англо-американская оговорка со словом "и"

то есть.

сто один (английский) сто один (американский)

Кроме того, использование "а" означает один на английском языке

тысяча = одна тысяча

... С другой стороны, калькулятор Google делает потрясающую работу.

скорость света в сто три тысячи раз больше

И даже...

две тысячи сто плюс дюжина

... WTF?!? оценка плюс дюжина римскими цифрами

4
Fraser

Вот чрезвычайно надежное решение в Clojure.

AFAIK это уникальный подход к реализации.

;----------------------------------------------------------------------
; numbers.clj
; written by: Mike Mattie [email protected]
;----------------------------------------------------------------------
(ns operator.numbers
  (:use compojure.core)

  (:require
    [clojure.string     :as string] ))

(def number-Word-table {
  "zero"          0
  "one"           1
  "two"           2
  "three"         3
  "four"          4
  "five"          5
  "six"           6
  "seven"         7
  "eight"         8
  "nine"          9
  "ten"           10
  "eleven"        11
  "twelve"        12
  "thirteen"      13
  "fourteen"      14
  "fifteen"       15
  "sixteen"       16
  "seventeen"     17
  "eighteen"      18
  "nineteen"      19
  "twenty"        20
  "thirty"        30
  "fourty"        40
  "fifty"         50
  "sixty"         60
  "seventy"       70
  "eighty"        80
  "ninety"        90
})

(def multiplier-Word-table {
  "hundred"       100
  "thousand"      1000
})

(defn sum-words-to-number [ words ]
  (apply + (map (fn [ Word ] (number-Word-table Word)) words)) )

; are you down with the sickness ?
(defn words-to-number [ words ]
  (let
    [ n           (count words)

      multipliers (filter (fn [x] (not (false? x))) (map-indexed
                                                      (fn [ i Word ]
                                                        (if (contains? multiplier-Word-table Word)
                                                          (vector i (multiplier-Word-table Word))
                                                          false))
                                                      words) )

      x           (ref 0) ]

    (loop [ indices (reverse (conj (reverse multipliers) (vector n 1)))
            left    0
            combine + ]
      (let
        [ right (first indices) ]

        (dosync (alter x combine (* (if (> (- (first right) left) 0)
                                      (sum-words-to-number (subvec words left (first right)))
                                      1)
                                    (second right)) ))

        (when (> (count (rest indices)) 0)
          (recur (rest indices) (inc (first right))
            (if (= (inc (first right)) (first (second indices)))
              *
              +))) ) )
    @x ))

Вот несколько примеров

(operator.numbers/words-to-number ["six" "thousand" "five" "hundred" "twenty" "two"])
(operator.numbers/words-to-number ["fifty" "seven" "hundred"])
(operator.numbers/words-to-number ["hundred"])
3
Mike Mattie

Моя LPC реализация некоторых ваших требований (только американский английский):

internal mapping inordinal = ([]);
internal mapping number = ([]);

#define Numbers ([\
    "zero"        : 0, \
    "one"         : 1, \
    "two"         : 2, \
    "three"       : 3, \
    "four"        : 4, \
    "five"        : 5, \
    "six"         : 6, \
    "seven"       : 7, \
    "eight"       : 8, \
    "nine"        : 9, \
    "ten"         : 10, \
    "eleven"      : 11, \
    "twelve"      : 12, \
    "thirteen"    : 13, \
    "fourteen"    : 14, \
    "fifteen"     : 15, \
    "sixteen"     : 16, \
    "seventeen"   : 17, \
    "eighteen"    : 18, \
    "nineteen"    : 19, \
    "twenty"      : 20, \
    "thirty"      : 30, \
    "forty"       : 40, \
    "fifty"       : 50, \
    "sixty"       : 60, \
    "seventy"     : 70, \
    "eighty"      : 80, \
    "ninety"      : 90, \
    "hundred"     : 100, \
    "thousand"    : 1000, \
    "million"     : 1000000, \
    "billion"     : 1000000000, \
])

#define Ordinals ([\
    "zeroth"        : 0, \
    "first"         : 1, \
    "second"        : 2, \
    "third"         : 3, \
    "fourth"        : 4, \
    "fifth"         : 5, \
    "sixth"         : 6, \
    "seventh"       : 7, \
    "eighth"        : 8, \
    "ninth"         : 9, \
    "tenth"         : 10, \
    "eleventh"      : 11, \
    "twelfth"       : 12, \
    "thirteenth"    : 13, \
    "fourteenth"    : 14, \
    "fifteenth"     : 15, \
    "sixteenth"     : 16, \
    "seventeenth"   : 17, \
    "eighteenth"    : 18, \
    "nineteenth"    : 19, \
    "twentieth"     : 20, \
    "thirtieth"     : 30, \
    "fortieth"      : 40, \
    "fiftieth"      : 50, \
    "sixtieth"      : 60, \
    "seventieth"    : 70, \
    "eightieth"     : 80, \
    "ninetieth"     : 90, \
    "hundredth"     : 100, \
    "thousandth"    : 1000, \
    "millionth"     : 1000000, \
    "billionth"     : 1000000000, \
])

varargs int denumerical(string num, status ordinal) {
    if(ordinal) {
        if(member(inordinal, num))
            return inordinal[num];
    } else {
        if(member(number, num))
            return number[num];
    }
    int sign = 1;
    int total = 0;
    int sub = 0;
    int value;
    string array parts = regexplode(num, " |-");
    if(sizeof(parts) >= 2 && parts[0] == "" && parts[1] == "-")
        sign = -1;
    for(int ix = 0, int iix = sizeof(parts); ix < iix; ix++) {
        string part = parts[ix];
        switch(part) {
        case "negative" :
        case "minus"    :
            sign = -1;
            continue;
        case ""         :
            continue;
        }
        if(ordinal && ix == iix - 1) {
            if(part[0] >= '0' && part[0] <= '9' && ends_with(part, "th"))
                value = to_int(part[..<3]);
            else if(member(Ordinals, part))
                value = Ordinals[part];
            else
                continue;
        } else {
            if(part[0] >= '0' && part[0] <= '9')
                value = to_int(part);
            else if(member(Numbers, part))
                value = Numbers[part];
            else
                continue;
        }
        if(value < 0) {
            sign = -1;
            value = - value;
        }
        if(value < 10) {
            if(sub >= 1000) {
                total += sub;
                sub = value;
            } else {
                sub += value;
            }
        } else if(value < 100) {
            if(sub < 10) {
                sub = 100 * sub + value;
            } else if(sub >= 1000) {
                total += sub;
                sub = value;
            } else {
                sub *= value;
            }
        } else if(value < sub) {
            total += sub;
            sub = value;
        } else if(sub == 0) {
            sub = value;
        } else {
            sub *= value;
        }
    }
    total += sub;
    return sign * total;
}
2
chaos

Что ж, я опоздал с ответом на этот вопрос, но я работал над небольшим тестовым сценарием, который, похоже, мне очень помог. Я использовал (простое, но безобразное и большое) регулярное выражение, чтобы найти все слова для меня. Выражение выглядит следующим образом:

(?<Value>(?:zero)|(?:one|first)|(?:two|second)|(?:three|third)|(?:four|fourth)|
(?:five|fifth)|(?:six|sixth)|(?:seven|seventh)|(?:eight|eighth)|(?:nine|ninth)|
(?:ten|tenth)|(?:eleven|eleventh)|(?:twelve|twelfth)|(?:thirteen|thirteenth)|
(?:fourteen|fourteenth)|(?:fifteen|fifteenth)|(?:sixteen|sixteenth)|
(?:seventeen|seventeenth)|(?:eighteen|eighteenth)|(?:nineteen|nineteenth)|
(?:twenty|twentieth)|(?:thirty|thirtieth)|(?:forty|fortieth)|(?:fifty|fiftieth)|
(?:sixty|sixtieth)|(?:seventy|seventieth)|(?:eighty|eightieth)|(?:ninety|ninetieth)|
(?<Magnitude>(?:hundred|hundredth)|(?:thousand|thousandth)|(?:million|millionth)|
(?:billion|billionth)))

Здесь показано с разрывами строк для форматирования.

В любом случае, мой метод состоял в том, чтобы выполнить этот RegEx с библиотекой, подобной PCRE, и затем прочитать названные совпадения. И он работал на всех различных примерах, перечисленных в этом вопросе, за исключением типов "Одна половина", поскольку я их не добавил, но, как вы можете видеть, это не составит труда сделать. Это решает много вопросов. Например, он обращается к следующим пунктам исходного вопроса и другим ответам:

  1. кардинальное/именное или порядковое: "один" и "первый"
  2. распространенные орфографические ошибки: "сорок"/"Сорок" (обратите внимание, что это НЕ БЫЛО адресовано к этому, это было бы то, что вы хотели бы сделать, прежде чем передать строку этому анализатору. Этот анализатор видит этот пример как "ЧЕТЫРЕ"). ..)
  3. сотни/тысячи: 2100 -> "двадцать одна сотня", а также "две тысячи сто"
  4. разделители: "одиннадцатьсот пятьдесят два", но также "одиннадцатьсот пятьдесят два" или "одиннадцатьсот пятьдесят два" и еще много чего
  5. colloqialisms: "тридцать-что-то" (Это также не ПОЛНОСТЬЮ адресуется, как то, что IS "что-то"? Ну, этот код находит это число как просто "30"). **

Теперь, вместо того, чтобы хранить этого монстра регулярного выражения в вашем источнике, я подумывал о создании этого RegEx во время выполнения, используя что-то вроде следующего:

char *ones[] = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve",
  "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"};
char *tens[] = {"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"};
char *ordinalones[] = { "", "first", "second", "third", "fourth", "fifth", "", "", "", "", "", "", "twelfth" };
char *ordinaltens[] = { "", "", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetieth" };
and so on...

Легкая часть здесь - мы только храним слова, которые имеют значение. В случае ШЕСТОГО вы заметите, что для него нет записи, потому что это просто обычный номер с прикрепленным TH ... Но такие, как TWELVE, требуют другого внимания.

Итак, теперь у нас есть код для построения нашего (уродливого) RegEx, теперь мы просто выполняем его на наших числовых строках.

Одна вещь, которую я бы порекомендовал, это фильтровать или есть слово "И". Это не обязательно, а только приводит к другим проблемам.

Итак, что вы хотите сделать, это настроить функцию, которая передает именованные совпадения для "Magnitude" в функцию, которая просматривает все возможные значения величины и умножает ваш текущий результат на это значение величины. Затем вы создаете функцию, которая просматривает именованные совпадения "Value" и возвращает int (или то, что вы используете) на основе обнаруженного там значения.

Все совпадения VALUE добавляются к вашему результату, а совпадения Magnitutde умножают результат на значение mag. Итак, двести пятьдесят тысяч становится "2", затем "2 * 100", затем "200 + 50", затем "250 * 1000", в итоге получая 250000 ...

Ради интереса я написал эту версию на vbScript, и она прекрасно работала со всеми приведенными примерами. Теперь он не поддерживает именованные совпадения, поэтому мне пришлось потрудиться, чтобы получить правильный результат, но я его получил. Суть в том, что если это совпадение VALUE, добавьте его в свой аккумулятор. Если это совпадение по величине, умножьте ваш аккумулятор на 100, 1000, 1000000, 1000000000 и т.д. ... Это даст вам довольно удивительные результаты, и все, что вам нужно сделать, чтобы приспособиться к таким вещам, как "половина", это добавить их в свой RegEx, вставьте кодовый маркер для них и обработайте их.

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

Если можно .. На каком конечном языке это будет написано? C++ или что-то вроде скриптового языка? Источник Грега Хьюгилла поможет понять, как все это происходит вместе.

Дайте мне знать, могу ли я чем-нибудь помочь. Извините, я знаю только английский/американский, поэтому не могу помочь вам с другими языками.

2
LarryF

Я конвертировал порядковые операторы из ранних современных книг (например, "2nd edition", "Editio quarta") в целые числа и нуждался в поддержке ординалов 1-100 на английском языке и ординалов 1-10 на нескольких романских языках. Вот что я придумал в Python:

def get_data_mapping():
  data_mapping = {
    "1st": 1,
    "2nd": 2,
    "3rd": 3,

    "tenth": 10,
    "eleventh": 11,
    "twelfth": 12,
    "thirteenth": 13,
    "fourteenth": 14,
    "fifteenth": 15,
    "sixteenth": 16,
    "seventeenth": 17,
    "eighteenth": 18,
    "nineteenth": 19,
    "twentieth": 20,

    "new": 2,
    "newly": 2,
    "nova": 2,
    "nouvelle": 2,
    "altera": 2,
    "andere": 2,

    # latin
    "primus": 1,
    "secunda": 2,
    "tertia": 3,
    "quarta": 4,
    "quinta": 5,
    "sexta": 6,
    "septima": 7,
    "octava": 8,
    "nona": 9,
    "decima": 10,

    # italian
    "primo": 1,
    "secondo": 2,
    "terzo": 3,
    "quarto": 4,
    "quinto": 5,
    "sesto": 6,
    "settimo": 7,
    "ottavo": 8,
    "nono": 9,
    "decimo": 10,

    # french
    "premier": 1,
    "deuxième": 2,
    "troisième": 3,
    "quatrième": 4,
    "cinquième": 5,
    "sixième": 6,
    "septième": 7,
    "huitième": 8,
    "neuvième": 9,
    "dixième": 10,

    # spanish
    "primero": 1,
    "segundo": 2,
    "tercero": 3,
    "cuarto": 4,
    "quinto": 5,
    "sexto": 6,
    "septimo": 7,
    "octavo": 8,
    "noveno": 9,
    "decimo": 10
  }

  # create 4th, 5th, ... 20th
  for i in xrange(16):
    data_mapping[str(4+i) + "th"] = 4+i

  # create 21st, 22nd, ... 99th
  for i in xrange(79):
    last_char = str(i)[-1]

    if last_char == "0":
      data_mapping[str(20+i) + "th"] = 20+i

    Elif last_char == "1":
      data_mapping[str(20+i) + "st"] = 20+i

    Elif last_char == "2":
      data_mapping[str(20+i) + "nd"] = 20+i

    Elif last_char == "3":
      data_mapping[str(20+i) + "rd"] = 20+i

    else:
      data_mapping[str(20+i) + "th"] = 20+i

  ordinals = [
    "first", "second", "third", 
    "fourth", "fifth", "sixth", 
    "seventh", "eighth", "ninth"
  ]

  # create first, second ... ninth
  for c, i in enumerate(ordinals):
    data_mapping[i] = c+1

  # create twenty-first, twenty-second ... ninty-ninth
  for ci, i in enumerate([
    "twenty", "thirty", "forty", 
    "fifty", "sixty", "seventy", 
    "eighty", "ninety"
  ]):
    for cj, j in enumerate(ordinals):
      data_mapping[i + "-" + j] = 20 + (ci*10) + (cj+1)
    data_mapping[i.replace("y", "ieth")] = 20 + (ci*10)

  return data_mapping
0
duhaime