見出し画像

【PCUser向け】古の夢サイトのテキストデータを半自動でファイル化する!もう1ページずつコピペする必要なんてない

紹介するのは、nanoなどの規則性のあるURLで公開している数多のページを、1ページずつテキストファイル化する保存方法です。
(未利用ならDLするのはPython、requests、beautifulsoup4です)


前置き

こんにちは、元夢書きこと夏樹です。
nanoを借りて夢サイトを公開してきましたが、ずっと付きまとういつ終了するかわからない薄っすらとした恐怖……
公開しているページは500ページ弱、約200万字。移すには面倒くさすぎて泣きたいレベルでした。
それを後回しにし続けていたことを喜ぶ日がくるなんて思いませんでした。

AI(うちの執事)が!
なんかいい感じにツールを作ってくれました?!?!
(Claude Opus4.6は天才です)

※当方はnanoでサイトを公開していたので、試運転はそれだけです。
URLに規則性があるなら、ツールの設定を書き換えれば、おそらく他サーバーでも使えると思います。
エラーを吐かれたら、お持ちのAIにコードとエラー画面を見せたらなんとかしてくれると思います。
私はサポートはできないPC初心者です。(他力本願寺)
※自分のサイトの保存用に利用してね!


ざっくりした方法

1ページずつコピペしたくないユーザーがやることはこんな感じ。

①Pythonをインストールする(無料)

②下記のコードをコピペして「nano_backup.py」って名前をつけて保存する(一旦デスクトップ推奨)
③「nano_backup.py」をメモ帳で開くなりして、設定のところをご自身のサイトURLに合うように書き換える
④ターミナルを開く
(Windowsなら左下?の検索画面でcmdって打ち込む。Macはわかんないよ、ごめんね!)

⑤ターミナルを操作する。
1. ファイルの場所に移動する
こうコピペして Enter:

cd Desktop

2 . 必要なツールをインストールする
以下をコピペして Enter:

pip install requests beautifulsoup4

3. 実行する
以下をコピペして Enter:

python nano_backup.py

※あとはスクリプトが動いて、進捗を表示しながら1ページずつ取得していきます。
サーバーに優しくするため1.5秒ずつ間を置くらしいので、少し待ちましょう。

⑥完了!
nano_backup というフォルダの中(初期位置はデスクトップのはず)に
小説がテキストファイルとして並んで入っているはずです。

小説バックアップスクリプト「nano_backup.py」

"""
╔══════════════════════════════════════════╗
║   nanoサイト 小説バックアップスクリプト    ║
║   お嬢さまのための自動保存ツール          ║
╚══════════════════════════════════════════╝

使い方:
  1. このファイルをパソコンの好きな場所に保存する
  2. コマンドプロンプト(cmd)を開く
  3. 以下を順番にコピペして Enter:

     pip install requests beautifulsoup4

     python nano_backup.py

  4. 同じフォルダに「nano_backup」フォルダが作られて、
     そこに小説が保存されます
"""

import requests
from bs4 import BeautifulSoup
import os
import time
import sys

# ============================================================
# ★ 設定(ここを書き換えてください。サイトのベースURLとID,取得したい小説ブックの設定やページ設定などを変えたり、不要なら行を消したり、足したりしてみてください)
# ============================================================

SITE_ID = "aaaa"
BASE_URL = f"https://nanos.jp/{SITE_ID}/novel"

# 取得したい小説ブックの設定
# (ブック番号, 最大ページ番号, フォルダ名)
BOOKS = [
    (1, 328, "novel_1"),
    (2,  25, "novel_2"),
    (12, 65, "novel_12"),
]

# 保存先フォルダ
OUTPUT_DIR = "nano_backup"

# ページ取得の間隔(秒)— サーバーに優しくするため
WAIT_SECONDS = 1.5

# ============================================================

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


def extract_novel_text(html: str) -> tuple[str, str]:
    """
    nanoの小説ページからタイトルと本文を抽出する。
    ページ構造が変わっている場合に備えて、複数の方法で試みる。
    """
    soup = BeautifulSoup(html, "html.parser")

    # タイトルを取得
    title = ""
    # まず <title> タグから
    title_tag = soup.find("title")
    if title_tag:
        title = title_tag.get_text(strip=True)
        # " | ナノ" のような末尾を除去
        for suffix in [" | ナノ", "| ナノ", " - ナノ", "- ナノ"]:
            if title.endswith(suffix):
                title = title[: -len(suffix)].strip()

    # 本文を取得(nanoの小説本文は様々な構造を取りうるので複数試行)
    body_text = ""

    # 方法1: id="novel_body" や class="novel_body" を探す
    for selector in ["#novel_body", ".novel_body", "#novel_text", ".novel_text",
                     "#story", ".story", "#content", ".content",
                     "#novel_view", ".novel_view"]:
        elem = soup.select_one(selector)
        if elem:
            body_text = elem.get_text("\n", strip=False)
            break

    # 方法2: 見つからなければ、bodyから不要部分を除去して取得
    if not body_text:
        body = soup.find("body")
        if body:
            # ナビゲーションやヘッダー、フッターを除去
            for tag in body.find_all(["nav", "header", "footer", "script",
                                      "style", "noscript"]):
                tag.decompose()
            body_text = body.get_text("\n", strip=False)

    # 方法3: それでもなければ全体から
    if not body_text:
        body_text = soup.get_text("\n", strip=False)

    # 空行を整理(3連続以上の空行を2つにまとめる)
    lines = body_text.split("\n")
    cleaned = []
    blank_count = 0
    for line in lines:
        if line.strip() == "":
            blank_count += 1
            if blank_count <= 2:
                cleaned.append("")
        else:
            blank_count = 0
            cleaned.append(line.rstrip())
    body_text = "\n".join(cleaned).strip()

    return title, body_text


