再帰的なウェブサイト内リンクチェッカーをPythonで作成する

2023-03-23-03-16-39.webp
目次

はじめに

ウェブサイトを運営していると、リンク切れを起こすことがあります。リンク切れはユーザーエクスペリエンスを低下させ、SEOにも悪影響を与えるため、定期的にチェックして修正することが重要です。しかし、手動でサイト内のすべてのリンクをチェックするのは大変な作業です。この記事では、Pythonを使って再帰的にウェブサイト内のリンクをチェックし、リンク切れを検出するスクリプトを作成する方法を紹介します。

ウェブクローラの実装

指定されたURLから内部リンク切れを検出する基本的なウェブクローラーを作っていきます。 今回はPythonを使います。

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse

def is_valid_url(url, base_url):
    parsed_url = urlparse(url)
    return bool(parsed_url.netloc) and parsed_url.netloc != urlparse(base_url).netloc

def get_internal_links(url, base_url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    internal_links = []
    
    for link in soup.find_all('a'):
        href = link.get('href')
        
        if href and not is_valid_url(href, base_url):
            internal_links.append(urljoin(base_url, href))
    
    return internal_links

def check_broken_links(url, base_url):
    internal_links = get_internal_links(url, base_url)
    broken_links = []
    
    for link in internal_links:
        response = requests.get(link)
        
        if response.status_code != 200:
            broken_links.append(link)
            print(f'Broken link: {link}')
    
    return broken_links

if __name__ == '__main__':
    base_url = 'https://yourwebsite.com'
    broken_links = check_broken_links(base_url, base_url)
    
    if not broken_links:
        print('No broken links found!')

コードの解説

ライブラリのインポート

requestsライブラリは、ウェブページの情報を取得するために使用されます。BeautifulSoupは、HTMLの解析と操作を容易にするために使用されます。

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse

is_valid_url関数

この関数は、与えられたURLが外部リンクかどうかを判定します。外部リンクは検査の対象外としています。

def is_valid_url(url, base_url):
    parsed_url = urlparse(url)
    return bool(parsed_url.netloc) and parsed_url.netloc != urlparse(base_url).netloc

get_internal_links関数

この関数は、指定されたURLのページから内部リンクを抽出します。BeautifulSoupを使用してHTMLを解析し、タグのhref属性からリンクを取得します。

def get_internal_links(url, base_url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    internal_links = []
    
    for link in soup.find_all('a'):
        href = link.get('href')
        
        if href and not is_valid_url(href, base_url):
            internal_links.append(urljoin(base_url, href))
    
    return internal_links

check_broken_links関数

この関数は、指定されたURLから内部リンク切れをチェックします。get_internal_links関数を使用して内部リンクを取得し、それぞれのリンクに対してHTTPリクエストを行い、ステータスコードが200以外の場合、リンク切れとして報告します。

def check_broken_links(url, base_url):
    internal_links = get_internal_links(url, base_url)
    broken_links = []
    
    for link in internal_links:
        response = requests.get(link)
        
        if response.status_code != 200:
            broken_links.append(link)
            print(f'Broken link: {link}')
    
    return broken_links

メイン処理

この部分は、実際にcheck_broken_links関数を呼び出し、リンク切れをチェックする部分です。base_urlに検査したいウェブサイトのURLを設定してください。

if __name__ == '__main__':
    base_url = 'https://yourwebsite.com'
    broken_links = check_broken_links(base_url, base_url)
    
    if not broken_links:
        print('No broken links found!')

このコードは、最も基本的な内部リンク切れチェックツールです。より詳細なクローリングやリンク切れ検出機能を追加することで、ウェブサイトのSEO対策に役立つツールを作成できます。

コネクションのリモートホストによる切断エラー

エラーの一部を記載します。

"C:\Users\kenpo\AppData\Local\Programs\Python\Python310\lib\ssl.py", line 1342, in do_handshake
    self._sslobj.do_handshake()
urllib3.exceptions.ProtocolError: ('Connection aborted.', ConnectionResetError(10054, '既存の接続はリモート ホストに強制的に切断されました。', None, 10054, None))

対象のWebサイトが一定数のリクエストに対して制限していることがあります。

リクエスト間のタイムアウトを設定する

シンプルですが、time.sleep()を使用してリクエスト間に一定の時間を設けることで、サーバーに対する負荷を軽減し、エラーを回避できる場合があります。

import time

# その他のインポートや関数定義は省略

def check_broken_links(url, base_url):
    internal_links = get_internal_links(url, base_url)
    broken_links = []
    
    for link in internal_links:
        response = requests.get(link)
        
        if response.status_code != 200:
            broken_links.append(link)
            print(f'Broken link: {link}')
        
        time.sleep(1)  # 1秒待機(適切な秒数に調整してください)
    
    return broken_links

リトライ設定を追加する

requestsライブラリのリトライ機能を利用して、失敗したリクエストを自動的に再試行するように設定できます。

from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

# その他のインポートや関数定義は省略

def get_requests_session(retries=3, backoff_factor=0.3):
    session = requests.Session()
    retry = Retry(
        total=retries,
        backoff_factor=backoff_factor,
        status_forcelist=[429, 500, 502, 503, 504],
        method_whitelist=["HEAD", "GET", "OPTIONS"]
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session

def check_broken_links(url, base_url):
    session = get_requests_session()
    internal_links = get_internal_links(url, base_url, session)
    broken_links = []
    
    for link in internal_links:
        response = session.get(link)
        
        if response.status_code != 200:
            broken_links.append(link)
            print(f'Broken link: {link}')
    
    return broken_links

# get_internal_links関数でsessionを使用するように変更
def get_internal_links(url, base_url, session):
    response = session.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    internal_links = []
    
    for link in soup.find_all('a'):
        href = link.get('href')
        
        if href and not is_valid_url(href, base_url):
            internal_links.append(urljoin(base_url, href))
    
    return internal_links

ユーザーエージェントを変更する

ウェブサイトは、デフォルトのPython requestsユーザーエージェントをブロックしている場合があります。リクエスト時に、異なるユーザーエージェントを設定することで、この問題を回避できることがあります。

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
}

# get_internal_links関数を次のように修正します。
def get_internal_links(url, base_url, session):
    response = session.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')
    internal_links = []
    
    for link in soup.find_all('a'):
        href = link.get('href')
        
        if href and not is_valid_url(href, base_url):
            internal_links.append(urljoin(base_url, href))
    
    return internal_links

# check_broken_links関数を次のように修正します。
def check_broken_links(url, base_url):
    session = get_requests_session()
    internal_links = get_internal_links(url, base_url, session)
    broken_links = []
    
    for link in internal_links:
        response = session.get(link, headers=headers)
        
        if response.status_code != 200:
            broken_links.append(link)
            print(f'Broken link: {link}')
    
    return broken_links

これらの対策を組み合わせることで、エラーが回避できる可能性が高まります。ただし、ウェブサイト側が特定の条件でアクセス制限をかけている場合、完全に回避できるとは限りません。その場合は、ウェブサイトの管理者に問い合わせてアクセス制限の解除を依頼することを検討してください。

コード全文

このコードには、リクエスト間のタイムアウト、リトライ設定の追加、およびユーザーエージェントの変更が含まれています。対象のウェブサイトのURLをbase_urlに設定して実行すると良いです。

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
import time

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
}

def is_valid_url(url, base_url):
    return url.startswith(base_url)

def get_requests_session(retries=3, backoff_factor=0.3):
    session = requests.Session()
    retry = Retry(
        total=retries,
        backoff_factor=backoff_factor,
        status_forcelist=[429, 500, 502, 503, 504],
        method_whitelist=["HEAD", "GET", "OPTIONS"]
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session

def get_internal_links(url, base_url, session):
    response = session.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')
    internal_links = []
    
    for link in soup.find_all('a'):
        href = link.get('href')
        
        if href and not is_valid_url(href, base_url):
            internal_links.append(urljoin(base_url, href))
    
    return internal_links

def check_broken_links(url, base_url):
    session = get_requests_session()
    internal_links = get_internal_links(url, base_url, session)
    broken_links = []
    
    for link in internal_links:
        if is_http_url(link):
            response = session.get(link, headers=headers)
            
            if response.status_code != 200:
                broken_links.append(link)
                print(f'Broken link: {link}')
        time.sleep(1)  # 1秒待機(適切な秒数に調整してください)
    
    return broken_links

def is_http_url(url):
    return url.startswith("http://") or url.startswith("https://")

if __name__ == '__main__':
    base_url = 'https://kenpos.dev'  # ここに対象のウェブサイトURLを入力してください
    broken_links = check_broken_links(base_url, base_url)
    print(f'Found {len(broken_links)} broken links:')
    for link in broken_links:
        print(link)

結果

サイトを走査して壊れたリンクを一覧化してくれます。

python .\main.py
D:\src\Python\webclorer\main.py:17: DeprecationWarning: Using 'method_whitelist' with Retry is deprecated and will be removed in v2.0. Use 'allowed_methods' instead
  retry = Retry(
Broken link: https://kenpos.dev/tags/juman++/

やったね。

再帰的にサイト内を走査

再帰的にサイト内を走査し、確認したURLを表示しながらリンク切れのアドレスを出力する機能を追加します。

def get_internal_links_recursive(url, base_url, session, visited=None):
    if visited is None:
        visited = set()

    if url in visited:
        return visited

    visited.add(url)
    internal_links = get_internal_links(url, base_url, session)

    for link in internal_links:
        if is_http_url(link) and link not in visited:
            visited = get_internal_links_recursive(link, base_url, session, visited)

    return visited

def check_broken_links_recursive(url, base_url):
    session = get_requests_session()
    internal_links = get_internal_links_recursive(url, base_url, session)
    broken_links = []

    for link in internal_links:
        print(f'Checking: {link}')
        response = session.get(link, headers=headers)

        if response.status_code != 200:
            broken_links.append(link)
            print(f'Broken link: {link}')

        time.sleep(1)  # 1秒待機(適切な秒数に調整してください)

    return broken_links

if __name__ == '__main__':
    base_url = 'https://kenpos.dev'  # ここに対象のウェブサイトURLを入力してください
    broken_links = check_broken_links_recursive(base_url, base_url)
    print(f'Found {len(broken_links)} broken links:')
    for link in broken_links:
        print(link)

このコードでは、get_internal_links_recursive関数が再帰的に内部リンクを取得し、check_broken_links_recursive関数がリンク切れのアドレスをチェックしています。対象のウェブサイトのURLをbase_urlに設定して実行する。

高速化

検出が遅いのでいくつか対応策を練ります。

マルチスレッドを使用する

マルチスレッディングを使ってリンク切れのチェックを並列化することで、処理速度を向上させることができます。例えば、concurrent.futures.ThreadPoolExecutorを使って、複数のスレッドでリンク切れの検出を行うことができます。

import concurrent.futures

def check_link(url):
    response = session.get(url, headers=headers)
    if response.status_code != 200:
        print(f'Broken link: {url}')
        return url
    return None

def check_broken_links_recursive(url, base_url):
    session = get_requests_session()
    internal_links = get_internal_links_recursive(url, base_url, session)
    broken_links = []

    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
        future_to_url = {executor.submit(check_link, link): link for link in internal_links}
        for future in concurrent.futures.as_completed(future_to_url):
            result = future.result()
            if result is not None:
                broken_links.append(result)

    return broken_links

非同期処理 (Asynchronous) を使う

asyncioライブラリとaiohttpライブラリを使って、非同期処理でリンク切れのチェックを行うことができます。これにより、同時に複数のリンク切れの検出を行うことができ、処理速度を向上させることができます。

import asyncio
import aiohttp

async def check_link_async(session, url):
    async with session.get(url, headers=headers) as response:
        if response.status != 200:
            print(f'Broken link: {url}')
            return url
    return None

async def check_broken_links_recursive_async(url, base_url):
    session = aiohttp.ClientSession()
    internal_links = get_internal_links_recursive(url, base_url, session)
    broken_links = []

    tasks = []
    for link in internal_links:
        task = asyncio.ensure_future(check_link_async(session, link))
        tasks.append(task)

    results = await asyncio.gather(*tasks)
    await session.close()

    for result in results:
        if result is not None:
            broken_links.append(result)

    return broken_links

if __name__ == '__main__':
    base_url = 'https://kenpos.dev'  # ここに対象のウェブサイトURLを入力してください
    loop = asyncio.get_event_loop()
    broken_links = loop.run_until_complete(check_broken_links_recursive_async(base_url, base_url))
    loop.close()
    print(f'Found {len(broken_links)} broken links:')
    for link in broken_links:
        print(link)

並列処理を行う際は、ウェブサイトに負荷をかけすぎないように注意してください。適切なスレッド数や同時リクエスト数を設定し、必要に応じてリクエスト間のウェイト時間を調整してください。

キャッシュを利用する

リンク切れのチェックが遅い原因の1つは、同じURLへの繰り返しリクエストです。これを解決する方法として、キャッシュを利用してすでにチェック済みのURLに対するリクエストを省略できます。cachetoolsライブラリを使ってキャッシュを実装しましょう。

import cachetools

cache = cachetools.LRUCache(maxsize=100)

def check_link(url):
    if url in cache:
        return cache[url]

    response = session.get(url, headers=headers)
    if response.status_code != 200:
        print(f'Broken link: {url}')
        cache[url] = url
        return url

    cache[url] = None
    return None

訪問済みURLの管理を工夫する

再帰的なクローラーで同じURLを何度も訪問しないように、訪問済みURLの管理を工夫することで処理速度を向上させることができます。以下のようにvisitedセットを引数で渡すのではなく、グローバルな変数として管理しましょう。

visited = set()

def get_internal_links_recursive(url, base_url, session):
    global visited

    if url in visited:
        return

    visited.add(url)
    internal_links = get_internal_links(url, base_url, session)

    for link in internal_links:
        if is_http_url(link) and link not in visited:
            get_internal_links_recursive(link, base_url, session)

これらの方法を組み合わせることで、リンク切れの検出速度をより高速化できます。ただし、ウェブサイトへのアクセス速度や負荷を考慮し、適切な設定やバランスを見つけることが重要です。

ウェブサイトへのアクセス速度や負荷を考慮し、適切な設定やバランスを見つけることが重要です。マルチスレッディングのmax_workersパラメータを調整して、最適なスレッド数を設定してください。また、必要に応じてリクエスト間のウェイト時間を調整が必要です。

まとめ

この記事では、Pythonを使ってウェブサイト内のリンク切れを検出するスクリプトの作成方法を学びました。このスクリプトは、サイト内を再帰的に走査し、リンク切れがあるかどうかをチェックして出力します。これにより、ウェブサイトのメンテナンスが容易になり、ユーザーエクスペリエンスとSEOを向上させることができます。今後もウェブ開発に役立つ情報をお届けしますので、ぜひチェックしてください。

Related Post

> 再帰的なウェブサイト内リンクチェッカーをPythonで作成する
HugoでSEO対策を施し、関連記事機能を実装した
> 再帰的なウェブサイト内リンクチェッカーをPythonで作成する
Nuxt.jsブログに全文検索機能を追加する方法
> 再帰的なウェブサイト内リンクチェッカーをPythonで作成する
ブログ作りを進める - パンくずリストの実装方法
> 再帰的なウェブサイト内リンクチェッカーをPythonで作成する
HugoブログをGithubActionを使ってS3サーバに差分アップする方法
> 再帰的なウェブサイト内リンクチェッカーをPythonで作成する
HugoでSEOスコアを改善するためにやったこと
> 再帰的なウェブサイト内リンクチェッカーをPythonで作成する
HugoでMobile端末向けのSEO対策を実践していく【画像の適正サイズ】

おすすめの商品

>