📕

Zenn本をKindle本に変換する方法

2024/10/09に公開

概要

この記事では、Zennで執筆した本をKindle出版に適した形式に変換する方法を紹介します。Zennの書籍をMarkdownで執筆している場合、pandocを使うことで簡単にEPUB形式の電子書籍に変換できます。また、カバー画像のリサイズなど、Kindleの要件に合わせた調整も行います。

前提

この記事で紹介する手順を進める前に、以下の条件が満たされていることを確認してください。

  • GitHub連携でZennの本を執筆している
  • 本のカバー画像がcover.pngとして保存されている
  • pandocがインストールされている
  • Python環境が整備されている

必要なパッケージ

  1. Pillow:
    画像の読み込み、リサイズ、形式変換(PNGからJPGなど)を行うためのライブラリ。

    • 用途: 表紙画像のリサイズと形式変換。
  2. PyYAML:
    YAMLファイル(書籍の設定など)を読み込み、解析するためのライブラリ。

    • 用途: config.yamlなどの書籍情報を扱うため。

完成するもの

このプロセスを経て、以下のファイルが生成されます。

  • EPUB形式の電子書籍ファイル
  • JPEG形式のカバー画像

これらをそのままKindle出版にアップロードすることができます。

URLの変換

Zennでは、URL単体を記載するとカード形式で表示されます。しかし、Kindleではその形式を利用できないため、本文中のURLはすべてタイトル付きリンクに変換する必要があります。これには、Pythonスクリプトを使って自動的に行います。

replace_urls_in_text.py
import re
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, parse_qs, urlunparse, urlencode
import time

def get_page_title(url):
    """
    指定されたURLのページタイトルを取得する
    :param url: タイトルを取得したいページのURL
    :return: ページのタイトル
    """
    try:
        time.sleep(1)  # サーバーへの負荷を減らすために1秒待つ
        response = requests.get(url)
        if response.status_code == 200:
            soup = BeautifulSoup(response.content, 'html.parser')
            return soup.title.string.strip()
        else:
            print(f"Failed to retrieve the page: {url}")
            return url  # タイトルが取得できない場合はURLをそのまま返す
    except Exception as e:
        print(f"Error fetching title for {url}: {e}")
        return url

def expand_amazon_short_url(url):
    """
    Amazonの短縮URL (https://amzn.to/) を展開し、アソシエイトタグを除去する
    :param url: Amazonの短縮URL
    :return: 展開されたURLからアソシエイトタグを除去したURL
    """
    try:
        # 短縮URLを展開
        response = requests.head(url, allow_redirects=True)
        full_url = response.url
        
        # アソシエイトタグ(tag=xxxx)をURLから除去
        parsed_url = urlparse(full_url)
        query_params = parse_qs(parsed_url.query)
        
        # 'tag'パラメータを削除
        if 'tag' in query_params:
            del query_params['tag']
        
        # 新しいクエリを構築
        new_query = urlencode(query_params, doseq=True)
        new_url = urlunparse(parsed_url._replace(query=new_query))
        
        return new_url
    except Exception as e:
        print(f"Error expanding or cleaning Amazon URL {url}: {e}")
        return url

def replace_urls_in_text(content):
    """
    テキスト内でURLだけが記載された行に対して、タイトル付きリンクに変換し、
    Amazonアソシエイトリンクの場合は展開してタグを除去する
    :param content: Markdownの内容
    :return: タイトル付きリンクに変換されたMarkdown内容
    """
    # 行ごとに分割して処理
    lines = content.splitlines()
    
    url_pattern = re.compile(r'^(https?://[^\s]+)$')  # URLだけが書かれた行を検出
    
    for i, line in enumerate(lines):
        match = url_pattern.match(line)
        if match:
            url = match.group(1)
            # Amazonの短縮URLの場合は展開してタグを外す
            if url.startswith('https://amzn.to/'):
                clean_url = expand_amazon_short_url(url)
            else:
                clean_url = url

            # URLからタイトルを取得
            title = get_page_title(clean_url)
            
            # Markdownのリンク形式に変換
            markdown_link = f'[{title}]({clean_url})'
            
            # 行をタイトル付きリンクに置換
            lines[i] = markdown_link
    
    return '\n'.join(lines)

