Excel 2007 一括 CSV 変換 - Spreadsheet::XLSX

かつて,Microsoft Excel ブックに含まれるワークシートすべてを一括して CSV データファイルに書き出すプログラムについて,記事を書いた(「Excel ワークシートの一括 CSV 変換」)。しかしながら,そこで示した方法は,Microsoft Excel 97-2003 ブック形式(拡張子 .xls)を前提としていた。Microsoft Office 2007 以降の最近の Excel はフォーマットが大きく変わり,このプログラムでは扱えない。ちょっと必要があり,Excel 2007 フォーマット(拡張子 .xlsx)のブックのワークシートすべてを一括して CSV に書き出すプログラムを作成した。今回も Perl 言語である。

Spreadsheet::XLSX

Excel 2007 フォーマットを処理できる Perl モジュールとして Spreadsheet::XLSX が公開されており,これを使うことにした。これは「Excel ワークシートの一括 CSV 変換」で触れた Spreadsheet::ParseExcel と同じ作者によるもので,使い方もほぼ同じようなものとなっている。

Spreadsheet::XLSX は CPAN から入手出来る。しかしながら,Spreadsheet::XLSX は日本語の取り扱いが不十分であり,仮名漢字変換ソフトで入力した日本語セルを出力させると,日本語とともにカナも出力されてしまう,という問題点がある。たとえば,「漢字」というセルが「漢字カンジ」と出てしまうのである。この問題についてはネットリソースに対策を記したものがいくつかあり,たとえば「【PERL】SPREADSHEET::XLSXでセルの値に勝手にフリガナが付く件について」が参考になった。

要するに,cpan -i Spreadsheet::XLSX で単純にインストールするだけでは不十分で,Spreadsheet::XLSX のモジュールを訂正しなければならない。次にその訂正方法について述べる。

Spreadsheet::XLSX-0.13 のモジュール訂正とインストール

以下の記述は Spreadsheet::XLSX バージョン 0.13 が前提である。まずは CPAN からアーカイブをダウンロードして解凍する。

% wget -nH -nd http://search.cpan.org/CPAN/authors/id/D/DM/DMOW/\
Spreadsheet-XLSX-0.13-withoutworldwriteables.tar.gz
% tar zxvf Spreadsheet-XLSX-0.13-withoutworldwriteables.tar.gz -C ~/var
% cd ~/var/Spreadsheet-XLSX-0.13

ここで,日本語問題訂正のパッチファイルを準備する。以下をコピーして xlsx-patch というファイル名で Spreadsheet-XLSX-0.13 ディレクトリの直下に格納しておく。

