【LINE Messaging API, GAS】複雑な単語検索ができるLINE Botを作った

## はじめに
複雑な単語検索（クロスワード検索＋α）ができるLINE Bot「ことばさがし」を作った。前半は主に機能の説明、後半は制作にあたり必要になった技術やソースコード、参考サイトを部分的に紹介する。

## LINE Bot「ことばさがし」
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/f657e869-2007-48ca-1cd7-47b21cee9dd7.png" width="150">
<a href="https://lin.ee/USAHiAp"><img src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png" alt="友だち追加" width="130" border="0"></a>

https://forms.gle/Ln3ajbrK4wKcebHr7

（上のボタンから友だち追加できます。フィードバックをいただけますと幸いです。）
## 背景
謎解きを作っていると「Xを含むN文字の単語」「XがM文字目にあるN文字の単語」などを考えることがよくある。このような条件の単語検索が手軽にできればと思い、この LINE Bot を作ることにした。謎解きやクロスワードの制作者にはぜひ使ってほしい。

## 使用例
### 友だち追加時のあいさつメッセージ
ルールとボタンパネルが送られてくる。
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/7791cc17-7176-7d0e-3a42-e63aa133c1e8.png" width="301">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/788621f8-4422-988f-e3c0-a9f68e145021.png" width="300"></p>

### 単語の検索
#### 1. 基本的な検索
基本的な検索は「？」や「～」を使って直感的に入力できる。

<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/0b9ceb3a-4863-3f3c-efc9-e8239c66d144.png" width="290">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/8da8e59a-1b2c-2802-4b25-903127a9d8e0.png" width="300"></p>

#### 2. 高度な検索
高度な検索は、ボタンパネル押下後に送られてくる入力形式に従って入力する。

<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/09ec54d7-8413-9164-bdff-87430e5b0d06.png" width="282">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/5f5416bf-d26d-d458-6406-694539b6d1b9.png" width="300"></p>

## 特徴
+ **全角対応**
  日本語入力モードの時に一々半角に変換するのは面倒なので、全角のアルファベット、数字、記号に対応した。
+ **ボタンパネル**
  高度な検索において正規表現をそのまま打つのは手間になるのでボタンパネルを設置し、最小限の入力で済むようにした。
+ **クイックリプライ**（スマホ版のみの機能）
  クリックリプライ（画面下部のボタン）を設置し、すぐにルールとボタンパネルを確認できるようにした。
+ **文字種フィルター**
  基本的な検索では先頭に `[特定の文字][ハイフン/長音符]` を付けることで文字種を絞ることができる（無ければひらがな）。高度な検索では `TYPE` に `特定の文字` を指定する。
  > ひらがな → ひ
  > 漢字 → 漢字
  > ひらがな・漢字 → ひ漢字
  > アルファベット → a

## ルール
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/3b0d7239-ed42-d0e6-6402-55cdccaa16b9.png" width="300">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/88884284-bc19-4c29-524f-0d58543cef58.png" width="300"></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/f763f720-8cf9-eb65-8159-456d9111c125.png" width="300"></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/b429483a-d942-07db-52fc-66e2d21bcff7.png" width="300">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/ecb043cf-d244-d8be-a4a2-5415baf47e03.png" width="300"></p>

## 機能・仕組み
### 大まかな流れ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/0dcba13a-77e0-339c-56f2-d3b12768e382.png" width="500">

