夜田たんの知的生活実践

ほそく・ながく・ゆるく続くブログを目指します(だいたい週一で更新中)

ブログのリンク切れをチェックする

black asus laptop computer showing 3 00 photo
Photo by Erik Mclean on Unsplash

Pythonの勉強がてら、ブログから外部サイトへ張ったリンクが正常かどうかを調べるスクリプトを書きました。

コード

まずは完成品から。

# -*- coding: utf-8 -*-

import logging
import os
import sys
import glob
import re
import requests
import datetime
from tqdm import tqdm

# =======================================================
# フォルダ
SRC_DIR = r"C:\path-to-markdown-folder"
DST_DIR = r"C:\path-to-output-folder"
# 検索条件 Markdownで ](http~) となっている箇所を外部リンクと判断
KEYWORD = r"]\(http([^\[]+)\)"
# ユーザエージェント
UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'}
# 自分のドメインは除外
MY_DOMAIN = 'yata-yatta-tan.xyz'

# =======================================================
# ログの設定
logger = logging.getLogger('DeadLinkCheck')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)


# =======================================================
# mdファイル一覧の取得
def get_md_list():
    name = sys._getframe().f_code.co_name
    logger.info(name + 'start')

    # globでフォルダ下にあるMarkdownファイルを探す
    mdlist = glob.glob(SRC_DIR + '\\**\\*.md', recursive=True)
    return mdlist


# =======================================================
# 外部リンク一覧の取得
def get_href_list(md_list):
    name = sys._getframe().f_code.co_name
    logger.info(name + 'start')

    # 初期化
    hreflist = []

    for md in md_list:
        # ファイル開く
        with open(md, mode='r', encoding='utf-8') as f:
            # 全行抽出
            lines = f.readlines()
            # 外部リンクのある箇所を抽出して、ファイル名とセットでリストに格納
            for line in lines:
                hrefs = re.findall(KEYWORD, line)
                for href in hrefs:
                    # 自サイトは除外
                    if MY_DOMAIN not in str(href):
                        md_and_href = ['http' + str(href), str(os.path.basename(md))]
                        hreflist.append(md_and_href)

    return hreflist


# =======================================================
# リンクへアクセス試行
def check_http_status(href_list):
    name = sys._getframe().f_code.co_name
    logger.info(name + 'start')

    # 初期化
    checklist = []

    for href in tqdm(href_list):
        url = href[0]
        md = href[1]

        try:
            # アクセス試行
            res = requests.get(url, headers=UA, timeout=3)
            status = str(res.status_code)

        except requests.Timeout:
            # タイムアウトエラー(たまにある)
            status = 'Timeout'

        except Exception:
            # その他うまくいかなかったとき
            status = 'UnexpctedError'

        # ステータス、url、ファイル名をセットにして配列に格納
        result = [status, url, md]
        checklist.append(result)

    return checklist


# =======================================================
# 結果をテキストファイルに出力
def output_to_file(check_list):
    name = sys._getframe().f_code.co_name
    logger.info(name + 'start')

    # 出力ファイル名の設定 link_check_YYYYmmdd-HHMMSS.txt
    now = datetime.datetime.now()
    filename = "link_check_" + now.strftime('%Y%m%d') + "-" + now.strftime('%H%M%S') + ".txt"

    # リストをテキストに書き込み
    with open(DST_DIR + r"/" + filename, mode="w", encoding="utf-8") as f:
        for check in check_list:
            f.write(','.join(check) + '\n')


# =======================================================
# メイン処理
def main():
    name = sys._getframe().f_code.co_name
    logger.info(name + 'start')

    # ファイル一覧の取得
    md_list = get_md_list()
    # 外部リンクを抽出
    href_list = get_href_list(md_list)
    # アクセス試行
    check_list = check_http_status(href_list)
    # 結果出力
    output_to_file(check_list)


if __name__ == '__main__':
    main()

出力結果

こんな感じになります。

200,https://loosedrawing.com/illust/0628/,2019-03-06-first-blog-post.md
200,https://loosedrawing.com/,2019-03-06-first-blog-post.md
200,https://www.kadokawa.co.jp/product/321801000474/,2019-03-06-first-blog-post.md
200,https://trello.com/,2019-03-13-unicode.md
...

自分の環境だと、370件程度で3分ちょっとかかりました。
tqdmを入れておくと超簡単に進捗が分かっていい感じです。

つくった経緯

ブログを生成しているPelicanでもリンク切れチェックのプラグインはあるのですが、以下の点が難点だと感じていました。

  • ページ生成のたびにチェックするほどでもない
  • ブラウザ以外でアクセスすると403 Forbiddenを返すサイトに対応できない
  • verboseモードで流してもどこの記事で貼ったリンクなのか分からない

だったら自分で作ろうかと。

要件

  • 記事ファイル(Markdownファイル)すべてを全文検索
  • 外部リンクをすべて抽出
  • リンクに対してアクセス試行
  • (不本意ながら)ユーザエージェントを偽装する
  • 正常でも異常でも、記事ファイル名と一緒に結果を返す
  • 動かすのは月1回程度にする

参考資料

大変お世話になりました!