なんだこれは

はてなダイアリーから移転しました。

XLSX with Common Lisp 1

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固定でいいはずだが... どうするか。

  1. UTF-8 固定
  2. UTF-8 (BOMあり、なし)か UTF-16LE か UTF-16BE かエンコードを推定する
    • BOM がある場合読み飛ばしを考慮する。
  3. :xlsx をあきらめる

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-8UTF-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 っていうのがあった。まあ、これでよさそう。