本文へ移動
cccskills
無料GitHub で公開

web-archive

ウェブページのアーカイブ(保存)と、キャッシュ・削除済みページの取得を支援する。閲覧できなくなったページへのアクセス、ウェブ内容の保全、証拠としてのアーカイブ作成、多重保存ワークフローの構築に使う。

Wayback Machine、Archive.today、ArchiveBox、国立国会図書館WARP、証拠保全(日時記録・ハッシュ)を扱う。「ページが消える前に保存したい」「削除されたページを取りたい」「証拠として残したい」などの依頼で使う。

sgbms1/ja-news-skillsスター 3(GitHub 3 + cccskills 0)
連携ツールX (Twitter)Docker

実行スコア 未検証

インストール方法を見る

含まれるファイル(1)

  • SKILL.md38.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

ウェブアーカイブの方法論

調査報道・取材・記録保全のために、アクセスできないウェブページを取得し、ウェブ内容を保存するための手順集。

削除や改変の前にページを押さえることは、調査報道における証拠保全そのものである。取材源が消す前、企業が訂正する前、SNS投稿が削除される前に保存しておく。本スキルは一般的な手引きであり、法的助言ではない。証拠として用いる場合の取り扱いは、自社の法務・デスクと相談すること。

アーカイブサービスの優先順位

最大限カバーするため、次の順で試す。

┌─────────────────────────────────────────────────────────────────┐
│                    アーカイブ取得カスケード                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. Wayback Machine (archive.org)                               │
│     └─ 9000億ページ超、長期の履歴、API公開                      │
│                         ↓ 見つからない                          │
│  2. Archive.today (archive.is/archive.ph)                       │
│     └─ オンデマンド保存、ペイウォール回避                       │
│     └─ 注意(2026): 2025年10月にFBIが登録業者へ召喚状。          │
│        2026年2月にWikipediaが引用元として非推奨化。             │
│        法的・引用用途ではWayback等を優先する                    │
│                         ↓ 見つからない                          │
│  3. Memento Time Travel(横断検索アグリゲータ)                  │
│     └─ 複数のアーカイブを同時に検索                             │
│                                                                  │
│  日本の公的アーカイブ:                                          │
│  ・国立国会図書館 WARP(インターネット資料収集保存事業)        │
│    https://warp.da.ndl.go.jp/ ─ 国・自治体・公的機関サイト等を │
│    継続収集。官公庁ページの過去版を探すときの第一候補。         │
│                                                                  │
│  廃止済み(使用不可):Googleキャッシュ(`cache:`演算子)は     │
│  2024年9月に終了。Bingのキャッシュ表示も同年に削除。いずれも    │
│  かつてはこのカスケードに含まれていた。                         │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

WARP補足:国立国会図書館のWARPは主に公的機関・公益団体等のサイトを対象とした保存事業で、誰でも検索・閲覧できる。官公庁・自治体・国立大学・公的団体のページが消えた/差し替えられた場合に有力。一方で民間サイトや一般のニュース記事は対象範囲が限られるため、Wayback Machine等と併用する。

Wayback Machine API

URLがアーカイブ済みか確認する

import requests
from typing import Optional
from datetime import datetime
from urllib.parse import quote, unquote

def check_wayback_availability(url: str) -> Optional[dict]:
    """指定URLがWayback Machineに存在するか確認する。"""
    api_url = "https://archive.org/wayback/available"

    try:
        response = requests.get(api_url, params={'url': url}, timeout=10)
        data = response.json()

        if data.get('archived_snapshots', {}).get('closest'):
            snapshot = data['archived_snapshots']['closest']
            return {
                'available': snapshot.get('available', False),
                'url': snapshot.get('url'),
                'timestamp': snapshot.get('timestamp'),
                'status': snapshot.get('status')
            }
        return None
    except Exception as e:
        return None

def get_wayback_url(url: str, timestamp: str = None) -> str:
    """あるページのWayback Machine URLを生成する。

    Waybackの再生URL規約に従い、正規の生形式
    (`.../web/<timestamp>/<url>`)を返す。返したリンクを
    ブラウザで開く予定で、かつ対象URLに `#` フラグメントが
    含まれる場合は、ブラウザがリクエスト送出前にフラグメントを
    落とさないよう、呼び出し側で urllib.parse.quote によって
    エンコードすること。

    Args:
        url: 取得したい元のURL
        timestamp: 任意。YYYYMMDDHHMMSS形式。Noneなら最新版
    """
    if timestamp:
        return f"https://web.archive.org/web/{timestamp}/{url}"
    return f"https://web.archive.org/web/{url}"

