Нечеткий интеллектуальный анализ чисел в Python

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

Я использую следующее:

# NOTE: Do not use, this algorithm is buggy. See below.
def extractnumber(value):

    if (isinstance(value, int)): return value
    if (isinstance(value, float)): return value

    result = re.sub(r'&#\d+', '', value)
    result = re.sub(r'[^0-9\,\.]', '', result)

    if (len(result) == 0): return None

    numPoints = result.count('.')
    numCommas = result.count(',')

    result = result.replace(",", ".")

    if ((numPoints > 0 and numCommas > 0) or (numPoints == 1) or (numCommas == 1)):
        decimalPart = result.split(".")[-1]
        integerPart = "".join ( result.split(".")[0:-1] )
    else:
        integerPart = result.replace(".", "")

    result = int(integerPart) + (float(decimalPart) / pow(10, len(decimalPart) ))

    return result

Такие работы...

>>> extractnumber("2")
2
>>> extractnumber("2.3")
2.3
>>> extractnumber("2,35")
2.35
>>> extractnumber("-2 000,5")
-2000.5
>>> extractnumber("EUR 1.000,74 €")
1000.74

>>> extractnumber("20,5 20,8") # Testing failure...
ValueError: invalid literal for int() with base 10: '205 208'

>>> extractnumber("20.345.32.231,50") # Returns false positive
2034532231.5

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

Есть ли какая-нибудь библиотека или интеллектуальная функция, которая может справиться с этим? В идеале 20.345.32.231,50 не должен проходить, но числа на других языках, таких как 1.200,50 или 1 200'50, будут извлечены, независимо от количества другого текста и символов (включая символы новой строки) вокруг.

(Обновленная реализация в соответствии с принятым ответом: https://github.com/jjmontesl/cubetl/blob/master/cubetl/text/functions.py#L91)


person jjmontes    schedule 23.11.2013    source источник
comment
Какой результат вы ожидаете от 123 456? 123456 или [123, 456]?   -  person Steinar Lima    schedule 23.11.2013
comment
:) Позвольте мне отредактировать и удалить часть с несколькими номерами.   -  person jjmontes    schedule 23.11.2013


Ответы (2)


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

Во-первых, шаблон регулярного выражения:

_pattern = r"""(?x)       # enable verbose mode (which ignores whitespace and comments)
    ^                     # start of the input
    [^\d+-\.]*            # prefixed junk
    (?P<number>           # capturing group for the whole number
        (?P<sign>[+-])?       # sign group (optional)
        (?P<integer_part>         # capturing group for the integer part
            \d{1,3}               # leading digits in an int with a thousands separator
            (?P<sep>              # capturing group for the thousands separator
                [ ,.]                 # the allowed separator characters
            )
            \d{3}                 # exactly three digits after the separator
            (?:                   # non-capturing group
                (?P=sep)              # the same separator again (a backreference)
                \d{3}                 # exactly three more digits
            )*                    # repeated 0 or more times
        |                     # or
            \d+                   # simple integer (just digits with no separator)
        )?                    # integer part is optional, to allow numbers like ".5"
        (?P<decimal_part>     # capturing group for the decimal part of the number
            (?P<point>            # capturing group for the decimal point
                (?(sep)               # conditional pattern, only tested if sep matched
                    (?!                   # a negative lookahead
                        (?P=sep)              # backreference to the separator
                    )
                )
                [.,]                  # the accepted decimal point characters
            )
            \d+                   # one or more digits after the decimal point
        )?                    # the whole decimal part is optional
    )
    [^\d]*                # suffixed junk
    $                     # end of the input
"""

И вот функция для его использования:

def parse_number(text):
    match = re.match(_pattern, text)
    if match is None or not (match.group("integer_part") or
                             match.group("decimal_part")):    # failed to match
        return None                      # consider raising an exception instead

    num_str = match.group("number")      # get all of the number, without the junk
    sep = match.group("sep")
    if sep:
        num_str = num_str.replace(sep, "")     # remove thousands separators

    if match.group("decimal_part"):
        point = match.group("point")
        if point != ".":
            num_str = num_str.replace(point, ".")  # regularize the decimal point
        return float(num_str)

    return int(num_str)

Некоторые числовые строки, содержащие ровно одну запятую или точку и ровно три цифры после нее (например, "1,234" и "1.234"), являются неоднозначными. Этот код будет анализировать их оба как целые числа с разделителем тысяч (1234), а не значения с плавающей запятой (1.234), независимо от фактического используемого символа-разделителя. Возможно, вы могли бы справиться с этим в особом случае, если вам нужен другой результат для этих чисел (например, если вы предпочитаете сделать число с плавающей запятой из 1.234).

Некоторый тестовый вывод:

>>> test_cases = ["2", "2.3", "2,35", "-2 000,5", "EUR 1.000,74 €",
                  "20,5 20,8", "20.345.32.231,50", "1.234"]
>>> for s in test_cases:
    print("{!r:20}: {}".format(s, parse_number(s)))


