PerlでExcelデータを読む
2015/09/09
Excelファイルの種類
  • 何だかんだ言っても、一番メジャーな表計算ソフトと言えば、やはり「Microsoft Excel」でしょう。データを下さいと頼めば、所有確認など無しにExcelのデータが送られてくることも多いです。すると自然に、もらったExcelファイルからデータを読み込んで、あれこれしたいという話が出てきます。

  • 今回はPerlでExcelデータを読むことについて扱いますが、対象データについては「1byte系」で構成されたものに限定させて頂きます(*1)。

  • Excelにはファイル(拡張子の種類)が何種類かあります。代表的なものを見ると、一つはExcel 97-2003方式の「.xls」ファイル。もう一つは Excel 2007以降の「.xlsx」ファイルです。前者はバイナリファイル。後者はzip圧縮したXMLファイルです。

  • もちろん互換性の点から、新しい2007以降のExcelでも「.xls」のファイルを読むことができるので、基本的な機能(*2)しか使っていないのであれば「.xls」のフォーマットでデータを扱うのがトラブル回避に繋がると、普通は思うでしょう。

  • ところが、Excel 2007以降、VBAマクロを含んだファイルには「.xlsm」が使われるようになっています。データの形式は「.xlsx」と同じですが、その目的はマクロウイルスへの事前警戒にあるため、マクロを含んでいてもファイル名が変わらない「.xls」は、そろそろ排除される傾向にあります。

  • こういった事情から、今回は「.xls」と「.xlsx」を対象にPerlからの読み込みを検討しますが、もともと「.xls」に対して作成したスクリプトをどうやって「.xlsx」に対応させるか、ソースの流用も意識して説明したいと思います。
xlsファイルの読み込み - Spreadsheet::ParseExcel
  • xlsファイルからデータを読み込むには、CPANモジュールの Spreadsheet::ParseExcel を使います。このCPANモジュールは ActivePerlStrawberry Perl にて標準ではなく追加モジュールの扱いです。使用環境に入っていない場合、モジュールを追加してください。

  • 例として、2枚のワークシートに下記のデータが書かれているxlsファイル「test.xls」があるとします。


    ワークシート「Sheet1」のデータ。有効データ範囲に罫線あり。左上の空白セルが「A1」/ [0,0]。


    ワークシート「Sheet2」のデータ。有効データ範囲に罫線無し。左上の空白セルが「A1」/ [0,0]。

  • 上記のExcelを読むためのコーディングですが、例を見た方が早いと思うのでリストを示します。リスト中の青文字部が、ParseExcel の機能を使った記述です。ワークシート毎にセルの範囲をチェック後、各セルの値を読み込んで表示します。

    #!/usr/bin/perl -w
    use Spreadsheet::ParseExcel;
    use strict;
    
    {
        my $FileName = 'test.xls';                     # テスト用Excel(xls)ファイル名
        my $ExcelObj = Spreadsheet::ParseExcel->new(); # ParseExcelのオブジェクト定義
        
        my $Book = $ExcelObj->parse($FileName);        # xlsファイル読み込み/book扱い
        
        for my $Sheet ($Book->worksheets()) {          # worksheetオブジェクト取得
            &GetValuesFromSheet($Sheet);               # worksheetデータ取得へ
        }
    }
    
    sub GetValuesFromSheet { # wroksheetデータ取得
        my ($Sheet) = @_;
        
        print "Sheet name : ",$Sheet->get_name(),"\n"; # worksheet名表示
        
        my ($Rmin, $Rmax) = $Sheet->row_range(); # 行のデータ範囲(最小,最大)
        my ($Cmin, $Cmax) = $Sheet->col_range(); # 列のデータ範囲(最小,最大)
        
        for (my $row=$Rmin; $row<=$Rmax; $row++) {                     # rowは行番号
            for (my $col=$Cmin; $col<=$Cmax; $col++) {                 # colは列番号
                my $Cell = $Sheet->get_cell($row,$col);                # Cellオブジェクト取得
                if (defined($Cell)) {
                    print "  CellValue[$row,$col] = ",$Cell->value(),"\n"; # 値の取得/表示
                } else {
                    print "  CellValue[$row,$col] = undefined\n";
                }
            }
        }
    }
    

  • 実際に実行した結果が下記になります。値が無くても罫線等の属性があれば、get_cell(行,列) はオブジェクトを返しますが、完全に空白のセルではundefを返していることがわかります。この点は少し注意です。

    Sheet name : Sheet1
      CellValue[1,1] = Sheet1:11:Toride
      CellValue[1,2] = Sheet1:12:Tenodai
      CellValue[1,3] = Sheet1:13:Abiko
      CellValue[2,1] = Sheet1:21:Kashiwa
      CellValue[2,2] = 
      CellValue[2,3] = Sheet1:23:Matsudo
      CellValue[3,1] = Sheet1:31:Kitasenjyu
      CellValue[3,2] = Sheet1:32:Mikawashima
      CellValue[3,3] = 
      CellValue[4,1] = 
      CellValue[4,2] = 
      CellValue[4,3] = 
      CellValue[5,1] = 
      CellValue[5,2] = Sheet1:52:Nippori
      CellValue[5,3] = Sheet1:53:Ueno
    Sheet name : Sheet2
      CellValue[1,1] = Sheet2:11:Tsujido
      CellValue[1,2] = Sheet2:12:Fijisawa
      CellValue[1,3] = Sheet2:13:Ofuna
      CellValue[2,1] = Sheet2:21:Totsuka
      CellValue[2,2] = undefined
      CellValue[2,3] = Sheet2:23:Yokohama
      CellValue[3,1] = undefined
      CellValue[3,2] = Sheet2:32:Kawasaki
      CellValue[3,3] = Sheet2:33:Shinagawa
      CellValue[4,1] = undefined
      CellValue[4,2] = undefined
      CellValue[4,3] = undefined
      CellValue[5,1] = undefined
      CellValue[5,2] = Sheet2:52:Shinbashi
      CellValue[5,3] = Sheet2:53:Tokyo
    
