15.14 传递Unicode字符串给C函数库

问题

你要写一个扩展模块,需要将一个Python字符串传递给C的某个库函数,但是这个函数不知道该怎么处理Unicode。

解决方案

这里我们需要考虑很多的问题,但是最主要的问题是现存的C函数库并不理解Python的原生Unicode表示。
因此,你的挑战是将Python字符串转换为一个能被C理解的形式。

为了演示的目的,下面有两个C函数,用来操作字符串数据并输出它来调试和测试。
一个使用形式为 char *, int 形式的字节,
而另一个使用形式为 wchar_t *, int 的宽字符形式:

void print_chars(char *s, int len) {
int n = 0; while (n < len) {
printf("%2x ", (unsigned char) s[n]);
n++;
}
printf("\n");
} void print_wchars(wchar_t *s, int len) {
int n = 0;
while (n < len) {
printf("%x ", s[n]);
n++;
}
printf("\n");
}

对于面向字节的函数 print_chars() ,你需要将Python字符串转换为一个合适的编码比如UTF-8.
下面是一个这样的扩展函数例子:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
char *s;
Py_ssize_t len; if (!PyArg_ParseTuple(args, "s#", &s, &len)) {
return NULL;
}
print_chars(s, len);
Py_RETURN_NONE;
}

对于那些需要处理机器本地 wchar_t 类型的库函数,你可以像下面这样编写扩展代码:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
wchar_t *s;
Py_ssize_t len; if (!PyArg_ParseTuple(args, "u#", &s, &len)) {
return NULL;
}
print_wchars(s,len);
Py_RETURN_NONE;
}

下面是一个交互会话来演示这个函数是如何工作的:

>>> s = 'Spicy Jalape\u00f1o'
>>> print_chars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> print_wchars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 f1 6f
>>>

仔细观察这个面向字节的函数 print_chars() 是怎样接受UTF-8编码数据的,
以及 print_wchars() 是怎样接受Unicode编码值的

讨论

在继续本节之前,你应该首先学习你访问的C函数库的特征。
对于很多C函数库,通常传递字节而不是字符串会比较好些。要这样做,请使用如下的转换代码:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
char *s;
Py_ssize_t len; /* accepts bytes, bytearray, or other byte-like object */
if (!PyArg_ParseTuple(args, "y#", &s, &len)) {
return NULL;
}
print_chars(s, len);
Py_RETURN_NONE;
}

如果你仍然还是想要传递字符串,
你需要知道Python 3可使用一个合适的字符串表示,
它并不直接映射到使用标准类型 char *wchar_t * (更多细节参考PEP 393)的C函数库。
因此,要在C中表示这个字符串数据,一些转换还是必须要的。
PyArg_ParseTuple() 中使用”s#” 和”u#”格式化码可以安全的执行这样的转换。

不过这种转换有个缺点就是它可能会导致原始字符串对象的尺寸增大。
一旦转换过后,会有一个转换数据的复制附加到原始字符串对象上面,之后可以被重用。
你可以观察下这种效果:

>>> import sys
>>> s = 'Spicy Jalape\u00f1o'
>>> sys.getsizeof(s)
87
>>> print_chars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> sys.getsizeof(s)
103
>>> print_wchars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 f1 6f
>>> sys.getsizeof(s)
163
>>>

对于少量的字符串对象,可能没什么影响,
但是如果你需要在扩展中处理大量的文本,你可能想避免这个损耗了。
下面是一个修订版本可以避免这种内存损耗:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
PyObject *obj, *bytes;
char *s;
Py_ssize_t len; if (!PyArg_ParseTuple(args, "U", &obj)) {
return NULL;
}
bytes = PyUnicode_AsUTF8String(obj);
PyBytes_AsStringAndSize(bytes, &s, &len);
print_chars(s, len);
Py_DECREF(bytes);
Py_RETURN_NONE;
}

而对 wchar_t 的处理时想要避免内存损耗就更加难办了。
在内部,Python使用最高效的表示来存储字符串。
例如,只包含ASCII的字符串被存储为字节数组,
而包含范围从U+0000到U+FFFF的字符的字符串使用双字节表示。
由于对于数据的表示形式不是单一的,你不能将内部数组转换为 wchar_t * 然后期望它能正确的工作。
你应该创建一个 wchar_t 数组并向其中复制文本。
PyArg_ParseTuple() 的”u#”格式码可以帮助你高效的完成它(它将复制结果附加到字符串对象上)。

