【Python】DuckDBで売上CSVをSQL検索してRichの表に表示するCLIを作ってみた

【Python】DuckDBで売上CSVをSQL検索してRichの表に表示するCLIを作ってみた

公開: 更新:
CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座

PythonのDuckDBで売上CSVをSQL検索してRichの表に表示するCLIを題材に、実際にPythonコードを作って検証しました。実行結果は「CLIツールを5回のコマンドで実行し、全て正常(終了コード0)に動作した」です。

DuckDBの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。

【Python】DuckDBで売上CSVをSQL検索してRichの表に表示するCLIを作ってみた|フルカリキュラム
動画の内容をテキストで確認する

オープニング。DuckDBとRichを使って売上CSV検索CLIを作るカリキュラムを始めます。概要紹介。

DuckDBとRichの役割と使い方を学ぶ 売上CSV検索CLIの要件を整理する 完成コードと実行結果を確認する 最後に実コマンドとファイル状態で完成挙動を確かめる 具体的にやること。

--init-sampleでサンプル売上CSVを120行書き出す サンプル作成後に保存先パスと列名一覧を表示する 必須7列がそろっているかCSVのヘッダーを検証するread_csv_autoでCSVをsalesビューとして登録する --sqlに渡したSELECT文をDuckDBで実行する 実装環境・必須アプリ。

OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、ターミナル、エクスプローラー パッケージ:pip、duckdb、rich DuckDBとRichとは。

DuckDB:DuckDBは、CSVファイルをテーブルのように扱ってSQLで集計できる、Pythonから直接呼び出せる分析用データベースライブラリRich:Richは、色付きの文字や罫線付きの表をターミナルへ出力できる表示整形ライブラリ DuckDBとRichで作る売上検索CLIの要点。

--init-sampleだけでサンプル120行を作成して終了 必須7列がそろわないCSVは検索前に停止 --max-rows 3なら表は3行で内訳メッセージが追加 売上CSV検索CLIの要件定義。

--init-sampleでsales.csvが作成される 作成メッセージに120行 と7列の列名が並ぶGROUP BYの集計結果が罫線付きの表で表示される 表の下に取得行数が表示される --max-rows 3で表示3行と取得120行が併記される 存在しないCSVの指定で赤字エラーと終了コード1になる INTRO: Monaco Editorで売上CSV検索CLIを実装。

コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。LINE 001: モジュールの説明文。このファイル全体が何をするCLIなのかを説明するドキュメント文字列です。

売上CSVをDuckDBのSQLで検索し、結果をRichの表で表示するツールであることを示しています。LINE 003: 型注釈の将来機能を有効化。Pythonの将来バージョンで標準になる型注釈の書き方を先取りして使えるようにする宣言です。

これにより関数の型ヒントを柔軟に書けるようになります。LINE 005: コマンドライン引数解析の準備。コマンドライン引数を扱うためのargparseモジュールを読み込んでいます。

CLIの--csvや--sqlといったオプションを解析する際に使います。LINE 006: CSV操作モジュールの読み込み。CSVファイルの読み書きに使うcsvモジュールを読み込んでいます。

サンプルCSVの書き出しやヘッダー確認で利用します。LINE 007: 乱数生成モジュールの読み込み。ランダムな値を生成するrandomモジュールを読み込んでいます。

サンプルデータの店舗や商品をランダムに選ぶために使います。LINE 008: システム機能モジュールの読み込み。プログラムの終了コードなどを扱うsysモジュールを読み込んでいます。

処理の最後にmain関数の戻り値を終了コードとして渡す際に使います。LINE 009: 日付関連クラスの読み込み。日付を表すdateクラスと日数計算に使うtimedeltaクラスを読み込んでいます。

サンプルの注文日を計算するために利用します。LINE 010: 高精度数値クラスの読み込み。小数を正確に扱うためのDecimalクラスを読み込んでいます。

検索結果の数値表示を整える際の型判定に使います。LINE 011: パス操作クラスの読み込み。ファイルパスを扱いやすくするPathクラスを読み込んでいます。

CSVファイルの存在確認や書き出し先の指定に使います。LINE 013: DuckDBライブラリの読み込み。SQLでデータを検索できるDuckDBライブラリを読み込んでいます。

CSVをビューとして扱い、SELECT文を実行するために使います。LINE 014: 表の枠線スタイルの読み込み。Richライブラリから表の罫線デザインを選べるboxモジュールを読み込んでいます。

表の見た目を整える際に使います。LINE 015: コンソール出力クラスの読み込み。色付きの文字や表をターミナルに表示するためのConsoleクラスを読み込んでいます。

プログラム全体の出力に利用します。LINE 016: 表描画クラスの読み込み。検索結果を見やすい表として描画するためのTableクラスを読み込んでいます。

検索結果の表示部分で使います。LINE 019: 必須列名リストの開始。売上CSVに必ず含まれているべき列名の一覧を定義するリストの開始行です。

この後の行に具体的な列名が続きます。LINE 020: 注文日の列名。必須列の一つとして注文日を表すorder_dateという列名を指定しています。

ヘッダー検証やCSV生成の基準になります。LINE 021: 店舗名の列名。必須列の一つとして店舗を表すstoreという列名を指定しています。

どの店舗の売上かを識別するための列です。LINE 022: カテゴリの列名。必須列の一つとして商品分類を表すcategoryという列名を指定しています。

飲料やスイーツなどの区分を表す列です。LINE 023: 商品名の列名。必須列の一つとして商品名を表すproductという列名を指定しています。

どの商品が売れたかを識別するための列です。LINE 024: 単価の列名。必須列の一つとして商品の単価を表すunit_priceという列名を指定しています。

数値として金額計算に使われる列です。LINE 025: 数量の列名。必須列の一つとして販売数量を表すquantityという列名を指定しています。

数値として合計計算などに使われる列です。LINE 026: 合計金額の列名。必須列の一つとして売上金額を表すamountという列名を指定しています。

単価と数量から計算された最終行の列です。LINE 027: 必須列名リストの終端。REQUIRED_COLUMNSリストの定義を閉じる行です。

ここまでの7つの列名がCSV検証やサンプル生成の基準として使われます。LINE 028: 数値列判定用キーワード。列名に含まれていれば数値列とみなすキーワードのタプルを定義しています。

表の右寄せや色分けを判断する際に参照します。LINE 029: DuckDBビュー名の定義。CSVを取り込んで作成するDuckDBのビュー名をsalesという文字列で定義しています。

SQL検索時にこの名前を使ってテーブルのように扱います。LINE 031: コンソールの初期化。Richのコンソールオブジェクトを作成し、プログラム全体で共有して使えるようにしています。

以降のメッセージや表の出力はすべてこのconsoleを通して行われます。RUN 1/8: 設定値とConsoleの初期化を確認する。必須列の定義とビュー名、そしてRichのConsoleが用意できた状態です。

まだ関数がないため、定数の中身と色付き出力だけを確かめます。CHECK 1/8: 途中実行に成功。必須列数: 7 ビュー名: sales 数値ヒント数: 8 Consoleの準備が完了 RETURN 01: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 035: サンプルCSV生成関数の定義。動作確認用のサンプル売上CSVを作成する関数を定義しています。

