Edited at

xlsx-populateで取得した値がややこしいことになってた時に文字列だけを取り出す方法

RedmineとJavaScriptに身も心も捧げる8amjpです。


xlsx-populate

さて、私の職場でもRedmineを導入したんですけど、今まで使っていたエクセル形式の帳票も継続して使わなきゃならんという事で、node.jsとxlsx-populateというライブラリを使用して、エクセル帳票のインポート/エクスポートツールを作成して運用しているんです。

このxlsx-populate、JavaScriptからエクセルファイルを非常に高い精度で読み書きできる素晴らしいツール。しかもエクセルのインストールは不要です。ありがたいですねー。

ところが、運用しているうちに一つの問題にぶつかりました。


ややこしい値

workbook.sheet("Sheet1").cell("A1").value();

xlsx-populateでは、こんな感じでセルの値を取得します。わかりやすいですね。

で、通常は数値やら文字列やら、とにかく単純な値が返ってくるんですけど。たまに、セルの一部の文字だけ赤字にしたり大きくしたりフォント変えたり、と余計な事する人いるじゃないですか。そういうセルの値を取得すると、ものすごくややこしいデータが返ってきます。

どれくらいややこしいかというと、こんな感じ。

[

{"name":"r","attributes":{},"children":[
{"name":"t","attributes":{},"children":["テキスト1"]}
]},
{"name":"r","attributes":{},"children":[
{"name":"rPr","attributes":{},"children":[
{"name":"sz","attributes":{"val":11},"children":[]},
{"name":"rFont","attributes":{"val":"MSPゴシック"},"children":[]},
{"name":"family","attributes":{"val":3},"children":[]},
{"name":"charset","attributes":{"val":128},"children":[]}
]},
{"name":"t","attributes":{},"children":["テキスト2"]}
]},
{"name":"r","attributes":{},"children":[
{"name":"rPr","attributes":{},"children":[
{"name":"sz","attributes":{"val":11},"children":[]},
{"name":"rFont","attributes":{"val":"MS Pゴシック"},"children":[]},
{"name":"family","attributes":{"val":3},"children":[]},
{"name":"charset","attributes":{"val":128},"children":[]}
]},
{"name":"t","attributes":{},"children":["テキスト3"]}
]},

/* 中略 */

{"name":"phoneticPr","attributes":{"fontId":2},"children":[]}
]

なんすかこれ。

まあ、エクセルはエクセルで、指定されたとおり忠実に再現しないといけないわけで、こんな律儀なデータになるんですけど。エクセルには罪はないんですけど。

でも、必要なのは単純なテキストだけなので、その部分だけ取り出したいんです。さて、どうしましょう。


こたえ

必要な文字列は、「nameプロパティの値がtであるオブジェクトのchildrenプロパティの配列内」に入ってます。

で、そのオブジェクトは、「nameプロパティの値がrであるオブジェクトのchildrenプロパティの配列内」に入ってます。

なので、それを上手いこと取り出せばいいんですね。

さあ、JavaScriptが誇る配列操作用の関数、filtermap関数の出番ですよ。

var value = workbook.sheet("Sheet1").cell("A1").value();

if (Array.isArray(value)) value = value.filter(prop => prop.name == 'r').map(r => r.children.filter(prop => prop.name == 't')[0].children[0]).join();

書いてて気持ちいいですねー、このコード。

特に細かく解説しませんけど、今まで説明したことをやってるだけですよ。

というわけで、xlsx-populateで取得した値がややこしいことになってた時に文字列だけを取り出す方法でした。


2019/9/13 追記

v1.20.0から仕様が変わりまして、RichTextオブジェクトがサポートされ、リッチテキストを使用しているセルの値を取得するとRichTextオブジェクトが返るようになりました。

const RichText = require('xlsx-Populate').RichText;

let value = workbook.sheet(0).cell('A1').value();
let text = value instanceof RichText ? value.text() : value;

とすれば、リッチテキストだろうがプレーンテキストだろうが文字列のみを取得できます。

……のはずなのですが、instanceof RichTextとしてもなぜかfalseが返ってきてしまうので、さしあたり

let text = value instanceof Object ? value.text() : value;

として逃げてます。