Python Cookbook(第3版)中文版:15.16 不确定编码格式的C字符串

Stella981
• 阅读 457

15.16 不确定编码格式的C字符串

问题

你要在C和Python直接来回转换字符串,但是C中的编码格式并不确定。
例如,可能C中的数据期望是UTF-8,但是并没有强制它必须是。
你想编写代码来以一种优雅的方式处理这些不合格数据,这样就不会让Python奔溃或者破坏进程中的字符串数据。

解决方案

下面是一些C的数据和一个函数来演示这个问题:

/* Some dubious string data (malformed UTF-8) */ const char *sdata = "Spicy Jalape\xc3\xb1o\xae"; int slen = 16; /* Output character data */ void print_chars(char *s, int len) { int n = 0; while (n < len) { printf("%2x ", (unsigned char) s[n]); n++; } printf("\n"); } 

在这个代码中,字符串 sdata 包含了UTF-8和不合格数据。
不过,如果用户在C中调用 print_chars(sdata, slen) ,它缺能正常工作。
现在假设你想将 sdata 的内容转换为一个Python字符串。
进一步假设你在后面还想通过一个扩展将那个字符串传个 print_chars() 函数。
下面是一种用来保护原始数据的方法,就算它编码有问题。

/* Return the C string back to Python */
static PyObject *py_retstr(PyObject *self, PyObject *args) {
  if (!PyArg_ParseTuple(args, "")) {
    return NULL;
  }
  return PyUnicode_Decode(sdata, slen, "utf-8", "surrogateescape");
}

/* Wrapper for the print_chars() function */
static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  PyObject *obj, *bytes;
  char *s = 0;
  Py_ssize_t   len;

  if (!PyArg_ParseTuple(args, "U", &obj)) {
    return NULL;
  }

  if ((bytes = PyUnicode_AsEncodedString(obj,"utf-8","surrogateescape"))
        == NULL) {
    return NULL;
  }
  PyBytes_AsStringAndSize(bytes, &s, &len);
  print_chars(s, len);
  Py_DECREF(bytes);
  Py_RETURN_NONE;
}

如果你在Python中尝试这些函数,下面是运行效果:

>>> s = retstr() >>> s 'Spicy Jalapeño\udcae' >>> print_chars(s) 53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f ae >>> 

仔细观察结果你会发现,不合格字符串被编码到一个Python字符串中,并且并没有产生错误,
并且当它被回传给C的时候,被转换为和之前原始C字符串一样的字节。

讨论

本节展示了在扩展模块中处理字符串时会配到的一个棘手又很恼火的问题。
也就是说,在扩展中的C字符串可能不会严格遵循Python所期望的Unicode编码/解码规则。
因此,很可能一些不合格C数据传递到Python中去。
一个很好的例子就是涉及到底层系统调用比如文件名这样的字符串。
例如,如果一个系统调用返回给解释器一个损坏的字符串,不能被正确解码的时候会怎样呢?

一般来讲,可以通过制定一些错误策略比如严格、忽略、替代或其他类似的来处理Unicode错误。
不过,这些策略的一个缺点是它们永久性破坏了原始字符串的内容。
例如,如果例子中的不合格数据使用这些策略之一解码,你会得到下面这样的结果:

>>> raw = b'Spicy Jalape\xc3\xb1o\xae' >>> raw.decode('utf-8','ignore') 'Spicy Jalapeño' >>> raw.decode('utf-8','replace') 'Spicy Jalapeño?' >>> 

surrogateescape 错误处理策略会将所有不可解码字节转化为一个代理对的低位字节(udcXX中XX是原始字节值)。
例如:

>>> raw.decode('utf-8','surrogateescape') 'Spicy Jalapeño\udcae' >>> 

单独的低位代理字符比如 \udcae 在Unicode中是非法的。
因此,这个字符串就是一个非法表示。
实际上,如果你将它传个一个执行输出的函数,你会得到一个错误:

>>> s = raw.decode('utf-8', 'surrogateescape') >>> print(s) Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'utf-8' codec can't encode character '\udcae' in position 14: surrogates not allowed >>> 

然而,允许代理转换的关键点在于从C传给Python又回传给C的不合格字符串不会有任何数据丢失。
当这个字符串再次使用 surrogateescape 编码时,代理字符会转换回原始字节。例如:

>>> s
'Spicy Jalapeño\udcae'
>>> s.encode('utf-8','surrogateescape') b'Spicy Jalape\xc3\xb1o\xae' >>> 

作为一般准则,最好避免代理编码——如果你正确的使用了编码,那么你的代码就值得信赖。
不过,有时候确实会出现你并不能控制数据编码并且你又不能忽略或替换坏数据,因为其他函数可能会用到它。
那么就可以使用本节的技术了。

最后一点要注意的是,Python中许多面向系统的函数,特别是和文件名、环境变量和命令行参数相关的
都会使用代理编码。例如,如果你使用像 os.listdir() 这样的函数,
传入一个包含了不可解码文件名的目录的话,它会返回一个代理转换后的字符串。
参考5.15的相关章节。

PEP 383
中有更多关于本机提到的以及和surrogateescape错误处理相关的信息。

艾伯特(http://www.aibbt.com/)国内第一家[人工智能](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fwww.aibbt.com%2Fa%2Ftag%2F%25e4%25ba%25ba%25e5%25b7%25a5%25e6%2599%25ba%25e8%2583%25bd%2F)门户

收 藏

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
8个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这