如果你想避免长时间内存损耗,你唯一的选择就是复制Unicode数据懂啊一个临时的数组,
将它传递给C函数,然后回收这个数组的内存。下面是一个可能的实现:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
PyObject *obj;
wchar_t *s;
Py_ssize_t len; if (!PyArg_ParseTuple(args, "U", &obj)) {
return NULL;
}
if ((s = PyUnicode_AsWideCharString(obj, &len)) == NULL) {
return NULL;
}
print_wchars(s, len);
PyMem_Free(s);
Py_RETURN_NONE;
}

在这个实现中,PyUnicode_AsWideCharString() 创建一个临时的wchar_t缓冲并复制数据进去。
这个缓冲被传递给C然后被释放掉。
但是我写这本书的时候,这里可能有个bug,后面的Python问题页有介绍。

如果你知道C函数库需要的字节编码并不是UTF-8,
你可以强制Python使用扩展码来执行正确的转换,就像下面这样:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
char *s = 0;
int len;
if (!PyArg_ParseTuple(args, "es#", "encoding-name", &s, &len)) {
return NULL;
}
print_chars(s, len);
PyMem_Free(s);
Py_RETURN_NONE;
}

最后,如果你想直接处理Unicode字符串,下面的是例子,演示了底层操作访问:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
PyObject *obj;
int n, len;
int kind;
void *data; if (!PyArg_ParseTuple(args, "U", &obj)) {
return NULL;
}
if (PyUnicode_READY(obj) < 0) {
return NULL;
} len = PyUnicode_GET_LENGTH(obj);
kind = PyUnicode_KIND(obj);
data = PyUnicode_DATA(obj); for (n = 0; n < len; n++) {
Py_UCS4 ch = PyUnicode_READ(kind, data, n);
printf("%x ", ch);
}
printf("\n");
Py_RETURN_NONE;
}

在这个代码中,PyUnicode_KIND()PyUnicode_DATA()
这两个宏和Unicode的可变宽度存储有关,这个在PEP 393中有描述。
kind 变量编码底层存储(8位、16位或32位)以及指向缓存的数据指针相关的信息。
在实际情况中,你并不需要知道任何跟这些值有关的东西,
只需要在提取字符的时候将它们传给 PyUnicode_READ() 宏。

还有最后几句:当从Python传递Unicode字符串给C的时候,你应该尽量简单点。
如果有UTF-8和宽字符两种选择,请选择UTF-8.
对UTF-8的支持更加普遍一些,也不容易犯错,解释器也能支持的更好些。
最后,确保你仔细阅读了 关于处理Unicode的相关文档