行数や乱数シードを引数で指定でき、作成したパスと行数を返します。LINE 036: 乱数生成器の初期化。指定したシード値をもとにrandomの乱数生成器を作成しています。

同じシードを使うことで毎回同じサンプルデータを再現できます。LINE 037: 店舗名リストの定義。サンプルデータで使う店舗名の候補をリストとして定義しています。

この中からランダムに1つが選ばれて各行の店舗として使われます。LINE 038: 商品カタログリストの開始。サンプルデータで使う商品情報のリストを定義する開始行です。

商品名、カテゴリ、単価の組み合わせがこの後に続きます。LINE 039: ドリップコーヒーの商品情報。カタログの1件目として、ドリップコーヒーという商品名、飲料というカテゴリ、480円という単価をタプルで定義しています。

LINE 040: カフェラテの商品情報。カタログの2件目として、カフェラテという商品名、飲料というカテゴリ、520円という単価をタプルで定義しています。LINE 041: チーズケーキの商品情報。

カタログの3件目として、チーズケーキという商品名、スイーツというカテゴリ、620円という単価をタプルで定義しています。LINE 042: シフォンケーキの商品情報。カタログの4件目として、シフォンケーキという商品名、スイーツというカテゴリ、560円という単価をタプルで定義しています。

LINE 043: ブレンド豆の商品情報。カタログの5件目として、ブレンド豆200gという商品名、豆というカテゴリ、1280円という単価をタプルで定義しています。LINE 044: 深煎り豆の商品情報。

カタログの6件目として、深煎り豆200gという商品名、豆というカテゴリ、1380円という単価をタプルで定義しています。LINE 045: ドリップバッグの商品情報。カタログの7件目として、ドリップバッグ10個という商品名、豆というカテゴリ、980円という単価をタプルで定義しています。

LINE 046: ロゴタンブラーの商品情報。カタログの8件目として、ロゴタンブラーという商品名、雑貨というカテゴリ、2480円という単価をタプルで定義しています。LINE 047: 商品カタログリストの終端。

catalogリストの定義を閉じる行です。ここで定義した8種類の商品からランダムに選ばれて各サンプル行に使われます。LINE 048: 注文日の起点の設定。

サンプルデータの注文日の起点として2024年1月1日を設定しています。この日付を基準に日数を加算して各行の注文日を作ります。LINE 049: 出力先パスの作成。

引数で受け取った文字列のパスをPathオブジェクトに変換しています。以降のファイル操作をパスとして扱いやすくするための準備です。LINE 050: CSVファイルを開く。

書き込み用にCSVファイルを開いています。文字コードをutf-8に指定し、余分な改行が入らないようnewlineを空に設定しています。LINE 051: CSVライターの作成。

開いたファイルに行データを書き込むためのcsv.writerオブジェクトを作成しています。この後このwriterを使って各行を書き出します。LINE 052: ヘッダー行の書き込み。

CSVの1行目としてREQUIRED_COLUMNSに定義した列名をヘッダーとして書き込んでいます。これによりCSVの構造が明確になります。LINE 053: データ行の繰り返し処理。

指定された行数分だけデータを生成するためのループを開始しています。ループ変数は使わず、回数分だけ処理を繰り返します。LINE 054: 注文日の計算。

起点日にランダムな日数を加算して、その行の注文日を計算しています。90日以内のランダムな範囲で日付がずれるようになっています。LINE 055: 商品情報のランダム選択。

商品カタログの中から1件をランダムに選び、商品名・カテゴリ・単価をそれぞれの変数に取り出しています。LINE 056: 販売数量のランダム決定。1個から12個までの範囲でランダムに販売数量を決めています。

この数量が売上金額の計算にも使われます。LINE 057: CSV行の書き込み開始。1行分のデータをリストとしてまとめてCSVに書き込む処理の開始行です。

この後の行に各列の値が続きます。LINE 058: 書き込む値のリスト開始。CSVの1行として書き込む値のリストを定義する開始行です。

この後に各列に対応する値が列挙されます。LINE 059: 注文日の文字列変換。date型の注文日をISO形式の文字列に変換しています。

CSVには文字列として保存する必要があるための変換です。LINE 060: 店舗名のランダム選択。店舗名リストの中から1つをランダムに選び、その行の店舗として使っています。

店舗ごとの売上データを再現するための処理です。LINE 061: カテゴリの書き込み。先ほどランダムに選ばれた商品のカテゴリをそのままCSVの列として書き込んでいます。

LINE 062: 商品名の書き込み。先ほどランダムに選ばれた商品名をそのままCSVの列として書き込んでいます。LINE 063: 単価の書き込み。

先ほどランダムに選ばれた商品の単価をそのままCSVの列として書き込んでいます。LINE 064: 数量の書き込み。先ほどランダムに決めた販売数量をそのままCSVの列として書き込んでいます。

LINE 065: 売上金額の計算と書き込み。単価と数量を掛け合わせて売上金額を計算し、CSVの最後の列として書き込んでいます。この値がamount列の値になります。

LINE 066: 値リストの終端。1行分の値をまとめたリストの定義を閉じる行です。ここまでの値がまとめて1行としてCSVに書き込まれます。

LINE 067: 行書き込み処理の終端。writer.writerowの呼び出しを閉じる行です。これによりこの回のループで生成した1行分のデータがCSVファイルに書き込まれます。

LINE 068: 生成結果の返却。作成したCSVファイルのパスと書き込んだ行数をタプルとして呼び出し元に返しています。この戻り値は呼び出し側で表示メッセージに使われます。

RUN 2/8: サンプルCSVが書き出せるか試す。write_sample_csvが完成したので、一時ディレクトリへ5行だけ書き出します。ヘッダー行と1件目のデータが期待どおりか確認しましょう。

CHECK 2/8: 途中実行に成功。書き出し行数: 5 ヘッダー行: order_date,store,category,product,unit_price,quantity,amount 1件目: 2024-03-01,博多店,スイーツ,チーズケーキ,620,12,7440 RETURN 02: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 072: ヘッダー検証関数の定義。CSVファイルのヘッダー行が正しいかどうかを確認する関数を定義しています。

パスを受け取り、検証済みの列名リストを返します。LINE 073: 対象パスの変換。引数で受け取ったパスをPathオブジェクトに変換しています。

文字列とPathのどちらが渡されても統一的に扱えるようにするための処理です。LINE 074: ファイル存在確認の開始。対象のファイルが実際に存在するかどうかを確認する条件分岐です。

存在しない場合はこの後の行でエラーを発生させます。LINE 075: ファイル未存在時のエラー発生。指定されたCSVファイルが見つからない場合に、FileNotFoundErrorを発生させてファイルパスを含むメッセージを表示しています。

LINE 076: CSVファイルを読み込み用に開く。ヘッダー行を確認するためにCSVファイルを読み込み専用で開いています。BOM付きutf-8にも対応できる文字コードを指定しています。

