また、ファイル・サイズを表示したり、画像ファイルの高さ、幅を表示する方法を紹介する。
(2024年5月25日)wait4.html の不具合修正
(2024年2月10日)記事「ファイル・サイズを表示する」「画像ファイル・サイズを表示する」「練習問題」を追加
サンプル・プログラムの実行例
目次
サンプル・プログラムを実行する
サンプル・プログラム
employeeList4.html | 社員名簿(ファイル読み書き) |
employeeList.json | 社員名簿データ(JSON) |
wait1.html | 待ち時間のサンプル・プログラム(1) |
wai2.html | 待ち時間のサンプル・プログラム(2) |
wait3.html | 待ち時間のサンプル・プログラム(3) |
wait4.html | 待ち時間のサンプル・プログラム(4) |
dispFileSize.html | ファイル・サイズを表示するサンプル・プログラム |
dispImageSize.html | 画像ファイル・サイズを表示するサンプル・プログラム |
バージョン | 更新日 | 内容 |
---|---|---|
2.1.0 | 2023/08/15 | ループ処理などを見直した |
2.0.0 | 2023/08/15 | ファイル名変更 |
1.0 | 2021/08/29 | 初版 |
ファイル・アクセスとセキュリティ
HTML5になって、ユーザーが手動で指定したファイルに限り、読み込んだり書き込むことができるようになった。
今回は、この機能を利用してローカル・ドライブにあるファイルを読んだり、書き込んだりするプログラムを作っていくことにする。
JSON
1: [{
2: "number": "社員番号",
3: "last_name": "姓",
4: "first_name": "名",
5: "sex": "性別",
6: "hire": "入社日",
7: "department": "所属部署",
8: "duties": "職掌",
9: "sales": "今期売上<br>(千円)"
10: }, {
11: "number": "094011",
12: "last_name": "相馬",
13: "first_name": "豊司",
14: "sex": "男",
15: "hire": "1994/4/1",
16: "department": "営業部",
17: "duties": "営業",
18: "sales": 52600
19: },{
20: "number": "099004",
21: "last_name": "原口",
22: "first_name": "奈緒子",
23: "sex": "女",
24: "hire": "1999/7/5",
25: "department": "人事部",
26: "duties": "事務"
27: },{
28: "number": "012023",
29: "last_name": "足立",
30: "first_name": "俊康",
31: "sex": "男",
32: "hire": "2012/3/12",
33: "department": "システム営業部",
34: "duties": "技術"
35: },{
36: "number": "004002",
37: "last_name": "畠山",
38: "first_name": "忠秋",
39: "sex": "男",
40: "hire": "2004/4/1",
41: "department": "経理部",
42: "duties": "事務"
43: },{
44: "number": "013010",
45: "last_name": "島崎",
46: "first_name": "晴生",
47: "sex": "男",
48: "hire": "2013/4/1",
49: "department": "営業部",
50: "duties": "営業",
51: "sales": 23800
52: },{
53: "number": "096008",
54: "last_name": "丹羽",
55: "first_name": "麻樹",
56: "sex": "女",
57: "hire": "1996/4/1",
58: "department": "営業部",
59: "duties": "営業",
60: "sales": 31600
61: },{
62: "number": "096011",
63: "last_name": "谷本",
64: "first_name": "房実",
65: "sex": "女",
66: "hire": "1996/4/1",
67: "department": "経理部",
68: "duties": "事務"
69: },{
70: "number": "020017",
71: "last_name": "深沢",
72: "first_name": "つばさ",
73: "sex": "男",
74: "hire": "2020/4/1",
75: "department": "システム営業部",
76: "duties": "営業",
77: "sales": 42800
78: },{
79: "number": "007005",
80: "last_name": "川島",
81: "first_name": "武久",
82: "sex": "男",
83: "hire": "2007/4/1",
84: "department": "営業部",
85: "duties": "営業",
86: "sales": 32700
87: },{
88: "number": "091001",
89: "last_name": "成田",
90: "first_name": "祥三",
91: "sex": "男",
92: "hire": "1991/4/1",
93: "department": ["開発部", "システム営業部"],
94: "duties": "技術"
95: }]
HTML部分
252: <body>
253: <h2 id="title"></h2>
254:
255: 名簿ファイル
256: <input type="file" name="infile" id="infile" onChange="loadAndDispTable(this)" />
257: <input type="hidden" name="success" id="success" value="" />
258:
259: <table id="tableEmployeeList"></table>
260: <a id="save" href="#" onClick="saveTable()"></a><br />
261: <p id="error"></p>
ファイルへの書き込みは、<a id="save"> タグを用いる。リンクがクリックされたとき、後述するユーザー関数 saveTable を呼び出す。
ファイル読み込み
160: /**
161: * 社員名簿データを読み込む.
162: * @param Object filelist 社員名簿データ・ファイル(filelistオブジェクト)
163: * @return Object 社員名簿データ
164: */
165: function loadAndDispTable(filelist) {
166: //表示クリア
167: reset();
168:
169: //ファイルが複数ある場合を想定
170: Array.from(filelist.files).forEach(function (file) {
171: //FileReaderオブジェクト生成
172: let reader = new FileReader();
173:
174: //読み込みエラー処理
175: reader.addEventListener('error', () => {
176: document.getElementById('error').innerHTML = 'エラー:社員名簿ファイルの読み込みに失敗しました.';
177: return;
178: });
179:
180: //JSONファイル以外はエラー
181: if (! file.type.match(/json/i)) {
182: document.getElementById('error').innerHTML = 'エラー:正しい社員名簿を用意してください.';
183: return;
184: }
185:
186: //ファイル読み込み
187: reader.readAsText(file);
188:
189: //読み込み完了後の処理
190: reader.onload = function () {
191: let persons = JSON.parse(reader.result); //JSON→配列変換
192: document.getElementById('tableEmployeeList').innerHTML = getTable3(persons); //一覧表示
193: document.getElementById('save').innerHTML = '社員一覧表保存';
194: }
195: });
196: }
ここで、FileListオブジェクトは配列でないことに留意したい。プロパティ files から fromメソッドを使って新たに配列を生成し、この生成された配列に対して forEachメソッド を適用してやる。
ファイル読み込みで肝になるのは FileReader オブジェクトである。FileReaderオブジェクトを使うと、ローカル・ドライブのファイルを非同期に読み取ることができる。
テキストファイルを読み込むには、readAsText メソッドを使うだけでいい。引数にはファイル名――fileオブジェクトで渡された filelist.files[i] を渡せばいい。
読み込んだテキストは、result プロパティを使って取り出す。
ファイル読み込み時に発生するエラーは、error プロパティにイベントハンドラを設定することで処理する。ここでは無名関数を用意し、エラー・メッセージを表示し、loadAndDispTable 関数を強制終了するようにした。
読み込んだファイルが JSON形式かどうかは、typeプロパティに "application/json" がセットされているかどうかを見れば分かる。ここでは正規表現のパターンマッチングで判断し、JSON でなければエラー・メッセージを表示し、このあとの処理をスキップする。
非同期処理
C言語などでテキスト・ファイルを1行ずつ読み込んで処理するフローは左図の通り。処理する都度、エラー・チェックを行い、ファイル終端かどうかを調べて繰り返し処理する。このように処理フローを1本の流れで表せる場合を同期処理と呼ぶ。
そこで、ファイル入出力が行われている間、メイン処理はできる限り先へ進めてしまおうという考え方である。
ファイル処理が終わったら、非同期処理をメイン処理に合流させなければならない。
そこでJavaScriptの場合、FileReaderオブジェクトに onload プロパティが用意されており、ここにファイル読み込み終了後のイベントを関数として登録することで、メイン処理と足並み合わせをすることができるようになっている
ここでは、読み込んだ内容を result プロパティから取り出し、JSON.parse メソッドを使って配列 directory に代入。それを、以前作った make_table2 関数で一覧表示させるとともに、社員一覧表保存ボタンを表示するようにした。
ファイル書き込み
141: /**
142: * 社員名簿をテキストファイルで保存する.
143: * @param なし
144: * @return なし
145: */
146: function saveTable() {
147: //一覧表部分を取得
148: let html = document.getElementById('tableEmployeeList').innerHTML;
149:
150: //Blobオブジェクト生成
151: let blob = new Blob([html], {type:"text/plan"});
152: let link = document.getElementById('save');
153: link.href = URL.createObjectURL(blob);
154: link.download = 'employeeList.txt'; //保存ファイル名
155:
156: //ファイル保存
157: link.click();
158: }
ファイル書き込みの肝は、Blob オブジェクトである。コンストラクタの引数は、書き込みたいデータを配列にしたものと、データ型の2つである。
Blobオブジェクトが生成できたら、それを、URL.createObjectURL メソッドに渡す。すると、Blobオブジェクトは DOMString に変換される。これをaタグのhref属性に代入することで、aタグをクリックしたときに渡したデータをダウンロードできるようになる。ファイル名はdownload属性に代入しておく。
待ち時間処理
22: <script>
23: document.getElementById('ss').innerHTML += '1番目<br>';
24: document.getElementById('ss').innerHTML += '2番目<br>';
25: document.getElementById('ss').innerHTML += '3番目<br>';
26: </script>
ところが、JavaScript には、他の言語によくある wait のような待ち時間命令がない。
似たような働きをする setTimeoutメソッドがあるので、これを使ってみることにする。
22: <script>
23: document.getElementById('ss').innerHTML += '1番目<br>';
24: //1秒待つ
25: setTimeout(function() {
26: document.getElementById('ss').innerHTML += '2番目<br>';
27: }, 1000);
28: document.getElementById('ss').innerHTML += '3番目<br>';
29: </script>
このように JavaScriptの関数やメソッドは、だいたいが非同期処理であるため、単純に1秒待つという命令が存在しない。
では、どうしたらいいか‥‥。
22: <script>
23: document.getElementById('ss').innerHTML += '1番目<br>';
24:
25: //1秒待つ
26: new Promise(function(resolve) {
27: setTimeout(function() {
28: document.getElementById('ss').innerHTML += '2番目<br>';
29: resolve();
30: }, 1000);
31: }).then(function() {
32: document.getElementById('ss').innerHTML += '3番目<br>';
33: });
34: </script>
22: <script>
23: /***
24: * 指定した秒数だけwaitする.
25: * @param Number sec waitしたい秒数
26: * @return なし
27: */
28: function wait(sec) {
29: return new Promise(resolve => {
30: setTimeout(function() {
31: resolve();
32: }, Math.trunc(sec * 1000)) //秒→ミリ秒変換
33: });
34: }
35:
36: //メインプログラムをasyncにする.
37: (async () => {
38: document.getElementById('ss').innerHTML += '1番目<br>';
39: await wait(1); //1秒待つ
40: document.getElementById('ss').innerHTML += '2番目<br>';
41: document.getElementById('ss').innerHTML += '3番目<br>';
42: })();
43: </script>
46: /**
47: * ファイル・サイズを表示する
48: * @param Object filelist ファイルリスト(filelistオブジェクト)
49: * @return なし
50: */
51: function dispFileSize(filelist) {
52: //表示クリア
53: reset();
54:
55: //ファイルが複数ある場合を想定
56: Array.from(filelist.files).forEach(function (file) {
57: let fileSize = file.size;
58: document.getElementById('fileSize').innerHTML = fileSize.toLocaleString(); //数字のカンマ区切り
59: return; //先頭ファイルを表示したらリターン
60: });
61: }
47: /**
48: * 画像の縦横サイズを表示する
49: * @param Object filelist ファイルリスト(filelistオブジェクト)
50: * @return なし
51: */
52: function dispImageSize(filelist) {
53: //表示クリア
54: reset();
55:
56: //ファイルが複数ある場合を想定
57: Array.from(filelist.files).forEach(function (file) {
58: //FileReaderオブジェクト生成
59: let reader = new FileReader();
60:
61: //読み込みエラー処理
62: reader.addEventListener('error', () => {
63: document.getElementById('error').innerHTML = 'エラー:ファイルの読み込みに失敗しました.';
64: return;
65: });
66:
67: //imageファイル以外はエラー
68: if (! file.type.match(/image/i)) {
69: document.getElementById('error').innerHTML = 'エラー:画像ファイルを指定してください.';
70: return;
71: }
72:
73: //画像ファイルを読み込む
74: reader.readAsDataURL(file);
75:
76: //読み込み完了後の処理
77: reader.onload = function () {
78: let image = new Image();
79: image.src = reader.result;
80: //画像をデコードする
81: image
82: .decode()
83: .then(() => {
84: document.getElementById('width').innerHTML = image.width.toLocaleString(); //数字のカンマ区切り
85: document.getElementById('height').innerHTML = image.height.toLocaleString(); //数字のカンマ区切り
86: });
87: }
88: });
89: }
FileReader に画像ファイルを読み込むには、readAsDataURLメソッドを使う。読み込みは非同期に行われるので、onload を使って、読み込んだ画像ファイルを Imageオブジェクトに流し込む。
流し込み(decodeメソッド)も非同期で行われるので、thenメソッドを使って、次の処理を書く。ここで、weidthプロパティ、heightプロパティを参照する。
練習問題
コラム:同期と非同期
つまり、非同期処理の目的は、複数のタスクを分業してスムーズに品質の高いアウトプットを得ることにある。逆に考えれば、タスクが1つであったり、スピードが求められない仕事を非同期処理にする必要はない。
工場内では在庫の食材を加工して弁当を製造する。醤油やソースといった調味料を製造するには別の工場が必要になるから、これは専門業者から調達しているとする。この場合、専門業者に外注するタスクは自社工場とは独立して動いているから非同期処理である。餅は餅屋――専門の調味料製造会社から調達した方が、弁当の味(品質)を保つことができる。
最後に、調味料を弁当に加えて1つの弁当として検査して梱包、出荷するところが同期処理である。
このとき、自社工場の製造が早すぎても、調味料の搬入が遅すぎても、出荷までの待ち時間が発生してしまう。最初に行う製造計画では、同期処理で足並み合わせができるよう綿密な計画を立てる。この部分がプログラム設計にかかわる。
デバイス・プログラムの開発経験がある方は、割り込み処理を書いたことがあるだろう。
IoT時代においても、同期・非同期処理は付きまとう。
たとえばドローンのセンサや駆動系デバイスは非同期処理で動いており、メイン処理が適切に足並み合わせを行いながら制御しないと、たちまちコントロールを失ってしまう。
マルチスレッド処理も非同期処理の延長にある。スーパーコンピュータのプログラミングも非同期処理の延長だ。
大量のデータ処理を行う中で、後に投入したスレッドの処理が軽く、“たまたま”先に投入したスレッドより速く終わってしまった場合、きちんと同期処理が行われていないと、後続のスレッド処理でエラーが発生したり、後勝ちでデータを書き換えてしまう恐れがある。
繰り返し処理の中でオブジェクト配列を変更したり、データベース処理する場合には注意が必要だ。とくにデータベース処理には時間がかかるため、開発環境では正しい順序に処理されたとしても、実運用環境では順序が前後したりする。トランザクションが多い金融や行政のネットサービスでは注意が必要だ。
「5.1 配列とオブジェクト」で作った社員名簿データを JSON形式ファイルとして保存しておき、これを読み込んだり、一覧表のHTML部分をファイルに書き込むことを目指す。ファイル読み込みは非同期で処理が行われることに留意すること。