10676 怎樣暴力讀取二進位數據文件

.

  最近幾天,有不止一個人問了我一個相同的問題:有人發給他們一些數據,但數據是用二進位格式存儲的,用記事本打開全是亂碼(如題圖),怎麼辦?

  這個問題的答案當然是「解鈴還需系鈴人」,提供數據的一方,同時也有責任提供數據文件格式的說明,甚至提供讀寫這種文件格式的代碼,這樣接收方才能方便地使用這些數據。不過,現實並不總是這麼理想。有時候我們聯繫不上數據的提供方,或者對方太忙、太懶……總之,我們手上就只有一個二進位數據文件。怎麼來讀取它呢?

  這篇文章就來告訴你,二進位數據文件的格式,也是有套路的。憑藉你對數據內容的先驗知識,加上對文件內容的觀察,有時我們也能從中提取出所需的數據。當然,本文的方法並非萬能,比如碰上壓縮過的二進位文件就無能為力了。

  本文首先介紹一些準備知識,包括十六進位編輯器、二進位文件的一般結構、整數與浮點數在計算機中的表示、大端格式與小端格式,以及如何用 Matlab、Python 兩種語言讀取二進位文件中的數據。之後,我會展示三個破解二進位數據文件的實例,三個文件分別為 wav 格式的聲音波形文件、htk 格式的 MFCC 語音特徵文件、bmp 格式的圖片文件。其中第二種格式只有做語音識別的研究者才會遇到,一般人不了解也沒關係。

一、準備知識

1.1 十六進位編輯器

  要打開一個文件,一般人都會直接雙擊,讓電腦根據擴展名,用默認程序打開。例如,txt 文件就會用記事本打開,bmp 文件就會用畫圖(或其它看圖軟體)打開,等等。如果用錯了程序,要麼就打不開,要麼打開就會是亂碼。而一些帶有非通用擴展名的文件,由於沒有默認程序,一般人就打不開了。

  但是有一種程序,叫「十六進位編輯器」(hex editor),它可以打開任意類型的文件。它所做的事情,是把文件里的每個位元組,用十六進位形式和字元形式顯示出來。例如,知乎首頁的源代碼(htm 格式)用十六進位編輯器打開後是這個樣子:

  窗口的主體部分是用十六進位逐位元組顯示的文件內容。窗口最左側的數字跟最上方的數字相加,表示的是每個位元組在文件中的地址(address,也叫「偏移量」offset),例如黃色游標所在的地址是 0x2B(前綴 0x 表示十六進位,下同)。窗口的右側是用字元形式顯示的文件內容。htm 文件的內容是網頁源代碼,所以還可以讀;如果是二進位文件,這裡就是亂碼,一般是不可讀的。

  十六進位編輯器軟體有許多,上圖展示的是 2001 年的 HEditor,現在網上應該很難找到了。據我觀察,人氣比較高的一款十六進位編輯器是 UltraEdit,大家不妨一試。

1.2 二進位文件的一般結構

  二進位文件一般由文件頭數據區組成。文件頭描述了文件格式、大小等信息,數據區里存的就是數據啦。在「暴力讀取」的場景下,文件頭的格式是未知的,我們也不關心,我們只想從數據區里把數據讀出來。那麼怎麼分辨數據區呢?一般來說,如果數據區是未經壓縮的,那麼裡面的內容比較整齊,很容易看出來。比如下面是一個 wav 文件的內容,很容易看出從黃色游標處開始,下面有一串一串的豎直方向的 FF 和 00,那麼從這兒開始就是數據區了。

而下面是一個 rar 壓縮包的內容,壓縮包的內容當然是壓縮過的,所以就顯得雜亂無章。除了第 4 行能看出一個文件名以外,其它的部分就真是天書了。碰到壓縮過的數據文件,就趁早放棄吧。

1.3 整數與浮點數在計算機中的表示

  既然要讀取數據,就得知道數據在計算機中是怎麼表示的,在文件中是怎麼存儲的。常見的數據有兩種類型:整數和浮點數。

1.3.1 整數在計算機中的表示

  一個整數可能占 1 個、2 個或 4 個位元組,即 8 個、16 個或 32 個二進位位。整數還分無符號數和有符號數。無符號數的所有二進位位都用於表示數值,於是 n 位無符號數的範圍就是 0 到2^n-1,例如 8 位無符號數的範圍是 0 ~ 255。有符號數則把最高位用作符號位,0 表示正數(或 0),1 表示負數。剩下的 n-1 位用於表示數值,正數直接表示,而負數則用「補碼」表示 —— 負數-a的這 n-1 位的值是2^{n-1} - a。因此,n 位有符號數的範圍是-2^{n-1}2^{n-1} - 1,例如 8 位有符號數的範圍是 -128 ~ 127。

  舉幾個例子。正數 233 的二進位形式是 11101001,它用不同長度的無符號數和有符號數的表示如下圖,紅色的 0 表示符號位。注意圖中沒有 8 位有符號數,因為 233 超出了 8 位有符號數的範圍。

