Monthly Archives: 十一月 2013

硬编码的沦陷

这篇文章写的

你有没有遇到过硬编码的困惑? 那种在你能够接触到的模块/代码中,放眼望去全是magic number/string。或许这是一个数据库表的名字,或者这是一个数据的状态值。在你试图想修改一个数据库名,或者更为普遍的,你发现两个状态值已不足以表达数据的状态了,你说了一句WTF.

我有。

近几个月来,我接触了几个项目,或大或小。我们的业务主要在于聚合视频资源。我们采用MongoDB做前端数据库,并使用JS/Python这些动态语言开发。NoSQL的好处在于你可以随时修改一个数据的结构(也就是任意添加一个字段并赋予其新的含义)。在编码初期,我们正急于搭建原型,MongoDB帮了我们大忙。

提需求了,需要对某个由于解码问题不能播放的视频禁用,那就添加一个status字段吧,就给它赋0/1吧,0表示启用,1表示禁用。

又提需求了,需要添加视频清晰度标识,那就添加一个definition字段吧,就给它赋1~5吧,分别是标清/高清/超清/蓝光/1080P。

今天的需求,最好下午就能看到效果并转测试,明天上线!

就是这样,动态语言以及NoSQL带给了我们自由扩展的能力。我们及时地满足了许多需求,直到有一天。

提需求了,在做视频解析扫描测试时,最好能判断视频的status,以便在检测到该视频能播放后,能够及时启用该视频。不是很好吗?让程序去完成这一自动化的过程。不,很不好。你忘了吗?我们只对视频进行启用/禁用操作。最初我们根据视频的播放与否来启用/禁用视频,但需求越长越大,在几个月以来,我们常常因为该视频的版权问题而禁用一个视频,并且我们同样的,也把它的状态置为了0.这有什么办法?这个问题在当时很急迫并且我们已经有了一套现成可用的启用/禁用接口。前人挖坑后人填,现在我们怎样才能判断出,一个视频状态为禁用的原因是因为它不能解码或是版权问题呢?我们不仅需要知道状态,还需要知道原因
为什么不再添加几个字段,decodeAvaliable/copyrightDeny.等等,天呐,你还想把数据库搞得更乱吗?这种方案的结果是可预见的。

对视频状态的存取只是其中一个问题,我还遇到许多其他问题。判断一个新爬取的视频与数据库中的视频的相似度,与某一条相似/与所有都不相似/不确定,相似则合并,不相似则插入,不确定则归到待处理队列;判断一个数据的处理状态,待处理/已处理(已合并/未合并/暂不处理等)。

我被这些逻辑判断搞晕了,如果一个判断涉及多种逻辑,我该如何是好?继续在代码中硬编码吗?如果有一天需求有发生变化了呢?我要如何维护这份代码?

于是我试图归纳出共性:它们都是某个数据的某个状态。每条数据有各种预定义的状态。

哈哈!我可以定义出一个模块,用来表达/操作一条数据的状态值

视频状态很复杂?那我们就从定义vidoeStatus模块开始吧。

首先定义已知的视频状态:

Stauts: {

ALLOW_PARSE_OK : 40,                    # 解析可用

ALLOW_COPYRIGHT_OK : 30,                # 版权可用

ALLOW_MANUALLY : 20,                    # 手工启用

ALLOW : 10,                             # 节目为启用状态

ALLOW_ALIAS : 1,                        # 启用状态,别名

DENY_ALIAS : 0,                         # 禁用状态,别名

DENY : -10,                             # 节目为禁用状态

DENY_MANUALLY : -20,                   # 手工禁用

DENY_PARSE_FAIL : -30,                  # 该视频不能解析

DENY_COPYRIGHT_FAIL : -40,              # 该视频涉及版权问题

}

再定义查询接口

  • isAllowBy…()
  • isAllow()
  • isDeny()
  • isDenyBy…()
  • queryAllow()                  # 查询所有启用状态的视频
  • queryDeny()                                      # 查询所有禁用状态的视频

再定义操作接口

  • setAllowBy…()
  • setAllow()
  • setDeny()
  • setDenyBy…()