LINE 077: CSVの先頭行を読み込む。csv.readerを使ってファイルの最初の1行だけを取り出し、ヘッダー候補として変数headerに格納しています。行が存在しない場合はNoneが入るようにしています。

LINE 078: ヘッダーの存在チェック。headerが空やNoneだった場合に、この後の処理に進まないようにする分岐です。CSVにヘッダー行がないケースを検出します。

LINE 079: ヘッダー不在時のエラー通知。ヘッダーが見つからなかった場合に、対象ファイルのパスを含めたValueErrorを発生させています。呼び出し元に問題を伝える役割です。

LINE 080: 列名の前後空白を除去。読み込んだヘッダーの各列名について、前後の余分な空白をstrip()で取り除き、比較しやすい形に整えています。LINE 081: 不足している必須列の抽出。

REQUIRED_COLUMNSのうち、実際のヘッダーに含まれていない列名だけをリスト内包表記で集めています。LINE 082: 不足列の有無を判定。missingリストに要素があるかどうかを調べ、必要な列が足りていない場合の処理へ分岐しています。

LINE 083: 不足列のエラー通知。不足している列名をカンマ区切りの文字列にまとめ、どの列が足りないかを明示したValueErrorを発生させています。LINE 084: 検証済みヘッダーを返す。

整形とチェックを終えたヘッダーのリストを呼び出し元へ返し、以降の処理で利用できるようにしています。RUN 3/8: CSVヘッダーの検証を動かす。read_headerが必須列をそろって認識できるかを見ます。

存在しないパスを渡した場合の例外メッセージもあわせて表示します。CHECK 3/8: 途中実行に成功。

列一覧: order_date, store, category, product, unit_price, quantity, amount 存在しない場合: 売上CSVが見つかりません: no_such_file.csv RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。

LINE 088: 検索用関数の定義。CSVのパスとSQL文を受け取り、DuckDBで検索した結果の列名と行データを返す関数を定義しています。LINE 089: パス文字列のエスケープ処理。

CSVのパスを文字列に変換し、シングルクォートをSQL内で安全に扱えるよう二重にエスケープしています。LINE 090: DuckDBへのメモリ接続。ディスクにファイルを作らないインメモリモードでDuckDBに接続し、一時的な検索環境を用意しています。

LINE 091: 接続を閉じるためのtry開始。この後の処理で例外が発生しても、確実に接続を閉じられるようtryブロックを開始しています。LINE 092: ビュー作成SQLの実行開始。

con.executeを呼び出し、次の行で組み立てるSQL文をDuckDBに実行させる処理の入口です。LINE 093: ビュー定義SQLの前半。既存のビューがあれば置き換える形で、salesという名前のビューを作成するSQL文の前半部分を組み立てています。

LINE 094: CSV読み込みSQLの後半。read_csv_auto関数でエスケープ済みパスのCSVをヘッダーあり読み込みとして指定し、ビューの元データとするSQLを完成させています。LINE 095: 実行文の終端。

前の2行にわたって組み立てたSQL文字列を閉じ、con.execute呼び出し全体を確定させています。LINE 096: ユーザー指定SQLの実行。引数で渡されたSQL文をsalesビューに対して実行し、実行結果を保持するカーソルを取得しています。

LINE 097: 列名一覧の取得。カーソルのdescription情報から各列の名前だけを取り出し、リストとしてcolumnsにまとめています。LINE 098: 全行データの取得。

cursor.fetchall()で検索結果の全行をタプルのリストとして取得し、rowsに格納しています。LINE 099: finallyブロックの開始。tryブロックの処理が成功しても失敗しても必ず実行される後片付け処理の入口を示しています。

LINE 100: DuckDB接続のクローズ。使い終わったDuckDBの接続を閉じ、確保していたリソースを解放しています。LINE 101: 検索結果を返す。

取得した列名リストと行データのリストをタプルとしてまとめ、呼び出し元に返しています。RUN 4/8: DuckDBでビューを作ってSQLを実行する。search_salesが書けたので、サンプルCSVをビューにして集計SQLを流します。

列名のリストと取得行数が返ることを確認します。CHECK 4/8: 途中実行に成功。列名: ['store', 'cnt'] 取得行数: 4 先頭行: ('札幌店', 5) RETURN 04: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 105: セル整形関数の定義。検索結果の1つの値を受け取り、表示用の文字列に変換する関数format_cellを定義しています。

LINE 106: None値の判定。値がNoneだった場合の分岐で、次の行で欠損値を表す表示に置き換える準備をしています。LINE 107: 欠損値の表示に置換。

値がNoneのときはハイフン記号を返し、表の中で欠損していることが一目でわかるようにしています。LINE 108: 真偽値かどうかの判定。値がbool型かどうかを調べ、真偽値専用の表示に分岐させています。

LINE 109: 真偽値を文字列に変換。Trueなら"true"、Falseなら"false"という文字列を返し、表内で読みやすい表記に統一しています。LINE 110: 整数値かどうかの判定。

値がint型かどうかを調べ、整数専用の桁区切り表示へ分岐させています。LINE 111: 整数を桁区切りで整形。フォーマット指定子","を使い、整数の値を3桁ごとにカンマで区切った文字列に変換しています。

LINE 112: 小数値かどうかの判定。値がfloatまたはDecimal型かどうかを調べ、小数専用の桁区切り表示へ分岐させています。LINE 113: 小数を桁区切りで整形。

値をfloatに変換したうえで、小数点以下1桁かつ桁区切りカンマ付きの文字列にフォーマットしています。LINE 114: その他の値を文字列化。上記いずれにも該当しない値は、str()でそのまま文字列に変換して返す最終的な処理です。

RUN 5/8: セルの整形結果を確かめる。format_cellが型ごとに表示を切り替えられるかを見ます。桁区切り、小数1桁、値なしのハイフンが出力されるはずです。

CHECK 5/8: 途中実行に成功。整数: 1,234,567 小数: 1,234.6 真偽: true 空値: - 文字: 新宿店 RETURN 05: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。

LINE 118: 表組み立て関数の定義。列名リストと行データ、タイトルを受け取り、Richのテーブルオブジェクトを組み立てる関数を定義しています。LINE 119: テーブルオブジェクトの生成開始。

Richのtable.Tableをインスタンス化する処理の開始で、次の行以降で見た目に関する設定を渡しています。LINE 120: 表タイトルの設定。引数で受け取ったtitleをテーブルのタイトルとして設定し、表の上部に表示されるようにしています。

LINE 121: 罫線スタイルの指定。表の枠線デザインとしてbox.SIMPLE_HEAVYを指定し、見やすい罫線スタイルにしています。LINE 122: ヘッダー文字装飾の指定。

見出し行の文字色と太字を指定し、列名部分が目立つ見た目になるようにしています。LINE 123: タイトル文字装飾の指定。表のタイトル部分を太字で表示するよう指定し、他の文字と区別しやすくしています。

LINE 124: Table生成呼び出しの終端。ここまでの引数指定を締めくくり、テーブルオブジェクトの生成を完了させています。LINE 125: 列名ループの開始。

