🙌

青空文庫からテキストをとってきたい

に公開

■テキスト欲しいな

なにかテキストを触ってみたいというときに、青空文庫はとても有り難い存在です。たまに、スクリプトで扱いたいとなったとき、毎回ちょっとはやってみるものの、その後忘れる、というのがよくありますね。
といったくらいの人向けのメモ書き。

●想定層

  • ファイルパスの概念がわかる
  • CLIで pythonを叩ける
  • pip を知っている
  • import を知っている

あたり。

■まずは

青空文庫における作品は、図書カードという形で管理されていて、
『吾輩は猫である』であれば、

https://www.aozora.gr.jp/cards/000148/card789.html

といった感じの場所にあります。
わりと馴染みのあるページのはず。

■一作のデータをとってくる

■テキストデータの場所

上のページを見ると、『吾輩は猫である』のテキストデータは、

https://www.aozora.gr.jp/cards/000148/files/789_ruby_5639.zip

にあるようです。

■これをとってみますか

ここでは、完全にaozoraの.zip狙い撃ちで書きます。

download_aozora_file.py
import requests
import os
from urllib.parse import urlparse

def download_aozora_zip(url: str, out_dir: str) -> None:
    headers = {
        "User-Agent": "AozoraDownloader/0.1 (you@example.com)"
    }
    r = requests.get(url, headers=headers)
    r.raise_for_status()

    save_as = os.path.basename(urlparse(url).path)

    os.makedirs(out_dir, exist_ok=True)

    out_path = os.path.join(out_dir, save_as)
    with open(out_path, "wb") as f:
        f.write(r.content)
    print("downloaded:", out_path)

if __name__ == "__main__":
    url = "https://www.aozora.gr.jp/cards/000148/files/789_ruby_5639.zip"
    out_dir = "neko"
    download_aozora_zip(url, out_dir)

こんな感じでしょうか。
直下の neko というフォルダの下に、URLの最後の部分の名前で zip をダウンロードしてきました。
(テストから書かないのか、みたいな話はあるかと思いますが、とりあえず手を動かしていく方針で)

if __name__ == "__main__": の使い方を知らない人は検索でもしてください。

この中の、

headers = {
    "User-Agent": "AozoraDownloader/0.1 (you@example.com)"
}

の部分は、先方への挨拶として(自分の情報を書く)。

r.raise_for_status()

は、エラーを見張る用。

■存在しているデータの把握

一作品を決め打ちすることはできるようになりましたが、それくらいなら手動でよいです。手動だとちょっとやだな、というところをなんとかしたい。

■作品の情報

作品リストは、

https://www.aozora.gr.jp/index_pages/person_all.html

の上部にある、
「公開中 作家別作品一覧拡充版:全て(CSV形式、UTF-8、zip圧縮)」
からダウンロードできる list_person_all_extended_utf8.csv というCSVが詳しいっぽいので、この中から必要な情報をとりだします。

ダウンロードしたファイル名と、作者名、作品名を関連づけるようなものをつくっておかないと、きっと先々面倒くさい。

必要な項目は、

  • 図書カードのURL
  • 作者名
  • 作品名
  • テキストファイルのURL

あたりでしょうか。
CSVはなにかとトラブルを引き起こしがちなので、JSONにしておきます。

本当は色々やらないとですが、とりあえずこのくらいで。

csv2json_unique.py
import csv, json, sys

# 使い方: python csv2json_unique.py input.csv output.json
inp, outp = sys.argv[1], sys.argv[2]

KEYS = ["図書カードURL", "姓", "名", "作品名", "テキストファイルURL"]

records = []
seen = set()

