balicat/qvd-dotnet
GitHub: balicat/qvd-dotnet
一个基于逆向工程的零依赖 .NET 库和 CLI 工具,用于在 Qlik 生态之外完整读取和写入 QVD 列式存储文件。
Stars: 0 | Forks: 0
# qvd-dotnet
[](https://github.com/balicat/qvd-dotnet/actions/workflows/ci.yml)
[](LICENSE)

这是一个零依赖的 .NET 库和命令行工具,用于读取**和写入** **QlikView / Qlik Sense QVD 文件**,基于我对这种未公开文档的二进制格式进行的逆向工程。
QVD 是 Qlik 的专有列式存储格式。目前没有官方规范,在 Qlik 产品之外也没有官方的读取方法。本项目涵盖了该格式的所有内容:
- **全部五种符号类型** —— 整数、双精度浮点数、字符串,以及两种**双值**(dual values,即一个值中包含数字 + 显示文本,例如日期序列值及其格式化文本),许多 QVD 读取器会跳过或将其展平。
- 通过格式的索引 *bias* 机制处理 **NULL**。
- **位压缩记录**(Bit-packed records)—— 数据行以小端序、按位填充的索引形式存储,指向各字段的符号表,并使用任意位宽(例如,年份列每行占用 6 位,布尔值占用 1 位)。
- **不仅是读取器,也是写入器** —— `QvdWriter` 生成的 QVD 文件可以被 QlikView 直接加载(已在 QlikView 桌面版中验证),包含双值和 NULL,并具有自动符号去重和最小位宽功能。据我所知,这是 .NET 生态系统中唯一的 QVD 写入器。
完整的逆向工程格式说明文档位于 [docs/qvd-file-format.md](docs/qvd-file-format.md)。
## 库
```
using Qvd;
var reader = QvdReader.Open("sales.qvd");
Console.WriteLine($"{reader.TableName}: {reader.RecordCount} records");
// Records decode to typed values, not strings
foreach (QvdValue[] record in reader.ReadRecords())
{
QvdValue amount = record[reader.GetFieldIndex("Amount")];
if (!amount.IsNull)
{
Console.WriteLine($"{amount.Number} (shown as '{amount.DisplayText}')");
}
}
// Column-oriented access: a field's distinct values without touching the rows
IReadOnlyList countries = reader.GetSymbolTable("Country");
// Or export everything
using var csv = new StreamWriter("sales.csv");
QvdCsv.Write(reader, csv);
```
`QvdValue` 直接对 Qlik 的双值(dual)语义进行建模:
| 类型 | `Number` | `Text` | 磁盘存储 |
|---|---|---|---|
| `Integer` | ✔ | — | `0x01` + int32 |
| `Double` | ✔ | — | `0x02` + float64 |
| `Text` | — | ✔ | `0x04` + UTF-8, NUL-terminated |
| `DualInteger` | ✔ | ✔ | `0x05` + int32 + text |
| `DualDouble` | ✔ | ✔ | `0x06` + float64 + text |
| `Null` | — | — | 不在符号表中 (bias) |
符号表是按字段延迟解码的,记录则是按需解码的,因此查看大型文件的几行数据只需为你读取的那部分内容付出开销。
### 写入
```
using Qvd;
var writer = new QvdWriter("Sales", "Country", "Amount", "Updated");
writer.AddRecord(
QvdValue.FromText("Denmark"),
QvdValue.FromDouble(1234.56),
QvdValue.FromDateTime(new DateTime(2026, 7, 3, 14, 30, 0))); // dual: serial + text
writer.AddRecord(
QvdValue.FromText("Norway"),
QvdValue.Null, // NULL (bias-encoded)
QvdValue.FromDualInteger(2026, "Y2026")); // dual: int + text
writer.Save("sales.qvd");
```
写入器会将值去重到符号表中,为每个字段计算尽可能窄的位宽(常量列每行成本为零位),并使用与 QlikView 相同的 bias 约定对 NULL 进行编码。
## CLI
```
$ qvdtool info sales.qvd
File: sales.qvd
Table: Sales
Records: 56,758
Fields: 9
Record: 30 bytes, bit-packed
FIELD TYPE SYMBOLS BITS NULLS TAGS
year INTEGER 54 6 $numeric $integer
quarter INTEGER 4 2 $numeric $integer
country ASCII 83 7 $text
mtons REAL 9,741 14 yes $numeric
...
$ qvdtool head sales.qvd -n 5 # first rows as a table
$ qvdtool csv sales.qvd -o sales.csv # full export
$ qvdtool symbols sales.qvd country # a field's distinct values
```
## 构建
```
dotnet build
dotnet test
```
需要 .NET 8 SDK。无运行时依赖。没有检入任何二进制测试固件:读取器测试会解码由独立的底层字节编码器生成的文件,而写入器测试则通过读取器进行往返测试——因此这两种实现可以相互交叉验证。
## 范围与限制
- 读取标准的未压缩 QVD 文件(这是 QlikView 和 Qlik Sense 唯一写入的类型)。会明确拒绝非空的 `Compression` 头,而不是产生误读。
- 文件在内存中缓冲,因此文件大小限制在 2 GB 以内。
- 写入器遵循与 QlikView 相同的布局生成标准的未压缩 QVD 文件;每次写入都会通过读取器进行往返测试,并且生成的文件已验证可以在 QlikView 桌面版中干净地加载(双值、日期和 NULL 完好无损)。
## 格式逆向工程过程
QVD 文件以一个纯 XML 标头开始,它直接暴露了表和字段的元数据——包括每个字段的位偏移量、位宽、索引 bias 以及符号表的范围。其背后的二进制部分是通过在十六进制编辑器中检查文件并将解码输出与 QlikView 中的源表进行交叉验证得出的:首先是带有每个值类型字节的符号表,其次是按位填充的记录部分,最后是 Qlik 用来将 NULL 表示为负索引的 bias 机制。详情请参见 [docs/qvd-file-format.md](docs/qvd-file-format.md)。
## 相关项目
- [Bondski.QvdLib](https://github.com/Bondski/QvdLib) — .NET QVD 读取器
- [devinsmith/qvdreader](https://github.com/devinsmith/qvdreader) — C++ QVD 读取器
- [PyQvd](https://github.com/MuellerConstantin/PyQvd) — Python QVD 读取器/写入器
## 许可证
[MIT](LICENSE) — David Linton
标签:Qlik Sense, QlikView, QVD, 数据读写, 文件解析, 文档结构分析