渡された列名を1つずつ取り出すforループを開始し、各列をテーブルに追加する準備をしています。LINE 126: 数値列かどうかの判定。列名を小文字化し、NUMERIC_HINTSに含まれる単語が含まれるかを調べて数値列かどうかを判定しています。

LINE 127: 列追加処理の開始。table.add_columnを呼び出し、この列に対する表示設定を次の行以降で指定する準備をしています。LINE 128: 列名の指定。

ループで取り出した列名をそのまま表の列見出しとして使用するよう指定しています。LINE 129: 数値列の右寄せ指定。数値列であれば右寄せ、それ以外は左寄せとなるようjustifyを条件分岐で指定しています。

LINE 130: 数値列の色分け指定。数値列は緑色、それ以外は白色の文字スタイルとなるよう条件分岐でstyleを指定しています。LINE 131: 列追加呼び出しの終端。

ここまでの引数指定を締めくくり、1列分のadd_column呼び出しを完了させています。LINE 132: 行データループの開始。検索結果のrowsを1行ずつ取り出すforループを開始し、テーブルへの行追加処理につなげています。

LINE 133: 整形済みの行を表に追加。1行分の各値をformat_cellで文字列に整形し、アンパック演算子で展開してtable.add_rowに渡し、表に1行分のデータを追加しています。LINE 134: 完成した表を返す。

ここまで組み立てたTableオブジェクトを呼び出し元へ返し、画面出力に使えるようにしています。RUN 6/8: Richの表が組み立てられるか見る。build_tableへ列名と2行分のデータを渡し、罫線付きの表として描画されるか確認します。

数値列が右寄せになる点にも注目しましょう。CHECK 6/8: 途中実行に成功。

動作確認 store amount ━━━━━━━━━━━━━━━━━━ 新宿店128,000 梅田店 - 列数: 2 RETURN 06: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 138: 引数パーサー構築関数の定義。コマンドライン引数の定義をまとめて行うbuild_parser関数を定義しています。

LINE 139: パーサーオブジェクトの生成開始。argparse.ArgumentParserをインスタンス化する処理の開始で、次の行で説明文を設定しています。LINE 140: コマンドの説明文設定。

コマンド実行時のヘルプ表示に使われる説明文を指定し、ツールの目的を利用者に伝えています。LINE 141: パーサー生成呼び出しの終端。ここまでの引数指定を締めくくり、ArgumentParserの生成を完了させています。

LINE 142: csv引数の定義。--csvオプションを追加し、指定がない場合はデフォルトでsales.csvを検索対象とするよう設定しています。LINE 143: sql引数の定義。

--sqlオプションを追加し、salesビューに対して実行したいSELECT文を利用者が指定できるようにしています。LINE 144: max-rows引数定義の開始。--max-rowsオプションを追加する処理を開始し、次の行で型や既定値を指定しています。

LINE 145: max-rowsの型と既定値設定。--max-rowsの値を整数型として扱い、指定がない場合は既定値20を使うよう設定しています。LINE 146: max-rows引数定義の終端。

ここまでの指定を締めくくり、表示行数を制限するオプションの定義を完了させています。LINE 147: init-sample引数定義の開始。--init-sampleオプションを追加する処理を開始し、次の行以降でフラグとしての動作を設定しています。

LINE 148: フラグ動作の指定。このオプションが指定された場合にTrueを格納するstore_true動作を設定し、単純なフラグとして扱えるようにしています。LINE 149: ヘルプ文の指定。

--init-sampleオプションのヘルプ文として、サンプル売上CSVを書き出す機能であることを説明しています。LINE 150: init-sample引数定義の終端。ここまでの指定を締めくくり、サンプルCSV作成用フラグの定義を完了させています。

LINE 151: 完成したパーサーを返す。ここまで定義した各種オプションを持つパーサーオブジェクトを呼び出し元へ返し、main関数で利用できるようにしています。LINE 152: 関数定義の終端。

build_parser関数の処理全体を終え、コマンドライン引数の定義が完了したことを示しています。RUN 7/8: コマンドライン引数の解釈を確認する。build_parserが4つのオプションを受け取れる状態です。

既定値と指定値がどう入るかを表示して確かめます。CHECK 7/8: 途中実行に成功。csv: shop.csv / max_rows: 5 / init_sample: False sql: SELECT * FROM sales 既定のcsv: sales.csv / 既定のmax_rows: 20 RETURN 07: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 156: main関数の定義。コマンドライン引数のリストargvを受け取り、終了コードを返すmain関数を定義しています。

この関数がプログラム全体の処理の入口になります。LINE 157: パーサーの作成。build_parser関数を呼び出して、コマンドライン引数を解析するためのparserを作成しています。

ここで作られたparserを使って後の行で実際の引数を読み取ります。LINE 158: 引数の解析。parserのparse_argsメソッドでargvを解析し、結果をargsに格納しています。

以降の処理ではargsを通じて--csvや--sqlなどの値を参照します。LINE 160: サンプル作成モードの判定。args.init_sampleがTrueかどうかを確認し、サンプルCSVを作成するモードかどうかを分岐しています。

Trueの場合は下の行でCSVを作成する処理に進みます。LINE 161: サンプルCSVの書き出し。write_sample_csv関数を呼び出し、args.csvで指定されたパスにサンプルの売上CSVを作成しています。

戻り値として作成したファイルのパスと書き込んだ行数を受け取ります。LINE 162: ヘッダーの読み込み確認。read_header関数で作成したCSVファイルのヘッダー行を読み込み、必要な列が揃っているか確認しています。

読み込んだヘッダーはheaderに格納されます。LINE 163: 作成完了メッセージの表示。console.printでサンプルCSVを作成した旨と、そのパスや書き込んだ行数を画面に表示しています。

Richのマークアップで文字を緑色の太字で強調しています。LINE 164: 列名一覧の表示。読み込んだheaderの各列名をカンマで連結し、console.printで画面に表示しています。

作成したCSVにどんな列が含まれているかを利用者に伝えます。LINE 165: サンプル作成モードの終了。サンプルCSVの作成が完了したので、正常終了を示す0を返してmain関数を終了させています。

これにより以降のSQL検索処理はスキップされます。LINE 167: SQL未指定の判定。args.sqlが指定されていない場合を判定しています。

--init-sampleを使わずSQLも指定しないケースをここで検知します。LINE 168: SQL未指定のエラー表示。parser.errorを呼び出し、--sqlを指定するよう促すエラーメッセージを表示してプログラムを終了させています。

使い方のヒントとして--init-sampleの案内も含めています。LINE 170: 検索処理の例外処理開始。CSVの検証やSQL検索でエラーが起きる可能性があるため、try文で例外処理の範囲を開始しています。

エラーが発生した場合はexcept節で捕まえます。LINE 171: CSVヘッダーの再検証。read_header関数を呼び出し、検索対象のCSVファイルに必要な列が存在するかを事前に確認しています。