值得注意的是:

  1. 我们需要兼容以前的状态值0/1,所以我定义了ALLOW/ALLOW_ALIAS, DENY/DENY_ALIAS来进行过渡。
  2. 我用了值的大小来表明状态的优先级,越靠近1/0,优先级越高。被手工禁用的节目,即使是检测到可解析,依然不能置为启用状态,必须手动启动。在set*()系列中必须对状态的优先级进行检查。
  3. 为了能够充分扩展,将状态值设计成不连续的值。取个例子,鉴于我们目前所知的,视频清晰度的分级:标清/高清/超清/蓝光/1080P,是基本满足所有需求的。将对应值设计成1~5固然没错,但如果对应值10/20/30/40/50岂不更加优雅。我将状态值设计成-50/-40/-30/-20/-10/0/1/10/20/30/40/50。你能否体会这种间断值的美妙,如果你用各种理由来阐明你只能用1~5,那就想想HttpStatusCode吧,我强烈建议间断值的设计。
  4. 将接口设计的“自说明性”,普通的程序员只需要写出机器能运行的代码,但优秀的程序员还需要让所有人都能读懂的代码。
  5. 如果你使用的语言支持enum,那再好不过了;但如果没有,不用灰心,情况不会太糟糕。在支持dict/map的语言中,使用key-value结构足够优雅;最简单粗暴的方案,就是直接定义一系列常量。就像C程序的习惯,定义一系列返回值在头文件中。

再举个例子,我是如何应用这些状态处理模块的吧。

对于每天待进入数据库的数据,我们有时需要人工干预,因此有未处理/已处理(已合并/未合并/暂不处理等)。

首先,定义状态:

  • UNHANDLED = 100
  • HANDLED = 200
  • ISNEW = 210
  • ISSIMTO = 220
  • ISUNKOWN = 230

再定义查询接口:

  • isUnhandled()
  • isHandled()
  • isNew()
  • isSimTo()
  • isUnknown()

再定义设置接口:

  • setUnhandled()
  • setHandled()

    • setNew()
    • setSimTo()
    • setUnknown()

最后,在你的调用模块中完美的组合它们吧.

if unhandled:
     do_unhandled()
else:
     if isnew:
         do_isnew()
     elif issimto:
        do_issimto()
     elif isunkwn:
        do_isunkown()
     else:
        do_exception()

或许很多人会辩解,你如何知道数据会有些什么状态,需求是不断变化的。没错,程序员都没有预见未来的能力,但我们有许多良好的设计可以借鉴。在可预见的范围内,满足尽可能的灵活性,没有借口。

讲到这里,我们已经可以总结出核心思想了:提取数据的共同特性,并对频繁操作或者逻辑复杂的状态进行封装。不必过于极端的将所有对状态的操作都封装成模块。

将这个想法写出来并非为了嘲笑或者是自夸这个想法有多好,但我认为这个方法在目前能够比较良好的解决我目前遇到的硬编码问题,并且提供了很好的模块扩展。如果你曾经碰到过硬编码,并且深陷其中,相信你能够体会。

python中的字符编码

这篇文章写的

这段时间在接触Hadoop的stream MapReduce,结果用python的时候出现了各种奇奇怪怪的编码问题,比如使用pymongo操作数据库数据时,将值为中文的title字段调试打印时,在本地测试时正常,但当用crontab运行,并重定向stderr/stdout时,却频繁抛出编码错误。

 

阅读这篇文章建议先掌握unicode与字符编码
(如无特别说明,均指Python 2.7.3)

(Python)[cipher@Rose ~]$ python --version
Python 2.7.3

在python中,序列化类型包括几种:str, unicode, list, tuple, bytearray, buffer, xrange等。这里我主要讲讲strunicode.

在python中,字符串(string)包含两种类型:

  • str         使用单引号或双引号包含,如'你好', "world"
  • unicode     在字符串前加一个u"你好", u"world"

并且,str object 和 unicode object 是两种不同的对象。str 对象是由字符组成的序列,而unicode 对象是由Unicode单元组成的序列。
str 对象里的字符是有多种编码方式的(ASCII/GB2312/UTF-8等),要想解读str,必须首先知道str里的字符是用什么编码方式编码的。 而unicode对象呢?每一个unicode 单元是一个16-bit或32-bit的数值。(在python中,16-bit的unicode对应的是ucs2编码的,32-bit的unicode 对应的是ucs4编码的)
除了unicode对象的字符串,其它的字符串我们都称之为str对象。
可以理解为unicode是某个字符的全球唯一的表达,不带有任何编码信息
那么,这两者有什么不同呢?显而易见的区别就是unicode的字符串前面多了一个'u'字符。

