はじめに
WordPressのページを公開状態から下書きや非公開へ変更したものの、メニューや過去記事からのリンクまで削除できたか分からないことがあります。更新作業が重なると、すべての導線を手作業で探すのは大変です。
そこで、トップページから同一ドメイン内のリンクを再帰的にたどり、対象URLを含む<a href>を探すLinux用の簡易クローラーを作成しました。設定と処理を2つのSHファイルへ分けているため、ドメインや対象URL、除外パスを変更して使い回せます。
サイトマップの検索だけでは足りない理由
サイトマップのURLを1件ずつ取得して検索する方法でも、公開ページの本文は確認できます。ただし、サイトマップに含まれないページや、テーマ・メニューから見つかるURLを取りこぼす可能性があります。
今回の方法は、トップページから実際のリンクをたどって巡回します。
| 確認方法 | 得意なこと | 注意点 |
|---|---|---|
| サイトマップを検索 | 掲載URLをまとめて確認する | サイトマップ外のページは確認できない |
| リンクを再帰的に巡回 | 実際の導線に近い範囲を確認する | トップページから到達できないページは見つからない |
| ブラウザ型クローラー | JavaScriptで作られるリンクも確認しやすい | 導入や実行の負荷が大きい |
「サイト内のどこからクリックできるのか」を知りたい場合は、リンク元とリンク先をセットで記録することが重要です。
作成する2つのファイル
今回は次の2ファイルを同じフォルダへ置きます。
crawl-config.sh: ドメイン、探すURL、上限、除外パスcrawl-check-links.sh: ページ取得、リンク抽出、巡回、結果保存
たとえば、ホームディレクトリの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>だけを抜き出し、対象文字列を含むリンクを記録します。
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付きで再実行します。
実行前に確認したい注意点
クローラーは読み取り目的のGETアクセスですが、巡回数だけリクエストが発生します。自分が管理しているサイト、または実行許可を得たサイトだけを対象にしてください。
https://example.com/robots.txtとサイトの利用条件を確認するREQUEST_DELAY_SECONDSを0にせず、まず1秒以上で試すMAX_PAGES_PER_RUNを小さくして対象範囲を確認する- 管理画面、API、フィード、大量の一覧ページを除外する
- アクセスログやサーバー負荷を確認しながら実行する
対象ページそのものが一般利用者から開けないかは、リンク調査とは別にログアウト状態のブラウザで確認します。HTTPレスポンスだけを確認する場合は、次のように実行できます。
curl -I -L --max-time 15 https://example.com/old-page/
Basic認証のID・パスワード、Cookie、顧客名、実際の管理URLは、記事、画面キャプチャ、Git管理する設定ファイルへ含めないでください。
まとめ
WordPressでページを非公開にした後は、ページの状態だけでなく、サイト内に古いリンクが残っていないかも確認する必要があります。
今回の簡易クローラーは、同一ドメインの<a href>を再帰的にたどり、対象URLのリンク元を記録します。設定ファイルを分けているため、ドメイン、検索対象、除外パス、巡回上限を変更して再利用できます。
結果が0件でも、JavaScript、iframe、フォーム、会員ページなどは対象外です。最後はログアウト状態での画面確認と組み合わせ、確認できた範囲を明確にして作業を完了します。


コメント