こんにちは、Juntechです。
今回はPythonとWordPress APIを使って、記事を投稿してみたいと思います。

WordPress APIについてはこちら

【投稿自動化】WordPress をAPIで操作する【はじめの一歩】

なお、Pythonはver3.7.7を使います。
それでは、早速やっていきます。

完成イメージ

今回APIで実現する投稿のイメージです。

これを実現するためには、下記を満たす必要があります。
– WordPressにマークダウンテキストを投稿する
– WordPressに画像を登録する
– 投稿にアイキャッチ画像を設定する
– 投稿にリサイズ画像を挿入する
– 投稿にリンクを挿入する
– 投稿にコードブロックを挿入する

設計してみる

ということでまずは実現方法を設計してみます。

処理フロー

こんな感じでやります。
ポイントは先に画像をPOSTして、
POSTした結果で記事内の画像パスを置換するところです。

ファイルツリー

これをやるために、先にファイルツリー(フォルダ構造)を決めておきます。

$ tree .
.
├── eyecatch-image
│   └── test-post.png
├── single
│   └── test-post.md
└── single-image
    ├── test-1.png
    ├── test-2.jpg
    └── test-3.png
└── post-wp-article.py

eyecatch-imageフォルダではアイキャッチ画像ファイルを、
singleフォルダでは記事ファイルを、
single-imageフォルダでは記事に挿入する画像ファイルを管理します。
画像の拡張子はjpg,pngの2つを想定します。

Pythonでやる理由

APIの実行だけであればShellとかでもいいのですが、
今回は画像のURLをレスポンスから受け取り、
POSTする記事の置換処理を実施する必要があります。
Pythonであればテキスト処理も簡単にできるので、Pythonを使います。
また、今後は記事の投稿をJenkinsジョブ化したいので、
今時点でスクリプト化しておくことで後々の実装も楽になります。
(Jenkinsジョブ化の記事は追って投稿予定です!)

実装してみる

素材となる記事

私は普段マークダウンで記事を作成しているので、
今回もマークダウンで書きます。

# テスト投稿
テスト投稿です。

### テスト投稿
この記事はマークダウンで書かれています。

### 盛り込む要素
#### リンクを入れる
このページの投稿にはWordPress APIを利用しています。
WordPress APIについてはこちら
【投稿自動化】WordPress をAPIで操作する【はじめの一歩】
#### 画像を入れる 画像も投稿します。 ![](../single-image/test-1.png) ![](../single-image/test-2.jpg) ![](../single-image/test-3.png) 投稿する画像はリサイズされたものにします。 #### コードブロックを入れる コードブロックも投稿できるようにします ```sql SELECT foo FROM bar ; ``` --- おわり。

余談ですが、マークダウンで記事を書くときには、
Visual Studio Code x MarkDown Preview Enhanced x Paste Image
の組み合わせが最強だと思います。

できた

早速ですが、完成版のコードです。

# -*- coding: utf-8 -*-

# コマンド:
# $ python3 post-wp-article.py ${article-name}

import os,sys,re,time,json
import chardet  # pip3 install chardet
import requests  # pip3 install requests
import glob # pip3 install glob

# エラーを吐いてExitする関数
def exit_with_error(message):
    status_sys_error = 1
    message_error = 'Error! : {}'
    print(message_error.format(message))
    sys.exit(status_sys_error)

# API叩いて失敗したらリトライする関数
def post_with_retry(data_name,url,header_obj,data_obj):
    max_error_count = 3
    sleep_second = 3
    post_success_status = 201
    response_status_code = None
    response = None
    error_count = 0
    while response_status_code != post_success_status:
        response = requests.post(url, headers=header_obj, data=data_obj)
        response_status_code = response.status_code
        if response_status_code != post_success_status:
            print('Error! :' + response.json()["message"])
            if error_count == max_error_count:
                message = 'Failed post. url={}, data_name={}.'.format(url,data_name)
                exit_with_error(message)
            else:
                error_count += 1
                print('Retry : count=' + str(error_count))
                time.sleep(sleep_second)
        else:
            print('Post Succeed: ' + data_name)
            response_json = response.json()
            return response_json

# ファイルパス・ディレクトリパス
article_name = sys.argv[1]
single_dir = './single'
article_file_path = '{}/{}.md'.format(single_dir, article_name)
eyecatch_dir = './eyecatch-image'

# APIを叩く用の設定
wp_authorization_string = os.environ['WP_AUTHORIZATION_STRING']
url_post_media = 'https://autohacks.net/wp-json/wp/v2/media'
url_post_single = 'https://autohacks.net/wp-json/wp/v2/posts'

# 記事ファイルの存在チェック
if os.path.exists(article_file_path) != True:
    message = '{} is not found.'.format(article_file_path)
    exit_with_error(message)

# 記事ファイルの文字コードチェック(UTF-8でなければエラー)
with open(article_file_path, mode='rb') as f:
    charset = chardet.detect(f.read())['encoding']
    if charset != 'utf-8':
        message = 'charset of {} must be utf-8, but detected {}.'.format(
            article_file_path, charset)
        exit_with_error(message)

# 記事ファイル内で画像パスが1行に2件以上存在しないことをチェック
# 問題なければ画像パスのディクショナリを作成
violation_list = []
image_file_dict = {}
with open(article_file_path, mode='r') as f:
    article = f.readlines()
    for i, line in enumerate(article):
        image_file = re.findall(r'!\[\]\(([^\):]+\.(jpg|png))\)', line)
        if len(image_file) == 1:
            image_file_dict[i] = {'local': image_file[0][0]}
        elif len(image_file) > 1:
            violation_list.append(i)