ページをWayback Machineに保存する

def save_to_wayback(url: str, s3_keys: Optional[tuple[str, str]] = None) -> Optional[str]:
    """Save Page Now 経由でWayback MachineにURLの保存を依頼する。

    成功すればアーカイブ後のURLを返す。

    匿名リクエストは概ね毎分15件程度に制限される。Internet Archive
    アカウントの `s3_keys=(access_key, secret)` を渡すと上限が上がり
    (匿名→認証で毎分50件程度)、ペイウォールや重いJSレンダリングの
    ページで保存が黙って失敗するのを避けられる。
    """
    # quote(unquote(url), ...) は既存の %xx エスケープを先に正規化し、
    # %25xx への二重エンコードを防ぐ。
    save_url = f"https://web.archive.org/save/{quote(unquote(url), safe='')}"

    headers = {'User-Agent': 'Mozilla/5.0 (research-archiver)'}
    if s3_keys:
        headers['Authorization'] = f'LOW {s3_keys[0]}:{s3_keys[1]}'

    try:
        response = requests.get(save_url, headers=headers, timeout=60)

        if response.status_code == 200:
            # SPN はリダイレクト追従後の最終URL(非同期キャプチャの
            # 場合は `Link` ヘッダ)で正規のアーカイブURLを返す。
            # 一般的なケースでは `response.url` が信頼できる。
            return response.url
        return None
    except Exception:
        return None

履歴スナップショットのためのCDX API

def get_all_snapshots(url: str, limit: int = 100) -> list[dict]:
    """CDX APIを使って、あるURLの全アーカイブ済みスナップショットを取得する。

    タイムスタンプとステータスコードを含むスナップショットの一覧を返す。
    """
    cdx_url = "https://web.archive.org/cdx/search/cdx"
    params = {
        'url': url,
        'output': 'json',
        'limit': limit,
        'fl': 'timestamp,original,statuscode,digest,length'
    }

    try:
        response = requests.get(cdx_url, params=params, timeout=30)
        data = response.json()

        if len(data) < 2:  # 先頭行はヘッダ
            return []

        headers = data[0]
        snapshots = []

        for row in data[1:]:
            snapshot = dict(zip(headers, row))
            snapshot['wayback_url'] = (
                f"https://web.archive.org/web/{snapshot['timestamp']}/{snapshot['original']}"
            )
            snapshots.append(snapshot)

        return snapshots
    except Exception:
        return []

digest(コンテンツのハッシュ)は、同一URLの版が「いつ実際に変わったか」を見分けるのに使える。digestが同じスナップショットは内容が同一で、digestが変わった時点が改変のタイミングを示す。差し替え・訂正の検証に有用。

Archive.today の利用

Archive.today に保存する

import re
import requests
from urllib.parse import quote, unquote, urljoin

def save_to_archive_today(url: str) -> Optional[str]:
    """URLをArchive.todayに送信してアーカイブを依頼する。

    注意:Archive.todayはレート制限とCAPTCHAがある。本関数は
    基本的な保存には機能するが、大量利用では手動対応が必要に
    なることがある。

    運用上の注意(2026):2025年10月にFBIがarchive.todayの登録
    業者へ召喚状を出した。2026年1月に同サイトがDDoS攻撃コードを
    配信したとして、2026年2月にWikipediaが引用元としての受け入れ
    を停止した。Wayback Machineで描画できないコンテンツの取得には
    依然有用だが、法的・引用用途ではWayback / 公的アーカイブを
    優先し、本サービスは補助として扱う。
    """
    submit_url = "https://archive.today/submit/"

    data = {
        'url': url,
        'anyway': '1'  # 直近のスナップショットがあっても再保存する
    }

    try:
        response = requests.post(
            submit_url,
            data=data,
            timeout=60,
            allow_redirects=False,
            headers={'User-Agent': 'Mozilla/5.0 (research-archiver)'},
        )
        # archive.today はスナップショットURLを次の2形態のいずれかで返す:
        #   - 30x で Location: https://archive.today/<snapshot_id>
        #     (Location は RFC 7231 上、相対でもよい)
        #   - 200 で Refresh: 0;url=https://archive.today/<snapshot_id>
        #     (Refresh キーワードはHTML仕様上、大文字小文字を区別しない)
        # 黙ってリダイレクトを追うと /wip/ ページに着地したり正規の
        # スナップショットURLを取り逃すため、両ヘッダを明示的に処理する。
        if response.status_code in (301, 302, 303, 307, 308):
            location = response.headers.get('Location')
            if location:
                return urljoin(response.url, location)
        if response.status_code == 200:
            refresh = response.headers.get('Refresh', '')
            m = re.search(r'\burl\s*=\s*(.+)', refresh, re.IGNORECASE)
            if m:
                target = m.group(1).strip().strip('\'"')
                return urljoin(response.url, target)
        return None
    except Exception:
        return None

