こんにちは、
2019年度新入社員の福田です。
今回はニフティものづくりブログを執筆する際に用語ガイドラインに沿った校正をしてくれるツールをサーバーレスで作ってみました。
用語ガイドラインとは
ニフティではものづくりブログのように外部に公開する文章に使われている言葉が正しいかチェックするための用語のリストが存在します。
例えば、「@Nifty」や「@NIFTY」はガイドライン上で正しくありません、正しくは「@nifty」です。
このようなニフティでの用語に加えて一般的な誤字などをまとめたガイドラインがあります。
ツールの構成
AWSのサービスであるAPI Gateway、Lambda、S3を使ってサーバーレスな構成にしました。
S3にホスティングされたウェブサイトからAPI Gatewayを経由してテキストデータをアップロードし、S3上に生成された構成データを取得する仕組みです。
Lambdaの言語はnode.js、フロントエンドはライブラリとしてReactを採用しました。
文章校正の仕組み
例えば以下のようなルールがあるとします。
- 誤:申込 → 正:申し込み
- 誤:頂ける → 正:いただける
この場合、「お申込頂けます」という文を校正すると、「申込」は「申し込み」、「頂けます」は「いただけます」なので「申し込みいただけます」です。
名詞の場合は当てはまる言葉をそのまま校正すればよいですが、動詞の場合は簡単ではありません。
動詞である「頂ける」という言葉を校正するには基本形である「頂ける」とその活用形である「頂け」「頂けれ」「頂ける」「頂けよ」を考慮しなければならないのです。
コンピューター上で文章を校正するには文章を小さな単位(=形態素)に分解して品詞を判別しなければならなければなりません。これを形態素解析と呼びます。
JavaScriptで形態素解析をするにはJavaの形態素解析ライブラリであるkuromojiをJavaScriptに移植したkuromoji.jsというライブラリを使います。
今回はkuromoji.jsを用いてAWSのLambda上で形態素解析を行い、文章の校正をしたいと思います。
kuromoji.jsの辞書に任意の単語を追加する
kuromoji.jsは辞書を内蔵しているのですが、内蔵されている辞書に存在しない単語を含む文章を形態素解析するとうまくいかないことがあります。
例えば「@nifty」をkuromoji.jsで形態素解析すると以下のような結果が得られます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[ { word_id: 80, word_type: 'UNKNOWN', word_position: 1, surface_form: '@', pos: '名詞', pos_detail_1: 'サ変接続', pos_detail_2: '*', pos_detail_3: '*', conjugated_type: '*', conjugated_form: '*', basic_form: '*' }, { word_id: 120, word_type: 'UNKNOWN', word_position: 2, surface_form: 'nifty', pos: '名詞', pos_detail_1: '固有名詞', pos_detail_2: '組織', pos_detail_3: '*', conjugated_type: '*', conjugated_form: '*', basic_form: '*' } ] |
辞書に「@nifty」という単語が存在しないため「@」と「nifty」が区切られてしまいました。
これでは校正ができないので、自分で辞書に単語を追加していこうと思います。
1 2 3 |
git clone https://github.com/takuyaa/kuromoji.js.git cd kuromoji.js npm install |
kuromoji.js/node_modules/mecab-ipadic-seed/lib/dict内に任意の名前でCSVファイルを作ります。
1 2 3 4 5 6 7 8 9 10 |
@nifty,,,,名詞,固有名詞,*,*,*,*,@nifty,アットニフティ,アットニフティ @Nifty,,,,名詞,固有名詞,*,*,*,*,@Nifty,アットニフティ,アットニフティ @NIFTY,,,,名詞,固有名詞,*,*,*,*,@NIFTY,アットニフティ,アットニフティ @nifty,,,,名詞,固有名詞,*,*,*,*,@nifty,アットニフティ,アットニフティ @nifty,,,,名詞,固有名詞,*,*,*,*,@nifty,アットニフティ,アットニフティ @ニフティ,,,,名詞,固有名詞,*,*,*,*,@ニフティ,アットニフティ,アットニフティ アット・ニフティ,,,,名詞,固有名詞,*,*,*,*,アット・ニフティ,アットニフティ,アットニフティ アットnifty,,,,名詞,固有名詞,*,*,*,*,アットnifty,アットニフティ,アットニフティ nifty,,,,名詞,固有名詞,*,*,*,*,nifty,ニフティ,ニフティ nifty,,,,名詞,固有名詞,*,*,*,*,nifty,ニフティ,ニフティ |
CSVのフォーマットは以下のようになっています。
1 |
表層形,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音 |
今回は左文脈ID、右文脈ID、コストは空にしておきます。
追加したら、ビルドコマンドを実行します。
1 |
npm run build-dict |
完了したらkuromoji.js/dict内に更新された辞書が生成されます。
生成した辞書を使って「@nifty」をkuromoji.jsで形態素解析をすると以下の結果になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { word_id: 9890, word_type: 'KNOWN', word_position: 1, surface_form: '@nifty', pos: '名詞', pos_detail_1: '固有名詞', pos_detail_2: '*', pos_detail_3: '*', conjugated_type: '*', conjugated_form: '*', basic_form: '@nifty', reading: 'アットニフティ', pronunciation: 'アットニフティ' } ] |
Lambdaの作成
AWS上でkuromoji.jsを使って校正する処理をLambdaで作っていこうと思います。
まずは、受け取った文字列をテキストファイルとして、S3のバケットに保存する関数を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const AWS = require("aws-sdk"); const uuid = require("uuid/v1"); const s3 = new AWS.S3(); const BUCKET_NAME = "japanese-proofreading"; const TEXT_KEY_PREFIX = "base-text/"; exports.handler = async (event, context) => { const text = event.text; const text_data = Buffer.from(text, "utf-8"); const text_id = uuid(); const uploadParams = { Bucket: BUCKET_NAME, Key: `${TEXT_KEY_PREFIX}${text_id}.txt`, Body: text_data }; const result = await s3.upload(uploadParams).promise(); return { key: result.Key, text_id }; }; |
次にテキストファイルから間違っている用語を探し、正しい用語と間違っている理由をJSONにして返す関数を作成します。
kuromoji.jsをそのまま使っても良かったのですが、今回はPromiseに対応したkuromoji.jsのラッパーライブラリであるkuromojinを使用しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
const AWS = require("aws-sdk"); const kuromojin = require("kuromojin"); const fs = require("fs"); const util = require("util"); const s3 = new AWS.S3(); const BUCKET_NAME = "japanese-proofreading"; const PROOFREADING_JSON_KEY_PREFIX = "proofreading-json/"; exports.handler = async (event, context) => { const base_text_key = event.Records[0].s3.object.key; const data = await s3 .getObject({ Bucket: BUCKET_NAME, Key: base_text_key }) .promise(); const base_text = data.Body.toString("utf-8"); const words = JSON.parse( await util.promisify(fs.readFile)("./data/word_list.json", { encoding: "utf-8" }) ); const tokens = await kuromojin.tokenize(base_text, { dicPath: "./dict" }); // dicPathで辞書の場所を指定 /* 形態素の基本形と用語リストの単語を比較して配列を返す */ const incorrect_tokens = tokens .map(token => { const word = words.find(word => word.incorrects.some(incorrect => incorrect == token.basic_form) ); return word ? { word: token.surface_form, word_position: token.word_position, correct_word: word.correct, description: word.description } : null; }) .filter(token => token); /* incorrect_tokensをJSONファイルにしてS3に保存 */ const proofreading_json = Buffer.from( JSON.stringify({ text: base_text, incorrect_tokens: incorrect_tokens }), "utf-8" ); const proofreading_json_key = base_text_key .replace(/^.+?\//, `${PROOFREADING_JSON_KEY_PREFIX}`) .replace(/.txt$/, ".json"); const result = await s3 .upload({ Bucket: BUCKET_NAME, Key: proofreading_json_key, Body: proofreading_json }) .promise(); return result; }; |
この関数のディレクトリ構成は以下のようになっています。
1 2 3 4 5 6 7 8 9 |
proofreading │ ├─data │ └─word_list.json <- 用語のリスト ├─dict <- 辞書が格納されているディレクトリ ├─node_modules ├─index.js ├─package-lock.json └─package.json |
校正の処理は対象となる用語を記述した以下のようなJSONを用意して、形態素の基本形と用語を比較しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[ { "correct": "いただく", "incorrects": ["頂く", "頂ける"], "description": "ひらがなで書く。例)「お申し込みいただけます」" }, { "correct": "いろいろと", "incorrects": ["色々と"], "description": "同じ漢字が連続するものはひらがなで書く。" }, { "correct": "エンターテインメント", "incorrects": ["エンターテイメント"], "description": "「ン」を入れる。" }, { "correct": "および", "incorrects": ["及び"], "description": "接続詞はひらがなで書く。" } ] |
API GatewayでREST APIの作成
API GatewayではテキストデータをJSON形式でPOSTリクエストするためのエンドポイントと、POSTリクエストで返ってきたIDの校正データをGETリクエストで取得するためのエンドポイントの2つを作ります。
前者のAPI Gatewayの設定は統合リクエストの統合タイプを「Lambda関数」に設定してS3バケットに保存するための関数を選択します。
後者のAPI Gatewayの設定は統合リクエストの統合タイプを「AWSサービス」に設定してS3バケットに保存された校正データのJSONファイルをGETリクエストするように設定します。
この状態では、テキストデータをS3バケットに保存しても校正データは生成されないため校正データを生成するLambdaにS3のトリガーを設定し、S3バケットにテキストファイルが保存されたらLambdaが実行されるように設定します。
フロントエンド部分の作成
文章を入力してREST APIにHTTPリクエストをするためのページを作成していきます。
フロントエンドのライブラリはReact、モジュールバンドラーはParcel、HTTPクライアントとしてaxios、CSS in JSライブラリとしてstyled-componentsを使用しました。
左の枠に校正する文章を入力して校正ボタンをクリックすると、中央に校正部分がハイライトされた文章、右に校正の詳細が表示されます。
作成したページはS3の静的ページホスティング機能でホスティングしました。
S3の静的ページホスティング機能についてはこちらの記事をご覧ください。
AWS初心者の私がAmazon S3のStatic website hostingを利用して静的Webページをホスティングしてみました
まとめ
今回はAWSのサービスを活用してサーバーレス構成で社内用ツールを作成しました。
ウェブアプリケーションを運用するとなるとコストが気になると思いますので、今回使用したサービスの料金ページを参考にコストを計算していきます。
- API Gateway : https://aws.amazon.com/jp/api-gateway/pricing/
- Lambda : https://aws.amazon.com/jp/lambda/pricing/
- S3 : https://aws.amazon.com/jp/s3/pricing/
1リクエストあたりのコストを無料枠を除いて計算してみたところ約0.000085USDでしたので日本円だと 0.009円ぐらいです。
仮にEC2でインスタンスを立てて運用すると考えると種類にもよりますが、1カ月あたり固定で数千円のコストがかかります。
サーバーレス構成にするとリクエストごとに課金されるので、非常にコストパフォーマンスに優れているのではないのでしょうか。