【初心者歓迎】CSVファイル分割ツールを作ろう

今回の自作ツールは、「CSVファイル分割ツール」です。指定行数ごとにCSVファイルを分割する方法は、色々な方が取り上げていますが、そこにはヘッダの存在を無視した方法も多いようです。

今回紹介する「CSVファイル分割ツール」は、CSVファイルのヘッダを考慮しており、分割後のファイルにもヘッダが正しく書き込まれるようになっています。

また、文字コードの変換や、指定したキーワードが含まれる行のみ抽出したり、削除する機能も付けました。

PythonでCSVファイルを分割するツールを作りたい方は、本記事を参考にトライしてみてください。

自作ツールを作るためのポイントと今後公開予定の自作ツール一覧を「【実践】ちょっと便利なツールをPythonで作ろう!」で紹介していますので、併せてご覧ください。

目次

CSVファイル分割ツールの紹介

今回紹介する自作ツールはCSVファイル分割ツールです。分割したいファイルと出力先フォルダを指定し、「CSVファイルの分割」ボタンをクリックすれば、指定行数ごとに分割してくれます。

  • メモリに収まらない巨大なCSVファイルの分割が可能です。
  • 分割したファイル全てにヘッダを付加されます。
  • ヘッダの行数は任意の行数の指定が可能です。
  • 分割対象のCSVファイルと、分割後のCSVファイルの文字コードを指定可能(文字コード変換機能)です。
  • 任意の分割行数の指定が可能です。
  • 指定したワードを含む行の抽出が可能(ワードはカンマ区切りで複数指定可能)です。
  • 指定したワードを含む行の除外が可能(ワードはカンマ区切りで複数指定可能)です。
  • CSVファイル名、保存先フォルダ名など、画面から入力した内容は保持され、次回起動時に復元されます。
  • CSVファイルとフォルダはドラッグ&ドロップでの指定が可能です。

本ツールは厳密にはテキストファイルを分割するものであり、CSVのフォーマット(区切り文字、ダブルクォーテーション有無)は意識していません。1行ごとに改行コードで区切られてさえいれば、任意の行数で分割が可能です。

画面レイアウト

使い方

CSVファイル分割の分割

次の手順でCSVファイルを分割できます。

  • 分割対象のCSVファイルを指定する
  • 保存先の出力フォルダを指定する
  • 分割したい行数を指定する
  • 「CSVファイルの分割」ボタンをクリックする

CSVファイル(またはテキストファイル)の文字コード変換

分割行数に、空白または「なし」を指定すると、ファイル分割されません。この状態で文字コードを指定することで、CSVを含むテキストファイルの文字コード変換が可能です。

CSVファイル(またはテキストファイル)から特定の行を抽出/削除する

「キーワードを含む行を選択」、「キーワードを含む行を除外」を指定することで、そのキーワードを含む行だけを取り出したり、削除することができます。

尚、両方に同じキーワードは指定しないでください。

プログラムのダウンロードと動作環境の設定

STEP
Pythonのインストール

Pythonのインストールが必要です。既に構築済みの方は読み飛ばしてください。プログラムを修正する場合は、VSCode Portable も併せてインストールしてください。

自作ツールのためのポータブルPython開発・実行環境を作ろう!プログラム実行時に必要
WinPythonにVSCode Portable版を入れよう!プログラム修正時に必要

自作ツールのためのポータブルPython開発・実行環境を作ろう! | 【初心者歓迎】Pythonで作る!かんたんツール工房(新しいタブで開く)

STEP
ライブラリのインストール

Python 環境に下記のライブラリをインストールします。 command.bat を実行後、下記のコマンドを実行してください。

pip install customtkinter
pip install tkinterdnd2

STEP
ソースコードのダウンロードと展開

下記のリンクからZipファイルをダウンロード&解凍します。次に、Python環境の任意の場所にフォルダを作成し、そこに解凍したファイルを丸ごとコピーしてください。

実行方法

次の手順で実行してください。

  • WinPython のインストールフォルダ内にある command.bat を実行
  • ダウンロードしたプログラムファイルをコピーした場所にカレントディレクトリを移動
  • Python ui.py を実行

Python ui.py

下記の画面が表示されるので、本記事の冒頭で説明した手順に従って操作してください。

