サーバー上にあるcsvファイルからデータを読み込み、Wordpress上に4択問題を出題するWebアプリをhtmlとjavascriptで作ったので紹介する
WordPressテーマ:Cocoon
サーバー:ConohaWing
FTPソフト:FFFTP
はじめに
サーバー上にあるcsvファイルからデータを読み込み、Wordpress上に4択問題を出題するWebアプリをhtmlとjavascriptで作った
↓これ
試験対策ノートはWeb上に作る時代になった
スマホさえあればいつでもどこでも勉強ができるし、本格的なスマホアプリよりははるかに簡単にできるので、興味がある人はぜひ挑戦してみてほしい
↓参考文献
javascriptを初めて触った筆者が2日で上記のWebアプリを作れるようになった良書
ある程度ほかのプログラミング言語を触ったことある人にもおすすめ
CSVファイルの準備
まず、以下に示すようなcsvファイルを準備する
Label,Year,Month,Subject,No,QuestionState,Answer,Figure,Option1,Option2,Option3,Option4
学科試験例題集 航空法規(P28),2024,8,航空法規(P28),問1,航空法第2条(定義)で定める「航空業務」の内容で誤りはどれか。,2,-,(1)航空機に乗り組んで行う無線設備の操作 ,(2)運航管理の業務 ,(3)航空機に乗り組んで行うその運航 ,(4)整備又は改造をした航空機について行う法で定める範囲の確認
学科試験例題集 航空法規(P28),2024,8,航空法規(P28),問10,航空法施行規則第5条の4(飛行規程)で定める飛行規程に記載する事項について誤りはどれか。,1,-,(1)航空機の構造 ,(2)非常の場合にとらなければならない各種装置の操作その他の措置 ,(3)航空機の限界事項 ,(4)航空機の概要
学科試験例題集 航空法規(P28),2024,8,航空法規(P28),問11,航空機に装備する救急用具の点検期間で誤りはどれか。,2,-,(1)非常信号灯: 60 日 ,(2)救命胴衣、これに相当する救急用具及び救命ボート: 60 日 ,(3)救急箱: 60 日,(4)携帯灯: 60 日
学科試験例題集 航空法規(P28),2024,8,航空法規(P28),問12,航空法第71条の2(操縦者の見張り義務)の説明で誤りはどれか。,3,-,(1)レーダーサービスを受けている場合にも見張りの義務はある。 ,(2)雲が多い所を飛行中にも見張りの義務はある。 ,(3)当該航空機外の物件を視認できない気象状態の場合にも見張りの義務はある。 ,(4)夜間飛行中にも見張りの義務はある。
学科試験例題集 航空法規(P28),2024,8,航空法規(P28),問13,航空法第71条の3(特定操縦技能の審査等)に関する説明で誤りはどれか。,4,-,(1)特定操縦技能の審査とは、航空機の操縦に従事するのに必要な知識及び能力であってその維持について確認することが特に必要であるものを有しているかどうかについて操縦技能審査員が行う審査である。,(2)この審査に合格していなければ、航空機に乗り組んで、その操縦に従事することはできない。 ,(3)この審査に合格し操縦を行うことができる期間は、国土交通大臣が許可した場合を除き 2 年である。 ,(4)この審査は、口述審査のみで行うことができるものとする。
学科試験例題集 航空法規(P28),2024,8,航空法規(P28),問14,航空法施行規則第164条の15(出発前の確認)で定める機長が出発前に確認しなければならない事項で該当しないものはどれか。,2,-,(1)当該航空機及びこれに装備すべきものの整備状況 ,(2)航空機が滑空機を曳航する場合の安全上の基準 ,(3)当該航行に必要な気象情報 ,(4)積載物の安全性
:
question.csv
各列の説明は以下の通り
Label | 試験問題を分類するためのラベル |
Year, Month, Subject | LabelやFigureを作るために使うダミー列 (javascriptのプログラムでは使わない) |
No | 問題番号 |
QuestionState | 問題文 |
Answer | 正答 |
Figure | 問題に付随する図 |
Option1 ~ Option4 | 選択肢 |
このフォーマットを使いまわせば、csvファイルを作り直すだけでいろいろな問題を出すWebアプリを簡単に量産することができる
HTMLとCSS
今回使ったHTMLはこれ
<label for="options">出題範囲を選択してください:</label>
<select id="options" name="options">
<option value="option1">全科目シャッフル</option>
<option value="option2">航空気象(P22)シャッフル</option>
<option value="option3">航空工学(P26)シャッフル</option>
<option value="option4">空中航法(P49)シャッフル</option>
<option value="option5">航空法規(P28)シャッフル</option>
</select>
<p id="Label"></p>
<p id="No"></p>
<p id="QuestionState"></p>
<p id="Figure"></p>
<div><button id="Option1"></button></div>
<div><button id="Option2"></button></div>
<div><button id="Option3"></button></div>
<div><button id="Option4"></button></div>
<p></p>
<button id="Next" disabled>次の問題へ</button>
<button id="Unknown">分からない</button>
<p id="Answer"></p>
<script src="wp-content/uploads/ppl-hcg-quiz/question.js"></script>
最後の<script src=".../question.js"></script>
の行で、この記事で使用するjavascriptのファイルをサーバーから読み込んでいる
CSSはこれ
この記事にだけCSSを適用させるために<style type="text/css">
を使ってHTMLファイルの中に書き込むようにした
<style type="text/css">
button {
background-color: white;
text-align: left;
font-size: inherit; /* 本文と同じフォントサイズに設定 */
line-height: 1.2; /* 行の高さを文字の高さの1.2倍に設定 */
padding: 0.5em; /* ボタン内の余白を設定 */
}
/* ボタンの横幅を揃える */
button[id^="Option"] {
display: inline-block;
width: 100%;
box-sizing: border-box;
}
button#Next {
background-color: white;
width: auto; /* 横幅を自動設定 */
}
button#Unknown {
background-color: white;
width: auto; /* 横幅を自動設定 */
}
</style>
Javascript
今回使ったJavascriptのコードはこれ
// CSVデータを非同期に取得する関数
async function csv_data(dataPath) {
const response = await fetch(dataPath); // dataPathにあるデータを非同期で取得 (この行の処理が終わるまで次の処理に進まない)
const text = await response.text(); // 取得したデータを非同期でテキストとして読み込む (この行の処理が終わるまで次の処理に進まない)
return csv_array(text); // csv_array関数を呼び出して返値を返す
}
// CSVデータを読み込んで配列に変換する & プルダウンリストにラベルを追加する関数
function csv_array(data) {
// CSVデータを読み込んで配列に変換する
const dataArray = data.split('\n').map(row => row.split(',')); // 改行で分割し、各行をカンマで分割
const questionList = dataArray.slice(1).map(row => ({ // csvファイルのデータを連想配列のリストに格納
Label: row[0],
No: row[4],
QuestionState: row[5],
Answer: row[6],
Figure: row[7],
Option1: row[8],
Option2: row[9],
Option3: row[10],
Option4: row[11]
})).filter(question => question.Label); // 'Label'が空でない要素のみをフィルタリング
// 重複のないラベルリストを作成してプルダウンに追加
const selectElement = document.getElementById('options'); // プルダウン要素を取得
const label_list = [...new Set(questionList.map(question => question.Label))]; // 重複のないラベルを取得
label_list.forEach((label, i) => {
const option = document.createElement('option'); // 新しいプルダウン要素を作成
option.value = `option${i + 5}`;
option.text = label;
selectElement.appendChild(option); // プルダウン要素に追加
});
return questionList; // 問題リストを返す
}
// 問題を更新する関数
function update_question(n, questionList) {
// すべてのkeyに対する処理
Object.keys(questionList[n]).forEach(key => {
if (key !== 'Answer' && key !== 'Figure') { // AnswerとFigure以外の要素の更新
document.getElementById(key).textContent = questionList[n][key]; // 各要素を更新
} else if (key === 'Figure' && questionList[n]['Figure'] !== '-') { // Figureがある場合は更新
const imgElement = document.createElement('img'); // img要素を作成
imgElement.src = 'wp-content/uploads/ppl-hcg-quiz/fig/' + questionList[n][key];
imgElement.alt = 'Figure';
document.getElementById(key).innerHTML = ''; // 既存の内容をクリア
document.getElementById(key).appendChild(imgElement); // 画像を追加
} else { // Answerを空にする (Figureがない場合はFigureも空にする)
document.getElementById(key).textContent = '';
}
});
}
// 配列をシャッフルする関数
function shuffle(array) {
// 重複している問題を削除
const uniqueArray = array.filter((item, index, self) =>
index === self.findIndex((t) => (
t.QuestionState === item.QuestionState && t.Answer === item.Answer
))
);
return uniqueArray.sort(() => Math.random() - 0.5);
}
//csv_data('wp-content/uploads/ppl-hcg-quiz/question.csv').then(questionList => { // CSVデータを取得して処理を開始
//csv_data('http://localhost:8000/question.csv').then(questionList => {
async function main() {
const questionList = await csv_data('wp-content/uploads/ppl-hcg-quiz/question.csv'); // CSVデータを非同期で取得 (この行の処理が終わるまで次の処理に進まない)
const nextButton = document.getElementById('Next'); // Nextボタンを取得
const optionButtons = document.querySelectorAll('button:not(#Next)'); // Nextボタン以外のボタンを取得
let selected_questionList = shuffle(questionList); // 問題リストをシャッフル
let n = 0; // 問題番号の初期化
update_question(n, selected_questionList); // 最初の問題を表示
// プルダウンメニューが変更されたときの処理
document.getElementById('options').addEventListener('change', function() {
nextButton.disabled = true; // Nextボタンを無効化
optionButtons.forEach(button => {
button.style.backgroundColor = 'white'; // 背景色を白にリセット
button.style.color = 'black'; // 文字色を黒にリセット
});
const selectedOptionText = this.options[this.selectedIndex].text; // プルダウンのテキストを取得
// プルダウンのラベルに基づいて問題をフィルタリングして取得
selected_questionList = selectedOptionText.includes('全科目') ? shuffle(questionList.filter(question => question.Label))
: selectedOptionText.includes('シャッフル') ? shuffle(questionList.filter(question => question.Label.includes(selectedOptionText.replace('シャッフル', ''))))
: questionList.filter(question => question.Label === selectedOptionText);
n = 0; // 問題番号をリセット
update_question(n, selected_questionList); // 問題を更新
//console.log(selected_questionList)
});
// 選択肢ボタンがクリックされたときの処理
optionButtons.forEach(button => {
button.addEventListener('click', function() {
// nextButtonが無効化されているときしか選択肢ボタンをクリックできない
if (nextButton.disabled) {
const correctAnswer = `Option${selected_questionList[n].Answer}`; // 正解の選択肢を取得
this.style.backgroundColor = this.id === correctAnswer ? 'blue' : 'red'; // 正解なら青、不正解なら赤に変更
this.style.color = 'white'; // 文字色を白に変更
document.getElementById('Answer').textContent = this.id === correctAnswer ? '正解' : '不正解'; // 正解なら青、不正解なら赤に変更
document.getElementById(correctAnswer).style.backgroundColor = 'blue'; // 正解の選択肢を青に変更
document.getElementById(correctAnswer).style.color = 'white'; // 文字色を白に変更
nextButton.disabled = false; // Nextボタンを有効化 -> 選択肢ボタンはクリックできなくなる
}
});
});
// Nextボタンがクリックされたときの処理
nextButton.addEventListener('click', function() {
nextButton.disabled = true; // Nextボタンを無効化 -> 選択肢ボタンがクリックできるようになる
optionButtons.forEach(button => {
button.style.backgroundColor = 'white'; // 背景色を白にリセット
button.style.color = 'black'; // 文字色を黒にリセット
});
n = (n + 1) % selected_questionList.length; // 次の問題に移動
update_question(n, selected_questionList); // 問題を更新
});
};
// ここからが実際に実行される処理
// メイン関数を実行
main();
フローチャートは以下の通り
ファイルの置き場所
これらのHTML(とCSS)はWordpressのカスタムHTMLブロックの中に書き込む
Javascriptのファイルは拡張子を.jsで保存してFFFTPでサイトのサーバー上の任意の場所に保存する(今回は/public_html/mtkbirdman.com/wp-content/uploads/ppl-hcg-quiz
)
窓の杜にアクセスして,FFFTP(64bit版)をダウンロードする
ダウンロードした「ffftp-v5.7-x64.msi」をダブルクリックしてインストールする
「次へ」
「次へ」
「インストール」
すぐに終わる
「完了」
Windowsキーを押して検索窓に「ffftp」と入力し,出てきたFFFTPを実行する
「Host List>New Host...」をクリックする
「Profile Name」「Host Name/Address」「Username」「Password/Phrase」を入力する
Profile Nameは任意の文字列,残り3つの文字列はConohaWingのコントロールパネルから確認できる
ホストを追加したら左下の「Connect」をクリックする
左の窓にはアップロードしたいファイルがあるローカルのパスを入力する
右の窓にはConohaWing上の任意のパスを入力する
左の窓で3つのファイルを選択し,右の窓にドラッグ&ドロップする
ファイルがアップロードされる
以上でConohaWingへのアップロードは完了である
ファイルの保存場所に応じて、HTMLファイルの以下の行を書き換える
<script src="wp-content/uploads/ppl-hcg-quiz/question.js"></script>
ConohaWingのセキュリティ設定の変更
ConohaWingのデフォルトの設定だと、Javascriptでサーバー上のcsvファイルを読みに行く行為がサイトへの攻撃だと判断されてブロックされてしまう
ConohaWingのコントロールパネルにログインし、サイト管理>サイトセキュリティ>WAF からcsvファイルへのアクセスを除外する
おわりに
サーバー上にあるcsvファイルからデータを読み込み、Wordpress上に4択問題を出題するWebアプリをhtmlとjavascriptで作った
工学系の資格は選択式の過去問暗記ゲーが多いので、このようなアプリは試験対策に非常に有効だと思う
電気工事士などのメジャー資格はすでにどこかの誰かが作ってくれているが、誰も作っていない場合は自分で作ってしまおう
自分のためだけでなく、後に続く後輩たちの役に立つかもしれない
コメント