def search_archive_today(url: str) -> Optional[str]:
    """既存のArchive.todayスナップショットを検索する。

    /newest/<url> 参照を使う。これは最新スナップショットへ302で
    転送する(レート制限時はCAPTCHAページへ転送される)。
    """
    search_url = f"https://archive.ph/newest/{quote(unquote(url), safe='')}"

    try:
        response = requests.get(
            search_url,
            timeout=30,
            allow_redirects=False,
            headers={'User-Agent': 'Mozilla/5.0 (research-archiver)'},
        )
        if response.status_code in (301, 302, 303, 307, 308):
            location = response.headers.get('Location')
            if location:
                resolved = urljoin(response.url, location)
                # CAPTCHAではなくアーカイブページに着地した場合のみ返す
                if 'archive.' in resolved and '/newest/' not in resolved:
                    return resolved
        return None
    except Exception:
        return None

多重アーカイブによる冗長化

最大限の保全のためのアーカイブ・カスケード

複数サービスに同時保存することで、特定サービスの停止・削除・係争に左右されにくくする。証拠保全の基本は「1か所に頼らない」こと。

from dataclasses import dataclass
from typing import Optional, List
from concurrent.futures import ThreadPoolExecutor, as_completed

@dataclass
class ArchiveResult:
    service: str
    url: str
    archived_url: Optional[str]
    success: bool
    error: Optional[str] = None

class MultiArchiver:
    """冗長化のため、URLを複数サービスにアーカイブする。"""

    def __init__(self):
        self.services = [
            ('wayback', self._save_wayback),
            ('archive_today', self._save_archive_today),
            ('perma_cc', self._save_perma),  # APIキーが必要
        ]

    def archive_url(self, url: str, parallel: bool = True) -> List[ArchiveResult]:
        """URLを全サービスにアーカイブする。

        Args:
            url: アーカイブするURL
            parallel: Trueなら全サービスに同時保存する
        """
        results = []

        if parallel:
            with ThreadPoolExecutor(max_workers=3) as executor:
                futures = {
                    executor.submit(save_func, url): name
                    for name, save_func in self.services
                }

                for future in as_completed(futures):
                    service = futures[future]
                    try:
                        archived_url = future.result()
                        results.append(ArchiveResult(
                            service=service,
                            url=url,
                            archived_url=archived_url,
                            success=archived_url is not None
                        ))
                    except Exception as e:
                        results.append(ArchiveResult(
                            service=service,
                            url=url,
                            archived_url=None,
                            success=False,
                            error=str(e)
                        ))
        else:
            for name, save_func in self.services:
                try:
                    archived_url = save_func(url)
                    results.append(ArchiveResult(
                        service=name,
                        url=url,
                        archived_url=archived_url,
                        success=archived_url is not None
                    ))
                except Exception as e:
                    results.append(ArchiveResult(
                        service=name,
                        url=url,
                        archived_url=None,
                        success=False,
                        error=str(e)
                    ))

        return results

    def _save_wayback(self, url: str) -> Optional[str]:
        return save_to_wayback(url)

    def _save_archive_today(self, url: str) -> Optional[str]:
        return save_to_archive_today(url)

    def _save_perma(self, url: str) -> Optional[str]:
        # Perma.cc のAPIキーが必要
        # APIクレデンシャルがある前提で実装する
        return None

ArchiveBox によるセルフホスト・アーカイブ

外部サービスに依存せず、手元で完全な保存物(HTML・スクリーンショット・PDF・WARC等)を残したいときに使う。取材源の保護や、サービス側の削除・係争リスクを避けたい証拠保全に向く。

ArchiveBox のセットアップ

# 推奨:Docker Compose(v0.8系はChromium、yt-dlp、wget、single-file
# などのキャプチャ用ツールを同梱)。
mkdir ~/web-archives && cd ~/web-archives
curl -O 'https://docker-compose.archivebox.io' && mv docker-compose.archivebox.io docker-compose.yml
docker compose run archivebox init --setup
docker compose up -d                    # :8000 でWeb UIを起動

# pip でのインストールも可能だが、完全なキャプチャには Chromium /
# yt-dlp / wget / single-file を別途入れる必要がある:
# pip install archivebox && archivebox init

# アーカイブするURLを追加(アーカイブ用ディレクトリ内で実行)
docker compose run archivebox add "https://example.com/article"

