1. 程式人生 > 實用技巧 >《Cython標準庫》1. libc.string

《Cython標準庫》1. libc.string

楔子

我們從現在開始Cython的新篇章,會學習一下Cython提供的標準模組。當然按照我們之前學習的知識,其實已經能夠實現很好的加速效果了,但是思來想去,覺得還是研究一下Cython提供的標準模組會比較好。

順便,鞏固一下之前學習的知識。

注意:從這裡開始,可能需要你有一定的C語言基礎。

我們這一次學習的是libc.string這個模組,從名字上也能看出這是用來處理C中字串的,那麼我們就來看看這個模組都提供了哪些關於字串的操作吧。

libc.string

memcpy, 函式原型: void *memcpy (void *pto, const void *pfrom, size_t size)

將一個字串拷貝到一個字元陣列中,並且可以指定拷貝的位元組數。舉個栗子:

# cython_test.pyx
from libc.string cimport memcpy

cdef:
    char s_to[10]  # 建立一個字元陣列
    char *s_from = "hello satori"  # 建立一個字串

# 將字串的內容拷貝到字元陣列中, 拷貝3個位元組
memcpy(s_to, s_from, 3)
print(s_to)
print(s_to.decode("utf-8-sig"))
import pyximport
pyximport.install(language_level=3)

import cython_test
"""
b'hel'
hel
"""

我們在宣告s_to的時候,不可以使用char *,如果你懂C的話那麼不用我多說。因此使用char *的話,那麼s_to是一個不可變的值,我們無法改變它的內容。

另外,這裡給s_from賦值的時候只能是ascii字元組成的字串,如果是非ascii字元的話,那麼必須要先轉換成Python中的bytes,然後再賦值給char *。因此如果使用libc.string中的函式的話,那麼最好是ascii字元,否則的話使用Python的方法就足夠了。

from libc.string cimport memcpy


name = "古明地覺".encode("utf-8")

cdef:
    char s_to[10]
    # 我們不能將一個非ascii字串賦值給char *,只能先轉為Python中的bytes才可以賦值
    char *s_from = name

# 將字串的內容拷貝到字元陣列中, 拷貝3個位元組
# 函式接收的實際上是void *、const void *,所以我們可以轉換一下,當然不轉也是可以的
memcpy(<void *>s_to, <const void*>s_from, 3)
print(s_to)
print(s_to.decode("utf-8-sig"))
import pyximport
pyximport.install(language_level=3)

import cython_test
"""
b'\xe5\x8f\xa4'
古
"""

memmove, 函式原型: void *memmove (void *pto, const void *pfrom, size_t size)

將一個字串拷貝到一個字串陣列中,和memcpy類似,但是更安全。

from libc.string cimport memmove


cdef:
    char s1[10]
    char *s2 = "hello world"

# 此時的字元陣列s1內部全部是0或者'\0',我們可以給s1填上一部分值
# 我們看到可以通過切片的方式,這在C裡面是不允許的
# 我們右邊的字元長度要多一些,而多餘的部分直接截斷了,但是最好長度保持一致
s1[0: 3] = "mashiro"
print(s1)  # b'mas'
# 這裡拷貝5個位元組
memmove(s1, s2, 5)
print(s1)  # b'hello'

我們說char *對應Python中的bytes,所以會打印出位元組。

另外,這裡的pyx檔案我們是單獨匯入的,只不過為了方便和直觀,我們將輸出直接寫在了pyx檔案裡面。

memset, 函式原型: void *memset (void *block, int c, size_t size)

將字元數組裡面的內容進行清空,它裡面是void *,所以可以適用於整型陣列、浮點型陣列、字元陣列,最後的size表示清空的位元組數

from libc.string cimport memset


cdef:
    char s1[5]
    long s2[5]
    double s3[5]


s1[0: 5] = "hello"
s2[0: 5] = [1, 2, 3, 4, 5]
s3[0: 5] = [1., 2., 3., 4., 5.]
print(s1)  # b'hello'
print(s2)  # [1, 2, 3, 4, 5]
print(s3)  # [1.0, 2.0, 3.0, 4.0, 5.0]

# memset是專門用於清空一個數組的,這裡的清空指的是設定為0
# 所以memset設定的時候直接設定成0即可,字串的話也是0,因為0對應的ascii碼就是'\0'
memset(s1, 0, 5 * sizeof(char))
# 清空三個
memset(s2, 0, 3 * sizeof(long))
# 清空四個
memset(s3, 0, 4 * sizeof(double))

# 字串遇到'\0'就結束了,所以列印一個空字串
print(s1)  # b''
print(s2)  # [0, 0, 0, 4, 5]
print(s3)  # [0.0, 0.0, 0.0, 0.0, 5.0]

strlen, 函式原型: size_t strlen (const char *s)

返回一個字串的長度

  • size_t:當成unsigned long即可
  • ssize_t:當成signed long即可
from libc.string cimport strlen


cdef:
    char s1[10]
    char *s2 = "satori"

