将代码点的numpy数组转换为字符串,以及从字符串转换回代码点的numpy数组

3

我有一个很长的Unicode字符串:

alphabet = range(0x0FFF)
mystr = ''.join(chr(random.choice(alphabet)) for _ in range(100))
mystr = re.sub('\W', '', mystr)

我希望将其视为代码点的一系列,因此目前我正在执行以下操作:

arr = np.array(list(mystr), dtype='U1')

我希望能够将字符串作为数字进行操作,并最终得到不同的编码点。现在,我想要反转这个转换:

mystr = ''.join(arr.tolist())

这些转换速度较快且可逆,但使用中间的list会占用不必要的空间。
有没有一种方法可以在不先转换为列表的情况下将numpy数组中的Unicode字符转换为Python字符串并相反地转换回来?
思考之后:
我可以使用类似以下内容使arr显示为单个字符串:
buf = arr.view(dtype='U' + str(arr.size))

这会导致一个包含整个原始内容的单元素数组。反过来也是可以的:
buf.view(dtype='U1')

唯一的问题是结果的类型是np.str_而不是str


那么这个怎么样:np.frombuffer(mystr, dtype=np.uint8) - Divakar
@DanielMesejo。抱歉,我删除了.view(np.uint32)部分,因为它相当无关紧要。 - Mad Physicist
@DanielMesejo。显然不是这样。我没有考虑过那个。那个的反义词是什么? - Mad Physicist
对于字符串表示,可以考虑使用以下代码:np.fromstring(mystr, dtype=np.uint8).view('S1') - Divakar
@Divakar。我已经发布了答案。 - Mad Physicist
显示剩余8条评论
2个回答

3

fromiter函数可以使用,但因为它需要遍历迭代器协议,所以速度比较慢。如果将数据编码为UTF-32(系统字节顺序),然后使用numpy.frombuffer函数,速度会更快:

In [56]: x = ''.join(chr(random.randrange(0x0fff)) for i in range(1000))

In [57]: codec = 'utf-32-le' if sys.byteorder == 'little' else 'utf-32-be'

In [58]: %timeit numpy.frombuffer(bytearray(x, codec), dtype='U1')
2.79 µs ± 47 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [59]: %timeit numpy.fromiter(x, dtype='U1', count=len(x))
122 µs ± 3.82 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [60]: numpy.array_equal(numpy.fromiter(x, dtype='U1', count=len(x)), numpy.fr
    ...: ombuffer(bytearray(x, codec), dtype='U1'))
Out[60]: True

我使用了sys.byteorder来确定是使用utf-32-le还是utf-32-be进行编码。此外,使用bytearray而不是encode可以获得可变的bytearray而不是不可变的bytes对象,所以结果数组是可写的。
关于反向转换,arr.view(dtype=f'U{arr.size}')[0]可以正常工作,但使用item()速度稍快,并生成普通字符串对象,避免可能的奇怪边缘情况,其中numpy.str_str不完全一样:
In [72]: a = numpy.frombuffer(bytearray(x, codec), dtype='U1')

In [73]: type(a.view(dtype=f'U{a.size}')[0])
Out[73]: numpy.str_

In [74]: type(a.view(dtype=f'U{a.size}').item())
Out[74]: str

In [75]: %timeit a.view(dtype=f'U{a.size}')[0]
3.63 µs ± 34 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [76]: %timeit a.view(dtype=f'U{a.size}').item()
2.14 µs ± 23.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

最后要注意的是,NumPy 不像普通的 Python 字符串对象一样处理 null 值。NumPy 无法区分 'asdf\x00\x00\x00''asdf',因此,如果你的数据可能包含 null 码点,则使用 NumPy 数组进行字符串操作是不安全的。


我相当确定U1代表系统字节顺序。在我的情况下,它转换为<U1,但实际的dtype将byteorder设置为`=。 - Mad Physicist
我甚至不知道item的存在。肯定会选择这个答案而不是我的。 - Mad Physicist
我也从未考虑过。只是偶然间发现了它 :) - Mad Physicist
@MadPhysicist:实际上,在我的测试中,它似乎比frombuffer慢。它足够快,我仍然会选择它,因为它更简单,更少容易出错,但它实际上并没有在速度上击败frombuffer。我想知道是什么导致了我们观察结果的差异。 - user2357112
我认为这种优势在较大的字符串中完全消失了。我测试了100、10K和1M,从缓冲区读取只在第一种情况下获胜。我在我的答案中放置了时间。 - Mad Physicist
显示剩余4条评论

2
将字符串转换为数组的最快方法,我发现是:
arr = np.array([mystr]).view(dtype='U1')

另一种将字符串转换为基于Unicode代码点的数组的方法(较慢),基于@Daniel Mesejo's comment
arr = np.fromiter(mystr, dtype='U1', count=len(mystr))

查看 fromiter 的源代码,可以发现将 count 参数设置为字符串的长度会导致整个数组一次性分配,而不是执行多个重新分配。
要转换回字符串:
str(arr.view(dtype=f'U{arr.size}')[0])

对于大多数情况而言,最终将其转换成Python的str并不是必要的,因为np.str_str的一个子类。
arr.view(dtype=f'U{arr.size}')[0]

附录:frombuffer与array的时间差
100
mystr = ''.join(chr(random.choice(range(1, 0x1000))) for _ in range(100))

%timeit np.array([mystr]).view(dtype='U1')
1.43 µs ± 27.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit np.frombuffer(bytearray(mystr, 'utf-32-le'), dtype='U1')
1.2 µs ± 9.06 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

10,000
mystr = ''.join(chr(random.choice(range(1, 0x1000))) for _ in range(10000))

%timeit np.array([mystr]).view(dtype='U1')
4.33 µs ± 13.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit np.frombuffer(bytearray(mystr, 'utf-32-le'), dtype='U1')
10.9 µs ± 29.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

One million.
mystr = ''.join(chr(random.choice(range(1, 0x1000))) for _ in range(1000000))

%timeit np.array([mystr]).view(dtype='U1')
672 µs ± 1.64 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit np.frombuffer(bytearray(mystr, 'utf-32-le'), dtype='U1')
732 µs ± 5.22 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

这并不是更好的扩展。相比于733微秒,这只有53.4毫秒。 - user2357112
@user2357112。我自己走了。去一所好学校。在那里我可以学会阅读。 - Mad Physicist
我自己也犯过同样的错误很多次。 - user2357112

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接