官方文档:https://docs.python.org/3.7/library/struct.html?highlight=struct#module-struct
该模块执行 Python 值和表示为 Python对象的 C struct 之间的转换。bytes可用于处理存储在文件中或来自网络连接以及其他来源的二进制数据。它使用 格式字符串作为 C 结构布局的紧凑描述以及与 Python 值的预期转换。
默认情况下,打包给定 C 结构的结果包括填充字节,以保持所涉及的 C 类型的正确对齐;同样,开箱时也会考虑对齐。选择此行为是为了使打包结构的字节与相应 C 结构的内存布局完全对应。要处理与平台无关的数据格式或省略隐式填充字节,请使用standard大小和对齐而不是 native大小和对齐。
struct 模块的用途:
1、按照指定格式将 Python 数据转换为字符串,该字符串为字节流,如:网络传输时不能传输int,此时先将int转化为字节流,然后再发送。 2、按照指定格式将字节流转换为 Python 指定的数据类型。 3、处理二进制数据,如果用 struct 来处理图片文件的话,需要使用 ‘rb’/‘wb’ 以二进制(字节流)读写的方式来处理文件。 4、处理 c 语言中的结构体。
该模块定义了以下异常和函数:
exception struct.error
在各种场合提出异常;参数是一个描述错误的字符串。
struct.pack(format, v1, v2, ...)
返回一个字节对象,其中包含根据格式字符串格式打包的值 v1、v2 、 ...。参数必须与格式要求的值完全匹配。
struct.pack_into(format, buffer, offset, v1, v2, ...)
根据格式字符串格式打包值 v1,v2 ,...... ,并将打包的字节写入从位置 offset 开始的可写缓冲区。注意,偏移量是必需的参数。
struct.unpack(format, buffer)
根据格式字符串 format 从缓冲区中解包 。结果是一个元组,即使它只包含一个项目。缓冲区的字节大小必须与格式所需的大小相匹配。
struct.unpack_from(format, buffer, offset=0)
根据格式字符串,从位置偏移开始的缓冲区解包。结果是一个元组,即使它只包含一个项目。缓冲区的大小(以字节为单位)减去 offset 后,必须至少是格式所需的大小。
struct.iter_unpack(format, buffer)
根据格式字符串 format从缓冲区中迭代解包。这个函数返回一个迭代器,它将从缓冲区中读取相同大小的块,直到它的所有内容都被消耗完。缓冲区的字节大小必须是格式所需大小的倍数。每次迭代都会产生一个由格式字符串指定的元组。
struct.calcsize(format)
返回与格式字符串 format 对应的结构体(以及由此产生的字节对象 )的大小。
格式字符串是用于在打包和解包数据时指定预期布局的机制。它们是通过格式字符构建的,它指定了被打包/解包的数据类型。此外,还有用于控制字节顺序、大小和对齐的特殊字符。
默认情况下,C 类型以机器的本机格式和字节顺序表示,并在必要时通过跳过填充字节来正确对齐(根据 C 编译器使用的规则)。或者,格式字符串的第一个字符可用于指示打包数据的字节顺序、大小和对齐方式,如下表所示:
Character | Byte order | Size | Alignment |
@ | native | native | native |
= | native | standard | none |
< | little-endian | standard | none |
> | big-endian | standard | none |
! | network (=big-endian) | standard | none |
如果第一个字符不是其中之一,'@'则为默认。
本机字节顺序是大端或小端,具体取决于主机系统。例如:
可以使用sys.byteorder检查系统的字节顺序。
本机大小和对齐方式是使用 C 编译器的 sizeof 表达式确定的。这总是与本机字节顺序相结合。
标准大小仅取决于格式字符;
'@'和'='之间的区别:两者都使用本机字节顺序,但后者的大小和对齐方式是标准化的。
'!'适用于那些声称他们不记得网络字节顺序是大端还是小端的人。
无法指示非本机字节顺序(强制字节交换);使用适当的 < 或 > 。
注意:
格式字符具有以下含义;考虑到它们的类型,C 和 Python 值之间的转换应该是显而易见的。“标准大小”列是指使用标准大小时打包值的大小(以字节为单位);也就是说,当格式字符串以 '<', '>', '!' 或 '=' 中的一个开头时。
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 |
格式字符前面可以有一个整数重复计数。例如,格式字符串'4h'的含义与'hhhh'。
格式之间的空白字符被忽略;计数及其格式不能包含空格。
对于's'格式字符,计数被解释为字节的长度,而不是像其他格式字符那样的重复计数;例如, '10s'表示单个 10 字节字符串,而'10c'表示 10 个字符。如果未给出计数,则默认为 1。对于打包,字符串将被截断或用空字节填充以使其适合。对于解包,生成的字节对象始终具有完全指定的字节数。作为一种特殊情况,'0s'表示单个空字符串(同时 '0c'表示 0 个字符)。
x当使用其中一种整数格式('b', 'B', 'h', 'H', 'i', 'I', 'l', 'L', 'q', 'Q')打包值时,如果x超出该格式的有效范围,则引发 struct.error。
格式字符对'p'“Pascal 字符串”进行编码,这意味着存储在固定字节数中的短可变长度字符串,由计数给出。存储的第一个字节是字符串的长度,或 255,以较小者为准。字符串的字节如下:如果传入的字符串pack()太长(长于 count 减 1),则只 count-1 存储字符串的前导字节。如果字符串短于 count-1,则用空字节填充它,以便使用精确计数的字节。请注意,对于unpack(),'p'格式字符会消耗 count字节,但返回的字符串不能包含超过 255 个字节。
对于'?'格式字符,返回值为True或 False。打包时使用参数对象的真值。本机或标准布尔表示中的 0 或 1 将被打包,并且任何非零值将 在解包时为 True。
所有示例都假定本机字节顺序、大小和与大端机器对齐。
打包/解包三个整数的基本示例:
>>> 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
解压的字段可以通过将它们分配给变量或将结果包装在命名元组中来命名:
>>> 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'在末尾指定两个填充字节,假设 long 在 4 字节边界上对齐:
>>> pack('llh0l', 1, 2, 3)
b'\x00\x00\x00\x01\x00\x00\x00\x02\x00\x03\x00\x00'
该模块还定义了以下类型:
class struct.Struct(format)
返回一个新的 Struct 对象,该对象根据格式字符串 format 写入和读取二进制数据。一次创建一个 Struct 对象并调用它的方法比调用 struct 具有相同格式的函数更有效,因为格式字符串只需要编译一次。
编译后的Struct对象支持以下方法和属性:
pack(v1, v2, ...)
与函数 pack() 相同,使用编译格式。
pack_into(buffer, offset, v1, v2, ...)
与函数 pack_into() 相同,使用编译格式 。
unpack(buffer)
与函数 unpack() 相同,使用编译格式。
unpack_from(buffer, offset=0)
与函数 unpack_from() 相同,使用编译格式。
iter_unpack(buffer)
与函数 iter_unpack() 相同,使用编译格式。
format
用于构造此 Struct 对象的格式字符串。
size
对应于 format 的结构体(以及由此 pack() 方法产生的字节对象)的大小。
struct是python(包括版本2和3)中的内建模块,它用来在c语言中的结构体与python中的字符串之间进行转换,数据一般来自文件或者网络。
返回的是一个字符串,是参数按照fmt数据格式组合而成。
按照给定数据格式解开(通常都是由struct.pack进行打包)数据,返回值是一个tuple
下面2张表来自官网
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) |
理论性的东西看起来都比较枯燥,来个实例代码就容易理解多了。本例来实现往一个2进制文件中按照某种特定格式写入数据,之后再将它读出。相信通过这个例子,你就能基本掌握struct的使用。
# -*- coding: utf-8 -*-
'''
数据格式为
姓名 年龄 性别 职业
lily 18 female teacher
'''
import os
import struct
fp=open('test.bin','wb')
# 按照上面的格式将数据写入文件中
# 这里如果string类型的话,在pack函数中就需要encode('utf-8')
name=b'lily'
age=18
sex=b'female'
job=b'teacher'
# int类型占4个字节
fp.write(struct.pack('4si6s7s', name,age,sex,job))
fp.flush()
fp.close()
# 将文件中写入的数据按照格式读取出来
fd=open('test.bin','rb')
# 21=4 + 4 + 6 + 7
print(struct.unpack('4si6s7s',fd.read(21)))
fd.close()
运行上面的代码,可以看到读出的数据与写入的数据是完全一致的。
python test.py
(b'lily', 18, b'female', b'teacher')
Process finished with exit code 0
近项目中遇到一个文档解析的场景,目标是在浏览器端能预览markdown文件。
拿到这个需求,相信很多前端同学会想到使用开源的库,比如github上很受欢迎的marked,当然,是一个简单而有效的方案。
但是如果你了解webassembly一点点的话,相信你也会觉得,像这种数据处理的活交给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);
}
入口文件是调用lib的方法实现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/
解释下其中的几个参数:
启动一个web Server,因为webAssembly不支持file协议下加载。
emrun --port 3000 ./web
emrun是Emscriptem自带的webServer工具,你也可以使用你喜欢的。
初始化并调用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();
// // 触发多次解析
// 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 来实现wasm的加载和初始化以及api。 默认情况下, web worker是不允许跨域的,但是,有方案的。web worker内部提供了一个importScripts方法来加载非同源的JS。
到此我们完成了今天的构建webassembly应用实例,有如下收获:
总结,本文可能只是一个很小的场景,而且单从效率这点来看,JS的200ms对比wasm的50ms,其实对于前端来说,并没有特别惊艳的优势。BUT,这只是一个开始,wasm对前端带来的性能提升会百花齐放,我们拭目以待吧~
*请认真填写需求信息,我们会在24小时内与您取得联系。