ファイルの読み込み

Zennの本の設定ファイルや章ごとのMarkdownファイルを読み込むための関数です。

main.py
import yaml

def load_config(slug):
    config_path = f'books/{slug}/config.yaml'
    with open(config_path, 'r', encoding='utf-8') as file:
        config = yaml.safe_load(file)
    return config

def load_chapter(slug, chapter_name):
    chapter_path = f'books/{slug}/{chapter_name}.md'
    with open(chapter_path, 'r', encoding='utf-8') as file:
        content = file.read()
    return content

ファイルの結合と見出しの階層修正

Zennの本は章ごとに分かれていますが、Kindle用のファイルとしては全体を1つに結合する必要があります。さらに、見出しの階層がばらついている場合、これを統一します。

main.py
import re
import os

def find_min_heading_level(content):
    headings = re.findall(r'^(#+) ', content, re.MULTILINE)
    if headings:
        return min(len(h) for h in headings)
    return None

def adjust_heading_levels(content, min_level, base_level=2):
    if min_level is None:
        return content
    level_diff = base_level - min_level
    def adjust(match):
        heading = match.group(1)
        text = match.group(2)
        return '#' * (len(heading) + level_diff) + ' ' + text
    adjusted_content = re.sub(r'^(#+)(\s+.*)', adjust, content, flags=re.MULTILINE)
    return adjusted_content

def create_combined_markdown(slug, config):
    os.makedirs(f'kindle/{slug}', exist_ok=True)
    combined_path = f'kindle/{slug}/all.md'

    with open(combined_path, 'w', encoding='utf-8') as outfile:
        for chapter in config['chapters']:
            chapter_content = load_chapter(slug, chapter)

            title_match = re.search(r'title: "(.+)"', chapter_content)
            chapter_title = title_match.group(1) if title_match else chapter

            chapter_title = chapter_title.replace('"', '')
            outfile.write(f'# {chapter_title}\n\n')

            body = extract_body(chapter_content)
            min_level = find_min_heading_level(body)
            adjusted_body = adjust_heading_levels(body, min_level, base_level=2)
            outfile.write(adjusted_body + '\n\n')

画像パスの修正

画像のパスを修正し、必要な画像を適切な場所にコピーします。

main.py
import re

def copy_images(slug, config):
    combined_path = f'kindle/{slug}/all.md'
    with open(combined_path, 'r+', encoding='utf-8') as file:
        content = file.read()
        
        # 画像パス中のサイズ指定を削除 (例: `=400x` を除去)
        updated_content = re.sub(r'(\s*=\d+x\d*)', '', content)
        # 画像パスを/images → ./imagesに変更
        updated_content = re.sub(r'/images', "./images", updated_content)
        
        # ファイルに修正したMarkdownを再度書き込み
        file.seek(0)
        file.write(updated_content)
        file.truncate()

カバー画像のリサイズと変換

Kindleで利用するカバー画像は特定のサイズと形式に準拠している必要があります。Pillowを使って、画像を自動的に調整します。

main.py
from PIL import Image

def resize_and_convert_cover(cover_source, cover_dest_jpg):
    MIN_WIDTH = 625
    MIN_HEIGHT = 1000
    MAX_SIZE = 10000

    with Image.open(cover_source) as img:
        width, height = img.size

        if width < MIN_WIDTH or height < MIN_HEIGHT:
            img

.thumbnail((MAX_SIZE, MAX_SIZE), Image.Resampling.LANCZOS)
            new_width = max(MIN_WIDTH, width)
            new_height = max(MIN_HEIGHT, height)
            img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

        img.thumbnail((MAX_SIZE, MAX_SIZE), Image.Resampling.LANCZOS)
        rgb_img = img.convert('RGB')
        rgb_img.save(cover_dest_jpg, 'JPEG')

メタデータの生成

書籍のメタデータを作成します。

main.py
import shutil
import yaml

