業務で受け取るCSVを毎回手作業で集計するのがつらくなり、ブラウザでファイルを選ぶだけでカテゴリ別の売上をまとめてくれる小さなアプリを作ってみました。使ったのはPythonの軽量WebフレームワークFlaskと、表計算に強いpandasです。
データベースは用意せず、アップロードされたCSVをメモリ上で読み込んでその場で集計し、結果をテーブルと総売上金額として表示します。この記事では、同じ画面でアップロードと結果表示を兼ねる設計から、pandasのgroupbyでの集計、実際にブラウザで動かした結果までを初心者向けに追いかけます。
Flaskの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。FlaskとpandasとJinja2を使ってCSVアップロード集計アプリを作るカリキュラムを始めます。概要紹介。
FlaskとpandasとJinja2の役割と使い方を学ぶCSVアップロード集計アプリの要件を整理する 完成コードと実行結果を確認する 最後に完成した画面を実際に操作して確かめる 具体的にやること。
単一ルートでGETとPOSTを受け付ける 画面上部にCSVのファイル選択フォームを表示する アップロードされたCSVをメモリ上で読み込む 必要な3列がそろっているかを検証する カテゴリごとに売上金額と数量と件数を集計する 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、Flask、pandas、Jinja2 PythonのFlaskとpandasとは。
Flask:Flaskは、少ないコードでルーティングやフォーム送信の受け取りを実現できる、Pythonの軽量なWebフレームワークpandas:pandasは、表形式のデータを読み込んで集計や並べ替えを簡潔に書ける、Pythonのデータ分析ライブラリ Flaskとpandasで作るCSV集計アプリの要点。
decodeでutf-8-sigのバイト列を文字列にする 必要な列が無ければValueErrorで知らせるiterrowsで各行を辞書に組み立てる CSVアップロード集計アプリの要件定義。
正しいCSVでカテゴリ別テーブルが表示される 集計結果が売上金額の降順で並ぶ 総売上金額が数値で表示される 必要な列が無いCSVでエラー文言が表示される ファイル未選択で集計するとエラー文言が表示される INTRO: Monaco EditorでCSVアップロード集計アプリを実装。コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。
LINE 001: ioモジュールの読み込み。バイナリデータをテキストのように扱うためのioモジュールを読み込みます。後でCSVのバイト列をファイルのように扱うために使います。
LINE 003: jinja2の読み込み。HTMLテンプレートを描画するためのjinja2ライブラリを読み込みます。画面のHTMLを組み立てる際に利用します。
LINE 004: pandasの読み込み。表形式データを扱うためのpandasライブラリをpdという名前で読み込みます。CSVの読み込みや集計処理に使います。
LINE 005: Flask関連の読み込み。Webアプリを作るためのFlaskクラスと、リクエスト情報を扱うrequestオブジェクトを読み込みます。画面の表示やファイル受け取りに使います。
LINE 008: Flaskアプリの生成。Flaskクラスからアプリ本体を作成し、appという変数に格納します。この後の画面定義や起動処理はこのappを使って行います。
LINE 009: Jinja2環境の作成。HTMLを描画するためのJinja2環境を作成し、autoescapeを有効にしてHTMLの自動エスケープを行います。これにより表示内容に含まれる特殊文字が安全に扱われます。
LINE 012: 確認対象の画面一覧。動作確認したい画面のパスをリストとして定義しています。ここではトップページのみを対象としています。
RUN 1/5: 初期設定した画面一覧を確認する。アプリとJinja2環境を初期化し、動作確認したい画面の一覧を用意できたことを確認します。CHECK 1/5: 途中実行に成功。
確認する画面: ['/'] autoescape: True RETURN 01: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 015: 画面テンプレートの開始。
画面全体のHTMLをJinja2テンプレートとして文字列で定義し始めています。PAGE_TEMPLATEという変数にこのあとのHTMLがすべて格納されます。LINE 016: HTML文書型の宣言。
HTML5の文書であることを示すdoctype宣言です。ブラウザが正しくHTMLを解釈するために必要な記述です。LINE 017: html要素と言語指定。
HTML文書全体を囲むhtml要素を開き、lang属性で日本語のページであることを指定しています。LINE 018: head要素の開始。ページのメタ情報を記述するhead要素を開始しています。
タイトルやスタイルなど画面本体には表示されない情報をここに書きます。LINE 019: 文字コードの指定。ページの文字コードをUTF-8に指定するメタタグです。
日本語が文字化けせずに表示されるようにするための設定です。LINE 020: ページタイトルの設定。ブラウザのタブに表示されるページタイトルを設定しています。
ここでは「CSVアップロード集計アプリ」という文字列を指定しています。LINE 021: スタイル定義の開始。ページの見た目を整えるためのCSSを記述するstyle要素を開始しています。
この後に各要素のデザインが定義されます。LINE 022: body全体のスタイル。ページ全体のフォントや余白、文字色を指定しています。
読みやすい表示になるよう基本的な見た目を整えています。LINE 023: table要素のスタイル。集計結果テーブルの罫線を隙間なく結合し、上部に余白を設ける指定をしています。
テーブルの見た目を整えるための設定です。LINE 024: th・tdのスタイル。テーブルの見出しとデータセルに罫線と余白を付け、文字を右寄せにする指定をしています。
金額や数量が見やすく揃うようにしています。LINE 025: 先頭列の左寄せ指定。各行の最初の列だけを左寄せにするスタイルです。
カテゴリ名の列だけ他の数値列と表示を分けています。LINE 027: ヒント文のスタイル。アップロード方法を案内する補足文の色とフォントサイズを控えめに指定しています。
目立ちすぎない案内表示にするための設定です。LINE 028: スタイル定義の終了。style要素を閉じ、CSSの定義を終了しています。
この後は本文のHTML構造が続きます。LINE 029: head要素の終了。ページのメタ情報を記述するhead要素を閉じています。
この後body要素で画面本体の内容が始まります。LINE 030: body要素の開始。実際に画面に表示される本体部分であるbody要素を開始しています。
ここから見出しやフォームなどが記述されます。LINE 031: 見出しの表示。ページの一番上に表示される見出しとして「CSVアップロード集計アプリ」という文字列を表示しています。
LINE 032: アップロード案内文。アップロードするCSVに必要な列を案内する説明文を表示しています。利用者が迷わずファイルを準備できるようにしています。
LINE 033: アップロードフォームの開始。CSVファイルをサーバーへ送信するためのフォームを開始しています。送信先はトップページ自身で、送信方法はPOST、ファイル送信用の形式を指定しています。
LINE 034: ファイル選択入力欄。CSVファイルを選択するためのファイル入力欄です。拡張子.csvのファイルのみを選びやすくする指定をしています。
LINE 035: 集計実行ボタン。フォームの内容を送信して集計を実行するためのボタンです。押すとサーバーへCSVファイルが送信されます。
LINE 036: フォームの終了。アップロードフォームの記述を閉じています。ここまでがファイル選択と送信ボタンの範囲です。
LINE 037: エラー表示の条件開始。errorという変数に値が入っている場合だけ、この後のエラーメッセージ表示部分を描画する条件分岐を開始しています。LINE 038: エラーメッセージの表示。
errorの内容をそのままページ上に表示する段落です。CSVの読み込みに失敗した際などにここへメッセージが表示されます。LINE 039: エラー表示の条件終了。
errorがある場合の表示ブロックを閉じています。ここでエラー表示に関する条件分岐が終わります。LINE 040: 集計結果表示の条件開始。
rowsという変数に集計結果が入っている場合だけ、この後の集計結果テーブルを表示する条件分岐を開始しています。LINE 041: 集計結果表示の説明コメント。ここから集計結果のテーブルと総売上金額を描画する範囲であることを示すHTMLコメントです。
画面には表示されず、構造の目印として書かれています。LINE 042: 集計結果の見出し。集計結果テーブルの上に表示される小見出しです。
「集計結果」という文字列でこの後の内容がなにかを示しています。LINE 043: 結果テーブルの開始。集計結果を表形式で表示するためのtable要素を開始しています。
このテーブルの中に見出し行とデータ行が入ります。LINE 044: テーブルヘッダーの開始。テーブルの見出し部分であるthead要素を開始しています。
ここに各列のタイトルが入ります。LINE 045: 見出し行の内容。カテゴリ、売上金額合計、数量合計、件数という各列の見出しを1行で定義しています。
この見出しにより各列の意味が分かるようにしています。LINE 046: テーブルヘッダーの終了。見出し部分であるthead要素を閉じています。
この後にデータ行を並べるtbodyが続きます。LINE 047: テーブル本体の開始。集計結果の各行データを並べるtbody要素を開始しています。
この中でカテゴリごとの結果行が繰り返し描画されます。LINE 048: 行の繰り返し処理開始。rowsに含まれる各集計結果を1件ずつrという変数に取り出しながら繰り返す処理を開始しています。
この繰り返しにより全カテゴリ分の行が生成されます。LINE 049: テーブル行の開始。1件分の集計結果を表示するtr要素を開始しています。
この行の中にカテゴリ名や金額などのセルが並びます。LINE 050: カテゴリ名のセル。集計結果1件分のカテゴリ名を表示するセルです。
r.categoryの値がそのままテーブルに表示されます。LINE 051: 売上金額合計のセル。そのカテゴリの売上金額合計を表示するセルです。
r.amountの値が表示されます。LINE 052: 数量合計のセル。そのカテゴリの数量合計を表示するセルです。
r.quantityの値が表示されます。LINE 053: 件数のセル。そのカテゴリに該当したデータの件数を表示するセルです。
r.countの値が表示されます。LINE 054: テーブル行の終了。1件分の集計結果を表示するtr要素を閉じています。
次の繰り返しでまた新しい行が作られます。LINE 055: 行の繰り返し処理終了。rowsを1件ずつ処理する繰り返しの終わりを示しています。
ここまでで全カテゴリ分の行が生成されたことになります。LINE 056: テーブル本体の終了。データ行をまとめていたtbody要素を閉じています。
全カテゴリの結果行がこの中に収められています。LINE 057: 結果テーブルの終了。集計結果を表示していたtable要素を閉じています。
ここまでがテーブル部分の全体です。LINE 058: 総売上金額の表示。すべてのカテゴリを合計した総売上金額を表示する段落です。
total_amountの値がそのまま表示されます。LINE 059: 集計結果表示の条件終了。rowsがある場合に集計結果を表示する条件分岐を閉じています。
ここで集計結果部分の記述が終わります。LINE 060: body要素の終了。画面本体を囲んでいたbody要素を閉じています。
ここまでが画面に表示される内容の範囲です。LINE 061: html要素の終了。HTML文書全体を囲んでいたhtml要素を閉じています。
文書構造としてはここで完結します。LINE 062: テンプレート文字列の終了。PAGE_TEMPLATEとして定義していたHTML文字列の記述を終えています。
この変数がこの後の画面描画に使われます。RUN 2/5: 土台となる画面テンプレートを描画する。画面テンプレートを定義し、結果が無い状態でもアップロード欄を含むHTMLを描画できることを確認します。
CHECK 2/5: 途中実行に成功。HTML文字数: 799 アップロード欄: True RETURN 02: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 065: 必須列の定義。CSVに必ず含まれている必要がある列名をリストとして定義しています。集計処理の前にこの列が揃っているかを確認するために使われます。
RUN 3/5: 集計に必要な列を確認する。集計に必要な列の定義を追加し、カテゴリと売上金額と数量の3列がそろっていることを確認します。CHECK 3/5: 途中実行に成功。
必要な列: ['カテゴリ', '売上金額', '数量'] 列数: 3 RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 069: 集計関数の定義。
アップロードされたCSVのバイト列を受け取り、集計結果を作成する関数aggregate_csvを定義しています。この関数の中で読み込みから集計までの処理がまとめて行われます。LINE 070: バイト列の文字列変換。
受け取ったCSVのバイト列をutf-8-sig形式でデコードし、文字列に変換しています。BOM付きのExcel由来のCSVでも正しく文字列化できるようにしています。LINE 071: CSVの読み込み。
文字列化したCSVデータをio.StringIOでファイルのように扱い、pandasのread_csvで表形式のデータに変換しています。この結果がdfという変数に格納されます。LINE 073: 必須列の不足チェック作成。
必須列のうちdfの列に含まれていないものだけを抽出し、missingというリストに格納しています。この後の判定に使われます。LINE 074: 不足列の有無判定。
missingリストに要素がある、つまり必要な列が不足している場合の処理へ分岐する条件です。LINE 075: 不足列エラーの送出。不足している列名を一覧にしたメッセージを組み立て、ValueErrorとして例外を発生させています。
この例外は呼び出し側でエラーメッセージとして扱われます。LINE 078: カテゴリ別のグループ化開始。dfをカテゴリ列でグループ化し、この後の集計処理の準備を行っています。
as_index=Falseによりカテゴリが通常の列として結果に残ります。LINE 079: 売上金額の合計集計。各カテゴリごとの売上金額列を合計し、amountという名前の列として集計結果に追加しています。
LINE 080: 数量の合計集計。各カテゴリごとの数量列を合計し、quantityという名前の列として集計結果に追加しています。LINE 081: 件数の集計列を定義。
売上金額の列を使って各カテゴリの行数を数え、countという集計列として作成しています。売上金額そのものではなく件数をカウントするためにsizeを使っている点がポイントです。LINE 082: agg処理の終了。
groupbyとaggによるカテゴリ別集計処理を閉じる括弧です。amount・quantity・countの3つの集計結果がまとまったデータフレームがここまでで作られます。LINE 083: 売上金額順に並び替え。
集計結果のデータフレームを売上金額合計が多い順に並び替えています。降順にすることで、画面の表で売上の大きいカテゴリから見やすく表示できます。LINE 085: 表示用データの作成開始。
テンプレートに渡すための行データを作るリスト内包表記を開始しています。ここから集計結果を1件ずつ辞書形式に変換していきます。LINE 086: 1行分の辞書の開始。
テーブルの1行に対応する辞書を作り始めています。この中にカテゴリ名や集計値をキーごとに格納していきます。LINE 087: カテゴリ名を格納。
データフレームの行からカテゴリ名を取り出し、categoryというキーで辞書に格納しています。この値がテーブルの1列目に表示されます。LINE 088: 売上金額合計を整数化。
集計された売上金額合計をint型に変換してamountキーに格納しています。整数にすることで画面表示時に余計な小数点が出ないようにしています。LINE 089: 数量合計を整数化。
集計された数量合計をint型に変換してquantityキーに格納しています。数量は個数を表すため整数として扱うのが自然です。LINE 090: 件数を整数化。
そのカテゴリに含まれる行数をint型に変換してcountキーに格納しています。この値が集計結果テーブルの件数列に表示されます。LINE 091: 辞書の終了。
1行分のカテゴリ・売上金額・数量・件数をまとめた辞書の定義を閉じています。この辞書がテーブルの1行分のデータになります。LINE 092: 行の繰り返し元指定。
sort_values済みのgrouped各行を順番に取り出し、行データrとして辞書作成に使っています。1件目から順に表示用の行を作っていく流れです。LINE 093: リスト内包表記の終了。
表示用の行データを作るリスト内包表記を閉じています。これでrowsという変数にテーブル表示用の全カテゴリ分のデータが格納されます。LINE 094: 総売上金額の計算。
全カテゴリの売上金額合計をさらに合計し、int型に変換してtotal_amountとして求めています。この値が画面下部の総売上金額表示に使われます。LINE 095: 集計結果を呼び出し元へ返却。
作成したrowsとtotal_amountをまとめて関数の戻り値として返しています。呼び出し側ではこの2つの値を使って画面を描画します。RUN 4/5: サンプルCSVの集計結果を確認する。
CSVを読み込んでカテゴリ別に集計する関数を追加し、サンプルデータの集計結果を確認します。CHECK 4/5: 途中実行に成功。カテゴリ数: 2 先頭カテゴリ: 食品 総売上金額: 2300 RETURN 04: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 099: ルートのHTTPメソッド設定。トップページに対してGETとPOSTの両方のリクエストを受け付けるルーティングを設定しています。
画面表示とCSVアップロードの両方をこの1つの関数で処理します。LINE 100: index関数の定義開始。トップページのリクエストを処理するindex関数を定義しています。
この関数の中でCSVの受け取りから画面描画までの一連の処理を行います。LINE 101: エラーメッセージの初期化。画面に表示するエラーメッセージを保持する変数errorを、まずは何もない状態のNoneで初期化しています。
エラーが発生した場合のみ後で値が入ります。LINE 102: 集計結果行の初期化。テーブル表示用の行データを保持する変数rowsをNoneで初期化しています。
CSVがまだアップロードされていない状態では集計結果は存在しません。LINE 103: 総売上金額の初期化。総売上金額を保持する変数total_amountをNoneで初期化しています。
集計が行われるまではこの値も未設定の状態にしておきます。LINE 105: POSTリクエストかどうかの判定。リクエストのメソッドがPOSTかどうかを確認しています。
CSVファイルがアップロードされたときの処理はこの条件の中でのみ実行されます。LINE 106: アップロードファイルの取得。フォームから送信されたファイルをcsv_fileという名前で取得しています。
ファイルが送信されていない場合はこの値がNoneになります。LINE 107: ファイル未選択の判定。ファイルが送信されていないか、ファイル名が空になっていないかを確認しています。
CSVが選択されていない状態を検出するための条件です。LINE 108: 未選択時のエラー設定。ファイルが選択されていない場合に、その旨を伝えるエラーメッセージをerrorに設定しています。
この値が画面上部にエラー表示として表示されます。LINE 109: ファイルがある場合の分岐。ファイルが正しく選択されている場合に進む処理の分岐です。
ここから実際にCSVの読み込みと集計を試みる処理が始まります。LINE 110: 例外処理の開始。CSVの読み込みや集計処理中に問題が起きる可能性があるため、try文で例外の発生に備えています。
想定外のCSVが送られてもアプリが落ちないようにするためです。LINE 111: CSVの集計処理を実行。アップロードされたファイルの内容を読み込み、aggregate_csv関数に渡して集計を行っています。
戻り値としてrowsとtotal_amountが受け取られ、画面表示に使われます。LINE 112: 列不足エラーの捕捉。CSVに必要な列が足りない場合に発生するValueErrorをここで捕まえています。
想定内のエラーとして専用の処理につなげています。LINE 113: エラーメッセージの設定。捕まえた例外の内容を文字列に変換し、errorに設定しています。
これにより不足している列名などが画面にそのまま表示されます。LINE 114: 想定外エラーの捕捉。列不足以外の予期しないエラー全般をここで捕まえています。
CSVの形式が壊れているなど様々な問題に幅広く対応するための処理です。LINE 115: 汎用エラーメッセージの設定。想定外のエラーが起きた場合に、詳細を出さずに分かりやすい共通のエラーメッセージをerrorに設定しています。
利用者に書式の見直しを促す内容になっています。LINE 118: テンプレートの生成。PAGE_TEMPLATEの文字列からJinja2のテンプレートオブジェクトを作成しています。
この後この変数を使って実際のHTMLを描画します。LINE 119: 画面の描画と返却。テンプレートにerror・rows・total_amountの値を渡して描画し、その結果のHTMLを関数の戻り値として返しています。
この処理でブラウザに実際に表示されるページが完成します。RUN 5/5: トップ画面のGET表示を確認する。ルーティングを追加し、トップ画面へアクセスするとアップロードフォームを含む画面が返ることを確認します。
CHECK 5/5: 途中実行に成功。ステータス: 200 フォームあり: True RETURN 05: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 123: 直接実行時の判定。このファイルが直接実行されたときにだけ真になる条件です。他のファイルからインポートされた場合にはこの中身は実行されません。
LINE 124: 開発用サーバーの起動。Flaskの開発用サーバーをデバッグモードで起動しています。手元で動作確認する際にエラー内容が詳しく表示されるようになります。
PREVIEW: 完成したCSVアップロード集計アプリを操作。入力・送信・結果表示を実際のブラウザ画面で確認します。UPLOAD: 実際の入力データでアプリを操作。
CSVをアップロードして集計すると、カテゴリ別の売上テーブルと総売上金額が表示されるのを確認できた 確認1/4: サンプルCSVをアップロードして集計する。カテゴリと売上金額と数量を含むCSVをアップロードし、集計結果のテーブルと総売上金額が表示されることを確認します。確認2/4: 集計結果が売上金額の降順で並ぶことを確認する。
売上金額が大きいカテゴリから順に行が並ぶことを、表示されたカテゴリの順序で確認します。確認3/4: 必要な列がないCSVでエラーを確認する。必要な列を含まないCSVを送信し、不足している列名を含むエラー文言が表示されることを確認します。
確認4/4: ファイルを選ばずに集計するとエラーを確認する。ファイルを選択せずに送信し、ファイル選択を促すエラー文言が表示されることを確認します。学習内容のまとめ。
GETアクセスで空のアップロード画面を返すread_csvでCSVをDataFrameに変換するdecodeでutf-8-sigのバイト列を文字列にする データベースを使わずメモリ上で集計する 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。
PythonのFlaskとpandasとは
今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。
リクエストを受け取り画面を返すFlask
Flaskは、少ないコードでルーティングやフォーム送信の受け取りを実現できる、Pythonの軽量なWebフレームワークです。今回のアプリでは同じ『/』のアドレスでCSVのアップロード受付と集計結果の表示を兼ねており、画面へのアクセスとフォーム送信の両方をこのFlaskがさばきます。
データベースを持たない小さな構成でも、ブラウザとやり取りする土台としてしっかり働いてくれます。
Flaskがこのアプリで受け持つ役割を具体的に挙げます
- GETアクセスで空のアップロード画面を返す
- POST送信で選択されたCSVを受け取る
- methodsにGETとPOSTを両方登録する
- request.filesからファイルを取り出す
- 起動時はapp.run(debug=True)で確認する
CSVを読み集計するpandasの役割
pandasは、表形式のデータを読み込んで集計や並べ替えを簡潔に書ける、Pythonのデータ分析ライブラリです。このアプリではアップロードされたCSVをDataFrameとして受け取り、groupbyでカテゴリごとに数値をまとめる中心的な計算を担います。for文で1行ずつ足し合わせる書き方に比べ、短いコードで正確な集計を実現できる点が持ち味です。
pandasがCSV集計で使う具体的な機能を挙げます
- read_csvでCSVをDataFrameに変換する
- groupbyでカテゴリ列ごとにまとめる
- aggで合計と件数を同時に求める
- sort_valuesで売上金額を降順に並べる
- sizeで各カテゴリの行数を数える
Python・Flaskで開発する場合の環境構築
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install Flask pandas Jinja2
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install Flask pandas Jinja2
- CSVは「カテゴリ」「売上金額」「数量」列が必須で、欠けるとエラー表示になります。
- ExcelからのBOM付きCSVも読めるようutf-8-sigでデコードしています。
- Jinja2はFlaskの依存として入りますが、役割を明示するため個別に導入しています。
CSVアップロード集計アプリの要件定義
目的は、ブラウザでCSVを選ぶだけでカテゴリ別に売上金額と数量を集計し、総売上金額まで画面で確認できるWebアプリをFlaskとpandasで作れるようになることです。
対象者として、Pythonの基本文法を学び終え、FlaskとpandasでCSVを扱うWebアプリの作り方を学びたい人を想定しています。
完成物は、FlaskとpandasとJinja2で作り、アップロードしたCSVをカテゴリ別に集計して総売上金額まで表示するWebアプリです。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- 単一ルートでGETとPOSTを受け付ける
- 画面上部にCSVのファイル選択フォームを表示する
- アップロードされたCSVをメモリ上で読み込む
- 必要な3列がそろっているかを検証する
- カテゴリごとに売上金額と数量と件数を集計する
- 集計結果を売上金額の降順で並べ替える
- カテゴリ別の集計結果をテーブルで表示する
- 総売上金額を整数で表示する
- ファイル未選択や列不足のときにエラー文言を表示する
非機能要件
- データベースを使わずメモリ上で集計する
- Jinja2のautoescapeを有効にして描画する
- CSVをutf-8-sigで文字列に変換する
- 必要な列をカテゴリ・売上金額・数量に固定する
- 例外の種類ごとにエラー文言を出し分ける
- ローカル起動はdebugモードで行う
実装方針
今回はFlaskとpandasとJinja2の基本動作を追いやすくするため、CSVアップロード集計アプリ本体を1つのPythonファイルへまとめます。
入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。
CSVアップロード集計アプリを安全に組み立てるための実装方針は次のとおりです。
- データベースを使わずメモリ上で集計する
- Jinja2のautoescapeを有効にして描画する
- CSVをutf-8-sigで文字列に変換する
- 必要な列をカテゴリ・売上金額・数量に固定する
- 例外の種類ごとにエラー文言を出し分ける
- ローカル起動はdebugモードで行う
完成と判断する条件
- 正しいCSVでカテゴリ別テーブルが表示される
- 集計結果が売上金額の降順で並ぶ
- 総売上金額が数値で表示される
- 必要な列が無いCSVでエラー文言が表示される
- ファイル未選択で集計するとエラー文言が表示される
FlaskとpandasでCSV集計アプリを作る際の重要ポイント
このアプリの中核は、受け取ったCSVのバイト列を文字列へ直し、pandasで集計した結果をJinja2テンプレートへ渡して同じ画面に描き出す一連の流れです。集計に必要な列がそろっているかを先に確かめ、問題があればエラー文言だけを、成功すればテーブルと総売上金額を映し出します。
アップロード欄と結果表示を1枚のHTMLにまとめることで、利用者は画面を移動せずに結果を読み取れます。
集計結果の表示までにコードが行う判定や変換を挙げます
- decodeでutf-8-sigのバイト列を文字列にする
- 必要な列が無ければValueErrorで知らせる
- iterrowsで各行を辞書に組み立てる
- int変換で数値を整数にそろえる
- autoescape付きJinja2でHTMLに描画する
必要な列の有無を先に検証する理由
集計を始める前に、『カテゴリ』『売上金額』『数量』の列がそろっているかを確認します。足りない列があれば、その名前を並べたメッセージとともにValueErrorを送出する仕組みです。想定と違うCSVをそのまま集計すると分かりにくい失敗になるため、早い段階で原因を伝える設計にしています。
列の検証で利用者に伝わる情報を挙げます
- 不足している列名を具体的に示す
- 読み込み失敗時は書式確認を促す
- 想定外のCSVでも画面が壊れない
Jinja2テンプレートに集計結果を渡す流れ
描画には、autoescapeを有効にしたJinja2の環境を使います。from_stringでHTMLテンプレートを読み込み、renderにエラー文言・集計行・総売上金額を渡すと、条件に応じてテーブルやメッセージが差し込まれる仕組みです。autoescapeが働くことで、CSV由来の文字がそのままHTMLとして解釈される危険を抑えられます。
renderへ渡す値と画面表示の対応を挙げます
- errorがあればエラー文言を表示する
- rowsがあればカテゴリ別テーブルを表示する
- total_amountで総売上金額を表示する
同じ画面で入力と結果を見せる利点
アップロード欄と集計結果を1枚のHTMLにまとめているため、送信後もページ構成は変わらず、上部のフォームを残したまま下に結果が加わります。画面を移動しないので、続けて別のCSVを試したいときもそのまま操作を進められます。データベースを持たないぶん、結果は都度計算して表示する構成です。
1画面構成がもたらす操作上の利点を挙げます
- 送信後もフォームが残る
- 別のCSVを続けて試せる
- 結果は毎回その場で計算する
PythonでCSVアップロード集計アプリの完成コード
全体は大きく3つに分かれます。Flaskアプリと画面テンプレートの用意、CSVを集計するaggregate_csv関数、リクエストを受けるルーティングです。
アップロードされたファイルはバイト列で届くので、まず文字コードを指定して文字列へ直し、pandasのread_csvでDataFrameにします。あとはgroupbyでカテゴリごとにまとめるだけです。
集計結果はそのままでは画面に出せないので、テンプレートが受け取りやすい辞書のリストへ詰め替えます。このrowsと総売上金額をJinja2へ渡すと、テーブルが描画されます。
ここまでの実装を安全かつ確実に動かすうえで押さえておきたいポイントは、次の3点です。
このセクションの用語
- pandas
- 表形式のデータを効率よく扱えるPythonのライブラリです。CSVの読み込みや集計を短いコードで書けます。
- DataFrame
-
pandasで表データを表す中心的な入れ物です。行と列を持ち、Excelの表のように操作できます。 - groupby
- 指定した列の値ごとに行をグループ分けし、合計や件数などをまとめて計算する
pandasの機能です。 - autoescape
- テンプレートに渡した文字列の記号を、自動で安全な形に置き換える仕組みです。表示崩れや不正なタグの混入を防ぎます。
import io
import jinja2
import pandas as pd
from flask import Flask, request
# アプリケーションとJinja2環境の初期化
app = Flask(__name__)
jinja_env = jinja2.Environment(autoescape=True)
# 動作確認したい画面
ROUTES = ["/"]
# 画面テンプレート(Jinja2で描画するHTML)
PAGE_TEMPLATE = """
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>CSVアップロード集計アプリ</title>
<style>
body { font-family: sans-serif; margin: 2rem; color: #222; }
table { border-collapse: collapse; margin-top: 1rem; }
th, td { border: 1px solid #999; padding: 6px 12px; text-align: right; }
th:first-child, td:first-child { text-align: left; }
#error-message { color: #c0392b; }
.hint { color: #666; font-size: 0.9rem; }
</style>
</head>
<body>
<h1>CSVアップロード集計アプリ</h1>
<p class="hint">カテゴリ・売上金額・数量の列を含むCSVをアップロードしてください。</p>
<form id="upload-form" action="/" method="post" enctype="multipart/form-data">
<input id="csv-file" type="file" name="csv_file" accept=".csv">
<button id="aggregate-btn" type="submit">集計する</button>
</form>
{% if error %}
<p id="error-message">{{ error }}</p>
{% endif %}
{% if rows %}
<!-- ここから集計結果表示: カテゴリ別テーブルと総売上金額をまとめて描画する -->
<h2>集計結果</h2>
<table id="result-table">
<thead>
<tr><th>カテゴリ</th><th>売上金額合計</th><th>数量合計</th><th>件数</th></tr>
</thead>
<tbody>
{% for r in rows %}
<tr>
<td>{{ r.category }}</td>
<td>{{ r.amount }}</td>
<td>{{ r.quantity }}</td>
<td>{{ r.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p id="total-amount">総売上金額: {{ total_amount }}</p>
{% endif %}
</body>
</html>
"""
# 集計に必要な列の定義
REQUIRED_COLUMNS = ["カテゴリ", "売上金額", "数量"]
# CSVの読み込みとカテゴリ別集計(集計結果表示に渡す値をここで作る)
def aggregate_csv(raw_bytes):
text = raw_bytes.decode("utf-8-sig")
df = pd.read_csv(io.StringIO(text))
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
raise ValueError("必要な列がありません: " + "、".join(missing))
# カテゴリごとに売上金額・数量・件数を集計する
grouped = df.groupby("カテゴリ", as_index=False).agg(
amount=("売上金額", "sum"),
quantity=("数量", "sum"),
count=("売上金額", "size"),
)
grouped = grouped.sort_values("amount", ascending=False)
rows = [
{
"category": r["カテゴリ"],
"amount": int(r["amount"]),
"quantity": int(r["quantity"]),
"count": int(r["count"]),
}
for _, r in grouped.iterrows()
]
total_amount = int(grouped["amount"].sum())
return rows, total_amount
# ルーティング(アップロード受け取りと集計結果表示)
@app.route("/", methods=["GET", "POST"])
def index():
error = None
rows = None
total_amount = None
if request.method == "POST":
file = request.files.get("csv_file")
if file is None or file.filename == "":
error = "CSVファイルを選択してください。"
else:
try:
rows, total_amount = aggregate_csv(file.read())
except ValueError as exc:
error = str(exc)
except Exception:
error = "CSVの読み込みに失敗しました。書式を確認してください。"
# ここで集計結果表示を行う: Jinja2テンプレートに集計結果を渡して描画する
template = jinja_env.from_string(PAGE_TEMPLATE)
return template.render(error=error, rows=rows, total_amount=total_amount)
# 手元で試すときのローカル起動
if __name__ == "__main__":
app.run(debug=True)
コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
Flaskアプリとautoescapeの初期化
app = Flask(__name__)
jinja_env = jinja2.Environment(autoescape=True)Flask(__name__)でアプリ本体を作ります。autoescape=Trueにすると、テンプレートへ渡した値の記号が自動でエスケープされ、表示崩れや不正なタグの混入を防げます。
同一ルートでGETとPOSTを受ける
@app.route("/", methods=["GET", "POST"])/という1つのURLに、GETとPOSTの両方を割り当てています。GETのときは入力画面、POSTのときはアップロードされたファイルの受け取りを担当させる狙いです。
utf-8-sigでCSVを読み込む
text = raw_bytes.decode("utf-8-sig")
df = pd.read_csv(io.StringIO(text))届いたバイト列をutf-8-sigで文字列に直しています。Excel由来のCSVに付きやすいBOMをうまく無視でき、io.StringIOで文字列をファイルのようにread_csvへ渡せます。
必要な列がそろっているか確認する
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
raise ValueError("必要な列がありません: " + "、".join(missing))カテゴリ・売上金額・数量の列がCSVにあるかを調べます。足りない列があればValueErrorを投げ、あとで画面にエラーメッセージとして表示する流れにしています。
groupbyでカテゴリ別に集計する
grouped = df.groupby("カテゴリ", as_index=False).agg(
amount=("売上金額", "sum"),
quantity=("数量", "sum"),
count=("売上金額", "size"),
)groupbyでカテゴリごとに行をまとめ、aggで売上金額の合計・数量の合計・件数を一度に計算します。as_index=Falseにすると、カテゴリ名が普通の列として残り、あとで扱いやすくなります。
iterrowsで辞書のリストに整える
rows = [
{
"category": r["カテゴリ"],
"amount": int(r["amount"]),
"quantity": int(r["quantity"]),
"count": int(r["count"]),
}
for _, r in grouped.iterrows()
]集計結果を1行ずつ取り出し、テンプレートが読みやすいキー名の辞書に詰め替えます。intで数値を整数にそろえておくと、画面での表示が小数点付きにならずに済みます。
総売上金額を計算して返す
total_amount = int(grouped["amount"].sum())
return rows, total_amount集計表の売上金額を合計し、intで整数にして総売上金額を作ります。テンプレートに渡すrowsと合わせて返すと、テーブルと総額の両方を描画できます。
参考:
©Flask公式ドキュメント(Quickstart)By default, a route only answers to GET requests. You can use the methods argument of the route() decorator to handle different HTTP methods.
自動エスケープ:値の記号を無害化
メモリ完結:DBを使わない構成
文字コード:ExcelのBOM対策
CSVアップロード集計アプリの動作確認
実際にアプリを起動し、ブラウザで/を開くとアップロードフォームが表示されました。ファイルを選ぶ前の初期画面はHTTP 200で返り、エラー表示も出ていません。
用意したCSVを選んで「集計する」を押すと、カテゴリ別の売上テーブルと総売上金額が表示されるのを確認できました。売上金額の大きい順に並ぶので、どのカテゴリが売れているか一目で分かります。
データベースを使っていないので、集計はアップロードのたびにその場で行われる仕組みです。別のCSVを選び直せば、新しい内容で結果がすぐ描画されます。
このセクションの用語
- HTTP 200
- リクエストが正常に処理されたことを表すステータスコードです。ページが問題なく返ったことを示します。
画面で確認できた動きは次のとおりです。
- 初期表示ではフォームだけが出て、結果テーブルは表示されない
- 集計後はカテゴリ・売上金額合計・数量合計・件数の4列が並ぶ
- テーブルの下に総売上金額が別行で表示される