艾伯特(http://www.aibbt.com/)国内第一家人工智能门户

Python Cookbook(第3版)中文版:15.14 传递Unicode字符串给C函数库的更多相关文章

  1. Python Cookbook(第3版)中文版:15.15 C字符串转换为Python字符串

    15.15 C字符串转换为Python字符串¶ 问题¶ 怎样将C中的字符串转换为Python字节或一个字符串对象? 解决方案¶ C字符串使用一对 char * 和 int 来表示, 你需要决定字符串到 ...

  2. Python Cookbook(第3版)中文版:15.17 传递文件名给C扩展

    15.17 传递文件名给C扩展¶ 问题¶ 你需要向C库函数传递文件名,但是需要确保文件名根据系统期望的文件名编码方式编码过. 解决方案¶ 写一个接受一个文件名为参数的扩展函数,如下这样: static ...

  3. Python Cookbook(第3版)中文版:15.18 传递已打开的文件给C扩展

    15.18 传递已打开的文件给C扩展¶ 问题¶ 你在Python中有一个打开的文件对象,但是需要将它传给要使用这个文件的C扩展. 解决方案¶ 要将一个文件转换为一个整型的文件描述符,使用 PyFile ...

  4. Python Cookbook(第3版) 中文版 pdf完整版|网盘下载内附提取码

    Python Cookbook(第3版)中文版介绍了Python应用在各个领域中的一些使用技巧和方法,其主题涵盖了数据结构和算法,字符串和文本,数字.日期和时间,迭代器和生成器,文件和I/O,数据编码 ...

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

    15.16 不确定编码格式的C字符串¶ 问题¶ 你要在C和Python直接来回转换字符串,但是C中的编码格式并不确定. 例如,可能C中的数据期望是UTF-8,但是并没有强制它必须是. 你想编写代码来以 ...

  6. Python Cookbook(第3版)中文版:15.21 诊断分段错误

    15.21 诊断分段错误¶ 问题¶ 解释器因为某个分段错误.总线错误.访问越界或其他致命错误而突然间奔溃. 你想获得Python堆栈信息,从而找出在发生错误的时候你的程序运行点. 解决方案¶ faul ...

  7. Python Cookbook(第3版)中文版:15.19 从C语言中读取类文件对象

    15.19 从C语言中读取类文件对象¶ 问题¶ 你要写C扩展来读取来自任何Python类文件对象中的数据(比如普通文件.StringIO对象等). 解决方案¶ 要读取一个类文件对象的数据,你需要重复调 ...

  8. Python Cookbook(第3版)中文版:15.20 处理C语言中的可迭代对象

    15.20 处理C语言中的可迭代对象¶ 问题¶ 你想写C扩展代码处理来自任何可迭代对象如列表.元组.文件或生成器中的元素. 解决方案¶ 下面是一个C扩展函数例子,演示了怎样处理可迭代对象中的元素: s ...

  9. python cookbook第三版学习笔记十九:未包装的函数添加参数

    比如有下面如下的代码,每个函数都需要判断debug的是否为True,而默认的debug为False def a(x,debug=False): if debug: print('calling a') ...

随机推荐

  1. Windows Server 2016-部署RODC只读域控制器

    只读域控制器Read-Only Domain Controller简称RODC.RODC是Windows Server 2008之后引入的一活动目录特性,与其他域控制器一样包含AD数据库,但RODC默 ...

  2. sql sever基本语法总结

    一.数据库导入表 1.先用sql语句创建相应的表,包括表的字段和字段类型 2.导入数据,选择相应的表名,不带'$'符号的表名 二.创建数据库 create datatable 数据库名 三.查看表里的 ...

  3. 启动MySql提示:The server quit without updating PID file(…)失败

    1.可能是/usr/local/mysql/data/rekfan.pid文件没有写的权限解决方法 :给予权限,执行 "chown -R mysql:mysql /var/data" ...

  4. [NOIP]2016天天爱跑步

    [NOIP]2016天天爱跑步 标签: LCA 树上差分 NOIP Description 小C同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是 ...

  5. ubuntu16 ftp 服务 vsftp 配置

    转载:沐心_ 地址:http://bbs.csdn.net/topics/392186116------------------------------------------------------ ...

  6. Java经典编程题50道之四十七

    读取7个数(1~50)的整数值,每读取一个值,程序打印出该值个数的*. public class Example47 {    public static void main(String[] arg ...

  7. ASP.NET Core 使用 URL Rewrite 中间件实现 HTTP 重定向到 HTTPS

    在传统 ASP.NET 程序中,我们可以通过配置 IIS 的“URL 重写”功能实现将 HTTP 请求重定向为 HTTPS .但是该方法在 ASP.NET Core 应用中不再工作.在 ASP.NET ...

  8. centos/linux下的安装Maven

    1.保证该项目安装了JDK 请在系统中输入java -version查看该命令是否存在 如果没有安装JDK请移步到Centos/linux下的JDK安装 2.下载Maven wget http://m ...

  9. Mysql的sql_mode

    (一) 基本介绍 set sql_mode="",即强制不设定MySql模式(如不作输入检测.错误提示.语法模式检查等)应该能提高性能,但有如下问题: 如果插入了不合适数据(错误类 ...

  10. java字符串以及字符类型基础

    介绍一下java字符集和字符的编码方式, 首先要区分一下字符集和字符编码.所谓的字符集 类似于unicode,GB2312,GBK,ASCII等等.因为一开始只有26个英文字母需要 编一下号.所有用下 ...