再如,負數 -23 用不同長度的有符號數的表示如下圖,紅色的 1 表示符號位。-23 用 8 位有符號數表示的形式跟 233 用 8 位無符號數表示的形式是一樣的,請讀者自行驗證。

  上面的內容在任意一本計算機入門教材中都會有,相信很多人已經聽得耳朵起繭子了。我為什麼還要重複一遍呢?我想指出的是:如果數據的範圍遠小於 16 位或 32 位整數所能表示的範圍,那麼用十六進位編輯器打開後,你就會發現一串一串的 00 或 FF,這就是整型數據區的標誌。在上一節我們已經見過了一個例子。

1.3.2 浮點數在計算機中的表示

  浮點數在計算機中是用二進位科學記數法表示的。舉個例子:2.75 這個數,用十進位科學記數法表示為2.75 times 10^0,用二進位科學記數法表示則是1.011 times 2^1。計算機中存儲的,是 1.011 這個「尾數」,和 1 這個「指數」。另外,浮點數都是有符號的,所以還要有一個符號位。

  浮點數分兩種精度:單精度占 4 個位元組(32 個二進位位),雙精度占 8 個位元組(64 個二進位位)。單精度數的結構為:1 個符號位 + 8 位指數 + 23 位尾數;雙精度數的結構為:1 個符號位 + 11 位指數 + 52 位尾數。

  仍以 2.75 為例。它用單精度和雙精度浮點數分別表示如下:

依次來看每個部分:

  • 紅色的符號位都是 0,代表正數。
  • 然後看藍色的尾數部分,可以發現只存了小數點後的 011,前面的 1 並沒有儲存。這是因為二進位科學記數法中,尾數的整數部分必定是 1,所以不必存儲。
  • 最後看綠色的指數部分。兩種浮點表示法中的指數部分其實是一個無符號整數,它們是把實際的指數進行「偏置」後的結果;單精度浮點數中儲存的是實際的指數加 127,雙精度浮點數中儲存的是實際的指數加 1023。例如在上面的例子中,單精度浮點數中的指數部分是 1 + 127 = 128,雙精度浮點數中的指數部分是 1 + 1023 = 1024。之所以要進行偏置,是因為指數可能是負數。那麼為什麼不直接用有符號數來表示呢?原來,採用帶偏置的無符號整數表示,在比較兩個浮點數大小時更方便。不過這一點與本文無關,可以不必深究。

  再舉一個例子:-1/3 的二進位是 -0.010101... 循環,寫成科學記數法是-1.dot{0}dot{1} times 2^{-2}。它用單精度和雙精度浮點數分別表示如下:

現在符號位變成了 1,指數部分分別是 -2 + 127 = 125 和 -2 + 1023 = 1021。

  知道這些有什麼用呢?我們發現,當數據的數量級接近 1(即指數的絕對值不大)時,浮點數的第一個位元組的前一半會是 3、4(正數)或 B、C(負數)。這就是浮點型數據區的標誌。在下文的第二個實例中,我們就會用到這一點。另外,當數據是分母不大的有理數時,對應於尾數部分的幾個位元組會是重複的。不過實際數據不一定是分母不大的有理數,所以這一點的用處有限。

1.3.3 大端格式與小端格式

  除了 8 位整型以外,所有的數值在計算機中的表示都佔多個位元組。這些位元組在文件中的存儲順序,就有兩種不同的選擇。如果就按上文中書寫的順序存儲,即先存高位,比如把整數 233 存儲為四個位元組 00 00 00 E9,那麼這種格式就叫「大端格式」(big-endian)。如果反過來,先存低位,比如把整數 233 存儲為 E9 00 00 00,那麼這種格式就叫「小端格式」(little-endian)。二進位數據文件不一定採用哪一種格式,需要觀察。

豆知識:「大端」和「小端」這兩個詞來自小說《格列佛遊記》。書中的小人國里有兩個敵對的派別,敵對的原因就是在剝煮雞蛋殼的時候,一撥人從大頭剝,另一撥人從小頭剝。

1.4 編程讀取二進位數據

1.4.1 Matlab 語言

  Matlab 語言中有一個 fread 函數,可以從文件中讀取二進位數據。它帶有五個參數,例如:

