Appearance
arrow 格式说明
format="arrow" 返回的是 Apache Arrow 的 IPC Stream——一种跨语言的列式二进制格式。
先确认你需不需要它
arrow 适合要跨语言消费(C++、Rust、CUDA 等),或者不希望高精度数值被转成字符串的场景。 纯 Python 且后续要喂 numba 做数值计算,用 npz 更直接; 只是查出来看看、导出,或接下来全是 groupby/merge,用 format="pandas" 最省心。
用 Python SDK 的话不必读这一页,ArrowResult 已经把下面这些处理好了。 如果你要绕开 SDK 自己解析——比如在 C++ 里读,或者落盘后用别的程序读——那就需要知道这些约定。
这不是我们自己定的格式
与 npz 不同,arrow 是公开标准,用各语言的官方库(pyarrow、arrow-cpp、arrow-rs…) 直接就能读,不需要知道任何 FinORM 私有约定。本页要说明的只有两件事: 元数据放在哪,以及和 npz 比有什么不一样。
快速上手
python
import pyarrow as pa
with pa.ipc.open_stream(raw_bytes) as reader:
table = reader.read_all()
print(table.schema.names)
print(table.num_rows)C++ 侧请参阅 C++ Arrow 查询示例,其中包含 CMake、libcurl、arrow::ipc::RecordBatchStreamReader、OpenMP 回退和环境变量配置。Rust 侧可使用 arrow::ipc::reader::StreamReader 等官方库按同一 IPC Stream 契约读取。
是 Stream 不是 File
响应体是 Arrow IPC Stream 格式,Content-Type 为 application/vnd.apache.arrow.stream。
Arrow 有两种封装,别弄混:
| Stream | File(随机访问) | |
|---|---|---|
| 开头 | 4 字节续帧标记 0xFFFFFFFF | ARROW1 魔数 |
| 用途 | 顺序读,不需要 seek | 可 mmap 后随机读 |
| 本接口 | 就是它 | 不是 |
想落盘后 mmap 随机读,请自己在本地转成 File 格式,不要指望响应体直接是它。
元数据在 schema 里
查询元数据放在 Schema 的 custom metadata,键为 finorm,值是一个 JSON 字符串:
python
import json
meta = json.loads(table.schema.metadata[b"finorm"].decode("utf-8"))json
{
"request_id": "req_8f3a...",
"table": "cneqa_mkt_sh_sz_bj_ab_dly_dat",
"category_code": "mkt",
"schema_version": null,
"data_version": "gen_20260913_030000",
"sql_hash": "…",
"returned_rows": 482000,
"truncated": false,
"warnings": []
}truncated 为 true 说明行数达到了上限,返回的是前一部分,不是全部。 这个字段务必检查——超上限是截断,不是报错。
同样一份元数据也在 X-FinORM-* 响应头里,不解包就能读到。
和 npz 不一样的地方
npz 的 __meta__ 里有 columns[],逐列说明 encoding、dtype 和成员名。 arrow 这边没有,因为类型就在 schema 里:table.schema.field("cls_prc").type 直接给你答案, 不需要另做一份说明。
列类型对照
| 库里的类型 | arrow 类型 | 取出来是 |
|---|---|---|
| 文本 | dictionary<values=string, indices=int32> | 字符串(已解码) |
| 日期 | date32[day] | datetime.date |
| 时间戳 | timestamp[us](无时区) | datetime.datetime |
| 整数 | int32 / int64 | int |
| 浮点 | double | float |
| 布尔 | bool | bool |
| 高精度数值 | decimal128(precision, scale) | decimal.Decimal |
空值就是空值
arrow 用 validity bitmap 表达空值,取出来是 None:
python
table.column("vol").to_pylist() # [100, None, 300]
table.column("vol").null_count # 1npz 那边需要另看一个 <列名>__null 数组、值位还填了 NaN/0/空串;arrow 不需要,也没有那些填充值。
字符串列是字典编码的
证券代码这类重复度高的列用 dictionary(int32, utf8) 承载。多数时候你不用管—— to_pylist()、to_pandas() 都会自动解码。但如果要做分组计算,直接用下标更快:
python
col = table.column("stk_cd").combine_chunks()
codes = col.indices # int32,正好当分组键
values = col.dictionary # 去重后的取值表,升序高精度数值不会变成字符串
这一条是 arrow 通道和 npz 最实质的差别,也是多数人选它的原因。
float64 只能精确表示约 15 位有效数字。库里 58% 的 decimal 列声明精度超过 15 位—— 成交额 DECIMAL(18,2)、涨跌幅、总资产这些常用列都在内。
| npz | arrow | |
|---|---|---|
| 精度 ≤ 15 位 | 编成 float64 | decimal128 |
| 精度 > 15 位 | 编成字符串 | decimal128 |
也就是说同一列 trd_val(成交额),npz 给你的是一列字符串,arrow 给你的是定长二进制数值。 要做数值计算时,前者必须逐个 parse,后者转一下就行:
python
import pyarrow.compute as pc
exact = table.column("trd_val").to_pylist() # [Decimal('123456.78'), ...] 精确
as_float = pc.cast(table.column("trd_val"), "double") # 不在乎那几位就转 float64精度要不要丢由你决定,服务端不替你做这个取舍。
和 npz 的完整差异
除 DECIMAL 外,两条通道的列语义逐一对应;DECIMAL 是唯一分岔点。 这句话有测试锁着,不是口头承诺。
其余不同都属于格式本身的差别,不是约定差异:
| npz | arrow | |
|---|---|---|
| 容器 | ZIP(整包,要收完才能解) | IPC Stream(可边收边解) |
| 空值 | <列名>__null 数组 + 值位填充 | validity bitmap |
| 字符串 | 定长 UCS-4,需自己拼 __codes/__uniques | 变长 UTF-8,库自动解码 |
| 元数据 | __meta__ 成员 | schema custom metadata |
| 依赖 | numpy | pyarrow |
| 跨语言 | 只有 Python 能方便地读 | 各语言官方库直接读 |
安装
bash
pip install pyarrow --index-url https://pypi.tuna.tsinghua.edu.cn/simplepyarrow 比 numpy 大不少,装起来也慢一些。只用 Python 且不涉及高精度列的话, npz 的依赖更轻。