何らかのエラーが発生した場合、コマンドプロンプトにエラー内容が表示されます。

CSVファイル分割ツールの設計

以下は、プログラム作成にあたって必要事項を取りまとめた内容(=設計)になります。単にCSVファイル分割ツールをダウンロードして使いたい方は無視して構いません。
ただ、DIYプログラミングにおいて参考になる内容なので、興味のある方は最後までお付き合いください。

仕様

画面項目入力内容
CSVファイル名分割対象のCSVファイルを選択するかドラッグ&ドロップします
文字コード分割対象のCSVファイルの文字コードを、SHIFT-JIS と UTF-8 から指定します。Pythonがサポートしている文字コードを直接入力することも可能です。
ヘッダ行数CSVファイルのヘッダ数を指定します。
保存先フォルダ名分割後のCSVファイルを保存するフォルダを選択またはドラッグ&ドロップします。
文字コード分割後のCSVファイルの文字コードを指定します。
分割行数分割したい行数を指定します。ヘッダ行数+分割行数でファイルが作成されます。ヘッダ含を含めた行数にしたい場合、分割行数ーヘッダ行数の数値を入力してください。
「なし」または空白を指定すると、ファイル分割されません。
キーワードを含む行を抽出キーワードをカンマ区切りで列挙すると、そのキーワードのいずれかが含まれている行だけが出力対象となります。
キーワードを含む行を除外キーワードをカンマ区切りで列挙すると、そのキーワードのいずれかが含まれている行は出力されなくなります。

設計段階で考えた画面レイアウトは、実際に出来上がった画面レイアウトと若干異なっています。

実現方法

  • 画面はCustomTkinter で作成
  • 巨大なCSVファイルが分割できるよう、1行づつ読み込んで処理する(pandas の read_csv は使わない)。

プログラムの構成

本プログラムは4つの py ファイルで構成しています。csv_spliter.py には split_file関数のみ記述していますが、これは他のプログラムで流用することを考慮しているからです。

モジュール名役割
ui.pyユーザインターフェースを担当
csv_spliter.pyCSVファイルを分割するための処理(関数)
customtkintercontrols.pyファイル選択やフォルダ選択、ドラッグ&ドロップを実現するための補助
youtube_dl.py指定された動画アドレスから動画をダウンロードする
appconfig.py画面に入力された値をJson形式のファイルに保存/読込する

個々の処理手順

「CSV分割」ボタンがクリックされた時の処理を、簡易的なフローチャートで表現しました。

ソースコード

ui.py

import os
import customtkinter as ctk
from customtkintercontrols import App
from customtkintercontrols import FileSelectionControl
from customtkintercontrols import FolderSelectionControl
from customtkintercontrols import EntryControl
import csv_spliter as csvspl
from appconfig import AppConfig 
# -----------------------------------------------
# コールバック関数の定義 
# -----------------------------------------------

# ウィンドウを閉じる時に呼ばれる関数
def close_window():
    # 画面入力値を保存
    conf.set("file_selection",file_selection.get())
    conf.set("input_encoding_name",input_encoding_name.get())
    conf.set("folder",folder_selection.get())
    conf.set("output_encoding_name",output_encoding_name.get())
    conf.set("input_header_cnt",input_header_cnt.get())
    conf.set("split_count",split_count.get())
    conf.set("contain_words",contain_words.get())
    conf.set("not_contain_words",not_contain_words.get())
    conf.write()
    # プログラムの終了
    app.quit()

# 変換ボタンが押された時に呼ばれる関数
def split():
    csv_file = file_selection.get()
    in_encoder = input_encoding_name.get()
    out_folder = folder_selection.get()
    out_encoder = output_encoding_name.get()
    head_cnt = input_header_cnt.get()
    spl_cnt = split_count.get().strip()
    spl_cnt = "0" if spl_cnt == "なし" or spl_cnt == "" else spl_cnt
    varid_words = contain_words.get() if is_contain.get() != 0 else ""
    omit_wors = not_contain_words.get() if is_not_contain.get() != 0 else ""
    csvspl.split_file(csv_file,out_folder,int(head_cnt),int(spl_cnt),in_encoder,out_encoder,varid_words,omit_wors)
