我目前所在的项目是一个老项目,里面的字符串编码有点乱,数据库中有些是GB2312,有些是UTF8;代码中有些是GBK,有些是UTF8,代码中转来转去,经常是不太清楚当前这个字符串是什么编码,由于是老项目,也没去修改。最近合服脚本由项目上进行维护了,我拿到脚本看了看是Python写的,我之前也没学习过Python,只有现学现用。

数据库中使用了Protobuf,这里面也有字符串,编码也是有GBK,也有UTF8编码的,而且是交叉使用,有过合服经验的同学应该知道,这里会涉及一些修改,比如名字冲突需要改名。Protobuf中的名字修改就需要先解析出来修改了再序列化回去。这个时候问题来了,Protobuf默认是使用的UTF8编码进行解析(Decode)与序列化的(Encode),可以参见:google.protobuf.internal中的decoder.py中的函数:

def StringDecoder(field_number, is_repeated, is_packed, key, new_default):
"""Returns a decoder for a string field.""" local_DecodeVarint = _DecodeVarint
local_unicode = unicode assert not is_packed
if is_repeated:
tag_bytes = encoder.TagBytes(field_number,
wire_format.WIRETYPE_LENGTH_DELIMITED)
tag_len = len(tag_bytes)
def DecodeRepeatedField(buffer, pos, end, message, field_dict):
value = field_dict.get(key)
if value is None:
value = field_dict.setdefault(key, new_default(message))
while 1:
(size, pos) = local_DecodeVarint(buffer, pos)
new_pos = pos + size
if new_pos > end:
raise _DecodeError('Truncated string.')
value.append(local_unicode(buffer[pos:new_pos], 'utf-8'))
# Predict that the next tag is another copy of the same repeated field.
pos = new_pos + tag_len
if buffer[new_pos:pos] != tag_bytes or new_pos == end:
# Prediction failed. Return.
return new_pos
return DecodeRepeatedField
else:
def DecodeField(buffer, pos, end, message, field_dict):
(size, pos) = local_DecodeVarint(buffer, pos)
new_pos = pos + size
if new_pos > end:
raise _DecodeError('Truncated string.')
field_dict[key] = local_unicode(buffer[pos:new_pos], 'utf-8')
return new_pos
return DecodeField

以及encoder.py中的函数

def StringEncoder(field_number, is_repeated, is_packed):
"""Returns an encoder for a string field.""" tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)
local_EncodeVarint = _EncodeVarint
local_len = len
assert not is_packed
if is_repeated:
def EncodeRepeatedField(write, value):
for element in value:
encoded = element.encode('utf-8')
write(tag)
local_EncodeVarint(write, local_len(encoded))
write(encoded)
return EncodeRepeatedField
else:
def EncodeField(write, value):
encoded = value.encode('utf-8')
write(tag)
local_EncodeVarint(write, local_len(encoded))
return write(encoded)
return EncodeField

如果Protobuf中的字符串编码为非UTF8编码,则在解析(Decode)的过程中会出现异常(有点奇怪的是我同事的电脑上没出现异常):

'utf8' codec can't decode byte……

我们有没有一个方法在不改变Protobuf原来的代码的情况下使用自己的函数来进行解析呢,这是我首先想到的,由于没学习过Python,恶补了一下Python基础后,研究发现Protobuf是把Decode的函数入口放在了一个数组中,在引入模块的时候就会自动初始化这些入口函数,然后保存到各个Protobuf类中,各个PB类都有一个decoders_by_tag字典,这个字典就存放了各种数据类型的解析函数入口地址。

通过上面的代码可以看出,具体解析函数(DecodeField)是放在一个闭包中的,不能直接修改,所以必须整个(StringDecoder)替换。通过深入研究,终于发现了其设置的入口,在google.protobuf.internal的type_checkers.py中有这样一段代码:

