D言語でExcel(.xlsx)ファイル読み込み

# はじめに
この記事では、`Excel(.xlsx)`ファイルをD言語で読み込む処理について、書きます。
`dub`パッケージを使用しますが、そのまま使うだけでは正しく動作しない場合があります。
その回避策についても、情報提供します。

# XLSXファイルを読み込むためのdubパッケージ
以下の`dub`パッケージが提供されています。
[mir-excel](https://code.dlang.org/packages/mir-excel)
[xlsx](https://code.dlang.org/packages/xlsx)
[xlsxreader](https://code.dlang.org/packages/xlsxreader)

この記事では、`xlsxreader`を使用します。

# 開発環境
ソースコードのコンパイルは、以下のバージョンの`DMD`を使います。
* [DMD](https://dlang.org/download.html) v2.105.3

インプットとなる`Excel`ファイルは、[このページ](https://www.jpx.co.jp/listing/event-schedules/financial-announcement/index.html)にあるものを使います。
（何でもよかったのですが、ネット上で手に入る`Excel`ファイルということで）

# 問題点：うまくいかない理由
`XLSX`ファイルは内部に`XML`形式でデータを持っていますが、`Excel`と`LibreOffice Calc`で作成した`XLSX`ファイルでは、`XML`の構造に違いがあります。
具体的には、`Excel`で作成した場合、`<t>`タグが入れ子で存在する場合があります。
`LibreOffice Calc`では入れ子の`<t>`タグは生成されません。
この差により、`Excel`の処理では文字列を正しく取得できないという問題が発生します。
どの`dub`パッケージを使っても同じ問題が発生するため、もしかしたら`Excel`日本語版特有の問題なのかもしれません。

![excel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/35b16fdb-8b80-94b8-3bc5-acbaced4dc49.png)

# 回避策とソースコード
問題を回避するために、以下のようなソースコードを実装しました。
具体的には、`xlsxreader`パッケージのソースを参考に`readSheetImpl2`を実装しました。

```d:readexcel.d
/+ dub.sdl:
    name "readexcel"
    dependency "xlsxreader" version="~>3.0.1"
+/
// dub build --build=release --single readexcel.d
import std.file : exists;
import std.path : extension;
import std.stdio;
import std.string : toLower;

import xlsxreader;

void main(string[] args)
{
	if ( args.length < 2 ){
		writeln("Error : Requires parameters");
		return;
	}
	string file = args[1];
	if ( !file.exists ){
		writefln("Error : Not exists %s", file);
		return;
	}
	if ( file.extension.toLower() == ".xlsx" ){
			file.readFile("HP用");
	} else {
		writeln("Error : Requires .xlsx file");
	}
}

void readFile(string fileName, string sheetName = "Sheet1")
{
	writefln("File  = %s", fileName);
	writefln("Sheet = %s", sheetName);
	Sheet sheet = readSheet2(fileName, sheetName);
	writefln("row = %d, col = %d", sheet.maxPos.row + 1, sheet.maxPos.col + 1);
	for ( int row = 3; row <= sheet.maxPos.row; row++ ){
		if ( sheet.getString(row, 1) == "" ){
			break;
		}
		writefln("%s, %s, %s",
			sheet.getString(row, 1),
			sheet.getString(row, 2),
			sheet.getString(row, 6));
	}
}

string getString(Sheet sheet, string sPos)
{
	Pos p = toPos(sPos);
	return ( sheet.getCell(p.row, p.col).xmlValue );
}

string getString(Sheet sheet, size_t row, size_t col)
{
	return ( sheet.getCell(row, col).xmlValue );
}

Cell getCell(Sheet sheet, size_t row, size_t col)
{
	if ( row > sheet.maxPos.row || col > sheet.maxPos.col ){
		return ( Cell.init );
	}
	return ( sheet.table[row][col] );
}

Sheet readSheet2(in string filename, in string sheetName) @safe {
	import std.algorithm : filter;
	SheetNameId[] sheets = sheetNames(filename);
	auto sRng = sheets.filter!(s => s.name == sheetName);
	return ( readSheetImpl2(filename, sRng.front.rid) );
}

Sheet readSheetImpl2(in string filename, in string rid) @trusted {
	import std.file : read;
	import std.zip;
	auto file = new ZipArchive(read(filename));
	auto ams = file.directory;
	immutable ss = "xl/sharedStrings.xml";
	string[] sharedStrings = (ss in ams)
		? readSharedEntries(file, ams[ss])
		: [];
	string[] sharedStrings2;
	for ( int n = 0; n < sharedStrings.length; n++ ){
		if ( sharedStrings[n] != "" ){
			sharedStrings2 ~= sharedStrings[n];
		}
	}
	Relationships[string] rels = parseRelationships(file,
			ams["xl/_rels/workbook.xml.rels"]);
	Relationships* sheetRel = rid in rels;
	string shrFn = eatXlPrefix(sheetRel.file);
	string fn = "xl/" ~ shrFn;
	ArchiveMember* sheet = fn in ams;
	
	Sheet ret;
	ret.cells = insertValueIntoCell(readCells(file, *sheet), sharedStrings2);
	Pos maxPos;
	foreach(ref c; ret.cells) {
		c.position = toPos(c.r);
		maxPos = elementMax(maxPos, c.position);
	}
	ret.maxPos = maxPos;
	ret.table = new Cell[][](ret.maxPos.row + 1, ret.maxPos.col + 1);
	foreach(const c; ret.cells) {
		ret.table[c.position.row][c.position.col] = c;
	}
	return ( ret );
}
```

回避策のキモは、問題のある`readSharedEntries`呼び出しの後に追加した、以下の処理です。
```d:
	string[] sharedStrings2;
	for ( int n = 0; n < sharedStrings.length; n++ ){
		if ( sharedStrings[n] != "" ){
			sharedStrings2 ~= sharedStrings[n];
		}
	}
// ・・・省略・・・
	ret.cells = insertValueIntoCell(readCells(file, *sheet), sharedStrings2);
```

# コンパイルと実行結果
いつも通り、コンパイルと実行結果を記します。

```text:コンパイル例
D:\Dev> dub build --build=release --single readexcel.d
    Fetching dxml 0.4.4 (getting selected version)
    Fetching xlsxreader 3.0.1 (getting selected version)
    Starting Performing "release" build using D:\App\Dev\dmd2\windows\bin64\dmd.exe for x86_64.
    Building dxml 0.4.4: building configuration [library]
    Building xlsxreader 3.0.1: building configuration [library]
C:\Users\user1\AppData\Local\dub\packages\xlsxreader\3.0.1\xlsxreader\source\xlsxreader.d(110,53): Deprecation: `@safe` function `getColumn` calling `array`
C:\Users\user1\AppData\Local\dub\packages\xlsxreader\3.0.1\xlsxreader\source\xlsxreader.d(66,8):        which wouldn't be `@safe` because of:
C:\Users\user1\AppData\Local\dub\packages\xlsxreader\3.0.1\xlsxreader\source\xlsxreader.d(66,8):        scope variable `range` assigned to non-scope parameter `items` calling `put`
C:\Users\user1\AppData\Local\dub\packages\xlsxreader\3.0.1\xlsxreader\source\xlsxreader.d(159,56): Deprecation: `@safe` function `getRow` calling `array`
C:\Users\user1\AppData\Local\dub\packages\xlsxreader\3.0.1\xlsxreader\source\xlsxreader.d(66,8):        which wouldn't be `@safe` because of:
C:\Users\user1\AppData\Local\dub\packages\xlsxreader\3.0.1\xlsxreader\source\xlsxreader.d(66,8):        scope variable `range` assigned to non-scope parameter `items` calling `put`
    Building readexcel ~master: building configuration [application]
     Linking readexcel
    Finished To force a rebuild of up-to-date targets, run again with --force

```
```text:実行結果
D:\Dev> readexcel kessan09_1102.xlsx
File  = kessan09_1102.xlsx
Sheet = HP用
row = 3133, col = 7
2753, あみやき亭, プライム
2408, ＫＧ情報, スタンダード
3593, ホギメディカル, プライム

・・・省略・・・

8160, 木曽路, プライム
9973, 小僧寿し, スタンダード
9980, ＭＲＫホールディングス, スタンダード

```