post Image
Go 言語で Excel ファイル内の文字列を自力で置換する

はじめに

皆さんご存知の通り Excel 2007 以降の Excel ファイルは Open XML 規格に準拠しており、その実態は XML ファイル群が zip 圧縮されたものです。

Excelファイルを zip 展開すると以下のようなファイル群が存在するのが確認できます。

a002.png

Excelの各セルに入っている文字列は、全て xl/sharedStrings.xml というファイル内に格納されています。

例えば、以下のような Excel ファイルがあるとします。

a004.png

このファイルを zip 展開して sharedStrings.xml をエディタで開くと、中身は以下のようになっています。

a006.png

さて、賢明な皆様ならお分かりいただけたと思いますが、
もし Excelファイル内の文字列をプログラムで置換したい場合

(1) Excelファイルを zip 展開

(2) xl/sharedStrings.xml を書き換え

(3) zip 圧縮し、拡張子を .xlsx にして保存

という3つの処理を順に行えば、Excel ファイル内の文字列を置換できるということになりますよね。

a008.png

このロジックに基づき、さっそく Go 言語で実装してみました。

が、いくつか困難にぶち当たりましたので、その試行錯誤した過程をお話しします。

試行錯誤 1

例えば、「Hello」という文字列を「こんにちは」に置換するとします。

これだけなら単純に sharedStrings.xml を開いて <t>Hello</t><t>こんにちは</t> に置換すれば良いだけなので、簡単に実装できそうです。

ところがこの方法では対応できないケースがあります。
例えば以下のようにセル内の文字のスタイルが複数ある場合です。

a010.png

この場合、文字列がスタイルごとに <r> 要素で分割され、以下のようになります。

a012.png

このようなケースを想定すると、単純に xml ファイル内の文字列を置換するだけでは対応できそうにありません。そこで “encoding/xml” パッケージを使い、XMLをパースすることにしました。

試行錯誤 2 「なんで Excel すぐ死んでしまうん」問題への対処

今回最も悩まされたのがこれです。
Excelファイルを生成したときに、少しでも定義設定や値をミスってしまうと、Microsoft Excel でファイルを開いたときに以下のエラーが表示されてしまうのです。

a014.png

a016.png

どのような状況で、↑ のようなエラーが表示されるのかと言うと、
例えば、sharedStrings.xml には、ルビ (フリガナ) を格納する <rPh> という要素があり、その中にルビの開始位置と終了位置を示す sb、eb という値があるのですが、この eb の値が文字列の長さを超えていたりすると「問題がある」とみなされ、上述のエラーが表示されます。

今回はこの問題を回避するため、Excel保存時に <rPh> 要素を削除してしまうことにしました。 (ルビ情報はたいして重要なデータではないので)

完成!

試行錯誤を経て、ついに成功しました!
今回書いたコードを実行すると、以下のように Excel ファイルの文字列を置換して別ファイル名で保存します。

a018.png

というわけで、コードを公開します。

main.go
package main

import (
    "encoding/xml"
    "io"
    "io/ioutil"
    "log"
    "os"
    "strings"

    "github.com/mholt/archiver"
)

type Sst struct {
    Xmlns       string `xml:"xmlns,attr"`
    Count       int    `xml:"count,attr"`
    UniqueCount int    `xml:"uniqueCount,attr"`
    Si          []Si   `xml:"si"`
}

//Si String Item
type Si struct {
    T          string     `xml:"t"`
    R          []R        `xml:"r"`
    RPh        []RPh      `xml:"rPh"`
    PhoneticPr PhoneticPr `xml:"phoneticPr"`
}
type R struct {
    RPr []RPr  `xml:"rPr"`
    T   string `xml:"t"`
}
type RPh struct {
    T  string `xml:"t"`
    Sb int    `xml:"sb,attr"` //ルビの開始位置
    Eb int    `xml:"eb,attr"` //ルビの終了位置
}
type RPr struct {
    Sz      Sz      `xml:"sz"`
    Color   Color   `xml:"color"`
    RFont   RFont   `xml:"rFont"`
    Family  Family  `xml:"family"`
    CharSet CharSet `xml:"charSet"`
}
type PhoneticPr struct {
    FontId int `xml:"fontId,attr"`
}
type Sz struct {
    Val int `xml:"val,attr"`
}
type Color struct {
    Theme int `xml:"theme,attr"`
}
type RFont struct {
    Val string `xml:"val,attr"`
}
type Family struct {
    Val int `xml:"val,attr"`
}
type CharSet struct {
    Val int `xml:"val,attr"`
}