# Maps from field types to encoder constructors.
TYPE_TO_ENCODER = {
_FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder,
_FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder,
_FieldDescriptor.TYPE_INT64: encoder.Int64Encoder,
_FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder,
_FieldDescriptor.TYPE_INT32: encoder.Int32Encoder,
_FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder,
_FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder,
_FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder,
_FieldDescriptor.TYPE_STRING: encoder.StringEncoder,
_FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder,
_FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder,
_FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder,
_FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder,
_FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder,
_FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder,
_FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder,
_FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder,
_FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder,
} # Maps from field types to sizer constructors.
TYPE_TO_SIZER = {
_FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer,
_FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer,
_FieldDescriptor.TYPE_INT64: encoder.Int64Sizer,
_FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer,
_FieldDescriptor.TYPE_INT32: encoder.Int32Sizer,
_FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer,
_FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer,
_FieldDescriptor.TYPE_BOOL: encoder.BoolSizer,
_FieldDescriptor.TYPE_STRING: encoder.StringSizer,
_FieldDescriptor.TYPE_GROUP: encoder.GroupSizer,
_FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer,
_FieldDescriptor.TYPE_BYTES: encoder.BytesSizer,
_FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer,
_FieldDescriptor.TYPE_ENUM: encoder.EnumSizer,
_FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer,
_FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer,
_FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer,
_FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer,
} # Maps from field type to a decoder constructor.
TYPE_TO_DECODER = {
_FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder,
_FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder,
_FieldDescriptor.TYPE_INT64: decoder.Int64Decoder,
_FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder,
_FieldDescriptor.TYPE_INT32: decoder.Int32Decoder,
_FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder,
_FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder,
_FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder,
_FieldDescriptor.TYPE_STRING: decoder.StringDecoder,
_FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder,
_FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder,
_FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder,
_FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder,
_FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder,
_FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder,
_FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder,
_FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder,
_FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder,
}

第一个是序列化(Encoder)的函数入口,第二个是计算大小的函数入口,第三个就是解析(Decoder)的入口,我们可以看到这里映射了所有类型的处理函数入口,那我们把这个入口函数替换成我们自己的函数,就可以根据实际需要进行处理了。

这里我们需要特别注意的是Protobuf中的各个类都是在模块导入的时候就初始化好了,所以,如果我们要修改入口函数,必须在PB各类引入之前进行修改。为此我写了一个模块文件:protobuf_hack.py,这个模块必须先于PB类import,其内容如下:

from google.protobuf.internal import decoder
from google.protobuf.internal import encoder
from google.protobuf.internal import wire_format
from google.protobuf.internal import type_checkers
from google.protobuf import reflection
from google.protobuf import message def StringDecoder(field_number, is_repeated, is_packed, key, new_default):
"""Returns a decoder for a string field.""" local_DecodeVarint = _DecodeVarint
local_unicode = unicode assert not is_packed
if is_repeated:
tag_bytes = encoder.TagBytes(field_number,
wire_format.WIRETYPE_LENGTH_DELIMITED)
tag_len = len(tag_bytes)
def DecodeRepeatedField(buffer, pos, end, message, field_dict):
value = field_dict.get(key)
if value is None:
value = field_dict.setdefault(key, new_default(message))
while 1:
(size, pos) = local_DecodeVarint(buffer, pos)
new_pos = pos + size
if new_pos > end:
raise _DecodeError('Truncated string.')
value.append(local_unicode(buffer[pos:new_pos], 'gbk'))
# Predict that the next tag is another copy of the same repeated field.
pos = new_pos + tag_len
if buffer[new_pos:pos] != tag_bytes or new_pos == end:
# Prediction failed. Return.
return new_pos
return DecodeRepeatedField
else:
def DecodeField(buffer, pos, end, message, field_dict):
(size, pos) = local_DecodeVarint(buffer, pos)
new_pos = pos + size
if new_pos > end:
raise _DecodeError('Truncated string.')
field_dict[key] = local_unicode(buffer[pos:new_pos], 'gbk')
return new_pos
return DecodeField type_checkers.TYPE_TO_DECODER[type_checkers._FieldDescriptor.TYPE_STRING] = StringDecoder

这样,我们可以把所有PB中的字符串解析按GBK编码解析了。但是项目中的字符串并不是所有的字符串都是GBK编码的,也有UTF8编码的,为了支持两种编码,我做了一个处理,就是先尝试使用一种编码解析,如果出现异常,再使用另一种编码进行解析,这样就保证了我们所有的字符串都可以正确解析。理想很丰满,现实很骨感,解析是正确了,但是如果我们序列化回去在服务器程序中去使用的时候就会出现乱码,因为原来的GBK或者UTF8统一成UTF8编码了,当然,我们也可以继续像Decoder调用自己的函数一样处理Encoder,但是在Encoder中我们并不知道这个字符串原来在数据库中是什么编码,也没有PB以及字段信息,无法差别处理。

至此,算是白忙活了,无法满足需要。

如果我们能够只修改我们指定的PB类的处理函数就好了,因为我们可以找出哪些PB的字符串是GBK编码的。再次经过深入研究,总算是做到了。

