📝
MS-Wordファイル(.docx)からGoのストリーム処理で文章を取り出すとハック感があって楽しい
はじめに
この記事は表題に関する遊びです。
細かな実装方法は記事中の物と違いますが、同等のコードを下記repo.に掲載しています。よろしければご参照ください。
読み出した内容を標準出力に吐き出す簡易appもあります。
go installl https://github.com/tenkoh/go-docc/cmd/docc@latest
.docxの苦悩
- 私は、私はただ文書の中身を取得したいのです。そのためだけに、わざわざMicrosoft Officeさんに頼りたくないのです。
- 大量の.docxを処理するのに、人海戦術なんて嫌なのです。バッチ処理をお手軽にやりたいのです。
このようなニーズは多々あるようで、.docxさんをハンドリングするライブラリが先達により作成されています。Goで実装されたものだと、例えば次の2つをお見かけしました。どちらもなかなか多機能です。すごいです。
閑話休題
さて、良く知られていることですが.docx
の実態はアーカイブファイルで、その内部にXML形式のファイル群を持っています。したがって単に文書の中身を取得したいだけであれば、.docx
を展開して得られるword/document.xml
をパースしてあげるだけでいけるはずです。
そうした処理ではGoのストリーム処理が火を吹きそうですね(私見)。 玄人感があって良いので無駄にトライしてみましょう
実装
.docx
ファイルをarchive/zip
で展開し、返り値の[]*zip.File
の中からファイル名がword/document.xml
の物だけを探し、*zip.File
をOpenして得られるio.ReadCloser
をxml.NewDecoder
に渡します。必要な文章情報はxmlタグのp>r>tの中身だけなので、それを結合して返したらおしまいです。
import (
"archive/zip"
"encoding/xml"
"errors"
"fmt"
"io"
"path/filepath"
)
var ErrDocumentsNotFound = errors.New("foo")
type Document struct {
XMLName xml.Name `xml:"document"`
Body struct {
P []struct {
R []struct {
T struct {
Text string `xml:",chardata"`
Space string `xml:"space,attr"`
} `xml:"t"`
} `xml:"r"`
} `xml:"p"`
} `xml:"body"`
}
func Decode(docxPath string) ([]string, error) {
archive, _ := zip.OpenReader(docxPath)
defer archive.Close()
for _, f := range archive.File {
target := filepath.Clean("word/document.xml")
if n := filepath.Clean(f.Name); n != target {
continue
}
fd, _ := f.Open()
defer fd.Close()
ps, _ := decodeXML(fd)
return ps, nil
}
return nil, ErrDocumentsNotFound
}
func decodeXML(r io.Reader) ([]string, error) {
doc := new(Document)
if err := xml.NewDecoder(r).Decode(doc); err != nil {
return nil, fmt.Errorf("could not decode the document: %w", err)
}
ps := []string{}
for _, p := range doc.Body.P {
t := ""
for _, r := range p.R {
t = t + r.T.Text
}
ps = append(ps, t)
}
return ps, nil
}
結び
途中で記載したように改善の余地しかない簡易実装ですが、これでもう.docx
を手動で開く日々からはおさらばです。おあとがよろしいようで。
Discussion