PythonのFlaskとpandasとJinja2のエラー対処
CSVアップロードアプリでは、ファイルの中身や列名のズレでつまずくことが多いです。ここでは初心者が一般に遭遇しやすいエラーと、その対処をまとめます。
特に多いのが文字コードと列名の問題です。全角スペースや余分な空白が列名に紛れていると、REQUIRED_COLUMNSのチェックに引っかかります。
このセクションの用語
- エンコーディング
- 文字をコンピュータが扱うバイト列に変換する方式です。
UTF-8やShift_JISなどの種類があります。
| エラー例 | 原因 | 対処 |
|---|---|---|
| 必要な列がありません: 数量 | 列名がカテゴリ・売上金額・数量と一致しない | ヘッダーの表記ゆれや全角空白を直して列名をそろえる |
| UnicodeDecodeError | CSVがShift_JISなどUTF-8以外で保存されている | UTF-8で保存し直すか、decodeの文字コードを合わせる |
| EmptyDataError: No columns to parse | 中身が空、または見出しのないCSVを渡した | 見出しとデータが入ったCSVを選び直す |
| ValueError: could not convert string to float | 売上金額や数量にカンマや通貨記号が混ざる | 金額から¥やカンマを除き、数値だけの列に整える |
| ParserError: Error tokenizing data | 行によって列数が違う崩れたCSV | 区切り文字や引用符を確認し、列数をそろえる |
CSVアップロード集計アプリで注意したい点
CSVアップロードアプリで最初に迷いやすいのが、フォームとサーバー側の受け取り方の対応です。formタグのmethodとルートのmethodsが食い違うと、送信してもうまく処理されません。
ファイルを送るフォームでは、enctypeの指定も欠かせません。multipart/form-dataを付けないと、ファイルがサーバーに届かず空になってしまいます。
文字コードや列名も定番のつまずきどころです。ポイントとしては、アップロード周りの設定と列名の一致を先に固めておくと、集計処理でのエラーをぐっと減らせます。
文字コード:utf-8-sigで開く
列名:全角スペースまで一致
受け取り:enctypeを指定
メソッド:GETとPOST両対応
CSV集計アプリをPythonの実務で応用できる場面
同じ仕組みは、手元のCSVをその場でまとめたい場面に幅広く応用できます。集計の軸や項目を差し替えるだけで、いろいろな帳票づくりに使い回せます。
groupbyでまとめる列を変えたり、aggに平均や最大を足したりすれば、売上以外の指標にも対応できるはずです。
このセクションの用語
- agg
-
groupbyでまとめたグループに対し、合計や平均などの集計を列ごとに指定して計算するメソッドです。
| 使える場面 | 具体的な使い方 |
|---|---|
| 店舗別の売上集計 | カテゴリの代わりに店舗名でgroupbyし、店舗ごとの売上と件数を一覧化する |
| 月次レポートの下ごしらえ | 日付列を月単位にまとめ、月ごとの合計を出して報告書に貼り付ける |
| 在庫・数量のチェック | 数量列だけを集計し、カテゴリ別の在庫数や出荷数をまとめて確認する |
| アンケート結果の集計 | 回答区分でgroupbyし、区分ごとの件数を数えて分布を把握する |
CSVアップロード集計アプリ開発のまとめ
PythonのFlaskとpandasを組み合わせ、CSVをアップロードするとカテゴリ別の売上と総売上金額を表示するアプリを作りました。同じ/でアップロードと結果表示を兼ねる、シンプルな構成です。
ブラウザで実際に操作すると、カテゴリ別の売上テーブルと総売上金額が表示されるのを確認できました。データベースを使わないぶん、手元のCSVを試しにまとめたいときにすぐ動かせます。
まずは列名と文字コードをそろえたCSVを用意し、groupbyのキーやaggの項目を用途に合わせて変えてみてください。集計の幅がぐっと広がります。
この記事の内容から、次に試したい発展のアイデアを挙げます。
- CSVをドラッグ&ドロップで選べるようにしてアップロードを楽にする
- 集計結果をダウンロード用のCSVとして書き出す機能を足す
- グラフ描画ライブラリを足して、カテゴリ別売上を棒グラフで可視化する
参考にした一次情報
- ^ Flask Documentation — The Pallets Projects. https://flask.palletsprojects.com/en/stable/, (参照26-07-29).
- ^ pandas.read_csv — pandas documentation. https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html, (参照26-07-29).
- ^ pandas.DataFrame.groupby — pandas documentation. https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.groupby.html, (参照26-07-29).
- ^ Jinja Documentation — The Pallets Projects. https://jinja.palletsprojects.com/en/stable/, (参照26-07-29).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。
ITやプログラミングに関するコラム
【Python】FastAPIで料金プラン見積もりシミュレーターを作ってみた
【Python】pandasとmatplotlibで在庫データのABC分析と構成比を可視化してみた
【Python】Flaskで社内FAQをカテゴリ検索できるWebアプリを作ってみた
【Python】argparseでJSON整形・構文検証・キー検索CLIを試してみた
【Python】NumPyとmatplotlibでモンテカルロ法による円周率推定と収束過程の可視化を試してみた
【Python】Playwrightでスクレイピングを試してみた
【CSS】notで複数の件を除外する方法
【Git】remote設定を変更する方法
【VBA】コメントアウトを設定する方法
x86とx64の違いを分かりやすく解説
ITやプログラミングに関するニュース
VercelがAI GatewayにSeedream 5.0 Proを追加、AI SDKのモデル指定で画像生成と編集が可能に
AWSがAmazon LocationのPlaces APIを強化、住所表記の指定と移動手段別の検索が可能に
VercelがトレースにTree・Waterfallビューを追加、ログ画面で処理の階層と所要時間を確認可能に
Googleがエージェント評価の再考を提唱、難易度を情報量で測るDiscovery Benchを解説
Google CloudがCloud Runサンドボックスを公開プレビューで提供、サービスヘルスは一般提供に
Google Cloud EMEAが英国金融の重要第三者に指定、イングランド銀行・PRA・FCAの直接監督下に
AWS DMS Schema ConversionがSQL Serverのオフライン変換に対応、ソースDBへ接続せずスキーマを変換可能に
EC2 G7インスタンスが米国東部(バージニア北部)で利用可能に、G6比でAI推論性能が最大4.6倍
SageMaker HyperPodが継続プロビジョニングでのAMIベース構成に対応、S3のスクリプト管理なしでSlurmクラスターを作成可能に
AWSがEMR on EKSでSparkトラブルシューティングエージェントに対応、失敗ジョブの原因分析を自然言語で依頼可能に