# ファイルから複数URLを追加
docker compose run archivebox add --depth=0 < urls.txt

# 定期アーカイブをスケジュール
docker compose run archivebox schedule --every=day --depth=1 "https://example.com/feed.rss"

ArchiveBox の Python 連携

import subprocess
from pathlib import Path
from typing import List, Optional

class ArchiveBoxManager:
    """ローカルのArchiveBoxインスタンスを管理する。"""

    def __init__(self, archive_dir: Path):
        self.archive_dir = archive_dir
        self._ensure_initialized()

    def _ensure_initialized(self):
        """必要ならArchiveBoxを初期化する。"""
        if not (self.archive_dir / 'index.sqlite3').exists():
            subprocess.run(
                ['archivebox', 'init'],
                cwd=self.archive_dir,
                check=True
            )

    def add_url(self, url: str, depth: int = 0) -> bool:
        """単一URLをアーカイブする。

        Args:
            url: アーカイブするURL
            depth: 0で単一ページ、1でリンクを1階層たどる
        """
        result = subprocess.run(
            ['archivebox', 'add', f'--depth={depth}', url],
            cwd=self.archive_dir,
            capture_output=True,
            text=True
        )
        return result.returncode == 0

    def add_urls_from_file(self, filepath: Path) -> bool:
        """テキストファイル(1行1URL)からアーカイブする。"""
        with open(filepath) as f:
            result = subprocess.run(
                ['archivebox', 'add', '--depth=0'],
                cwd=self.archive_dir,
                stdin=f,
                capture_output=True
            )
        return result.returncode == 0

    def search(self, query: str) -> List[dict]:
        """アーカイブ済みコンテンツを検索する。"""
        result = subprocess.run(
            ['archivebox', 'list', '--filter-type=search', query],
            cwd=self.archive_dir,
            capture_output=True,
            text=True
        )
        # 出力をパースする…
        return []

証拠としての保全

削除・改変が予想されるページを「いつ・誰が・どんな内容を」取得したかを記録し、後から第三者が検証できる形で残す。日本では米国のような統一的な「ウェブ証拠の認証」制度はないが、(1)取得日時(UTC・JST)、(2)コンテンツのハッシュ(SHA-256)、(3)複数アーカイブのURL、(4)取扱い履歴 を残しておくことが、来歴(プロビナンス)を示す実務上の基本となる。最終的な証拠能力の判断は法務・弁護士と相談すること。

取得・取扱い履歴(チェーン・オブ・カストディ)の記録

import hashlib
import sys
import json
import requests
from datetime import datetime, timezone
from dataclasses import dataclass, asdict
from typing import List

@dataclass
class EvidenceRecord:
    """検証に耐える証拠レコード。"""

    # コンテンツの識別
    original_url: str
    archived_urls: List[str]  # 複数のアーカイブ控え
    content_hash_sha256: str

    # 日時
    capture_time_utc: str
    first_observed: str

    # メタデータ
    page_title: str
    captured_by: str
    capture_method: str
    tool_versions: dict

    # 取扱い履歴(誰がいつアクセスしたか)
    custody_log: List[dict]

    def add_custody_entry(self, accessor: str, action: str, notes: str = ""):
        """証拠へのアクセスを記録する。"""
        self.custody_log.append({
            'timestamp': datetime.now(timezone.utc).isoformat(),
            'accessor': accessor,
            'action': action,
            'notes': notes
        })

    def to_json(self) -> str:
        return json.dumps(asdict(self), indent=2, ensure_ascii=False)

    @classmethod
    def from_capture(cls, url: str, content: bytes, captured_by: str):
        """取得したコンテンツから証拠レコードを生成する。"""
        now = datetime.now(timezone.utc).isoformat()
        py = sys.version_info
        return cls(
            original_url=url,
            archived_urls=[],
            content_hash_sha256=hashlib.sha256(content).hexdigest(),
            capture_time_utc=now,
            first_observed=now,
            page_title="",
            captured_by=captured_by,
            capture_method="automated_capture",
            tool_versions={
                # 'archiver' は自分のツールの実際の __version__ に置き換える
                'archiver': '1.0.0',
                'python': f'{py.major}.{py.minor}.{py.micro}',
                'requests': requests.__version__,
            },
            custody_log=[]
        )

