意外と需要のある JavaScript のデータを CSV として保存するスニペットを書き留めます。
var data = [ ['name' , 'age', 'gender'], ['Andrew', 26 , 'male' ], ['Lisa' , 21 , 'female'], ['Fred' , 41 , 'male' ], ]
このような多重配列を元にして、
このような CSV を保存します。
ちなみに、
var data = [ {name: 'Andrew', age:26 , gender: 'male' }, {name: 'Lisa' , age:21 , gender: 'female'}, {name: 'Fred' , age:41 , gender: 'male' }, ]
このような オブジェクトの配列 にも対応させました。
んで、
最初に書いておきますが、 Mac版 Excel には対応していない CSV を扱っています 。ご容赦ください。
コード
さっそくドン、
class CSV { constructor(data, keys = false) { this.ARRAY = Symbol('ARRAY') this.OBJECT = Symbol('OBJECT') this.data = data if (CSV.isArray(data)) { if (0 == data.length) { this.dataType = this.ARRAY } else if (CSV.isObject(data[0])) { this.dataType = this.OBJECT } else if (CSV.isArray(data[0])) { this.dataType = this.ARRAY } else { throw Error('Error: 未対応のデータ型です') } } else { throw Error('Error: 未対応のデータ型です') } this.keys = keys } toString() { if (this.dataType === this.ARRAY) { return this.data.map((record) => ( record.map((field) => ( CSV.prepare(field) )).join(',') )).join('\n') } else if (this.dataType === this.OBJECT) { const keys = this.keys || Array.from(this.extractKeys(this.data)) const arrayData = this.data.map((record) => ( keys.map((key) => record[key]) )) console.log([].concat([keys], arrayData)) return [].concat([keys], arrayData).map((record) => ( record.map((field) => ( CSV.prepare(field) )).join(',') )).join('\n') } } save(filename = 'data.csv') { if (!filename.match(/\.csv$/i)) { filename = filename + '.csv' } console.info('filename:', filename) console.table(this.data) const csvStr = this.toString() const bom = new Uint8Array([0xEF, 0xBB, 0xBF]); const blob = new Blob([bom, csvStr], {'type': 'text/csv'}); const url = window.URL || window.webkitURL; const blobURL = url.createObjectURL(blob); let a = document.createElement('a'); a.download = decodeURI(filename); a.href = blobURL; a.type = 'text/csv'; a.click(); } extractKeys(data) { return new Set([].concat(...this.data.map((record) => Object.keys(record)))) } static prepare(field) { return '"' + (''+field).replace(/"/g, '""') + '"' } static isObject(obj) { return '[object Object]' === Object.prototype.toString.call(obj) } static isArray(obj) { return '[object Array]' === Object.prototype.toString.call(obj) } }
CSV
というクラスを定義しています。
使い方はこのように、
(new CSV(data)).save('foobar.csv')
調子こいて スプレッド演算子 や Set
などを多用しているので、比較的新しい Chrome とかでないと動かないかも知れませんね。
CSV の仕様
CSV はとてもシンプルな仕様です。
フィールド(Excel でいうところのセル)をカンマ ,
で区切ったものがレコードになります。
レコード同士は改行 \n
で区切ります。
フィールドに ,
と \n
が 値として 含まれる場合は、それがフィールドやレコードの区切り文字ではないことを示すためにフィールド全体をダブルクォート "
で囲む必要があります。
さらに "
で囲ったフィールドの中に "
が値として含まれる場合は "
自体をエスケープしてあげる必要があります。エスケープは "
を二つ重ねて ""
に置換することで行います。
文字コード
CSV ファイルを保存する際の文字コードについては特に規定されていませんが、 日本語を含む CSV を Excel で開きたい 場合には少々のテクニックを要します。
採用する文字コードの選択肢はいくつかありますが、
- Shift_JIS
- BOM 付き UTF-8
- BOM 付き UTF-16LE
今回は 2. BOM 付き UTF-8 を採用しています。
ただし、そうやって保存した CSV は Mac 版の Excel で開くと文字化けします 。
日本語を含む CSV を Excel で正しく開かせるためのテクニックについては、「CSV Unicode Excel」などのフレーズで検索していただけると闇が垣間見られると思います。
元データを用意する
もうこの記事で伝えたいことの本題は終わっているんですが、データを用意する方法にも軽く触れておきます。
例えば、ここに食べログの東京都内のラーメン屋の検索結果画面がありまして、
インスペクタとにらめっこしまして、
jQuery などを駆使してこのようなコードを書きますと、
const data = $('.list-rst').map(function() { const $this = $(this) const name = $this.find('.list-rst__rst-name a').text() const score = parseFloat($this.find('.list-rst__rating-val').text()) const reviewCount = parseInt($this.find('.list-rst__rvw-count-num').text(), 10) const dinnerBudget = $this.find('.cpy-dinner-budget-val').text() const lunchBudget = $this.find('.cpy-lunch-budget-val').text() const holiday = $this.find('.list-rst__holiday-datatxt').text() const comment = $this.find('.list-rst__pr-title').text().trim() const searchWord = $this.find('.list-rst__search-word .list-rst__search-word-item').map(function() { return $(this).text().trim() }).get() return { name, score, reviewCount, dinnerBudget, lunchBudget, holiday, comment, searchWord, } }).get()
すると、このようなデータが取れますので、
先ほどの CSV
クラスとしてインスタンス化して保存すると、
(new CSV(data)).save('ramen.csv')
このようなダイアログが開いてデータを保存できるわけですね。
Excel で開くと、
はい、このように。
まとめ
数ヶ月後に『あー、このサイトの情報テキトーに CSV 保存してぇ』という場面に出くわすであろう自分に捧げます。
私からは以上です。