scala で gzip解凍とXML操作
scala3 で
scala3 やってみた
JDK のバージョン
まず、scala3 を動かすには Java 11 以上とされているので、インストールしているJDK を調べる
$ /usr/libexec/java_home -V
Matching Java Virtual Machines (5):
21.0.8 (x86_64) "Eclipse Adoptium" - "OpenJDK 21.0.8" /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
21.0.1 (x86_64) "Oracle Corporation" - "Oracle GraalVM 21.0.1+12.1" /Library/Java/JavaVirtualMachines/graalvm-jdk-21.0.1+12.1/Contents/Home
17.0.16 (x86_64) "Eclipse Adoptium" - "OpenJDK 17.0.16" /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
11.0.28 (x86_64) "Eclipse Adoptium" - "OpenJDK 11.0.28" /Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home
1.8.0_392 (x86_64) "Eclipse Temurin" - "Eclipse Temurin 8" /Library/Java/JavaVirtualMachines/temurin-8.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
Java 17 でよさそうなのでこれに変更
$ export JAVA_HOME=$(/usr/libexec/java_home -v 17)
java のバージョンを再確認
$ java -version Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 openjdk version "17.0.16" 2025-07-15 OpenJDK Runtime Environment Temurin-17.0.16+8 (build 17.0.16+8) OpenJDK 64-Bit Server VM Temurin-17.0.16+8 (build 17.0.16+8, mixed mode)
scala-cli と sbt
scala を動かす方法として簡単なのが scala-cli で プロジェクトをきっちり管理するのが sbt らしい。一旦更新しておいた
cs update
ソースコード
scala3で書くとこうなるんだって感じがある。Javaの資産をうまく使えていて便利。 scala3は本当にライブラリがいくらでもある。すごい
//> using scala "3.7.2" //> using dep "org.scala-lang.modules::scala-xml:2.4.0" import java.io.{FileInputStream, InputStreamReader, BufferedReader} import java.util.zip.GZIPInputStream import scala.xml.XML @main def main(args: String*): Unit = if args.isEmpty then println("Usage: scala-cli run Main.scala -- <path-to-xml.gz>") else // gzip 展開 val xmlString = val fis = FileInputStream(args(0)) val gis = GZIPInputStream(fis) val br = BufferedReader(InputStreamReader(gis, "UTF-8")) val sb = new StringBuilder() var line: String | Null = null while { line = br.readLine(); line != null } do sb.append(line).append("\n") br.close() sb.toString() // XML パース val xml = XML.loadString(xmlString) // <file-name> タグ抽出 val files = (xml \\ "file-name").map { node => val contentId = (node \@ "contentId") // 属性 val value = node.text.trim // タグの値 (value, contentId) } // 出力 println("file-name tags found:") files.foreach { case (value, contentId) => println(s"$value,$contentId") }
実行
scala-cli run Main.scala -- ~/Downloads/NOTICE.xml.gz
nimとzigはじめました
nim はじめました
教訓
LLM は マイナー言語や最新バージョンなどは把握していないため、LLMに聞くと高確率でハルシネーションしたり、実装してエラー→旧バージョンではこうだった的な嘘を教えてくれます。
環境は intel macOS 15.6.1です。
nim で gzip されたxmlファイルを解凍してパースする
xml を行う方法は 二種類あるらしいが簡単な方を選んだ。
import os, strutils, strformat, zippy, xmlparser, xmltree, options when isMainModule: if paramCount() < 1: quit("Usage: nim2 <gzipFile> [csvOutputFile]") let gzipFile = paramStr(1) let csvFile = if paramCount() >= 2: some(paramStr(2)) else: none(string) # GZIPを解凍 let compressed = readFile(gzipFile) let decompressed = uncompress(compressed) # XMLを解析 let xmlDoc = parseXml(decompressed) # CSVのヘッダ var rows: seq[string] = @["xmlFile,fileName,contentId"] # 必要な要素を探索(例: <file-name> 要素から 値 と contentId を取る) for node in xmlDoc.findAll("file-name"): let fileName = node.innerText().strip() let contentId = node.attr("contentId") rows.add(&"{gzipFile},{fileName},{contentId}") # 出力処理 if csvFile.isSome: writeFile(csvFile.get(), rows.join("\n")) else: echo rows.join("\n")
これでコンパイルも実行もできた。やったね
nim c nim2.nim # nim という実行ファイルの生成 nim c -r nim2.nim /path/to/NOTICE.xml.gz # 実行ファイルを生成して、引数を与えて実行
クロスコンパイルできる?
nimコンパイラはC言語のソースを生成しているのでC言語のソースから linux用のバイナリを生成するクロスコンパイルすれば、nim もクロスコンパイルできる。 ということは二つの方法がある。
ここで zig cc を使うとクロスコンパイルが簡単にできるという情報があったので試してみた。 もちろん、 glibc のバージョンが違って動かないという問題を回避するために musl libc を使おう。
一気にクロスコンパイルで失敗
いろいろ調べた結果これでいけそうなんだがうまくいかない。
CC="zig cc" nim c --cc:clang --os:linux --cpu:amd64 --passC:"-target x86_64-linux-musl" --passL:"-target x86_64-linux-musl -static" nim2.nim
これを実行すると、 musl-linux環境の stddef.h を探しにいくのだけれでども、intel MacOS の方を見てしまう。この問題の解決にこうすればいいとLLMはいうのだが、
--passC:"-target x86_64-linux-musl --sysroot $(zig libc-path)" \
zig libc-path なんてオプションには対応していなくてエラーがでた。
というところで撤退
じゃあ分割してコンパイル
つぎのコマンドで nimcache: で指定したディレクトリにC 言語に書き出すまで行える。
nim c --cc:clang --os:linux --cpu:amd64 --compileOnly \ --nimcache:build/c nim2.nim
それからこれでクロスコンパイルします、としたいのですが。
zig cc -target x86_64-linux-musl -static \ -o nim2 \ build/c/nim2.nim.c \ $(find build/c -name '*.c' ! -name 'nim2.nim.c')
nim が提供する nimbase.h のパスを教える必要があります。
nim dump
すると、${HOME}/.choosenim/toolchains/nim-${NIMVERSION}/lib のようなディレクトリが表示されますこのディレクトリに nimbase.h があります。 このディレクトリを I オプションで指定します。
zig cc -target x86_64-linux-musl \
-I${HOME}/.choosenim/toolchains/nim-2.2.4/lib \
build/c/nim2.nim.c \
-o nim2
SIMD命令SSSE3 対策
これで成功すると思ったのですが、次のようなエラーがでました。
'_mm_maddubs_epi16' requires target feature 'ssse3', but would be inlined into function 'adler32_ssse3__pkgZzippyZadler3295simd_u84' that is compiled without support for 'ssse3'ect 219 | nimln_(92); mad1_1 = _mm_maddubs_epi16(bytes1_1, tap1_1); |
これは ssse3 というSIMD 命令 (SSSE3) が必要なのに、ツールチェーン(llvm or zig)がデフォルトでサポートしていないようです。これは、zippy ライブラリ(Nim の gzip/zip ライブラリ)が要求しています。
解決方法としては、二つあります。一つは Zig 側に SSSE3 を有効化する です。もう一つは SIMD サポートを無効化したC言語ソースファイルを nim で生成する の二つです。今回は後者を選びました。
Zig 側に SSSE3 を有効化する
最近のx86_64のCPUでは ssse3 のSIMD命令はサポートされています。そのため、実際には有効にしても問題ないはずです。その場合、-mssse3 オプションで ssse3 を有効にすることもできますし、アーキテクチャを指定してあげる方法もあるらしいです。
zig cc -target x86_64-linux-musl \
-I${HOME}/.choosenim/toolchains/nim-2.2.4/lib \
-o nim2l \
-march=x86_64 -mssse3 \
build/c/nim2.nim.c $(find build/c -name '*.c' ! -name 'nim2.nim.c')
SIMD サポートを無効化したC言語ソースファイルを nim で生成する
SIMD サポートを無効化したC言語ソースファイルを nim で生成するには、-d:noSimd
というオプションで SIMD サポートを無効にできることが多いのです。ですが、zippy は -d:zippyNoSimd というオプションで無効にするようです。
nim c --cc:clang --os:linux --cpu:amd64 --compileOnly \
-d:zippyNoSimd --nimcache:build/c nim2.nim
zig cc -target x86_64-linux-musl \
-I${HOME}/.choosenim/toolchains/nim-2.2.4/lib \
-o nim2 \
build/c/nim2.nim.c $(find build/c -name '*.c' ! -name 'nim2.nim.c')
キャッシュチェックエラー対策
これで SIMD サポートを無効化できたのですが次に以下のようなエラーがでました。
build/c/nim2.nim.c:1:1: error: CacheCheckFailed
これは、Zig の クロスコンパイル時キャッシュ検証エラーだそうです。
# nim の不要なソースコードを先に削除
rm -rf build/c
# nim で zippy で SIMD命令を無効にしてCソースコードを生成
nim c --cc:clang --os:linux --cpu:amd64 --compileOnly \
-d:zippyNoSimd --nimcache:build/c nim2.nim
# zig のキャッシュを削除
rm -rf ~/.cache/zig
# zig でキャッシュを無効化して各ファイルを分割コンパイル
for f in $(find build/c -name '*.c'); do
zig cc -fno-cache -target x86_64-linux-musl -static \
-I${HOME}/.choosenim/toolchains/nim-2.2.4/lib -c $f -o ${f%.c}.o
done
# zig で全部をリンク
zig cc -target x86_64-linux-musl -static \
-o nim2l $(find build/c -name '*.o')
これでなんとかnimとzigでクロスコンパイルできました。
nim はじめました
nim はじめました
教訓
LLM は マイナー言語や最新バージョンなどは把握していないため、LLMに聞くと高確率でハルシネーションしたり、実装してエラー→旧バージョンではこうだった的な嘘を教えてくれます。
環境は intel macOS 15.6.1です。
nim 2.2.4
nim をインストールするとき、 choosenim でインストールした方がいいらしい
homebrewでインストールしてすすめるとnimlsp がパッケージマネージャの nimble でインストールできないので消す
# brew で nim をインストールしている場合 のみ brew uninstall nim # インストール curl https://nim-lang.org/choosenim/init.sh -sSf | sh
指示にしたがって、 ~/.bashrc を編集して読み込み
# インストール確認 nim --version nimble --version which nimsuggest # インストール nimble install nimlsp
gzip 圧縮されたファイルの解凍
シリーズとして NOTICE.xml.gz ファイルを解凍して、XMLファイルを抽出。そこから情報を取るというものを実装することを題材にしている。
import zlib はダメ
LLMによると、nimでは import zlibで標準添付されている zlibパッケージでgzip解凍できるとの情報があった。これはシステム側の zlib.h と libz.*dylibが必要らしい。
そのまま実行すると import zlibで zlibがみつからないというエラーになった。
nim で C言語に変換しているので、システム側の zlib.h と libz.*dylibがみつからないためかもしれないと判断して、探した。
Intel macOSの新しめのバージョンでそれらは $(xcrun --show-sdk-path)/usr/includel/zlib.h と/usr/lib/libz.1.2.12.dylibだろう。
これにあわせて、インクルードパスとリンカオプションを渡して試した。
nim c -r --passC:"-I$(xcrun --show-sdk-path)/usr/include" --passL:"-lz" src/nim2.nim
しかしこれも同様にだめ。結局 v2.2.4は import zlib で失敗する。
念のためにimport zlibで読み込む zlib.nim がそもそもない。
# インポートするzlib.nim がない find ~/.choosenim/toolchains/nim-2.2.4/ -name 'zlib*'
さらに調べると、v2.2.4のnimは、zlibはサポートしていないことを調べてみたら、nim標準の std/compress 等でzip圧縮などはサポートしているが、gzip解凍はサポートしていないとのことがわかった。
import zlibは終了
zippy を選ぶ
gzip展開する方法を調べると外部ライブラリで、zippy などがあるらしい。zippyはnimだけでいいので楽そうということでインストール
# インストール nimble install zippy # インストールできたらあるはず find ~/.nimble/ -type f -name '*zippy*'
import zippy, os, strutils proc gunzipFile(inputPath, outputPath: string) = let compressed = readFile(inputPath) let decompressed = uncompress(compressed, dfGzip) writeFile(outputPath, decompressed) echo "解凍完了: ", outputPath when isMainModule: if paramCount() < 1: echo "使い方: ", getAppFilename(), " <input.gz> [output]" quit(1) let inputFile = paramStr(1) let outputFile = if paramCount() >= 2: paramStr(2) else: # 拡張子 .gz を削除して出力ファイル名を生成 if inputFile.endsWith(".gz"): #inputFile[0 ..^4] inputFile[0 .. inputFile.len - 4] # else: inputFile & ".out" gunzipFile(inputFile, outputFile)
# コンパイル nim c src/gunzip.nim # NOTICE.xml.gz を解凍して NOTICE.xml に保存 src/gunzip ~/Downloads/NOTICE.xml.gz NOTICE.xml
一応できた。
NOTICE.xml.gz から file-nameの値とcontentIdを取り出す Gauche編
NOTICE.xml.gz から file-nameの値とcontentIdを取り出す
NOTICE.xml.gz から file-nameの値とcontentIdを取り出す by gauche
gzip 圧縮された XMLファイル、NOTICE.xml.gz があったとします。それで XML ファイルのトップレベル licenses タグ内に file-name タグがあって、そのプロパティ contentId の値と その file-name の値を抽出してみるとします。
<licenses> <file-name contentId="1234567890abcdef">value</file-name> <file-name contentId="1234567890abcdef">value</file-name> <file-name contentId="1234567890abcdef">value</file-name> <file-content contentId="1234567890abcdef"><![CDATA[ここにライセンスファイルの中身]]></file-content> </licenses> <!-- contentId はライセンス文の md5ハッシュ値 -->
この NOTICE.xml.gz は AOSP の中で使われているライセンス情報を管理するファイルです。多分検索したら手にはいる。 これをパースしてみよう。
gauche way
gauche だと標準で含まれる rfc.zlib と sxml で gzip 解凍 と xml をサポートしているらしい。
(use rfc.zlib)
(use sxml.ssax)
(use sxml.sxpath)
(use sxml.tools)
(define (gz->raw gz-file raw-file)
(call-with-input-file gz-file
(lambda (in-port)
(call-with-output-file raw-file
(lambda (out-port)
(copy-port
(open-inflating-port in-port :window-bits 47)
out-port))))))
(define (read-xml-file filename)
(call-with-input-file filename
(lambda (port)
(ssax:xml->sxml port '()))))
(define (extract-file-names sxml-data)
((sxpath '(licenses file-name)) sxml-data))
(define (extract-name-contentId record)
(let ((content-id-value
(cadar (filter (lambda (ll) (eq? 'contentId (car ll)))
(cdadr record))))
(file-name-value (caddr record)))
(cons file-name-value content-id-value)))
;;;;
; gzip -d
(gz->raw "NOTICE.xml.gz" "NOTICE.xml")
; S式風xml に変換して file-name 部分を抽出
(define fns
(extract-file-names (read-xml-file "NOTICE.xml")))
; あとはS式なのでごにょごにょ
(map extract-name-contentId fns)
XLSX with Common Lisp Part 3
XLSX with Common Lisp Part 3
link
対応策3 :xlsx パッケージのダウンロード元に
xlsx common-lisp のような検索ワードで検索すると、https://gitlab.common-lisp.net/cungil/xlsx がみつかる。なるほど、これらしい。
このリポジトリ、プロジェクトをみていると、かなり古い。作成日が 2015 年になっており、更新日は7 年前から10 年前になっている。
これは、ここに書いても応答がないだろう。でも、一応、ここまできたので、今までの内容を短く Issue #3 にまとめておいた。
Issue #3 UTF-8 support to use your language character in XLSX package
The xlsx is very nice tool. But some special case, it may not treat some string in excel, if it is not ASCII characters.
why ?
octets-to-string without :external-format assumes that the encoding is latin-1. And you can treat a Latin-1 character as UTF-8, you know. Because latin-1 data in excel are safe to show. But non Latin-1 character is not.
The encoding of Excel XML files generated by Excel-2013, LibreOffice, and Google Stylesheet is UTF-8 without BOM. Furthermore, according to a section "6.2.5 XML usage" Part 2 of ECMA-376 / ISO/IEC 29500 (Office Open specification), the encoding of Excel XML files is limited to UTF-8 with BOM, UTF-8 without BOM, UTF-16 LE or UTF-16 BE. Therefore, it can be determined just by checking the BOM at the beginning of the file.
A simple workaround
Assuming that the xml encoding of the Excel file is UTF-8 you can only add :external-format :utf-8 to the octets-to-string in the get-content function.
(ql:quickload :xlsx) ; load your xlsx package (in-package :xlsx) ; go to xlsx package ; redefine get-entry function (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 ;;;; add this ! ))))) (in-package :cl-user) ; go back to cl-user to use xlsx with multilingual support ;;; for example (xlsx:sheet-names "Your_Language.xlsx")
full workaround
(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-8-BOM / UTF-16LE / UTF-16BE (flet ((decode (octets) (let* ((len (length octets)) ;; Determine encoding based on BOM (Byte Order Mark) (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 to UTF-8 if no BOM is present (t :utf-8))) ;; Skip length: number of BOM bytes to exclude from string ;; This ensures the returned string does not include the BOM character (U+FEFF) (skip (case enc (:utf-8 (if (and (>= len 3) (= #xEF (aref octets 0))) 3 0)) (:utf-16le 2) (:utf-16be 2) (otherwise 0)))) ;; Decode octets to string, skipping BOM (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)))))))