Pelicanからはてなブログへの移行

Posted by 技術ブログ by Strawhat.net on Wednesday, October 14, 2015

はてなブログに移行する前は、PelicanというMarkdownなどマークアップ言語のファイルをHTMLファイルに変換するツールと、BitBucket・Dockerを組み合わせて、自前のブログを構築していました。

Gitで管理されたMarkdownファイルをBitBucketにPushすると、BitBucketのフックが起動されて、Docker上のWebサーバでMarkdownファイルをPelicanで変換する仕組みです。

この自前ブログシステムからはてなブログへの移行手順をメモしておきます。

はてなブログへの移行方法

通常は、はてなブログへの移行は、移行元ブログでエクスポートしたMovableType(MT)形式かWordpress形式のデータを画像URLを調整してからインポートすればよいのですが、自前システムからの移行ではそれが使えません。

そのため、以下の手順で移行しました。

  1. 画像ファイルをはてなフォトライフにアップロードする。このとき、画像ファイルのパスとアップロードした画像のIDの対応表を作成する。
  2. 既存のMarkdownファイルで、画像の相対パスをIDに置換する。
  3. 置換したMarkdownファイルをはてなブログのAtom APIで投稿する。

なお、移行処理のスクリプトはすべてPythonで作成しました。

はてなフォトライフへのアップロード

画像ファイルは階層構造のフォルダで管理されて、Markdownファイルには特定フォルダからの相対パスで記述しています。この各ファイルについて、Base64でエンコードして、はてなフォトライフAtomAPIに従ってRestAPIを呼び出します。

# coding: utf-8
import sys
import os
import os.path
import requests
from requests_oauthlib import OAuth1Session
import xml.etree.ElementTree as ET
import base64
import csv

TEMPLATE = """
<entry xmlns="http://purl.org/atom/ns#">
  <title>{0}</title>
  <dc:subject xmlns:dc="http://purl.org/dc/elements/1.1/">Hatena Blog</dc:subject>
  <content mode="base64" type="{2}">{1}</content>
</entry>"""

header = {'Accept':'text/xml', 'Content-Type':'text/xml'}

infile = open(abspath, 'rb')
data = infile.read()
content = base64.b64encode(data).decode()
body = TEMPLATE.format(title, content, filetype)

oauth_session = OAuth1Session(CONSUMER_KEY, client_secret=CONSUMER_SECRET, resource_owner_key=ACCESS_TOKEN, resource_owner_secret=ACCESS_TOKEN_SECRET)
resp = oauth_session.post('http://f.hatena.ne.jp/atom/post', data=body, headers=header)
    
xmlTree = ET.fromstring(resp.text)

with open(logfile, 'w', newline='') as csvfile:
    logger = csv.writer(csvfile, dialect='excel-tab')
    data.append(dirpath)
    data.append(filename)
    hatenaSyntaxOriginal = xmlTree.find('{http://www.hatena.ne.jp/info/xmlns#}syntax').text
    data.append(hatenaSyntaxOriginal)
    data.append(hatenaSyntaxOriginal.replace(':image', ':plain'))
    logger.writerow(data)

Requests: HTTP for Humansrequests-oauthlibを使うと、OAuthを使ったRest APIに簡単にアクセスできます。今回初めて使いましたが、今後はRequestとrequests-oauthlibを常用しそうです。

Rest APIからの戻り値であるXML文書は、ElementTreeで解析しています。

画像の相対パス⇒IDの置換

これはCSVファイルを読んで作成した辞書を使って、MDファイルにある画像リンクを置き換えるだけです。

なお、csvファイルの読み込みは標準のcsvモジュールを使っていて、以下の通りです。 画像ファイルのパス(絶対パスの一部から作成)からはてなフォトライフのIDに変換する辞書を作成しています。

def readCsv(filename, contentFolder):
    result = dict()
    with open(filename, encoding='sjis', newline='') as csvfile:
      csvreader = csv.reader(csvfile, dialect='excel-tab')
      for row in csvreader:
          if row[2].startswith(contentFolder):
              abspath = row[2][len(contentFolder):].replace('\\', '/')
              hatenaSyntaxPlain = row[12]
              result[abspath] = hatenaSyntaxPlain
    return result

はてなブログへの投稿

Markdownファイルは、冒頭にタイトル、投稿日時、カテゴリなどのメタデータがあり、空行を空けて本文が続きます。

---
title: "いよいよMADOSMAが発売!"
date: 2015-06-18T23:00:00Z:00
categories: Windows Phone
tags: 
- "Windows Phone"
slug: madosma-has-been-released

---

Windows Phone 8.1 Updateを搭載した端末の[MADOSMA][1]がいよいよ発売されました。
(以下省略)

このうち、Title、Date、Categoryははてなブログに投稿するXMLデータ(下記)に含まれるため、 ヘッダ部分のみ解析して値を取得します。Pelicanではタグを扱えたのですが、今回はカテゴリのみ扱うようにしています。また、Authorは私1人だけなので、解析していません。

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>{1}</title>
<author><name>{2}</name></author>
<summary>test summary</summary>
<content type="text/plain">{0}</content>
<updated>{3}</updated>
<category term="{4}" />
<app:control>
<app:draft>{5}</app:draft>
</app:control>
</entry>
"""

アップロードははてなブログのAtom Publishing Protocolに従って、 画像と同様にRequestとrequests-oauthlibでRestAPIを呼び出しています。

まとめ

このように、Requestとrequests-oauthlib、csv、ElementTreeを使って 短時間で移行用スクリプトを作成して、移行を終えることができました。

なお、この方法だと1記事の移行=1投稿になりますが、はてなブログでは 1日100投稿までという制限があるため、移行する記事が多い場合は MovableType形式のデータなどを作成したほうがよいと思います。