こんにちは、ほけきよです。
ここ数回に分けて書いた『面倒がすぎる内容をpythonにさせよう』シリーズの集大成。
ブログ最適化のために必要なものを『全て』『一気に』抜き出すプログラム、作りました!
この記事を読めば、下記の情報がゲットできます
※ 現在ははてなブログのみ対応となっています。WordPress用にも作成中なので、少々お待ちを。
*1
・記事とURLとブックマークの情報
・記事内画像を全て抜きとったもの
・自サイトの内部リンクがどうなっているかを可視化したもの
・リンク切れリスト
・
はてなブックマークがどのような伸び方でついたかを可視化したもの
使い方(情報技術に明るい人)
情報技術に明るい人と、そうでない人向けに使い方を分けて書きます。
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:
print(e.reason)
break
except error.URLError as e:
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:
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:
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
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
flag3 = "www.amazon.co.jp" not in url and "http://amzn.to/" not in url
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")
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")
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)
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")
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
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 |
はてブの付き方の初動解析 |
なお、各要素技術はすでに過去記事にて説明しているので、気になる方はそちらをどうぞ
内部ネットワークについては、新たにグラフの次数分布を出せるようにしました(下記画像)。次数というのは、どのくらい他の記事と結びついているか(リンクの数)です。縦軸は記事数です。どの程度浮いた記事があるかを把握するのにお使いください。
この機能を応用すると、次のような可視化も可能です。yowaiが私です。。。強くしたい。。。
また、余談ですが、下記記事のsearch consoleの解析もpythonで行っています
pandas
を使えばこういう表情報の抜き出しも簡単にできるので、興味のある方は是非。
注意
下記簡単に注意事項です。
- リンク切れのチェックはまだ動作が不安定でエラーが出ることも多いかもしれません。
- 楽天のリンクが多い人はチェックに時間がかかると思います。
- リンクに対する日本語対応はまだ不完全です。リンクに日本語が含まれる場合は、フォルダ名等通し番号になります。
※注意事項追記(7/9)
- 楽天アフィリエイト等はこのリンク切れ検査実行時に、クリック数をカウントしてしまうため、もしかするといつもより多くアクセスするために不正クリックとみなされる恐れがあります。楽天等アフィリンクだけ別にアクセスするようコード修正予定ですが、現状使用の際は自己責任でお使いくださいませ。
- ⇨楽天/もしもからのリンクは取らないようにコードを修正しました。オプション付加は今後対応いたします。
出力結果
実行後は下記のフォルダ構成で出力されます。(procrasistで調べた場合)
├─all_in_one.py 実行するコード
└─procrasist
├─articles.csv 記事一覧
├─invalid_url_list.csv リンク切れ
├─graph 内部ネットワーク
├─hatebu はてブ解析
└─imgs 画像
また、出力ログは次のようにしています。
正直、スクールに行ったりするよりも、
- 何をやりたいかを明確にする
- 自分で手を動かしながら知り合いに聞きまくる
ほうが伸びると思います。
今回はブログ最適化という目的があったので、私は色々と勉強することができました。
正直Qiitaとかネット上に解説が溢れているので、適宜調べながらでOKだとは思います。
ただ、何に使うかという目的ベースから書かれているこの本は少し気になってます。
この記事はこの本タイトルにinspiredされて書いたものです。
時間があれば読んでみようかなと思っています。
私が書いてるコードは初心者なので未熟ですが、
単純なものの組み合わせでもこのくらいはできるようになるので、是非チャレンジしてみてください!
まとめ
ブログ最適化って、本当に大事なのはネットワークの構成を考えたり、記事内容をリライトしたり
ってところですよね。それ以前の大変面倒な単純作業は人間がするようなところではありません。
頭の使わない退屈なことはpythonにやらせて、余った時間で有意義な最適化作業をしましょう。ではではっ!
いくつかのブログでチェックはしましたが、まだエラーが出ると思います。
エラー起きたり、こんな機能が欲しい!というのがあれば教えてね!機能は随時追加します!
準備は整った!!いよいよ最適化作業へとうつr( ˘ω˘ ) スヤァ…