
目次
はじめに
前回はNotionの文章をPDFに変換する際に必要になるHTMLの編集作業を、ローカルPCに構築したPython環境を利用して自動化しました。
これと同様の処理をクラウドで処理するようにしてみます。
そうすることで、ローカルPCにPython環境がなくても、WEBブラウザを使って編集処理の自動化が可能です。
利用するクラウド PythonAnywhereとは?
PythonAnywhereは、Pythonで作ったWebアプリを無料(または有料)でインターネット上に公開できるサービスです。
サーバーやネットワークの知識がなくても、Webアプリを公開できます。
アカウントを作るだけで、すぐにWebサービスを公開できるのが魅力です。
Flaskとは?
Flask(フラスク)は、PythonでWebアプリを作るための「フレームワーク」と呼ばれるものです。
フレームワークとは、Webアプリを作る際の土台や便利な道具をまとめたもの。
Flaskはシンプルで使いやすく、初心者でもWebアプリを作りやすいのが特徴です。
PythonAnywhereでFlaskアプリを公開する手順
PythonAnywhereでアカウントを作る
PythonAnywhereの公式サイトで無料アカウントを作成します。
Flaskアプリのファイルを準備する
今回のアプリのために次のファイルを準備します。
app.py
(Webアプリ本体)requirements.txt
(必要なパッケージ)templates/index.html
(画面のテンプレート)static/custom.css
(スタイルシート)
各ファイルの内容は次の通りです。
app.py
import os
from flask import Flask, render_template, request, send_file, url_for
from bs4 import BeautifulSoup
import uuid
import threading
app = Flask(__name__)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
CSS_FILE_PATH = os.path.join(BASE_DIR, 'static', 'custom.css')
def process_html(input_file_path):
with open(input_file_path, 'r', encoding='utf-8') as f:
soup = BeautifulSoup(f, 'html.parser')
# 追加: imgタグがaタグの中にある場合、aタグを削除してimgタグだけ残す
for img in soup.find_all('img'):
if img.parent.name == 'a':
img.parent.unwrap()
head_tag = soup.find('head')
if not head_tag:
head_tag = soup.new_tag('head')
soup.html.insert(0, head_tag)
script1 = soup.new_tag('script', src='https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js')
head_tag.append(script1)
script2 = soup.new_tag('script')
script2.string = "mermaid.initialize({startOnLoad:true,theme: 'default'});"
head_tag.append(script2)
for code in soup.find_all('code', class_='language-Mermaid'):
new_div = soup.new_tag('div', **{'class': 'mermaid'})
for content in code.contents:
content.extract()
new_div.append(content)
code.replace_with(new_div)
toc_h1 = soup.find('h1', string='目次')
if toc_h1:
prev_elements = []
current = toc_h1.previous_sibling
while current:
prev_elements.insert(0, current)
current = current.previous_sibling
if prev_elements:
center_div = soup.new_tag('div', id='top-page', style='text-align:center;margin:20px 0;')
for elem in prev_elements:
elem.extract()
center_div.append(elem)
toc_h1.insert_before(center_div)
for elem in soup.find_all(True, class_=lambda c: c and 'table_of_contents-item' in c.split() and 'table_of_contents-indent-0' in c.split()):
if elem.text.strip() == '目次':
prev = elem.find_previous_sibling(name=True)
if prev:
prev.decompose()
elem.decompose()
break
if os.path.exists(CSS_FILE_PATH):
with open(CSS_FILE_PATH, 'r', encoding='utf-8') as css_file:
css_content = css_file.read()
style_tag = head_tag.find('style')
if style_tag:
style_tag.string = (style_tag.string or '') + '\n' + css_content
else:
style_tag = soup.new_tag('style')
style_tag.string = css_content
head_tag.append(style_tag)
else:
pass
for details in soup.find_all('details'):
prev_p = details.find_previous_sibling('p')
if prev_p and (not prev_p.get_text(strip=True)):
prev_p.decompose()
next_p = details.find_next_sibling('p')
if next_p and (not next_p.get_text(strip=True)):
next_p.decompose()
return str(soup)
def delete_files_after_delay(input_path, output_path, delay_seconds=60):
def delete():
try:
if os.path.exists(input_path):
os.remove(input_path)
if os.path.exists(output_path):
os.remove(output_path)
except Exception as e:
app.logger.error(f'ファイル削除に失敗しました: {e}')
timer = threading.Timer(delay_seconds, delete)
timer.start()
@app.route('/', methods=['GET', 'POST'])
def index():
message = ''
if request.method == 'POST':
if 'file' not in request.files:
message = 'ファイルがありません'
return render_template('index.html', message=message)
file = request.files['file']
if file.filename == '':
message = 'ファイル名がありません'
return render_template('index.html', message=message)
input_filename = str(uuid.uuid4()) + '_' + file.filename
input_file_path = os.path.join(UPLOAD_FOLDER, input_filename)
file.save(input_file_path)
try:
modified_html = process_html(input_file_path)
except Exception as e:
message = f'HTML変換中にエラーが発生しました: {e}'
return render_template('index.html', message=message)
output_filename = str(uuid.uuid4()) + '_modified.html'
output_file_path = os.path.join(UPLOAD_FOLDER, output_filename)
try:
with open(output_file_path, 'w', encoding='utf-8') as f:
f.write(modified_html)
except Exception as e:
message = f'ファイル保存中にエラーが発生しました: {e}'
return render_template('index.html', message=message)
if not os.path.exists(output_file_path):
message = '変換後ファイルの保存に失敗しました'
return render_template('index.html', message=message)
# 60秒後にファイルを削除
delete_files_after_delay(input_file_path, output_file_path, delay_seconds=60)
return send_file(output_file_path, as_attachment=True, download_name='modified.html', mimetype='text/html')
return render_template('index.html', message=message)
if __name__ == '__main__':
app.run(debug=True)
requirements.txt
flask
beautifulsoup4
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Notion PDF Converter</title>
</head>
<body>
<h1>Notion PDF Converter</h1>
<form method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="ファイル変換">
</form>
{% if message %}
<p>{{ message }}</p>
{% endif %}
</body>
</html>
static/custom.css
header {
display: none;
}
#top-page h1 {
margin-top: 10rem;
margin-bottom: 10rem;
}
#top-page .column-list {
display:block;
width: 100%;
text-align: center;
}
#top-page .column-list .column {
width: 100% !important;
padding: 0;
text-align: center;
}
#top-page .column-list .column .image {
margin-top: 5rem;
margin-bottom: 5rem;
text-align: center !important;
}
h1 {
page-break-before: always;
}
/* 先頭の見出し1は改ページ無効 */
/*
h1:first-of-type {
page-break-before: auto;
}
*/
hr {
page-break-after: always;
visibility: hidden !important;
}
a, a.visited {
color: #000;
}
.table_of_contents-link {
opacity: 1;
border-bottom: none;
}
.table_of_contents {
margin-top:1.5rem;
}
.table_of_contents-item {
font-size: 1rem;
line-height: 1.5;
}
table {
width: 100%;
table-layout: fixed;
max-width: 100%;
}
tr, th, td {
color: #000 !important;
border-color: #000 !important;
}
th {
font-weight: bold !important;
background: rgb(247, 246, 243);
text-align: center;
}
.code:has(.mermaid) {
padding: 0 !important;
}
ul li details td {
text-align: right;
}
.toggle > li > details {
padding-left: 0;
}
.toggle > li > details > summary {
margin-left: 0;
}
details > summary {
list-style: none;
}
details > summary::-webkit-details-marker {
display: none;
}
@media print {
@page {
size: portrait;
}
@page rotated {
size: landscape;
}
body {
margin: 0;
padding: 0;
}
details:not(ul li details) {
page: rotated;
}
td, th {
word-wrap: break-word;
overflow-wrap: break-word;
}
.mermaid > svg {
max-width: 100% !important; /* ページの幅に合わせて最大幅を100%にする */
max-height: 100vh !important; /* ページの高さに合わせて最大高さを100%にする */
width: auto !important; /* アスペクト比を保ちつつ幅を自動調整 */
height: auto !important; /* アスペクト比を保ちつつ高さを自動調整 */
page-break-inside: avoid !important; /* 図の途中で改ページされるのを防ぐ */
}
#top-page .column-list {
position: absolute;
bottom: 0;
}
}
ディレクトリ作成とファイルをアップロードする
次の構造になるようにディレクトリを作成し、先程のファイルをアップロードします。
/home/ユーザー名/
└── notionconv/ # プロジェクトルート
├── app.py # Flaskアプリ本体(エントリーポイント)
├── requirements.txt # 必要なPythonパッケージリスト
├── uploads/ # アップロード・変換ファイル保存用ディレクトリ
├── static/ # CSSやJSなどの静的ファイル
│ └── custom.css # カスタムCSSファイル
└── templates/ # HTMLテンプレート
└── index.html # メインページ用テンプレート
仮想環境を作ってパッケージをインストールする
「Consoles」タブからBashコンソールを開き、以下のコマンドを入力します。
cd notionconv
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Webアプリを作成・設定する
- 「Web」タブで「Add a new web app」をクリック。
- 「Manual configuration」を選び、Pythonのバージョン(今回は3.13)を選択。
- 「Source code」に
/home/ユーザー名/
notionconvを指定。
WSGI設定ファイルを編集する
- 「Web」タブの「WSGI configuration file」リンクをクリック。
- 下記のように編集します。
import sys
path = '/home/ユーザー名/notionconv'
if path not in sys.path:
sys.path.append(path)
from app import app as application
アプリを起動・再読み込みする
- 「Web」タブの「Reload」ボタンを押してアプリを再起動します。
https://ユーザー名.pythonanywhere.com/
にアクセスして、アプリが動いているか確認します。
正常に起動していれば、下図のような画面が表示されます。