def capture_as_evidence(url: str, captured_by: str) -> EvidenceRecord:
    """証拠としての来歴を完全に記録しつつURLを取得する。"""

    # コンテンツを取得
    response = requests.get(url)
    content = response.content

    # 証拠レコードを作成
    record = EvidenceRecord.from_capture(url, content, captured_by)
    record.page_title = extract_title(content)

    # 複数サービスにアーカイブ
    archiver = MultiArchiver()
    results = archiver.archive_url(url)

    for result in results:
        if result.success:
            record.archived_urls.append(result.archived_url)

    # 初回取得を記録
    record.add_custody_entry(
        captured_by,
        'initial_capture',
        f'{url} から取得、{len(record.archived_urls)} 個のサービスにアーカイブ'
    )

    return record

実務メモ:取得日時はUTCで記録しつつ、記事・報告にはJST(日本時間)を併記すると分かりやすい。SHA-256ハッシュは、後で「保存物が改変されていないこと」を示すための指紋になる。元の生データ(HTML・スクリーンショット・PDF)は加工せずに別途保管し、加工版とは分けて管理する。

Perma.cc(引用の長期保存)

import requests
from typing import Optional

class PermaCC:
    """法的グレードのアーカイブのためのPerma.cc APIクライアント。

    perma.cc のAPIキーが必要(限定利用は無料)。米国の裁判所・
    法律実務で利用されている。日本の裁判手続で同等の地位を持つ
    わけではないが、引用先URLを長期に固定したい場合の選択肢に
    なる。日本ではこれに直接相当する公的サービスはないため、
    Wayback Machine・WARP・ArchiveBox等と組み合わせて来歴を残す。
    """

    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.perma.cc/v1"
        self.headers = {
            'Authorization': f'ApiKey {api_key}',
            'Content-Type': 'application/json'
        }

    def create_archive(self, url: str, folder_id: int = None) -> Optional[dict]:
        """新しいPerma.ccアーカイブを作成する。

        guid、作成日時、キャプチャを含むdictを返す。
        """
        data = {'url': url}
        if folder_id:
            data['folder'] = folder_id

        try:
            response = requests.post(
                f"{self.base_url}/archives/",
                json=data,
                headers=self.headers,
                timeout=60
            )

            if response.status_code == 201:
                result = response.json()
                return {
                    'guid': result['guid'],
                    'url': f"https://perma.cc/{result['guid']}",
                    'creation_timestamp': result['creation_timestamp'],
                    'title': result.get('title', '')
                }
            return None
        except Exception:
            return None

    def get_archive(self, guid: str) -> Optional[dict]:
        """GUIDでアーカイブのメタデータを取得する。"""
        try:
            response = requests.get(
                f"{self.base_url}/archives/{guid}/",
                headers=self.headers,
                timeout=30
            )
            return response.json() if response.status_code == 200 else None
        except Exception:
            return None

アーカイブしたPDF・画像のOCR(文字化)

保存したPDFやスクリーンショットには、文字情報が入っていない「画像だけ」のものが多い。プレスリリース、行政のスキャン文書、新聞の切り抜き、チラシ、画像で作られたウェブページなどが典型で、そのままでは本文検索も差分比較もできない。まず埋め込みのテキスト層を確認し、無ければOCR(光学文字認識)で文字に起こす。

┌────────────────────────────────────────────────────────────┐
│              PDF/画像 → テキスト化の判断フロー               │
├────────────────────────────────────────────────────────────┤
│  1. まず埋め込みテキスト層を試す(pypdf 等)               │
│       └─ 文字が取れれば、それが最も正確。OCR不要            │
│                        ↓ ほぼ空(=画像PDF)                │
│  2. ページを画像化(pdftoppm 300dpi)→ tesseract(jpn)      │
│       └─ 日本語は言語データ jpn / 縦書きは jpn_vert         │
│                        ↓ 精度が低い                        │
│  3. 解像度を上げる(400-600dpi)・前処理(二値化/傾き補正)     │
│       新聞切り抜き・複雑レイアウトは誤認識が残る→人が確認   │
└────────────────────────────────────────────────────────────┘

導入(Debian/Ubuntu 系):

sudo apt-get install -y tesseract-ocr tesseract-ocr-jpn tesseract-ocr-jpn-vert poppler-utils
pip install pytesseract pdf2image pypdf pillow
import io, os, re, subprocess, tempfile
from pypdf import PdfReader
import pytesseract
from PIL import Image

