こんにちは。Supership株式会社 の Sanosです。
この記事は Supershipグループ Advent Calendar 2025 の 15日目の記事です。
TL;DR
-
Notionエクスポートの差分を自動分析するWebアプリを開発しました
- FastAPI + Next.js 15で構築
- AWS Bedrock(Amazon Nova Pro)でAI差分解析
- 前編では差分分析ツールの実装を解説(後編でS3自動アップロードを解説予定)
- Notion IDベースの差分検出、HTMLパース、AIプロンプト設計など実装のポイントを紹介
はじめに
Notion で仕様書やドキュメントを管理していると、「前回から何が変わったんだっけ?」と確認したくなることってありませんか?
私たちのチームでは、Notionで管理している仕様書をS3にホスティングして公開するワークフローを運用していました。しかし、運用を続ける中で以下のような課題に直面していました:
- 差分の把握が困難: 数十〜数百のHTMLファイルを目視で比較するのは現実的ではない
- 変更履歴の追跡: 何がいつ変わったのか記録に残らず、レビューが困難
- レビューの負担: 更新前に変更内容を確認するのに膨大な時間がかかる
特に困ったのが、「前回のアップロードから何が変わったのか」を把握するのに、毎回ファイルを1つずつ開いて確認する必要があったことです。これでは効率が悪く、見落としのリスクも高まります。
そこで、Notionエクスポートファイルの差分を自動で分析し、AIがレポートを生成してくれるWebアプリを開発することにしました。
開発したツールには大きく分けて2つの機能があります:
- 差分分析ツール ← 本記事(前編)で解説
- S3アップロードツール ← 後編で解説予定
本記事では、前編として差分分析ツールの実装について、実際に試行錯誤した経験を交えながら解説します。
この記事でわかること
- Notionエクスポート(HTML)の差分を自動分析する方法
- AWS Bedrock(Amazon Nova Pro)を使ったAI差分解析の実装
- FastAPIでのレイヤー化アーキテクチャの設計
- 効果的なAIプロンプトの設計手法(失敗例も含めて)
- Next.js 15(App Router)とReact 19での状態管理
対象読者
- Notionでドキュメント管理をしている方
- AI(LLM)を使った実用的なアプリケーション開発に興味がある方
- FastAPIやNext.jsでのWeb開発を学びたい方
- HTMLの差分分析や比較ツールの実装に興味がある方
環境
| 項目 | バージョン |
|---|---|
| Python | 3.10+ |
| FastAPI | 0.104+ |
| Node.js | 20+ |
| Next.js | 15 |
| React | 19 |
| TypeScript | 5 |
| AWS Bedrock | Amazon Nova Pro v1 |
ツールの機能概要
開発した差分分析ツールは、シンプルながら強力な機能を提供します。
まず、ローカルにある最新のNotionエクスポート(ZIPファイル)をアップロードします。次に、S3に保存されている過去のバージョンを選択すると、ツールが自動的に2つのバージョンを比較し、以下の情報を抽出します:
- ローカルのNotionエクスポート(最新版)とS3に保存された過去バージョンを比較
- AI(Amazon Nova Pro)による自動差分解析
- マークダウン形式のレポート生成
- 追加・変更・削除されたページの可視化
- ページ単位での詳細な差分表示
特に便利なのが、AIによる差分の自動要約機能です。単なるファイルリストだけでなく、「どのページがどのように変わったのか」を自然言語で説明してくれるため、レビュー作業が格段に楽になりました。
UI画面イメージ
実際の画面はこのような構成になっています:
画面上部でZIPファイルをアップロードし、案件とバージョンを選択すると、下部に差分分析結果がマークダウン形式で表示されます。このレポートはその場で編集することもでき、最終的にマークダウンファイルとしてダウンロード可能です。
アーキテクチャ
システム構成
このツールは、バックエンド:FastAPI と フロントエンド:Next.jsの構成で、それぞれが明確な責務を持っています。
バックエンドは主に以下を担当します:
- ZIPファイルの処理とセッション管理
- S3からのファイル取得
- HTML差分の生成
- AI(Amazon Nova Pro)との連携
フロントエンドは以下を担当します:
- ユーザーインターフェース
- ファイルアップロード
- 案件・バージョン選択
- マークダウンエディタ機能
この構成により、フロントエンドとバックエンドを独立して開発・デプロイできるようになっています。
バックエンドの設計思想
FastAPIでレイヤー化アーキテクチャを採用しました。これは、コードの保守性と可読性を高めるための設計パターンです。
app/
├── routers/ # APIエンドポイント定義(HTTPリクエスト/レスポンス)
├── services/ # ビジネスロジック層(実際の処理)
├── models/ # データモデル定義(Pydanticスキーマ)
├── utils/ # ユーティリティ関数(共通処理)
└── lib/ # 外部ライブラリ連携(AWS Bedrockなど)
この設計により、例えば「S3の処理を変更したい」という場合でも、services/s3_service.pyだけを修正すればよく、他の部分に影響を与えません。特に複数人で開発する際に、この分離が非常に有効でした。
実装のポイント
ここからは、開発中に特に工夫した点や、苦労した点を中心に解説していきます。
1. Notion IDベースの差分検出 - ページ名が変わっても追跡できる仕組み
最初に直面した課題は、「ページ名が変更された場合、どうやって同一ページだと判断するか」でした。
Notionエクスポートでは、各ページのファイル名が以下のような形式になっています:
example-page-abc123def456.html
最初は単純にファイル名で比較していましたが、ページ名を変更すると完全に別のファイルとして扱われてしまい、「削除」と「追加」として認識されてしまいました。
そこで、ファイル名の末尾にある32文字のID(Notion ID)に着目しました。このIDはページ名が変わっても不変です。
def extract_id_from_filename(filename: str) -> str:
"""
ファイル名からNotion IDを抽出
例: "example-page-abc123def456.html" -> "abc123def456"
"""
match = re.search(r'([a-f0-9]{32})\.html$', filename)
return match.group(1) if match else ""
このIDベースの比較により、ページ名が変更されても「変更」として正しく認識されるようになりました。これは想像以上に重要な機能で、実際の運用で何度も助けられています。
2. HTMLの差分抽出 - ノイズとの戦い
次の課題は、「どうやって意味のある差分だけを抽出するか」でした。
HTMLファイルには、コンテンツ以外にも大量のタグ、スタイル、スクリプトが含まれています。単純に行単位で差分を取ると、タグの微妙な変更で大量の差分が検出され、本当に重要な変更が埋もれてしまいます。
そこで、BeautifulSoupを使ってHTMLを解析し、テキストコンテンツのみを抽出する方法を採用しました:
def extract_text_content(html: str) -> str:
"""
HTMLからテキストコンテンツのみを抽出
"""
soup = BeautifulSoup(html, 'html.parser')
# スクリプトやスタイルタグを削除
for tag in soup(['script', 'style']):
tag.decompose()
# テキストのみを取得
text = soup.get_text(separator='\n')
# 余分な空白を整理
lines = [line.strip() for line in text.splitlines() if line.strip()]
return '\n'.join(lines)
この処理により、本当に変更されたコンテンツだけを抽出できるようになり、差分の精度が大幅に向上しました。
3. AIによる差分解析 - Amazon Nova Proの活用
差分データが取れたら、次はそれを人間が理解しやすい形にまとめる必要があります。ここでAWS BedrockのAmazon Nova Proを活用しました。
Amazon Nova Proは、AWS独自の大規模言語モデルで、高性能かつコスト効率が良いのが特徴です。Claude APIとは異なる独自のフォーマットを使用します:
def call_claude_bedrock(prompt: str, model_id: str = 'us.amazon.nova-pro-v1:0') -> str:
"""
AWS Bedrock経由でAmazon Nova Proを呼び出し、差分分析を依頼
"""
client = boto3.client('bedrock-runtime', region_name='ap-northeast-1')
# Nova用のリクエストフォーマット
body = {
"messages": [{
"role": "user",
"content": [{"text": prompt}]
}],
"inferenceConfig": {
"max_new_tokens": 32768
}
}
response = client.invoke_model(
modelId=model_id,
body=json.dumps(body)
)
# Nova用のレスポンス構造
response_body = json.loads(response['body'].read())
return response_body['output']['message']['content'][0]['text']
ポイント:
-
inferenceConfigで最大トークン数を指定 - リクエスト・レスポンスの構造がClaude APIとは異なる
-
us.amazon.nova-pro-v1:0のようなinference profile ARNを使用
4. プロンプト設計の試行錯誤 - 失敗から学んだこと
AIに差分を分析させる際、プロンプトの設計が最も重要でした。ここでは、実際に失敗した例と、その改善過程を紹介します。
❌ 初期バージョン(全然ダメでした):
最初は本当にシンプルなプロンプトから始めました:
prompt = "差分を分析してください。"
結果は散々で、AIは何を出力すべきか分からず、毎回異なるフォーマットで返答してきました。構造化されていないテキストは、後続の処理で扱いにくく、実用になりませんでした。
✅ 改善後(期待通りの結果が得られました):
試行錯誤を重ね、最終的に以下のようなプロンプトに落ち着きました:
def create_analysis_prompt(diff_data: dict) -> str:
"""
差分分析用のプロンプトを生成
"""
added_pages = diff_data.get('added', [])
deleted_pages = diff_data.get('deleted', [])
modified_pages = diff_data.get('modified', [])
prompt = f"""
あなたはドキュメントの差分を分析する専門家です。
以下の差分データを分析し、変更内容を**具体的に**要約してください。
## 追加されたページ({len(added_pages)}件)
{format_page_list(added_pages)}
## 削除されたページ({len(deleted_pages)}件)
{format_page_list(deleted_pages)}
## 変更されたページ({len(modified_pages)}件)
{format_modified_pages_with_diff(modified_pages)}
以下のマークダウン形式で出力してください:
# 差分分析レポート
## 📊 概要
- 追加: X件
- 変更: Y件
- 削除: Z件
## ➕ 追加されたページ
### [ページ名1]
- 追加理由の推測
- 主な内容
(以下略)
"""
return prompt
改善のポイント:
- 役割を明確に定義:「ドキュメントの差分を分析する専門家」と役割を与えた
- 件数を明示:AIが全体像を把握しやすくなった
- 具体的な出力フォーマットを指示:マークダウン形式で詳細に指定
この改善により、AIは毎回一貫したフォーマットで、読みやすいレポートを生成してくれるようになりました。
5. セッション管理 - 複数ユーザー対応の工夫
このツールは複数人が同時に使う可能性があるため、セッション管理が必要でした。
各ユーザーのアップロードしたファイルが混ざらないよう、ユーザーごとに一意のセッションIDを生成し、独立したディレクトリで管理しています:
def create_session_id() -> str:
"""
ユニークなセッションIDを生成
"""
session_uuid = uuid.uuid4()
session_id = base64.urlsafe_b64encode(
session_uuid.bytes
).decode('utf-8').rstrip('=')
return session_id
各セッションは以下のような構造でファイルを管理します:
/tmp/
├── session_abc123/
│ ├── local/ # アップロードされた最新版
│ └── s3/ # S3からダウンロードした過去版
これにより、Aさんがアップロードしたファイルと、Bさんがアップロードしたファイルが混ざることなく、安全に並行処理できるようになりました。
6. フロントエンドの状態管理 - カスタムフックで見通しを良く
フロントエンドでは、「ZIPアップロード→案件選択→バージョン選択→ファイル選択→差分分析」という複数のステップがあり、状態管理が複雑になりました。
そこで、カスタムフックを使って状態管理をカプセル化し、コンポーネントをシンプルに保ちました:
// hooks/useFileSelection.ts
export function useFileSelection(initialFiles: string[]) {
const [selectedFiles, setSelectedFiles] = useState<string[]>(initialFiles);
const toggleFile = (file: string) => {
setSelectedFiles(prev =>
prev.includes(file)
? prev.filter(f => f !== file)
: [...prev, file]
);
};
const selectAll = () => setSelectedFiles(initialFiles);
const deselectAll = () => setSelectedFiles([]);
return { selectedFiles, toggleFile, selectAll, deselectAll };
}
このように状態管理ロジックをフックに切り出すことで、コンポーネントは「見た目」だけに集中できるようになり、コードの可読性が大幅に向上しました。
データフローの全体像
ここまで個別の機能を解説してきましたが、全体のフローを見てみましょう。
ユーザーがZIPファイルをアップロードしてから、差分レポートが表示されるまでの流れは以下の通りです:
1. ユーザー → ZIPファイルアップロード
↓
2. バックエンド → セッションID発行、ファイル展開
↓
3. ユーザー → 案件を選択
↓
4. バックエンド → S3から案件のバージョン一覧を取得
↓
5. ユーザー → 比較したいバージョンを選択
↓
6. バックエンド → S3から過去バージョンをダウンロード
↓
7. ユーザー → 「差分分析開始」ボタンをクリック
↓
8. バックエンド → HTMLファイルを比較、差分データ生成
↓
9. バックエンド → Amazon Nova Proで差分を分析
↓
10. フロントエンド → マークダウンレポートを表示
各ステップで適切なエラーハンドリングとローディング表示を実装しているため、ユーザーは現在の処理状況を常に把握できるようになっています。
マークダウンエディタで確認・編集
AIが生成したレポートは、そのまま使えることもあれば、少し手直しが必要な場合もあります。そこで、リアルタイムでプレビューしながら編集できるマークダウンエディタを実装しました。
エディタの便利な機能
1. 編集タブ(Edit)
左側の編集タブでは、マークダウン記法を使ってレポートを自由に編集できます。AIが生成した内容に追加情報を書き足したり、表現を調整したりできます。
2. プレビュータブ(Preview)
右側のプレビュータブでは、編集した内容がどのように表示されるかをリアルタイムで確認できます。見出し、リスト、コードブロックなどが正しくレンダリングされているか、すぐにチェックできます。
3. ダウンロードボタン
編集が完了したら、右上のダウンロードボタンでマークダウンファイルとして保存できます。ファイル名は自動的にYYYYMMDD.md形式(例:20241208.md)で生成されるため、日付管理も簡単です。
このエディタ機能により、AIが生成した差分レポートを、プロジェクトの実情に合わせてカスタマイズできるようになり、実用性が大幅に向上しました。
パフォーマンス最適化 - 実運用で見えてきた課題と対策
実際に運用を始めると、パフォーマンスの問題がいくつか見えてきました。特に、Notionエクスポートが数百ファイルになると、処理時間が無視できなくなりました。
1. S3からのダウンロード高速化
最初はboto3でファイルを1つずつダウンロードしていましたが、これが非常に遅かったです。そこで、AWS CLIのsyncコマンドを使うことで、大幅に高速化できました:
def download_from_s3(bucket: str, prefix: str, local_dir: str):
"""
AWS CLIを使って高速にダウンロード
"""
cmd = [
"aws", "s3", "sync",
f"s3://{bucket}/{prefix}",
local_dir,
"--quiet"
]
subprocess.run(cmd, check=True)
AWS CLIは内部で並列ダウンロードを行うため、boto3で逐次ダウンロードするより速くなりました。
2. 並列処理の導入
ファイルの差分生成も、最初は1つずつ処理していましたが、これを並列処理に変更しました:
from concurrent.futures import ThreadPoolExecutor
def process_files_parallel(files: list, func: callable) -> list:
"""
複数ファイルを並列処理
"""
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(func, files))
return results
これにより、処理時間が短縮されました。特にファイル数が多い場合に効果が顕著です。
3. キャッシングで無駄なリクエストを削減
S3バケット一覧など、頻繁に変更されないデータはキャッシュすることで、無駄なAWSリクエストを削減しました:
from functools import lru_cache
@lru_cache(maxsize=128)
def get_project_list() -> list:
"""
案件一覧をキャッシュ(メモリ内に保持)
"""
# S3バケット一覧を取得...
return projects
これにより、レスポンスタイムが改善されました
まとめ
今回は、Notionエクスポートの差分分析ツールを開発した経験について解説しました。
開発を通じて学んだポイントをまとめます:
技術的な学び:
- Notion IDベースの差分検出により、ページ名変更にも対応できた
- BeautifulSoupでのHTML解析で、意味のある差分だけを抽出できた
- Amazon Nova Proの活用で、AIによる自動レポート生成を実現
- プロンプト設計の重要性を実感(試行錯誤が必要)
- FastAPIのレイヤー化アーキテクチャで保守性の高いコードを実現
- Next.js 15のカスタムフックで状態管理をシンプルに
運用面での学び:
- パフォーマンス最適化は実運用で初めて見えてくる
- ユーザーフィードバックが改善の最大のヒント
- マークダウンエディタ機能が想像以上に重宝された
次回予告
【後編】では、NotionエクスポートをS3に自動アップロードする機能について解説します:
- CSS注入による統一デザインの適用
- パンくずリストの自動生成
- CloudFrontキャッシュの自動クリア
- バリデーション機能の実装
Notionエクスポートを美しく、使いやすい形でS3にホスティングする方法を詳しく紹介します。お楽しみに!
参考
最後に宣伝です。
Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
Supership 採用サイト
是非ともよろしくお願いします。