//SearchText 文字列を検索し Si (String Item) の場所を返す
func (s Sst) SearchText(text string) int {
    result := -1
    target := ``
    for i, si := range s.Si {
        target = si.T
        for _, r := range si.R {
            target += r.T
        }
        if target == text {
            result = i
        }
    }

    return result
}

//UnMarshal XMLを入力
func UnMarshal(xmlDoc []byte) Sst {
    s := Sst{}
    xml.Unmarshal(xmlDoc, &s)
    return s
}

//Marshal XMLを出力
func (s Sst) Marshal() string {

    xTmp := s
    for i, si := range s.Si {
        //ルビ情報を削除
        xTmp.Si[i].RPh = []RPh{}

        //文字列が空の場合、R要素を削除する
        xTmp.Si[i].R = []R{}
        for _, sr := range si.R {
            if sr.T != `` {
                xTmp.Si[i].R = append(xTmp.Si[i].R, sr)
            }
        }
    }

    buf, _ := xml.MarshalIndent(xTmp, "", "  ")
    result := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + string(buf)
    result = strings.Replace(result, `<Sst `, "<sst ", -1)
    result = strings.Replace(result, `</Sst>`, "</sst>", -1)

    return result
}

const tmpDir = "tmp\\"

func main() {
    tmpPath := tmpDir + "\\tmp.xlsx"
    srcPath := "src.xlsx"
    dstPath := "dst.xlsx"

    srcText := []string{"Hello", "World"}
    dstText := []string{"こんにちは", "世界"}

    //tmpフォルダ作成
    os.MkdirAll(tmpDir, 0777)

    //元のファイルをコピーし tmp に保存
    fileCopy(srcPath, tmpPath)

    //tmpファイルをzip展開
    unzip(tmpPath)

    //展開後のsharedStrings.xml を書き換える
    replace(srcText, dstText)

    //zip 圧縮し、Excelファイルを生成
    zip(dstPath)
}

func failOnError(err error) {
    if err != nil {
        log.Fatal("Error:", err)
    }
}

//Excelファイルを指定パスに zip 展開する
func unzip(filePath string) {
    archiver.Unzip(filePath, tmpDir)
}

//XMLファイル群をzip圧縮し、Excelファイル化する
func zip(outputPath string) {
    archiver.Zip(
        outputPath,
        []string{tmpDir + "_rels", tmpDir + "docProps", tmpDir + "xl", tmpDir + "[Content_Types].xml"})
}

//文字列を置換し、その結果のExcelファイルを保存する
func replace(srcText []string, dstText []string) {
    filePath := tmpDir + "xl\\sharedStrings.xml"
    bs, err := ioutil.ReadFile(filePath)
    if err != nil {
        log.Fatal(err)
    }
    x := Sst{}
    x = UnMarshal(bs)

    var siID int

    for i, s := range srcText {
        siID = x.SearchText(s)

        if siID >= 0 {
            for j := range x.Si[siID].R {
                x.Si[siID].R[j].T = ``
            }
            x.Si[siID].T = dstText[i]
        }
    }

    s := x.Marshal()
    ioutil.WriteFile(filePath, []byte(s), os.ModePerm)
}

//ファイルを指定パスにコピーする
func fileCopy(srcPath, dstPath string) {
    src, err := os.Open(srcPath)
    if err != nil {
        panic(err)
    }
    defer src.Close()

    dst, err := os.Create(dstPath)
    if err != nil {
        panic(err)
    }
    defer dst.Close()

    _, err = io.Copy(dst, src)
    if err != nil {
        panic(err)
    }
}

終わりに

Go言語には tealeg/xlsx という非常に優れた Excel ライブラリがあるので、通常はそれを利用した方が良いでしょう。

ただ、今回のように文字列置換だけに目的が限定されている場合、自前で組んだ方が tealeg/xlsx を使うより高速に処理できるのではないかと思い、チャレンジしてみた次第です。
それではまた。


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。