如何将 utf-8 字节偏移量转换为 utf-8 字符偏移量
Howt to convert utf-8 byte offsets to utf-8 character offsets
我需要 post 处理报告 utf-8 字节偏移而不是 utf-8 字符偏移的遗留工具的输出。
例如,对于七字节 utf-8 字符串 'aβgδe'
中的 5 个字符,它将报告 [0, 1, 3, 4, 6]
而不是 [0, 1, 2, 3, 4]
,因为希腊字母“β”和“δ”是编码为双字节序列。 (实际文本还可能包含3字节和4字节的utf-8序列。)
是否有可用于将 utf-8 字节偏移量转换为 utf-8 字符偏移量的内置 Python 函数?
我认为没有用于此的内置或标准库实用程序,但您可以编写自己的小函数来创建字节偏移到代码点偏移的映射。
天真的方法
import typing as t
def map_byte_to_codepoint_offset(text: str) -> t.Dict[int, int]:
mapping = {}
byte_offset = 0
for codepoint_offset, character in enumerate(text):
mapping[byte_offset] = codepoint_offset
byte_offset += len(character.encode('utf8'))
return mapping
让我们用你的例子来测试一下:
>>> text = 'aβgδe'
>>> byte_offsets = [0, 1, 3, 4, 6]
>>> mapping = map_byte_to_codepoint_offset(text)
>>> mapping
{0: 0, 1: 1, 3: 2, 4: 3, 6: 4}
>>> [mapping[o] for o in byte_offsets]
[0, 1, 2, 3, 4]
优化
我没有对此进行基准测试,但对每个字符分别调用 .encode()
可能不是很有效。此外,我们只对编码字符的字节长度感兴趣,它只能取对应于连续代码点范围的四个值之一。
要获得这些范围,可以研究 UTF-8 编码规范,在互联网上查找它们,或者 运行 在 Python REPL 中进行快速计算:
>>> import sys
>>> bins = {i: [] for i in (1, 2, 3, 4)}
>>> for codepoint in range(sys.maxunicode+1):
... # 'surrogatepass' required to allow encoding surrogates in UTF-8
... length = len(chr(codepoint).encode('utf8', errors='surrogatepass'))
... bins[length].append(codepoint)
...
>>> for l, cps in bins.items():
... print(f'{l}: {hex(min(cps))}..{hex(max(cps))}')
...
1: 0x0..0x7f
2: 0x80..0x7ff
3: 0x800..0xffff
4: 0x10000..0x10ffff
此外,朴素方法返回的映射包含间隙:如果我们查找位于多字节字符中间的偏移量,我们将得到一个 KeyError(例如,没有键 2
在上面的例子中)。为避免这种情况,我们可以通过重复代码点偏移量来填补空白。由于生成的索引将是从 0 开始的连续整数,因此我们可以使用列表而不是字典进行映射。
TWOBYTES = 0x80
THREEBYTES = 0x800
FOURBYTES = 0x10000
def map_byte_to_codepoint_offset(text: str) -> t.List[int]:
mapping = []
for codepoint_offset, character in enumerate(text):
mapping.append(codepoint_offset)
codepoint = ord(character)
for cue in (TWOBYTES, THREEBYTES, FOURBYTES):
if codepoint >= cue:
mapping.append(codepoint_offset)
else:
break
return mapping
以上面的例子为例:
>>> mapping = map_byte_to_codepoint_offset(text)
>>> mapping
[0, 1, 1, 2, 3, 3, 4]
>>> [mapping[o] for o in byte_offsets]
[0, 1, 2, 3, 4]
我需要 post 处理报告 utf-8 字节偏移而不是 utf-8 字符偏移的遗留工具的输出。
例如,对于七字节 utf-8 字符串 'aβgδe'
中的 5 个字符,它将报告 [0, 1, 3, 4, 6]
而不是 [0, 1, 2, 3, 4]
,因为希腊字母“β”和“δ”是编码为双字节序列。 (实际文本还可能包含3字节和4字节的utf-8序列。)
是否有可用于将 utf-8 字节偏移量转换为 utf-8 字符偏移量的内置 Python 函数?
我认为没有用于此的内置或标准库实用程序,但您可以编写自己的小函数来创建字节偏移到代码点偏移的映射。
天真的方法
import typing as t
def map_byte_to_codepoint_offset(text: str) -> t.Dict[int, int]:
mapping = {}
byte_offset = 0
for codepoint_offset, character in enumerate(text):
mapping[byte_offset] = codepoint_offset
byte_offset += len(character.encode('utf8'))
return mapping
让我们用你的例子来测试一下:
>>> text = 'aβgδe'
>>> byte_offsets = [0, 1, 3, 4, 6]
>>> mapping = map_byte_to_codepoint_offset(text)
>>> mapping
{0: 0, 1: 1, 3: 2, 4: 3, 6: 4}
>>> [mapping[o] for o in byte_offsets]
[0, 1, 2, 3, 4]
优化
我没有对此进行基准测试,但对每个字符分别调用 .encode()
可能不是很有效。此外,我们只对编码字符的字节长度感兴趣,它只能取对应于连续代码点范围的四个值之一。
要获得这些范围,可以研究 UTF-8 编码规范,在互联网上查找它们,或者 运行 在 Python REPL 中进行快速计算:
>>> import sys
>>> bins = {i: [] for i in (1, 2, 3, 4)}
>>> for codepoint in range(sys.maxunicode+1):
... # 'surrogatepass' required to allow encoding surrogates in UTF-8
... length = len(chr(codepoint).encode('utf8', errors='surrogatepass'))
... bins[length].append(codepoint)
...
>>> for l, cps in bins.items():
... print(f'{l}: {hex(min(cps))}..{hex(max(cps))}')
...
1: 0x0..0x7f
2: 0x80..0x7ff
3: 0x800..0xffff
4: 0x10000..0x10ffff
此外,朴素方法返回的映射包含间隙:如果我们查找位于多字节字符中间的偏移量,我们将得到一个 KeyError(例如,没有键 2
在上面的例子中)。为避免这种情况,我们可以通过重复代码点偏移量来填补空白。由于生成的索引将是从 0 开始的连续整数,因此我们可以使用列表而不是字典进行映射。
TWOBYTES = 0x80
THREEBYTES = 0x800
FOURBYTES = 0x10000
def map_byte_to_codepoint_offset(text: str) -> t.List[int]:
mapping = []
for codepoint_offset, character in enumerate(text):
mapping.append(codepoint_offset)
codepoint = ord(character)
for cue in (TWOBYTES, THREEBYTES, FOURBYTES):
if codepoint >= cue:
mapping.append(codepoint_offset)
else:
break
return mapping
以上面的例子为例:
>>> mapping = map_byte_to_codepoint_offset(text)
>>> mapping
[0, 1, 1, 2, 3, 3, 4]
>>> [mapping[o] for o in byte_offsets]
[0, 1, 2, 3, 4]