在这里有一个函数帮了我大忙,reflection.py中的ParseMessage函数,我们看一下:

def ParseMessage(descriptor, byte_str):
"""Generate a new Message instance from this Descriptor and a byte string. Args:
descriptor: Protobuf Descriptor object
byte_str: Serialized protocol buffer byte string Returns:
Newly created protobuf Message object.
""" class _ResultClass(message.Message):
__metaclass__ = GeneratedProtocolMessageType
DESCRIPTOR = descriptor new_msg = _ResultClass()
new_msg.ParseFromString(byte_str)
return new_msg

这个函数其实就是通过描述符信息(descriptor)来解析二进制串,生成一个新的PB消息实例。这中间的关键就是函数中的那个动态生成类实例的代码,在这里会走一次PB类的初始化流程,即会初始化我们所需要的Decoder以及Encoder函数映射字典。为了工作需要,我修改一下这个函数:

def ParseMessage(descriptor):
class _ResultClass(message.Message):
__metaclass__ = reflection.GeneratedProtocolMessageType
DESCRIPTOR = descriptor new_msg = _ResultClass()
return new_msg

然后加入我们需要使用自定义函数处理的PB类,注意这里一定是所需要的最小的PB结构。

def hacker(msg):
ParseMessage(msg.DESCRIPTOR) def hack_pb():
#修改默认的字符串处理函数入口为自定义函数
type_checkers.TYPE_TO_DECODER[type_checkers._FieldDescriptor.TYPE_STRING] = StringDecoder
type_checkers.TYPE_TO_ENCODER[type_checkers._FieldDescriptor.TYPE_STRING] = StringEncoder
type_checkers.TYPE_TO_SIZER[type_checkers._FieldDescriptor.TYPE_STRING] = StringSizer try:
# 这里加入我们需要修改的PB类
hacker(DbProto.DB_FriendAssetEntry_PB)
except Exception as e:
print(e) #还原字符串处理函数入口
type_checkers.TYPE_TO_DECODER[type_checkers._FieldDescriptor.TYPE_STRING] = decoder.StringDecoder
type_checkers.TYPE_TO_ENCODER[type_checkers._FieldDescriptor.TYPE_STRING] = encoder.StringEncoder
type_checkers.TYPE_TO_SIZER[type_checkers._FieldDescriptor.TYPE_STRING] = encoder.StringSizer

由于Encode的时候Protobuf是先计算字段的长度,然后再处理的各字段,所以我们还需要把计算大小的函数使用自定义函数,否则再次解析会出问题。

现在基本上满足了需要,算是大功告成了!

细心的读者,不知你发现没,这里还是有一个问题,目前无法解决的问题,就是如果我们一个最小的PB中如果有两个字符串字段,采用的不同的编码怎么办?一般情况下,正常的设计者不会这样做,但是就像我们项目中的编码混乱一样,如果一个不小心就搞成不一样的编码就悲剧了!如果哪位高手有此解决方案,欢迎分享!!!

把整个文件附上:

