Кодирование арабских букв с их диакритиками (если есть)

Вопрос задан: 1 год назад Последняя активность: 1 год назад
up 11 down

Я работаю на глубокий учебный проект, в котором мы используем РННА. Я хочу, чтобы кодировать данные, прежде чем он поступает в сеть. Ввод является арабскими стихами, которые имеют диакритические, которые рассматриваются как отдельные символы в Python. Я должен кодировать/представление символа с символом следующей за ним с номером, если символ после него является диакритическим, то я только закодировать характер.

Это миллионы стихов, надеялся использовать lambda с участием map. Тем не менее, я не могу перебирать с двумя персонажами одновременно, т.е. надеялся:

map(lambda ch, next_ch: encode(ch + next_ch) if is_diacritic(next_ch) else encode(ch), verse)

Мое намерение за вопрос найти самый быстрый способ сделать выше. Нет ограничений на функции лямбда, но for ответы петли не то, что я ищу.

Близкий пример не-арабам, предположим, что вы хотите закодировать следующий текст:

 XXA)L_I!I%M<LLL>MMQ*Q

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

Выход:

['X', 'X', 'A)', 'L_', 'I!', 'I%', 'M<', 'L', 'L', 'L>', 'M', 'M', 'Q*', 'Q']

Для Аравитян:

Стих, например:

"قفا نبك من ذِكرى حبيب ومنزل بسِقطِ اللّوى بينَ الدَّخول فحَوْمل"

Диакритические эти маленькие символы выше буквы (т.е..,)


[Обновить]

Диапазон диакритиков начинается в 64B HEX или INT 1611 и заканчивается 652 HEX или INT 1618.

И буквы 621 HEX - 1569 INT в 63А HEX - 1594 INT и из 641 HEX - 1601 INT в 64А HEX - 1610 INT

Письмо может иметь не более одного диакритическое.


Дополнительная информация:

Подобная методика кодирования с тем, что я делаю, представляющим двоичную форму стиха как матрицы с формой (number of bits needed, number of characters in a verse). И число битов и количество символов вычисляются после того как мы объединить каждую букву с диакритическим, если она существует.

Например, предположим, что этот стих является следующий, и диакритические специальные символы:

X+Y_XX+YYYY_

Алфавит различных комбинаций:

['X', 'X+', 'X_', 'Y', 'Y+', 'Y_']  

Поэтому мне нужно 3 биты (по крайней мере), чтобы представить эти 6 символов, так number of bits needed является 3

Рассмотрим следующие кодировки:

{
'X' : 000,
'X+': 001,
'X_': 010,
'Y':  011,
'Y+': 100,
'Y_': 101,
}

И я, чтобы представить пример в матрице (двоичное представление по вертикали):

X+     Y_    X    X+    Y    Y    Y    Y_
0      1     0    0     0    0    0    1
0      0     0    0     1    1    1    0
1      1     0    1     1    1    1    1

Именно поэтому я ищу, чтобы объединить диакритические с письмами первым.


Примечание: Перебрать строки 2 (или п) символов в то время в Python а также Повторяя каждый символ в строке с помощью Python не дают намеченный ответ.

3 ответа

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

Реклама

up 4 down

map не кажется, правильный инструмент для работы. Вы не хотите, чтобы отобразить символы других персонажей, но сгруппировать их вместе. Вместо этого, вы можете попробовать reduce (или же functools.reduce в Python 3). Здесь, я использую isalpha чтобы проверить, какой характер она; вам может понадобиться что-то другое.

>>> is_diacritic = lambda x: not x.isalpha()
>>> verse = "XXA)L_I!I%M<LLL>MMQ*Q"
>>> reduce(lambda lst, x: lst + [x] if not is_diacritic(x) else lst[:-1] + [lst[-1]+x], verse, [])
['X', 'X', 'A)', 'L_', 'I!', 'I%', 'M<', 'L', 'L', 'L>', 'M', 'M', 'Q*', 'Q']

Однако, это едва читаемое, а также создает множество промежуточных списков. Лучше всего использовать скучный старый for цикл, даже если вы явно просили что-то еще:

res = []
for x in verse:
    if not is_diacritic(x):
        res.append(x)
    else:
        res[-1] += x

Повторяя пары следующих друг за другом символов, например, с помощью zip(verse, verse[1:]) (Т.е. (1,2), (2,3),..., не (1,2), (3,4), ...), Вы можете действительно использовать список понимание, но я бы все-таки проголосовать за for петля для удобства чтения.

>>> [x + y if is_diacritic(y) else x
...  for x, y in zip_longest(verse, verse[1:], fillvalue="")
...  if not is_diacritic(x)]
...
['X', 'X', 'A)', 'L_', 'I!', 'I%', 'M<', 'L', 'L', 'L>', 'M', 'M', 'Q*', 'Q']

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

up 4 down accepted

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

arr = np.array([verse]).view(np.uint32)

Вы можете маскировать места, где следующий символ является диакритическим:

