はてなブログAPIで全記事の一括編集
はてなブログAPIで全記事の一括編集
はてなブログAPを使って,全記事の一括編集をしてみたのでメモ. すべての公開記事に対して,以下3つの処理を行う.
パース・加工処理は,BeautifulSoup4を使う.
環境
- Windows10
- python 3.5.4
- requests (2.18.4)
- beautifulsoup4 (4.6.0)
- python 3.5.4
記事のバックアップ
全記事を扱うので,その前に全記事をバックアップ(export)をしておく.
はてなブログの「詳細設定」 > 「エクスポート」からバックアップできる.
全記事のentry idの取得
はてなAPIを使った全記事の一覧からentry idの取得は, 以下の記事で解説している.
はてなAPIによるentry idから記事の編集
はてなAPIを使ったentry idからの記事の編集は,以下の記事で解説している.
全記事に対しての処理
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を入れておく.