杰瑞科技汇

Python中Unicode字符串长度如何计算?

  • Python 3 (推荐): len() 函数默认返回的是 Unicode 码位 的数量,对于绝大多数情况,这就是你想要的“字符”数量。
  • Python 2 (已过时): len() 返回的是 字节 数量,这通常不是我们想要的。
  • 复杂字符: 某些 Unicode 字符(如表情符号、某些特殊符号)可能由多个码位组成(家庭” emoji 👨‍👩‍👧‍👦 是由多个码位组合而成的)。len() 会将其视为多个独立的码位。
  • 获取“字形簇”数量: 如果你需要计算用户感知的“字符”数量(即字形簇),需要使用第三方库 unicodedata

Python 3 中的情况 (推荐)

在 Python 3 中,字符串是 Unicode 字符串,这是默认行为。len() 函数返回的是字符串中 Unicode 码位 的数量。

Python中Unicode字符串长度如何计算?-图1
(图片来源网络,侵删)

一个码位通常对应一个字符,但也不总是如此。

示例 1: 基本情况(ASCII 和常见汉字)

# ASCII 字符
s1 = "hello"
print(f"'{s1}' 的长度是: {len(s1)}")  # 输出: 5
# 常见汉字
s2 = "你好,世界"
print(f"'{s2}' 的长度是: {len(s2)}")  # 输出: 6

这里,“你”、“好”、“世”等每个汉字都是一个独立的码位,len() 正确地返回了 6。

示例 2: 复杂字符(代理对和组合字符)

这是 len() 行为的关键点。

情况 A: 代理对

Python中Unicode字符串长度如何计算?-图2
(图片来源网络,侵删)

某些字符(如 Emoji)在 BMP(基本多语言平面)之外,需要用两个 16 位的码位(一个“代理对”)来表示。

# 一个普通的 smiley emoji (在 BMP 内)
s_emoji_simple = "😊"
print(f"'{s_emoji_simple}' 的长度是: {len(s_emoji_simple)}")  # 输出: 1
# 一个需要代理对的 emoji ("PILE OF POO" - 💩)
s_emoji_complex = "💩"
print(f"'{s_emoji_complex}' 的长度是: {len(s_emoji_complex)}")  # 输出: 1

在 Python 3 中,即使内部存储可能需要多个字节,len() 仍然返回 1,因为它被正确地视为一个单一的码位。

情况 B: 组合字符

这是最容易出现“意外”的地方,一个完整的字符可能由一个基础字符和一个或多个组合标记(变音符号、上标等)组成。

Python中Unicode字符串长度如何计算?-图3
(图片来源网络,侵删)
# 'é' 可以由一个码位 U+00E7 直接表示
s_precomposed = "é"
print(f"'{s_precomposed}' (预组合) 的长度是: {len(s_precomposed)}")  # 输出: 1
# 'é' 也可以由 'e' + 组合重音符号 '´' 组成
s_decomposed = "e\u0301" # \u0301 是组合重音符号
print(f"'{s_decomposed}' (分解形式) 的长度是: {len(s_decomposed)}")  # 输出: 2

尽管这两个字符串在视觉上完全一样,但 len() 的结果不同,第一个是 1 个码位,第二个是 2 个码位('e' 和一个组合符号)。


Python 2 中的情况 (已过时)

在 Python 2 中,情况要复杂得多,字符串默认是字节串,而不是 Unicode 字符串。

  • 对于字节串 (str): len() 返回字节数。
  • 对于 Unicode 字符串 (unicode): len() 返回码位数。

强烈建议不要再使用 Python 2 进行新项目开发。

示例:

# -*- coding: utf-8 -*-
# Python 2 默认字节串
s_str = "你好"
print(f"字节串 '{s_str}' 的长度是: {len(s_str)}")  # 输出: 6 (假设是 UTF-8 编码,每个汉字3字节)
# Python 2 Unicode 字符串
s_unicode = u"你好"
print(f"Unicode 字符串 '{s_unicode}' 的长度是: {len(s_unicode)}")  # 输出: 2

