pptxをPythonでハックして業務効率化した話

# なぜこの記事を書いたか
同じようなpptxを何度も書く必要があり、精神が疲弊してきたため。
あと、Excelで原寸画像を取り出す方法としてzip化するものがあるが、Powerpointでも可能で少し感動したため。
一応pythonでpptxを扱うライブラリもあるんだけどドキュメントみるのが面倒。簡単な文章操作ならxmlいじった方が楽だと思ったため。

# やること
1. pptxの雛形を作り、拡張子を".zip"に変更する。
2. 元のファイル名/ppt/slides 内のslidex.xmlを置換し、書き換える。
3. 元のファイル名/ppt/media/imagex.png などを別の画像ファイルで置換する。
4. 再びzipとして圧縮し、拡張子を".pptx"に変更
5. 完了

# pptxの構造
内容がどうなっているかを解説しようと思います。
### フォルダ内容
.zipにして解凍した後のフォルダ内容は以下のようになります。

```
pptx名/
  _rels/
    .rels
  docProps/
    app.xml
    core.xml
    thumbnail.jpeg
  ppt/
    _rels/
    media/ :画像ファイルが入ってます(スライドマスタのものも含む)
      image1.png
      image2.png
      ...
    slideLayouts/
    slideMasters/
    slides/ :スライドの内容の核
      _rels/
      slide1.xml
      slide2.xml
      ...
    theme/
    presentation.xml
    presProps.xml
    tableStyles.xml

    viewProps.xml
  [Content_Types].xml
```

めんどくさかったのですべて書いてません+作ったファイルによって構成が違う場合があります。

mediaフォルダ内の画像はpptx内に貼り付けられている画像の原寸。こいつを入れ替えてやればpptx内の画像も入れ替わります。
slidesフォルダ内のxmlは以下のような感じになっており、pptxに書かれた文章はそのまま見ることができます。

