はしくれエンジニアもどきのメモ

情報系技術・哲学・デザインなどの勉強メモ・備忘録です。

はてなブログAPIで全記事の一括編集

はてなブログAPIで全記事の一括編集

はてなブログAPを使って,全記事の一括編集をしてみたのでメモ. すべての公開記事に対して,以下3つの処理を行う.

パース・加工処理は,BeautifulSoup4を使う.

環境

  • Windows10
    • python 3.5.4
      • requests (2.18.4)
      • beautifulsoup4 (4.6.0)

記事のバックアップ

全記事を扱うので,その前に全記事をバックアップ(export)をしておく.

はてなブログの「詳細設定」 > 「エクスポート」からバックアップできる.

エクスポート
エクスポート > バックアップ

全記事のentry idの取得

はてなAPIを使った全記事の一覧からentry idの取得は, 以下の記事で解説している.

cartman0.hatenablog.com

はてなAPIによるentry idから記事の編集

はてなAPIを使ったentry idからの記事の編集は,以下の記事で解説している.

cartman0.hatenablog.com

全記事に対しての処理

BeautifulSoup4と文字列処理を行って,次の処理を行う.

ここでは,以下の記事テンプレートを

<article>
<h1>title</h1>
<nav>
<h6>目次</h6>
<ul>
  <li>toc1</li>
  <li>toc2</li>
</ul>
</nav>
====
<section>
</section>
...
</article>

から以下のように変更する.

<h1>title</h1>
<nav>[:contents]</nav>
<p><\!-- more --></p>
<section>
</section>
...

記事の外側に入れていたarticleタグを削除

今までの記事は,articleタグをルートとして, 以下のよう書いていたので,

<article>
<h1>title</h1>...
</article>

このarticleタグを削除してルート要素がないように変更.

<!-- <article>削除 -->
<h1>title</h1>...
<!-- </article>削除 -->

BeautifulSoup_article.unwrap() を使う.

import bs4

hatena_id = "<hatena_id>"
password = "<password>"
blog_id = "<blog_id>"
entry_id = "<entry_id>"
content_str = get_entry_content_str(hatena_id, blog_id, password, entry_id)
# soup化
content_soup = bs4.BeautifulSoup(content_str), "lxml")

# article 削除
if content_soup.article: content_soup.article.unwrap()
content_soup

results:

<html>
<body>
<h1>title</h1>
<nav>
<h6>目次</h6>
<ul>
  <li>toc1</li>
  <li>toc2</li>
</ul>
</nav>
\====
<section>
</section>
...
</body>
</html>

目次部分を目次記法へ変更

目次部分を目次記法[:contents]へ変更,navタグでwrapする.

# 目次のnavタグをはてな記法へ書き換え
nav = content_soup.nav
if nav:
    nav.clear()
    p = soup_update_html.new_tag("p")
    p.string = "[:contents]"
    nav.append(p)
content_soup

results:

<html>
<body>
<h1>title</h1>
<nav>[:contents]</nav>
\====
<section>
</section>
...
</body>
</html>

続きを読む記法<p><\!--more--></p> へ統一

文字列処理でpタグなしの続きを読む記法====を, <p><\!-- more --></p>へreplaceする.

import re

# soupを文字列化
if soup_content.html: soup_content.html.unwrap()
if soup_content.body: soup_content.body.unwrap()
content_str = str(soup_content)

