fatsheep's memo.txt

気になったこと、試したこと、その他もろもろを書いていきます。

MarkdownをはてなブログにポストするPythonスクリプト メモ

はてなブログのTipsです。

CTFのWriteupなど長い記事をポストするときは、MarkdownエディタのTyporaを使っていったんローカルで作成して、ほぼ完成したらはてなにアップしています。

画像が多くなるとアップロードも大変なので、スクリプトを作成しました。エラーハンドリングやバグチェックなど細かく行えていないので、使用される際は自己責任でお願いします。(バグのご指摘は大歓迎です。)

スクリプト

import datetime
import random
import hashlib
import base64
import requests
import time
import mimetypes
import os
import xml.etree.ElementTree as ET
import re
import sys

END_POINT = ''
username = ''
api_key = ''


def create_wsse(username, password):
    created = datetime.datetime.now().isoformat() + "Z"
    b_nonce = hashlib.sha1(str(random.random()).encode()).digest()
    b_digest = hashlib.sha1(b_nonce + created.encode() + password.encode()).digest()
    s = 'UsernameToken Username="{0}", PasswordDigest="{1}", Nonce="{2}", Created="{3}"'
    return s.format(username, base64.b64encode(b_digest).decode(), base64.b64encode(b_nonce).decode(), created)

def post_photo(username, api_key, filepath):
    with open(filepath, "rb") as f:
        b_photo_data = f.read()
    
    b64_photo_data = base64.b64encode(b_photo_data).decode()
    content_type = mimetypes.guess_type(filepath)[0]
    
    post_data = '''<entry xmlns="http://purl.org/atom/ns#">
                   <title></title>
                   <content mode="base64" type="''' + content_type + '''">''' + b64_photo_data + '''</content>
                   <generator>Hatena Blog</generator>
                   </entry>'''
    
    wsse = create_wsse(username, api_key)
    r = requests.post("https://f.hatena.ne.jp/atom/post", data=post_data, headers={'X-WSSE': wsse})
    if r.status_code == 201:
        r_xml = ET.fromstring(r.text)
        photo_id = r_xml.find('{http://www.hatena.ne.jp/info/xmlns#}syntax').text.split(':')[3]
        return photo_id
    else:
        print("写真の投稿に失敗しました" + filepath)
        return ""


def del_photos(username, api_key, photo_ids):
    wsse = create_wsse(username, api_key)
    
    print("--- Delete Photos ---")
    
    for id in photo_ids:
        r = requests.delete("https://f.hatena.ne.jp/atom/edit/" + id[:-1], headers={'X-WSSE': wsse})
        if r.status_code == 200:
            print(id + " : Deleted")
        else:
            print(id + " : Delete Failed")


def get_entry_data(title, content):

    TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
    <entry xmlns="http://www.w3.org/2005/Atom"
           xmlns:app="http://www.w3.org/2007/app">
      <title>{title}</title>
      <author><name>{name}</name></author>
      <content type="text/plain"><![CDATA[{content}]]></content>
      <updated></updated>
      <app:control>
        <app:draft>yes</app:draft>
      </app:control>
    </entry>
    """

    #テンプレにtitle, name, contentを挿入
    entry = TEMPLATE.format(
            title = title,
            name = username,
            content = content,
            draft = "yes"
            )

    return entry


def post_entry_hatena(END_POINT, username, api_key, entry):
    print("--- Post Blog Entry ---")
    wsse = create_wsse(username, api_key)
    r = requests.post(END_POINT + "/entry", data=entry.encode('utf-8'), headers={'X-WSSE': wsse})
    if r.status_code == 201:
        print(u"投稿しました")
    else:
        print(u"投稿に失敗しました")
        print(r.text)



if(__name__ == "__main__"):
    
    if(len(sys.argv) == 1):
        exit()
    
    md_path = sys.argv[1]
    
    print('Input Markdown file : ' + md_path)
    print('Press enter key to continue')
    input()
    
    photo_ids = []
    md_path_dir = os.path.dirname(md_path) + '\\'
    
    md_data = open(md_path, "rb").read().decode()
    print("--- Post Photos ---")
    cnt = 1
    md_ppaths = re.findall(r'!\[.*\]\(.*\)', md_data)
    for md_ppath in md_ppaths:
        ppath = md_path_dir + re.findall(r'!\[.*\]\((.*)\)', md_ppath)[0].replace('/','\\')
        
        if(os.path.exists(ppath)):
            print(str(cnt) + "/" + str(len(md_ppaths)) + " : " + ppath)
            cnt += 1
            
            res = post_photo(username, api_key, ppath)
            if(res != ""):
                md_data = md_data.replace(md_ppath, '[f:id:' + username + ':' + res + ':plain]')
                photo_ids.append(res)
            else:
                del_photos(username, api_key, photo_ids)
                
                print('Press enter key to continue')
                input()
                
                exit()
    
    post_entry_hatena(END_POINT, username, api_key, get_entry_data(os.path.splitext(os.path.basename(md_path))[0], md_data))
    
    open(md_path + '_output.txt', "wb").write(md_data.encode('utf-8'))
    
    print('Press enter key to continue')
    input()
    
    exit()

補足

Markownファイルをそのままブログにポストするため、はてなブログの編集モードはMarkdownモードになっている必要があります。

エンドポイント、APIキーは、ブログ設定→詳細設定から確認できます。 f:id:fatsheep:20220113092945p:plain

動作の補足です。

  • コマンドラインオプションで指定したMDファイルを処理します。
  • ブログをポストする前に、画像をフォトライフにアップロードします。画像のアップロードが1つでも失敗した場合、すべての処理は中断され、それまでアップロードした写真はすべて削除します。
  • ブログは下書き(draft)にアップロードされます。
  • ポストしたmarkdownは入力したMDファイルと同じフォルダに、「(MDファイルのファイル名)_output.txt」というファイル名で保存されます。

参考

pythonでwsse認証を用いて、はてなブログにエントリーを投稿する - Qiita

Python3→はてなフォトライフへ画像のアップロード - lisz-works

はてなブログAtomPub - Hatena Developer Center