1. 程式人生 > >新手學python(2):C語言呼叫完成資料庫操作

新手學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程式碼。