新手學python(2):C語言呼叫完成資料庫操作
繼續介紹本人的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程式碼。