# 可以容納10個字元,但是strlen是計算字元長度,遇到'\0'停止搜尋
# 這裡我們沒有賦值,所以預設都是'\0',因此長度為0
print(strlen(s1))  # 0

# 賦上值
s1[0: 5] = "ab\0de"
# 出現了'\0',就表示結束了,所以只有兩個字元
print(strlen(s1))  # 2

# 將'\0'改掉, 因為s1[2]對應的是C中的char,而C中的char對應Python中的bytes
# 因此這裡不能賦值為字串了,因為賦值字串的話會被解析成C的字串,但這裡接收的是一個char
# 所以要賦一個位元組,但如果非要想賦值字串,那麼通過s1[2: 3] = "c"即可
s1[2: 3] = "c"
print(strlen(s1))  # 5

# s2的長度顯然是6個
print(strlen(s2))  # 6

這裡強調一下,strlen計算的字元長度,不包括'\0',但是'\0'確實存在於字串中,所以sizeof的計算結果會比strlen的結果多1,因此sizeof計算的位元組數,顯然'\0'也是屬於字串的一部分的。

from libc.string cimport strlen


cdef:
    char s1[5]

# 儘管s1裡面都是'\0',但是它確實佔用了位元組
print(strlen(s1), sizeof(s1))  # 0 5

strcpy, 函式原型: char *strcpy (char *pto, const char *pfrom)

將一個字串拷貝到一個字元陣列中,和memcpy比較像,只不過strcpy只能接受char *。

from libc.string cimport strcpy


cdef:
    char s1[10]
    char *s2 = "satori"

# len和strlen作用一樣,都是計算字元長度,遇到'\0'停止
print(len(s1))  # 0
print(sizeof(s1))  #10
strcpy(s1, s2)
print(s1)  # b'satori'
print(sizeof(s1))  # 10

另外,如果字元陣列本身就有字元的話,會怎麼樣呢?

from libc.string cimport strcat, strcpy


cdef:
    char s1[20]
    char *s2 = "satori"

# 注意:對於陣列來說,它是一個常量,所以我們不能這樣做s = "xxx"
# 比如通過索引或者切片(不支援負數),使用負數的話,直譯器會異常退出
s1[0: 10] = "abcdefghij"
print(s1, sizeof(s1))  # b'abcdefghij' 20

strcpy(s1, s2)
print(s1, sizeof(s1))  # b'satori' 20

我們看到strcpy相當於檔案讀寫中的w,先清空、再重頭寫。

strncpy, 函式原型: char *strcpy (char *pto, const char *pfrom, size_t size)

用法和strcpy一樣,只不過可以多指定一個拷貝的字元數量。

from libc.string cimport strncpy


cdef:
    char s1[10]
    char *s2 = "satori"

strncpy(s1, s2, 3)
print(s1)  # b"sat"

strdup, 函式原型: char *strdup (const char *s)

接收一個char *,將其指向的空間拷貝一份,然後再返回char *。

from libc.string cimport strdup
from libc.stdlib cimport free


cdef:
    char *s1
    char *s2 = "satori"

# 注意:返回的char *指向的是堆區的空間,所以一定要記得釋放
s1 = strdup(s2)
print(s1)  # b'satori'
# 既然是堆區,那麼就可以隨便修改
# 將第一個字元修改成'S'
s1[0] = b"S"
# s1是指向第一個字元的指標, 那麼s1 + 3就是指向第4個字元的指標
(s1 + 4)[0] = b'O'
print(s1)  # b'SatoOi'
# 最後要記得釋放,當然你程式比較小的話,不釋放也沒關係,因為程式結束時也會釋放
# 但這顯然不是一個好習慣,有可能就是將來造成你記憶體洩漏的根源,所以堆區的記憶體要手動釋放掉
# 因為這不是Python,沒有人自動幫我們管理記憶體了,所以一切要靠我們手動管理了
free(s1)

strcat, 函式原型: char *strcat (char *pto, const char *pfrom)

和strcpy用法一致,只不過strcpy是從頭覆蓋,strcat是追加。舉個栗子:

from libc.string cimport strcat


cdef:
    char s1[20]
    char *s2 = "satori"

# s1裡面全是'\0'的話,strcpy和strcat是等價的
strcat(s1, "love ")
print(s1)  # b'love '

# 但是現在s1的前5個字元不是'\0'了,所以此時strcat和strcpy的區別就出來了
# 如果是strcpy(s1, s2),那麼s1的結果就是"satori",從頭覆蓋
# 如果是strcat(s1, s2),那麼s1的結果就是"love satori",也就是會從第一個'\0'開始追加
strcat(s1, s2)
print(s1)  # b'love satori'

strncat, 函式原型: char *strncat (char *pto, const char *pfrom, size_t size)

和strncat類似,追加指定個數的字元。

from libc.string cimport strncat, memset
from libc.stdlib cimport malloc, free


cdef:
    # 我們用malloc動態申請記憶體,當然返回的是void *,我們需要轉成char *
    # char *可以自動轉成void *,但是void *不能自動轉成char *
    char *s1 = <char *>malloc(sizeof(char) * 10)
    char *s2 = "satori"