with open(inp, encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        card = row["図書カードURL"].strip()
        last = row["姓"].strip()
        first = row["名"].strip()
        title = row["作品名"].strip()
        text = row["テキストファイルURL"].strip()

        # URL が空、または aozora.gr.jp 配下の zip 以外は除外
        if (
            not card
            or not text
            or not text.lower().endswith(".zip")
            or not text.startswith("https://www.aozora.gr.jp")
        ):
            continue

        key = (card, last, first, title, text)
        if key in seen:
            continue
        seen.add(key)

        records.append({
            "図書カードURL": card,
            "姓": last,
            "名": first,
            "作品名": title,
            "テキストファイルURL": text,
        })

with open(outp, "w", encoding="utf-8") as f:
    json.dump(records, f, ensure_ascii=False, indent=2)
% python csv2json_unique.py list_person_all_extended_utf8.csv aozora_list_all.json 

として実行。aozora_list_all.json をつくりました。

CSVでは19398作(20250923時点)あったのですが、テキストファイルURLの項目は色々なものが入っていたので、https://www.aozora.gr.jp ではじまり、 .zip で終わるものに制限しています。
もしかすると同じ内容の行があるかもしれないので、それも消しています。

count_json_record.py
import json
import sys

with open(sys.argv[1], encoding="utf-8") as f:
    data = json.load(f)

print("レコード数:", len(data))
% python count_json_record.py aozora_list_all.json 
レコード数: 18991

としてレコード数を数えると、
18991まで減りました。

■もう少し見てみる。

さてここで、

check_uniqness.py
import json

with open("aozora_list_all.json", encoding="utf-8") as f:
    data = json.load(f)

urls = [item["テキストファイルURL"] for item in data if "テキストファイルURL" in item]

unique_urls = list(set(urls))
print(len(unique_urls))
% python check_uniqness.py 
17526

とかして、Web上にあるページと、テキストファイルの場所のペアが同じレコード数を調べてみると、17526と出ました。
ユニークなエントリ数は18991だったのですが、Webページとテキストファイルの実体は、17526ということのようです。
? となるわけですが、これは主に「姓」「名」の項目によるようで、「紫式部」と「与謝野晶子」が別に立っていたりしているようです。検索の時の便利のために、JSONでは残しておくことにして、重なってる場合もあると覚えておくことにします。

■どうダウンロードするかを決める

ここまでくれば、このJSONから、「テキストファイルURL」の部分を取り出して取りに行き、ローカルに保存すればいいんじゃない? となります。
ただ、

  • どの程度の対象を
  • どこに
  • なんていう名前で保存するか

は考えておく必要があります。

●対象

全部欲しいときとか、特定の作者の作が欲しいときとか、場合場合。これは、今つくったJSONから、都度ひっぱることにします。

●保存場所

は、そのたびごとに名前と場所を決める感じ。

●ファイル名

ファイルは青空文庫以下の zip を対象とすることにしたので、URL 最後の foo.zip みたいなところをそのままファイル名にすることにします。

■対象を取り出す

たとえば、漱石の作品のURLをとりたいときは、

url_picker.py
import json
import sys

save_as = sys.argv[1]

with open("aozora_list_all.json", encoding="utf-8") as f:
    data = json.load(f)

urls = [
    item["テキストファイルURL"]
    for item in data
    if item.get("姓") == "夏目" and item.get("名") == "漱石"
]

with open(save_as, "w", encoding="utf-8") as f:
    for u in urls:
        f.write(u + "\n")
% python url_picker.py soseki.txt

みたいなことをすると、soseki.txtというファイルに、姓が夏目、名が漱石の作品がおいてあるurlのリストが出力されます。
名が金之助で入っていたときは? というと、それは勿論、いちいち個別に調べることになります。

■実際にとる

ここまでくると、

  • url を指定してzipファイルをとってくることができる。
  • 欲しいデータのある url のリストを持っている。
    という状態になっています。

ディレクトリ直下に、
aozora_downloader.pysoseki.txt があるとして、
soseki.txt から、./soseki以下に zipファイルをダウンロードしてきたい、としますか。

download_aozora_files.py
import time
from download_aozora_file import download_aozora_zip

def download_aozora_files(urls_filename: str, outdir: str) -> None:
    with open(urls_filename, "r", encoding="utf-8") as f:
        urls = [line.strip() for line in f if line.strip()]

    for i, url in enumerate(urls, start=1):
        try:
            print(f"[{i}/{len(urls)}] downloading: {url}")
            download_aozora_zip(url, out_dir)
        except Exception as e:
            print(f"  failed: {url} -> {e}")
        time.sleep(1)

if __name__ == "__main__":
    urls_filename = "soseki.txt"
    out_dir = "soseki_org"
    download_aozora_files(urls_filename, out_dir)

って感じでやれば、漱石作品のzipファイルを、souseki_org 以下に取得できるはずです。

  • sleep を挟むのを忘れないこと。
  • この手のコードを走らせると、PCがスリープしてジョブが中断されるのを避けるため、SHIFTキーを叩き続けなければ! となったりしますが、ジョブが中断しないようにする設定がどこかにあります

■zipはとれた

……しばし待ったのち……
ローカルに欲しかった zip を持つことができたはずです。あとはこれを解凍すればテキストファイルが得られるはずです。

なにかあったときのために、この soseki_org はそのままにしておいて、zipを解凍したものを別のフォルダに置きますか。青空文庫からダウンロードしてきたテキストファイルは基本的に SHIFT-JIS なので、ここで UTF-8 に変更して soseki_utf8 に置くことにしました。

unzip_convert.py
import zipfile
import pathlib
import sys

inp = pathlib.Path(sys.argv[1])
out = pathlib.Path(sys.argv[2])
out.mkdir(parents=True, exist_ok=True)

for zip_path in inp.glob("*.zip"):
    with zipfile.ZipFile(zip_path, "r") as zf:
        for name in zf.namelist():
            if not name.lower().endswith(".txt"):
                continue

            raw = zf.read(name)
            try:
                text = raw.decode("shift_jis")
            except UnicodeDecodeError:
                text = raw.decode("cp932", errors="replace")

            orig_name = pathlib.Path(name).name
            # zipファイル名(拡張子込み)+ "_" + 元のtxtファイル名
            safe_name = f"{zip_path.name}_{orig_name}"
            dest = out / safe_name

            with open(dest, "w", encoding="utf-8") as f:
                f.write(text)

            print(f"展開+変換: {zip_path.name} -> {dest}")

% python unzip_convert.py soseki_org/ soseki_utf8/

とすれば、soseki_utf8にテキストデータが並んでいるはずです。

Discussion