def pdf_to_text(pdf_bytes: bytes, dpi: int = 300, max_pages: int = 5) -> tuple[str, str]:
    """PDFを文字化する。まず埋め込みテキスト層、無ければOCR。

    戻り値は (text, method)。method は 'text-layer' か 'OCR'。
    text-layer が取れたらそれを優先する(OCRより正確)。
    """
    # 1. 埋め込みテキスト層
    try:
        reader = PdfReader(io.BytesIO(pdf_bytes))
        layer = "\n".join((p.extract_text() or "") for p in reader.pages)
    except Exception:
        layer = ""
    if len(re.sub(r"\s", "", layer)) >= 25:   # 実質的な文字があれば採用
        return layer, "text-layer"

    # 2. 画像PDF → ページ画像化 → OCR(日本語)
    out = []
    with tempfile.TemporaryDirectory() as d:
        with open(os.path.join(d, "in.pdf"), "wb") as f:
            f.write(pdf_bytes)
        # pdftoppm で各ページをPNGに(-l で先頭 max_pages ページに制限)
        subprocess.run(
            ["pdftoppm", "-r", str(dpi), "-png", "-l", str(max_pages),
             os.path.join(d, "in.pdf"), os.path.join(d, "page")],
            check=False, capture_output=True,
        )
        for png in sorted(f for f in os.listdir(d) if f.endswith(".png")):
            img = Image.open(os.path.join(d, png))
            # 縦書き主体の文書は lang="jpn_vert" を試す
            out.append(pytesseract.image_to_string(img, lang="jpn"))
    return "\n".join(out), "OCR"

Waybackに保存された画像PDFをそのままOCRする例:

import requests

def ocr_archived_pdf(original_url: str, timestamp: str) -> tuple[str, str]:
    """Waybackの保存版PDF(`<ts>id_` で原本取得)を文字化する。"""
    raw = f"https://web.archive.org/web/{timestamp}id_/{original_url}"
    pdf_bytes = requests.get(raw, headers={"User-Agent": "research-archiver"}, timeout=90).content
    return pdf_to_text(pdf_bytes)

実務上の注意:

  • テキスト層があるPDFをOCRしない。埋め込み文字の方が正確で、OCRは固有名詞・数字を誤認識する。まず層を確認。
  • OCR結果は生成物であって原本ではない。金額・日付・氏名など核心部分は、必ず保存した画像(原本)を目視で照合する。数字や固有名詞の誤認識は珍しくない。
  • スキャンした新聞・チラシ・複雑レイアウトは精度が落ちる。解像度を上げる(400〜600dpi)、二値化・傾き補正などの前処理、縦書きなら jpn_vert を併用する。それでも残る誤りは人が直す。
  • OCRした本文は、検索・一次スクリーニングに使い、引用や事実主張には原本を典拠にする。証拠として扱うなら、原本PDFのSHA-256とOCR結果を分けて保管する(証拠としての保全参照)。
  • 画像で作られたウェブページ(本文が画像の広告的サイト)も、スクリーンショットに同じOCRを当てれば本文検索できる。

ブラウザ拡張とブックマークレット

ワンクリック保存ブックマークレット

// Wayback Machineに保存 ─ ブックマークとして登録する
javascript:(function(){
    window.open('https://web.archive.org/save/' + encodeURIComponent(location.href), '_blank');
})();

// Archive.todayに保存
javascript:(function(){
    window.open('https://archive.today/?run=1&url=' + encodeURIComponent(location.href), '_blank');
})();

// 全アーカイブを横断確認(Memento)
javascript:(function(){
    window.open('https://timetravel.mementoweb.org/list/0/' + encodeURIComponent(location.href), '_blank');
})();

消えたページを復元するブックマークレット

// 消えたページを複数アーカイブで試す
// 注:Googleキャッシュ(webcache.googleusercontent.com)は
// 2024年9月に廃止されたため、ここには含めない。
javascript:(function(){
    // location.href をエンコードし、内部の '#' / '?' がパス引数の
    // 一部として渡るようにする。素朴に連結すると、ブラウザが
    // フラグメントを落としクエリ文字列を外側URLに付け替えてしまう。
    var encoded = encodeURIComponent(location.href);
    var archives = [
        'https://web.archive.org/web/*/' + encoded,
        'https://archive.ph/newest/' + encoded,
        'https://timetravel.mementoweb.org/list/0/' + encoded
    ];
    archives.forEach(function(a){ window.open(a, '_blank'); });
})();

アーカイブサービス比較