# 動態申請的記憶體,本身可能帶有髒資料,因此我們需要設定成0
memset(s1, 0, sizeof(char) * 10)
# 拷貝3個字元
strncat(s1, s2, 3)
print(s1, sizeof(s1))  # b'sat' 8
free(s1)

strcmp, 函式原型: int strcmp (const char *s1, const char *s2)

比較兩個字串,如果相等返回0,s1大於s2返回1,s1小於s2返回-1。

from libc.string cimport strcmp


cdef:
    char *s1 = "satori"
    char *s2 = "satori"


print(strcmp(s1, s2))  # 0

# s1指向一個靜態字串,雖然不可以修改,但可以指向其它的字串
# 換句話說,只要是一個字元指標,都可以賦值給它,同理s2也是如此
s1 = "satori1"
print(strcmp(s1, s2))  # 1
s2 = "satori2"
print(strcmp(s1, s2))  # -1

strncmp, 函式原型: int strncmp (const char *s1, const char *s2, size_t size)

和strcmp類似,strncmp是比較前n個字元。

from libc.string cimport strcmp, strncmp


cdef:
    char *s1 = "satori"
    char *s2 = "satorI"


print(strcmp(s1, s2))  # 1
print(strncmp(s1, s2, 5))  # 0

strchr, 函式原型: char *strchr (const char *string, int c)

查詢第一次出現的字元之後的字元(包括本身)

from libc.string cimport strchr


cdef:
    char *s1 = "satori"

# 如果C中接收一個char或者int,那麼自python中則要傳遞bytes或者int
print(strchr(s1, ord('o')))  # b'ori'
# 或者b'o'也是可以的,但是'o'不行,因為C中接收的不是char *,而是int或char
print(strchr(s1, b'o'))  # b'ori'


# 如果不存在則返回NULL
cdef char * res = strchr(s1, b"k")
if res == NULL:  # 也可以用res is NULL
    print("未找到該字元")

strrchr, 函式原型: char *strrchr (const char *string, int c)

和strchr類似,只不過strrchr是從右往左查。

from libc.string cimport strchr, strrchr


cdef:
    char *s1 = "hello satori"

print(strchr(s1, b'o'))  # b'o satori'
print(strrchr(s1, b'o'))  # b'ori'

strstr, 函式原型: char *strstr (const char *haystack, const char *needle)

和strchr類似,strstr查詢的是第一次出現的字串以及其後面的所有字元。

from libc.string cimport strstr


cdef:
    char *s1 = "hello satori"


# 對於char *,可以是Python中的ASCII字串,也可以是bytes
# 但是char只能接收bytes(並且位元組長度為1),當然int也是可以的,因為底層int和char是互轉的
print(strstr(s1, "sato"))  # b'satori'
# b"satori"、"satori".encode("utf-8")、bytes("satori", encoding="utf-8")都是可以的
# 因此一個char對應一個bytes,一個char *還是對應一個bytes,只不過此時的bytes可以包含多個位元組,前者只能有一個
print(strstr(s1, b"sato"))  # b'satori'

strcspn, 函式原型: size_t strcspn (const char *string, const char *stopset)

從左往右遍歷string,找到第一個出現在stopset中的字元的位置。

from libc.string cimport strcspn


cdef:
    char *s1 = "hello satori"

# 第一次出現'o'是在索引為4的地方
print(strcspn(s1, "o"))  # 4

# 查詢l、o第一次在s1中出現的位置,哪個先出現就返回哪個
print(strcspn(s1, "lo"))  # 2
print(strcspn(s1, "ol"))  # 2

# 如果不存在,那麼返回的值為strlen(s1)
cdef int res = strcspn(s1, "K")
if res == strlen(s1):
    print("沒有該字元")  # 沒有該字元

strspn, 函式原型: size_t strspn (const char *string, const char *set)

從左往右遍歷string,找到第一個不出現在set中的字元的位置。

from libc.string cimport strspn, strlen


cdef:
    char *s1 = "hello satori"

# 從左往右遍歷s1,第一個沒有出現在"hel o"中的字元
# 顯然是字元s
print(strspn(s1, "hel o"))  # 6

# 如果是strcspn,那麼表示第一次出現在"hel o"中的字元, 顯然是0,上來就出現了

# 如果都出現了,那麼返回值也是strlen
print(strspn(s1, "hello satori"))  # 12
print(strlen(s1))  # 12

strtok, 函式原型: char *strtok (char *newstring, const char *delimiters)

對newstring使用delelimiters進行分隔,返回分隔後的第一個結果。

from libc.string cimport strtok


cdef:
    char *s1 = "he-ll-o"
    char *s2

s2 = strtok(s1, "-")
print(s2)  # b'he'

如果想要每一個字元都分隔的話,怎麼做呢?

from libc.string cimport strtok


cdef:
    char *s1 = "he-ll-o-sa-to-ri"
    char *s2

s2 = strtok(s1, "-")
while s2 != NULL:
    print(s2)
    s2 = strtok(NULL, "-")
"""
b'he'
b'll'
b'o'
b'sa'
b'to'
b'ri'
"""