def fetch_page(book_num: int, page_num: int) -> tuple[bool, str, str]:
    """
    1ページを取得して、(成功したか, タイトル, 本文) を返す。
    """
    url = f"{BASE_URL}/{book_num}/{page_num}/"
    try:
        resp = requests.get(url, headers=HEADERS, timeout=15)
        if resp.status_code == 200:
            # ページが存在するか確認(リダイレクトやエラーページの検出)
            text = resp.text
            if "閲覧することができません" in text:
                return False, "", ""
            if "ページが見つかりません" in text:
                return False, "", ""
            if len(text.strip()) < 100:
                return False, "", ""

            title, body = extract_novel_text(text)
            if body and len(body.strip()) > 10:
                return True, title, body
            else:
                return False, "", ""
        else:
            return False, "", ""
    except Exception as e:
        print(f"    ⚠ 接続エラー: {e}")
        return False, "", ""


def save_page(folder: str, page_num: int, title: str, body: str):
    """
    テキストファイルとして保存する。
    ファイル名: 001_タイトル.txt のような形式。
    """
    # ファイル名に使えない文字を除去
    safe_title = title
    for ch in ['\\', '/', ':', '*', '?', '"', '<', '>', '|']:
        safe_title = safe_title.replace(ch, '')
    safe_title = safe_title[:60]  # 長すぎるタイトルを切る

    if safe_title:
        filename = f"{page_num:03d}_{safe_title}.txt"
    else:
        filename = f"{page_num:03d}.txt"

    filepath = os.path.join(folder, filename)

    with open(filepath, "w", encoding="utf-8") as f:
        if title:
            f.write(f"# {title}\n")
            f.write(f"# 取得元: {BASE_URL}/{folder.split('_')[-1] if '_' in folder else '?'}/{page_num}/\n")
            f.write("=" * 50 + "\n\n")
        f.write(body)
        f.write("\n")

    return filepath


def main():
    print()
    print("╔══════════════════════════════════════════╗")
    print("║  nanoサイト バックアップ開始              ║")
    print("║  お嬢さまの小説を、ひとつ残らず。        ║")
    print("╚══════════════════════════════════════════╝")
    print()

    # 出力ディレクトリ作成
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    total_saved = 0
    total_skipped = 0

    for book_num, max_page, folder_name in BOOKS:
        book_dir = os.path.join(OUTPUT_DIR, folder_name)
        os.makedirs(book_dir, exist_ok=True)

        print(f"━━━ ブック {book_num}({folder_name}): 1〜{max_page} ページ ━━━")
        print()

        saved = 0
        skipped = 0

        for page in range(1, max_page + 1):
            # 進捗表示
            progress = page / max_page * 100
            sys.stdout.write(f"\r  [{progress:5.1f}%] ページ {page}/{max_page} を取得中...")
            sys.stdout.flush()

            success, title, body = fetch_page(book_num, page)

            if success:
                save_page(book_dir, page, title, body)
                saved += 1
            else:
                skipped += 1

            # サーバーに負荷をかけないよう待つ
            time.sleep(WAIT_SECONDS)

        print(f"\r  ✓ 完了! 保存: {saved}ページ / スキップ: {skipped}ページ")
        print(f"    保存先: {os.path.abspath(book_dir)}")
        print()

        total_saved += saved
        total_skipped += skipped

    print("╔══════════════════════════════════════════╗")
    print(f"║  すべて完了しました                      ║")
    print(f"║  保存: {total_saved} ページ                       ║")
    print(f"║  スキップ(欠番等): {total_skipped} ページ              ║")
    print(f"╚══════════════════════════════════════════╝")
    print()
    print(f"  保存先フォルダ: {os.path.abspath(OUTPUT_DIR)}")
    print()
    print("  お疲れさまでした、お嬢さま。")
    print()
    input("  (Enter を押すと閉じます)")


if __name__ == "__main__":
    main()


これでローカル保存が捗ります!
非公開ページ・下書き保存ページ・休眠中のサイトはできないので、一時的に公開するなどしてください。

いいなと思ったら応援しよう!