サービス向いている用途API削除最大サイズ備考
Wayback Machine履歴調査あり(無料)申請により対応無制限匿名SPNは毎分15件程度。S3キー認証で上限が上がる
国立国会図書館 WARP官公庁・自治体・公的機関の過去版検索・閲覧可制度に基づく─公的サイト中心。民間・一般記事は対象範囲が限られる
Archive.todayペイウォール回避・素早い保存非公式しない50MBFBI召喚状(2025/10)、Wikipediaが引用元非推奨(2026/2)。法的・引用用途は避ける
Perma.cc引用の長期固定あり(無料枠)作成者による標準的なページ米国の裁判所で利用。日本での法的地位は別
ArchiveBoxセルフホスト・秘匿性ローカルしないディスク容量次第v0.8はDocker ComposeにChromium / yt-dlp / wget同梱
Browsertrix対話的・JS過多ページの取得あり作成者によるプラン依存Webrecorder.net による Conifer 後継。WARC出力
Conifer対話的コンテンツあり作成者による5GBまで無料旧Webrecorderサービス。現行はBrowsertrix

エラー処理とフォールバック

import requests
from enum import Enum
from typing import Optional
from urllib.parse import quote, unquote

class ArchiveError(Enum):
    NOT_FOUND = "アーカイブが見つからない"
    RATE_LIMITED = "サービスによるレート制限"
    BLOCKED = "アーカイブが拒否されたURL"
    TIMEOUT = "リクエストがタイムアウト"
    SERVICE_DOWN = "アーカイブサービス停止中"

def get_archived_page(url: str) -> tuple[Optional[str], Optional[ArchiveError]]:
    """全アーカイブサービスを、適切なエラー処理付きで試す。"""

    # 1. まずWayback Machineを試す
    try:
        result = check_wayback_availability(url)
        if result and result.get('available'):
            return result['url'], None
    except requests.Timeout:
        pass  # 次のサービスへ
    except Exception:
        pass

    # 2. Archive.todayを試す
    try:
        result = search_archive_today(url)
        if result:
            return result, None
    except Exception:
        pass

    # 3. Memento アグリゲータを試す
    try:
        memento_url = f"https://timetravel.mementoweb.org/api/json/0/{quote(unquote(url), safe='')}"
        response = requests.get(memento_url, timeout=30)
        data = response.json()

        if data.get('mementos', {}).get('closest'):
            return data['mementos']['closest']['uri'][0], None
    except Exception:
        pass

    return None, ArchiveError.NOT_FOUND

実地で踏んだ落とし穴(日本版・検証済みメモ 2026-07)

日本の対象・当環境で繰り返し当たった罠。サンプルを流用する前に必ず対処すること。

  • 官公庁サイトは Shift_JIS で文字化けする。requests の既定デコードや r.text をそのまま使うと化ける。 取得後は r.encoding = r.apparent_encoding(または明示的に cp932)で復号してから本文比較・差分をとる。
    r = requests.get(url, timeout=30)
    r.encoding = r.apparent_encoding   # 総務省統計局等の Shift_JIS ページで必須
    html = r.text
    
  • Save Page Now(SPN)の成否はページ単位。トップページや軽いページは保存できても、 重いJavaScript/SPA/巨大ページは 520 で連続失敗することがある(実測で同一URLに5回連続520)。 「保存を依頼して終わり」にせず、availability/CDX で保存済みURLを必ず確認し、 失敗するページはローカル保全(HTML保存+SHA-256)+ブラウザ描画型(single-file 等)にフォールバックする。
  • archive.today は 429(レート制限)で全試行が通らない環境がある(データセンターIP等)。 引用・証拠用途は Wayback を優先し、archive.today はその場限りの補助に留める。
  • 画像に本文を焼き込んだページはテキスト差分がほぼ空になる。差し替え検知では テキストだけでなく画像参照(src)や画像自体の差分も見る。
  • CDX/availability の並列取得は非JSON応答・TLS一時切断が頻発。 URL一覧は一度取ってローカルにキャッシュ(例 all_urls.json)し、matchType=domain の CDXで最新タイムスタンプを一括取得する方が確実。

ベストプラクティス

いつアーカイブするか

  • 公開前:自分の記事で引用した出典はすべてアーカイブする
  • 速報時:内容が変わる・消える前に直ちにアーカイブする
  • 係争・法的事項:複数アーカイブで日時入りの記録を作る
  • 取材・調査:再現性のため一次情報を保存する
  • SNS:投稿が削除される前にアーカイブする
  • 官公庁の発表・統計:差し替え・削除に備え、WARPとWaybackの両方で確認・保存する

アーカイブの冗長化

最低2つのサービスにアーカイブしておく。

def ensure_archived(url: str) -> bool:
    """URLが最低2つのサービスにアーカイブされていることを確かめる。"""
    archiver = MultiArchiver()
    results = archiver.archive_url(url)

    successful = [r for r in results if r.success]
    return len(successful) >= 2

