僕のProofReading API　～VBAマクロ撲滅計画Phase#2 ～

# 導入
[前回](https://qiita.com/SSKNOK/items/4e859a6e8d1aae8e24b0)に引き続き、VBAマクロを撲滅するために何か使えるものはないかな～とか探っております。
基本的にはJSとPythonあたりで何かうまいことできないかなと。

今回はProofReading APIを使って、ドキュメント内容を校正するツールを作りたいと思います。

あと、地味に今回のツール作成を通して画面周りで学びが多かったです。クロスドメインとかJSONPとか。（フロント周り弱くてすみません）

# そもそもProofReading APIとは
[ProofReading API](https://a3rt.recruit.co.jp/product/proofreadingAPI/)を見ていただければ、手っ取り早いですが、「リクルートテクノロジーズが無償で公開しているAPIでAIを使って日本語文章の校正（校正が必要そうな箇所を指摘）するAPI」です。

# 作成したソース
結論、作成したツールのソースは以下の通りです。
メインのHTMLファイル以外は割愛します。割愛したjsの中でやってることもあんまり大したことないので、、、（サブのソースのうちいくつかは後述します）

処理の内容としてはエクセルファイルの中身を読み込んで、各セルの値をProofReading APIにリクエストとして飛ばすとただそれだけです。

<details><summary>document_froofreading_tool</summary>

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

<head>
	<title>Proofread Document Tool</title>
	<meta charset="UTF-8">
	<link rel="preconnect" href="https://fonts.googleapis.com">
	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
	<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@300&display=swap" rel="stylesheet">
	<style>
		* {
			font-family: 'Noto Serif JP', serif;
		}

		body {
			text-align: center;
		}

		h1.tool-title {
			color: rgb(57, 57, 57);
			padding-left: 1em;
			font-weight: 900;
		}

		h2 {
			color: rgb(57, 57, 57);
			margin-top: 2em;
		}

		.note {
			width: 50%;
			text-align: center;
			margin-left: auto;
			margin-right: auto;
		}

		button {
			margin-top: 1em;
			background-color: darkgray;
			border-radius: 10px;
			border-color: rgb(40, 40, 40);
			color: #fff;
			padding: 5px 30px;
			font-size: medium;
		}

		.table-area {
			text-align: center;
			margin-left: auto;
			margin-right: auto;
		}

		table {
			width: 80%;
		}
	</style>
</head>

<body>
	<div id="app">
		<h1 class="tool-title">Proofread Document Tool</h1>
		<h2>NOTE</h2>
		<div clsss="note">
			<p>Excelファイルにのみ対応しています。</p>
		</div>
		<h2>INPUT</h2>
		<input type="file" id="excelFile" /><br>
		<button v-on:click="doFormat">EXECUTE</button>
		<h2>OUTPUT</h2>
		<p v-if="cellValueList.length === 0">チェック開始前です。</p>
		<p v-else-if="cellValueList.length > apiResultList.length">チェック中です。</p>
		<p v-else>チェックが完了しました。</p>
		<div class="table-area">
			<table align="center" border="1" v-show="apiResultList.length > 0">
				<tr>
					<th>Sheet</th>
					<th>Cell</th>
					<th>Status</th>
					<th>Error</th>
					<th>Checked Sentence</th>
				</tr>
				<tr v-for="result in apiResultList" v-on:key="result.resultId">
					<td>{{ result["sheetName"] }}</td>
					<td>{{ result["cellAddress"] }}</td>
					<td>{{ result["status"] }}</td>
					<td>{{ result["message"] }}</td>
					<td v-html="result.checkedSentence"></td>
				</tr>
			</table>
		</div>

	</div>
</body>
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
	integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="./logics/checkExtension.js"></script>
<script src="./logics/getAllCellAddressAndValue.js"></script>
<script src="./logics/ProofReadingAPIInfo.js"></script>
<script src="./logics/decorateCheckedSentence.js"></script>
<script>
	// ロードされ、Vueがグローバル変数として定義されているか確認
	console.assert(typeof Vue !== 'undefined');
	// ファイル不正メッセージ
	const FILE_FORMAT_ERROR_MSG = "ファイルの拡張子が対応していない拡張子です。";

	new Vue({
		el: '#app',

		data: {
			// 校正対象文言リスト{シート名, セルアドレス, 値}のフォーマットで格納
			cellValueList: [],
			// APIリクエストパラメータ
			param: {},
			// APIレスポンス結果格納リスト{シート名, セルアドレス, errorFlag, status, message, checkedSentence}
			apiResultList: [],
			// test
			requestCouter: 0
		},

		methods: {
			/**
			 * 「チェック開始」ボタン押下時のイベント
			*/
			doFormat: function () {
				const vm = this;
				vm.errorList = [];

				// 拡張子チェック
				const fileName = document.getElementById("excelFile").files[0].name;
				if (checkExtension(fileName) === false) {
					alert(FILE_FORMAT_ERROR_MSG);
					return;
				}

				vm.fileName = fileName;

				// ファイル内容取得および整形の実処理部分
				let fileReader = new FileReader();
				fileReader.onload = function (event) {
					// ファイル内容取得
					let uint8 = new Uint8Array(event.target.result);
					let workBook = XLSX.read(uint8, { type: "array" });

					// 全てのシートの値の入ったセルのアドレスと値を取得
					vm.cellValueList = getAllCellAddressAndValue(workBook);


					// ProofReading APIリクエスト準備
					let proofReadingAPIInfo = new ProofReadingAPIInfo();
					// API URL取得
					let url = proofReadingAPIInfo.getUrl();
					// APIキーをリクエストに設定
					vm.param.apikey = proofReadingAPIInfo.getApiKey();

					for (let cellValueObj of vm.cellValueList) {
						// リクエストパラメータ追加
						vm.param.sensitivity = proofReadingAPIInfo.getSensitivity();
						vm.param.sentence = cellValueObj.cellValue;

						// APIリクエスト実行
						$.ajax({
							type: 'post',
							url: 'https://api.a3rt.recruit.co.jp/proofreading/v2/typo',
							dataType: 'jsonp',
							jsonpCallback: 'callback',
							data: vm.param,

						}).done(function (json) {
							vm.apiResultList.push({
								sheetName: cellValueObj.sheetName,
								cellAddress: cellValueObj.cellAddress,
								errorFlag: false,
								resultId: json.resultID,
								status: json.status,
								message: json.message,
								checkedSentence: decorateCheckedSentence(json.checkedSentence)
							});
						}).fail(function (jqXHR, textStatus, errorThrown) {
							console.log("-------------------------------------------------------------------");
							console.log(errorThrown);
							console.log("-------------------------------------------------------------------");
							vm.apiResultList.push({
								sheetName: cellValueObj.sheetName,
								cellAddress: cellValueObj.cellAddress,
								errorFlag: true,
								resultId: "error",
								status: "-",
								message: "error was happened",
								checkedSentence: null
							});
						});

					}

				}

				// ファイル読み込み
				let file = document.getElementById("excelFile").files[0];
				fileReader.readAsArrayBuffer(file);
			},

		}
	});
</script>

</html>
```
</details>

# ソース各部について

## APIキー取得
ソースの説明と言っておきながらソース外のところで、このAPIは利用にAPIキーの取得が必要なので[A3RT](https://a3rt.recruit.co.jp/product/proofreadingAPI/registered/)でAPIキーを発行しましょう。

## UI部分

L59～93
後述しますが、このツールではVueをCDNで読み込んで、画面の描画処理やらをVueで行ってます。

```html
<body>
	<div id="app">
		<h1 class="tool-title">Proofread Document Tool</h1>
		<h2>NOTE</h2>
		<div clsss="note">
			<p>Excelファイルにのみ対応しています。</p>
		</div>
		<h2>INPUT</h2>
		<input type="file" id="excelFile" /><br>
		<button v-on:click="doFormat">EXECUTE</button>
		<h2>OUTPUT</h2>
		<p v-if="cellValueList.length === 0">チェック開始前です。</p>
		<p v-else-if="cellValueList.length > apiResultList.length">チェック中です。</p>
		<p v-else>チェックが完了しました。</p>
		<div class="table-area">
			<table align="center" border="1" v-show="apiResultList.length > 0">
				<tr>
					<th>Sheet</th>
					<th>Cell</th>
					<th>Status</th>
					<th>Error</th>
					<th>Checked Sentence</th>
				</tr>
				<tr v-for="result in apiResultList" v-on:key="result.resultId">
					<td>{{ result["sheetName"] }}</td>
					<td>{{ result["cellAddress"] }}</td>
					<td>{{ result["status"] }}</td>
					<td>{{ result["message"] }}</td>
					<td v-html="result.checkedSentence"></td>
				</tr>
			</table>
		</div>

	</div>
</body>
```

## 各ライブラリ読み込み

L94～L101

画面描画のためのVue
エクセルファイル読み込みのためのSheetJS
APIリクエストのためのjQuery
その他、自作のJSファイル

を読み込んでます。

自作のJSファイルについては
<dl>
    <dt>checkExtension.js<dt>
    <dd>拡張子がxlsx、xls、xlsmのいずれかであるかチェックしてます。
    <dt>getAllCellAddressAndValue.js</dt>
    <dd>ワークブックのオブジェクトを与えると値が入っている全セルのアドレスと値をオブジェクトのリストとして返してくれます。（こちらは後でソースを貼付します。）
    <dt>ProofReadingAPIInfo.js</dt>
    <dd>ProofReading APIのリクエストパラメータに設定する値を返してくれるだけです。中にはパラメータがべた書きしてあります。</dd>
    <dt>decorateCheckedSentence.js</dt>
    <dd>APIでは誤りがありそうな箇所を「<<」「>>」で囲んでレスポンスとしてかえしてくれるので、囲まれた箇所が赤字になるようにstyle属性を加えたHTMLとしてレスポンス内容を加工して返却してくれます。（こちらも後でソースを貼付します。）</dd>
<dl>

```html
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
	integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="./logics/checkExtension.js"></script>
<script src="./logics/getAllCellAddressAndValue.js"></script>
<script src="./logics/ProofReadingAPIInfo.js"></script>
<script src="./logics/decorateCheckedSentence.js"></script>
```

### getAllCellAddressAndValue.js

ミソなのは使用されているセルの範囲取得とそのセルの巡回走査くらいですかね。

```js
/**
 * getAllCellAddressAndValue：セル情報取得
 * @param {*} workBook ワークブックオブジェクト
 * @returns cellValueList セルアドレスおよび値リスト
 */
function getAllCellAddressAndValue(workBook) {
    // 値格納用リスト
    let cellValueList = [];

    // シート名一覧取得
    const workSheetNames = Array.from(workBook.SheetNames);

    // 各シートの値が格納されてるセルの値とアドレスを取得
    workSheetNames.forEach((workSheetName, index) => {

        const currentWorkSheet = workBook.Sheets[workSheetName];

        // 使用されているセル範囲を取得
        const range = XLSX.utils.decode_range(currentWorkSheet['!ref']);

        // 全セルを巡回
        for (var colIdx = range.s.c; colIdx <= range.e.c; colIdx++) {
            for (var rowIdx = range.s.r; rowIdx <= range.e.r; rowIdx++) {

                // セルの情報を取得
                let address = XLSX.utils.encode_cell({ r: rowIdx, c: colIdx });
                let cell = currentWorkSheet[address];
                if (cell !== undefined) {
                    cellValueList.push({ sheetName: workSheetName, cellAddress: address, cellValue: cell.v });
                }
            }
        }
        
    })
    return cellValueList
}
```


### decorateCheckedSentence.js

とくに言うこともないです。

```js
function decorateCheckedSentence(checkedSentence) {
    const START_MARKUP = '<<';
    const END_MARKUP = '>>';
    const START_TAG = '<span style="color:red">';
    const END_TAG = '</span>';

    return checkedSentence.replaceAll(START_MARKUP, START_TAG).replaceAll(END_MARKUP, END_TAG)
}
```

## APIリクエスト実行

L157～193
値が入っているセルすべてをAPIリクエストとして投げます。
「VueならaxiosでAPIリクエストすりゃいいじゃん」と思うかもしれませんが、このProofReading APIはクロスドメインでAPIを実行できないためJSONPを使ってレスポンスを確認する必要があります。
axiosでもJSONPを実現できるようですが、うまくいかなかったので、簡単に実現できるJQueryを利用しています。

クロスドメインとJSONPについては以下を参照してください。

[クロスドメイン](https://wa3.i-3-i.info/word15224.html)
[JSONP](https://www.tohoho-web.com/ex/jsonp.html)

```js
					for (let cellValueObj of vm.cellValueList) {
						// リクエストパラメータ追加
						vm.param.sensitivity = proofReadingAPIInfo.getSensitivity();
						vm.param.sentence = cellValueObj.cellValue;

						// APIリクエスト実行
						$.ajax({
							type: 'post',
							url: 'https://api.a3rt.recruit.co.jp/proofreading/v2/typo',
							dataType: 'jsonp',
							jsonpCallback: 'callback',
							data: vm.param,

						}).done(function (json) {
							vm.apiResultList.push({
								sheetName: cellValueObj.sheetName,
								cellAddress: cellValueObj.cellAddress,
								errorFlag: false,
								resultId: json.resultID,
								status: json.status,
								message: json.message,
								checkedSentence: decorateCheckedSentence(json.checkedSentence)
							});
						}).fail(function (jqXHR, textStatus, errorThrown) {
							console.log("-------------------------------------------------------------------");
							console.log(errorThrown);
							console.log("-------------------------------------------------------------------");
							vm.apiResultList.push({
								sheetName: cellValueObj.sheetName,
								cellAddress: cellValueObj.cellAddress,
								errorFlag: true,
								resultId: "error",
								status: "-",
								message: "error was happened",
								checkedSentence: null
							});
						});
```

# 使ってみる

## 画面表示

まずは、Phase#1から若干見た目が改善した画面を表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/06ddcb46-afe2-21a6-313f-c191db8e1a2a.png)

## エクセルを読み込ませる

今回は↓のような誤字った内容を入力したエクセルを用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/9bff96a5-dde8-ba0d-605a-8fd634a6f9ed.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/68c88e93-2492-30b9-1fb2-0d15b1ded598.png)

画面ファイルを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/f4cf756b-1993-428e-46a7-c5c512f22f7e.png)


## APIリクエスト実行

↓のような感じでAPIが実行されて結果が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/9e02add6-1c4b-4741-6b71-8c47947cc6f5.png)

赤字の部分がAI的に「日本語ミスってね？」って箇所らしいですが、まあ間違ってないよって箇所が指摘されてたり、逆にミスってる箇所がスルーされてたりします。（校正は完全にAI任せなので精度はノーコメントなんですが、一応、APIのリクエストパラメータとして校正精度の強弱をつけることはできて、今回は中くらいの強度でリクエストを飛ばしてます。）

`Error` 列に「Error was happend」と表示されている行はエラった行になります。
スリープとかさせたら大丈夫なのかな？
そこはあまり興味がなくて追ってないです。。。

# 感想
本当はaxiosを使ってみたかったんですが、使えなかったのは残念。
ただ、クロスドメインとかJSONPとか全然知らなかったので、バタつきながらもそこを解決して知識が広まったのはよかった。
ちなみに日本語の軽いAIを使わない校正であればtextlint？を使うという手もあります。