## 単語のデータ
### 1. ウィクショナリーの見出し語
> ウィクショナリー (Wiktionary) は wiki と dictionary の合成語です。ウィキを使った参加型プロジェクトで、辞書を作成することを目的としています。ウィクショナリーは GFDL および CC BY-SA 3.0 というライセンスを採用しています。ウィクショナリーでの著作はすべてこのライセンスの下に公開されます。これは、複製や翻案を、一般的な著作物よりも自由に誰でもできることを意味します。（[ウィクショナリーとは](https://ja.wiktionary.org/wiki/Wiktionary:%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8)）

[こちら](https://dumps.wikimedia.org/jawiktionary/latest/)から`jawiktionary-latest-all-titles.gz`（全項目のページ名一覧）をダウンロードできる。（参考：[Wikipedia:データベースダウンロード](https://ja.wikipedia.org/wiki/Wikipedia:%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89)）

### 2. 百科辞書
[私立ＰＤＤ図書館](http://pddlib.v.wol.ne.jp/default.htm)の百科辞書は単語がすべてひらがなで、クロスワード検索を作るのに適した辞書である。[こちら](http://pddlib.v.wol.ne.jp/japanese/index.htm#download)からテキストファイルをダウンロードできる。

これらの単語データを一つにまとめ、csvファイルにして Google Drive にアップロードした。

## csvファイルを読み込み文字列配列へ
単語データが入ったcsvファイルを読み込んで文字列配列にする。
```javascript:gs
const wordsId = "csvファイルのid";
const wordsFile = DriveApp.getFileById(wordsId);  // ファイルを取得
const wordsArray = wordsFile.getBlob().getDataAsString("UTF-8").split(",");  // 配列にする
```

https://for-dummies.net/gas-noobs/how-to-get-files-and-folders-in-drives-by-gas/#getFileById

https://teratail.com/questions/366903


## 正規表現を用いた検索
基本的な正規表現はこちらを参考にした。

https://murashun.jp/article/programming/regular-expression.html

複雑な正規表現についてはこちらにまとめた。

https://qiita.com/probabilityhill/items/7c1977063afaccf32694


### 複数マッチした場合すべて置換する
`g` オプションが必要
```javascript:gs
let text = `文字列`;
text = text.replace(/正規表現/g, '置き換える文字列');
```

https://1-notes.com/javascript-multiple-specifications-of-replace/

### 文字種フィルター
文字種でフィルターをかけるための正規表現を返す `getFilterRgx(type)` を実装した。
```javascript:gs
function getFilterRgx(type){
  if(type === "ひ"){
    // ひらがなのみで構成される単語にマッチする正規表現
    return /^[\u3040-\u309F]+$/;
  }
  else if(type === "a"){
    // アルファベットのみで構成される単語にマッチする正規表現
    return /[a-z]+/;
  }
  else if(type === "漢字"){
    // 漢字のみで構成される単語にマッチする正規表現
    return /^[\u3005-\u3006\u4E00-\u9FFF]+$/;
  }
  else{
    // type === "ひ漢字"
    // ひらがなと漢字のみで構成される単語にマッチする正規表現
    return /^[\u3040-\u309F\u3005-\u3006\u4E00-\u9FFF]+$/;
  }
  
}
```
https://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/cjku_klist.html

### 正規表現で配列にフィルターをかける
```javascript:gs
// wordsArray: 単語の配列
let str = "/正規表現/";
let filterRgx = getFilterRgx("TYPE");

// 単語の配列から正規表現にマッチしたものだけを取り出す
let resultArray = wordsArray.filter(RegExp.prototype.test, eval(str));
// マッチした単語に文字種フィルターをかける
resultArray = resultArray.filter(RegExp.prototype.test, eval(filterRgx)); 
```

https://qiita.com/mascii/items/0a505698a9e5b7c70d93#bind%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AF%E8%A6%81%E3%82%89%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F

### 全角を半角に変換
日本語入力の場合の入力ストレスを減らすために全角にも対応した。全角のアルファベット、数字を半角にする `getHalfWidth(str)` を実装した。

```javascript:gs
function getHalfWidth(str){
  return str.replace(/[Ａ-Ｚａ-ｚ０-９]/g, function(s) {
    return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
  });
}
```
https://www.yoheim.net/blog.php?q=20191101


## LINE BotとGASの連携
こちらの記事では Flex Message の実装と Channel access token の取得からデプロイまでの詳しい手順が書かれている。

https://qiita.com/probabilityhill/items/1f923e742a6f18d3f163

## ユーザIDの記録
友だち追加時にスプレッドシートにユーザIDを記録
```javascript:gs
function doPost(e){
  const events = JSON.parse(e.postData.contents).events;
  for (var i = 0; i < events.length; i++){
    execute(events[i]);
  }
}

function execute(event){
  const eventType = event.type;
  const userId = event.source.userId;

  if(eventType === "follow"){
    const writeRow = data.getLastRow()+1;  // 書く行を取得
    data.getRange(writeRow,1).setValue(userId);  // A列目にユーザIDを記入
    data.getDataRange().removeDuplicates([1]);  // ユーザIDの重複を削除
  }
}
```

https://qiita.com/probabilityhill/items/e691a852d431ac99b125

## さいごに
昔自分用につくった単語検索サイト「KOTOBA SAGASHI」を紹介。
+ データはLINE版と同じ
+ ルールをコピペできるため入力が楽
+ シンプルなデザイン
+ 今のところ謎解き専用の機能はなし

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/57295e99-a377-c798-c6bb-356ccafbb486.png" width="600">

https://probabilityhill.pythonanywhere.com/

***
`クロスワード検索` `単語検索` `辞書検索` `ワイルドカード検索` `部分一致` `謎解き` `熟語` `英単語` `英語`