mask = np.empty(arr.shape, dtype=np.bool)
np.bitwise_and((arr[1:] > lower), (arr[1:] < upper), out=mask[:-1])
mask[-1] = False

Здесь диапазон [upper, lower] это составлен способ для проверки диакритиков. Реализовать фактическую проверку, однако вам нравится. В этом примере я использовал полномасштабную форму bitwise_and с участием empty чтобы избежать потенциально дорогостоящего Append последнего элемента.

Теперь, если у вас есть численный метод для кодирования ваших точек коды на номер, который я уверен, что вы можете векторизации, вы можете сделать что-то вроде:

combined = combine(letters=arr[mask], diacritics=arr[1:][mask[:-1]])

Для того, чтобы получить оставшиеся, несвязанные символы, вы должны удалить оба diactitics и символы, они связываются. Самый простой способ, которым я могу думать делать это размазывание маски вправо и отрицая его. Опять же, я полагаю, что у вас есть векторизованный метод для кодирования одного символа, а также:

smeared = mask.copy()
smeared[1:] |= mask[:-1]
single = encode(arr[~smeared])

Комбинируя результат в конечный массив является концептуально простой, но занимает несколько шагов. Результат будет np.count_nonzeros(mask) элементы короче, чем на входе, начиная с диакритическими знаками, удаляются. Нам нужно перенести все маски элементов от суммы их индекса. Вот один из способов сделать это:

ind = np.flatnonzero(mask)
nnz = ind.size
ind -= np.arange(nnz)

output = np.empty(arr.size - nnz, dtype='U1')
output[ind] = combined

# mask of unmodified elements
out_mask = np.ones(output.size, dtype=np.bool)
out_mask[ind] = False
output[out_mask] = single

Причина, почему я предлагаю NumPy в том, что он должен быть в состоянии обрабатывать несколько миллионов символов в считанные секунды таким образом. Получение вывода обратно в виде строки должно быть простым.

Похожие реализации

Я обдумывал свой вопрос и решил играть с некоторыми задержками и возможными реализациями. Моя идея состояла в том, чтобы отобразить символы Юникода в 0x0621-0x063A, 0x0641-0x064A (26 + 10 = 36 букв) в нижние 6 битов uint16, и символы 0x064B-0x0652 (8 диакритические) к вышестоящему 3 бита, предполагая, что это на самом деле только диакритические вам нужно:

def encode_py(char):
    char = ord(char) - 0x0621
    if char >= 0x20:
        char -= 5
    return char

def combine_py(char, diacritic):
    return encode_py(char) | ((ord(diacritic) - 0x064A) << 6)

В Numpy условиях:

def encode_numpy(chars):
    chars = chars - 0x0621
    return np.subtract(chars, 5, where=chars > 0x20, out=chars)

def combine_numpy(chars, diacritics):
    chars = encode_numpy(chars)
    chars |= (diacritics - 0x064A) << 6
    return chars

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

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

import random

random.seed(0xB00B5)

alphabet = list(range(0x0621, 0x063B)) + list(range(0x0641, 0x064B))
diactitics = list(range(0x064B, 0x0653))

alphabet = [chr(x) for x in alphabet]
diactitics = [chr(x) for x in diactitics]

def sample(n=1000000, d=0.25):
    while n:
        yield random.choice(alphabet)
        n -= 1
        if n and random.random() < d:
            yield random.choice(diactitics)
            n -= 1

data = ''.join(sample())

Эти данные полностью распределены случайным образом символов, с примерно 25% шанс любого характера будучи последующим диакритическим. Это занимает всего несколько секунд, чтобы произвести на моем не слишком одолели ноутбук.

Преобразование NumPy будет выглядеть следующим образом:

def convert_numpy(verse):
    arr = np.array([verse]).view(np.uint32)
    mask = np.empty(arr.shape, dtype=np.bool)
    mask[:-1] = (arr[1:] >= 0x064B)
    mask[-1] = False

    combined = combine_numpy(chars=arr[mask], diacritics=arr[1:][mask[:-1]])

    smeared = mask.copy()
    smeared[1:] |= mask[:-1]
    single = encode_numpy(arr[~smeared])

    ind = np.flatnonzero(mask)
    nnz = ind.size
    ind -= np.arange(nnz)

    output = np.empty(arr.size - nnz, dtype=np.uint16)
    output[ind] = combined

    # mask of unmodified elements
    out_mask = np.ones(output.size, dtype=np.bool)
    out_mask[ind] = False
    output[out_mask] = single

    return output

Ориентиры

А теперь давайте %timeit чтобы увидеть, как она идет. Во-первых, здесь и другие варианты реализации. Я конвертировать все в Numpy массив или список целых чисел для справедливого сравнения. Я также сделал незначительные изменения, чтобы сделать эти функции возвращают списки тех же величины для проверки точности:

from itertools import tee, zip_longest
from functools import reduce

def is_diacritic(c):
    return ord(c) >= 0x064B

