スクレイピングでは、取得したHTMLをその場で解析すると試行錯誤のたびにネットワーク通信が発生しがちです。
HTMLをファイルに保存しておけば同じ内容で何度でも解析でき、ネットワークに依存せずに素早くデバッグできます。
ローカルに保存するだけで再現性・速度・オフライン対応が大きく向上します。
HTMLをファイル保存する理由
スクレイピングの再現性向上
その時点のスナップショットを固定化できます
HTMLは時間とともに変化します。
取得直後に処理してしまうと、あとから同じ条件で再現することが難しくなります。
ファイルとして保存しておけば、同じHTMLを何度でも解析できるため、選択子の調整やロジックのバグ取りが安定して行えます。
また、保存したスナップショットをチームで共有すれば、環境差による再現性問題も減ります。
Beautiful Soupのセレクタ調整がしやすくなります
オフラインの保存HTMLに対してfind
やselect
の検証を繰り返すことで、実サイトに負荷をかけずに素早く要素の抽出方法を磨けます。
通信回数を減らし高速化
キャッシュとして使えば無駄な再取得を避けられます
一度保存したHTMLがあるなら、次回はローカルファイルを読み込むだけで解析を開始できます。
ネットワーク待ちが無くなることで、開発サイクルが短縮されます。
さらに、サイトへのアクセス回数も減るため、相手サイトへの負荷軽減にもつながります。
オフラインで解析・デバッグ
通勤中やネット不調時でも作業可能です
保存済みHTMLさえあれば、オフラインでもパースや正規表現の見直しができます。
通信に依存しないデバッグ環境は、時間と場所の自由度を高めます。
保存したHTMLはあくまで取得時点の静的な内容です。
JavaScriptで後から描画される要素はrequests
だけでは含まれない場合があります。
動的レンダリングが必要なサイトは別途Seleniumなどを検討してください。
PythonでHTMLを保存する手順
requestsで取得→.htmlで保存
最小の保存スクリプト(UTF-8で保存)
# 最小構成: requestsで取得し、.htmlとして保存する例
# - 取得先: https://example.com (静的なHTML例)
# - 保存先: saved_pages/example.com/index.html
from pathlib import Path
import requests
def save_html_min(url: str, out_path: Path) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True) # 保存先フォルダを作成
headers = {
# 最低限のUser-Agentを指定(拒否回避や識別のため)
"User-Agent": "Mozilla/5.0 (compatible; HTML-Save-Demo/1.0; +https://example.com/)"
}
# タイムアウトは(接続, 読み込み)の秒数タプルで指定
resp = requests.get(url, headers=headers, timeout=(5, 15))
# サーバがエンコーディングを明示していないことがあるため、requestsの推定結果を採用
resp.encoding = resp.apparent_encoding or "utf-8"
html_text = resp.text # テキストとして読み出す(ここでresp.encodingが使われる)
# UTF-8で書き出し
out_path.write_text(html_text, encoding="utf-8")
if __name__ == "__main__":
url = "https://example.com"
out = Path("saved_pages/example.com/index.html")
save_html_min(url, out)
print(f"Saved: {out.resolve()}")
Saved: /absolute/path/to/saved_pages/example.com/index.html
HTML取得にはrequests.get
を使い、レスポンスのapparent_encoding
をUTF-8に再エンコードして保存します。
こうすることで、文字化けを避けつつ統一的に扱えます。
UTF-8で書き出す
文字化け防止の基本
保存ファイルをUTF-8に統一すると、エディタや後段の解析処理で扱いやすくなります。
以下のようにresp.apparent_encoding
で一度テキスト化してからencoding="utf-8"
で書き出すのが簡単です。
# エンコーディングの扱いを明示化する例
import requests
def fetch_text_utf8(url: str) -> str:
resp = requests.get(url, timeout=(5, 15))
resp.encoding = resp.apparent_encoding or "utf-8"
return resp.text # ここで推定エンコーディングに従いデコード済みのテキストが得られる
text = fetch_text_utf8("https://example.com")
# 以降はUTF-8で保存すればOK
with open("example.html", "w", encoding="utf-8") as f:
f.write(text)
print("Saved as UTF-8")
Saved as UTF-8
オリジナルのバイト列をそのまま保存したい場合はresp.content
を使ってバイナリ保存します。
保存フォルダを用意
フォルダ作成は最初に済ませておきます
保存先が無いとエラーになるためPath(...).mkdir(parents=True, exist_ok=True)
で事前に作っておきます。
ドメインごとに分けると管理がしやすくなります。
from pathlib import Path
base_dir = Path("saved_pages")
(base_dir / "example.com").mkdir(parents=True, exist_ok=True)
print("Prepared:", (base_dir / "example.com").resolve())
Prepared: /absolute/path/to/saved_pages/example.com
スクレイピング向けファイル名と保存場所のコツ
タイムスタンプで履歴管理
過去取得との比較を簡単にします
ファイル名にタイムスタンプを付けてスナップショットを蓄積すると、サイトの変更点を比較できます。
index_20250115_101530.html
URLを安全な名前に変換
OSに優しいファイル名に正規化します
URLの文字はそのままだとファイル名として不適切な記号を含むことがあります。
英数字と-
や_
中心に整形し、長すぎるときはハッシュを添えると安全です。
# URLから安全なファイル名を作る関数
from urllib.parse import urlparse
import re
import hashlib
def slugify(text: str, limit: int = 120) -> str:
# 非英数字をハイフンに
s = re.sub(r"[^0-9A-Za-z._-]+", "-", text)
# 連続ハイフンを1つに
s = re.sub(r"-{2,}", "-", s).strip("-._")
# 空ならindex
if not s:
s = "index"
# 長すぎる場合は末尾にハッシュを付けて詰める
if len(s) > limit:
digest = hashlib.md5(s.encode("utf-8")).hexdigest()[:8]
s = s[:limit] + "__" + digest
return s
def safe_stem_from_url(url: str) -> str:
p = urlparse(url)
# パスの最後の要素をファイル名のベースに
last = p.path.rstrip("/").split("/")[-1] if p.path and p.path != "/" else "index"
base = slugify(last or "index")
if p.query:
# クエリを短く要約(先頭部分 + ハッシュ)
q_hash = hashlib.md5(p.query.encode("utf-8")).hexdigest()[:8]
base = f"{base}_{q_hash}"
return base # 拡張子は別で付与(.html)
ページごとにフォルダ分け
ドメイン/パス階層をそのままミラーします
ドメイン単位でフォルダを作り、URLパスをサブフォルダにします。
これにより、大量のページでも衝突しにくく、整理が容易です。
# URLからローカルの保存パスを作る(ドメイン/パスをミラー)
from urllib.parse import urlparse
from pathlib import Path
def url_to_local_paths(url: str, base_dir: Path, with_timestamp: bool = True) -> tuple[Path, Path | None]:
"""
戻り値:
(安定ファイル(stable).html, スナップショット(snapshot_YYYYMMDD_HHMMSS).html or None)
"""
import datetime as dt
p = urlparse(url)
netloc = p.netloc.replace(":", "_")
parts = [seg for seg in p.path.split("/") if seg]
if not parts:
parts = ["index"]
# ディレクトリはパスの全要素(最後はファイル名になる)
dir_parts = [slugify(seg) for seg in parts[:-1]]
stem = safe_stem_from_url(url) # ファイル名のベース
stable_dir = base_dir / netloc / Path(*dir_parts) if dir_parts else base_dir / netloc
stable_path = stable_dir / f"{stem}.html"
snapshot_path = None
if with_timestamp:
ts = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
snapshot_path = stable_dir / f"{stem}_{ts}.html"
return stable_path, snapshot_path
実際の構造例:
目的 | 例のパス | メリット |
---|---|---|
安定ファイル(上書き) | saved_pages/example.com/news/index.html | 解析の入口を固定化できる |
スナップショット(履歴) | saved_pages/example.com/news/index_20250115_101530.html | 変更点の比較が容易 |
クエリ区別 | saved_pages/example.com/search/result_1a2b3c4d.html | クエリの違いで衝突しない |
保存HTMLの活用でPythonスクレイピングを効率化
以下は、保存・再利用・履歴化・簡易デバッグを1つにまとめた実用スクリプトです。
まず保存、次にローカルで解析という流れを徹底することで、開発効率が上がります。
# 実用スクリプト: 取得→UTF-8保存→キャッシュ活用→スナップショット履歴→ローカル解析
# 依存: requests, beautifulsoup4
from __future__ import annotations
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse
import requests
import hashlib
import re
import datetime as dt
# ===== ファイル名・パス生成ユーティリティ =====
def slugify(text: str, limit: int = 120) -> str:
s = re.sub(r"[^0-9A-Za-z._-]+", "-", text)
s = re.sub(r"-{2,}", "-", s).strip("-._")
if not s:
s = "index"
if len(s) > limit:
digest = hashlib.md5(s.encode("utf-8")).hexdigest()[:8]
s = s[:limit] + "__" + digest
return s
def safe_stem_from_url(url: str) -> str:
p = urlparse(url)
last = p.path.rstrip("/").split("/")[-1] if p.path and p.path != "/" else "index"
base = slugify(last or "index")
if p.query:
q_hash = hashlib.md5(p.query.encode("utf-8")).hexdigest()[:8]
base = f"{base}_{q_hash}"
return base
def url_to_local_paths(url: str, base_dir: Path, with_timestamp: bool = True) -> tuple[Path, Optional[Path]]:
p = urlparse(url)
netloc = p.netloc.replace(":", "_")
parts = [seg for seg in p.path.split("/") if seg]
if not parts:
parts = ["index"]
dir_parts = [slugify(seg) for seg in parts[:-1]]
stem = safe_stem_from_url(url)
stable_dir = base_dir / netloc / Path(*dir_parts) if dir_parts else base_dir / netloc
stable_path = stable_dir / f"{stem}.html"
snapshot_path = None
if with_timestamp:
ts = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
snapshot_path = stable_dir / f"{stem}_{ts}.html"
return stable_path, snapshot_path
# ===== 取得・保存のメイン関数 =====
def fetch_and_save_html(
url: str,
base_dir: Path = Path("saved_pages"),
use_cache: bool = True,
make_snapshot: bool = True,
headers: Optional[dict] = None,
timeout: tuple[int, int] = (5, 15),
) -> Path:
"""
URLのHTMLをUTF-8で保存し、安定ファイルのパスを返す。
- use_cache=True なら、既存の安定ファイルがあれば再取得をスキップ
- make_snapshot=True なら、タイムスタンプ付きの履歴ファイルも保存
"""
stable_path, snapshot_path = url_to_local_paths(url, base_dir=base_dir, with_timestamp=make_snapshot)
stable_path.parent.mkdir(parents=True, exist_ok=True)
if use_cache and stable_path.exists():
print(f"[CACHE] Skip fetch: {url}")
return stable_path
if headers is None:
headers = {
"User-Agent": "Mozilla/5.0 (compatible; HTML-Save/1.0; +https://example.com/)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}
print(f"[GET] {url}")
try:
resp = requests.get(url, headers=headers, timeout=timeout)
except requests.RequestException as e:
# ネットワークエラーは安定ファイルにエラー情報を残す(解析時のヒントになる)
stable_path.write_text(f"<!-- FETCH ERROR: {e} -->", encoding="utf-8")
print(f"[ERROR] Network error: {e}")
return stable_path
# ステータスコードを表示(200以外でも本文がある場合、デバッグ用に保存しておく)
print(f"[STATUS] {resp.status_code}")
resp.encoding = resp.apparent_encoding or "utf-8"
text = resp.text
# 安定ファイルをUTF-8で保存(上書き)
stable_path.write_text(text, encoding="utf-8")
print(f"[SAVE] Stable: {stable_path}")
# 任意: スナップショットも保存(履歴管理)
if make_snapshot and snapshot_path is not None:
snapshot_path.write_text(text, encoding="utf-8")
print(f"[SAVE] Snapshot: {snapshot_path}")
return stable_path
# ===== ローカルHTMLでのパース(最小限) =====
def parse_local_html(html_path: Path) -> None:
"""
ローカルのHTMLから<title>と<h1>の数を確認する簡易解析。
Beautiful Soupの詳しい使い方は別記事で扱います。
"""
from bs4 import BeautifulSoup
html = html_path.read_text(encoding="utf-8", errors="ignore")
soup = BeautifulSoup(html, "html.parser")
title = soup.title.get_text(strip=True) if soup.title else "(no title)"
h1_count = len(soup.find_all("h1"))
print(f"[PARSE] title='{title}', h1_count={h1_count}")
if __name__ == "__main__":
base = Path("saved_pages")
# 1回目: 取得して保存
p1 = fetch_and_save_html("https://example.com", base_dir=base, use_cache=True, make_snapshot=True)
# 2回目: 既存があればスキップ(キャッシュヒット)
p2 = fetch_and_save_html("https://example.com", base_dir=base, use_cache=True, make_snapshot=True)
# ローカルHTMLでパーステスト
parse_local_html(p1)
# ステータスが200でない例(デバッグ用保存)
p3 = fetch_and_save_html("https://httpstat.us/404", base_dir=base, use_cache=False, make_snapshot=False)
print(f"[INFO] 404 body saved to: {p3}")
[GET] https://example.com
[STATUS] 200
[SAVE] Stable: saved_pages/example.com/index.html
[SAVE] Snapshot: saved_pages/example.com/index_20250115_101530.html
[CACHE] Skip fetch: https://example.com
[PARSE] title='Example Domain', h1_count=1
[GET] https://httpstat.us/404
[STATUS] 404
[SAVE] Stable: saved_pages/httpstat.us/404.html
[INFO] 404 body saved to: saved_pages/httpstat.us/404.html
ローカルHTMLでパースをテスト
まずはローカルで選択子を固めます
上記のparse_local_html
のように、保存済みHTMLをBeautiful Soupで読み込んで検証します。
選択子が固まってからネットワーク越しの本番フローに組み込みましょう。
これにより、通信待ちがなく、失敗時の反復が高速です。
既存ファイルなら再取得をスキップ
キャッシュを使って素早く再実行します
安定ファイル(stable).htmlが存在するなら取得をスキップします。
APIやページのレート制限にも優しく、再現性の高い開発が可能です。
必要に応じてuse_cache=False
で強制再取得します。
エラー時に保存内容で原因特定
ステータス非200でも本文を残します
エラー画面にも有益な情報が含まれることがあります。
ステータスが200以外でも本文を保存しておけば、ブロック理由やWAFメッセージなどから対処の手掛かりが得られます。
ネットワーク例外時は簡易的なコメントを保存しておくと、失敗の痕跡が残せます。
法的・倫理的注意: スクレイピングは必ずサイトの利用規約やrobots.txt、著作権、アクセス間隔に配慮してください。
HTMLの保存・二次利用可否はサイトポリシーに従いましょう。
まとめ
HTMLをファイルに保存するだけで、再現性・速度・オフライン対応・デバッグ容易性が一度に手に入ります。
実装の要点は次のとおりです。
まずrequests
で取得しUTF-8で保存、URLを安全なファイル名に正規化してドメイン/パスをフォルダでミラーします。
安定ファイルでキャッシュし、必要に応じてタイムスタンプ付きスナップショットも残せば履歴比較が容易です。
保存済みHTMLを使ってローカルでパースを詰め、問題が出たら保存内容から原因を特定しましょう。
これらの工夫で、スクレイピングの開発効率は大きく向上します。