xlsx / xlsmファイルの読み込み - Spreadsheet::ParseXLSX
  • xlsx 又は xlsmファイルからデータを読み込むには、Spreadsheet::ParseXLSX を使用します。こちらも追加モジュールになるので、使用環境に入っていない場合、モジュールを追加してください。また、PareseXLSX は PareseExcel のアダプターとして動作するので、環境としては両方のモジュールが必要になります。

  • xlsxファイルを読み込むためのCPANモジュールは他にもありますが、もともとxlsファイル用としてParseExcelを使っていたスクリプトを流用する場合、ParseXLSXが最も簡単だと考えています。

  • 下記に ParseXLSX を使用したリストを示します。ほとんど ParseExcel のコードと変わりませんが、変更部を青文字にしてあります。use文とオブジェクト定義を変更するだけですね。

    #!/usr/bin/perl -w
    use Spreadsheet::ParseXLSX;
    use strict;
    
    {
        my $FileName = 'test.xlsx';                    # テスト用Excel(xlsx)ファイル名
        my $ExcelObj = Spreadsheet::ParseXLSX->new();  # ParseXLSXのオブジェクト定義
        
        my $Book = $ExcelObj->parse($FileName);        # xlsxファイル読み込み/book扱い
        
        for my $Sheet ($Book->worksheets()) {          # worksheetオブジェクト取得
            &GetValuesFromSheet($Sheet);               # worksheetデータ取得へ
        }
    }
    
    sub GetValuesFromSheet { # wroksheetデータ取得
        my ($Sheet) = @_;
        
        print "Sheet name : ",$Sheet->get_name(),"\n"; # worksheet名表示
        
        my ($Rmin, $Rmax) = $Sheet->row_range(); # 行のデータ範囲(最小,最大)
        my ($Cmin, $Cmax) = $Sheet->col_range(); # 列のデータ範囲(最小,最大)
        
        for (my $row=$Rmin; $row<=$Rmax; $row++) {                     # rowは行番号
            for (my $col=$Cmin; $col<=$Cmax; $col++) {                 # colは列番号
                my $Cell = $Sheet->get_cell($row,$col);                # Cellオブジェクト取得
                if (defined($Cell)) {
                    print "  CellValue[$row,$col] = ",$Cell->value(),"\n"; # 値の取得/表示
                } else {
                    print "  CellValue[$row,$col] = undefined\n";
                }
            }
        }
    }
    

  • 実行結果はParseExcelと全く同じなので割愛いたします。

  • 上記リストを見るとわかるように、変更点は $ExcelObj を取得するパーサーを変えているだけなので、データ読み込みに限ると、ParseExcelで作成したスクリプトを xls と xlsx の両対応に変更するのは容易です。ParseXLSXは、parse()メソッドだけが独自ですが、parse後のデータ処理については、ParseExcelのメソッドをそのまま使っているからです。

  • ただし冒頭でも書きましたが、今回のレポートは日本語のようなマルチバイトデータが混ざったケースには対応していません。あくまで1バイトのデータでのみ構成されたファイルが対象です。

  • どうもParseXLSXのparse()メソッドに問題があるらしく、日本語を含んだデータを読み出すと、振り仮名と思われるデータが一緒にくっついてきます。デリミタっぽいものもないためローカルで処理できず、恐らくParseXLSXを解析してバグ修正版を作成し、ローカルモジュールとして自分のスクリプトから参照させるような真似をしなければならないかも...しれません。

  • このあたりの問題も対処判明したらレポートしたいと思います。
    (2015/10/21追記)
    少し荒っぽいですがどうにかしました。こちら「PerlでExcelデータを読む:日本語処理」を参照下さい。
Notes
  • ParseXLSXの日本語データ読み出し結果が想定と違っており、その対処法が思いつかないのです...フリ仮名?が...
  • ほとんどのケースにあてはまるでしょう。VBAマクロも含めて。
Copyright(C) 2015 Altmo
本HPについて