# -----------------------------------------------
# メインウインドウの定義
# -----------------------------------------------

# 前回入力値の保存・復元用インスタンスの生成
conf = AppConfig("./config.json")

# メインウィンドウの作成
app = App()
app.minsize(width=500, height=500)
app.geometry(f"800x550")
app.title("CSVファイル分割")

# customtkinterをダークモードに設定
ctk.set_appearance_mode("light")

# ウィンドウを閉じる時に呼びたい関数を登録
app.protocol("WM_DELETE_WINDOW", close_window)

# -----------------------------------------------
# 画面レイアウト(ウィジェット)の定義 
# -----------------------------------------------

# ファイル選択ダイアログを配置
file_selection = FileSelectionControl(app)
file_selection.label.configure(text="   CSVファイル名")
file_selection.pack(padx=10,pady=10,fill="x")

# 文字コードとヘッダ行数の指定ウィジェットを配置
group = ctk.CTkFrame(app)
label = ctk.CTkLabel(group , text="      文字コード")
label.pack(padx=10,pady=10,side="left")

# 入力ファイル文字コードコンボボックスを配置
input_encoding_name = ctk.CTkComboBox(group , values=["SHIFT-JIS","UTF-8"])
input_encoding_name.pack(padx=10,pady=10,side="left")

# ヘッダ行数の選択コンボボックスを配置
label = ctk.CTkLabel(group , text="ヘッダ行数")
label.pack(padx=10,pady=10,side="left")
input_header_cnt = ctk.CTkComboBox(group , values=["0","1","2","3","4","5"])
input_header_cnt.set("1")
input_header_cnt.pack(padx=10,pady=10,side="left")
group.pack(padx=10,pady=10,fill="x")

# フォルダ選択ダイアログを配置
folder_selection = FolderSelectionControl(app)
folder_selection.label.configure(text="保存先フォルダ名")
folder_selection.pack(padx=10,pady=10,fill="x")

# 出力ファイル文字コードコンボボックスを配置
group = ctk.CTkFrame(app)
label = ctk.CTkLabel(group , text="      文字コード")
label.pack(padx=10,pady=10,side="left")
output_encoding_name = ctk.CTkComboBox(group , values=["SHIFT-JIS","UTF-8"])
output_encoding_name.pack(padx=10,pady=10,side="left")
group.pack(padx=10,pady=10,fill="x")


# 分割行数選択コンボボックスを配置
group = ctk.CTkFrame(app)
label = ctk.CTkLabel(group , text="        分割行数")
label.pack(padx=10,pady=10,side="left")
split_count = ctk.CTkComboBox(group , values=["なし","1000","2000","5000","10000","20000","50000"])
split_count.pack(pady=10,side="left")
group.pack(padx=10,pady=10,fill="x")

# 抽出対象のキーワード入力欄を配置
group = ctk.CTkFrame(app)
is_contain = ctk.CTkCheckBox(group,text="キーワードを含む行を抽出",width=90)
is_contain.pack(padx=10,pady=10,side="left")
contain_words = EntryControl(group)
contain_words.pack(padx=10,pady=10,fill="x", expand=True)
group.pack(padx=10,pady=10,fill="x")

# 除外対象のキーワード入力欄を配置
group = ctk.CTkFrame(app)
is_not_contain = ctk.CTkCheckBox(group,text="キーワードを含む行を除外",width=90)
is_not_contain.pack(padx=10,pady=10,side="left")
not_contain_words = EntryControl(group)
not_contain_words.pack(padx=10,pady=10,fill="x", expand=True)
group.pack(padx=10,pady=10,fill="x")

# 実行ボタンの配置
split_button = ctk.CTkButton(app,text="CSVファイルの分割",height=40,width=300,command=split)
split_button.pack(padx=20,pady=10,side="top", anchor="e")


# -----------------------------------------------
# 前回実行時の画面入力値を復元
# -----------------------------------------------
file_selection.set(conf.get("file_selection",""))
input_encoding_name.set(conf.get("input_encoding_name","SHIFT-JIS"))
input_header_cnt.set(conf.get("input_header_cnt","1"))
output_encoding_name.set(conf.get("output_encoding_name","SHIFT-JIS"))
split_count.set(conf.get("split_count",""))
contain_words.set(conf.get("contain_words",""))
not_contain_words.set(conf.get("not_contain_words",""))

