👋

Zennの自分の全記事をダウンロードしてファイル化するスクリプト

に公開

Zennの非公式APIを使って、自分の全記事をダウンロードするスクリプトを書いてみました。
アシスタントにChatGPTを使いました。

chmod +x zenndl.pyで実行権を付け、./zenndl.pyで実行させてください。

zenndl.py
#!/usr/bin/env python3
import requests
import os
from bs4 import BeautifulSoup
import re
import html2text

USERNAME = "fygar256"  # ← あなたのZennユーザー名
OUTPUT_DIR = "./zenn_articles"

fileno=1

def fetch_articles(username):
    url = f"https://zenn.dev/api/articles?username={username}&order=latest"
    resp = requests.get(url)
    resp.raise_for_status()
    return resp.json().get("articles", [])

def extract_and_replace_code_blocks(content_div):
    global fileno
    """
    <pre><code> ... </code></pre> を ```lang filename=xxx ... ``` に変換し、
    プレースホルダに置き換えて返す
    """
    placeholders = {}
    counter = 0

    for pre in content_div.find_all("pre"):
        code_tag = pre.code
        if not code_tag:
            continue

        # 言語クラス取得
        lang = None
        for cls in code_tag.get("class", []):
            if cls.startswith("language-"):
                lang = cls.replace("language-", "")
                break

        # ファイル名取得(もし code_tag に data-filename 属性があればそれを使う)
        filename = code_tag.get("data-filename", None)
        if not filename and lang:
            # 例: lang = python → filename = snippet.py
            ext_map = {
                "python": "py",
                "javascript": "js",
                "typescript": "ts",
                "ruby": "rb",
                "java": "java",
                "c": "c",
                "cpp": "cpp",
                "go": "go",
                "bash": "sh",
                "shell": "sh",
                "html": "html",
                "css": "css",
                "json": "json",
                "yaml": "yaml",
                "markdown": "md",
                # 必要に応じて拡張してください
            }
            ext = ext_map.get(lang, None)
            if ext:
                filename = f"snippet{fileno}.{ext}"
                fileno+=1
            else:
                filename = None

        # コードテキスト取得
        code_text = code_tag.get_text()

        # インデント1段浅く
        code_lines = [line[1:] if line.startswith(" ") else line for line in code_text.splitlines()]
        code_text = "\n".join(code_lines)

        if filename:
            fenced_code = f"\n```{lang}:{filename}\n{code_text}\n```\n"
        else:
            fenced_code = f"\n```{lang or ''}\n{code_text}\n```\n"

        # プレースホルダに置き換え
        placeholder = f"{{{{CODEBLOCK_{counter}}}}}"
        placeholders[placeholder] = fenced_code
        pre.replace_with(placeholder)
        counter += 1

    return placeholders

def convert_html_to_markdown(content_div):
    # コードブロック抽出 → プレースホルダ化
    placeholders = extract_and_replace_code_blocks(content_div)

    # 通常本文をMarkdown化
    h2t = html2text.HTML2Text()
    h2t.body_width = 0
    h2t.single_line_break = True
    h2t.ignore_links = False
    h2t.ignore_images = False

    markdown_text = h2t.handle(str(content_div))
    markdown_text = re.sub(r"\n{3,}", "\n\n", markdown_text).strip()

    # プレースホルダを元のフェンスコードに戻す
    for placeholder, code in placeholders.items():
        markdown_text = markdown_text.replace(placeholder, code)

    return markdown_text

def fetch_article_body(article_url):
    resp = requests.get(article_url)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    content_div = soup.select_one("article .znc")
    if not content_div:
        return None
    return convert_html_to_markdown(content_div)

def save_article(article, body_text):
    safe_title = re.sub(r'[\\/*?:"<>|]', "_", article["title"])
    filename = f"{safe_title or article['slug']}.md"
    path = os.path.join(OUTPUT_DIR, filename)
    with open(path, "w", encoding="utf-8") as f:
        f.write(f"# {article['title']}\n\n")
        f.write(body_text or "[本文取得失敗]")
    print(f"Saved: {filename}")

def main():
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    articles = fetch_articles(USERNAME)
    print(f"Found {len(articles)} articles.")
    for a in articles:
        article_url = f"https://zenn.dev{a['path']}"
        body_text = fetch_article_body(article_url)
        save_article(a, body_text)

if __name__ == "__main__":
    main()

Discussion