官方文檔:https://docs.python.org/3.7/library/struct.html?highlight=struct#module-struct
該模塊執(zhí)行 Python 值和表示為 Python對象的 C struct 之間的轉換。bytes可用于處理存儲在文件中或來自網(wǎng)絡連接以及其他來源的二進制數(shù)據(jù)。它使用 格式字符串作為 C 結構布局的緊湊描述以及與 Python 值的預期轉換。
默認情況下,打包給定 C 結構的結果包括填充字節(jié),以保持所涉及的 C 類型的正確對齊;同樣,開箱時也會考慮對齊。選擇此行為是為了使打包結構的字節(jié)與相應 C 結構的內(nèi)存布局完全對應。要處理與平臺無關的數(shù)據(jù)格式或省略隱式填充字節(jié),請使用standard大小和對齊而不是 native大小和對齊。
struct 模塊的用途:
1、按照指定格式將 Python 數(shù)據(jù)轉換為字符串,該字符串為字節(jié)流,如:網(wǎng)絡傳輸時不能傳輸int,此時先將int轉化為字節(jié)流,然后再發(fā)送。 2、按照指定格式將字節(jié)流轉換為 Python 指定的數(shù)據(jù)類型。 3、處理二進制數(shù)據(jù),如果用 struct 來處理圖片文件的話,需要使用 ‘rb’/‘wb’ 以二進制(字節(jié)流)讀寫的方式來處理文件。 4、處理 c 語言中的結構體。
該模塊定義了以下異常和函數(shù):
exception struct.error
在各種場合提出異常;參數(shù)是一個描述錯誤的字符串。
struct.pack(format, v1, v2, ...)
返回一個字節(jié)對象,其中包含根據(jù)格式字符串格式打包的值 v1、v2 、 ...。參數(shù)必須與格式要求的值完全匹配。
struct.pack_into(format, buffer, offset, v1, v2, ...)
根據(jù)格式字符串格式打包值 v1,v2 ,...... ,并將打包的字節(jié)寫入從位置 offset 開始的可寫緩沖區(qū)。注意,偏移量是必需的參數(shù)。
struct.unpack(format, buffer)
根據(jù)格式字符串 format 從緩沖區(qū)中解包 。結果是一個元組,即使它只包含一個項目。緩沖區(qū)的字節(jié)大小必須與格式所需的大小相匹配。
struct.unpack_from(format, buffer, offset=0)
根據(jù)格式字符串,從位置偏移開始的緩沖區(qū)解包。結果是一個元組,即使它只包含一個項目。緩沖區(qū)的大小(以字節(jié)為單位)減去 offset 后,必須至少是格式所需的大小。
struct.iter_unpack(format, buffer)
根據(jù)格式字符串 format從緩沖區(qū)中迭代解包。這個函數(shù)返回一個迭代器,它將從緩沖區(qū)中讀取相同大小的塊,直到它的所有內(nèi)容都被消耗完。緩沖區(qū)的字節(jié)大小必須是格式所需大小的倍數(shù)。每次迭代都會產(chǎn)生一個由格式字符串指定的元組。
struct.calcsize(format)
返回與格式字符串 format 對應的結構體(以及由此產(chǎn)生的字節(jié)對象 )的大小。
格式字符串是用于在打包和解包數(shù)據(jù)時指定預期布局的機制。它們是通過格式字符構建的,它指定了被打包/解包的數(shù)據(jù)類型。此外,還有用于控制字節(jié)順序、大小和對齊的特殊字符。
默認情況下,C 類型以機器的本機格式和字節(jié)順序表示,并在必要時通過跳過填充字節(jié)來正確對齊(根據(jù) C 編譯器使用的規(guī)則)。或者,格式字符串的第一個字符可用于指示打包數(shù)據(jù)的字節(jié)順序、大小和對齊方式,如下表所示:
Character | Byte order | Size | Alignment |
@ | native | native | native |
= | native | standard | none |
< | little-endian | standard | none |
> | big-endian | standard | none |
! | network (=big-endian) | standard | none |
如果第一個字符不是其中之一,'@'則為默認。
本機字節(jié)順序是大端或小端,具體取決于主機系統(tǒng)。例如:
可以使用sys.byteorder檢查系統(tǒng)的字節(jié)順序。
本機大小和對齊方式是使用 C 編譯器的 sizeof 表達式確定的。這總是與本機字節(jié)順序相結合。
標準大小僅取決于格式字符;
'@'和'='之間的區(qū)別:兩者都使用本機字節(jié)順序,但后者的大小和對齊方式是標準化的。
'!'適用于那些聲稱他們不記得網(wǎng)絡字節(jié)順序是大端還是小端的人。
無法指示非本機字節(jié)順序(強制字節(jié)交換);使用適當?shù)?< 或 > 。
注意:
格式字符具有以下含義;考慮到它們的類型,C 和 Python 值之間的轉換應該是顯而易見的。“標準大小”列是指使用標準大小時打包值的大小(以字節(jié)為單位);也就是說,當格式字符串以 '<', '>', '!' 或 '=' 中的一個開頭時。
Format | C Type | Python type | Standard size |
x | pad byte | no value | |
c | char | bytes of length 1 | 1 |
b | signed char | integer | 1 |
B | unsigned char | integer | 1 |
? | _Bool | bool | 1 |
h | short | integer | 2 |
H | unsigned short | integer | 2 |
i | int | integer | 4 |
I | unsigned int | integer | 4 |
l | long | integer | 4 |
L | unsigned long | integer | 4 |
q | long long | integer | 8 |
Q | unsigned long long | integer | 8 |
n | ssize_t | integer | |
N | size_t | integer | |
e | (6) | float | 2 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | bytes | |
p | char[] | bytes | |
P | void * | integer |
格式字符前面可以有一個整數(shù)重復計數(shù)。例如,格式字符串'4h'的含義與'hhhh'。
格式之間的空白字符被忽略;計數(shù)及其格式不能包含空格。
對于's'格式字符,計數(shù)被解釋為字節(jié)的長度,而不是像其他格式字符那樣的重復計數(shù);例如, '10s'表示單個 10 字節(jié)字符串,而'10c'表示 10 個字符。如果未給出計數(shù),則默認為 1。對于打包,字符串將被截斷或用空字節(jié)填充以使其適合。對于解包,生成的字節(jié)對象始終具有完全指定的字節(jié)數(shù)。作為一種特殊情況,'0s'表示單個空字符串(同時 '0c'表示 0 個字符)。
x當使用其中一種整數(shù)格式('b', 'B', 'h', 'H', 'i', 'I', 'l', 'L', 'q', 'Q')打包值時,如果x超出該格式的有效范圍,則引發(fā) struct.error。
格式字符對'p'“Pascal 字符串”進行編碼,這意味著存儲在固定字節(jié)數(shù)中的短可變長度字符串,由計數(shù)給出。存儲的第一個字節(jié)是字符串的長度,或 255,以較小者為準。字符串的字節(jié)如下:如果傳入的字符串pack()太長(長于 count 減 1),則只 count-1 存儲字符串的前導字節(jié)。如果字符串短于 count-1,則用空字節(jié)填充它,以便使用精確計數(shù)的字節(jié)。請注意,對于unpack(),'p'格式字符會消耗 count字節(jié),但返回的字符串不能包含超過 255 個字節(jié)。
對于'?'格式字符,返回值為True或 False。打包時使用參數(shù)對象的真值。本機或標準布爾表示中的 0 或 1 將被打包,并且任何非零值將 在解包時為 True。
所有示例都假定本機字節(jié)順序、大小和與大端機器對齊。
打包/解包三個整數(shù)的基本示例:
>>> from struct import *
>>> pack('hhl', 1, 2, 3)
b'\x00\x01\x00\x02\x00\x00\x00\x03'
>>> unpack('hhl', b'\x00\x01\x00\x02\x00\x00\x00\x03')
(1, 2, 3)
>>> calcsize('hhl')
8
解壓的字段可以通過將它們分配給變量或?qū)⒔Y果包裝在命名元組中來命名:
>>> record=b'raymond \x32\x12\x08\x01\x08'
>>> name, serialnum, school, gradelevel=unpack('<10sHHb', record)
>>> from collections import namedtuple
>>> Student=namedtuple('Student', 'name serialnum school gradelevel')
>>> Student._make(unpack('<10sHHb', record))
Student(name=b'raymond ', serialnum=4658, school=264, gradelevel=8)
格式字符的順序可能會影響大小,因為滿足對齊要求所需的填充是不同的:
>>> pack('ci', b'*', 0x12131415)
b'*\x00\x00\x00\x12\x13\x14\x15'
>>> pack('ic', 0x12131415, b'*')
b'\x12\x13\x14\x15*'
>>> calcsize('ci')
8
>>> calcsize('ic')
5
以下格式'llh0l'在末尾指定兩個填充字節(jié),假設 long 在 4 字節(jié)邊界上對齊:
>>> pack('llh0l', 1, 2, 3)
b'\x00\x00\x00\x01\x00\x00\x00\x02\x00\x03\x00\x00'
該模塊還定義了以下類型:
class struct.Struct(format)
返回一個新的 Struct 對象,該對象根據(jù)格式字符串 format 寫入和讀取二進制數(shù)據(jù)。一次創(chuàng)建一個 Struct 對象并調(diào)用它的方法比調(diào)用 struct 具有相同格式的函數(shù)更有效,因為格式字符串只需要編譯一次。
編譯后的Struct對象支持以下方法和屬性:
pack(v1, v2, ...)
與函數(shù) pack() 相同,使用編譯格式。
pack_into(buffer, offset, v1, v2, ...)
與函數(shù) pack_into() 相同,使用編譯格式 。
unpack(buffer)
與函數(shù) unpack() 相同,使用編譯格式。
unpack_from(buffer, offset=0)
與函數(shù) unpack_from() 相同,使用編譯格式。
iter_unpack(buffer)
與函數(shù) iter_unpack() 相同,使用編譯格式。
format
用于構造此 Struct 對象的格式字符串。
size
對應于 format 的結構體(以及由此 pack() 方法產(chǎn)生的字節(jié)對象)的大小。
struct是python(包括版本2和3)中的內(nèi)建模塊,它用來在c語言中的結構體與python中的字符串之間進行轉換,數(shù)據(jù)一般來自文件或者網(wǎng)絡。
返回的是一個字符串,是參數(shù)按照fmt數(shù)據(jù)格式組合而成。
按照給定數(shù)據(jù)格式解開(通常都是由struct.pack進行打包)數(shù)據(jù),返回值是一個tuple
下面2張表來自官網(wǎng)
Character | Byte order | Size | Alignment |
@ | native | native | native |
= | native | standard | none |
< | little-endian | standard | none |
> | big-endian | standard | none |
! | network (=big-endian) | standard | none |
Format | C Type | Python type | Standard size | Notes |
x | pad byte | no value | ||
c | char | string of length 1 | 1 | |
b | signed char | integer | 1 | (3) |
B | unsigned char | integer | 1 | (3) |
? | _Bool | bool | 1 | (1) |
h | short | integer | 2 | (3) |
H | unsigned short | integer | 2 | (3) |
i | int | integer | 4 | (3) |
I | unsigned int | integer | 4 | (3) |
l | long | integer | 4 | (3) |
L | unsigned long | integer | 4 | (3) |
q | long long | integer | 8 | (2), (3) |
Q | unsigned long long | integer | 8 | (2), (3) |
f | float | float | 4 | (4) |
d | double | float | 8 | (4) |
s | char[] | string | ||
p | char[] | string | ||
P | void * | integer | (5), (3) |
理論性的東西看起來都比較枯燥,來個實例代碼就容易理解多了。本例來實現(xiàn)往一個2進制文件中按照某種特定格式寫入數(shù)據(jù),之后再將它讀出。相信通過這個例子,你就能基本掌握struct的使用。
# -*- coding: utf-8 -*-
'''
數(shù)據(jù)格式為
姓名 年齡 性別 職業(yè)
lily 18 female teacher
'''
import os
import struct
fp=open('test.bin','wb')
# 按照上面的格式將數(shù)據(jù)寫入文件中
# 這里如果string類型的話,在pack函數(shù)中就需要encode('utf-8')
name=b'lily'
age=18
sex=b'female'
job=b'teacher'
# int類型占4個字節(jié)
fp.write(struct.pack('4si6s7s', name,age,sex,job))
fp.flush()
fp.close()
# 將文件中寫入的數(shù)據(jù)按照格式讀取出來
fd=open('test.bin','rb')
# 21=4 + 4 + 6 + 7
print(struct.unpack('4si6s7s',fd.read(21)))
fd.close()
運行上面的代碼,可以看到讀出的數(shù)據(jù)與寫入的數(shù)據(jù)是完全一致的。
python test.py
(b'lily', 18, b'female', b'teacher')
Process finished with exit code 0
近項目中遇到一個文檔解析的場景,目標是在瀏覽器端能預覽markdown文件。
拿到這個需求,相信很多前端同學會想到使用開源的庫,比如github上很受歡迎的marked,當然,是一個簡單而有效的方案。
但是如果你了解webassembly一點點的話,相信你也會覺得,像這種數(shù)據(jù)處理的活交給C++來干,沒錯。
好吧,我們抱著這個猜想開始下面的嘗試吧。
為了把C++代碼編譯成能在瀏覽器上運行的wasm,我們需要使用 Emscripten。 安裝Emscripten依賴如下幾個工具:Git、CMake、GCC、Python 2.7.x。
編譯 Emscripten:
git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install sdk-incoming-64bit binaryen-master-64bit
./emsdk activate sdk-incoming-64bit binaryen-master-64bit
source ./emsdk_env.sh
推薦如下的目錄結構:
.
├── build
├── build.sh
├── include
│ └── sundown
│ ├── autolink.c
│ ├── autolink.h
│ ├── buffer.c
│ ├── buffer.h
│ ├── houdini.h
│ ├── houdini_href_e.c
│ ├── houdini_html_e.c
│ ├── html.c
│ ├── html.h
│ ├── html_blocks.h
│ ├── markdown.c
│ ├── markdown.h
│ ├── stack.c
│ └── stack.h
├── src
│ ├── index.cc
│ └── wasm.c
└── web
├── index.html
├── index.js
├── index.wasm
└── test.md
這里為了測試,我是直接使用了通過C解析markdown文檔開源庫sundown。就是目錄中的include/sundown。
我們需要一個入口文件,取一個名字wasm.c。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <emscripten/emscripten.h>
#include "markdown.h"
#include "html.h"
#include "buffer.h"
#define READ_UNIT 1024
#define OUTPUT_UNIT 64
const char*
EMSCRIPTEN_KEEPALIVE wasm_markdown(char* source)
{
struct buf *ib, *ob;
struct sd_callbacks callbacks;
struct html_renderopt options;
struct sd_markdown *markdown;
ib=bufnew(READ_UNIT);
bufgrow(ib, READ_UNIT);
size_t char_len=strlen(source);
bufput(ib, source, char_len);
ob=bufnew(OUTPUT_UNIT);
sdhtml_renderer(&callbacks, &options, 0);
markdown=sd_markdown_new(0, 16, &callbacks, &options);
sd_markdown_render(ob, ib->data, ib->size, markdown);
sd_markdown_free(markdown);
/* cleanup */
bufrelease(ib);
bufrelease(ob);
return (char *)(ob->data);
}
入口文件是調(diào)用lib的方法實現(xiàn)md字符解析,輸出html格式的字符。完成編碼部分,接下來就可以構建了。
這是我的build腳本:
emcc src/wasm.c \
-O3 \
./include/sundown/markdown.c \
./include/sundown/buffer.c \
./include/sundown/autolink.c \
./include/sundown/html.c \
./include/sundown/houdini_href_e.c \
./include/sundown/houdini_html_e.c \
./include/sundown/stack.c \
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap", "ccall"]' \
-s TOTAL_MEMORY=67108864 \
-s TOTAL_STACK=31457280 \
-o build/index.js -I./include/sundown \
cp build/index.js build/index.wasm web/
解釋下其中的幾個參數(shù):
啟動一個web Server,因為webAssembly不支持file協(xié)議下加載。
emrun --port 3000 ./web
emrun是Emscriptem自帶的webServer工具,你也可以使用你喜歡的。
初始化并調(diào)用C接口。
<script src="http://127.0.0.1:3000/markdown.js"></script>
<script>
const wasm_markdown=Module.cwrap('wasm_markdown', 'string', ['string']);
console.log(wasm_markdown('# hello wasm'));
// 輸出:<h1>hello wasm</h1>
</script>
先看DEMO,分析在代碼之后。
const mdUrl='http://127.0.0.1:3000/markdown.js';
class MarkdownParse {
isInited=false;
worker=undefined;
async init(url) {
if (this.isInited) {
return;
}
return new Promise(rs=> {
const workerScripts=`
addEventListener('message', async(e)=> {
if (e.data=="startWorker") {
importScripts("${url}");
postMessage({ type: 'init' });
} else if (e.data.type==='parseData') {
await markdown.ready;
const data=markdown.parse(e.data.input);
postMessage({ type: 'parseSuccess', data });
}
}, false)`;
this.worker=new Worker(window.URL.createObjectURL(new Blob([workerScripts])));
this.worker.addEventListener('message', e=> e.data.type==='init' ? rs() : '');
this.worker.postMessage("startWorker");
this.isInited=true;
})
}
async parse(input) {
if (!this.isInited) {
await this.init(mdUrl);
}
return new Promise(resolve=> {
this.worker.addEventListener('message',
e=> e.data.type==='parseSuccess' ?
resolve(e.data.data) : null
);
this.worker.postMessage({ type: 'parseData', input });
});
}
};
(async()=> {
const md=new MarkdownParse();
// // 觸發(fā)多次解析
// const html=[
// await md.parse('# Hello Markdown'),
// await md.parse('- [ ] Todo1'),
// await md.parse('- [ ] Todo2'),
// await md.parse('- [x] Todo3'),
// await md.parse('> Date.now()'),
// await md.parse('`const a=Date.now();`'),
// ];
// document.querySelector('#markdown-body').innerHTML=html.join('');
md.parse('123');
const text=await (await fetch('test.md')).text();
const testJS=()=> {
const a=Date.now();
// marked 是JS版本的markdown解析庫
marked(text);
return Date.now() - a;
};
const testWasm=async()=> {
const a=Date.now();
await md.parse(text);
return Date.now() - a;
};
const vs=async()=> {
const result={
js_parse_time: testJS(),
wasm_parse_time: await testWasm(),
};
result.speed=result.js_parse_time / result.wasm_parse_time;
// 顯示wasm和JS的解析速度對比
document.querySelector('#markdown-body').innerHTML=JSON.stringify(result);
}
await vs();
// 輸出markdown的HTML
// document.querySelector('#markdown-body').innerHTML=await md.parse(text);
})();
解析下思路,線抽線一個類 MarkdownParse 來實現(xiàn)wasm的加載和初始化以及api。 默認情況下, web worker是不允許跨域的,但是,有方案的。web worker內(nèi)部提供了一個importScripts方法來加載非同源的JS。
到此我們完成了今天的構建webassembly應用實例,有如下收獲:
總結,本文可能只是一個很小的場景,而且單從效率這點來看,JS的200ms對比wasm的50ms,其實對于前端來說,并沒有特別驚艷的優(yōu)勢。BUT,這只是一個開始,wasm對前端帶來的性能提升會百花齊放,我們拭目以待吧~
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。