WordPress

プレゼン資料からWordPressへ投稿する

こんばんはcrz33です。

今回は、プレゼン資料からWordPressへ投稿するツールを作ってみたので紹介します。

ツールの内容

ツールはKeynoteのプレゼン資料とMarkdownで文章を作成し、その2つからWordPressへ記事を投稿するものです。

工夫点としては次があります。

  • WordPress REST APIを調べ記事の投稿や更新
  • アイキャッチ画像にも対応
  • アイキャッチ以外は通常のFTPでアップロード

興味のある人は最後まで一読ください。

ツールの全体

(1)画像抽出変換ツールは、Keynoteファイルから各スライドを画像1枚1枚に抽出します。

(2)Wordpress変換ツールは、MarkdownをWordpress投稿記事向けにHTMLに変換します。(1)の画像を本文に貼り付けます。(1)の画像もWordPress向けに圧縮します。

(3)Wordpressアップロードツールは、できあがった画像とHTMLをWordPressへアップロードします。記事や画像の命名ルールを決め、記事の更新もできるようにします。

Keynoteファイルから画像抽出

Keynoteで作成したプレゼン資料から画像へエクスポートはAppleScriptで簡単にできます。

Googleで「AppleScript Keynote Export」で検索すればいろいろサンプルがでてきます。

このサイトがおすすめです。使用例といくつかサンプルがあるので試してみるとよいです。

今回のツールでは、画質は最高としたJPEG形式でエクスポートします。

そのためのスクリプトは次のようになります。

tell application "Keynote"
  set keynote_file to open ("{keynote_path}" as POSIX file)
  export keynote_file to ("{out_path}" as POSIX file) as slide images with properties {{ compression factor: 1.0, image format: {out_format} }}
  close keynote_file saving no
end tell

画像ファイルは指定した出力パスにディレクトリが作られ、ディレクトリ名、3桁の数値、拡張子で作成されます。

このスクリプトファイルを作って osascript の引数にファイルパスを指定すれば実行できますが、今回はAppleScriptをPythonスクリプトに閉じ込められるように工夫しました。作った関数のソースは次のようになります。