from google.protobuf.internal import decoder
from google.protobuf.internal import encoder
from google.protobuf.internal import wire_format
from google.protobuf.internal import type_checkers
from google.protobuf import reflection
from google.protobuf import message def StringDecoder(field_number, is_repeated, is_packed, key, new_default):
"""Returns a decoder for a string field.""" local_DecodeVarint = decoder._DecodeVarint
local_unicode = unicode assert not is_packed
if is_repeated:
tag_bytes = encoder.TagBytes(field_number,
wire_format.WIRETYPE_LENGTH_DELIMITED)
tag_len = len(tag_bytes) def DecodeRepeatedField(buffer, pos, end, message, field_dict):
value = field_dict.get(key)
if value is None:
value = field_dict.setdefault(key, new_default(message))
while 1:
(size, pos) = local_DecodeVarint(buffer, pos)
new_pos = pos + size
if new_pos > end:
raise decoder._DecodeError('Truncated string.')
str = '' #这里先尝试使用UTF8编码进行解析,如果出现异常则尝试使用GBK编码解析
try:
str = local_unicode(buffer[pos:new_pos], 'utf-8')
except Exception as e:
try:
str = local_unicode(buffer[pos:new_pos], 'gbk')
except Exception as e1:
str = '' value.append(str)
# Predict that the next tag is another copy of the same repeated field.
pos = new_pos + tag_len
if buffer[new_pos:pos] != tag_bytes or new_pos == end:
# Prediction failed. Return.
return new_pos return DecodeRepeatedField
else:
def DecodeField(buffer, pos, end, message, field_dict):
(size, pos) = local_DecodeVarint(buffer, pos)
new_pos = pos + size
if new_pos > end:
raise decoder._DecodeError('Truncated string.') str = '' #这里先尝试使用UTF8编码进行解析,如果出现异常则尝试使用GBK编码解析
try:
str = local_unicode(buffer[pos:new_pos], 'utf-8')
except Exception as e:
try:
str = local_unicode(buffer[pos:new_pos], 'gbk')
except Exception as e1:
str = '' field_dict[key] = str
return new_pos return DecodeField def StringEncoder(field_number, is_repeated, is_packed):
"""Returns an encoder for a string field.""" tag = encoder.TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)
local_EncodeVarint = encoder._EncodeVarint
local_len = len
assert not is_packed
if is_repeated:
def EncodeRepeatedField(write, value):
for element in value:
encoded = element.encode('gbk') #序列化的时候就直接使用GBK编码了
write(tag)
local_EncodeVarint(write, local_len(encoded))
write(encoded) return EncodeRepeatedField
else:
def EncodeField(write, value):
encoded = value.encode('gbk') #序列化的时候就直接使用GBK编码了
write(tag)
local_EncodeVarint(write, local_len(encoded))
return write(encoded) return EncodeField def StringSizer(field_number, is_repeated, is_packed):
"""Returns a sizer for a string field.""" tag_size = encoder._TagSize(field_number)
local_VarintSize = encoder._VarintSize
local_len = len
assert not is_packed
if is_repeated:
def RepeatedFieldSize(value):
result = tag_size * len(value)
for element in value:
l = local_len(element.encode('gbk')) #注意序列化前计算长度时也需要使用与序列化相同的编码,否则会出错
result += local_VarintSize(l) + l
return result return RepeatedFieldSize
else:
def FieldSize(value):
l = local_len(value.encode('gbk')) #注意序列化前计算长度时也需要使用与序列化相同的编码,否则会出错
return tag_size + local_VarintSize(l) + l return FieldSize def ParseMessage(descriptor):
class _ResultClass(message.Message):
__metaclass__ = reflection.GeneratedProtocolMessageType
DESCRIPTOR = descriptor new_msg = _ResultClass()
return new_msg def hacker(msg):
ParseMessage(msg.DESCRIPTOR) def hack_pb():
# 修改默认的字符串处理函数入口为自定义函数
type_checkers.TYPE_TO_DECODER[type_checkers._FieldDescriptor.TYPE_STRING] = StringDecoder
type_checkers.TYPE_TO_ENCODER[type_checkers._FieldDescriptor.TYPE_STRING] = StringEncoder
type_checkers.TYPE_TO_SIZER[type_checkers._FieldDescriptor.TYPE_STRING] = StringSizer try:
# 这里加入我们需要修改的PB类,注意这里需要自行import DbProto模块
hacker(DbProto.DB_FriendAssetEntry_PB)
except Exception as e:
print(e) # 还原字符串处理函数入口
type_checkers.TYPE_TO_DECODER[type_checkers._FieldDescriptor.TYPE_STRING] = decoder.StringDecoder
type_checkers.TYPE_TO_ENCODER[type_checkers._FieldDescriptor.TYPE_STRING] = encoder.StringEncoder
type_checkers.TYPE_TO_SIZER[type_checkers._FieldDescriptor.TYPE_STRING] = encoder.StringSizer #这里让其在引入模块时自动执行
hack_pb()

