fiona属性表读取和Python字符编码

fiona是OGR/GDAL的Python封装,可以方便读取空间数据。在使用fiona读取shapefile属性表时,发现输出的中文字符乱码。

1
2
3
4
5
import fiona
places = [pol for pol in fiona.open(shp_path, encoding='gbk')]
for place in places:
print place['properties'][name_field][name_field]
>>> Àö½­ÊÐ

利用一个在线的乱码恢复工具检查发现这是一个以Windows-1252也就是Latin-1或者是ISO 8859-1编码的字符串,但实际上正确的编码应该是GBK。通过调试发现乱码的变量类型是Unicode,值是u'\xc0\xf6\xbd\xad\xca\xd0'。那么问题就变成了如何让Python用GBK而不是8859来解析这一串数据。

由于自己太笨,在反复查找和试图通过把数据转成十六进制再解码未遂之后,发现了fiona的open函数有encoding可选参数来指定属性表的字符编码,如果不指定的话fiona会自动推测编码并且将其统一以Unicode类型表示。而这个看似贴心的功能却导致了一旦fiona的推测有误就会形成错误的Unicode值,不能被我恰当地操作和解释。于是我只能通过对比指定和不指定编码得到的结果来被迫了解其中缘由。

1
2
3
4
5
6
# open without specific encoding
print name, repr(name)
>>> Àö½­ÊÐ u'\xc0\xf6\xbd\xad\xca\xd0'
# open with gbk
print name, repr(name)
>>> 丽江市 u'\u4e3d\u6c5f\u5e02'

这个两个数值也不一样,甚至格式都不一样,一个是\u引导的四位十六进制数,一个是\x引导的两位十六进制数,让我一度非常困惑。\xhh是Python中单字节字符的转义表示[1],\uxxxx\Uxxxxxxxx则是16位和32位字符的转义表示。在Unicode中,带修饰符的拉丁字符的码点在一个字节的范围以内,而CJK字符位于两个字节的区间4E00-9FFF内(在UTF-8编码中由于字节前缀的定义需要以三个字节表示,与这里没有必然联系,只是在查资料过程中了解到的)。试着用encode('latin-1')把乱码转成str型,也就是去掉了开头的u,这时总算可以再把它根据自己的情况进行合适的解码decode('gbk')成为正确的Unicode对象。

1
2
3
4
5
6
7
8
name.encode('latin-1') # to str
>>> '\xc0\xf6\xbd\xad\xca\xd0'
s1 = name.encode('latin-1')
s1.decode('gbk') # to unicode
>>> u'\u4e3d\u6c5f\u5e02'
s2 = s1.decode('gbk')
print s2
>>> 丽江市

可以发现问题就出在fiona把0xC0F6当成两个拉丁字母扩展字符Àö处理并转成Unicode码点,而实际上需要这两个字节应该按GBK理解为一个字符,其正确的Unicode编码应该是\u4e3d。要想正确工作,要么告诉读文件的接口这是什么编码,要么就得把按不当编码读好的unicode倒回str状态再正确解码。整体感觉encodedecode函数名跟我的理解完全是反的,前者实际上把Unicode倒回了字符串,但是编码在我的概念里是把字符串转成某种码。对照下表乱码的编码和正确字符的编码,整个过程就是GBK被误以为是Latin-1处理成了Unicode,实际上需要的是GBK编码字符对应的Unicode对象。

字符串 Unicode码点 GBK
丽江市 4E3D 6C5F 5E02 C0F6 BDAD CAD0
Àö½­ÊÐ C0 F6 BD AD CA D0