こんにちは。クロスマートで請求書を始めとした帳票サービスの開発を行っているDev2 テックリードのたけじい(@pouhiroshi)です。
弊社はビジネス側と開発側の距離が近く、お互いをリスペクトし合う関係性です。
先日、いつも(?)のように社内のSlackチャンネルを徘徊していると、こんな悲痛な叫び声が聞こえて来ました。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240715/20240715115646.jpg)
上記の画像はDALL-Eで生成した画像を元に、以下のアプリで吹き出しをつけました。(聞いてない)
apps.apple.comちょっとふざけすぎました。実際はこんな悩みでした。どうもGoogleスプレッドシートで作ったCSVファイルが文字化けして困るというお悩みのようです。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240716/20240716174343.png)
スプレッドシートでCSVファイルをエクスポートすると、文字コードはUTF-8で出力されるのですが、これだけだとエクセルで開いたとき文字化けしてしまいます。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240719/20240719153434.png)
エクセルで開いても文字化けしないUTF-8 CSVにするためには、ファイルに「BOM」(Byte Order Mark)と呼ばれる印みたいなものをつける必要があります。
BOMについてはこちらの記事がわかりやすいです。
qiita.com
スプレットシートのGASを使ってBOMをつける方法や、MacのターミナルでゴネゴネしてShift_JISに変換してエクセルで文字化けさせない方法もあるのですが、エンジニアではない方々にこれらの方法が取れるかというと難しいことも多いかと思います。
ですが、この悩みを見た私は気づきました。
以前、CSVファイルを分割する「CSV分割くん」(そのまんま)をすでに作っていました。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240719/20240719153923.png)
この分割くんを流用すれば、
- ファイル選択、出力するディレクトリ選択の仕組みはそのまま使えそうだな
- UIもそんなに変えなくて良いな(カッコよくはないけれど笑)
ということで、「まあ2時間もあれば作れるんじゃないかな」と、思いが熱いうちに作ってみることにしました。
(当初はそんなに困ってるとは思わず、気軽な感じで作り始めたのでした。)
作ったアプリは個人用の公開リポジトリに置いてあります。
使いたい方は自己責任でどうぞ。Mac用アプリしか用意してなかったりとか、制限はあります。
github.com
ちなみにCSV分割くんはこちらにあります。
github.com
プログラムを見ていただくとわかると思いますが、私はFlutterの実装は初心者で、プログラム言語のDartもあまり詳しくないので、あまりいいコードではないと思います(笑
ですが、同じく初心者の方にはきっと役に立つであろう、実装のポイントとなった部分(ファイル選択、ディレクトリ選択、BOM付与処理)について、説明しておこうと思います。
(開発はAndroidStudioで行いました。Flutterの開発環境構築は調べればたくさん出てくるので、今回は割愛します。)
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240716/20240716182652.png)
1. ファイルを選択するファイル選択の実装
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240719/20240719154802.png)
ファイルやディレクトリ選択には file_pickerというライブラリを使っています。
pubspec.yamlのdependenciesに
file_picker: ^4.0.0
を追加してライブラリを取得してくれば使えます。以下は、プログラムです。
import 'package:file_picker/file_picker.dart'; // ファイル選択UI部分 ElevatedButton( onPressed: executing ? null : _pickFileIsSuccess, child: Text("CSVファイル選択"), ), SizedBox(height: 8), Text("BOMを付与するCSVファイル:$fileName") //↑で呼び出しているファイル選択処理 Future<void> _pickFileIsSuccess() async { final filePickerResult = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['csv'], // 選択できるファイルの拡張子を限定できる。 ); String selectFileName = ''; if (filePickerResult != null) { pickSuccess = true; file = File(filePickerResult.files.single.path!); //UTF-8エンコーディングのチェックとBOMのチェック String? checkResult = await checkCSVFileEncoding(file); if(checkResult != null) { selectFileName = checkResult; pickSuccess = false; } else { selectFileName = filePickerResult.files.single.name; pickSuccess = true; } } else { pickSuccess = false; selectFileName = '何も選択されませんでした'; fileContents = 'ファイルの中身がここに表示されます'; } setState(() { fileName = selectFileName; }); }
allowedExtensions: ['csv']で、選択できるファイルの拡張子を限定することができます。
ファイルを選択したら、情報が返ってきますので、filePickerResult.files.singleからファイルパスやファイル名などを抽出すると良いでしょう。
2. 出力ディレクトリを選択するディレクトリ選択の実装
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240719/20240719155012.png)
ディレクトリ選択もFilePickerのgetDirectoryPath()で簡単に取得することができます。
//出力ディレクトリ選択UI部分 ElevatedButton( onPressed: executing ? null : _selectFolder, child: Text("出力ディレクトリ選択"), ), SizedBox(height: 8), Text("出力するディレクトリ:${_directoryPath ?? ""}"), // ↑で呼び出している選択処理 void _selectFolder() { FilePicker.platform.getDirectoryPath().then((value) { setState(() => _directoryPath = value); }); }
3. BOMをつける処理
ファイルを出力する際、先頭に0xEF, 0xBB, 0xBFという3つのデータをつけることでBOMをつけたことになります。
以下のように実装しました。
Future<IOSink> _createNewOutputSink() async { String outputFilePath = '$_directoryPath/utf8_bom.csv'; File outputFile = File(outputFilePath); IOSink sink = outputFile.openWrite(); // UTF-8 BOM sink.add([0xEF, 0xBB, 0xBF]); return sink; }
最初にあったエクセルで文字化けしていたcsvを開いてみると。。。。?
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240719/20240719155305.png)
アプリ化について
本来Macアプリとしてリリースするには、証明書・プロビジョンファイルなどを取得しAppleのAppStoreConnectなどを通じて、レビューなどを受け、AppStore経由で配信する必要があります。
一般的に「どこの誰が作ったかわからん」アプリを一般の方に配布するのはセキュリティ的にも好ましくありません。
ですが、今回のような社内限定の特定ニーズを満たす簡単なツールであれば、誰が作ったかはわかってるわけですし、AppStoreを通さずにアプリを渡すことができます。
FlutterでMacアプリのビルドを行うには、ターミナルで以下のようなコマンドを打ちます。
flutter build macos --release
上手くいくと、build/macos/Build/Products/Releaseのディレクトリにアプリが作成されます。 これをzipして使ってもらっています。
使う場合は、このzipを解凍して、utf8_bom_appをダブルクリックすることで使い始めることができます。
最初にセキュリティ警告「どこの誰が作ったアプリかわからない」というのが出ますが、Macのセキュリティ設定から許可することで使うことができます。
アプリ提供後の反響
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240716/20240716181010.png)
このアプリでどうやら困っていたことは解決できたようです。よかったよかった。多分、漫画のキャラに例えていただいたと思うのですが、知らなくて微妙な反応をしてしまいました。申し訳ありません。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/x/xmart_techblog/20240716/20240716181425.png)
後日、CSチーム内にもこのアプリを共有していただきました。みんなが地獄というほど困ってたらしい(助けに来るのが遅れてごめんよ・・・・というお気持ち)こんなに喜んでもらえるとは思っていなかったので、また困ってる人がいたら助けたいなというアンパ◯マンな気持ちになりました。
今後について
弊社は利用しているパソコンをMacに統一していることもあり、今回はMac向けアプリを作りましたが、FlutterはWindows向けアプリも同じコードで作ることができます。
もしWindowsユーザも弊社に増えてきても、すぐにWindows向けのアプリをビルドすることができ、Flutterの強みでもあるクロスプラットホーム開発のメリットを感じることができると思います。
(ただし、Windowsアプリをビルドするには、Windowsで開発環境を構築しビルドを行う必要があります。)
プロダクトチームにとっては、CSや営業チームも、自分たちが作ったシステムを使ってユーザと伴走する、ある種お客様のような存在です。そのお客様たちが困っていたら、自分たちの技術・経験・知識を駆使してサッと助けられる存在でありたいなと思います。
稚拙なやり方でもいいので、「ユーザの課題を素早く解決する」をモットーに、これからも社内の課題解決に軽率に貢献していきたいと思っています!
クロスマートでは、ユーザや仲間の課題を解決していきたい心優しいエンジニアやBizDevなど広く仲間を募集しています。
興味のある方はぜひご連絡ください!