プロクラシスト

今日の寄り道 明日の近道

【コードで一発】ブログ最適化/SEO対策で面倒なことは全てPythonにやらせよう


スポンサーリンク

こんにちは、ほけきよです。

ここ数回に分けて書いた『面倒がすぎる内容を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

この説明で???という方は次章からの説明をどうぞ

使い方(一般向け)

環境構築

MacLinuxユーザの方は、探せばいくらでも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 はてブの付き方の初動解析

なお、各要素技術はすでに過去記事にて説明しているので、気になる方はそちらをどうぞ

内部ネットワークについては、新たにグラフの次数分布を出せるようにしました(下記画像)。次数というのは、どのくらい他の記事と結びついているか(リンクの数)です。縦軸は記事数です。どの程度浮いた記事があるかを把握するのにお使いください。

この機能を応用すると、次のような可視化も可能です。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だとは思います。 ただ、何に使うかという目的ベースから書かれているこの本は少し気になってます。

この記事はこの本タイトルにinspiredされて書いたものです。 時間があれば読んでみようかなと思っています。

私が書いてるコードは初心者なので未熟ですが、 単純なものの組み合わせでもこのくらいはできるようになるので、是非チャレンジしてみてください!

まとめ

ブログ最適化って、本当に大事なのはネットワークの構成を考えたり、記事内容をリライトしたり ってところですよね。それ以前の大変面倒な単純作業は人間がするようなところではありません。

頭の使わない退屈なことはpythonにやらせて、余った時間で有意義な最適化作業をしましょう。ではではっ!

いくつかのブログでチェックはしましたが、まだエラーが出ると思います。 エラー起きたり、こんな機能が欲しい!というのがあれば教えてね!機能は随時追加します!
準備は整った!!いよいよ最適化作業へとうつr( ˘ω˘ ) スヤァ…

*1: 7/10 WP版にも対応しました!下記リンクのコードをコピペし、all_in_one.pyをall_in_one_wp.pyとしてお使いください! WP用ツール

*2:コードが長くなっちゃったので分けようかと思ったのですが、記事にする都合全て一箇所にまとめています

*3:日曜大工程度なので、pull-requestやissue等お待ちしております!

*4:windowsボタン+cmd+Enterで開きます。

PROCRASIST