「ファイルを選択」ボタンで、NotionからエクスポートしたHTMLを選択したあと、「ファイル変換」ボタンをクリックすると、変換後HTMLのダウンロード画面になります。
運用上の注意点
限定公開
「Web」タブのSecurityブロックに、Password protection の設定があります。
有効にするとベーシック認証が利用でき、アプリ利用時にユーザー名とパスワードを要求されます。
無料利用における3ヶ月ごとの更新
無料で利用を継続するためには、3ヶ月毎に「Web」タブの「Run until 3 months from tody」をクリックする必要があります。

FAQ
-
これは何をする機能ですか?
-
Notionで作成したドキュメント(HTML形式)を、クラウド上で自動的に整形してHTMLに変換し、ダウンロードできる機能です。最終的にPDF化したい場合は、ダウンロードしたHTMLをブラウザで印刷してPDFとして保存できます。
-
利用するのに必要なものは何ですか?
-
PythonAnywhereの無料アカウントと、NotionからエクスポートしたHTMLファイルが必要です。ローカルPCにはPython環境は不要です。
-
どうやってアプリを公開するのですか?
-
PythonAnywhereでアカウントを作成し、Flaskアプリのファイルをアップロードして設定するだけで公開できます。サーバーやネットワークの知識は不要です。
-
仮想環境の作成やパッケージのインストールは必要ですか?
-
はい、PythonAnywhereのBashコンソールで仮想環境を作成し、
requirements.txt
に記載されたパッケージをインストールする必要があります。
-
アプリのセキュリティはどうなっていますか?
-
PythonAnywhereの「Web」タブでパスワード保護(ベーシック認証)を設定できます。これにより、アプリ利用時にユーザー名とパスワードを要求できます。
-
無料プランで利用する場合、注意点はありますか?
-
無料プランでは、3ヶ月ごとに「Run until 3 months from today」をクリックして継続利用を申請する必要があります。
-
アップロードしたファイルはどこに保存されますか?
-
アップロード・変換ファイルは
/home/ユーザー名/notionconv/uploads/
ディレクトリに保存されます。
-
変換後のHTMLをPDFにするにはどうすればいいですか?
-
ダウンロードしたHTMLファイルをブラウザで開き、「印刷」からPDFとして保存してください。