正确处理文本,特别是正确处理Unicode。是个老生常谈的问题,有时甚至会难倒经验丰富的开发者。并不是因为这个问题很难,而是因为对软件中的文本,开发者没有正确理解一些关键概念及其表示方法。在StackOverflow上搜索关于UnicodeDecodeError相关的问题,可以看到很多人都有这样的误解。这些错误的概念可以追溯到Unicode出现之前。那时许多现今的开发者还没入职,也包括我自己。如果这些错误的概念没有散布开来,其实不是个问题。现在很多人都有这些错误概念,部分原因是因为有些非常流行的语言传播,甚至固化了这些错误概念,使得纠正起来反而变得很困难。

根据对Unicode的支持情况,编程语言可以划分为4类:

  • 在Unicode出现或流行之前编写的语言。C和C++就属于这一类。这类语言对unicode的支持参差不齐。或没有内置到语言中,或很难正确的使用。因此开发者常常会用错。
  • 对Unicode支持稍好一点。这些语言在Unicode广泛流行后才出现的,但语言中对unicode的操作方式是严重错误的。虽然这些语言诞生较晚,但依然含有第一类语言中的所有缺点。以我的经验,其中代表语言就是PHP。尽管还有其他语言也同样糟糕。
  • 对Unicode支持基本正确,但有少数致命缺点的语言。这一类语言比较“现代”,且能理解Unicode,但依然无法让开发者正确的处理unicode,导致在这些语言中对unicode会出现一些严重不足。让我很沮丧的是,Python 2.x就属于这一类(下文会详细介绍)。
  • 能正确处理Unicode的语言。这些语言完全支持Unicode,可以用Unicode方便快速的完成任务,且不易出错。Java和.NET平台就属于这一类语言。

那么,Unicode到底是什么,我们在Unicode上犯了哪些错误?Joel这篇The absolute minimum every software developer absolutely, positively must know about unicode绝对是每个软件开发者必须阅读的文章。为了为简洁起见,以及照顾那些天生耐心不够的朋友,我会在本文中对其进行总结。

 

字符和字节

基本事实是,若想正确的处理文本,就必须了解字符的抽象概念。不严谨的定义一下,字符表示的是文本中的单个符号。更重要的是,一个字符不是一个字节。我再强调一遍!一个字符不是一个字节!!!而且,一个字符有许多表示方法,不同的表示方法会使用不同的字节数。就像前面我说的那样,字符就是文本中最小的单元。

Unicode以大家都认可的方式定义了一系列的字符。可以将Unicode理解成一个字符数据库,每个字符都与唯一的数字关联,称为code point。这样,英文大写字母A的codepoint是U+0041。而欧元符号的codepoint是U+20A0,其他类似。一个文本字符串就是这样一系列的codepoint,表示字符串中每个字符元素。

当然,你迟早会需要储存和传输这些理论上的Unicode字符串。如果选择一种其他人可以理解的方式以字节方式进行表示,就可以以大家都理解的方式互相发送文本。这里就需要引入字符编码(encoding)。

字符编码是在理想的字符和实际的字节表示方法之间的映射。这种映射无需面面俱到,即在某种编码中也许无法表示一些特定的字符。同时也无须为每个字符使用相同的内存空间,譬如某些字符使用单字节编码,而其他字符需要多个字节。

由于同一个字符的字节表现形式不止一种。这意味着当遇到了一串字节,如果不知道使用的是什么编码,即使知道这些字节表示的是文本,也不知道是什么意思。所能做的就是猜使用的编码。简而言之,字节不是文本。即使忘了文中介绍的所有内容,也要记住这句话。为了读写文本,归根结底就是要知道其中使用的编码方式,不管是从约定、标识信息、或是其他方法得知。

 

Python是如何处理Unicode

从这里开始介绍Python的Unicode支持。在Python的类型层次中,有3种不同的字符串类型:“unicode”,表示Unicode字符串(文本字符串)、“str”,表示字节字符串(二进制数据);“basestring”。表示前两种字符串类型的父类。在我看来,Python在这里犯了一个错误,根据前面的定义,这让Python成为第三类语言,而没有成为第四类。

我用了很长的篇幅苦口婆心的强调字节和字符在本质上是不同的东西,只有通过字符编码才能互相转换。但不幸的是,Python犯了两个互不相关的错误,轻轻松松的就会让你忘掉这些。

第一个错误的严重性值得商榷:即将一串字节视为字符串。是否应该这样做还有争议。Java和,NET认为这样做是不对的,而其他一些语言却持有相反的态度。无论如何,你可能希望对文本进行某些操作,如正则匹配、字符串替代等。将这些操作应用到字节序列上都是没有意义的。而Python将字节序列作为另一种类型的字符串对待,允许在这两者上执行同样的操作。