問題があればここで例外が発生します。LINE 172: SQL検索の実行。search_sales関数を呼び出し、指定したCSVとSQL文を使ってDuckDBで検索を実行しています。

戻り値として列名のリストcolumnsと結果の行rowsを受け取ります。LINE 173: 例外の捕捉。FileNotFoundError、ValueError、duckdb.Errorのいずれかが発生した場合にこのexcept節で捕捉し、errという変数にエラー内容を格納しています。

LINE 174: エラーメッセージの表示。console.printで発生したエラーの内容を赤色の太字で強調して画面に表示しています。利用者にどのような問題が起きたかを分かりやすく伝えます。

LINE 175: エラー時の終了。エラーが発生したことを示す終了コード1を返してmain関数を終了させています。これにより異常終了したことをシェル側で判定できます。

LINE 177: 表示行数の絞り込み。args.max_rowsが0より大きい場合はrowsの先頭からその件数だけをshownに切り出し、そうでない場合は全件をshownに代入しています。表に表示する行数を制御するための処理です。

LINE 178: 実行SQLの表示。console.printで実行したSQL文を薄い色で画面に表示しています。どのようなクエリを実行したのかを利用者が確認できるようにしています。

LINE 179: 検索結果テーブルの表示。build_table関数でcolumnsとshownからRichの表を組み立て、console.printで画面に表示しています。表のタイトルにはCSVファイル名を含めています。

LINE 180: 表示件数の比較判定。表示した行数shownが取得した全行数rowsより少ないかどうかを判定しています。表示が絞られている場合は下の行で補足メッセージを出します。

LINE 181: 件数補足メッセージの開始。console.printを呼び出し、取得件数と表示件数の両方を伝えるメッセージを表示する処理を開始しています。長いメッセージのため複数行の文字列に分けて渡しています。

LINE 182: 取得件数と表示件数の文言。取得した全行数len(rows)と実際に表示した行数len(shown)を太字で埋め込んだ文字列を作成しています。この行だけでは出力されず、次の行の文字列と連結されます。

LINE 183: 連結する文字列の付加。前の行の文字列に連結される白白の部分を継続しています。Pythonの文字列は連続して記載すると自動で連結されます。

LINE 184: 調整方法の案内文言。--max-rowsオプションで表示件数を調整できることを案内する文字列で、前の行の文字列と自動的に連結されてメッセージ全体を完成させています。利用者に表示件数を変更する方法を伝えます。

LINE 185: 全件表示時の分岐。表示行数が取得行数と同じだった場合の処理を示すelse節です。この場合は件数の絞り込みが発生していないことを意味します。

LINE 186: 全件表示時の件数表示。console.printで取得した全行数len(rows)を太字で強調して表示しています。絞り込みが行われていないため、取得件数のみを伝えるシンプルな表示です。

LINE 187: 正常終了。検索処理と表示が正常に完了したことを示す0を返してmain関数を終了させています。プログラム全体の処理結果を呼び出し元に伝える役割を持ちます。

RUN 8/8: メイン処理をサンプル作成から検索まで通す。mainが完成したので、サンプル作成と集計検索を続けて実行します。どちらも終了コード0で終わるかを確認します。

CHECK 8/8: 途中実行に成功。

サンプル売上CSVを作成しましたcp_sales.csv (120行) 列: order_date, store, category, product, unit_price, quantity, amount 作成の終了コード: 0 SQL: SELECT category, SUM(amount) AS total FROM sales GROUP BY category ORDER BY total DESC cp_sales.csv の検索結果 category total ━━━━━━━━━━━━━━━━━━━━ 豆354,160 雑貨282,720 スイーツ135,260 飲料89,400 取得4行 検索の終了コード: 0 RETURN 08: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 190: スクリプト実行時の判定。このファイルが直接実行されたときにTrueとなる__name__変数を確認する条件式です。

他のファイルからインポートされた場合は下の行は実行されません。LINE 191: main関数の呼び出しと終了。main関数を呼び出し、その戻り値をsys.exitに渡してプログラムの終了コードとして設定しています。

これによりmain関数の結果がプロセスの終了ステータスに反映されます。実行1/4: サンプル売上CSVを作成する。--init-sampleを付けて実行し、サンプルCSVの保存先と行数、列名の一覧が表示されることを確認します。

確認1/4: サンプル売上CSVを作成する。--init-sampleを付けて実行し、サンプルCSVの保存先と行数、列名の一覧が表示されることを確認します。RETURN 09: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。実行2/4: 店舗別の売上をSQLで集計する。サンプルCSVを作ってからGROUP BYのSELECT文を渡し、店舗ごとの売上合計がRichの表で並ぶことを確認します。

確認2/4: 店舗別の売上をSQLで集計する。サンプルCSVを作ってからGROUP BYのSELECT文を渡し、店舗ごとの売上合計がRichの表で並ぶことを確認します。RETURN 10: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。実行3/4: 表示行数を絞って件数の内訳を見る。--max-rows 3を付けて金額の高い順に検索し、表が3行で止まり取得件数との内訳が出ることを確認します。

確認3/4: 表示行数を絞って件数の内訳を見る。--max-rows 3を付けて金額の高い順に検索し、表が3行で止まり取得件数との内訳が出ることを確認します。RETURN 11: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。実行4/4: 存在しないCSVを指定してエラーを確認する。ないファイルを --csvに渡し、赤字のエラーメッセージと終了コード1で止まることを確認します。

確認4/4: 存在しないCSVを指定してエラーを確認する。ないファイルを --csvに渡し、赤字のエラーメッセージと終了コード1で止まることを確認します。RETURN 12: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。学習内容のまとめ。

SELECT store, SUM(amount)で店舗別の売上を合計 見出し行をbold cyanで強調 --init-sampleだけでサンプル120行を作成して終了DuckDBへの接続をインメモリ(:memory:)で開く 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。

Python研修はCodeCampでご確認ください。

DuckDBとRichとは

今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。

CSVをSQLで検索できるDuckDBの役割

DuckDBは、CSVファイルをテーブルのように扱ってSQLで集計できる、Pythonから直接呼び出せる分析用データベースライブラリです。サーバーの起動や事前のテーブル作成が要らず、pip install duckdbだけで使い始められます。

本記事のCLIでは :memory: 接続でCSVをsalesビューとして登録し、絞り込みや合計の計算をSQL側へ任せました。集計処理をPythonのループで書かずに済むため、条件を変えたいときはSELECT文を差し替えるだけで結果を確かめられます。

DuckDBを使うことで売上CSVに対して書けるようになる検索と、準備作業の一覧です

  • SELECT store, SUM(amount)で店舗別の売上を合計
  • WHERE category = '豆' でカテゴリを絞り込む
  • ORDER BY amount DESCで高額な注文から並べる
  • COUNT(*)で条件に合う行数だけを確認
  • サーバー起動やテーブル作成の準備が不要

ターミナル表示を整えるRichの役割

Richは、色付きの文字や罫線付きの表をターミナルへ出力できる表示整形ライブラリです。printだけでは読みづらい検索結果を、見出しの強調や桁のそろった表として見せてくれます。今回はConsoleでメッセージを出し、Tableに列と行を追加して検索結果を組み立てました。

