继续介绍本人的python学习过程。本节介绍如何利用python调用c代码。内容还是基于音乐信息提取的过程,架构如图一。Python调用c实现的功能是利用python访问c语言完成mysql数据库操作。
在利用python调用c语言之前,我们需要首先完成c语言功能代码,然后再考虑语言的转换问题,所以我们先介绍c语言实现的数据库访问代码。数据库操作主要包括DDL和DML,DDL在创建数据库和表时完成,c语言完成的是DML。在具体的实现中,c语言主要完成了:连接数据库,insert和select三个操作。可以认为音乐信息数据库是一个read-only数据库,只允许添加和检索,不允许删除(删除可以通过直接操作数据库完成)。
1. 数据库设计
关于音乐信息的数据库只有一个表格:all_music,创建表的SQL语句如下:
create table all_music( id integer auto_increment not null primary key, original_name varchar(100), name varchar(500) not null, artist varchar(500), genre varchar(30), album varchar(500), release_date date, directory varchar(300), size integer );
由于音乐没有很好的主键,所以我们采用surrogate key,并设定其为自动增长的字段。其他信息主要包括音乐名,歌手,专辑,类型和发行时间等。
2. C语言操作数据库
定义完数据库表格,我们就可以实现访问该表格的c代码,相关代码如下:
#include <mysql.h> typedef struct { char original_name[100]; char name[500]; char artist[500]; char genre[30]; char album[500]; char date[20]; char directory[300]; int size; }music; typedef struct { char id[20]; char original_name[100]; char directory[300]; }row_result; char* column_name[8]={"original_name","name","artist","genre","album","release_date","directory","size"}; MYSQL * connect_to(char* host,char* database,char* user,char* password) { MYSQL* con_ptr; con_ptr=mysql_init(NULL); if((con_ptr=mysql_real_connect(con_ptr,host,user,password,database,3306,NULL,0))==NULL) { fprintf(stderr,"connect failed:%s\n",mysql_error(con_ptr)); exit(-1); } return con_ptr; } void close_connect(MYSQL * con_ptr) { mysql_close(con_ptr); } void insert(MYSQL * con_ptr,char* table,music* m) { int res; char sql[2000]; sprintf(sql,"insert into %s(%s,%s,%s,%s,%s,%s,%s,%s) values (‘%s‘,‘%s‘,‘%s‘,‘%s‘,‘%s‘,‘%s‘,‘%s‘,‘%d‘)",table,column_name[0],column_name[1],column_name[2],column_name[3],column_name[4],column_name[5],column_name[6],column_name[7],m->original_name,m->name,m->artist,m->genre,m->album,m->date,m->directory,m->size); res=mysql_query((MYSQL*)con_ptr,sql); if(res) { fprintf(stderr,"insert failed:%s",mysql_error(con_ptr)); return; } } MYSQL_RES * select_music(MYSQL * con_ptr,char* table) { int res; char sql[100]; sprintf(sql,"select id,original_name,directory from %s ",table); res=mysql_query((MYSQL*)con_ptr,sql); if(res) { fprintf(stderr,"select failed:%s",mysql_error(con_ptr)); return NULL; } else { MYSQL_RES* result=mysql_store_result((MYSQL*)con_ptr); if(result) return result; else return NULL; } } row_result * fetch_row(MYSQL_RES * result) { MYSQL_ROW mysql_row; if(result==NULL) return NULL; mysql_row=mysql_fetch_row((MYSQL_RES*)result); if(mysql_row) { row_result* row=(row_result*)malloc(sizeof(row_result)); strcpy(row->id,mysql_row[0]); strcpy(row->original_name,mysql_row[1]); strcpy(row->directory,mysql_row[2]); return row; } else { fprintf(stderr,"no rows to return!\n"); return NULL; } } void free_row(row_result * row) { free(row); } void free_result(MYSQL_RES * result) { mysql_free_result(result); }
上述代码首先定义了一个music结构体,对应要访问数据库表的一行,每一个元素的大小也和数据库表的定义一致。之后定义了一个表示返回结果的结构体row_result。此外,为了方便操作数据库表格,还定义了一个column_name数组表示数据库表的每一列。
后面开始具体的数据库操作。connect_to函数建立与数据库的连接(特别注意不要将自定义的函数名字与库函数重名,否则会带来非常难找的bug!),并返回数据库连接指针。close_connect断开数据库连接。
Insert函数将一个music结构体对应的行插入数据库表中,代码的关键是构造一个没有错误的sql语句,构造sql语句时容易存在的问题是sql中如果存在“’”就会导致实际插入时的格式错误。这是因为当我们在指定某列的值时,需要采用类似‘%s‘这样的格式,如果要插入的数值也包括“’”就会导致错误的匹配。解决方案就是利用转义字符,python的mysql库为我们提供了一个可用的函数,在介绍python调用时会再次介绍。C语言貌似没有很好的函数解决该问题。
select_music函数会检索表中所有的行,我们利用mysql_store_result一次获得所有的行;另外一个可以利用的函数是mysql_use_result,这个函数会一次返回一行结果。两种函数的对比显而易见,但是在测试mysql_use_result时,它总会在返回部分结果后终止,不太可靠,因而采用了mysql_store_result函数。mysql_store_result返回的并不是可以直接访问的行数据,而是所有行的一个结果集,我们还需要利用fetch_row遍历结果集,获得每一行的真正数据。free_row和free_result分别释放每一行的空间和整个结果集。
最后,将上述代码打包成动态链接库,以供python调用。编译代码为:gcc -I/usr/include/mysql -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -fno-strict-aliasing -fwrapv db_operation.c -rdynamic -L/usr/lib64/mysql -lmysqlclient -lz -lcrypt -lnsl -lm -L/usr/lib64 -lssl -lcrypto -fPIC -shared -o libdb_operation.so
3. Python调用C代码
3.1 数据类型的对应
在调用c代码时,我们需要创建music数据结构来存放插入的音乐信息,也需要row_result结构体来保存select返回的结果。如果利用python调用上面的c代码,我们不可避免地要创建上面两个结构体。为了将python中的数据结构映射到c中的数据结构,python提供了一个叫ctypes的包,用以实现数据类型的转换。ctypes是Python的一个外部库,提供和C语言兼容的数据类型,可以很方便地调用C DLL中的函数。针对上面定义的music结构体,python中对应的数据类型如下:
#!/usr/bin/python2.6 from ctypes import * class Music(Structure): _fields_=[ (‘original_name‘,c_char*100), (‘name‘,c_char*500), (‘artist‘,c_char*500), (‘genre‘,c_char*30), (‘album‘,c_char*500), (‘release_date‘,c_char*20), (‘directory‘,c_char*300), (‘size‘,c_int) ] def setAttr(self,original_name,name,artist,genre,album,release_date,directory,size): self.original_name=original_name; self.name=name; self.artist=artist; self.genre=genre; self.album=album; self.release_date=release_date; self.directory=directory; self.size=size;
我们需要定义一个表示结构体的类Music,设置其_fields_属性,每一个属性的设置都包括属性名和属性类型。由于该类是在c中使用,所以数据类型都被转换为c语言可识别的类型,如c_char和c_int等。ctypes的类型对应如下:
和Music类似,表示返回结果的结构体在python中也有对应的类:
#!/usr/bin/python2.6 from ctypes import * class Row(Structure): _fields_=[ (‘id‘,c_char*20), (‘original_name‘,c_char*100), (‘directory‘,c_char*300) ]
3.2 函数调用
完成数据结构的对应之后,下面就可以实现具体的python函数:
#!/usr/bin/python2.6 import sys import ctypes import music as m import row import MySQLdb as mysql class CallDB: connect_lib=ctypes.cdll.LoadLibrary(‘./libdb_operation.so‘); connect_lib.fetch_row.restype=ctypes.POINTER(row.Row);#capitalized POINTER @staticmethod def connectTo(host,database,user,password): c_host=ctypes.c_char_p(host); c_database=ctypes.c_char_p(database); c_user=ctypes.c_char_p(user); c_password=ctypes.c_char_p(password); c_con_ptr =CallDB.connect_lib.connect_to(c_host,c_database,c_user,c_password); return c_con_ptr; @staticmethod def insert(c_con_ptr,table,music): c_table=ctypes.c_char_p(table); CallDB.connect_lib.insert(c_con_ptr,c_table,ctypes.pointer(music)); @staticmethod def closeConnect(c_con_ptr): CallDB.connect_lib.close_connect(c_con_ptr); @staticmethod def select(c_con_ptr,table): c_table=ctypes.c_char_p(table); c_result=CallDB.connect_lib.select_music(c_con_ptr,c_table); return c_result; @staticmethod def fetchRow(c_result): c_row_result=CallDB.connect_lib.fetch_row(c_result); return c_row_result if c_row_result else None; @staticmethod def freeRow(c_row_result): CallDB.connect_lib.free_row(c_row_result); @staticmethod def freeResult(c_result): CallDB.connect_lib.free_result(c_result);
上述代码首先引入几个必要的包,然后定义一个类CallDB。类的开始定义了一个全局变量connect_lib表示加载的动态链接库。C语言实现的函数就是通过该全局变量进行访问。下一行代码稍后再做解释。
第一个静态函数实现数据库的连接,调用的是c语言的connect_to函数。由于connect_to的参数都是c语言下的数据类型,我们不能直接传递python下的数据类型,需要首先利用ctypes将其转换成c语言可识别的类型。返回值c_con_ptr在c语言是一个MYSQL指针,python不知道其具体类型。由于我们在python中不会访问该指针,所以我们无需指定其具体类型。后面的静态函数通过调用c函数实现了数据库的插入和检索。
可以看出,利用python实现基本的c调用很简单,但是需要注意两点。第一,非基本数据类型指针参数的传递。在insert函数中,music参数通过ctypes.pointer函数被转化成一个指针类型。虽然我们实现了c语言下music结构体在python下对应的类Music,但是python没有指针的概念,传递的参数必须被手动转换成指针。下面的代码演示了insert函数的具体使用:
music=m.Music(); directory=mysql.escape_string(results[0]); album=mysql.escape_string(results[1].encode(‘utf-8‘)); release_date=results[2][4:8]+results[2][2:4]+results[2][0:2]; name=mysql.escape_string(results[i][0].encode(‘utf-8‘)); genre=mysql.escape_string(results[i][1]); artist=mysql.escape_string(results[i][2].encode(‘utf-8‘)); original_name=mysql.escape_string(results[i][3]); size=int(results[i][4]); music.setAttr(original_name,name,artist,genre,album,release_date,directory,size); db.CallDB.insert(MyRequestHandler.c_con_ptr,‘all_music‘,music);
在前面我们提到过SQL语句中转义字符的问题,MySQLdb为我们提供了一个函数escape_string可以解决转义的问题。第二,使用c代码的返回值。函数fetchRow在c语言下的返回值是一个row_result结构体指针。虽然这个指针在python下有对应的类Row,但是这需要我们手工指定,这就是开头代码
connect_lib.fetch_row.restype=ctypes.POINTER(row.Row);
的作用。特别注意,这个地方的POINTER需要大写,小写会报错。在获得返回值之后,访问对应的属性可通过下面的代码完成:
c_row_result=db.CallDB.fetchRow(MyRequestHandler.c_row_result); if c_row_result: print c_row_result.contents.original_name;
如果是基本类型,如int,char则无需指定,可以直接访问返回值;如果返回类型是char*,则我们也需要手工指定返回类型为c_char_p。
当然,如果单纯从数据库操作来看,完全可以利用MySQLdb包完成同样的功能,在此只是演示python如何调用c代码。