こんにちは、ほけきよです。
ここ数回に分けて書いた『面倒がすぎる内容をpythonにさせよう』シリーズの集大成。
ブログ最適化のために必要なものを『全て』『一気に』抜き出すプログラム、作りました!
この記事を読めば、下記の情報がゲットできます
※ 現在ははてなブログのみ対応となっています。WordPress用にも作成中なので、少々お待ちを。 *1
・記事内画像を全て抜きとったもの
・自サイトの内部リンクがどうなっているかを可視化したもの
・リンク切れリスト
・はてなブックマークがどのような伸び方でついたかを可視化したもの
使い方(情報技術に明るい人)
情報技術に明るい人と、そうでない人向けに使い方を分けて書きます。
githubからtaikutsu_blog_worksリポジトリをcloneしてきて、all_in_one.py
実行してください。
git clone https://github.com/hokekiyoo/taikutsu_blog_works.git
requirement、引数等はREADMEに書いています。 *2 *3
この説明で???という方は次章からの説明をどうぞ
使い方(一般向け)
環境構築
MacやLinuxユーザの方は、探せばいくらでもpython3の導入説明記事があると思います。
Windowsで動かしたい!という方には、私の過去記事を参考に環境構築してみてください。他にも方法はありますが、この方法が一番手っ取り早いと思います。(容量が2GBほど必要であることにご注意ください。)
実行するコード
下記コードをコピペしてall_in_one.py
などで保存してください。
from argparse import ArgumentParser from urllib import request from urllib import error from bs4 import BeautifulSoup import os import csv import json import datetime import time import matplotlib.pyplot as plt def extract_urls(args): page = 1 is_articles = True urls = [] while is_articles: try: html = request.urlopen("{}/archive?page={}".format(args.url, page)) except error.HTTPError as e: # HTTPレスポンスのステータスコードが404, 403, 401などの例外処理 print(e.reason) break except error.URLError as e: # アクセスしようとしたurlが無効なときの例外処理 print(e.reason) break soup = BeautifulSoup(html, "html.parser") articles = soup.find_all("a", class_="entry-title-link") for article in articles: urls.append(article.get("href")) if len(articles) == 0: # articleがなくなったら終了 is_articles = False page += 1 return urls def make_directories(args): directory = args.directory if not os.path.exists(directory): os.mkdir(directory) if args.image: if not os.path.exists(directory+"/imgs"): os.mkdir(directory+"/imgs") if args.graph: if not os.path.exists(directory+"/graph"): os.mkdir(directory+"/graph") if args.hatebu: if not os.path.exists(directory+"/hatebu"): os.mkdir(directory+"/hatebu") def articles_to_img(args, url, soup, name): """ 各記事内の画像を保存 - gif, jpg, jpeg, png - 記事ごとにフォルダ分けして保存される - imgs/{urlの最後の部分}/{0-99}.png """ # ディレクトリの作成 article_dir = os.path.join(args.directory+"/imgs", name) if not os.path.exists(article_dir): os.mkdir(article_dir) entry = soup.select(".entry-content")[0] imgs = entry.find_all("img") count=0 for img in imgs: filename = img.get("src") if "ssl-images-amazon" in filename: # print("amazon img") continue # 拡張子チェック if filename[-4:] == ".jpg" or filename[-4:] == ".png" or filename[-4:] == ".gif": extension = filename[-4:] print("\t IMAGE:",filename) elif filename[-5:] == ".jpeg": extension = filename[-5:] print("\t IMAGE:",filename,extension) else: continue try: image_file = request.urlopen(filename) except error.HTTPError as e: print("\t HTTPERROR:", e.reason) continue except error.URLError as e: print("\t URLERROR:", e.reson) continue # ValueErrorになった場合に試す(httpで始まらないリンクも貼れるっぽい?) except ValueError: http_file = "http:"+filename try: image_file = request.urlopen(http_file) except error.HTTPError as e: print("\t HTTPERROR:", e.reason) continue except error.URLError as e: print("\t URLERROR:", e.reason) continue # 画像ファイルの保存 with open(os.path.join(article_dir,str(count)+extension), "wb") as f: f.write(image_file.read()) count+=1 def make_network(G, args, url, urls, soup): entry_url = args.url + "/entry/" article_name = url.replace(entry_url,"").replace("/","-") entry = soup.select(".entry-content")[0] links = entry.find_all("a") for link in links: l = link.get("href") if l in urls: linked_article_name = l.replace(entry_url,"").replace("/","-") print("\t NETWORK: 被リンク!{} -> {}".format(article_name, linked_article_name)) j = urls.index(l) G.add_edge(article_name, linked_article_name) else: continue def url_checker(url, urls): #変なリンクは除去したい flag1 = "http" in url[:5] #ハテナのキーワードのリンクはいらない flag2 = "d.hatena.ne.jp/keyword/" not in url #amazonリンクはダメ flag3 = "www.amazon.co.jp" not in url and "http://amzn.to/" not in url #rakutenリンクはダメ flag4 = "rakuten.co.jp" not in url #もしももダメ flag5 = "af.moshimo" not in url return flag1 and flag2 and flag3 and flag4 and flag5 def check_invalid_link(args, urls, url, soup, writer): import re from urllib.parse import quote_plus regex = r'[^\x00-\x7F]' #正規表現 entry_url = args.url + "/entry/" entry = soup.select(".entry-content")[0] links = entry.find_all("a") for link in links: l = link.get("href") if l == None: continue #日本語リンクは変換 matchedList = re.findall(regex,l) for m in matchedList: l = l.replace(m, quote_plus(m, encoding="utf-8")) check = url_checker(l, urls) if check: #リンク切れ検証 try: html = request.urlopen(l) except error.HTTPError as e: print("\t HTTPError:", l, e.reason) if e.reason != "Forbidden": writer.writerow([url, e.reason, l]) except error.URLError as e: writer.writerow([url, e.reason, l]) print("\t URLError:", l, e.reason) except TimeoutError as e: print("\t TimeoutError:",l, e) except UnicodeEncodeError as e: print("\t UnicodeEncodeError:", l, e.reason) def get_timestamps(args, url, name): """ はてブのタイムスタンプを取得 """ plt.figure() data = request.urlopen("http://b.hatena.ne.jp/entry/json/{}".format(url)).read().decode("utf-8") info = json.loads(data.strip('(').rstrip(')'), "r") timestamps = list() if info != None and "bookmarks" in info.keys(): # 公開ブックマークが存在する時に、それらの情報を抽出 bookmarks=info["bookmarks"] title = info["title"] for bookmark in bookmarks: timestamp = datetime.datetime.strptime(bookmark["timestamp"],'%Y/%m/%d %H:%M:%S') timestamps.append(timestamp) timestamps = list(reversed(timestamps)) # ブックマークされた時間を保存しておく count = len(timestamps) number = range(count) if(count!=0): first = timestamps[0] plt.plot(timestamps,number,"-o",lw=3,color="#444444") # 3時間で3 plt.axvspan(first,first+datetime.timedelta(hours=3),alpha=0.1,color="blue") plt.plot([first,first+datetime.timedelta(days=2)],[3,3],"--",alpha=0.9,color="blue",label="new entry") # 12時間で15 plt.axvspan(first+datetime.timedelta(hours=3),first+datetime.timedelta(hours=12),alpha=0.1,color="green") plt.plot([first,first+datetime.timedelta(days=2)],[15,15],"--",alpha=0.9, color="green",label="popular entry") # ホッテントリ plt.plot([first,first+datetime.timedelta(days=2)],[15,15],"--",alpha=0.7, color="red",label="hotentry") plt.xlim(first,first+datetime.timedelta(days=2)) plt.title(name) plt.xlabel("First Hatebu : {}".format(first)) plt.legend(loc=4) plt.savefig(args.directory+"/hatebu/{}.png".format(name)) plt.close() def graph_visualize(G, args): import networkx as nx import numpy as np # グラフ形式を選択。ここではスプリングモデルでやってみる pos = nx.spring_layout(G) # グラフ描画。 オプションでノードのラベル付きにしている plt.figure() nx.draw(G, pos, with_labels=False, alpha=0.4,font_size=0.0,node_size=10) plt.savefig(args.directory+"/graph/graph.png") nx.write_gml(G, args.directory+"/graph/graph.gml") # 次数分布描画 plt.figure() degree_sequence=sorted(nx.degree(G).values(),reverse=True) dmax=max(degree_sequence) dmin =min(degree_sequence) kukan=range(0,dmax+2) hist, kukan=np.histogram(degree_sequence,kukan) plt.plot(hist,"o-") plt.xlabel('degree') plt.ylabel('frequency') plt.grid() plt.savefig(args.directory+'/graph/degree_hist.png') def main(): parser = ArgumentParser() parser.add_argument("-u", "--url", type=str, required=True,help="input your url") parser.add_argument("-d", "--directory", type=str, required=True,help="output directory") parser.add_argument("-i", "--image", action="store_true", default=False, help="extract image file from articles") parser.add_argument("-g", "--graph", action="store_true", default=False, help="visualize internal link network") parser.add_argument("-l", "--invalid_url", action="store_true", default=False, help="detect invalid links") parser.add_argument("-b", "--hatebu", action="store_true", default=False, help="visualize analyzed hatebu graph") args = parser.parse_args() urls = extract_urls(args) # 保存用ディレクトリ作成 make_directories(args) # 記事リストを作る with open (args.directory+"/articles_list.csv", "w") as f: writer = csv.writer(f, lineterminator='\n') writer.writerow(["Article TITLE", "URL","Hatebu COUNT"]) if args.invalid_url: f = open(args.directory+'/invalid_url_list.csv', 'w') writer_invalid = csv.writer(f, lineterminator='\n') writer_invalid.writerow(["Article URL", "ERROR", "LINK"]) if args.graph: import networkx as nx G = nx.Graph() for i, url in enumerate(urls): name = url.replace(args.url+"/entry/","").replace("/","-") G.add_node(name) for i, url in enumerate(urls): name = url.replace(args.url+"/entry/","").replace("/","-") print("{}/{}".format(i+1,len(urls)), name) # 抽出したurlに対して各処理実行 try: html = request.urlopen(url) except error.HTTPError as e: print(e.reason) except error.URLError as e: print(e.reason) soup = BeautifulSoup(html, "html.parser") # WordPressならいらない data = request.urlopen("http://b.hatena.ne.jp/entry/json/{}".format(url)).read().decode("utf-8") info = json.loads(data.strip('(').rstrip(')'), "r") try: count = info["count"] except TypeError: count = 0 # 記事の名前とurl、はてブを出力 try: writer.writerow([soup.title.text, url, count]) except UnicodeEncodeError as e: # ふざけた文字が入ってる場合はエラー吐くことも print(e.reason) print("\tArticleWriteWarning この記事のタイトルに良くない文字が入ってます :",url) continue if args.image: if "%" in name: name = str(i) #日本語対応 articles_to_img(args, url, soup, name) if args.graph: make_network(G, args, url, urls, soup) if args.invalid_url: check_invalid_link(args, urls, url, soup, writer_invalid) if args.hatebu: if "%" in name: name = str(i) #日本語対応 get_timestamps(args, url, name) time.sleep(3) if args.invalid_url: f.close() if args.graph: graph_visualize(G, args) if __name__ == '__main__': main()
※コマンドプロンプトやターミナルを動かしたことがない人は、デスクトップ上に保存しておくと、後々の説明でハマりづらいと思います。
または、githubページに飛び、下記画像に従いファイル一覧をDLしてください。
コマンド一発!実行する
コードの実行方法です。
コマンドプロンプト*4かターミナルを開き、all_in_one.py
を保存したフォルダまで行きます。デスクトップに保存している方は、下記コマンドを入力ください。
cd Desktop
デフォルトだとタイトル/URL/はてブ数のcsvファイルだけですが、オプションの付け方でできることを選べるようにしています。詳しくはgithubページをお読みください。また、ここに使用上の注意点もかいていますので、一度読むと良いと思います。
全部使いたいときは、下記のように入力してください。
python all_in_one.py --url http://procrasist.com --directory procrasist --graph --image --hatebu --invalid_link
または
python all_in_one.py -u http://procrasist.com -d procrasist -g -i -b -l
この一発で、あとはきちんと動くはずです。(--url
と--directory
の後ろは自分が調べたいサイトURLを入れてください)
中身がどうなっているか
引数とできることをまとめたのがコチラ。必要ないなと思う情報は上記コマンドのオプションから消してください!
引数 | 意味 |
---|---|
-u, –url | 調べたいサイトのurl |
-d, –directory | 結果を保存するフォルダ名 |
-i, –image | 記事内画像を抜き出す |
-g, –graph | 内部リンクの解析結果を表示する |
-l, –invalid_url | リンク切れを調べる |
-b, –hatebu | はてブの付き方の初動解析 |
なお、各要素技術はすでに過去記事にて説明しているので、気になる方はそちらをどうぞ
■ 記事名とURLの取り出し方
リライトのために、ブログの記事タイトルとURLを自動でぶっこ抜いてcsv化する - プロクラシスト
■ 記事内画像の抜き取り方
はてなブログ記事内の画像を自動で全部ぶっこ抜く - プロクラシスト
■ 内部ネットワークの見方
SEO対策!自分のブログの内部リンクを自動で可視化する - プロクラシスト
■ ブックマークの可視化
はてブ情報をpythonで可視化できるようにしたよ! - プロクラシスト
■ リンク切れのチェック
自サイトの記事のリンク切れを自動で抽出するアレを作った - プロクラシスト
内部ネットワークについては、新たにグラフの次数分布を出せるようにしました(下記画像)。次数というのは、どのくらい他の記事と結びついているか(リンクの数)です。縦軸は記事数です。どの程度浮いた記事があるかを把握するのにお使いください。
この機能を応用すると、次のような可視化も可能です。yowaiが私です。。。強くしたい。。。
また、余談ですが、下記記事のsearch consoleの解析もpythonで行っています
pandas
を使えばこういう表情報の抜き出しも簡単にできるので、興味のある方は是非。
注意
下記簡単に注意事項です。
- リンク切れのチェックはまだ動作が不安定でエラーが出ることも多いかもしれません。
- リンクに対する日本語対応はまだ不完全です。リンクに日本語が含まれる場合は、フォルダ名等通し番号になります。
※注意事項追記(7/9)
- 楽天アフィリエイト等はこのリンク切れ検査実行時に、クリック数をカウントしてしまうため、もしかするといつもより多くアクセスするために不正クリックとみなされる恐れがあります。楽天等アフィリンクだけ別にアクセスするようコード修正予定ですが、現状使用の際は自己責任でお使いくださいませ。
- ⇨楽天/もしもからのリンクは取らないようにコードを修正しました。オプション付加は今後対応いたします。
出力結果
実行後は下記のフォルダ構成で出力されます。(procrasistで調べた場合)
├─all_in_one.py 実行するコード └─procrasist ├─articles.csv 記事一覧 ├─invalid_url_list.csv リンク切れ ├─graph 内部ネットワーク ├─hatebu はてブ解析 └─imgs 画像
また、出力ログは次のようにしています。
pythonを勉強したい方に
正直、スクールに行ったりするよりも、
- 何をやりたいかを明確にする
- 自分で手を動かしながら知り合いに聞きまくる
ほうが伸びると思います。
今回はブログ最適化という目的があったので、私は色々と勉強することができました。
正直Qiitaとかネット上に解説が溢れているので、適宜調べながらでOKだとは思います。 ただ、何に使うかという目的ベースから書かれているこの本は少し気になってます。

退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング
- 作者: Al Sweigart,相川愛三
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/06/03
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る
この記事はこの本タイトルにinspiredされて書いたものです。 時間があれば読んでみようかなと思っています。
私が書いてるコードは初心者なので未熟ですが、 単純なものの組み合わせでもこのくらいはできるようになるので、是非チャレンジしてみてください!
まとめ
ブログ最適化って、本当に大事なのはネットワークの構成を考えたり、記事内容をリライトしたり ってところですよね。それ以前の大変面倒な単純作業は人間がするようなところではありません。
頭の使わない退屈なことはpythonにやらせて、余った時間で有意義な最適化作業をしましょう。ではではっ!
準備は整った!!いよいよ最適化作業へとうつr( ˘ω˘ ) スヤァ…