金額のような数値列は右寄せと色分けへ自動で切り替わるので、行数が増えても値を目で追いやすくなります。

Richを取り入れたことで検索結果の画面に加わった見た目の工夫の一覧です

  • 見出し行をbold cyanで強調
  • 実行したSQLを控えめな装飾で併記
  • 表題に対象CSVのファイル名を入れる
  • 値のないセルを - に置き換える
  • 整数を1,234形式、小数を0.5形式で表示
さらに詳しく学べるPython研修の詳細はこちら

Python・DuckDBで開発する場合の環境構築

この記事のセットアップ手順と掲載コードは、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 duckdb rich

macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。

python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install duckdb rich
  • DuckDBはサーバ不要でpip導入だけで動きます。今回はdatabase=":memory:" のインメモリ接続なのでファイルは作られません。
  • PowerShellで --sqlを渡すときはSQL全体を二重引用符で囲み、SQL内の文字列リテラルには単引用符を使うと崩れません。
  • 対象CSVはヘッダー行付きのUTF-8を想定しています。BOM付きでもヘッダー検証側はutf-8-sigで読み込みます。
さらに詳しく学べるPython研修の詳細はこちら

PythonのDuckDBとRichで作る売上CSV検索CLIの要件定義

目的は、PythonのDuckDBで売上CSVをSQL検索し、結果をRichの表と取得行数としてターミナルに表示するコマンドラインツールを作ることです。

対象者として、Pythonの基本文法を学び終え、CSVデータをSQLで集計するコマンドラインツールの作り方を学びたい人を想定しています。

完成物は、サンプルCSVの生成とSQL検索をオプションで切り替え、DuckDBの結果をRichの表で表示する売上CSV検索CLIです。

実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。

機能要件

  • --init-sampleでサンプル売上CSVを120行書き出す
  • サンプル作成後に保存先パスと列名一覧を表示する
  • 必須7列がそろっているかCSVのヘッダーを検証する
  • read_csv_autoでCSVをsalesビューとして登録する
  • --sqlに渡したSELECT文をDuckDBで実行する
  • 検索結果の列名と行をRichの表に組み立てる
  • 数値列を右寄せにして桁区切りで表示する
  • --max-rowsで表に出す行数を制限する
  • 取得行数と表示行数を表の下に表示する
  • 実行したSQL文を表の前に併記する
  • --sqlを省略した場合は使い方を案内する

非機能要件

  • DuckDBへの接続をインメモリ(:memory:)で開く
  • finallyで必ずcon.close()を呼ぶ
  • CSVパス中のシングルクォートを二重化してSQLへ渡す
  • CSVの読み込みはutf-8-sigでBOM付きにも対応
  • FileNotFoundErrorとValueErrorとduckdb.Errorを捕捉
  • エラー時は終了コード1、正常時は0を返す
  • サンプル生成はseed=2024固定で同じ内容を再現
  • サンプルCSVの書き出しは標準ライブラリのcsvモジュール
  • 外部サーバーやデータベースファイルを使わない構成

実装方針

今回はDuckDBとRichの基本動作を追いやすくするため、売上CSV検索CLI本体を1つのPythonファイルへまとめます。

入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。

売上CSV検索CLIを安全に組み立てるための実装方針は次のとおりです。

  • DuckDBへの接続をインメモリ(:memory:)で開く
  • finallyで必ずcon.close()を呼ぶ
  • CSVパス中のシングルクォートを二重化してSQLへ渡す
  • CSVの読み込みはutf-8-sigでBOM付きにも対応
  • FileNotFoundErrorとValueErrorとduckdb.Errorを捕捉
  • エラー時は終了コード1、正常時は0を返す
  • サンプル生成はseed=2024固定で同じ内容を再現
  • サンプルCSVの書き出しは標準ライブラリのcsvモジュール
  • 外部サーバーやデータベースファイルを使わない構成

完成と判断する条件

  1. --init-sampleでsales.csvが作成される
  2. 作成メッセージに120行 と7列の列名が並ぶ
  3. GROUP BYの集計結果が罫線付きの表で表示される
  4. 表の下に取得行数が表示される
  5. --max-rows 3で表示3行と取得120行が併記される
  6. 存在しないCSVの指定で赤字エラーと終了コード1になる
  7. 5回のコマンド実行が全て終了コード0で終わる
さらに詳しく学べるPython研修の詳細はこちら

DuckDBとRichで売上CSV検索CLIを作る際の重要ポイント

完成した売上CSV検索CLIは、引数の解釈からヘッダー検証、SQL実行、表の描画までをmain関数がひとつの流れとしてつないでいます。まず --init-sampleの有無を調べ、サンプル作成だけで終わる場合はそこで終了コード0を返します。通常実行では必須7列の検証を先に済ませ、DuckDBから受け取った列名と行をRichの表へ渡す流れです。

想定した例外はまとめて捕まえ、赤字のメッセージと終了コード1で失敗を伝えます。

コマンドを実行したときにmainが判断している分岐と、画面へ出る結果の一覧です

  • --init-sampleだけでサンプル120行を作成して終了
  • 必須7列がそろわないCSVは検索前に停止
  • --max-rows 3なら表は3行で内訳メッセージが追加
  • --sqlを省略すると使い方エラーで終了
  • 存在しないCSVは赤字エラーと終了コード1
  • 検索が通れば表を描画して終了コード0

argparseで4つのオプションを受け取る設計

引数の定義はbuild_parser関数へまとめました。--csvは既定値をsales.csvにしてあるため、同じディレクトリで作業するなら省略できます。--sqlは必須指定にせず、値がないときだけparser.errorで使い方を案内する形にしました。

--max-rowsはtype=intを付けて数値として受け取り、既定値は20です。--init-sampleはフラグなので、付ければサンプル生成、付けなければ検索という分岐になります。

CLIが受け取る4つのオプションの役割の一覧です

  • --csvは検索対象のCSVパスで既定はsales.csv
  • --sqlはsalesビューへ実行するSELECT文
  • --max-rowsは表に出す最大行数で既定20
  • --init-sampleはサンプル生成を選ぶフラグ

検索前にCSVヘッダーを検証する理由

SQLを実行してから列がないと気付くより、先にヘッダーを見て止めたほうが原因が分かりやすくなります。read_headerは先頭行だけを読み、必須7列のうち足りない名前を集めてValueErrorで知らせます。ファイル自体がなければFileNotFoundErrorを送出する作りです。

読み込みにはutf-8-sigを指定しました。表計算ソフトが付けるBOMがあっても列名が崩れないため、Excelから書き出したCSVもそのまま検証できます。

ヘッダー検証で行っているチェック内容の一覧です

  • ファイルの存在を先に確認する
  • csv.readerで先頭行だけ読み取る
  • 列名の前後の空白を取り除く
  • 不足している必須列名をまとめて通知する

表示行数の制限と終了コードの決め方

