青空文庫からテキストをとってきたい
■テキスト欲しいな
なにかテキストを触ってみたいというときに、青空文庫はとても有り難い存在です。たまに、スクリプトで扱いたいとなったとき、毎回ちょっとはやってみるものの、その後忘れる、というのがよくありますね。
といったくらいの人向けのメモ書き。
●想定層
- ファイルパスの概念がわかる
- 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狙い撃ちで書きます。
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()
は、エラーを見張る用。
■存在しているデータの把握
一作品を決め打ちすることはできるようになりましたが、それくらいなら手動でよいです。手動だとちょっとやだな、というところをなんとかしたい。
■作品の情報
作品リストは、
の上部にある、
「公開中 作家別作品一覧拡充版:全て(CSV形式、UTF-8、zip圧縮)」
からダウンロードできる list_person_all_extended_utf8.csv というCSVが詳しいっぽいので、この中から必要な情報をとりだします。
ダウンロードしたファイル名と、作者名、作品名を関連づけるようなものをつくっておかないと、きっと先々面倒くさい。
必要な項目は、
- 図書カードのURL
- 作者名
- 作品名
- テキストファイルのURL
あたりでしょうか。
CSVはなにかとトラブルを引き起こしがちなので、JSONにしておきます。
本当は色々やらないとですが、とりあえずこのくらいで。
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 で終わるものに制限しています。
もしかすると同じ内容の行があるかもしれないので、それも消しています。
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まで減りました。
■もう少し見てみる。
さてここで、
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をとりたいときは、
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.py と soseki.txt があるとして、
soseki.txt から、./soseki以下に zipファイルをダウンロードしてきたい、としますか。
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 に置くことにしました。
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