![スライド1.PNG](https://qiita-image-store.s3.amazonaws.com/0/334824/9f474730-179d-8a1a-86ff-dee60c76bdc8.png)
こちらが作った雛形スライド

``` xml:slide1.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="2" name="テキスト ボックス 1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="631767" y="515390"/><a:ext cx="6483928" cy="707886"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr><p:txBody><a:bodyPr wrap="square" rtlCol="0"><a:spAutoFit/></a:bodyPr><a:lstStyle/><a:p><a:r><a:rPr kumimoji="1" lang="en-US" altLang="ja-JP" sz="4000" dirty="0" err="1" smtClean="0"/><a:t>hogehoge</a:t></a:r><a:endParaRPr kumimoji="1" lang="ja-JP" altLang="en-US" sz="4000" dirty="0"/></a:p></p:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="3" name="テキスト ボックス 2"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="631767" y="1715194"/><a:ext cx="6483928" cy="1477328"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr><p:txBody><a:bodyPr wrap="square" rtlCol="0"><a:spAutoFit/></a:bodyPr><a:lstStyle/><a:p><a:r><a:rPr kumimoji="1" lang="en-US" altLang="ja-JP" dirty="0" smtClean="0"/><a:t>HOGEHOGE</a:t></a:r></a:p><a:p><a:endParaRPr lang="en-US" altLang="ja-JP" dirty="0"/></a:p><a:p><a:r><a:rPr kumimoji="1" lang="en-US" altLang="ja-JP" dirty="0" smtClean="0"/><a:t>FUGAFUGA</a:t></a:r></a:p><a:p><a:endParaRPr lang="en-US" altLang="ja-JP" dirty="0"/></a:p><a:p><a:r><a:rPr kumimoji="1" lang="en-US" altLang="ja-JP" dirty="0" err="1" smtClean="0"/><a:t>testMessage</a:t></a:r><a:endParaRPr kumimoji="1" lang="ja-JP" altLang="en-US" dirty="0"/></a:p></p:txBody></p:sp><p:pic><p:nvPicPr><p:cNvPr id="4" name="図 3"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="rId2"><a:extLst><a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}"><a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/></a:ext></a:extLst></a:blip><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="8454076" y="5086317"/><a:ext cx="1619333" cy="1257365"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic></p:spTree><p:extLst><p:ext uri="{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}"><p14:creationId xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="1097765679"/></p:ext></p:extLst></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>
```

こちらが上のスライドの中身。いろいろ書いてありますが"HOGEHOGE"とかがそのまま入ってるのがわかればOKです。これを置換すればいいわけですね。
1行目がxmlの情報。2行目が内容となっており、形式はこれで固定のようなのでpythonで読みやすいです。
置換することで何らかの弊害が出るかもしれませんがその時は頑張ってください。

# 作ったPythonスクリプト
本題に移ります。先ほどのスライドの文章を置換したいと思います。
### 前提
Python: 3.6.5

### ファイル構成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/334824/f04a7e5d-d956-0b31-17ce-01f79b9458a5.png)
test.pptxを中身変換してoutputフォルダに入れます。
あとスライド内に貼られたRのアイコンをPythonのものに変更します。
### スクリプト

``` python:myLibrary.py
# -*- coding: utf-8 -*-
"""
Created on Mon Aug 27 16:53:26 2018
python3以降用なので注意
@author: Ark496
"""
import codecs
import os
import shutil
import glob
import zipfile

#pptxをzip化し、中身を編集するための準備
def pptxUnzip(pptxPath,outputfd):
    #ひな形pptxをコピー
    powerpointPath = pptxPath
    #出力フォルダ作成
    outputFd = outputfd+"/result"
    try:
        os.mkdir(outputFd)
    except FileExistsError:
        pass
    #rename & 解凍
    zipName = powerpointPath.replace(".pptx","_copy.zip")
    #pptxのあるフォルダにzipを作成
    shutil.copyfile(powerpointPath,zipName)
    #解凍結果をoutputfdに出力
    with zipfile.ZipFile(zipName,'r') as inputFile:
        inputFile.extractall(outputFd)
    #zipは不要なので削除
    os.remove(zipName)

#pptxのxmlを編集
def pptxTextEdit(outputfd,information,slideNum):
    """
    information :置換前と置換後の文字列を代入
    outputted: 出力先のフォルダ
    slideNum: 何枚目のスライドを編集するか
    """
    outputFd = outputfd+"/result"
    #xmlファイルの場所
    slidefn = outputFd + "/ppt/slides/slide"+str(slideNum)+".xml"
    #slideの内容を見に行く
    text = []
    with codecs.open(slidefn,"r",'utf-8') as f:
        for row in f:
            text.append(row)
    
    #text[1]を編集
    for info in information:
        text[1] = text[1].replace(info[0],str(info[1]))#置換    

    #上書き保存
    with codecs.open(slidefn,"w",'utf-8') as f:
        for txt in text:
            f.write(txt) # シーケンスが引数。

#画像の置換
def pptxPicEdit(outputfd,picInfo):
    """
    picInfo:
        pptx側: "image7.png"のように置換する画像の名前
        入力画像側: "C:\...\picture.png"のように置換後の画像のパスを入力
    """
    outputFd = outputfd+"/result"
    #図のすげ替え
    #スライド側の置換する図
    for pic in picInfo:
        pptxpicpath = outputFd + "/ppt/media/"+pic[0]
        picpath = pic[1]
        try:
            shutil.copyfile(picpath,pptxpicpath)
        except FileNotFoundError:
            pass
#置換し終わったファイルをpptxにする
def pptxZip(outputfd):
    outputFd = outputfd+"/result"
    
    #現在のフォルダを記録
    nowDir = os.getcwd()
    
    #フォルダ移動して書き込み
    os.chdir(outputFd)        
    #圧縮
    zipFile = zipfile.ZipFile('./../result.zip', 'w', zipfile.ZIP_DEFLATED)
    #圧縮するファイル群を取得
    fileList = glob.glob("./**", recursive=True)
    #なぜか入らないので追加。"."で始まっているため？
    fileList.append(".\_rels/.rels")
    for i in range(len(fileList)):
        fileList[i]= fileList[i].replace(outputFd,"./")
        zipFile.write(fileList[i])
    zipFile.close()

    #改名
    shutil.copyfile('./../result.zip','./../slide.pptx')
    #中間ファイル削除
    os.remove("./../result.zip")
    os.chdir("./../")
    shutil.rmtree("./result")
    
    #フォルダ移動を戻す
    os.chdir(nowDir)

if __name__ == '__main__':
    print("myLibrary.py")
    pass
```


``` python:testpptx.py
import myLibrary as mylib

if __name__ == "__main__":
    #編集したいpptxの入っているフォルダ
    pptxPath = "C:/Users/Desktop/pptxTest/test.pptx"
    #出力したいフォルダ
    outputFd = "C:/Users/Desktop/pptxTest/output"

    #置換テーブル
    #textのほう
    tabletxt1 = [["HOGEHOGE" ,"ほげほげ"],["FUGAFUGA","ふがふが"],["testMessage",""]]
    #pictureのほう
    tablepic1 = [["image1.png","C:/Users/Desktop/pptxTest/picture/python.png"]]
    
    #pptxをzipに改名して解凍
    mylib.pptxUnzip(pptxPath,outputFd)
    #xmlを変更してテキスト内容を変更(1枚目のスライド)
    mylib.pptxTextEdit(outputFd,tabletxt1,1)
    #2枚目以降のスライドを変更するときにはこちらに追加

    #画像の置換
    mylib.pptxPicEdit(outputFd,tablepic1)
    #再び圧縮してpptx化
    mylib.pptxZip(outputFd)
```

###注意点
* path入力の時に一度は自分で中身を見てスライド番号とか画像番号を見る必要がある。
* スライドを作る時に画像をコピーしていたりすると、同じ画像番号で参照されているものが2つ以上になる可能性がある。
* 置換する時に困るので、雛形pptxの"HOGEHOGE"などの文字列は独立なものにしましょう。

### 結果
![slide.png](https://qiita-image-store.s3.amazonaws.com/0/334824/4a159ba7-7aec-f1dd-7cc7-387118838fd4.png)

置換できました。画像の場合は表示される大きさがそのまま(縦横比も自動的に変更される)ので画像サイズは確認しておきましょう。

#使用例
* 複数のexcelファイルなどから内容をコピペした資料を作らなければならない
* 同じフォーマットの画像を複数回貼らなければならない

テンプレを同じくするスライドが大量であればあるほど楽になります。


#感想
これで90枚くらいスライド作ったので全部手書きでやってたら疲れただろうなーと思う。
Qiita結構書きやすい。
Excelとかで画像データ送ってくる文化は即刻根絶やしにすべきだと思う。