def create_image():

    # 絶対パス変換
    keynote_path = os.path.abspath(KEYNOTE_FILE)
    out_path = os.path.abspath(IMG_OUTPUT_DIR)

    # 実行するスクリプト
    myscript = u"""tell application "Keynote"
    set keynote_file to open ("{keynote_path}" as POSIX file)
    # noqa: ignore=E501
    export keynote_file to ("{out_path}" as POSIX file) as slide images with properties {{ compression factor: 1.0, image format: {out_format} }}
    close keynote_file saving no
    end tell
    """.format(
        keynote_path=keynote_path, out_path=out_path, out_format="jpeg"
    )
    args = [
        item
        for x in [("-e", l.strip()) for l in myscript.split("\n") if l.strip() != ""]
        for item in x
    ]
    proc = subprocess.run(
        ["osascript"] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    if proc.returncode != 0:
        raise Exception("[create_image] ERR : " + proc.stderr.decode("utf-8"))

    return proc.returncode

画像ファイルのWordPress向け変換

Keynoteから画像を抽出する際には最高画質にしました。これはこのあとYoutube用の動画にも使いたいためです。

WordPressに投稿するブログ記事だと差し込み画像に使うだけなので、画質は低くて構いません。そこで、アップロードように画像を圧縮します。

画像圧縮には画像処理ライブラリPillowを使います。このライブラリはOpenCVのような高度な画像処理ではなく簡単なことであれば適しています。

画像は100KBくらいに抑えたかったので、品質は50にしました。スクリプトとしては単純でファイル読んで品質を変えて保存するだけです。ついでに、ゼロ埋めの3桁数値のファイル名にしていきます。

作った関数のソースは次のようになります。

from PIL import Image
def compress_img():
    images = glob.glob(KEYNOTE_IMG_FILE)
    images.sort()
    for i, org_img_path in enumerate(images):
        img = Image.open(org_img_path)
        img.save(IMG_FILE.format(NNN=str(i).zfill(3)), quality=50)

MarkdownのWordPress向け変換

Markdown形式で書いた文章をHTMLへ変換します。

変換にはシンプルなマークダウンパーサ misutune を使います。 misutune はデフォルトでHTMLへの変換するレンダラーも用意されています。必要なAPIのみオーバーライドすれば、出力するHTMLをカスタマイズできます。

Markdownで書いた文章をブログにどういう構成で乗せるかは次のとおりです。

レベル1のヘッダはブログのタイトルに、レベル2のヘッダは各章のタイトルになります。
スライドの1番目の画像はアイキャッチ画像に使い、それ以降のスライドはレベル2のヘッダの直後に差し込み画像としてリンクを貼ります。

レベル3以降のヘッダは無視しますが、スライド画像のリンクを差し込むだけに利用します。

これらの構成を考えて作った WordPress 用のレンダラーは次のようになります。

class WordPressRenderer(mistune.Renderer):
    def init(self, project_name):
        self.page_count = -1
        self.project_name = project_name

    def header(self, text, level, raw=None):
        self.page_count = self.page_count + 1
        url = "/wp-content/uploads/image/{project_name}/{NNN}.jpeg".format(
            project_name=self.project_name, NNN=str(self.page_count).zfill(3)
        )
        if level == 1:
            self.title = text
            return ""
        elif level == 2:
            return "{header}<a href='{href}' class='fancybox image'><img src='{img}'/></a>".format(
                header=super().header(text, level, raw), href=url, img=url
            )
        else:
            return "<a href='{href}' class='fancybox image'><img src='{img}'/></a>".format(
                href=url, img=url
            )

    def getTitle(self):
        return self.title

getTitle API は、のちにWordPressに投稿する際に必要なタイトルを抽出するためです。

レンダラーができれば、変換するコードはシンプルです。Markdown形式のファイルを開いて mistune を使って変換するだけです。そのコードは次のようになります。

def make_html():
    project_name = pathlib.Path(os.path.abspath(".")).name
    renderer = WordPressRenderer(hard_wrap=False)
    renderer.init(project_name)
    md = mistune.Markdown(renderer=renderer)
    with open(MARKDOWN_FILE, "r") as f, open(HTML_FILE, "w") as fo:
        fo.write(md(f.read()))
    return renderer.getTitle()

WordPressへ画像をアップロード

WordPressを公開しているサーバへFTPでアップロードします。アップロードが必要か、FTPでアップロードが可能なのかなどは、どのようにWordPressを運用しているかによります。私はレンタルサーバでWordPressを運用しており、FTPでファイルを転送できるので、今回は画像をFTPで送るツールを作りました。

また、 WordPress の管理ツールのメディアアップロードには頼らず、画像をWordPressで静的コンテンツを公開しているフォルダへアップロードします。

FTPするので多少長いですが、簡単なコードで実現できます。そのコードは次のようになります。

from ftplib import FTP
def upload_image():
    project_name = pathlib.Path(os.path.abspath(".")).name
    images = glob.glob(IMG_FILE.format(NNN="*"))
    images.sort()
    ftp = FTP(
        os.environ["FTP_HOST"], os.environ["FTP_USER"], os.environ["FTP_PASSWORD"]
    )
    project_dir = os.environ["FTP_IMAGE_DIR"].format(project_name=project_name)
    try:
        ftp.mkd(project_dir)
    except Exception as e:
        print(e)

    for img_path in images:
        filename = pathlib.Path(os.path.abspath(img_path)).name
        with open(img_path, "rb") as f:
            ftp.storbinary(
                "STOR {project_dir}/{filename}".format(
                    project_dir=project_dir, filename=filename
                ),
                f,
            )
    ftp.close()

WordPressをアイキャッチ画像をアップロードする

アイキャッチ画像に使う画像はFTPでアップロードしたものは使えません。WordPressの管理ツールでメディアアップロードしたものだけです。記事へのアイキャッチ画像の指定の仕方は後で伝えますが、この理由からアイキャッチ用の画像のみ WordPress REST API でメディアアップロードします。

WordPress REST API のメディア用の URL は、 wp-json/wp/v2/media です。

POST するデータは、実際の画像データと画像用のヘッダだけでシンプルです。レスポンスは WordPress からいくつか返ってきますが、重要なのは id です。これが、 WordPress で採番されたもので、記事にしていするアイキャッチ画像の指定に必要になります。

あとは、何度も同じアイキャッチ画像をアップロードしないように、次のように考えました。

  • アップロードされたアイキャッチ画像の id をファイルに保存
  • そのファイルが存在すれば、アップロードしない
  • もう一度アップロードしたくなったらファイルを消して、管理画面からアップロードした画像も消して実行

これらを考えて実装したコードは次のとおりです。

def upload_eyecatch():

    # 作成済みならIDを返すだけ
    if os.path.exists(EYECATCH_ID):
        with open(EYECATCH_ID, "r") as f:
            return int(f.read())

    # 作っていく
    project_name = pathlib.Path(os.path.abspath(".")).name
    headers = {
        "Content-Disposition": "attachment; filename={file_name}.jpeg".format(
            file_name=project_name
        ),
        "Content-Type": "image/jpeg",
    }

    # メディアアップロード
    with open(EYECATCH_FILE, "rb") as f:
        res = requests.post(
            urljoin(os.environ["WP_URL"], "wp-json/wp/v2/media"),
            data=f.read(),
            headers=headers,
            auth=(os.environ["WP_USER"], os.environ["WP_PASS"]),
        )

    # 成功したら記録
    if res.status_code == 201:
        id = json.loads(res.content.decode("utf-8"))["id"]
        with open(EYECATCH_ID, "w") as f:
            f.write(str(id))
        return id
    else:
        raise Exception("ERR [upload_eyecatch] HTTP STATUS {0}".format(str(id)))

WordPressにHTMLを投稿する

ここまで長くなりましたが、最後に WordPress に記事を投稿します。

投稿時に指定できるオプションはいくつかありますが、今回は1年間運用して使ってきている次のものだけにしています。

  • featured_media = アイキャッチ画像の id
  • slug = 記事の URL に使う識別子
  • categories = カテゴリ
  • title = 記事のタイトル
  • content = HTML コンテンツ
  • status = 記事の状態、 draft 固定

新規投稿と更新する URL が異なるので使い分けが必要です。

  • 新規投稿用は wp-json/wp/v2/posts
  • 更新する場合は wp-json/wp/v2/posts/{post_id}

アイキャッチ画像のメディアアップロードと同じように一度投稿したらファイルに id を控えておき、2回目以降はそのファイルがあれば、更新するようにしました。

これらを考えて実装したコードは次のとおりです。

def upload_html(title: str, media_id: int):
    project_name = pathlib.Path(os.path.abspath(".")).name
    # コンテンツ(HTML)
    with open(HTML_FILE, "r") as f:
        contents = "".join(f.readlines())
    # カテゴリ
    category_ids = []
    if os.path.exists(CATEGORY_ID):
        with open(CATEGORY_ID, "r") as f:
            category_ids = [int(f.readline())]

    payload = {
        "featured_media": media_id,
        "slug": project_name,
        "categories": category_ids,
        "title": title,
        "content": contents,
        "status": "draft",
    }

    # 投稿抑止
    post_id = -1
    if os.path.exists(POST_ID):
        with open(POST_ID, "r") as f:
            post_id = int(f.readline())

    # 追加か更新でURLを切り替える
    if post_id < 0:
        url = urljoin(os.environ["WP_URL"], "wp-json/wp/v2/posts")
    else:
        url = urljoin(
            os.environ["WP_URL"],
            "wp-json/wp/v2/posts/{post_id}".format(post_id=post_id),
        )

    res = requests.post(
        url,
        headers={"Content-type": "application/json"},
        data=json.dumps(payload),
        auth=(os.environ["WP_USER"], os.environ["WP_PASS"]),
    )
    # 成功したら記録
    if res.status_code == 201:
        id = json.loads(res.content.decode("utf-8"))["id"]
        with open(POST_ID, "w") as f:
            f.write(str(id))
        return id
    elif res.status_code == 200:
        pass
    else:
        raise Exception("ERR [upload_eyecatch] HTTP STATUS {0}".format(str(id)))

まとめ

次の作業のPythonスクリプトを紹介しました。

  • Keynoteから画像へエクスポート
  • Markdown形式からWordPress向けHTMLへ変換
  • 画像をWordPressへアップロード
  • アイキャッチ画像としてWordPressへ画像をアップロード
  • HTMLを記事としてWordPressへ投稿

お気づきと思いますが、この記事もこのツールを使っています。

次回は、同じ流れでYoutubeへ動画投稿するツールを作って紹介したいと思います。

ABOUT ME
crz33
アラフォーのシステムエンジニアです。 それなりの規模のSI会社でいろんなプロジェクトを渡り歩き、 立場もプログラマー、アーキテクト、プロジェクトリーダーと経験してきました。 今はプロジェクトマネージャーとして活躍?中です。 早期リタイアを目指し、アフィリエイトで稼げないかと奮闘中です。

COMMENT

メールアドレスが公開されることはありません。