使用自己的Python函数处理Protobuf中的字符串编码的更多相关文章

  1. 描述了say_hello函数的具体内容,调用zend_printf系统函数在php中打印字符串

    下载一个php的源代码包,这里使用的是php 4.0.5版,解压后会看到php的根目录下会有README.EXT_SKEL这样一个文件,打开详细阅读了一下,发现了一个非常好用的工具,这个工具可以帮你构 ...

  2. Mapreduce中的字符串编码

    Mapreduce中的字符串编码 $$$ Shuffle的执行过程,需要经过多次比较排序.如果对每一个数据的比较都需要先反序列化,对性能影响极大. RawComparator的作用就不言而喻,能够直接 ...

  3. 关于python中的字符串编码理解

    python2.x 中中间编码为unicode,一个字符串需要decode为unicode,再encode为其它编码格式(gbk.utf8等) 以gbk转utf8为例: s = "我是字符串 ...

  4. python中的字符串编码问题——1.理解编码和解码问题

    理解编码与解码(python2.7):1)编码 是根据一个想要的编码名称,把一个字符串翻译为其原始字节形式.>>> u_str=u'字符串编码aabbbcccddd'>> ...

  5. JavaScript中有对字符串编码的三个函数:escape,encodeURI,encodeURIComponent

    JavaScript中有三个可以对字符串编码的函数,分别是: escape,encodeURI,encodeURIComponent,相应3个解码函数:unescape,decodeURI,decod ...

  6. Python:判断列表中含有字符串且组成新的列表打印输出-Dotest董浩

    '''题一:判断列表中含有字符串且组成新的列表打印输出知识点:列表.列表的增删改查.for循环.if判断'''#@Author:Dotest软件测试#@QQ:1274057839names = ['D ...

  7. Python3中转换字符串编码

    在使用subprocess调用Windows命令时,遇到了字符串不显示中文的问题,源码如下:#-*-coding:utf-8-*-__author__ = '$USER' #-*-coding:utf ...

  8. javascript中的字符串编码、字符串方法详解

    js中的字符串是一种类数组,采用UTF-16编码的Unicode字符集,意味字符串的每个字符可用下标方式获取,而每个字符串在内存中都是一个16位值组成的序列.js对字符串的各项操作均是在对16位值进行 ...

  9. Python2和Python3中的字符串编码问题解决

    Python2和Python3在字符串编码上是有明显的区别. 在Python2中,字符串无法完全地支持国际字符集和Unicode编码.为了解决这种限制,Python2对Unicode数据使用了单独的字 ...

随机推荐

  1. bootstrap学习(五)代码

    内联代码: <code>public static</code>void main 用户输入: to edit setting,press <kbd><kbd ...

  2. React的性能优化 - 代码拆分之lazy的使用方法

    我们在某些网站上肯定看到过这样一种现象,页面上图片只有你滚动到那个位置附近的时候才会加载,否则就只占了个位,这就是延迟加载最普遍的应用场景. 我们react框架进行开发的时候也是一样,没有使用的组件是 ...

  3. Vue之自建管理后台(一)准备工作

    完成最基础的Vue环境及新建一个vue项目. 一般来说,我们拿到一个项目需求或者得到一个需求的时候,第一件应该做的事情不是立马坐在电脑前面去写代码,如果你这么做的,好吧...我只能暂时认定你为一个刚上 ...

  4. JNI中修改(基本类型)参数并返回到Java层使用

    最近在JNI相关项目中遇到一个问题:在Java层传入多个int类型的参数,在jni层修改参数值或地址之后重新返回到Java层.这应该算是基本知识了,尤其是基本类型的参数往往看似简单,所以在之前学习jn ...

  5. 让微信小程序每次请求的时候不改变session_id的方法

    让微信小程序每次请求的时候不改变session_id的方法 每次微信小程序请求的时候都会改变session id, 还好他的请求方法内可以设置header头 所以只需要在启动程序后第一次请求服务器获得 ...

  6. Ubuntu图形界面和终端界面切换快捷键

    Ctrl+Alt+F1可以从图形界面切换到终端界面. Ctrl+Alt+F7可以从终端界面退出来,重新回到图形界面

  7. JMeter目录结构

    转载自https://www.cnblogs.com/imyalost/p/6959797.html 首先得了解一下这些东西,以后才能快速的找到某些配置文件进行修改(举个例子,改配置只是其中之一) 一 ...

  8. apue第4章习题

    4.1 用 stat 函数替换图 4-3 程序中的 lstat函数,如若命令行残数之一是符号链接,会发生什么变化? stat不支持链接,如果有参数是链接符号,会显示链接后的文件属性. 4.2 如果文件 ...

  9. 「ZJOI2019」语言 解题报告

    「ZJOI2019」语言 3个\(\log\)做法比较简单,但是写起来还是有点麻烦的. 大概就是树剖把链划分为\(\log\)段,然后任意两段可以组成一个矩形,就是个矩形面积并,听说卡卡就过去了. 好 ...

  10. oracle主要的动态视图与基表的对应关系

    动态视图 基表 GV$ACCESS x$ksuses,x$kglob,x$kgldp,x$kgllk GV$ACTIVE_INSTANCES x$ksimsi GV$ACTIVE_SESS_POOL_ ...