まえがき
この記事は生成AIだけでどこまで出来るかの実験も兼ねています。
- 使用したツール: Manus で基本形を作成、 Windsurf で詳細を修正・デバッグして完成
- この記事の本文: Windsurf で自動生成
- Manusの再生リンク: https://manus.im/share/eXzMrB2VPkvkqbzOYDFZQC?replay=1
- GitHubリポジトリ: https://github.com/tomorrow56/qiita_web_downloader
- デプロイURL(Vercel): https://qiitawebdownloader.vercel.app/
はじめに
Qiitaの記事を読んでいると、「この記事、手元に残しておきたいな」「オフラインでも読めるようにしたいな」と思うことはありませんか?
ブラウザでページを保存しても、画像のリンクが切れてしまったり、レイアウトが崩れてしまったりと、なかなかうまくいかないものです。
そこで今回は、 Qiitaの記事URLを入力するだけで、記事本文と画像をまとめてZIPファイルとしてダウンロードできるWebツール「Qiita Web Downloader」 を開発しました。
この記事では、そのツールの機能と、それを実現している技術的な仕組みについて詳しく解説します。
ツールの主な機能
このツールには、主に以下の機能が搭載されています。
- 🔗 URL入力だけで簡単ダウンロード: Qiitaの記事URLを貼り付けてボタンを押すだけで、誰でも簡単に記事をダウンロードできます。
- 📝 HTMLからMarkdownへ変換: 取得した記事のHTMLを、プレーンなMarkdown形式に変換します。
- 🖼️ 画像の自動ダウンロード: 記事内に含まれる画像を自動で検出し、すべてダウンロードします。
- 📦 ZIP形式で一括配信: 変換後のMarkdownファイルと、ダウンロードした画像群を一つのZIPファイルにまとめて提供します。
- ✨ Markdown品質の向上: 自動変換だけでは不十分な部分を補正し、より見やすく、再利用しやすいMarkdownを生成します。
技術的な仕組みの解説
このツールは、バックエンドにPythonのWebフレームワークであるFlaskを使用しています。全体の処理の流れは、以下のようになっています。
- ユーザーがWebフロントからQiitaのURLを送信
- FlaskサーバーがURLを受け取り、記事のダウンロード処理を開始
- 処理結果(Markdownと画像)をZIPファイルに圧縮
- 生成されたZIPファイルをユーザーに送信
ここからは、この処理を実現している3つのコア技術について、コードを交えながら解説します。
コア技術1: HTMLの解析と情報抽出 (BeautifulSoup)
まず、ユーザーから受け取ったURLを元に、requestsライブラリで記事ページのHTMLコンテンツを取得します。
import requests
from bs4 import BeautifulSoup
url = "https://qiita.com/..."
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
次に、取得したHTMLのスープ(soupオブジェクト)から、強力なHTML解析ライブラリBeautifulSoupを使って、必要な情報だけをピンポイントで抽出します。
Qiitaの記事では、タイトルはh1タグ、本文はit-MdContentというクラス名を持つsectionタグに格納されています。
# 記事タイトルを取得
title_tag = soup.find('h1', attrs={'data-logly-title': 'true'})
title = title_tag.get_text().strip()
# 記事本文のコンテンツを取得
content_div = soup.find('section', class_='it-MdContent')
これにより、広告やヘッダー、フッターといった不要な情報を取り除き、記事の核となる部分だけを正確に抜き出すことができます。
コア技術2: 画像のダウンロードとパス置換
次に、抽出した記事本文(content_div)に含まれる画像を処理します。
まず、find_all('img')を使って、本文中のすべての<img>タグをリストアップします。
for img_tag in content_div.find_all('img'):
img_url = img_tag.get('src')
if not img_url:
continue
# 画像をダウンロードして保存
img_response = requests.get(img_url)
with open("images/image_001.png", 'wb') as f:
f.write(img_response.content)
# imgタグのsrc属性をローカルパスに書き換える
img_tag['src'] = "images/image_001.png"
ループ処理の中で、各<img>タグのsrc属性から画像URLを取得し、ダウンロードしてimagesフォルダに保存します。
ここでの重要なポイントは、ダウンロード後にsrc属性の値を、保存したローカル画像の相対パスに書き換えている点です。これにより、ダウンロードしたMarkdownファイルをどの環境で開いても、画像が正しく表示されるようになります。
コア技術3: Markdownへの変換と品質向上 (markdownify + re)
画像パスの置換が完了したHTMLコンテンツを、いよいよMarkdownに変換します。変換にはmarkdownifyというライブラリを使用しており、非常にシンプルに実行できます。
from markdownify import markdownify as md
markdown_content = md(str(content_div), heading_style="ATX")
しかし、markdownifyによる自動変換だけでは、完璧なMarkdownが得られない場合があります。例えば、以下のようなケースです。
- 見出しの
#とテキストの間にスペースがない (#見出し) - 太字を表す
**がバックスラッシュでエスケープされてしまう (\*\*強調\*\*)
そこで、Pythonの正規表現モジュールreを使い、これらの不要な部分を置換・修正する後処理を加えています。
import re
# 見出しのスペースを修正: #見出し → # 見出し
markdown_content = re.sub(r'^(#{1,6})([^\s#])', r'\1 \2', markdown_content, flags=re.MULTILINE)
# エスケープされたアスタリスクを修正: \*\* → **
markdown_content = re.sub(r'\\(\*\*|_\_)', r'\1', markdown_content)
このような地道な整形処理を加えることで、手直し不要な、クリーンで読みやすいMarkdownを生成しています。
こだわったポイント
最後に、開発で特に工夫した点をいくつか紹介します。
-
ファイル名のサニタイズ: 記事のタイトルをそのままフォルダ名にすると、OSで使えない文字(
?や*など)が含まれている場合にエラーとなります。そのため、正規表現を使ってこれらの禁止文字をあらかじめ除去する処理を入れています。 -
一時ファイルの安全な管理: 処理中に生成されるMarkdownファイルや画像は、
tempfileモジュールで作成した一時ディレクトリに保存しています。これにより、他の処理とファイルが衝突するのを防ぎ、処理完了後にはディレクトリごと自動でクリーンアップされるため、安全かつ効率的です。 - 丁寧なエラーハンドリング: ネットワークエラーで記事が取得できなかったり、無効なURLが入力されたりした場合を想定し、それぞれの状況に応じたエラーメッセージをユーザーに返すようにしています。
まとめ
今回は、Qiitaの記事をMarkdown形式でバックアップするためのWebツール「Qiita Web Downloader」を開発しました。
BeautifulSoupでのHTML解析、markdownifyと正規表現を組み合わせた高品質なMarkdown変換など、Pythonの強力なライブラリを活かすことで、シンプルながらも実用的なツールが実現できたと思います。
ソースコードはGitHubで公開していますので、興味のある方はぜひご覧ください。
- GitHubリポジトリ: tomorrow56/qiita_web_downloader