第二个错误的严重性大一些,Python试图在字节串和字符串之间以不为人所察觉的方式进行转化。在不同的转换中,在条件允许的情况下,Python会试图在字节串和unicode字符串直接进行转换。例如将字节串和unicode字节串连接到一起时。根据前面的介绍,不使用encoding就在不同类型之间进行转换是没有意义的。所以Python依赖一个“默认编码”,该编码由sys.setdefaultencoding()指定。在大多数平台上,默认的是ASCII编码。但对于所有转换,使用这种编码几乎都是错误的。如果不手动指定编码就调用str()unicode(),或是函数以字符串作为参数,但传递的是其他类型的参数时,都会使用这个默认编码。

走出这个unicode困境的一个解决办法是,调用sys.setdefaultencoding()将默认的编码设置为真正会用到的编码。但这样仅仅是将问题隐藏起来,虽然这样刚开始能解决一些文本处理问题。但缺乏实际可行性,因为许多应用,特别是网络应用,在不同的地方会使用不同的文本编码。

正确的解决方法是修改代码,以正确的方式处理文本。下面是一些应该做到的指导性意见:

  • 所有文本字符串都应该是unicode类型,而不是str类型。如果处理的是文本,而变量类型是str,这就是bug了!
  • 若要将字节串解码成字符串,需要使用正确的解码,即var.decode(encoding)(如,var.decode('utf-8'))。将文本字符串编码成字节,使用var.encode(encoding)。
  • 永远不要对unicode字符串使用str(),也不要在不指定编码的情况下就对字节串使用unicode()
  • 当应用从外部读取数据时,应将其视为字节串,即str类型的,接着调用.decode()将其解释成文本。同样,在将文本发送到外部时,总是对文本调用.encode()
  • 如果代码中使用字符串字面值来表示文本,总是应该含有’u’前缀。但实际上,永远不要在代码中定义原始的字符串字面值。不管怎样,我自己是很讨厌这一条,也许其他人也和我一样吧。

顺便说一句,Python 3修复了这些问题,可以正确的处理unicode和字符串,这样Python就完全位于第四类中了,更多信息参见官方的更新说明中关于Unicode的部分。

希望这些内容能帮到你,如果对unicode到底是什么,如何处理unicode有疑惑的话,现在应该都清楚了。下次遇到UnicodeEncodeErrorUnicodeDecodeError错误时,就应该完全知道问题出在哪,也知道如何去修复这些问题!

 

应用实例

在做 米扑域名 的项目过程中,whois 查询出来的内容出各种妖蛾子,遇到问题就想办法解决

Python 中文包含判断及unicode

# 检测是否为字母数字混合, 默认(0),字母(1),数字(2), 数字字母(3),中文(4),异常(9)
@classmethod
def check_domain_name_type(cls, domain_name_prefix):
    domain_name_type = 0
    try:
        # 精确判断
        if type(domain_name_prefix).__name__ == "unicode":
            print domain_name_prefix, "\t\t   unicode"
            domain_name_prefix.encode('utf-8').decode('unicode_escape')     #  u'abc中国123'
        else:
            print domain_name_prefix, "\t\t   no unicode"
            domain_name_prefix = unicode(domain_name_prefix, 'utf-8')       #  'abc中国123'
             
        # 简洁判断
        if type(domain_name_prefix).__name__ != "unicode":
            domain_name_prefix = unicode(domain_name_prefix, 'utf-8')       #  'abc中国123'
             
             
        zhPattern = re.compile(u'[\u4e00-\u9fa5]+')
        zhMatch = zhPattern.search(domain_name_prefix)
          
        if re.match('^[a-z]+$', domain_name_prefix):            # 1 - letter
            domain_name_type = 1
        elif re.match('^[0-9]+$', domain_name_prefix):          # 2 - num
            domain_name_type = 2
        elif re.match('^[0-9a-z]+$', domain_name_prefix):       # 3 - letter-num
            domain_name_type = 3
        elif zhMatch:                                           # 4 - 中文
            domain_name_type = 4
        elif "-" in domain_name_prefix:                         # 7 - 横杠(-)
            domain_name_type = 7
    except:
        domain_name_type = 9
    return domain_name_type

 

解决 UnicodeDecodeError

UnicodeDecodeError: 'utf8' codec can't decode byte 0xa3 in position 4077

解决思路: 

按行检测,看一行字符是否可解析utf8编码,如果此行不支持utf8编码(乱码),则剔除本行

## 检测行字符是否可解析utf8编码,如果此行不支持utf8编码(乱码),则剔除本行, 示例:  whois dqu.com
## 解决 UnicodeDecodeError: 'utf8' codec can't decode byte 0xa3 in position 4077
@classmethod
def filter_out(cls, out="", domain_name=''):
    if not out or out == "":
        return ''
    
    out2 = ''
    lines = out.split('\n')
    for line in lines:
        try:
            line.encode('utf-8')
            out2 = out2 + line + '\n'
        except:
            p_info = "UnicodeDecodeError continue   ++++  "  + domain_name + "  ++++  " + line
            YGCommon.logger.warn(p_info)
            continue
    return out2

 

原文伯乐在线