# CSVファイル名を復元
folder_selection.set(conf.get("csvfile"))

# 保存先フォルダの値を復元
folder_selection.set(conf.get("folder"))
if folder_selection.get() == "":
    folder_selection.set(os.getcwd())


# -----------------------------------------------
# CustomTkinterの起動 
# -----------------------------------------------

# メインループを開始
app.mainloop()

split_file.py

関数の中に、 get_new_fp とmatchという2つのローカル関数を用意しています。
get_new_fp で出力ファイル名を生成していますので、別のファイル名にしたい場合はこれを修正してください。match は 読み込んだ行が抽出対象か、除外対象化を判定していますので、もっと複雑な条件を指定したい場合は、この関数を修正してください。

import os

def split_file(input_file, output_folder, header_count=1,line_max=10000,
               input_encoding="shift-jis",output_encoding="shift-jis",valid_words="",omit_words=""):
    """
    指定されたファイルを指定された行数ごとに分割します。

    Args:
        source_file (str): 分割対象のファイルパス
        output_dir (str): 出力フォルダパス
        header_rows (int, optional): ヘッダー行数 (デフォルト: 0)
        lines_per_file (int, optional): 1ファイルあたりの最大行数 (デフォルト: 10000)
        source_encoding (str, optional): 入力ファイルのエンコーディング (デフォルト: "shift-jis")
        output_encoding (str, optional): 出力ファイルのエンコーディング (デフォルト: "shift-jis")
        valid_patterns (str, optional): 継続行として扱う文字列 (デフォルト: "")
        omit_patterns (str, optional): 除外する文字列 (デフォルト: "")

    Returns:
        None
    """

    # 出力ファイル名とファイルポインタを作成する関数
    def get_new_fp(file,no,output_folder,enc_name):
        name,ext = os.path.splitext(file)
        path = os.path.join(output_folder,os.path.basename(name))
        return  open(f"{path}_{no}{ext}", 'w', encoding=enc_name)

    # 1行の中に指定したキーワードが含まれているかをチェックする関数
    def match(line,words):
        return any(word in line for word in words.split(','))

    headers = [] # ヘッダーを格納するリスト
    file_no = 0  # 出力ファイルの連番
    line_no = 0  # 行番号
    body_count = 0 # ボディの行数カウンタ
    output_fp = get_new_fp(input_file,file_no,output_folder,output_encoding) # 出力ファイルポインタ

    with open(input_file, 'r', encoding=input_encoding) as input_fp: 
        for line in input_fp:
            # ヘッダをリストに保存する
            if line_no < header_count :
                headers.append(line)
            else :
                # 指定したキーワードが含まれていなければ読み飛ばす
                if valid_words != "" and not match(line,valid_words):
                    continue
                # 指定したキーワードが含まれていたら読み飛ばす
                if omit_words != "" and match(line,omit_words):
                    continue
                               
                # ボディの行数が分割数に達すると新しいファイルを作成
                if line_max > 0 and body_count >= line_max :
                    output_fp.close()
                    output_fp = get_new_fp(input_file,file_no,output_folder,output_encoding) 
                    # ヘッダを書き込む
                    output_fp.writelines(headers)
                    file_no += 1
                    body_count = 0
                # ボディのカウントを1つ増やす
                body_count += 1
            # ファイルに書き込む
            output_fp.write(line)
            # 行番号を1つ増やす
            line_no += 1

        if output_fp is not None:
            output_fp.close()

まとめ

今回は、「CSVファイル分割ツール」について、使い方から環境構築に至るまでの手順を解説しました。また、実際に私がこのツールを作るにあたって、事前に検討した内容も併せて紹介しました。

今回は、メモリに乗らないくらいのファイルを分割することを考慮したため、処理が少し複雑になりましたが、メモリに乗る程度のCSVファイルであれば、もっと簡単なコードで実現できます。もしよければ挑戦してみてください。

プログラムはできだけ再利用できるよう関数にしていますので、コマンドとして書き直すなり、画面を別のライブラリで作り直すなり、ご都合に合わせて再利用してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次