1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# folio
docx、xlsx、pptx を読むライブラリです。依存はありません。
3 形式とも読めます。
ZIP と XML とパッケージの土台は共通で、その上に xlsx、docx、pptx の層が載っています。
旧形式 (.xls、.doc、.ppt) も読めます。
`Book::open` と `Document::open` と `Presentation::open` が先頭のバイトを見て振り分けるので、読む側は形式を気にしなくてかまいません。
```
1| A=名前 B=数 C=日付
2| A=あ B=3.5 C=2026-03-31 00:00:00
3| A=い B=-1 C=2026-09-08 13:45:30
```
英語の説明は [README.md](./README.md) にあります。
## 巨大なシート
100 万行かける 5 列の xlsx を読む時間を、同じ機械で測りました。
folio はメモリの山が 6MB で止まります。
シート全体をメモリに置かず、行を順に流すためです。
```
読み メモリの山
folio 0.17 秒 6MB
openpyxl 27.3 秒 114MB (read_only)
openpyxl 32.7 秒 1790MB (ふつうの読み込み)
```
読みは約 160 倍です。
ふつうの読み込みで 1.8GB 要るところが、6MB で済みます。
## たくさんのファイル
`Book` はスレッドをまたいで渡せるので、ファイルをスレッドに分けて同時に読めます。
3 シート × 200 行の小さな xlsx を 1000 個読んだ時間は、次のとおりです (Apple M4、8 コア)。
```
1 スレッド 8 スレッド
folio 0.31 秒 0.06 秒
```
分け方は `examples/scale.rs` にあります。
## 使い方
シートを開いて、行を順に受け取ります。
```
use folio::book::Book;
use folio::cell::Value;
use std::path::Path;
let mut book = Book::open(Path::new("data.xlsx"))?;
for one in book.sheet() {
println!("{}", one.name);
}
let mut sheet = book.read("Sheet1")?;
while let Some(row) = sheet.read()? {
for cell in &row.cell {
println!("{} 列 {:?}", cell.column, cell.value);
}
}
```
## セルに入るもの
`Value` は 5 通りです。
- `Empty` - 何も入っていない。書式だけ付いたセルもこれになる
- `Number(f64)` - 数。日付もここに入る
- `Text(String)` - 文字
- `Boolean(bool)` - 真偽
- `Error(String)` - `#N/A` などのエラー
xlsx のセルに日付という型はありません。
日付は数で入っていて、表示形式が日付なら日付として読みます。
```
let style = sheet.style();
let epoch1904 = sheet.epoch1904();
while let Some(row) = sheet.read()? {
for cell in &row.cell {
if let Value::Number(number) = &cell.value {
if style.is_date(cell.style) {
let when = folio::date::moment(*number, epoch1904);
println!("{}-{:02}-{:02}", when.year, when.month, when.day);
}
}
}
}
```
## JSON で出す
ほかの言語から読むときは、`examples/json.rs` で行を 1 つずつ JSON にして出せます (JSON Lines)。
シート名を渡すとそのシートの行を、渡さないとシートの一覧を出します。
```
cargo run --release --example json -- data.xlsx
cargo run --release --example json -- data.xlsx Sheet1
```
1 行は次の形です。何も入っていないセルは出しません。
```
{"number":2,"cell":[{"column":1,"letter":"A","value":"あ"},{"column":2,"letter":"B","value":3.5}]}
```
値は JSON の型で `value` に入ります。文字は文字列、数は数、真偽は真偽です。
表示形式が日付の数は、数のまま `value` に入り、`date` に `2026-03-31T00:00:00` の形の日時が付きます。
エラーは `value` の代わりに `error` を持ちます。数式のあるセルには `formula` が付きます。
Rust からは `folio::json` の `row` と `sheet` で、同じ形の文字列を作れます。
## docx
本文を段落と表に分けて、順に受け取ります。文字だけが要るときは `text` で一度に取れます。
```
use folio::document::{Block, Document};
let mut document = Document::open(Path::new("file.docx"))?;
println!("{}", document.text()?);
let mut body = document.read()?;
while let Some(block) = body.read()? {
match block {
Block::Paragraph(one) => println!("{} {}", one.style, one.text()),
Block::Table(one) => println!("表 {} 行", one.row.len()),
}
}
```
段落は「続きの文字」(`Run`) の並びです。
太字のところだけ書式が違うので、1 つの文が複数の続きに分かれます。
`Run` が持つのは太字、斜体、下線、取り消し線、大きさ、色、書体です。
改行は `\n`、タブは `\t` として文字に入ります。
2 万段落の docx で測ると、次のようになります。
```
読み
folio 0.01 秒
python-docx 0.39 秒
```
## pptx
スライドは図形の並びです。文字は図形の中にあり、図形の外に文字は置けません。
位置と大きさは EMU で、`INCH` が 1 インチにあたります。
```
use folio::presentation::Presentation;
let mut file = Presentation::open(Path::new("deck.pptx"))?;
for index in 0..file.count() {
let one = file.read(index)?;
for shape in &one.shape {
println!("{} {}", shape.name, shape.body());
}
}
```
## 旧形式
.xls、.doc、.ppt は、ZIP ではなく複合ファイルという別の容れ物に、それぞれのバイナリを入れたものです。
中身も XML ではありません。それでも、新しい形式と同じ書き方で読めます。
```
use folio::book::Book;
// 拡張子ではなく、先頭のバイトで決まる
let mut book = Book::open(Path::new("old.xls"))?;
let mut sheet = book.read("Sheet1")?;
while let Some(row) = sheet.read()? {
// xlsx のときと同じ書き方
}
```
読めるのは Excel 97 以降 (BIFF8) と Word 97 以降です。
それより前の形式は、文字がその土地の符号で入っていて戻せないので、はっきり断ります。
パスワードの掛かったファイルも断ります。
旧形式で返らないものがあります。.xls は数式の文字列 (計算しておいた答えは返ります)。
.doc は太字などの飾りと、表の行や列のまとまり (セルは 1 つずつ段落として出てきます)。
.ppt は図形の位置と大きさです。
## 中の作り
土台は 3 形式で共通です。上から順に、パッケージ、XML、ZIP、展開と重なっています。
- `book` - ワークブック。開いて、シートを読む
- `sheet` - シートを行ごとに読む
- `cell`, `shared`, `style`, `date` - セルの値、共有文字列、表示形式、日付の数
- `json` - 行とシートの一覧を JSON に書く
- `document` - 文書。開いて、本文を読む
- `paragraph`, `table` - 段落と続きの文字、docx の表
- `presentation` - プレゼンテーション。開いて、スライドを読む
- `slide` - スライド 1 枚。図形と、その中の段落
- `package` - OPC。パートと関係
- `xml` - 事象を 1 つずつ返す読み取り
- `scan` - シートと共有文字列を、バイトのまま走査する道具
- `zip` - ZIP の読み取り。ZIP64 も読む
- `inflate`, `crc` - deflate の展開と、中身の確かめ
- `cfb` - 旧形式の容れ物 (複合ファイル)
- `xls`, `doc`, `ppt` - 旧形式の中身
大きなパートの展開は別のスレッドが進めます。読む側と同時に動くので、待ち時間が減ります。
縮めたバイトもまとめては読まず、ファイルから少しずつ渡します。
小さなパートは、スレッドを立てずにその場で展開します。
4MB 以下のファイルは、開いたときに丸ごと読みます。
## 何が入っていないか
- 書き込みはしない。folio は読むだけ
- パスワードで暗号化されたファイルは読まない
- 数式は文字列として持つだけで、計算はしない
- フォントや罫線は読まない
- docx は本文だけを読む。ヘッダ、フッタ、コメント、脚注は読まない
- docx の表の枠線は、表そのものに指定があるときだけ読める。書式から来る枠線は出ない
- pptx の差し込み口から受け継ぐ位置と書式は読まない。図形自身に書いてあるぶんだけを返す
## 試す
```
cargo test
cargo run --release --example show -- data.xlsx
cargo run --release --example show -- data.xlsx Sheet1
cargo run --release --example show -- file.docx
cargo run --release --example show -- deck.pptx
cargo run --release --example show -- old.xls Sheet1
cargo run --release --example json -- data.xlsx Sheet1
cargo run --release --example scale -- data.xlsx
cargo run --release --example scale -- directory 8
```
## ライセンス
MIT と Apache-2.0 の二択です。好きなほうを選んでください。
- [LICENSE-MIT](./LICENSE-MIT)
- [LICENSE-APACHE](./LICENSE-APACHE)