# 続きを読む記法を文字列置換でreplace
content_str = re.sub( pattern=r"<p>(====+)</p>|(====+)",  repl="

", string=content_str, count=1)
content_str

results:

'<html>
<body>
<h1>title</h1>
<nav>[:contents]</nav>
<p><\!-- more --></p>
<section>
</section>
...
</body>
</html>`

一括処理

まず,entry_idを取得

# entry_id を取得
import requests
import bs4
import time
import re

def get_entry_id_list(hatena_id, blog_id, password, limit_max_iterations=50, wait_s=0.01, print_collection_uri=True):
    '''
    return relesed order
    '''
    collection_uri = get_collection_uri(hatena_id, blog_id, password)
    if not collection_uri:
        raise Exception("Not get collection uri.")

    entry_id_list = []

    for i in range(limit_max_iterations):
        if print_collection_uri:
            print(collection_uri)

        # Basic認証で記事一覧を取得
        res_collection = requests.get(collection_uri, auth=(hatena_id, password))
        if not res_collection.ok:
            return False
        # Beatifulsoup4でDOM化
        soup_collection_xml = bs4.BeautifulSoup(res_collection.content, features="xml")
        # entry elementのlistを取得
        entries = soup_collection_xml.find_all("entry")
        # 下書きを無視
        pub_entry_list = list(filter(lambda e: e.find("app:draft").string != "yes", entries))
        # entry idを取得
        entry_id_list.extend([re.search(r"-(\d+)$", string=e.id.string).group(1) for e in pub_entry_list])

        # 次のcollection_uriへ更新
        link_next = soup_collection_xml.find("link", rel="next")
        if not link_next:
            return entry_id_list
        collection_uri = link_next.get("href")
        if not collection_uri:
            return entry_id_list
        # wait
        time.sleep(wait_s)# 10ms

    print("warning: possible to left some entry_id")
    return entry_id_list

entry_id_list = get_entry_id_list(hatena_id, blog_id, password, limit_max_iterations=50, wait_s=0.01)
entry_id_list.reverse()
entry_id_list # 古い順

古いentry_idから,entry_id ごとに加工処理を行って編集する.

前回の内容から変更されたのみ更新する. 前回から変更があったかどうかは,文字列比較で判断する.

def get_soupXML_soupHTML_from_entryid(hatena_id, blog_id, password, entry_id):
    '''
    return soup(XML) of respose, soup(HTML) content in reponse
    '''
    member_uri = "https://blog.hatena.ne.jp/{hatena_id}/{blog_id}/atom/entry/{entry_id}".format(hatena_id=hatena_id, blog_id=blog_id,entry_id=entry_id)
    res_member = requests.get(member_uri, auth=(hatena_id, password))
    if not res_member.ok:
        print("status_code: " + str(res_member.status_code))
        return False

    soup_response_xml = bs4.BeautifulSoup(res_member.content, features="xml")
    soup_content_html = bs4.BeautifulSoup(soup_response_xml.find("content").string, "lxml")
    if soup_content_html.html : soup_content_html.html.unwrap()
    if soup_content_html.body : soup_content_html.body.unwrap()

    return soup_response_xml, soup_content_html

def edit_put_entry(hatena_id, blog_id, password, entry_id, base_xml_soup,
                   updated_content_str):
    '''
    - args:
        - base_xml_soup: soup(XML), response XML
        - updated_content_str: str, eg. "<h1>title</h1><p>..."
    - return:
        - requests.put.response
    '''
    if not updated_content_str:
            raise ValueError("updated_content is null string.")

    def create_xml(base_xml_soup,
               title=None,
               author=None,
               updated=None,
              categories=[],
              draft=None,
              content=None):
        # XMLsoupのクローン
        update_soup_xml = bs4.BeautifulSoup(str(base_xml_soup), features="xml")
        # id 削除
        if update_soup_xml.id: update_soup_xml.id.decompose()
        # link 削除
        for l in update_soup_xml.findAll("link"):
            l.decompose()
        # delete published
        if update_soup_xml.published: update_soup_xml.published.decompose()
        # delete app:edited
        edited = update_soup_xml.find("app:edited")
        if edited:
            edited.decompose()
        # delete summary
        if update_soup_xml.summary: update_soup_xml.summary.decompose()
        # delete formatted_content
        formatted = update_soup_xml.find("formatted-content")
        if formatted:formatted.decompose()

        # title
        if title: update_soup_xml.title.string = title
        # author
        if author: update_soup_xml.author.string = author
        # updated
        if updated: update_soup_xml.updated.string = updated
        # category
        for new_c in categories:
            cate_tag =  update_soup_xml.new_tag("category")
            cate_tag.attrs = {"term": new_c}
            update_soup_xml.append(cate_tag)
        # draft: yes, no
        if draft: soup_response_xml.find("app:draft").string = draft

        # content書き換え
        if content: update_soup_xml.content.string = content

        return update_soup_xml

    member_uri = "https://blog.hatena.ne.jp/{hatena_id}/{blog_id}/atom/entry/{entry_id}".format(
        hatena_id=hatena_id, blog_id=blog_id, entry_id=entry_id)
    xml_str = create_xml(content=updated_content_str)
    res_put = requests.put(
        member_uri, auth=(hatena_id, password), data=xml_str.encode("utf-8"))
    return res_put


# entry id に対して書き換え
for entry_id in entry_id_list:
    print("get: " + entry_id)
    # 記事取得
    soup_xml, soup_content_html = get_soupXML_soupHTML_from_entryid(
    hatena_id, blog_id, password, entry_id=entry_id)
    print(soup_xml.title)

    # 記事操作
    soup_update_html = bs4.BeautifulSoup(str(soup_content_html), "lxml")  # clone
    # article 削除
    if soup_update_html.article:
        soup_update_html.article.unwrap()
    # 目次のnavタグをはてな記法へ書き換え
    nav = soup_update_html.nav
    if nav:
        nav.clear()
        p = soup_update_html.new_tag("p")
        p.string = "[:contents]"
        nav.append(p)
    # 文字列化
    if soup_update_html.html: soup_update_html.html.unwrap()
    if soup_update_html.body: soup_update_html.body.unwrap()
    content_str = str(soup_update_html)
    # 続きを読む記法を文字列置換でreplace
    content_str = re.sub(
        pattern=r"<p>(====+)</p>|(====+)",
        repl="<p><\!-- more --></p>",
        string=content_str,
        count=1)

    # 更新されたか比較
    if str(soup_content_html) == content_str:
        print("through: " + entry_id)
        continue

    # 記事をアップロード
    res = edit_put_entry(
        hatena_id,
        blog_id,
        password,
        entry_id=entry_id,
        base_xml_soup=soup_xml,
        updated_content_str=content_str)
    if not res.ok:
        print("faild PUT: " + entry_id)
        print(res.text)
        continue

    print("PUT: " + str(res.status_code) + " : " + entry_id)
    print("")
    time.sleep(0.01)

print("completed!")

gist: はてなAPI 一括編集 · GitHub

forループではsleepを入れておく.

結果