'2'                 : 2
'2.3'               : 2.3
'2,35'              : 2.35
'-2 000,5'          : -2000.5
'EUR 1.000,74 €'    : 1000.74
'20,5 20,8'         : None
'20.345.32.231,50'  : None
'1.234'             : 1234
person Blckknght    schedule 23.11.2013
comment
Круто, как ты их строишь? рукой? - person jjmontes; 23.11.2013
comment
Этот я построил вручную, да. Изначально он не был в подробном режиме. Отдельные части (знак, целая часть, десятичная часть) изначально находились в отдельных строках. Но подробный режим, безусловно, помог с отладкой и объяснением! Единственной действительно тонкой частью шаблона был условный отрицательный просмотр вперед, который не позволяет десятичной точке быть тем же символом, что и разделитель тысяч. Это гарантирует, что строка типа "1,234,56" не будет сопоставляться с 1234.56. О, и я добавил поддержку чисел без целой части (например, .5) только в самом конце. - person Blckknght; 23.11.2013
comment
Фантастически, я реализовал ваше решение, добавив исключение для случая 1,234 (я предпочел, чтобы оно было десятичным). Окончательная версия на своем последнем месте: github.com /jjmontesl/cubetl/blob/master/src/cubetl/функции/ - person jjmontes; 25.11.2013
comment
Кроме того, я проверил старую и новую версии более чем на 40 000 документов по сравнению с моей, и в моей версии был обнаружен сбой («0,06», проанализированный как «-0,06»). Так что мой алгоритм не работает и его нельзя использовать ни в коем случае :), ваш крут. - person jjmontes; 25.11.2013
comment
Отличное решение с исчерпывающими комментариями! Очень познавательно. - person skabbit; 03.05.2020

Я немного переделал ваш код. Это, вместе с функцией valid_number ниже, должно помочь.

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

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

Ограничения

  • ., , и ' принимаются как в качестве разделителя тысяч, так и в качестве десятичного разделителя.
  • Не более двух разных сепараторов
  • Максимум один разделитель с более чем одним вхождением
  • Разделитель рассматривается как десятичный разделитель, если присутствует только один разделитель и только один такого типа. (т.е. 123,456 интерпретируются как 123.456, а не 123456)
  • Строка разбивается на список чисел двойным пробелом (' ')
  • Все части числа, разделенного тысячами, за исключением первой части, должны состоять из 3 цифр (123,456.00 и 1,345.00 считаются действительными, но 2345,11.00 не считается допустимым)

Код

import re

from itertools import combinations

def extract_number(value):
    if (isinstance(value, int)) or (isinstance(value, float)):
        yield float(value)
    else:
        #Strip the string for leading and trailing whitespace
        value = value.strip()
        if len(value) == 0:
            raise StopIteration
        for s in value.split('  '):
            s = re.sub(r'&#\d+', '', s)
            s = re.sub(r'[^\-\s0-9\,\.]', ' ', s)
            s = s.replace(' ', '')
            if len(s) == 0:
                continue
            if not valid_number(s):
                continue
            if not sum(s.count(sep) for sep in [',', '.', '\'']):
                yield float(s)
            else:
                s = s.replace('.', '@').replace('\'', '@').replace(',', '@')
                integer, decimal = s.rsplit('@', 1)
                integer = integer.replace('@', '')
                s = '.'.join([integer, decimal])
                yield float(s)

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

def valid_number(s):
    def _correct_integer(integer):
        # First number should have length of 1-3
        if not (0 < len(integer[0].replace('-', '')) < 4):
            return False
        # All the rest of the integers should be of length 3
        for num in integer[1:]:
            if len(num) != 3:
                return False
        return True
    seps = ['.', ',', '\'']
    n_seps = [s.count(k) for k in seps]

    # If no separator is present
    if sum(n_seps) == 0:
        return True

    # If all separators are present
    elif all(n_seps):
        return False

    # If two separators are present
    elif any(all(c) for c in combinations(n_seps, 2)):
        # Find thousand separator
        for c in s:
            if c in seps:
                tho_sep = c
                break

        # Find decimal separator:
        for c in reversed(s):
            if c in seps:
                dec_sep = c
                break

        s = s.split(dec_sep)

        # If it is more than one decimal separator
        if len(s) != 2:
            return False

        integer = s[0].split(tho_sep)

        return _correct_integer(integer)

    # If one separator is present, and it is more than one of it
    elif sum(n_seps) > 1:
        for sep in seps:
            if sep in s:
                s = s.split(sep)
                break
        return _correct_integer(s)

    # Otherwise, this is a regular decimal number
    else:
        return True

Выход

extract_number('2'                  ):  [2.0]
extract_number('.2'                 ):  [0.2]
extract_number(2                    ):  [2.0]
extract_number(0.2                  ):  [0.2]
extract_number('EUR 200'            ):  [200.0]
extract_number('EUR 200.00  -11.2'  ):  [200.0, -11.2]
extract_number('EUR 200  EUR 300'   ):  [200.0, 300.0]
extract_number('$ -1.000,22'        ):   [-1000.22]
extract_number('EUR 100.2345,3443'  ):  []
extract_number('111,145,234.345.345'):  []
extract_number('20,5  20,8'         ):  [20.5, 20.8]
extract_number('20.345.32.231,50'   ):  []
person Steinar Lima    schedule 23.11.2013
comment
Допустимы несколько запятых и точек, так как в некоторых языках используется . для разделителя тысяч и , для десятичных знаков. Некоторое другое использование ' для десятичных знаков, которое в идеале тоже было бы принято. Для этих языков группировка всегда должна производиться блоками по 3 цифры: 1.245.000,50 — действительный испанский. - person jjmontes; 23.11.2013
comment
Включая пробелы, я хотел бы принимать несколько чисел, если отдельные числа четко разделены более чем одним пробелом или другим текстом, но принятие нескольких чисел на самом деле мне не нужно. - person jjmontes; 23.11.2013
comment
Хорошо, вы должны прочитать о более продвинутом регулярном выражении :) У меня нет большого опыта в этой области, но я нашел эта ссылка, которая может помочь вам в пути. - person Steinar Lima; 23.11.2013