if len(violation_list) > 0:
    for i in violation_list:
        line_num = i + 1
        print('line {} has 2 or more image files.'.format(line_num))
    message = 'md-article violate writing rule.'
    exit_with_error(message)

# 画像ファイル存在チェック
non_existence_list = []
for image_file_index in image_file_dict:
    image_file_local = image_file_dict[image_file_index]["local"]
    image_file_path = '{}/{}'.format(single_dir, image_file_local)
    if os.path.exists(image_file_path) != True:
        non_existence_list.append(image_file_path)

if len(non_existence_list) > 0:
    for non_existence_file in non_existence_list:
        print('{} is not found.'.format(non_existence_file))
    message = 'some image files are not found.'
    exit_with_error(message)

# アイキャッチ画像ファイル存在チェック
if len(glob.glob('{}/{}'.format(eyecatch_dir, article_name + '.*'))) == 0:
    message = 'eycatch image file is not found.'
    exit_with_error(message)

# 記事ファイル内の画像をPOST
for image_file_index in image_file_dict:
    image_file_local = image_file_dict[image_file_index]["local"]
    image_file_path = '{}/{}'.format(single_dir, image_file_local)
    file_name = os.path.basename(image_file_path)
    with open(image_file_path, mode='rb') as f:
        headers = {
            'Authorization': 'Basic {}'.format(wp_authorization_string),
            'Content-Type': 'application/octet-stream',
            'Content-Disposition': 'attachment; filename="{}"'.format(file_name)
        }
        image_data = f.read()
        response = post_with_retry(image_file_path,url_post_media,headers,image_data)
        media_source_path = response["source_url"]
        media_medium_path = response["media_details"]["sizes"]["medium"]["source_url"]
        image_file_dict[image_file_index]["source"] = media_source_path
        image_file_dict[image_file_index]["medium"] = media_medium_path

# アイキャッチ画像をPOST
eyecatch_image_file_path = glob.glob('{}/{}'.format(eyecatch_dir, article_name + '.*'))[0]
eyecatch_image_file_name = os.path.basename(eyecatch_image_file_path)
eyecatch_image_file_id = ''
with open(eyecatch_image_file_path, mode='rb') as f:
    headers = {
        'Authorization': 'Basic {}'.format(wp_authorization_string),
        'Content-Type': 'application/octet-stream',
        'Content-Disposition': 'attachment; filename="{}"'.format(eyecatch_image_file_name)
    }
    image_data = f.read()
    response = post_with_retry(eyecatch_image_file_path,url_post_media,headers,image_data)
    eyecatch_image_file_id = response["id"]

# 記事をPOST
with open(article_file_path, mode='r') as f:
    article = f.readlines()
    # 記事内の画像挿入箇所にWordPress上の画像パスをセット
    for image_file_index in image_file_dict:
        image_file_path_local = image_file_dict[image_file_index]["local"]
        image_file_path_medium = image_file_dict[image_file_index]["medium"]
        image_file_path_source = image_file_dict[image_file_index]["source"]
        text_before = "![]({})".format(image_file_path_local)
        text_after = "[![]({})]({})".format(image_file_path_medium, image_file_path_source)
        article[image_file_index] = article[image_file_index].replace(text_before, text_after)
    # 記事POST
    headers = {
        'Authorization': 'Basic {}'.format(wp_authorization_string),
        'Content-Type': 'application/json',
    }
    title = article[0].replace('# ','')
    # 記事の1行目(=タイトル行)を削除
    del article[0]
    content = ""
    for line in article:
        content += line
    body = {
        "slug": article_name,
        "title": title,
        "content": content,
        "featured_media" : eyecatch_image_file_id,
        "status": "draft"
    }
    post_with_retry(article_file_path,url_post_single,headers,json.dumps(body))
print('Success!')

コードの解説をしつつ完成させる予定でしたが、
長いので、次回で解説をしたいと思います。

ということで早速実行してみます。

$ python3 post-wp-article.py test-post
Post Succeed: ./single/../single-image/test-1.png
Post Succeed: ./single/../single-image/test-2.jpg
Post Succeed: ./single/../single-image/test-3.png
Post Succeed: ./eyecatch-image/test-post.png
Post Succeed: ./single/test-post.md
Success!

無事正常終了しました。

投稿した記事を管理画面から確認する

管理画面から投稿を確認すると、
先ほどPOSTした記事が下書きで反映されています。

カテゴリーは指定していないので、
デフォルトで設定したものになっています。

投稿された記事の中身です。

テスト投稿です。

### テスト投稿
この記事はマークダウンで書かれています。

### 盛り込む要素
#### リンクを入れる
このページの投稿にはWordPress APIを利用しています。
WordPress APIについてはこちら
【投稿自動化】WordPress をAPIで操作する【はじめの一歩】
#### 画像を入れる 画像も投稿します。 [![](https://autohacks.net/wp-content/uploads/2021/01/test-1-300x300.png)](https://autohacks.net/wp-content/uploads/2021/01/test-1.png) [![](https://autohacks.net/wp-content/uploads/2021/01/test-2-300x235.png)](https://autohacks.net/wp-content/uploads/2021/01/test-2.png) [![](https://autohacks.net/wp-content/uploads/2021/01/test-3-300x225.png)](https://autohacks.net/wp-content/uploads/2021/01/test-3.png) 投稿する画像はリサイズされたものにします。 #### コードブロックを入れる コードブロックも投稿できるようにします ```sql SELECT foo FROM bar ; ``` --- おわり。

画像パスがWordPress上のパスにできています。
リンクやコードブロックも問題なく挿入できていました。

プレビューしてみると…

できてますね!


今回はここまで。
次回はコードの解説をしたいと思います。