【そのまま使えるサンプルコードあり】JavaScriptでローカルファイルの読み込み、書き出しを行う

ローカルファイルの読み込みと書き出しを行うソフトというだけなら言語なんか選び放題なのですが、最近個人的にどうしてもこれをJavaScriptで実現しなければならない状況にぶち当たりました。

その時のコードと注意点についてまとめた解説、備忘録です。

実装サンプル

細かい話は後に回すとして、以下はまるっとコピーするだけで使えるソースコードです。TXTファイルに対応させていますが、同じ手法でCSVやXMLの読み込み、書き出しも可能です。

※jQueryを使用しているため、実行にはインターネット接続かjQueryのダウンロードが必要です。

main.html

<!DOCTYPE html>
<html lang="ja">

<head>
<meta charset="utf-8">
<title>Read & Write Local File</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div id="container">
<main>
    <div>
        <p id="message"></p>
        <input id="save" type="button" value="Save">
        <input id="load" type="button" value="Load">
    </div>
    <textarea id="log">
    </textarea>
</main>
</div>
</body>
</html>

script.js

//alert("script.js successfully loaded");

$(function () {
  const pickerOpts = {
    types: [
      {
        description: 'Texts(.txt)',
        accept: {
          'text/*': ['.txt']
        }
      }
    ],
    multiple: false,
  }

  let fileReader = new FileReader();
  let start = 0;
  let end = 0;
  $("#load").click(async function () {

    [fileHandle] = await window.showOpenFilePicker(pickerOpts);

    $("#message").html("Loading...");
    start = performance.now();			//track start
    const file = await fileHandle.getFile();
    const fileContents = await file.text();
    console.log(fileContents);
    $("#log").val(fileContents);

    end = performance.now();			//track end
    const time = (end - start) | 0;
    $("#message").html("Data is successfully loaded in " + (time / 1000) + "s");
  });

  $("#save").click(async function () {
    let fileHandle = await window.showSaveFilePicker(pickerOpts);
    const writable = await fileHandle.createWritable();
    await writable.write($("#log").val());
    await writable.close();
  });

});

解説

外部ファイルを読み込んでTextareaに表示、また逆の手順で保存を行うことができます。

読み込み

[fileHandle] = await window.showOpenFilePicker(pickerOpts);

Window.showOpenFilePicker() メソッドでファイル選択ダイアログを表示します。戻り値の型は FileSystemFileHandle の Arrayです。配列になっているのは一度に複数のファイルを読み込む可能性があるためです。このコードではオプションで複数選択を拒否しています。ユーザーの応答を待つため await にします。

const file = await fileHandle.getFile();
const fileContents = await file.text();

FileSystemFileHandle.getFile() メソッドでFileオブジェクトを取得し、File.text() で文字列型に変換します。これでファイルの内容を自由に使えます。

書き出し

let fileHandle = await window.showSaveFilePicker(pickerOpts);

Window.showSaveFilePicker() メソッドで保存用のダイアログを表示します。読み込み時とほぼ同じですが、戻り値はFileSystemFileHandleで、配列ではありません(読み込み時と違い一度に複数のファイルを選択できないため)。

const writable = await fileHandle.createWritable();

FileSystemFileHandle.createWritable() メソッドで、FileSystemWritableFileStreamオブジェクトを生成します。これを介してファイルへの書き込みが可能になります。

await writable.write($("#log").val());
await writable.close();

ファイルに書き込みたい内容をwrite() で書き込み、FileSystemWritableFileStreamオブジェクトを閉じつつ保存します。最後のclose() を忘れないようにしてください。

注意点

このコードを編集していて引っかかった部分についてです。

文字化け(エンコードの変更)

読み込むファイルが日本語のファイルだと、多くの場合ファイルのエンコード方法がShift_JISになっていて、読み込んだ時に文字化けが発生します。UTF-8で保存しておけばとりあえずこの問題は回避できます。

拡張子の指定(書き出し時)

Window.showSaveFilePicker() メソッドは引数を省略しても動きますが、その場合デフォルトで拡張子を指定しないため、ユーザーが拡張子を入力しないといけなくなります。ここは指定しておいた方が無難です。

実行時間(処理速度)

サンプルソースでも読み込みの処理速度は表示するようになっていますが、5MBのテキストファイルを0.1s程度で読み込めるので、大規模データを扱わない限りは問題ないと思います。

セキュリティ上の制約

毎回同じファイルを読み込む場合や、ログファイルのようなものを勝手に出力したい場合など、パスをコード上で指定して勝手に読み込みや書き出しをしてもらうことを考えたくなりますが、JavaScriptではそのような操作は許可されておらず、必ずダイアログを介してユーザーの操作を要求します。そうしないと、開いた瞬間にウイルスを勝手にダウンロードさせるような悪意のあるhtmlが作り放題になってしまうためです。

この制約があるため、そもそもJavaScript(HTML)はローカルファイルの入出力を行うようなプログラムを組むのに根本的に向いていないと思われます。JavaなりC#なり、なんでもいいから他の言語を使うべきなのでしょう。