A = fread(fid, [5 10], int32, 0, b);n

各個參數的含義如下:

  • 第一個參數是文件句柄,可由 fopen 函數獲得。
  • 第二個參數指明要讀取的數據數量,以及結果的形狀。它可以是一個整數或一個二維向量。當它是一個整數時,結果是一個列向量,可以用 inf 表示讀到文件末尾。當它是二維向量時,結果是一個二維矩陣,數據逐列填充。
  • 第三個參數指明數據的類型,允許的值包括 int8, uint8, int16, uint16, int32, uint32, float, double。前綴 u 表示無符號,其它值的意義不言自明。讀入後的數據在內存中都會被轉換成 double 格式,如果要保留源格式,則要寫成 int32=>int32 這樣,箭頭後面表示讀入後存成的格式。
  • 第四個參數指明每讀一個數據後跳過幾個位元組,在此取為 0。
  • 第五個參數用字母 b 或 l 指明大端還是小端格式。當大小端格式無關緊要(即數據為 8 位整數)時,可以省略第四、五個參數。

上面的命令,將從 fid 代表的文件中,按大端格式讀取 50 個 32 位有符號整數,並存入 5 * 10 的二維矩陣 A。

  Matlab 中還有一個函數 fseek,可以在 fread 之前使用,指定開始讀取的位置。例如,命令 fseek(fid, 44, -1) 可以跳過文件開頭的 44 個位元組。fseek 函數的第二個參數指明下一次讀取的位置是以什麼為基準計算的:-1 表示相對於文件開頭,0 表示相對於當前位置,1 表示相對於文件結尾。

1.4.2 Python 語言

  用 Python 處理數據,常常會用到 numpy 庫。numpy 庫中有一個 numpy.fromfile 函數,可以從二進位文件中讀取數據。它的用法如下:

A = numpy.fromfile(file, dtype, count)n

各個參數的含義為:

  • 第一個參數為文件對象,也可以是字元串形式的文件名;
  • 第二個參數為數據格式,用字元串表示,例如 >i4。第一個字元用大於號或小於號表示大端或小端格式;第二個字元為 i 表示有符號整數,為 u 表示無符號整數,為 f 表示浮點數;第三個字元表示每個數據所佔的位元組數。
  • 第三個參數表示讀取多少個數,可以用 -1 表示讀到文件尾。

讀入的數據儲存在一維 numpy 數組 A 中,你可以再把它 reshape 成所需的形狀,例如 A.reshape((5, 10))。注意 reshape 時數據是逐行填充的,這與 Matlab 不同。

  Python 的文件對象同樣有 seek 功能,例如 f.seek(44, 0) 表示移動到距文件開頭 44 位元組處。這裡,第二個參數的含義與 Matlab 語言不同,在 Python 語言中,用 0、1、2 分別表示文件開頭、當前位置、文件末尾。

二、實戰演練

  在這一部分中,我們將用暴力方法從三個二進位數據文件中讀取數據。第一個文件是 2008 年夏季清華電子系 Matlab 課上用過的一段 wav 格式的語音波形,內容是男聲「電燈比油燈進步多了」。第二個文件是用 openSMILE 工具包提取的上述語音的 MFCC 特徵(13 維 + 兩階差分,共 39 維)。第三個文件是下面這張比丟圖,bmp 格式:

三個文件可以在這裡下載:

  • wav 格式的聲音波形文件:cs.cmu.edu/~yunwang/dem
  • htk 格式的 MFCC 語音特徵文件:cs.cmu.edu/~yunwang/dem
  • bmp 格式的圖片文件:cs.cmu.edu/~yunwang/dem

  為節省篇幅,讀取時的試錯過程我就只用 Matlab 語言演示,因為我更熟悉 Matlab 語言的繪圖操作。最終的代碼會有 Matlab 和 Python 兩個版本。

  當然,Matlab 和 Python 語言中都有讀取 wav 波形和 bmp 圖片的庫,事實上並不需要使用下面要講的「暴力」讀取方式。但重要的是舉一反三,本文展示的技術,可以用於許多未知格式的二進位數據文件。

2.1 暴力讀取 wav 文件

  首先用十六進位編輯器打開文件:

可以看到從游標處開始,有一串串豎直的 FF 或 00,這說明數據類型為有符號整型,這些 FF 或 00 是高位。由於每 2 列就會出現一串 FF 或 00,所以每個整數占 2 個位元組,即 16 位有符號整型。這些數據是大端格式還是小端格式呢?我們把滾動條拉到文件末尾:

可以看到最後一個位元組是 FF,所以最後的 71 FF 代表一個整數,數據為小端格式。此時再回到文件開頭,第一個整數應該是 34 FF,所以文件頭一共有 44 個位元組。

  有了這些信息,就可以用以下的 Matlab 代碼讀取波形了:

fid = fopen(voice.wav, rb); % 注意:在Windows下打開二進位文件必須指明b模式nfseek(fid, 44, -1); % 跳過 44 位元組的文件頭nA = fread(fid, inf, int16, 0, l); % 按小端格式讀取 16 位有符號整數,直到文件末尾nfclose(fid);n

  把讀進來的波形畫出來看一下:

plot(A);n

看起來是對的。採樣率並不知道,先用 8000 Hz 試著播放一下:

soundsc(A, 8000);n

聽起來聲音完全正常,破解成功!

  (事實上,我們並未排除聲音是 4000 Hz 採樣、雙聲道的可能。不過 4000 Hz 這個採樣率並不常見,就先不管了)

  用 Python 語言讀取波形的代碼如下:

import numpynwith open(voice.wav, rb) as f: # 注意:在Windows下打開二進位文件必須指明b模式n f.seek(44, 0) # 跳過 44 位元組的文件頭n A = numpy.fromfile(f, <i2, -1) # 按小端格式讀取 16 位有符號整數,直到文件末尾n

  除了 wav 格式以外,有一些語音識別資料庫中的語音是以 NIST sph 格式存儲的。sph 格式與 wav 格式相似,只不過文件頭的長度是 1024 位元組。而這個文件頭是純文本的,其中包含了採樣率、聲道數、每個樣本的格式等信息,利用它們可以減少「猜測」的工作量。

2.2 暴力讀取 htk 格式的 MFCC 特徵文件

  在本節中,你並不需要知道 MFCC 是什麼東西,只需要知道文件中存的是一個 n * 39 或 39 * n 的矩陣就行了。

  同樣先用十六進位編輯器查看文件內容:

可以看到有 4 列都是以 3, 4, B, C 開頭的,這是浮點數據區的特徵。由於每 4 列出現一次這種特徵,所以數據為單精度浮點型。這種特徵開始的位置是 0x0C。拉到文件末尾可以看出,數據為大端格式。

  從上圖還可以讀出文件的總長度為 0x67A4。文件頭的長度為 0x0C,所以數據區的總長為 0x6798,換算成十進位為 26,520。每個浮點數占 4 個位元組,所以數據區共有 6,630 個浮點數,正好組成一個 170 * 39 或 39 * 170 的矩陣。

  先按 170 * 39 讀進來試一下:

fid = fopen(voice.htk, rb);nfseek(fid, 12, -1); % 跳過 12 位元組的文件頭nA = fread(fid, [170 39], float, 0, b); % 按大端格式讀入170*39的單精度浮點數矩陣nfclose(fid);n

畫圖:

imagesc(A);n

  出現這種斜紋,一般就表示矩陣的行、列弄反了。反過來按 39 * 170 試一下:

fid = fopen(voice.htk, rb);nfseek(fid, 12, -1); % 跳過 12 位元組的文件頭nA = fread(fid, [39 170], float, 0, b); % 按大端格式讀入39*170的單精度浮點數矩陣nfclose(fid);n

畫圖:

imagesc(A);n

這次正常了!懂行的朋友應該能看出,橫軸是時間軸,表示語音信號一共有 170 幀;矩陣的前 13 行是 MFCC 係數本身,中間 13 行是一階差分,最後 13 行是二階差分。

  用 Python 語言讀取 MFCC 特徵的代碼如下。注意 Matlab 中二維矩陣是逐列填充的,而 Python 語言中二維矩陣是逐行填充的,所以 reshape 時指定的矩陣形狀應該是 170 * 39,得到的矩陣與 Matlab 中的矩陣互為轉置關係。

import numpynwith open(voice.htk, rb) as f:n f.seek(12, 0) # 跳過 12 位元組的文件頭n A = numpy.fromfile(f, >f4, -1).reshape((170, 39))n # 按大端格式讀入單精度浮點數,直到文件末尾,n # 並存入 170 * 39 的矩陣n

2.3 暴力讀取 bmp 圖片

  依然先用十六進位編輯器查看比丟圖的內容:

  從地址 0x36 起,發現一大片的 FF。比丟圖的背景為白色,其紅、綠、藍三個分量都應取最大值,所以推測 0x36 就是數據區的開頭,數據類型為 8 位無符號整數,每 3 個位元組表示一個像素,這 3 個位元組分別表示紅、綠、藍分量。

  把滾動條往下拉,可以發現在 FF 中穿插著一些 3 個位元組一重複的區域,這更加印證了「每 3 個位元組表示一個像素」的猜想。

  比丟圖文件的總長度為 0x12C36(圖略),減去文件頭長度 0x36,數據區的長度就是 0x12C00 = 76,800 位元組,這表示了 25,600 個像素。比丟圖的大小是 160 * 160,正好吻合。

  用 Matlab 讀入比丟圖的代碼如下:

