アプリの概要
- 文章を読みながら外国語の語彙を増やす
- アプリに外国語のテキストを貼り付ける → 覚えたい単語をハイライト → 単語の意味が表示される (現在は仏→英に対応)
- Github: https://github.com/jesuissuyaa/tangochou
スクリーンショット
仕様
構成図
- 画面
- データベース: 以下のデータを保存
- ユーザの入力した単語
- ユーザがハイライトした単語
- 外部サイトから取得した単語の意味
- API: 外部の辞書サイトから単語の意味をスクレイピング→アプリに返す
ライブラリ & フレームワーク
- React
- Next.js:
create-next-app
でブートストラップ (https://nextjs.org/docs#quick-start) - Typescript
- [json-server] (https://www.npmjs.com/package/json-server): JSON形式でデータを読み書きできるデータベース
- [cheerio] (https://www.npmjs.com/package/cheerio): スクレイピング用のパッケージ; jQueryの書き方でスクレイピングできる点がわかりやすい
- [request] (https://www.npmjs.com/package/request): APIで外部サイトにリクエストするために使用
- [isomorphic-unfetch] (https://www.npmjs.com/package/isomorphic-unfetch): json-serverでデータを読み書きするときに
fetch
を使用
アクティビティ図
開発のポイント
APIを自分で書きました
経緯
最初は翻訳のライブラリかAPIを使うことを考えていましたが、
- 一つの単語に対して複数の意味を返してくれるものが見つからない
- APIキーの取得が大変
- 有料の場合もある
- 単語単位でなく文章単位の翻訳が想定されていることが多い
ということから、自分でAPIを書くことにしました
セットアップ
- 公式ドキュメンテーションを読む (https://nextjs.org/docs#api-routes)
- API用のフォルダを作る
/ # ルートディレクトリ
└ pages
└ api
└ # api用の.jsファイルを置く
/pages/api
に置いたAPIは、/api/*
にアクセスすることで叩くことができます
例: /pages/api/dictionary.js
はlocalhost:3000/dictionary
にアクセスするとGETやPOSTができます
コード
下のサンプルコードでは、
url
で設定したサイトにアクセス
→ cheerioでh1タグをスクレイピング
→ h1タグのテキストをレスポンスで返す
ということをしています
import request from 'request';
import cheerio from 'cheerio';
export default ({ query: { id } }, res) => {
request(url,
(err, response, body) => {
if (err) {
console.error(err);
res.status(500);
res.end('server error');
}
const $ = cheerio.load(body);
const foo = $('h1').text();
res.status(200).json({ message: `scraped h1 tag and got ${foo} ` });
},
);
};
クエリパラメータ
APIのURLにはパラメータを設定することができます
サンプルコードの中では { query: { id } }
となっている部分で、URLの?id=
の値を取得しています
例えば localhost:3000/api/my-api?id=10
にアクセスすると、APIの中でid
の値を使うことができます
cheerioでスクレイピング
サンプルコードの中では、以下の2行でcheerioを使っています
const $ = cheerio.load(body);
const foo = $('h1').text();
$ = cheerio.load(body)
でレスポンスのbodyを$
に入れることで、$('.myclass')
や$('#myid')
のようにjQuery風の書き方でスクレイピングしたいサイトに要素を取得できます
responseを返す
エラーの場合は500を返します
res.status(500);
res.end('server error');
成功した場合は200とJSON形式のデータを返します
res.status(200).json({ message: `scraped h1 tag and got ${foo} ` });
json-serverでデータベースを実装しました
こちらの記事が大変参考になりました
https://qiita.com/t12u/items/2be73956b788c745048f
json-serverの使い方
- json-serverを
npm
でアプリに追加 -
db.json
をルートディレクトリに用意 → データを書き込む - ターミナルからjson-serverを起動:
npm run json-server
- アプリから
localhost:30001/<データベース名>
にHTTPリクエストを投げることで読み書きする
json-serverでの読み書き
- 新規作成: GET
- 編集: PUT
- 削除: DELETE
でそれぞれデータを操作できます
アプリのデータベース構成
texts
+ id: 1 (number) # データのID; json-serverによって自動で振られる
+ text: the quick brown fox jumps over the lazy dog (string) # ユーザの入力した文章
+ wordlist: ['fox', 'lazy'] (string[]) # 文章の中でハイライトされた単語
vocab
+ id: 1 (number)
+ word: lazy (string) # 単語
+ definitions: ['気だるい', '怠けている', 'ゆっくりとした'] (string[]) # 単語の意味の配列
コード
アプリの中ではfetch
を使ってlocalhost:3001/text
やlocalhost:3001/vocab
にHTTPリクエストを投げます
下の例ではvocabデータベースのすべてのエントリーのwordの値をとってきています
export async function getWords () {
const res = await fetch('http://localhost:3001/vocab')
const data = await res.json();
return (<ul>{data.map(entry => <li>entry.word</li>)}</ul>)
};
<!-- getWords()の返り値の一例 -->
<ul>
<li>lazy</li>
<li>fox</li>
<li>dog</li>
</ul>
新たに書き込みをするときは、このようなコードになります
fetch('http://localhost:3001/vocab', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
word: 'hoge',
definition: ['ほげ', 'ホゲホゲ']
}),
}).then(getWords);
アクサンの処理が大変でした
経緯
冒頭の概要で書いたように、今回はフランス語の文章を対象にしました
フランス語の単語の意味をAPIでとってくるときは、例えば https://www.collinsdictionary.com/dictionary/french-english/vouloir のように、URLに調べたい単語を入れます
ところが、フランス語にはアクサン記号つきの文字 (e.g. î, é) が含まれるため、単語をそのままURLのパラメータにするとエラーになります
そこで、文字列を処理する必要があります
処理の流れ
- 単語がクリックされる → être
- 単語の文字列を取得 → être
- アクサン付きの文字をエンコーディング -> %EAtre
- URLにアクセス -> https://www.collinsdictionary.com/dictionary/french-english/%EAtre
コード
アクサンのエンコードをする関数は以下の通りです
何も考えずにアクサン文字を1つずつString.replace()
で置換します
文字列の処理は、アクサン以外にもユーザが入力した文章をデータベースに保存するとき、'
や&
をエスケープすることも行ったため、
関数をutils/strUtils.tsx
の中にまとめて置きました
アプリ本体で使うときは import { encodeAccents } from ../utils/strUtils
のようインポートします
export const encodeAccents: (str: string) => string = (str: string) =>
str
.toLowerCase()
.replace(/ç/g, '%E7')
.replace(/é/g, '%E9')
.replace(/â/g, '%E2')
.replace(/ê/g, '%EA')
.replace(/î/g, '%EE')
.replace(/ô/g, '%F4')
.replace(/û/g, '%FB')
.replace(/à/g, '%E0')
.replace(/è/g, '%E8')
.replace(/ù/g, '%F9')
.replace(/ë/g, '%EB')
.replace(/ï/g, '%EF')
.replace(/ü/g, '%FC');
備考
文字列の処理について、アポストロフィには'
の他にも’
が使われていることがあります
#まとめ
開発を通して、
- Next.jsを使ったReactアプリケーションの開発
- json-serverでのデータベース実装
- APIの書き方
- fetchの使い方
などを学びました
未解決な点としては、
- Next.jsで
.scss
ファイルを扱う方法 -
request
をネストする方法
などがあります
機能を実装していくにあたり、細かい点を決めるとき(単語リスト画面に単語の意味を表示するかどうかなど)にどうするべきかの判断基準がぶれていたので、次の開発では最初に想定ユーザ・想定シーンをより具体的に定めていきます
データベースも、とりあえずライブラリを入れてからデータ構造を設計しましたが、こちらはライブラリをある程度使わないと仕様がわからないことと、データ構造が比較的単純だったことがあり、今回のやり方でもあまり困りませんでした
今後の予定としては、しばらく自分でアプリを使ってみて、バグ取りや更にあったら便利な機能を実装していきます