WordPressの非公開ページへのリンクをLinuxで確認|簡易クローラーの作り方




はじめに

WordPressのページを公開状態から下書きや非公開へ変更したものの、メニューや過去記事からのリンクまで削除できたか分からないことがあります。更新作業が重なると、すべての導線を手作業で探すのは大変です。

そこで、トップページから同一ドメイン内のリンクを再帰的にたどり、対象URLを含む<a href>を探すLinux用の簡易クローラーを作成しました。設定と処理を2つのSHファイルへ分けているため、ドメインや対象URL、除外パスを変更して使い回せます。

この記事のスクリプトが確認するのは、読者がクリックできるaタグのリンク元です。HTML内に文字列があるだけのAPI参照やメタ情報とは区別して結果を表示します。

サイトマップの検索だけでは足りない理由

サイトマップのURLを1件ずつ取得して検索する方法でも、公開ページの本文は確認できます。ただし、サイトマップに含まれないページや、テーマ・メニューから見つかるURLを取りこぼす可能性があります。

今回の方法は、トップページから実際のリンクをたどって巡回します。

確認方法 得意なこと 注意点
サイトマップを検索 掲載URLをまとめて確認する サイトマップ外のページは確認できない
リンクを再帰的に巡回 実際の導線に近い範囲を確認する トップページから到達できないページは見つからない
ブラウザ型クローラー JavaScriptで作られるリンクも確認しやすい 導入や実行の負荷が大きい

「サイト内のどこからクリックできるのか」を知りたい場合は、リンク元とリンク先をセットで記録することが重要です。

作成する2つのファイル

今回は次の2ファイルを同じフォルダへ置きます。

  • crawl-config.sh: ドメイン、探すURL、上限、除外パス
  • crawl-check-links.sh: ページ取得、リンク抽出、巡回、結果保存
必要なコマンドはBash、curl、grep、sed、sortです。Ubuntu・WSL・Git Bashなど、これらを利用できる環境で実行します。

たとえば、ホームディレクトリのscriptsへ保存します。

mkdir -p ~/scripts
cd ~/scripts

設定ファイルを作成する

crawl-config.shを作り、次の内容を保存します。サンプルは架空のexample.comを使用しています。

# 調査対象サイト。末尾のスラッシュは付けません。
DOMAIN="https://example.com"

# href内で探すURLまたはパスの一部。
TARGET="/old-page/"

# 1回の実行で巡回する最大ページ数。続きは再実行できます。
MAX_PAGES_PER_RUN=300

# サーバー負荷を抑えるための待機秒数。
REQUEST_DELAY_SECONDS=1

# 1ページの取得を打ち切る秒数。
TIMEOUT_SECONDS=15

# 状態ファイルの保存先。
STATE_DIR="$SCRIPT_DIR/.crawl-state"

# 巡回しないURLの一部。必要に応じて追加します。
EXCLUDE_PATHS=(
  "/wp-json/"
  "/wp-admin/"
  "/wp-login.php"
  "/xmlrpc.php"
  "/feed/"
)

普段変更するのは、主にDOMAINとTARGETです。大量の個別ページや検索結果など、確認対象にしなくてよい場所はEXCLUDE_PATHSへ追加します。

クローラー本体を作成する

同じフォルダにcrawl-check-links.shを作成します。

#!/usr/bin/env bash

set -u

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${CRAWL_CONFIG_FILE:-$SCRIPT_DIR/crawl-config.sh}"
RESET_STATE=0

if [[ "${1:-}" == "--reset" ]]; then
  RESET_STATE=1
elif [[ -n "${1:-}" ]]; then
  echo "使い方: bash crawl-check-links.sh [--reset]" >&2
  exit 1
fi

if [[ ! -f "$CONFIG_FILE" ]]; then
  echo "設定ファイルが見つかりません: $CONFIG_FILE" >&2
  exit 1
fi

# 自分で管理している信頼できる設定ファイルだけを読み込んでください。
# shellcheck source=/dev/null
source "$CONFIG_FILE"

