如何将 utf-8 字节偏移量转换为 utf-8 字符偏移量

Posted

技术标签:

【中文标题】如何将 utf-8 字节偏移量转换为 utf-8 字符偏移量【英文标题】:Howt to convert utf-8 byte offsets to utf-8 character offsets 【发布时间】:2022-01-09 17:00:43 【问题描述】:

我需要对报告 utf-8 字节偏移量而不是 utf-8 字符偏移量的旧版工具的输出进行后处理。 例如,对于七字节 utf-8 字符串 'aβgδe' 中的 5 个字符,它将报告 [0, 1, 3, 4, 6] 而不是 [0, 1, 2, 3, 4],因为希腊字母 'β' 和 'δ' 被编码为两字节-序列。 (实际文本还可能包含 3 字节和 4 字节 utf-8 序列。)

是否有任何内置 Python 函数可用于将 utf-8 字节偏移量转换为 utf-8 字符偏移量?

【问题讨论】:

我不确定我是否理解正确。为什么不使用 b 字符串。所以你有字节偏移量。然后对于python,您只需使用一个字符串(因此您有“字符偏移量,但字符串不是UTF-8)。最终您在需要时解码/编码(以获得正确的索引[如果额外的CPU不是问题])。否则你可以建立一个偏移表,但一次只编码一个字符(并检查 len):一个简单的列表压缩。(如果字符串不是很大 [作为一本书/大文件] 很好) 当我遇到类似问题时,我没有找到将字符串编码为 UTF-8 然后创建字节到字符偏移表的方法;实施here。 @lenz 显然,没有办法创建一个字节到字符偏移表。您能否将您的代码添加为答案,以便我投票。 【参考方案1】:

我认为没有内置或 std-lib 实用程序可以解决此问题,但您可以编写自己的小函数来创建字节偏移量到代码点偏移量的映射。

天真的方法

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 编码规范,在 Internet 上查找它们,或者在 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 开始的连续整数,因此我们可以使用列表而不是 dict 进行映射。

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]

【讨论】:

以上是关于如何将 utf-8 字节偏移量转换为 utf-8 字符偏移量的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 中将 UTF-8 转换为 ISO-8859-1 - 如何将其保持为单字节

如何将4字节utf-8的emoji表情转换为unicode字符编码

如何在Dart中将UTF-8字符串转换为字节数组?

Ruby 1.9:将字节数组转换为具有多字节 UTF-8 字符的字符串

是否可以将字节解码为 UTF-8,将错误转换为 Rust 中的转义序列?

UTF-8 字节 [] 到字符串