青空文庫 - python によるスクレイピング

(2025-04-19)

青空文庫のコンテンツをローカルにダウンロードして、少し編集して、縦でも読めるようにして自分のレンタルサーバーにアップロードします。

青空文庫にある xhtml ファイルを手動でダウンロードしていろいろ操作するのはできるのですが、コンピュータを使って一括で実行します。

python による xhtml の一括ダウンロード

青空文庫の xthml ファイルを一括ダウンロードするのは簡単ではありません。

例えば芥川竜之介のファイルは「https://www.aozora.gr.jp/cards/000879」にあることがすぐわかるのですが、 いわばファイルリストがわかりません。

chatGPT はとても賢いのですが、こういうことはできないようで、あれこれ提示してくれましたが、例によって全然駄目なので自分で考えることにしました。

芥川竜之介の作品一覧のページのソースを見ると、

<li><a href="../cards/000879/card4872.html">愛読書の印象</a> (新字旧仮名、作品ID:4872) </li>
<li><a href="../cards/000879/card16.html">秋</a> (新字旧仮名、作品ID:16) </li>
<li><a href="../cards/000879/card178.html">芥川竜之介歌集</a> (新字旧仮名、作品ID:178) </li>
<li><a href="../cards/000879/card15.html">アグニの神</a> (新字旧仮名、作品ID:15) </li>
<li><a href="../cards/000879/card43014.html">アグニの神</a> (新字新仮名、作品ID:43014) </li>
<li><a href="../cards/000879/card3804.html">悪魔</a> (新字旧仮名、作品ID:3804) </li>

となっているので、これがわかれば card*.html のリストを作ることができます。

そうして、それぞれのページにアクセスすると、

<td><a href="./files/14_14602.html">14_14602.html</a></td>

となっているので、それぞれをダウンロードすることができます。

文字コードを utf-8 にする

そのままダウンロードすると、文字コードが「shift-jis」になってしまうので、それを「utf-8」に変更する必要があります。

html を書き換える

普通にダウンロードすると、html 文書の始めの方は以下のようになっています。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" >
<head>
<meta http-equiv="Content-Type" content="text/html;charset=Shift_JIS" />
<meta http-equiv="content-style-type" content="text/css" />
<link rel="stylesheet" type="text/css" href="../../default.css" />

これではまずいので、python で以下のように一括変換します。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="content-style-type" content="text/css" />
<link rel="stylesheet" type="text/css" href="../../viewer/css/style.css" />

python のコード

import os
import re
import requests
from bs4 import BeautifulSoup
class AozoraCardFetcher:
def __init__(self):
# 設定(ここで一括定義)
self.base_url = "https://www.aozora.gr.jp"
self.author_id = "1833"
self.author_name = "鈴木大拙"
self.author_page = f"{self.base_url}/index_pages/person{self.author_id}.html"
self.card_urls = []
sanitized_name = re.sub(r'[\\/*?:"<>|]', '_', self.author_name)
self.download_folder = f"./downloads/{sanitized_name}"
if not os.path.isdir(self.download_folder):
os.makedirs(self.download_folder)
# 起動時にすべて実行
self.fetch_cards()
for card_url in self.card_urls:
print(f"[INFO] {card_url} から .html ファイルを取得中...")
html_files = self.fetch_html_links_from_card(card_url)
self.download_and_convert_html_files(html_files)
def fetch_cards(self):
res = requests.get(self.author_page)
soup = BeautifulSoup(res.content, "html.parser")
for li in soup.select("ol > li > a"):
href = li.get("href")
if href and "cards" in href and href.endswith(".html"):
full_url = self.base_url + href.replace("..", "")
self.card_urls.append(full_url)
def fetch_html_links_from_card(self, card_url):
res = requests.get(card_url)
soup = BeautifulSoup(res.content, "html.parser")
table = soup.find("table", class_="download")
if not table:
return []
links = []
for a in table.find_all("a"):
href = a.get("href")
if href and href.endswith(".html"):
links.append(href.replace("./files/", ""))
return links
def clean_html_header(self, content):
content = re.sub(r'<\?xml[^>]*?\?>', '', content)
content = re.sub(r'<!DOCTYPE[^>]*?>', '<!DOCTYPE html>', content)
content = re.sub(r'<html[^>]*?xml:lang="ja"[^>]*?>', '<html lang="ja">', content)
content = content.replace(
'<meta http-equiv="Content-Type" content="text/html;charset=Shift_JIS" />',
'<meta http-equiv="content-type" content="text/html; charset=UTF-8" />\n<meta name="viewport" content="width=device-width, initial-scale=1.0" />'
)
content = content.replace(
'<link rel="stylesheet" type="text/css" href="../../default.css" />',
'<link rel="stylesheet" type="text/css" href="../../viewer/css/style.css" />'
)
content = content.replace(
'<link rel="stylesheet" type="text/css" href="../../aozora.css" />',
'<link rel="stylesheet" type="text/css" href="../../viewer/css/style.css" />'
)
content = re.sub(r'(<meta[^>]*?)(?<!/)>', r'\1 />', content)
content = re.sub(r'</meta>', '', content)
content = re.sub(r'(<link[^>]*?)(?<!/)>', r'\1 />', content)
content = re.sub(r'</link>', '', content)
content = re.sub(r'^\s*\n', '', content, flags=re.MULTILINE)
return content
def extract_title(self, html_content):
soup = BeautifulSoup(html_content, "html.parser")
title_tag = soup.find("title")
if title_tag:
full_title = title_tag.text.strip()
cleaned_title = re.sub(f'^{re.escape(self.author_name)}\\s*', '', full_title)
return cleaned_title
return "unknown_title"
def sanitize_filename(self, title):
return re.sub(r'[\\/*?:"<>|]', '_', title)
def download_and_convert_html_files(self, html_files):
for file_name in html_files:
download_url = f"{self.base_url}/cards/{int(self.author_id):06}/files/{file_name}"
try:
response = requests.get(download_url)
response.encoding = 'shift_jis'
original_html = response.text
converted_html = self.clean_html_header(original_html)
title = self.extract_title(original_html)
safe_title = self.sanitize_filename(title)
output_path = os.path.join(self.download_folder, f"{safe_title}.html")
with open(output_path, "w", encoding="utf-8") as f:
f.write(converted_html)
print(f"✅ {safe_title}.html を保存しました。")
except requests.exceptions.RequestException as e:
print(f"⚠️ ダウンロード失敗: {file_name} - {e}")
if __name__ == "__main__":
AozoraCardFetcher()