def create_metadata(slug, config, author='Unknown'):
    metadata_path = f'kindle/{slug}/metadata.yaml'
    cover_source = f'books/{slug}/cover.png'
    cover_dest = f'kindle/{slug}/cover.png'
    cover_dest_jpg = f'kindle/{slug}/cover.jpg'
    
    if os.path.exists(cover_source):
        shutil.copy(cover_source, cover_dest)

        resize_and_convert_cover(cover_source, cover_dest_jpg)
    else:
        cover_dest = None  # カバーがない場合の処理

    metadata = {
        'title': config['title'],
        'author': author,
        'cover_image': './cover.png' if cover_dest else None
    }

    with open(metadata_path, 'w', encoding='utf-8') as file:
        yaml.dump(metadata, file, default_flow_style=False)

GitHub Markdown CSSのダウンロード

GitHubのMarkdown用CSSをダウンロードして、EPUBファイルに適用します。

main.py
import requests

def download_css(css_url, output_path):
    """
    指定されたURLからCSSファイルをダウンロードし、指定された場所に保存する
    :param css_url: CSSファイルのURL
    :param output_path: 保存先のパス
    """
    try:
        response = requests.get(css_url)
        response.raise_for_status()  # HTTPエラーがあれば例外を発生させる
        with open(output_path, 'wb') as css_file:
            css_file.write(response.content)
        print(f"Downloaded CSS from {css_url} and saved to {output_path}")
    except requests.exceptions.RequestException as e:
        print(f"Failed to download CSS: {e}")

EPUBファイルの生成

ZennのMarkdownファイルとカバー画像を用意したら、pandocを使ってEPUB形式の電子書籍に変換します。

main.py
import subprocess

def convert_to_epub(slug):
    output_epub = f'kindle/{slug}/book.epub'
    markdown_file = f'kindle/{slug}/all.md'
    metadata_file = f'kindle/{slug}/metadata.yaml'
    css_file = f'kindle/{slug}/github-markdown.css'
    cover_image = f'kindle/{slug}/cover.jpg'

    subprocess.run([
        'pandoc',
        markdown_file,
        '--metadata-file', metadata_file,
        '--css', css_file,
        '--epub-cover-image', cover_image,
        '--toc',
        '--toc-depth=1',
        '--highlight-style', 'tango',
        '-o', output_epub
    ])

toc-depthは目次の階層を指定します。必要に応じて変更してください。

全体の処理

最後に、Zenn本をKindle用に変換するためのすべての手順をまとめて実行します。

main.py
from replace_urls import replace_urls_in_text
GITHUB_MARKDOWN_CSS_URL = 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css'

def convert_zenn_to_kindle(slug, author='Unknown'):
    config = load_config(slug)
    
    create_combined_markdown(slug, config)
    copy_images(slug, config)

    combined_path = f'kindle/{slug}/all.md'
    with open(combined_path, 'r', encoding='utf-8') as file:
        content = file.read()
        updated_content = replace_urls_in_text(content)
    with open(combined_path, 'w', encoding='utf-8') as file:
        file.write(updated_content)

    create_metadata(slug, config, author)

    css_output_path = f'kindle/{slug}/github-markdown.css'
    download_css(GITHUB_MARKDOWN_CSS_URL, css_output_path)

    convert_to_epub(slug)

コマンドライン引数の処理

コマンドライン引数で書籍のslugと著者名を指定して実行します。

main.py
import argparse

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Zenn書籍をKindle用に変換するプログラム')
    parser.add_argument('slug', type=str, help='Zenn書籍のスラッグ(ディレクトリ名)')
    parser.add_argument('--author', type=str, default='Unknown', help='著者名(オプション)')

    args = parser.parse_args()

    convert_zenn_to_kindle(args.slug, args.author)

実行方法

以下のコマンドを実行することで、Zenn書籍をKindle用に変換できます。

python main.py slug --author "著者名"

これで、指定したslugに対応する書籍がkindle/slugディレクトリに出力され、book.epubcover.jpgが生成されます。これらをKindle出版にアップロードしてください。

Discussion