fid = fopen(biu.bmp, rb);nfseek(fid, 54, -1); % 跳過 54 位元組的文件頭nA = fread(fid, inf, uint8=>uint8); % 把文件剩餘內容當作 8 位無符號整數讀取,n % 讀取的結果也保存成 8 位無符號整型nA = reshape(A, [160 160 3]); % 把讀入的數據轉換成 160 * 160 的 RGB 圖片nfclose(fid);n

畫圖看看吧!

imshow(A);n

  比丟!比丟你怎麼了比丟!

  出現這種色彩的混亂,一般是因為弄混了圖片張量的三個軸。在 reshape(A, [160 160 3]) 這條命令中,Matlab 是按如下的順序填充大小為 160 * 160 * 3 的張量的:先填滿紅色平面,再填滿綠色平面,最後填滿藍色平面。而實際上,文件中的數據是按紅、綠、藍、紅、綠、藍……的順序存儲的。為了讓 reshape 按照期望的順序填充圖片張量,需要先把 A reshape 成一個 3 * 160 * 160 的張量,再用 permute 函數把第一維挪到後面去:

A = reshape(A, [3 160 160]);nA = permute(A, [2 3 1]);nimshow(A);n

  上面的比丟圖有兩個問題:一是圖片順時針旋轉了 90 度,二是比丟變成了淺藍色。注意 Matlab 是從左往右逐列填充圖片的,既然圖片順時針旋轉了 90 度,那說明在 bmp 文件中,數據本來的順序是從下往上逐行填充的。這要求我們把圖片的水平、豎直兩個維度也調換一下,並且再把上下方向顛倒。至於顏色問題,比丟本來的橙黃色是紅色加一點綠色組成的,而現在的淺藍色是藍色加一點綠色組成的。這說明 bmp 文件中每個像素點三個顏色分量的順序是藍、綠、紅,而不是紅、綠、藍,圖片的第 3 個維度的方向也需要顛倒。

  包含了上述所有變換的完整讀取代碼如下:

fid = fopen(biu.bmp, rb);nfseek(fid, 54, -1); % 跳過 54 位元組的文件頭nA = fread(fid, inf, uint8=>uint8); % 把文件剩餘內容當作 8 位無符號整數讀取,n % 讀取的結果也保存成 8 位無符號整型nA = reshape(A, [3 160 160]); % 三個維度分別為色、列、行nA = permute(A, [3 2 1]); % 調整三個維度的順序為行、列、色nA = A(end:-1:1, :, end:-1:1); % 把行、色兩個維度的方向反轉nfclose(fid);n

這回讀入的比丟圖就正常了。

  讀取並顯示 bmp 圖片的 Python 代碼如下。由於 Python 在 reshape 時的填充順序與 Matlab 不同,Python 並不需要調換張量的三個維度,顯得更加簡潔。

import numpynwith open(biu.bmp, rb) as f:n f.seek(54, 0) # 跳過 54 位元組的文件頭n A = numpy.fromfile(f, u1, -1) # 把文件剩餘內容當作 8 位無符號整數讀取n A = A.reshape((160, 160, 3)) # 三個維度分別為行、列、色n A = A[::-1, :, ::-1] # 把行、色兩個維度的方向反轉nnimport matplotlib.pyplot as pltnplt.imshow(A) # 畫圖nplt.show()n

  在本節中,我們通過反覆試錯,弄清了 bmp 文件中數據存儲的順序:從下往上逐行掃描,每個像素點的顏色按藍、綠、紅的順序存儲。不過,實際的 bmp 文件格式比這略複雜一些:它要求每行數據占的位元組數必須是 4 的倍數,否則需要添 0 補齊。本節用的比丟圖的寬度為 160 像素,存儲它們共需要 480 位元組,正好是 4 的倍數,所以不存在添 0 補齊的問題。如果圖片寬度不是 4 的倍數,那麼讀取時就要更加麻煩一些了。


推薦閱讀:

乾貨稿|數據科學職業發展講座內容整理
一個優秀數據分析師應該培養哪些好習慣?
No.16 沒有專業搞測試的可咋辦
Uber員工被曝侵犯用戶隱私,跟蹤前任和碧昂斯等名
說一說最近打車難的原因

TAG:文件格式 | 二进制 | 数据 |