Skip to content

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 有两种封装,别弄混:

StreamFile(随机访问)
开头4 字节续帧标记 0xFFFFFFFFARROW1 魔数
用途顺序读,不需要 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 / int64int
浮点doublefloat
布尔boolbool
高精度数值decimal128(precision, scale)decimal.Decimal

空值就是空值 ​

arrow 用 validity bitmap 表达空值,取出来是 None:

python
table.column("vol").to_pylist()   # [100, None, 300]
table.column("vol").null_count    # 1

npz 那边需要另看一个 <列名>__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)、涨跌幅、总资产这些常用列都在内。

npzarrow
精度 ≤ 15 位编成 float64decimal128
精度 > 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 是唯一分岔点。 这句话有测试锁着,不是口头承诺。

其余不同都属于格式本身的差别,不是约定差异:

npzarrow
容器ZIP(整包,要收完才能解)IPC Stream(可边收边解)
空值<列名>__null 数组 + 值位填充validity bitmap
字符串定长 UCS-4,需自己拼 __codes/__uniques变长 UTF-8,库自动解码
元数据__meta__ 成员schema custom metadata
依赖numpypyarrow
跨语言只有 Python 能方便地读各语言官方库直接读

安装 ​

bash
pip install pyarrow --index-url https://pypi.tuna.tsinghua.edu.cn/simple

pyarrow 比 numpy 大不少,装起来也慢一些。只用 Python 且不涉及高精度列的话, npz 的依赖更轻。