>>> s = '你好'
>>> s
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> u = u'你好'
>>> u
u'\u4f60\u597d'
>>> len(s)
6
>>> len(u)
2
>>> isinstance(s, str)
True
>>> isinstance(u, unicode)
True

由此,我们可以断定,

  1. unicode字符串在字符串前面有一个'u'字符,以表明它是unicode的字符串。
  2. len(str)返回的是str的字节数,len(unicode)返回的是unicode的字符

Unicode(basedstring, encoding) String在io中使用,还有呢?。
json.loads(stringReadFromFile) 会自动编码成unicode。
The principle is that when str and unicode instances meet, the result is a unicode instance.
Allow str() to return unicode strings [http://www.python.org/dev/peps/pep-0349/]

下面再来讲讲老生长谈的问题,unicode对象和str对象之间如何进行转换。

依旧是先明确概念,概念总是在闭幕时才被抽象出来,理解后再看又发现概念如此的简洁。(这个过程就像是用推论去证明假设,总是正确的 🙂 )

  • 编码    unicode -> str
  • 解码    str -> unicode

时刻记住,utf-8 为 unicode的一种实现。

[cipher@Eleanor ~]$ python
Python 2.7.3 (default, Aug 9 2012, 17:23:57) [GCC 4.7.1 201
20720 (Red Hat 4.7.1-5)] on linux2 Type "help", "copyright", "credits" or "license" for more information.
>>> from chardet import detect
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
# python cli 默认的编码方式

>>> a = "中国"
>>> a
'\xe4\xb8\xad\xe5\x9b\xbd'
>>> detect(a)
{'confidence': 0.7525, 'encoding': 'utf-8'}
>>> isinstance(a, str)
True 
# a 为utf-8编码的字符串(str类型)

>>> b = unicode(a, "utf-8")
>>> b
u'\u4e2d\u56fd'
# 将a转换成unicode对象,需要知道str对象是如何编码的才能转换成功。
>>> detect(b)
{'confidence': 1.0, 'encoding': 'ascii'}
>>> isinstance(b, unicode)
True
# b为unicode字符串

>>> a.encode("utf-8")
Traceback (most recent call last): File "", line 1, in UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>> b.encode("utf-8")
'\xe4\xb8\xad\xe5\x9b\xbd'
# str对象已经经过编码,不能再进行编码
# unicode对象可以进行编码

>>> a.decode("utf-8")
u'\u4e2d\u56fd'
>>> b.decode("utf-8")
Traceback (most recent call last): File "", line 1, in File "/home/note/Development/Python/lib64/python2.7/encodings/utf_8.py", line 16, in decode return codecs.utf_8_decode(input, errors, True) UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
# 可以将str类型先进行解码,转换成unicode类型
# 而unicode类型不包含任何编码,不能解码

>>> str(a)
'\xe4\xb8\xad\xe5\x9b\xbd'
>>> str(b)
# 在你的python shell 中试试:help(str)
# str()返回对象的str对象表示。如果对象本身就是一个str,返回该对象本身。

>>> unicode(a)
Traceback (most recent call last): File "", line 1, in UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>> unicode(b)
u'\u4e2d\u56fd'

# 在你的python shell 中试试:help(unicode)
# unicode()从指定编码的string类型对象,返回一个unicode对象。
# 关键字参数encoding,若未指定,默认使用当前编码(文件/当前环境指定)。
## 文件:默认为“ascii”编码,当文件中出现非"ascii"编码时,需要手工指定文件的编码方式。
## cli:通过sys.getdefaultencoding()查看。
>>> sys.getdefaultencoding()
'ascii'

>>> a.encode('utf-8')
Traceback (most recent call last): File "", line 1, in UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
# str.encode(“utf-8”) 会先将str以默认的编码方式解码(这里为"ascii")
# ascii并不是unicode的编码形式之一。所以无法进行这种类型的转换。

>>> sys.setdefaultencoding("utf-8")
# 将cli的默认编码设为"utf-8"
>>> sys.getdefaultencoding()
'utf-8'
>>> a.encode("utf-8")
'\xe4\xb8\xad\xe5\x9b\xbd'
# 已将默认编码设为"utf-8",再次走一遍str.encode()的流程,先按默认解码,再按指定编码。
>>> len(a)
6
>>> a.decode("utf-8")
u'\u4e2d\u56fd'
>>> a.decode("utf-8") == b
True
# 留个小疑问,如果对unicode.decode("utf-8")会发生什么事呢?

总结

  1. str()unicode()并不是互相转换对应的函数。
  2. str()/unicode()对于各自的类型,不会抛出异常。
  3. str()调用对象的.__str__()方法。
  4. str.encode()sys.getdefaultencoding()先将str解码 再按指定/默认的编码方式编码 str.decode() 按指定的默认/解码方式解码。
  5. unicode.encode() 按指定的默认/解码方式编码 unicode.decode()sys.getdefaultencoding()先将unicode编码 再按指定/默认的编码方式解码。
  6. 在程序内部,总是使用unicode编码字符串。对所接收的字符串进行解码,对所发送的字符串进行编码。
  7. pymongo查询出的数据默认为unicode编码。

Encoding of Python stdout 中也可以看到,“python 根据 LC_CTYPE 的值决定 stderr/stdout输出的编码方式,但stdout必须为tty。如果输出到终端(terminal),LC_CTYPE(或者 LC_ALL)会决定编码方式。 然而,当输出是以管道(pipe)方式输出到文件或者其它进程时,编码是未定义的。默认是‘ascii’”。
至此,已经可以解决开篇提出的疑问了:为什么直接输出到stderr/stdout没有任何问题,而对stderr/stdout重定向到文件却会抛出异常了。

题外话:
事实上,python2.7.3版本中,已经决定了stdout/stderr的编码方式,可以通过sys模块查询:

>>> sys.stdout.encoding
'UTF-8'
>>> sys.stderr.encoding
'UTF-8'

但当python的输出编码方式不对时,我们却不能指定其编码方式。
>>> sys.stderr.encoding = "utf-8"
Traceback (most recent call last): File "
", line 1, in TypeError: readonly attribute

TODO: python re unicode

Reference
setting-the-correct-encoding-when-piping-stdout-in-python
Encoding_of_Python_stdout
howto/ unicode
diveintopython unicode

unicode与字符编码

这篇文章写的

    我想我已经弄明白了这些问题,但要表述清楚却多花费我好几倍的时间。

    这里的大部分内容都是从其他博客引用而来。针对python,我另外写了一篇 python中的编码

    先定义几个基本概念,概念总是如此的平实而高深:(,在看完整篇博文之后概念才会显得清晰:

  • 字符集:每个符号的二进制代码的集合。

如,汉字“严”的unicode是一个十六进制数0X4E25。
又如,“Hello”用unicode表示为

U+0048 U+0065 U+006C U+006C U+006F

  • 编码/解码:如何从内存中读取二进制代码并转换为字符,以及字符在内存中如何存储。

如,“Hello”

00 48 00 65 00 6C 00 6C 00 6F

48 00 65 00 6C 00 6C 00 6F 00

(这只是特例,许多汉字并不仅仅只有两个字节存储,所以编解码不仅仅是大/小端的问题)

ASCII

        自unix诞生之日,代码世界中只有英文字母,我们就称它为ASCII吧。它使用值为32~127之间的数字代表字符。比如“32”代表空格符,“65”代表“A”等。

         随着Internet的普及,不同国家的人需要在邮件/程序中表达特定的字符。这个时候出现了诸侯间的128~255之间的字符大战。各国对于128~255之间的字符有着不同的想法,导致例如130可能在某些IBM PC(就拿美国做例子吧)上表示é,而在以色列的PC中则表示e。也就是说一个美国人发了一封résumés到以色列会变成resumes。

        除了128~255之间的字符之争,不同国家的语言中包含奇奇怪怪的字符,使用ASCII的8-bit(256)试图容纳全世界所有的字符也是不可能的。

       在亚洲许多国家,即便使用256个字符也不能表达亚洲语言中字符串的万分之一!即便排除某些不能正常/完全访问Internet的国家,256个字符也完全不够用!

Unicode

        这个时候催生了Unicode字符集的发明。

        时刻记住,Unicode与ASCII一样,仅仅是一种特定的字符集。但“Unicode不得不说是项伟大的发明”。

        很多人误认为Unicode只是简单的16位编码的字符,每个字符使用2个字节表示,因此Unicode总共有能表示65536个字符。

         呃,这并不完全正确。

         这也是关于Unicode最神秘的地方。如果你也这样认为,不需要觉得自己过于糟糕。

         严格来说,unicode使用4个字节(32-bit)来编码,总共能代表2^32个字符(我的计算器溢出了:))。

         实际的情况是, Unicode的字符,其二进制编码不一定需要四个字节,像所有的英文字母,只需要一个字节就绰绰有余(这部分集合与ASCII完全相同,这也可以体现unicode的高明之处。Unicode这个超大字符集与ASCII这个古老的字符集的交集在于从0~127这128个字符完全相同。)

         Unicode是一个很大的字符集,可以在unicode网站查到某个特定的字符对应的unicode。

编码/解码 与 UTF-8

        问题又来了,既然我们已经有了一个伟大的集合,可以包容目前存在的所有字符,为什么不直接在计算机间使用unicode进行传输呢?这样所有人收到的字符都表示相同的含义,一就是一,二就是二。
         还得回到那个各国128~255字符的争夺之战中。假设由创造整个宇宙的斯密达国度与创造斯密达国度的天朝组成统一战线,对抗万恶的资本主义。在战争一触即发的情形下,斯密达首领发了封邮件给天朝将军,”129″(特定的字符,在ASCII中表示“开打吗?”)。天朝人民眼看国内生产还没有发展起来,回复了斯密达,说”250“(特定的字符,在天朝中表示“不打”)。斯密达将领一看,”250“在我大斯密达不就是”打“的意思吗?结果就悲了个剧了。
        到这,编码方式已经隐含在其中了。假设天朝将领与斯密达首领都使用中文(同一套标准)那么双方表达就不会出现歧义。
        再对编码进行细说,”129″(开打吗?)经过存储在邮件中并不一定是“129”,而是“0x1234”(经过我本人特定的编码),而“250”存储在邮件中也不一定是”250″,而是“0x0973”(同样,经过我本人特定的编码)。编码过程就是如何将“250”存储到邮件(内icun)中,而解码过程则是如何从邮件(内存)中解释”0x0973″的含义的过程。

        编解码的意义在于,若发送端和接收端认同同一套编码方式,那么:发送端以这套编码方式编码并发送,接收端以这编码方式解码,就能得到同一个字符,使用万能的unicode表一查,就知道这个字符想表达什么意思了。
由此,我们可以断定,“UTF-8”只是Unicode的一种编码方式,只是Unicode的实现的一种子集。

附1:

UTF-8又是如何编码的呢?

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
——————–+———————————————
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
跟据上表,解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字”严”为例,演示如何实现UTF-8编码。
已知”严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此”严”的UTF-8编码需要三个字节,即格式是”1110xxxx 10xxxxxx 10xxxxxx”。然后,从”严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,”严”的UTF-8编码 是”11100100 10111000 10100101″,转换成十六进制就是E4B8A5。

附2:

Big/Little Endian
端模式(Endian)的这个词出自Jonathan Swift书写的《格列佛游记》。这本书根据将鸡蛋敲开的方法不同将所有的人分为两类,从圆头开始将鸡蛋敲开的人被归为Big Endian,从尖头开始将鸡蛋敲开的人被归为Littile Endian。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。在计算机业Big Endian和Little Endian也几乎引起一场战争。
在计算机业界,Endian表示数据在存储器中的存放顺序。在考虑endian之前,首先了解对于一个32位整型0x12345678,12表示MSB(Most Significant Byte),78表示LSB(Least Significant Byte)。端模式之争也就是MSB和LSB的顺序问题。
那么,计算机如何知道某个文件是采用大端还是小端的编码方式呢?
Unicode规范中定义,每一个字符串的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(ZERO WIDTH NO-BREAK SPACE),用”FE FF”表示。这正好是两个字节,而且”FF”比”FE“大1。
对于文本而言,如果一个文本文件的头两个字节是”FE FF”,就表示该文件采用大端方式;如果头两个字节是”FF FE”,就表示该文件采用小端方式。

Reference:
1. joel on unicode
2. ascii/unicode和utf-8
3. ASCII
4. ASCII速查表