我要把我的帽子扔到戒指里,让我麻木。可以将字符串转换为可使用的格式
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
以避免最后一个元素的附加成本可能很高。
现在,如果您有一个将代码点编码为数字的数字方法,我确信您可以对其进行矢量化,那么您可以执行如下操作:
combined = combine(letters=arr[mask], diacritics=arr[1:][mask[:-1]])
要获得剩余的未组合字符,您必须同时删除二元符号和它们绑定到的字符。我能想到的最简单的方法就是把面具涂到右边,然后把它涂掉。同样,我假设您有一个矢量化方法来编码单个字符:
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的原因是它应该能够在几秒钟内以这种方式处理数百万个字符。将输出作为字符串返回应该很简单。
建议实施
我一直在思考您的问题,并决定考虑一些时间安排和可能的实现。我的想法是将Unicode字符映射到
0x0621-0x063A
,
0x064 1-0x064
(26+10=36个字母)输入
uint16
以及角色
0x064 B-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)
麻木地:
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比其他实现快10倍,但它并没有提供巨大的优势。
译码
为了完整起见,这里有一个函数来解码您的结果:
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()
作为旁注,我在这里所做的工作启发了这个问题:
Converting numpy arrays of code points to and from strings