1. 程式人生 > >Python處理二進位制結構化資料時的技術細節

Python處理二進位制結構化資料時的技術細節

Find an English version here

轉載請保留原文地址 http://blog.csdn.net/ir0nf1st/article/details/69583752

<0x00> 前言

處理二進位制檔案或者從網路接收位元組流時,位元組流中的結構化資料可能存在二進位制有符號數。雖然開發者根據位元組流協議可以先驗的知道有符號數的位元組序、字長、符號位等資訊,但在使用Python進行型別轉換時缺少將這些資訊顯式傳遞給Python直譯器的手段。本文介紹了兩種在Python開發中處理二進位制有符號數的方法。

<0x01> Python如何處理數字型別

很多程式語言在處理有符號數時將數字的最高位作為符號位,但Python的數字型別實現有所不同。以下是CPython中對長整形的定義:(以下皆為Python 2.7版本) include/longobject.h
/* Long (arbitrary precision) integer object interface */

typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
include/longintrepr.h
/* Long integer representation.
   The absolute value of a number is equal to
   	SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)
   Negative numbers are represented with ob_size < 0;
   zero is represented by ob_size == 0.
   In a normalized number, ob_digit[abs(ob_size)-1] (the most significant
   digit) is never zero.  Also, in all cases, for all valid i,
   	0 <= ob_digit[i] <= MASK.
   The allocation function takes care of allocating extra memory
   so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available.

   CAUTION:  Generic code manipulating subtypes of PyVarObject has to
   aware that longs abuse  ob_size's sign bit.
*/

struct _longobject {
	PyObject_VAR_HEAD
	digit ob_digit[1];
};

include/object.h
/* PyObject_VAR_HEAD defines the initial segment of all variable-size
 * container objects.  These end with a declaration of an array with 1
 * element, but enough space is malloc'ed so that the array actually
 * has room for ob_size elements.  Note that ob_size is an element count,
 * not necessarily a byte count.
 */
#define PyObject_VAR_HEAD               \
    PyObject_HEAD                       \
    Py_ssize_t ob_size; /* Number of items in variable part */
從以上程式碼以及註釋中我們知道,Python中的長整形,即_longobject,其符號位是通過PyObject_VAR_HEAD中的ob_size來表示,而ob_digit只儲存著長整形的絕對值。 另一方面,當我們初始化一個長整形物件(即將一個數值賦值給物件)的時候,Python直譯器並不會把這個數值的最高位當作符號位而是將其當作有效位。 下面以數值-500為例,來看看使用該數值對Python物件賦值會發生什麼,簡便起見,這裡使用16bit字長。 我們知道,為了計算方便,負數在計算機中以其二進位制補碼形式儲存,我們首先將-500轉換為二進位制補碼:
十進位制 -500
十六進位制 -0x01 F4
二進位制 -0b 0000 0001 1111 0100
二進位制原碼(符號在最高位)  0b 1000 0001 1111 0100
二進位制反碼(除符號位外,對原碼按位取反)  0b 1111 1110 0000 1011
二進位制補碼(二進位制反碼加1)  0b 1111 1110 0000 1100
二進位制補碼的十六進位制表示  0xFE 0C
下面程式碼模擬從位元組流中接收到一個大位元組序的二進位制串(0xFE 0x0C)並把它賦值給一個Python變數然後列印該變數的值:
>>> stream = '\xFE\x0C'
>>> number = (ord(stream[0]) << 8) + ord(stream[1])
>>> '0x{:0X}'.format(number)
'0xFE0C'
>>> print number
65036
>>>
這顯然不是我們想要的結果,為了讓Python在以有符號數對物件進行初始化時正確工作,上面的程式碼需要修改。

<0x02> 使用負號傳遞數值的符號資訊

現在我們知道Python並不從數字的符號位讀取符號資訊,為了讓Python正確的處理該數值,開發者必須顯示的通知Python直譯器:這是一個負數。開發者可以藉助負號(-),即negative操作符來完成這個任務。 先看一下negtive操作符在CPython中的實現: Objects/longobject.c
static PyObject *
long_neg(PyLongObject *v)
{
    PyLongObject *z;
    if (v->ob_size == 0 && PyLong_CheckExact(v)) {
        /* -0 == 0 */
        Py_INCREF(v);
        return (PyObject *) v;
    }
    z = (PyLongObject *)_PyLong_Copy(v);
    if (z != NULL)
        z->ob_size = -(v->ob_size);
    return (PyObject *)z;
}

可見negtive操作符只對_longobject的ob_size域(即Python記載的符號位)進行了negtive操作,而ob_digit域不變。 我們還知道,如果數值value為負數,則有value = - abs(value)。當我們將負數的絕對值和negtive操作符一起傳遞給Python直譯器,Python直譯器就能正確的處理該數值。 對於本例中的二進位制串0xFE 0x0C,我們已經先驗的知道了該串的位元組序,字長資訊,所以其符號位在最高位即bit15。因符號位為1,則該數值為負數。下一步是對該數值求絕對值,由於數值是二進位制補碼形式儲存,則其絕對值為二進位制補碼減去1(即其二進位制反碼)再按位取反。 程式碼如下:
>>> number = 0xFE0C
>>> if (number & 0x8000) != 0:
...     number = -((number - 1) ^ 0xFFFF)
...
>>> print number
-500
>>>


細心的讀者可能會注意到上面程式碼中按位取反操作並沒有使用~(即invert操作符),而是使用的和0xFFFF異或來實現。 如果改為invert操作符會如何呢?
>>> number = 0xFE0C
>>> if (number & 0x8000) != 0:
...     number = -(~(number - 1))
...
>>> print number
65036
>>>
還是讓我們來看一下CPython對invert操作符的實現: Objects/longobject.c
static PyObject *
long_invert(PyLongObject *v)
{
    /* Implement ~x as -(x+1) */
    PyLongObject *x;
    PyLongObject *w;
    w = (PyLongObject *)PyLong_FromLong(1L);
    if (w == NULL)
        return NULL;
    x = (PyLongObject *) long_add(v, w);
    Py_DECREF(w);
    if (x == NULL)
        return NULL;
    Py_SIZE(x) = -(Py_SIZE(x));
    return (PyObject *)x;
}
正如程式碼中的註釋說明,Python的invert操作符的實現並不是真的按位取反,而是對數值加1後的negtive操作。


<0x03> 一個更通用的庫

struct是一個對二進位制結構化資料解包與打包的庫,可以讓你使用對開發者更友好的方式來傳遞位元組序、字長、符號位等資訊,同時也有著足夠的錯誤報告機制,讓你的開發更高效。
>>> import struct
>>> stream = '\xFE\x0C'
>>> number, = struct.unpack('>h', stream)
>>> print number
-500
>>>
'>h'是struct支援的格式化串,‘>'表明位元組流為大位元組序,‘h'表明位元組流包含signed short。關於struct模組更多的說明可參見這個連結