def pairwise(iterable, fillvalue):
    """ Slightly modified itertools pairwise recipe
    s -> (s0,s1), (s1,s2), (s2, s3), ... 
    """
    a, b = tee(iterable)
    next(b, None)
    return zip_longest(a, b, fillvalue=fillvalue)

def combine_py2(char, diacritic):
    return char | ((ord(diacritic) - 0x064A) << 6)

def convert_FHTMitchell(verse):
    def convert(verse):
        was_diacritic = False  # variable to keep track of diacritics -- stops us checking same character twice

        # fillvalue will not be encoded but ensures last char is read
        for this_char, next_char in pairwise(verse, fillvalue='-'):
            if was_diacritic:  # last next_char (so this_char) is diacritic
                was_diacritic = False
            elif is_diacritic(next_char):
                yield combine_py(this_char, next_char)
                was_diacritic = True
            else:
                yield encode_py(this_char)

    return list(convert(verse))

def convert_tobias_k_1(verse):
    return reduce(lambda lst, x: lst + [encode_py(x)] if not is_diacritic(x) else lst[:-1] + [combine_py2(lst[-1], x)], verse, [])

def convert_tobias_k_2(verse):
    res = []
    for x in verse:
        if not is_diacritic(x):
            res.append(encode_py(x))
        else:
            res[-1] = combine_py2(res[-1], x)
    return res

def convert_tobias_k_3(verse):
    return [combine_py(x, y) if y and is_diacritic(y) else encode_py(x) for x, y in zip_longest(verse, verse[1:], fillvalue="") if not is_diacritic(x)]

Теперь для таймингов:

%timeit result_FHTMitchell = convert_FHTMitchell(data)
338 ms ± 5.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit result_tobias_k_1 = convert_tobias_k_1(data)
Aborted, took > 5min to run. Appears to scale quadratically with input size: not OK!

%timeit result_tobias_k_2 = convert_tobias_k_2(data)
357 ms ± 4.94 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit result_tobias_k_3 = convert_tobias_k_3(data)
466 ms ± 4.62 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit result_numpy = convert_numpy(data)
30.2 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Сравнение полученных массивов/списков показывает, что они равны, а также:

np.array_equal(result_FHTMitchell, result_tobias_k_2)  # True
np.array_equal(result_tobias_k_2, result_tobias_k_3)   # True
np.array_equal(result_tobias_k_3, result_numpy)        # True

я использую array_equal здесь, потому что он выполняет все необходимые преобразования типов для проверки фактических данных.

Таким образом, мораль этой истории состоит в том, что есть много способов сделать это, и разбор несколько миллионов символов не должно быть слишком дорогими по себе, пока вы не получите в перекрестные ссылки и другие действительно трудоемкие задачи. Главное, чтобы извлечь из этого не использовать reduce в списках, так как вы будете перераспределять намного больше, чем вам нужно. Даже простой for цикл будет работать нормально для ваших целей. Несмотря на то, NumPy примерно в десять раз быстрее, чем в других реализациях, она не дает огромное преимущество.

расшифровка

Для полноты картины, здесь есть функция, чтобы декодировать результаты:

def decode(arr):
    mask = (arr > 0x3F)
    nnz = np.count_nonzero(mask)
    ind = np.flatnonzero(mask) + np.arange(nnz)

    diacritics = (arr[mask] >> 6) + 41
    characters = (arr & 0x3F)
    characters[characters >= 27] += 5

    output = np.empty(arr.size + nnz, dtype='U1').view(np.uint32)
    output[ind] = characters[mask]
    output[ind + 1] = diacritics

    output_mask = np.zeros(output.size, dtype=np.bool)
    output_mask[ind] = output_mask[ind + 1] = True
    output[~output_mask] = characters[~mask]

    output += 0x0621

    return output.base.view(f'U{output.size}').item()

В качестве примечания, работа, которую я сделал здесь вдохновила на этот вопрос: Преобразование Numpy массивов кодовых точек и из строк

up 2 down

Вы не читаете два символа в то время, и даже если вы были, map не разделить их на два параметра для lambda.

from itertools import tee, zip_longest

def pairwise(iterable, fillvalue):
    """ Slightly modified itertools pairwise recipe
    s -> (s0,s1), (s1,s2), (s2, s3), ... 
    """
    a, b = tee(iterable)
    next(b, None)
    return zip_longest(a, b, fillvalue=fillvalue)

def encode_arabic(verse):

    was_diacritic = False  # variable to keep track of diacritics -- stops us checking same character twice

    # fillvalue will not be encoded but ensures last char is read
    for this_char, next_char in pairwise(verse, fillvalue='-'):

        if was_diacritic:  # last next_char (so this_char) is diacritic
            was_diacritic = False

        elif is_diacritic(next_char):
            yield encode(this_char + next_char)
            was_diacritic = True

        else:
            yield this_char

encode_arabic(verse)  # returns a generator like map -- wrap in list / string.join / whatever

Ошибка 505

Что-то пошло не так

Попробуйте воспользоваться поиском