完全に煽りにいくスタイルでいきます。恐縮です。
はてなブログのAPIを利用して自動投稿をするようにしてみたというお話。
なんで自動投稿??
「2度以上同じ作業をしたらエンジニアではない。」
これを会社の先輩に言われ続けてきて育ったので、同じような作業を何度も繰り返すことに罪悪感を覚えるようになってしまってます。
そんな先輩の教えを忠実に守って、はてなブログでの記事投稿で同じようなテンプレで記事を書いているものは自動で記事生成をして、自動ではてなブログに投稿してくれるようにしました。
自動でPVアクセス稼いで不労所得化しta
単純に技術的な興味から自動化することに( ´ v ` )ノ
自動投稿【準備編】
記事の自動投稿をする上で必要な環境を用意します。以下のような構成としました。
####構成
![Screen Shot 2018-07-02 at 16.01.19.png](https://qiita-image-store.s3.amazonaws.com/0/253272/7bb979de-99e7-64b7-75a3-9ff4a3736e8e.png)
#####サーバー ・Heroku(無料枠) #####言語 ・Python 3\.6.4 #####API ・Hatena API ・CoinGecko API
####環境構築
PythonのプログラムをHeorku上で動くように準備します。↓やったことない方は、ここらへん参考にどうぞ。
Pythonでwebアプリケーション開発① ~環境構築編~
PythonでSlackBot開発②「Herokuにデプロイする」
記事の生成・投稿処理の流れ
記事の自動生成と投稿をする上で、処理の流れは以下のようなものとなります。
- CoinGecko APIにアクセスして仮想通貨価格の取得
- 仮想通貨価格の変動に応じて適当なコメントを生成する
- 仮想通貨価格データ、適当なコメントを事前に用意していた記事のテンプレートファイルと組み合わせたtxtファイルを生成する
- はてなブログAPIでPOST!!
なんて簡単なんでしょう。。。
プログラムは以下のような感じです。
####CoinGecko APIで仮想通貨データ取得
import requests
import json
COINS_API_URL = 'https://api.coingecko.com/api/v3/coins/'
payload = {'Content-Type': 'application/json', 'per_page': per_page, 'page': page_num}
response = requests.get(COINS_API_URL, params=payload)
json = response.json()
print(json)
実行すると、100コイン分の価格・直近での値動き・コミュニティ情報などが含まれたjsonデータが取得できます。
$ python coingecko_api.py
[{'id': 'bitcoin', 'symbol': 'btc', 'name': 'Bitcoin', 'localization': {'en': 'Bitcoin', 'es': 'Bitcoin', 'de': 'Bitcoin', 'pt': 'Bitcoin', 'fr': 'Bitcoin', 'it': 'Bitcoin', 'pl': 'Bitcoin', 'id': 'Bitcoin', 'zh': '比特币', 'zh-tw': '比特幣', 'ja': 'ビットコイン', 'ko': '비트코인', 'ru': 'биткоина', 'ar': 'بيتكوين', 'th': 'บิตคอยน์', 'vi': 'Bitcoin', 'tr': 'Bitcoin'}, 'image': {'thumb': 'https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1510040391', 'small': 'https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1510040391'}, 'coingecko_score': 90.785, 'market_data': {'current_price': {'aud': 8887.34433422866, 'brl': 25593.7486743805, 'btc': 1.0, 'cad': 8638.271142251, 'chf': 6522.15418554039, 'cny': 43666.6458587973, 'dkk': 42015.0183446382, 'eth': 14.0347340999793, 'eur': 5638.92719189477, 'gbp': 4981.27331616835, 'hkd': 51534.2761883196, 'idr': 93725807.761016, 'inr': 450639.049448336, 'jpy': 727270.463582273, 'krw': 7322273.56474949, 'mxn': 129124.957527213, 'myr': 26596.6637723585, 'nzd': 9747.53113784829, 'php': 350930.935394565, 'pln': 24780.9693447351, 'rub': 414808.827502573, 'sek': 58211.1025135184, 'sgd': 8974.36096294105, 'twd': 200627.694044467, 'usd': 6569.77189221517, 'xag': 410.726896830502, 'xau': 5.25568611833429, 'xdr': 4673.63717754349, 'zar': 90199.6503453086}, 'roi': None, 'market_cap': {'aud': 152230094647.4846, 'brl': 438391789106.14655, 'btc': 17128862.0, 'cad': 147963754314.19974, 'chf': 111717078986.84373, 'cny': 747959950918.2104, 'dkk': 719669451152.7761, 'eth': 240399023.60523963, 'eur': 96588405698.01303, 'gbp': 85323543216.93004, 'hkd': 882723505099.6124, 'idr': 1605416426976972, 'inr': 7718934089811.724, 'jpy': 12457315407376.78, 'krw': 125422213416842.08, 'mxn': 2211763578239.4927, 'myr': 455570583417.1282, 'nzd': 166964115700.90634, 'php': 6011047563904.42, 'pln': 424469804132.19794, 'rub': 7105203162673.378, 'sek': 997089941821.9098, 'sgd': 153720590472.40436, 'twd': 3436524084665.897, 'usd': 112532716113.23253, 'xag': 7035284335.497906, 'xau': 90023922.23626372, 'xdr': 80054086252.21194, 'zar': 1545017363213.0435}, 'total_volume': {'aud': 4678019885.05915, 'brl': 13471748221.888775, 'btc': 526368.7001573974, 'cad': 4546915552.753815, 'chf': 3433057820.869024, 'cny': 22984755620.928535, 'dkk': 22115390593.156414, 'eth': 7387444.745260804, 'eur': 2968154776.279853, 'gbp': 2621986360.5602627, 'hkd': 27126029970.798103, 'idr': 49334331602368.1, 'inr': 237202290698.28574, 'jpy': 382812408578.6688, 'krw': 3854215618474.0615, 'mxn': 67967336051.47825, 'myr': 13999651338.379684, 'nzd': 5130795294.772961, 'php': 184719060308.65677, 'pln': 13043926622.628527, 'rub': 218342383346.3434, 'sek': 30640502364.769688, 'sgd': 4723822714.806569, 'twd': 105604138529.76212, 'usd': 3458122291.2359037, 'xag': 216193782.8043528, 'xau': 2766428.6705428977, 'xdr': 2460056326.150854, 'zar': 47478272706.91183}, 'high_24h': {'aud': 9063.92495929762, 'brl': 26008.4698740575, 'btc': 1.0, 'cad': 8770.4974791518, 'chf': 6608.44747003825, 'cny': 44534.7416706287, 'dkk': 42566.9522286037, 'eth': 14.1464209023202, 'eur': 5712.60956568583, 'gbp': 5060.59575056709, 'hkd': 52136.7188136986, 'idr': 94982964.9249489, 'inr': 458105.422138475, 'jpy': 737324.09345108, 'krw': 7467619.39836213, 'mxn': 133217.398564206, 'myr': 26904.7330452576, 'nzd': 9917.95381668165, 'php': 355621.886159652, 'pln': 25204.6655574203, 'rub': 421761.054163531, 'sek': 59609.2420703987, 'sgd': 9123.70153058301, 'twd': 203580.748547576, 'usd': 6644.794495931, 'xag': 419.97621338502, 'xau': 5.36214981438143, 'xdr': 4727.00713248787, 'zar': 92233.1298039206}, 'low_24h': {'aud': 8491.49964095077, 'brl': 24384.2814324839, 'btc': 1.0, 'cad': 8228.31359040978, 'chf': 6208.73717991839, 'cny': 41579.073456101, 'dkk': 40010.0182873796, 'eth': 13.8160755038863, 'eur': 5369.72959580919, 'gbp': 4755.4009600189, 'hkd': 48917.3033964698, 'idr': 88809577.375957, 'inr': 428326.590297124, 'jpy': 691077.627710631, 'krw': 6980922.19952923, 'mxn': 125409.43030168, 'myr': 25259.0416138529, 'nzd': 9296.98467810652, 'php': 333108.956401974, 'pln': 23603.5729038995, 'rub': 394777.828572906, 'sek': 56128.8591537325, 'sgd': 8541.39774253922, 'twd': 190482.418320556, 'usd': 6235.19310425976, 'xag': 392.288028542612, 'xau': 5.00074957347841, 'xdr': 4435.62284647383, 'zar': 86459.2025046896}, 'price_change_24h': '259.03958341285', 'price_change_percentage_24h': '4.1047468144313', 'price_change_percentage_7d': '5.25362269506387', 'price_change_percentage_14d': '-1.61379216238405', 'price_change_percentage_30d': '-13.184530421404', 'price_change_percentage_60d': '-31.2147533693418', 'price_change_percentage_200d': '-63.4578847873222', 'price_change_percentage_1y': '154.966993509826', 'market_cap_change_24h': '4446121799.144', 'market_cap_change_percentage_24h': '4.11348125765163', 'circulating_supply': '17128862.0'}, 'community_data': {'facebook_likes': 38180, 'twitter_followers': 59768, 'reddit_average_posts_48h': 3.704, 'reddit_average_comments_48h': 227.778, 'reddit_subscribers': 878033, 'reddit_accounts_active_48h': 10462}, 'developer_data': {'forks': 19811, 'stars': 32903, 'subscribers': 3389, 'total_issues': 4011, 'closed_issues': 3462, 'pull_requests_merged': 5567, 'pull_request_contributors': 509, 'commit_count_4_weeks': 30}, 'public_interest_stats': {'alexa_rank': 12970, 'bing_matches': 33000000}, 'last_updated': '2018-07-03T13:07:11.865Z'}, ...
これだけのデータがパッと取得できるのは控えめに言って最高。
####仮想通貨データ元に適当にコメント生成
仮想通貨が直近1週間で値上がりしているのか、値下がりしているのか?によって「値下がりしてますね!やばい(T ^ T)」とか適当にコメントを生成します。
お好きなように処理書いて記事ファイルを生成。
<br>
[f:id:virtual-surfer:20180616200012j:plain]
毎週日曜日20時に、その1週間の仮想通貨界隈で起こったニュースをまとめて記事にして公開しています!!
####はてなブログAPIで記事投稿
ブログに記事を投稿します。
# coding=utf-8
import requests
from datetime import datetime
import hashlib
import base64
from xml.sax.saxutils import escape
from chardet.universaldetector import UniversalDetector
import random
USER_NAME = 'username'
BLOG_NAME = 'blogname.hatenablog.com'
PASSWORD = 'password'
TITLE = '仮想通貨価格変動まとめ'
FILE_NAME = 'test_article.txt'
def create_hatena_text(title, name, body, updated, categories, is_draft):
is_draft = 'yes' if is_draft else 'no'
categories_text = ''
for category in categories:
categories_text = categories_text + '<category term="{}" />\n'.format(category)
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>{0}</title>
<author><name>{1}</name></author>
<content type="text/x-markdown">{2}</content>
<updated>{3}</updated>
{4}
<app:control>
<app:draft>{5}</app:draft>
</app:control>
</entry>"""
text = template.format(title, name, body, updated, categories_text, is_draft).encode()
return text
def post_hatena_blog(user_name, password, entry_id, blog_name, data):
headers = {'X-WSSE': create_wsse_auth_text(user_name, password), 'content-type': 'application/xml'}
if entry_id is None:
url = 'http://blog.hatena.ne.jp/{0}/{1}/atom/entry'.format(user_name, blog_name)
else:
url = 'http://blog.hatena.ne.jp/{0}/{1}/atom/entry/{2}'.format(user_name, blog_name, entry_id)
request = requests.post(url, data=data, headers=headers)
if request.status_code == 201:
print('POST SUCCESS!!\n' + 'message: ' + request.text)
else:
print('Error!\n' + 'status_code: ' + str(request.status_code) + '\n' + 'message: ' + request.text)
def create_wsse_auth_text(user_name, password):
created = datetime.now().isoformat() + "Z"
b_nonce = hashlib.sha1(str(random.random()).encode()).digest()
b_digest = hashlib.sha1(b_nonce + created.encode() + password.encode()).digest()
c = 'UsernameToken Username="{0}", PasswordDigest="{1}", Nonce="{2}", Created="{3}"'
return c.format(user_name, base64.b64encode(b_digest).decode(), base64.b64encode(b_nonce).decode(), created)
file = open(FILE_NAME)
body = escape(file.read())
file.close()
categories = ['仮想通貨', '価格変動速報']
now = datetime.now()
# is_draftをFalseにすると公開になります。Trueで下書き投稿
article = create_hatena_text(TITLE, USER_NAME, body, now, categories, is_draft=True)
post_hatena_blog(USER_NAME, PASSWORD, entry_id=None, blog_name=BLOG_NAME, data=article)
USER_NAME、BLOG_NAME、PASSWORDは自分のはてなブログの情報に書き換えてください。
これでファイルを実行してみると。
$ python hatena_api.py
POST SUCCESS!!
message: <?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom"
xmlns:app="http://www.w3.org/2007/app">
<id>tag:blog.hatena.ne.jp,2013:blog-virtual-surfer-8599973812338209264-10257846132597788465</id>
<link rel="edit" href="http://blog.hatena.ne.jp/virtual-surfer/virtual-surfer.hatenablog.com/atom/entry/10257846132597788465"/>
<link rel="alternate" type="text/html" href="https://www.virtual-surfer.com/entry/2018/07/03/223637"/>
<author><name>virtual-surfer</name></author>
<title>仮想通貨価格変動まとめ</title>
<updated>2018-07-03T22:36:37+09:00</updated>
<published>2018-07-03T22:36:37+09:00</published>
<app:edited>2018-07-03T22:36:37+09:00</app:edited>
<summary type="text">毎週日曜日20時に、その1週間の仮想通貨界隈で起こったニュースをまとめて記事にして公開しています!!</summary>
<content type="text/x-markdown"><br>
[f:id:virtual-surfer:20180616200012j:plain]
毎週日曜日20時に、その1週間の仮想通貨界隈で起こったニュースをまとめて記事にして公開しています!!
</content>
<hatena:formatted-content type="text/html" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#"><p><br>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/v/virtual-surfer/20180616/20180616200012.jpg" alt="f:id:virtual-surfer:20180616200012j:plain" title="f:id:virtual-surfer:20180616200012j:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>毎週日曜日20時に、その1週間の仮想通貨界隈で起こったニュースをまとめて記事にして公開しています!!</p>
</hatena:formatted-content>
<category term="仮想通貨" />
<category term="価格変動速報" />
<app:control>
<app:draft>yes</app:draft>
</app:control>
</entry>
投稿できたぽい!はてなブログを確認してみると下書き投稿できていることがわかります。
↓ 記事の一覧画面
↓ 記事をプレビューモードで見てみる
自動投稿してみて何が嬉しかったのか?
はてなブログで現状4種類くらいの記事を自動投稿するようにしているのですが、運用を始めたのが3週間前なので、自動で記事投稿してもPVは落ち込まないのか?むしろ上がるのか?などは今後検証していきたいと思っています。
自動投稿の現状までの結果検証などはこちらの記事にまとめています。
プログラムの力でブロガーの仕事は奪えるのか。ブログ記事投稿自動化してみた
↑はてなブログの自動投稿
ちなみに、WordPressの自動投稿プログラムは↓以下に書いています。
【Python】WordPressで記事の自動投稿できるようになる
ハマったこと
はてなブログAPIを使って投稿する上で、txtファイルのescapeをしていなくて、400 parse Errorに悩みました。
Error!
status_code: 400
message: 400 XML Parse Failed
冷静に考えれば、txtファイルにhtml文書にする際にエスケープしないといけない特殊文字(「<」とか)をそのままPOSTしていたのが問題だとわかって解決。
今後の課題
ブログの自動更新の仕組みは整えることができたので、あとはコンテンツの質(いかにプログラムで自動生成した記事の内容を読者にとって価値のあるものにしていけるか)を高めていくのが課題だと思ってます。
ゴリゴリ条件分岐させて記事内の文章を生成させるのではなくて、それっぽい文章を作ってくれるようにしたいところです。
「2度以上同じ作業をしたらエンジニアではない。」
今後も先輩の教えを胸に頑張りまする。
記事の執筆者について
noteで、Webエンジニア未経験~2年目くらいの方に向けて、プログラミングチュートリアルや情報まとめて書いています。
note(プログラミングチュートリアル)
TwitterでもWebエンジニアの日常をツイートしているので、フォローしてみてください。
Twitter(仮想サーファー@virtual_techX)
気になったことを検証してみたりの雑記ブログも書いてます。
雑記ブログ(仮想サーファーの日常)