取得件数が多いときに全行を流すと、ターミナルは読みづらくなります。そこで表へ渡す前に行をスライスし、表示した行数が取得行数より少ない場合だけ内訳のメッセージを足しました。--max-rowsへ0以下を渡したときは全行が表示対象です。

戻り値は成功で0、想定した例外で1に分けています。最後にsys.exitへ渡すため、シェルやCIからも実行結果を判定できるでしょう。

行数の切り出しと終了コードの分岐で使っている条件の一覧です

  • rows[: args.max_rows]で表示分だけ切り出す
  • max-rowsが0以下なら全行を表示対象にする
  • 表示行が取得行より少ないときだけ内訳を出す
  • 想定した例外は戻り値1、成功は戻り値0
さらに詳しく学べるPython研修の詳細はこちら

PythonとDuckDBの処理の流れ

入力、主要処理、出力の順に役割を分けて実装します。処理の境界を明確にすると、実行結果とエラー箇所を照合しやすくなります。

実装から検証までの流れ 入力を準備DuckDBで処理結果を検証成果物を確認
実装から検証までの流れ

Python研修一覧はこちら

目的に合うPython研修を一覧形式から探したい方は、ぜひご利用ください。

Python研修を比較する

Java研修一覧はこちら

目的に合うJava研修を一覧形式から探したい方は、ぜひご利用ください。

Java研修を比較する

PHP研修一覧はこちら

目的に合うPHP研修を一覧形式から探したい方は、ぜひご利用ください。

PHP研修を比較する

新入社員研修

目的に合う新入社員研修を一覧形式から探したい方は、ぜひご利用ください。

新入社員研修を比較する

全ての研修からも探したい方はこちら

PythonのDuckDBとRichで作る売上CSV検索CLIの完成コード

実行に成功したコード全文を確認できます。実行環境で使ったものと同じコードを掲載しています。

"""売上CSVをDuckDBのSQLで検索し、Richの表に表示するCLI。"""

from __future__ import annotations

import argparse
import csv
import random
import sys
from datetime import date, timedelta
from decimal import Decimal
from pathlib import Path

import duckdb
from rich import box
from rich.console import Console
from rich.table import Table

# 設定: 売上CSVのスキーマと表示ルール
REQUIRED_COLUMNS = [
    "order_date",
    "store",
    "category",
    "product",
    "unit_price",
    "quantity",
    "amount",
]
NUMERIC_HINTS = ("price", "quantity", "amount", "total", "sum", "avg", "count", "cnt")
VIEW_NAME = "sales"

console = Console()


# サンプル売上CSVの生成(csvモジュールで書き出す)
def write_sample_csv(path: str, row_count: int = 120, seed: int = 2024) -> tuple[Path, int]:
    rng = random.Random(seed)
    stores = ["新宿店", "梅田店", "博多店", "札幌店"]
    catalog = [
        ("ドリップコーヒー", "飲料", 480),
        ("カフェラテ", "飲料", 520),
        ("チーズケーキ", "スイーツ", 620),
        ("シフォンケーキ", "スイーツ", 560),
        ("ブレンド豆200g", "豆", 1280),
        ("深煎り豆200g", "豆", 1380),
        ("ドリップバッグ10個", "豆", 980),
        ("ロゴタンブラー", "雑貨", 2480),
    ]
    start = date(2024, 1, 1)
    out = Path(path)
    with out.open("w", encoding="utf-8", newline="") as fp:
        writer = csv.writer(fp)
        writer.writerow(REQUIRED_COLUMNS)
        for _ in range(row_count):
            order_date = start + timedelta(days=rng.randrange(90))
            product, category, unit_price = rng.choice(catalog)
            quantity = rng.randint(1, 12)
            writer.writerow(
                [
                    order_date.isoformat(),
                    rng.choice(stores),
                    category,
                    product,
                    unit_price,
                    quantity,
                    unit_price * quantity,
                ]
            )
    return out, row_count


# CSVヘッダーの検証(csvモジュールで先頭行だけ読む)
def read_header(path: str | Path) -> list[str]:
    target = Path(path)
    if not target.exists():
        raise FileNotFoundError(f"売上CSVが見つかりません: {target}")
    with target.open(encoding="utf-8-sig", newline="") as fp:
        header = next(csv.reader(fp), None)
    if not header:
        raise ValueError(f"売上CSVにヘッダー行がありません: {target}")
    header = [name.strip() for name in header]
    missing = [name for name in REQUIRED_COLUMNS if name not in header]
    if missing:
        raise ValueError("必要な列が足りません: " + ", ".join(missing))
    return header


# 売上CSVをDuckDBのビューにしてSQLで検索する
def search_sales(path: str | Path, sql: str) -> tuple[list[str], list[tuple]]:
    escaped = str(Path(path)).replace("'", "''")
    con = duckdb.connect(database=":memory:")
    try:
        con.execute(
            f"CREATE OR REPLACE VIEW {VIEW_NAME} AS "
            f"SELECT * FROM read_csv_auto('{escaped}', header=true)"
        )
        cursor = con.execute(sql)
        columns = [desc[0] for desc in cursor.description]
        rows = cursor.fetchall()
    finally:
        con.close()
    return columns, rows


# 検索結果の整形(数値は桁区切り)
def format_cell(value: object) -> str:
    if value is None:
        return "-"
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, int):
        return f"{value:,}"
    if isinstance(value, (float, Decimal)):
        return f"{float(value):,.1f}"
    return str(value)


# 検索結果をRichの表に組み立てる
def build_table(columns: list[str], rows: list[tuple], title: str) -> Table:
    table = Table(
        title=title,
        box=box.SIMPLE_HEAVY,
        header_style="bold cyan",
        title_style="bold",
    )
    for name in columns:
        numeric = any(hint in name.lower() for hint in NUMERIC_HINTS)
        table.add_column(
            name,
            justify="right" if numeric else "left",
            style="bright_green" if numeric else "white",
        )
    for row in rows:
        table.add_row(*[format_cell(value) for value in row])
    return table


# コマンドライン引数の定義
def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="売上CSVをDuckDBのSQLで検索し、結果をRichの表に表示します。",
    )
    parser.add_argument("--csv", default="sales.csv", help="検索対象の売上CSVのパス")
    parser.add_argument("--sql", help="salesビューに対して実行するSELECT文")
    parser.add_argument(
        "--max-rows", type=int, default=20, help="表に表示する最大行数(既定20)"
    )
    parser.add_argument(
        "--init-sample",
        action="store_true",
        help="動作確認用のサンプル売上CSVを書き出す",
    )
    return parser