: "${DOMAIN:?crawl-config.shにDOMAINを設定してください}"
: "${TARGET:?crawl-config.shにTARGETを設定してください}"
: "${MAX_PAGES_PER_RUN:?MAX_PAGES_PER_RUNを設定してください}"
: "${REQUEST_DELAY_SECONDS:?REQUEST_DELAY_SECONDSを設定してください}"
: "${TIMEOUT_SECONDS:?TIMEOUT_SECONDSを設定してください}"
: "${STATE_DIR:?STATE_DIRを設定してください}"

if [[ ! "$DOMAIN" =~ ^https?://[^/]+$ ]]; then
  echo "DOMAINは https://example.com の形式で指定してください。" >&2
  exit 1
fi

DOMAIN="${DOMAIN%/}"
HOST="${DOMAIN#*://}"
SCHEME="${DOMAIN%%://*}"
USER_AGENT="InternalLinkChecker/1.0"

mkdir -p "$STATE_DIR"
VISITED_FILE="$STATE_DIR/visited.txt"
QUEUE_FILE="$STATE_DIR/queue.txt"
FOUND_FILE="$STATE_DIR/found.txt"

is_excluded() {
  local candidate="$1"
  local path
  for path in "${EXCLUDE_PATHS[@]}"; do
    if [[ -n "$path" && "$candidate" == *"$path"* ]]; then
      return 0
    fi
  done
  return 1
}

if [[ "$RESET_STATE" -eq 1 ]]; then
  rm -f -- "$VISITED_FILE" "$QUEUE_FILE" "$FOUND_FILE"
fi

touch "$VISITED_FILE" "$FOUND_FILE"
if [[ ! -e "$QUEUE_FILE" ]]; then
  printf '%s/\n' "$DOMAIN" > "$QUEUE_FILE"
fi

count=0

while [[ -s "$QUEUE_FILE" && "$count" -lt "$MAX_PAGES_PER_RUN" ]]; do
  url="$(head -n 1 "$QUEUE_FILE")"
  sed -i '1d' "$QUEUE_FILE"

  [[ -n "$url" ]] || continue
  if grep -qxF -- "$url" "$VISITED_FILE"; then
    continue
  fi

  printf '%s\n' "$url" >> "$VISITED_FILE"
  count=$((count + 1))
  printf '[%d/%d] 巡回中: %s\n' "$count" "$MAX_PAGES_PER_RUN" "$url"

  if ! html="$(curl -fsSL \
    --connect-timeout 5 \
    --max-time "$TIMEOUT_SECONDS" \
    --user-agent "$USER_AGENT" \
    "$url")"; then
    echo "  取得失敗: $url" >&2
    sleep "$REQUEST_DELAY_SECONDS"
    continue
  fi

  links="$(
    printf '%s' "$html" \
      | grep -Eio "<a[[:space:]][^>]*href[[:space:]]*=[[:space:]]*(\"[^\"]*\"|'[^']*')" \
      | sed -E "s/.*href[[:space:]]*=[[:space:]]*(\"([^\"]*)\"|'([^']*)').*/\2\3/I" \
      | sort -u \
      || true
  )"

  while IFS= read -r link; do
    [[ -n "$link" ]] || continue

    if [[ "$link" == *"$TARGET"* ]]; then
      result="$url -> $link"
      echo "  FOUND: $result"
      grep -qxF -- "$result" "$FOUND_FILE" || printf '%s\n' "$result" >> "$FOUND_FILE"
    fi

    link="${link%%#*}"
    link="${link%%\?*}"
    [[ -n "$link" ]] || continue

    case "$link" in
      "$DOMAIN"|"$DOMAIN"/*)
        absolute="$link"
        ;;
      "//$HOST"|"//$HOST"/*)
        absolute="$SCHEME:$link"
        ;;
      /*)
        absolute="$DOMAIN$link"
        ;;
      *)
        continue
        ;;
    esac

    case "${absolute,,}" in
      *.jpg|*.jpeg|*.png|*.gif|*.webp|*.svg|*.ico|*.pdf|*.zip|*.css|*.js)
        continue
        ;;
    esac

    is_excluded "$absolute" && continue

    if ! grep -qxF -- "$absolute" "$VISITED_FILE" \
      && ! grep -qxF -- "$absolute" "$QUEUE_FILE"; then
      printf '%s\n' "$absolute" >> "$QUEUE_FILE"
    fi
  done <<< "$links"

  sleep "$REQUEST_DELAY_SECONDS"
done

echo
echo "今回巡回したページ数: $count"
echo "累計巡回ページ数: $(wc -l < "$VISITED_FILE")"
echo "未巡回URL数: $(wc -l < "$QUEUE_FILE")"
echo "--- 発見結果 ---"

if [[ -s "$FOUND_FILE" ]]; then
  cat "$FOUND_FILE"
else
  echo "対象への<a href>は見つかりませんでした。"
fi

if [[ -s "$QUEUE_FILE" ]]; then
  echo "続きは同じコマンドを再実行してください。最初から調べ直す場合は --reset を付けます。"
fi

このスクリプトでは、curlの--max-timeでページごとの待ち時間に上限を設け、--location相当の-Lでリダイレクトを追跡しています。取得したHTMLから<a href>だけを抜き出し、対象文字列を含むリンクを記録します。

curl公式マニュアルを確認する

grepやパイプの動きを先に確認したい場合は、grepと標準入力の基本とLinuxのパイプの仕組みも参考になります。

Git BashまたはWSLから実行する

初回は--resetを付け、以前の巡回結果を消して開始します。

cd ~/scripts
bash crawl-check-links.sh --reset

300件に達して未巡回URLが残った場合は、オプションを付けずに再実行します。前回のqueue.txtとvisited.txtが残るため、続きから処理できます。

bash crawl-check-links.sh

Windows側のフォルダへ保存した場合は、利用中のターミナルによってパスが変わります。

環境 Cドライブの表記例
Git Bash /c/Users/ユーザー名/Documents/scripts/
WSL /mnt/c/Users/ユーザー名/Documents/scripts/

MINGW64と表示されるのがGit Bash、ユーザー名@PC:~$のように表示されるのがWSLの目安です。プロンプトの$より前は入力せず、コマンド部分だけを実行します。

FOUNDの結果を確認する

対象リンクが見つかると、次の形式で表示されます。

FOUND: https://example.com/source-page/ -> /old-page/

左側がリンクを置いているページ、右側が実際のhrefです。.crawl-state/found.txtにも同じ結果が保存されます。該当ページの本文、メニュー、ウィジェットなどを確認してリンクを削除し、--reset付きで再実行します。

「見つかりませんでした」は絶対に導線がないという保証ではありません。JavaScriptで生成されるリンク、iframeのsrc、フォームのaction、トップページから到達できないページ、ログイン後だけ表示されるリンクは別途確認が必要です。

実行前に確認したい注意点

クローラーは読み取り目的のGETアクセスですが、巡回数だけリクエストが発生します。自分が管理しているサイト、または実行許可を得たサイトだけを対象にしてください。

  1. https://example.com/robots.txtとサイトの利用条件を確認する
  2. REQUEST_DELAY_SECONDSを0にせず、まず1秒以上で試す
  3. MAX_PAGES_PER_RUNを小さくして対象範囲を確認する
  4. 管理画面、API、フィード、大量の一覧ページを除外する
  5. アクセスログやサーバー負荷を確認しながら実行する

対象ページそのものが一般利用者から開けないかは、リンク調査とは別にログアウト状態のブラウザで確認します。HTTPレスポンスだけを確認する場合は、次のように実行できます。

curl -I -L --max-time 15 https://example.com/old-page/

Basic認証のID・パスワード、Cookie、顧客名、実際の管理URLは、記事、画面キャプチャ、Git管理する設定ファイルへ含めないでください。

まとめ

WordPressでページを非公開にした後は、ページの状態だけでなく、サイト内に古いリンクが残っていないかも確認する必要があります。

今回の簡易クローラーは、同一ドメインの<a href>を再帰的にたどり、対象URLのリンク元を記録します。設定ファイルを分けているため、ドメイン、検索対象、除外パス、巡回上限を変更して再利用できます。

結果が0件でも、JavaScript、iframe、フォーム、会員ページなどは対象外です。最後はログアウト状態での画面確認と組み合わせ、確認できた範囲を明確にして作業を完了します。

参考情報

コメント

タイトルとURLをコピーしました