xlsx Common Lisp
ql:qucikload xlsx は日本語のxlsxファイルに対応していない件について
TL;DR
(ql:quickload :xlsx) (in-package :xlsx) (defun get-entry (name zip) (let ((entry (zip:get-zipfile-entry name zip))) (when entry (xmls:parse (flex:octets-to-string (zip:zipfile-entry-contents entry) :external-format :utf-8))))) ;; added (in-package :cl-user)
経緯
xlsxファイルをちょっと処理したいなというときに、 quciklisp
で :xlsx
ってのがあった。
なので試してみたら、日本語対応してないことがわかった。日本語のシート名の一覧をとると化ける。
シート名に日本語をつかうのがいけないのかもしれない
(ql:quickload :xlsx) (xlsx:sheet-names "xlsxファイル")
調査1
調査の結果、xlsx.lisp の次の関数が原因っぽいことがわかった。
(defun get-entry (name zip) (let ((entry (zip:get-zipfile-entry name zip))) (when entry (xmls:parse (flex:octets-to-string (zip:zipfile-entry-contents entry))))))
ここの flex:octets-to-string
でバイナリデータを文字列に変換しているときに、文字コードを指定していない。指定していないとデフォルトは latin-1
のはず。
1 byte=1 charの人には問題ないかもしれないが、マルチバイトなどではただただ文字化けになる。
ここで flex:octets-to-string
の仕様を読んで、:external-format :utf-8
を指定してあげればOK、のはず。
しかし、本当に大丈夫だろうか?XLSXに含まれるXMLの文字コードは常にUTF-8ですか?
調査2
実際の仕様の一部
6.2.5 XML usage XML content in parts and streams defined in this document (specifically, the Media Types stream, the Core Properties part, Digital Signature XML Signature parts, and Relationships parts) shall conform to the following: a) XML content shall be encoded using either UTF-8 or UTF-16. If any part includes an encoding declaration, as defined in 4.3.3 of the XML 1.0 specification, that declaration shall not name any encoding other than UTF-8 or UTF-16.
ECMA-376 / ISO/IEC 29500 (Office Open XML仕様書) Part 2 6.2.5 XML usage より
対応
たぶん、UTF-8固定でいいはずだが... どうするか。
UTF-8 固定
たぶん、これで 99% は満足するはず。ロマン駆動開発でなければここで終了だ。
(defun get-entry (name zip) (let ((entry (zip:get-zipfile-entry name zip))) (when entry (xmls:parse (flex:octets-to-string (zip:zipfile-entry-contents entry) :external-format :utf-8))))) ;; added
UTF-8 (BOMあり、なし)か UTF-16LE か UTF-16BE かエンコードを推定する
Excel の xlsx は 仕様で UTF-8 か UTF-16 かでなければならないので他はもうないはずと割り切る。しかし、UTF-8はBOM あり、なしのバリエーションがあって、UTF-16 は ビッグエンディアンかリトルエンディアンかの二種類がある。たぶん、BOMなしのUTF-8がほとんどだろうが判定しよう。
(defun get-entry (name zip) ;; ECMA-376 Part 2 (Open Packaging Conventions), section 6.2.5 ;; https://ecma-international.org/publications-and-standards/standards/ecma-376/ ;; specifies that XML parts must be encoded in UTF-8 or UTF-16 only. ;; Therefore, we detect encoding based on BOM only (UTF-8 / UTF-16LE / UTF-16BE). (flet ((decode (octets) (let* ((len (length octets)) (enc (cond ;; UTF-8 BOM: EF BB BF ((and (>= len 3) (= #xEF (aref octets 0)) (= #xBB (aref octets 1)) (= #xBF (aref octets 2))) :utf-8) ;; UTF-16LE BOM: FF FE ((and (>= len 2) (= #xFF (aref octets 0)) (= #xFE (aref octets 1))) :utf-16le) ;; UTF-16BE BOM: FE FF ((and (>= len 2) (= #xFE (aref octets 0)) (= #xFF (aref octets 1))) :utf-16be) ;; Default: assume UTF-8 (most common case) (t :utf-8))) ;; Skip length: number of BOM bytes to exclude from string ;; This ensures the returned string does not include the BOM character (skip (case enc (:utf-8 (if (and (>= len 3) (= #xEF (aref octets 0))) 3 0)) (:utf-16le 2) (:utf-16be 2) (otherwise 0)))) (flex:octets-to-string (subseq octets skip) :external-format enc)))) (let ((entry (zip:get-zipfile-entry name zip))) (when entry (xmls:parse (decode (zip:zipfile-entry-contents entry)))))))
あきらめる
探すと、cl-xlsx
っていうのがあった。まあ、これでよさそう。