# メイン処理
def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    if args.init_sample:
        path, written = write_sample_csv(args.csv)
        header = read_header(path)
        console.print(f"[bold green]サンプル売上CSVを作成しました[/bold green] {path} ({written}行)")
        console.print("[dim]列:[/dim] " + ", ".join(header))
        return 0

    if not args.sql:
        parser.error("--sql に検索したいSELECT文を指定してください(--init-sample でサンプルCSVを作成できます)")

    try:
        read_header(args.csv)
        columns, rows = search_sales(args.csv, args.sql)
    except (FileNotFoundError, ValueError, duckdb.Error) as err:
        console.print(f"[bold red]エラー:[/bold red] {err}")
        return 1

    shown = rows[: args.max_rows] if args.max_rows > 0 else rows
    console.print(f"[dim]SQL:[/dim] {args.sql}")
    console.print(build_table(columns, shown, f"{Path(args.csv).name} の検索結果"))
    if len(shown) < len(rows):
        console.print(
            f"取得 [bold]{len(rows)}[/bold] 行 / 表示 [bold]{len(shown)}[/bold] 行"
            "(--max-rows で調整できます)"
        )
    else:
        console.print(f"取得 [bold]{len(rows)}[/bold] 行")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Python基礎・実践(Django)

企業・法人向けのPython研修では、基礎から応用まで体系的に学べます。

Python研修の詳細

DX社員研修

企業・法人向けのDX研修では、実務に繋がるリスキリングでITレベルを向上させます。

DX研修の詳細

Javaエンジニア育成研修

企業・法人向けのJavaエンジニア育成研修では、Javaの基礎から応用まで確実に習得できます。

Java研修の詳細

新卒・新入社員向け研修

企業・法人に新入社員・新卒社員に向けたプログラミング研修を提供しています。

新入社員研修の詳細

コードキャンプのIT研修を全て見る

PythonのDuckDBとRich利用時に起きやすいエラーと対処法

エラーが起きた場合は、入力値、実行環境、出力先の順に確認します。

確認箇所 確認内容
入力 形式や必須値が想定どおりか確認する
実行環境 Pythonと必要ライブラリの版を確認する
出力 保存先と権限、生成されたファイルを確認する
さらに詳しく学べるPython研修の詳細はこちら

Pythonコードを実行して検証

検証結果は「CLIツールを5回のコマンドで実行し、全て正常(終了コード0)に動作した」でした。

python subject.py --init-sample --csv sales.csvの実行結果(終了コード0)
python subject.py --init-sample --csv sales.csvの実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT order_date, store, product, quantity, amount FROM sales ORDER BY amount DESC LIMIT 8の実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT order_date, store, product, quantity, amount FROM sales ORDER BY amount DESC LIMIT 8の実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT category, COUNT(*) AS cnt, SUM(amount) AS total_amount FROM sales GROUP BY category ORDER BY total_amount DESCの実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT category, COUNT(*) AS cnt, SUM(amount) AS total_amount FROM sales GROUP BY category ORDER BY total_amount DESCの実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT store, SUM(amount) AS total_amount, ROUND(AVG(amount), 1) AS avg_amount FROM sales WHERE order_date >= '2024-03-01' GROUP BY store ORDER BY total_amount DESCの実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT store, SUM(amount) AS total_amount, ROUND(AVG(amount), 1) AS avg_amount FROM sales WHERE order_date >= '2024-03-01' GROUP BY store ORDER BY total_amount DESCの実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT product, SUM(quantity) AS quantity FROM sales WHERE category = '豆' GROUP BY product ORDER BY quantity DESC --max-rows 3の実行結果(終了コード0)
python subject.py --csv sales.csv --sql SELECT product, SUM(quantity) AS quantity FROM sales WHERE category = '豆' GROUP BY product ORDER BY quantity DESC --max-rows 3の実行結果(終了コード0)
さらに詳しく学べるPython研修の詳細はこちら

Python実装で確認するポイント

入力、処理、出力を分けて確認すると問題を切り分けやすくなります。

ポイントとしては、確認対象と実測範囲を押さえておきましょう。

POINT

確認対象:入力と処理と出力を分離

実測範囲:未計測の性能を断定しない

さらに詳しく学べるPython研修の詳細はこちら

PythonとDuckDBを活用できる場面

同じ処理の流れを、入力や出力形式を変えた別の業務・学習用途へ応用できます。まず小さな入力で動作を確認してから対象を広げます。

さらに詳しく学べるPython研修の詳細はこちら

PythonのDuckDBとRichによる売上CSV検索CLI開発のまとめ

実行済みコードと「CLIツールを5回のコマンドで実行し、全て正常(終了コード0)に動作した」を基に結果を整理しました。

さらに詳しく学べるPython研修の詳細はこちら

参考にした一次情報

  1. ^ DuckDB Python API(クライアント概要). https://duckdb.org/docs/stable/clients/python/overview, (参照26-08-08).
  2. ^ DuckDB CSV Import(read_csv / read_csv_auto). https://duckdb.org/docs/stable/data/csv/overview, (参照26-08-08).
  3. ^ Richドキュメント: Tables. https://rich.readthedocs.io/en/stable/tables.html, (参照26-08-08).
  4. ^ Python標準ライブラリ: csv — CSVファイルの読み書き. https://docs.python.org/ja/3/library/csv.html, (参照26-08-08).

※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。

ブログに戻る

コメントを残す

コメントは公開前に承認される必要があることにご注意ください。

企業・法人向けのIT・プログラミング・生成AI研修を探す、比較する - IT・プログラミングを知って学べるコネクトメディア CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプが提供する無料で学べるプログラミングスクール講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア 3.5日の研修で、年間1,600時間の削減効果が見込まれる。東京きらぼしフィナンシャルグループのDX人材育成事例 - IT・プログラミングを知って学べるコネクトメディア 配属3ヶ月で30%の生産性向上を実現するいよぎんコンピュータサービスの新人研修に迫る - IT・プログラミングを知って学べるコネクトメディア 金融業界の業務効率化を加速するニッセイアセットマネジメントの生成AI×GAS活用研修事例 - IT・プログラミングを知って学べるコネクトメディア 【製造業のDX人材育成事例】デジタル人材の即戦力化を実現する、日本ガイシ株式会社の異動者向オンボーディング研修 - ITやプログラミングを知って学べるコネクトメディア フューチャーアーキテクト株式会社が実現した新入社員向けIT研修プログラムでタスクフォース制度が主体的な学びと成長を生み出す - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【IT新入社員研修】オンラインとオフラインの最適バランスを実現したFutureOneの導入事例 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【新入社員研修】柔軟なハイブリッド型Java研修で実現した新卒20名の成長と成果|サークレイス株式会社 - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/現場により近いところにデジタルを根付かせるDX基礎講座研修|株式会社ブリヂストン - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/業務の効率化・DX推進に向けたIT人材育成への第一歩|株式会社カナエ - ITやプログラミングを知って学べるコネクトメディア 企業・法人向けのIT・プログラミング研修 - ITやプログラミングを知って学べるコネクトメディア

新着記事

対象者別で探す

子供(小学生・中学生・高校生)向け
プログラミング教室検索する

子供(小学生・中学生・高校生)がロボットやプログラミング言語を学ぶことができるオフラインからオンラインスクールを検索、比較することが可能です。

子供(小学生・中学生・高校生)
プログラミング教室検索する

ITやプログラムなどの
最新情報を検索する

日々、新しいITやプログラミング言語の情報が流れていきますが、特定の情報を時系列でニュースやコラムを確認することができます。

ITやプログラムなどの
最新情報を検索する