如果你在 Python 2 中处理非英文字符,必须时刻记得你的变量是 str 还是 unicode,并正确地进行编码和解码,否则 len() 的结果会非常混乱。


如何获取“字形簇”的数量?

如上所述,len() 无法正确处理分解形式的组合字符,如果你需要计算用户感知的“字符”数量(即字形簇),你需要使用 unicodedata 模块进行 规范化

规范化 是将 Unicode 字符串转换成一个标准形式的过程,对于我们的目的,最常用的是 NFC (Normalization Form C) 和 NFD (Normalization Form D)。

  • NFC (Normalization Form C): 将字符转换为“预组合”形式,将 "e\u0301" 转换为 "é"。
  • NFD (Normalization Form D): 将字符转换为“分解”形式,将 "é" 转换为 "e\u0301"。

策略:将字符串先规范化为 NFC 形式,然后再计算长度,这样可以确保大多数组合字符都被视为一个单一的码位。

示例:

import unicodedata
# 分解形式的 'é'
s_decomposed = "e\u0301"
print(f"分解前 '{s_decomposed}' 的长度是: {len(s_decomposed)}")  # 输出: 2
# 将其规范化为 NFC (预组合) 形式
s_normalized = unicodedata.normalize('NFC', s_decomposed)
print(f"规范化后 '{s_normalized}' 的长度是: {len(s_normalized)}")  # 输出: 1

一个更健壮的函数来获取“用户感知的字符数”:

import unicodedata
def get_grapheme_count(s):
    """
    获取字符串中字形簇(用户感知的字符)的数量。
    这是一个近似方法,对于所有情况(如某些 Emoji 组合)可能不完全准确,
    但对于大多数情况已经足够。
    """
    # 规范化为 NFC 形式,将组合字符合并
    normalized_s = unicodedata.normalize('NFC', s)
    return len(normalized_s)
# 测试
s1 = "hello"
s2 = "你好"
s3 = "e\u0301" # 分解的 é
s4 = "café"    # 预组合的 é
s5 = "👨‍👩‍👧‍👦" # 家庭 emoji (由多个码位组合而成)
print(f"'{s1}': {get_grapheme_count(s1)}") # 输出: 5
print(f"'{s2}': {get_grapheme_count(s2)}") # 输出: 6
print(f"'{s3}': {get_grapheme_count(s3)}") # 输出: 1 (正确)
print(f"'{s4}': {get_grapheme_count(s4)}") # 输出: 4 (正确)
print(f"'{s5}': {get_grapheme_count(s5)}") # 输出: 1 (正确,因为它们被视为一个整体)

注意: 对于像 "👨‍👩‍👧‍👦" 这样的复杂 Emoji 组合,unicodedata.normalize 可能无法将其合并成一个码位,len() 仍然可能返回大于 1 的值,要完美处理这类情况,需要更专业的库,如 grapheme 库 (pip install grapheme)。


总结与实践建议

需求 Python 3 推荐方法 说明
获取码位数量 len(my_string) 最常用,对于绝大多数现代应用,这就是正确的“长度”。
获取字节长度 len(my_string.encode('utf-8')) 当你需要将字符串存入数据库或通过网络传输时,需要知道它占用的字节数。
获取字形簇数量 len(unicodedata.normalize('NFC', my_string)) 当你需要计算用户看到的字符数时(如截断文本显示),对于 Emoji 组合,可能需要 grapheme 库。

最佳实践:

  1. 始终使用 Python 3,它从设计上就更好地支持了 Unicode。
  2. 在处理文本长度时,首先考虑 len(),它能满足 99% 的需求。
  3. 只有在遇到明显的组合字符问题(len("e\u0301") 返回 2 但你期望 1)时,才考虑使用 unicodedata.normalize()
  4. 如果你正在开发一个需要精确显示字符数量的应用(如社交媒体的字数限制),并且需要完美处理所有 Emoji,请考虑使用 grapheme 这样的专业库。
分享:
扫描分享到社交APP
上一篇
下一篇