レート制限と倫理

  • 一括アーカイブでは robots.txt を尊重する
  • リクエスト間に間隔を空ける(最低1〜3秒)
  • 個人・私的なページは本人の同意なくアーカイブしない
  • 利用可能ならAPIキーを使い、適切なレート制限の範囲で動かす
  • 結果をキャッシュし、重複リクエストを避ける
  • 取材で得た情報の保存・利用は、個人情報保護法および自社の取材倫理規程に従う

出典・ライセンス(帰属表示)

本スキルは research-toolkit(by Joe Amditis, MIT License, © Joe Amditis, https://github.com/jamditis/research-toolkit )の skills/web-archiving を 原典の構成(アーカイブ優先順位・Wayback/Archive.today/ArchiveBox/Perma.cc・多重アーカイブ・証拠保全・ブックマークレット・サービス比較・エラー処理)に沿って日本語化したものです。 米国固有のサービス・文脈は日本/汎用に再構成し、国立国会図書館WARP(インターネット資料収集保存事業)など日本の制度・実務を補っています。 コード・スクリプトは機能を保持し、コメントのみ日本語化しています。原典の著作物本文は転載していません。原作者の公認・推奨を受けたものではありません。 MIT ライセンス全文は同梱の LICENSE-MIT.txt を参照。

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

ai-bun-shuusei

無料概要あり

AIが生成しがちな「いかにもAIっぽい」日本語の定型表現・水増し・空疎な形容を取り除き、読者の信頼を損なわない自然な文章に直す。記事・原稿・プレスリリース・告知・SNS投稿など、AIの支援を受けて書いた文章を公開前に点検するときに使う。「AI臭を消したい」「文章が機械っぽい」「人が書いたように直したい」などの依頼で使う。

sgbms1/ja-news-skills32026年7月14日 更新

data-api-catalog

無料概要あり

取材・調査・データ報道で使える無料データ源(API)のカタログ。政府統計(e-Stat)、地域経済(RESAS)、気象(気象庁)、法令(e-Gov)、企業開示(EDINET)、書誌・国会会議録(国立国会図書館)、地理空間(国土地理院)、汎用の国際API(GDELT・Open-Meteo・Wayback等)を用途別に整理し、登録要否・利用条件・選び方を示す。「無料のデータ源が欲しい」「このテーマのAPIは?」「公的データをプログラムで取りたい」といった依頼で使う。

sgbms1/ja-news-skills32026年7月14日 更新

data-hodo

無料概要あり

データ報道(データジャーナリズム)の手順を支援する。データセットの分析、グラフや地図の作成、雑然としたデータのクリーニング、統計の算出、データに基づく記事の構築に使う。「e-Statのデータを分析したい」「人口あたりで比較したい」「グラフが誤解を招かないか確認したい」「統計から記事を組み立てたい」などの依頼で使う。記者・編集者・調査担当向け。

sgbms1/ja-news-skills32026年7月14日 更新

digital-archive

無料概要あり

取材で集めた資料(開示文書・記事・SNS・反訳など)をデジタルアーカイブ化し、AIで要約・分類・エンティティ抽出・関係づけ(知識グラフ化)して横断検索できる調査基盤を作るための手引き。「取材資料を整理したい」「大量の開示文書をデータベース化したい」「人物・組織・出来事の関係を可視化したい」「調査報道のアーカイブを作りたい」などの依頼で使う。

sgbms1/ja-news-skills32026年7月14日 更新

fact-check

無料概要あり

発言・記事・SNS投稿などの主張を体系的にファクトチェックする。主張の抽出、検証可能な命題への分解、根拠の収集、当事者への確認、判定(レーティング)、記録、訂正対応までを支援する。掲載前の裏取り、ファクトチェック記事の作成、編集部の検証手順づくりに使う。「ファクトチェックして」「この発言は本当か」「裏取りの手順を」などの依頼で使う。

sgbms1/ja-news-skills32026年7月14日 更新

fact-checker-flo

無料概要あり

テキスト・記事・ウェブページの内容を、厳密な検証方法論にもとづいてファクトチェックする。検証可能な主張を抽出し、信頼できる一次情報・公的データ・検証機関と突き合わせ、各主張の真偽と確からしさ(確信度)、情報源の信頼度スコアを評価し、根拠つきの検証レポートにまとめる。「この主張を検証して」「記事をファクトチェックして」「情報源の信頼性を見て」「真偽を判定して」「誤情報か確かめて」などの依頼で使う。付属の references(信頼できる情報源/検証方法論)を参照する検証重視版。

sgbms1/ja-news-skills32026年7月14日 更新

sgbms1 のスキルをすべて見る

このスキルの問題を報告する