*** lib/Spreadsheet/XLSX.pm.orig    2015-03-12 14:39:43.000000000 +0900
--- lib/Spreadsheet/XLSX.pm 2015-03-12 14:40:36.000000000 +0900
***************
*** 43,48 ****
--- 43,49 ----
        my $mstr = $member_shared_strings->contents; 
        $mstr =~ s/<t\/>/<t><\/t>/gsm;  # this handles an empty t tag in the xml <t/>
        foreach my $si ($mstr =~ /<si.*?>(.*?)<\/si/gsm) {
+           $si =~ s/<rPh[^>]*>.*?<\/rPh>//gsm;
            my $str;
            foreach my $t ($si =~ /<t.*?>(.*?)<\/t/gsm) {
                $t = $converter -> convert ($t) if $converter;

上記パッチを適用すると,XLSX.pm モジュールが訂正される。

% pwd
/Users/isao/var/Spreadsheet-XLSX-0.13
% ls
Changes*    Makefile.PL*    lib/        xlsx-patch
MANIFEST*   README*         t/
% patch -l -p0 < xlsx-patch
patching file lib/Spreadsheet/XLSX.pm
%

パッチを適用すると 45 行目の次に $si =~ s/<rPh[^>]*>.*?<\/rPh>//gsm; という行が追加されているはずである。Excel ファイルのセルデータ XML の rPh タグにカナが格納されており,パッチで追加したコードは,この rPh タグデータをごっそり削除してしまう処理を実行するものである。パッチが面倒であればテキストエディタを使って XLSX.pm の 45 行目の次にこれを挿入してもよい。

        foreach my $si ($mstr =~ /<si.*?>(.*?)<\/si/gsm) {
            $si =~ s/<rPh[^>]*>.*?<\/rPh>//gsm;
            my $str;
            foreach my $t ($si =~ /<t.*?>(.*?)<\/t/gsm) {
                $t = $converter -> convert ($t) if $converter;
                $str .= $t;
            }

さて,パッチを当てたらモジュールをインストールする。パッチ適用したモジュールを組み込むので,ここは make を使う。Spreadsheet::XLSXArchive::Zip モジュールを前提とするので,以下はこれもあらかじめ組み込むオペレーションをも示している(すでにインストール済みなら cpan -i Archive::Zip は不要である)。

% sudo cpan -i Archive::Zip
% perl Makefile.PL
% make
% make test
% sudo make install

xlsx2csv.pl Perl コード

Excel 2007 シート CSV 一括変換 Perl プログラム xlsx2csv.pl のコードを以下に掲載する。Excel xlsx ブックファイル名を引数に取り,そのシートすべてについてセルを “|” 区切りの CSV データとして “シート名.csv” に書き出す。

#!/usr/bin/perl -w
# -*- coding: utf-8; mode: cperl; -*-
# Excel 2007 format to CSV
# 2015(c) isao yasuda, All Rights Reserved.
# $Id: xlsxtocsv.pl 164 2015-03-12 10:32:38Z isao $
# DESCRIPTION
# -----------
# - .xlsx Excel Book 2007 format を読んで,Sheet名.csv を書き出す
# - セパレータは '|'
# - 表頭(項目名)行は無視する
 
use strict;
use utf8;
use Spreadsheet::XLSX;
binmode STDOUT, ":utf8"; binmode STDERR, ":utf8";
 
($#ARGV < 0) && die "Usage: $0 xlsx-file\n";
my $fnm = $ARGV[0]; # Excel file name
my $ct = 0;         # csv file 出力行数
my $ca = 0;         # 全出力行数
my $sep = '|';
my $re = "\\" . $sep;
# Excel object
my $xlsx = Spreadsheet::XLSX->new($fnm);
utf8::decode($fnm); # 日本語ファイル名対応
 
# ワークシート毎の処理
foreach my $ws (@{$xlsx->{Worksheet}}) {
    my $wsnm  = $ws->{"Name"};
    my $mxrow = $ws->{"MaxRow"};
    my $mxcol = $ws->{"MaxCol"};
    my $csvfn = $wsnm . ".csv";
    open(CF, ">:utf8", $csvfn) || die "* $0: $csvfn open failed:$!\n";
    print STDERR "* $0: $fnm - sheet: $wsnm\n";
    if ((defined $mxrow) && (defined $mxcol)) {
        # 行毎の処理
        for (my $row = 0; $row <= $mxrow; $row++) {
            my $line = "";
            # 列毎の処理
            for (my $col = 0; $col <= $mxcol; $col++) {
                # セル値をセット
                my $cell = $ws->get_cell($row, $col);
                if (defined $cell) {
                    my $celv = $cell->value() ;
                    utf8::decode($celv);
                    $line .= $celv;
                    $line .= $sep;
                }
            }
            unless ($line =~ /^[\s$re]*$/) {  # 空白文字,セパレータのみの行は無視
                $line =~ s/$re$//;       # 末尾のセパレータを削除
                print CF "$line\n";
                $ct++;
            }
        }
    }
    close(CF);
    print STDERR "* $0: $csvfn $ct lines\n";
    $ca += $ct;
    $ct = 0;
}
print STDERR "* $0: $fnm $ca lines proceeded.\n";

処理のポイントは以下のとおりである。

  1. my $xlsx = Spreadsheet::XLSX->new("xlsxファイル名"); でブックオブジェクトを取得
  2. @{$xlsx->{Worksheet}} で得られるシートオブジェクト配列について,シート毎(変数 $ws に格納されている)にセルを参照する
  3. あらかじめ $ws->{MaxRow}(最大行)及び $ws->{MaxCol}(最大列)を取得しておき,セル参照ループの限界値として用いる
  4. セルは $ws->{Cells}[$row][$col]->{Val} でその値を取得することができる(undef でないことを確認しつつ操作すべきである)

文字コードについて

通常 Spreadsheet::XLSX は文字を UTF-8 で出力する(Excel 2007 が内部で Unicode を用いているからだと思われる)。UTF-8 で困ることはないと思われるが,互換性等のアプリケーションの要請で,出力を別の日本語コード,たとえば,Shift_JIS で出力したい場合があるかも知れない。そのときは,Spreadsheet::XLSX ブックオブジェクト生成時の第 2 引数に,Text::Iconv モジュールのコード変換ハンドラを指示すればよい。from に utf-8 を,to に shift_jis を指定してコード変換ハンドラを生成しておく。

use Text::Iconv;
my $converter = Text::Iconv->new("utf-8", "shift_jis");
my $xlsx = Spreadsheet::XLSX->new($fnm, $converter);