👋
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