最初の方の init で作者名とその id を設定すれば、すべての html ファイルがダウンロードされます。

ソートのための json 作成

漢字混じりのタイトルをソートしても思い通りに並んではくれません。

芥川竜之介の作品は 360 ほどあるので、本棚から見たい作品を探すのに時間がかかってしまいます。

python は漢字の読みがなを取得できるようです。

それによって json を作成します。

import MeCab
import json
import os
# MeCabの設定
def get_yomi(text):
tagger = MeCab.Tagger("-Ochasen -r /etc/mecabrc")
node = tagger.parseToNode(text)
yomi = []
while node:
features = node.feature.split(",")
if features[0] == "名詞" and len(features) > 7:
yomi.append(features[7]) # 音読みや訓読みを取得
node = node.next
return ''.join(yomi) # 全部の読みを結合して返す
# ディレクトリ内のファイルタイトルを取得して読みを生成
def generate_yomi_for_directory(directory):
works_with_yomi = []
# ディレクトリ内のファイルを取得
for filename in os.listdir(directory):
if filename.endswith(".html"):
title = os.path.splitext(filename)[0] # 拡張子を取り除く
yomi = get_yomi(title)
works_with_yomi.append({'title': title, 'yomi': yomi})
return works_with_yomi
# 結果をJSONに保存
def save_to_json(data, filename='names.json'):
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
# 使用例
directory = '/home/mituo/aozora/shelf/芥川竜之介' # 本棚のディレクトリパス
works_with_yomi = generate_yomi_for_directory(directory)
save_to_json(works_with_yomi)

でもこれは完全ではないようで、

[
{
"title": "早春",
"yomi": "ソウシュン"
},
{
"title": "母",
"yomi": "ハハ"
},
{
"title": "素戔嗚尊",
"yomi": "モトミコト"
},
{
"title": "杜子春",
"yomi": "モリコハル"
},

杜子春をモリコハルと読んでます。

でも9割型はきちんと読んでいるので、それを編集することにしました。

以下のような php プログラムで編集します。

<?php
// JSONファイルを読み込む
function load_json($filename) {
return json_decode(file_get_contents($filename), true);
}
function save_json($data, $filename) {
$result = file_put_contents($filename, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
if ($result === false) {
echo "<p style='color:red;'>⚠️ 保存に失敗しました!ファイルパスまたはパーミッションを確認してください。</p>";
} else {
echo "<p>✅ <strong>" . $filename . "</strong> に保存しました。</p>";
}
}
// 読みを変更する関数
function edit_yomi(&$data, $title, $new_yomi) {
foreach ($data as &$entry) {
if ($entry['title'] === $title) {
$entry['yomi'] = $new_yomi;
}
}
}
// 使用例
$data = load_json('names.json');
// 一括編集処理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['yomi'])) {
foreach ($_POST['yomi'] as $title => $new_yomi) {
edit_yomi($data, $title, $new_yomi); // 各タイトルごとに読みを更新
}
save_json($data, 'names_edited.json'); // 更新されたデータをJSONファイルに保存
echo "<p>すべての読みが更新されました。</p>";
}
// 一覧表示
echo '<h1>タイトル一覧</h1>';
echo '<form method="POST" action="">';
echo '<ul>';
foreach ($data as $item) {
echo "<li><span style='display:inline-block; width:300px;'>{$item['title']}</span>
- <input type='text' name='yomi[{$item['title']}]' value='{$item['yomi']}' placeholder='新しい読み'>
</li>";
}
echo '</ul>';
echo '<input type="submit" value="一括編集">';
echo '</form>';
?>

このようにして、芥川竜之介と夏目漱石の作品合わせて 470 ほどをサーバー上にアップしました。

https://kunokatura-library.site/

作品のタイトル名の前の作者名の削除

xthml ファイルを普通にダウンロードすると、作品の前に作家名が付いてきます。 それではおかしいので上のプログラムでは作者名を削除するようになっているのですが、それとは別に古い名前の方が付いてくることがあります。

例えば、「柳田国男」の作品の一部には「柳田國男」という作家名が付いてくることがあります。それもやはり削除したいので、シェルスクリプトを作成しました。

#!/bin/bash
TARGET_DIR="柳田国男"
for file in "$TARGET_DIR"/*; do
new_name=$(echo "$file" | sed 's/柳田國男//g')
if [[ "$file" != "$new_name" ]]; then
mv "$file" "$new_name"
echo "ファイル名を変更しました: $file$new_name"
fi
done