Developers.IO ブログで日本語の辞書を作ってみた

こんにちは。コンサルティング部のキムです :D

今回は私の日本語の勉強に参考になれるカスタマイズされた日本語の辞書を実装してみましたので、その内容をブログ化したいと思います。

目次

背景

私がクラメソに入社して日本で働いてからももう4ヶ月が経っています。初めての海外生活で大変なこともありましたが、順調に新しい生活にも慣れています。

ですが、やはり日本語はまだ難しいですね。

特にエンジニアとして技術の議論はおろか、技術に関わっている言葉や表現等分からないことが多いため、社内で常に起こっている技術的な議論やプレゼンテーション時に私だけがその内容を聞き取れなかったり、お客様とのやり取りの対応が出来なかったりします。これは会社的にも自分にもよくないので、これからは日本語の勉強に集中しようと思っています。

なので、より効果的な日本語の勉強方法について悩んだあと思い付いたのが技術的なコンテキストでよく使われる言葉や表現等を纏めて勉強すればということでした。ですが、そんなことを探すのも手強いですよね。

そう思っているうちに自分で作ってみれば?という面白い発想が思いつきました。

具体的には私の仕事に一番関わっている弊社で提供しているこのDevelopers.IOのブログの内容を活かせて、そのブログの日本語を学ぼうということです。結果としてDynamoDBのテーブルにこのようなカスタマイズされた日本語の辞書DBが出来上がりました!

result-dynamodb-table-items

正直言うと、ただ面白いからという理由だけではあまりやる気が出なかったのですが、 これからずっと改善しながらDevOps辺りの技術を実際にこのプロジェクトベースでやりたいところもあるし、 自分自身で勉強ツールを作って勉強すればもっと楽しく勉強できるところもあって作ることになりました。 あ、それに最後にブログのネタにもなるんですね(笑)

やったこと

自分のカスタマイズされた日本語の辞書を実装する為にやったことは以下のようです。

  1. ブログURLをインプットとしてブログの内容のテキストのみ抽出
  2. 抽出したテキストの形態素分析
  3. JSONファイルで保存
  4. JSONファイルのデータをDynamoDBにアップロード
  5. クエリを実行・結果確認

0. 前提・準備事項

仕事に直接関わることではないので実装時間はなるべく短くしたかったです。 なので、コードの再利用や厳密なテスト等は略して進めました。 最初は3時間ほどで十分ではないかと思いましたが、結果としては30分オーバーして3時間30分で作りました。

開発環境は Mac OS 上で python3.7 を用いて構成しました。 以下のようにターミナルで新しいフォルダを作って virtualenv を作ります。

$ mkdir japanese-study-proj
$ cd japanese-study-proj
$ virtualenv venv
$ source venv/bin/activate

以下のようにプロジェクトフォルダの構成を作ります。ターミナルの (venv) 表示は略します。

$ mkdir src
$ mkdir generated

必要な package をインストールしておきます。

$ pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"
$ pip install beautifulsoup4
$ pip install requests
$ pip install boto3

一番上の ginza ライブラリーに関しましては後で説明させて頂きます。

1. ブログURLをインプットとしてブログの内容のテキストのみ抽出

まず、src フォールだに移動して parse.py ファイルを作ります。

$ cd src
$ touch parse.py

parse.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

from bs4 import BeautifulSoup
import requests


def get_html_doc(url):
	response = requests.get(url)
	return response.text


def extract_all_tags(raw_content, tag_name):
	tag_list = raw_content.findAll(tag_name)
	for tag in tag_list:
		tag.extract()

	return raw_content

def get_blog_content(html_doc):
	soup = BeautifulSoup(html_doc, 'html.parser')

	raw_content = soup.find('div', class_='single_article_contents')

	raw_content = extract_all_tags(raw_content, 'pre')
	raw_content = extract_all_tags(raw_content, 'script')
	raw_content = extract_all_tags(raw_content, 'img')
	raw_content = extract_all_tags(raw_content, 'iframe')

	content = raw_content.get_text()

	return content



if __name__ == '__main__':

	url = input('Dev.IO Page URL : ')
	html_doc = get_html_doc(url)
	content = get_blog_content(html_doc)
	
	print(content)

このファイルを実行してみると、このような結果が出力されます。(一部だけスクリンショット)

terminal-output-1

2. 抽出したテキストの形態素分析

本プロジェクトのメインである日本語の形態素分析のライブラリーをご紹介いたします。

GINZAというライブラリーですが、spacyを活用しています。

私もほぼ今回初めて弄ってみましたが、凄く簡単に活用できました。サンプルコードを読むことで10分ぐらいで理解できるほど簡単なライブラリーでした。(中身は全然分からないですが)

analyze.py と vocabulary.py ファイルを作成しました。 インポートの為 init.py ファイルも作っておきました。

$ touch __init__.py
$ touch analyze.py
$ touch vocabulary.py

analyze.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import spacy
import re

from vocabulary import Vocabulary


def create_vocabulary_list(content):

	nlp = spacy.load('ja_ginza')
	doc = nlp(content)

	vocab_list = []
	for sentence in doc.sents:
		for token in sentence:
			if token.pos_ == 'PUNCT' or token.pos_ == 'ADP' or token.pos_ == 'SCONJ' or token.pos_ == 'NUM':
				continue

			elif token.pos_ == 'AUX' and token.dep_ == 'aux':
				continue

			elif re.match("^[A-Za-z]*$", token.orth_):
				continue

			else:
				vocab = Vocabulary(token, sentence)
				vocab_list.append(vocab)

	return vocab_list


if __name__ == '__main__':

	content = '本エントリでは、サブスクリプションフィルタを利用せず、CloudWatch Logsのロググループを、S3バケットにエクスポートしてみたいと思います。'
	vocab_list = create_vocabulary_list(content)
	print(vocab_list[0].__dict__)

vocabulary.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import datetime
import json

class Vocabulary:
	def __init__(self, token, sentence):
		self.lemma = token.lemma_ # 頂く
		self.orth = token.orth_ # 頂き
		self.pos = token.pos_ # AUX
		self.sentence = str(sentence) # お話させて頂きました
		self.timestamp = datetime.datetime.utcnow().isoformat()

	def toJson(self):
		return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)

これだけ見るとコレナニ?!ということになりがちだと思い、コードを説明させて頂きます。

nlp = spacy.load('ja_ginza')
doc = nlp(content)

analyze.py の create_vocabulary_list を見ると最初は spacy ライブラリーからja_ginzaというモデルを持ってきます。 その結果の nlp というモデルに日本語の文書(content)を入力して doc という変数を得ます。

	...
	
	for sentence in doc.sents:
		for token in sentence:
			if token.pos_ == 'PUNCT' or token.pos_ == 'ADP' or token.pos_ == 'SCONJ' or token.pos_ == 'NUM':
				continue

			elif token.pos_ == 'AUX' and token.dep_ == 'aux':
				continue

			elif re.match("^[A-Za-z]*$", token.orth_):
				continue

	...

token は形態素分析の結果です。より簡単に理解する為に以下のコードを実行して結果を貼ります。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import spacy

def create_vocabulary_list(content):
	nlp = spacy.load('ja_ginza')
	doc = nlp(content)

	vocab_list = []
	for sentence in doc.sents:
		for token in sentence:
			print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)

if __name__ == '__main__':
	content = '本エントリでは、サブスクリプションフィルタを利用せず、CloudWatch Logsのロググループを、S3バケットにエクスポートしてみたいと思います。'
	vocab_list = create_vocabulary_list(content)

terminal-output-2

この結果を見ると

2 で で ADP 助詞-格助詞 case 1
3 は は ADP 助詞-係助詞 case 1
4 、 、 PUNCT 補助記号-読点 punct 1

'で'、'は'、'、' みたいなことは日本語辞書のターゲット単語の対象外にした方が良いと思いましたので if elif で除きました。 同様の理由で英語も regexp で除きました。

3. JSONファイルで保存

次は先ほど纏めた vocab_list をJSONファイルにて保存します。

$ touch save.py

save.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import os
import uuid
import json
from datetime import date

from vocabulary import Vocabulary

def generate_filename():
	today = date.today().strftime('%Y%m%d')
	postfix = str(uuid.uuid4()).replace('-', '')
	filename = today + '_' + postfix + '.json'
	return filename

def save_file(filename, vocab_list):
	if len(vocab_list) > 0:
		
		dirname = filename[0:8]

		try:
			os.mkdir(os.path.join(os.path.dirname(__file__), '../generated/' + dirname))
		except FileExistsError:
			print('directory "' + dirname + '" already exists')

		filepath = os.path.join(os.path.dirname(__file__), '../generated/' + dirname + '/' + filename)

		with open(filepath, 'w', encoding='utf8') as json_file:
			json_file.write('[')

			for vocab in vocab_list:
				json.dump(vocab.__dict__, json_file, ensure_ascii=False)
				json_file.write(',')

			json_file.seek(json_file.tell() - 1, os.SEEK_SET)
			json_file.write(']')


if __name__ == '__main__':
	filename = generate_filename()
	save_file(filename, [])

このファイルは ${PROJECT_ROOT}/generated/20190919/20190919_bc6849a04abc44eaa30b82b7f0dbc2df.json のような形で保存されます。 これらを集めて次のステップで DynamoDB にアップロードする流れになります。

今までのコードが三つのファイルに分けてあって、纏めて実行する方が楽なので纏めて実行してみました。

$ touch main.py

main.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

from parse import get_html_doc, get_blog_content
from analyze import create_vocabulary_list
from save import generate_filename, save_file

url = input('Dev.IO Page URL : ')
html_doc = get_html_doc(url)
content = get_blog_content(html_doc)
vocab_list = create_vocabulary_list(content)
filename = generate_filename()
save_file(filename, vocab_list)

すると以下のようなコマンドで、ブログの内容をテキストで抽出、形態素分析、JSON形式で保存までの流れが自動化されます。

$ python src/main.py

4. JSONファイルのデータをDynamoDBにアップロード

コードを作成する前、aws cli で DynamoDBのテーブルを作るスクリプトを作ります。

$ cd ..
$ echo aws dynamodb create-table --table-name JapaneseVocabulary --key-schema AttributeName=lemma,KeyType=HASH AttributeName=timestamp,KeyType=RANGE --attribute-definitions AttributeName=lemma,AttributeType=S AttributeName=timestamp,AttributeType=S --billing-mode PAY_PER_REQUEST > create_resources.sh
$ sh create_resources.sh

作業中のディレクトリ(src)に戻って upload.py ファイルを作ります。

$ cd src
$ touch upload.py

upload.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import boto3
import json
import os
import datetime
from boto3.dynamodb.conditions import Key, Attr
from botocore.exceptions import ClientError

def read_vocab_files():
    cwd = os.path.dirname(__file__)
    generated_dirname = cwd + '/../generated' if cwd == 'src' else '../generated'
    target_dirname = os.path.abspath( generated_dirname + '/' + datetime.date.today().strftime('%Y%m%d') + '/')
    
    vocab_files = os.listdir(target_dirname)
    vocab_files = [ target_dirname + '/' + f for f in vocab_files if os.path.isfile(os.path.join(target_dirname, f))]

    vocab_list = []
    for filepath in vocab_files:
        with open(filepath, 'r') as f:
            json_data = json.load(f)
            vocab_list = vocab_list + json_data

    return vocab_list

def batch_write_item(table, vocab_list):
    try:
        with table.batch_writer() as batch:
            for vocab in vocab_list:
                batch.put_item(Item=vocab)

    except ClientError as e:
        print(e.response['Error']['Message'])


def add_vocab_list(vocab_list):
    dynamodb = boto3.resource("dynamodb", region_name='ap-northeast-1')
    table = dynamodb.Table('JapaneseVocabulary')
    batch_write_item(table, vocab_list)

if __name__ == '__main__':
    vocab_list = read_vocab_files()
    add_vocab_list(vocab_list)
    print('done.')

これを実行してみると日本語辞書が出来上がります!

5. クエリを実行・結果確認

最後に簡単にクエリを実行してみることで作業結果を確認してみます。

$ touch search.py

search.py

#!/usr/bin/python
# -*- coding: utf-8 -*-


import boto3
import json
import os
import datetime
from boto3.dynamodb.conditions import Key, Attr
from botocore.exceptions import ClientError

def search_vocab(keyword):
    dynamodb = boto3.resource("dynamodb", region_name='ap-northeast-1')
    table = dynamodb.Table('JapaneseVocabulary')
    response = table.query(
        KeyConditionExpression=Key('lemma').eq(keyword)
    )

    result = response['Items']

    return result

def print_result(result):

    print('------ SEARCH RESULT ------')
    for r in result:
        print(r['lemma'])
        print(r['sentence'])
        print()

if __name__ == '__main__':
    keyword = input('search keyword: ')
    result = search_vocab(keyword)
    print_result(result)

terminal-output-3

最後に

日本語の勉強を加速化する為の対策として自分の日本語辞書を実装してみました。 これを作ってみたら確かに有用なところがあることが分かりました。

例えば、私は新しい単語を覚えても、その使い方が誤っていたのが多いです。 自分では感じられませんが、多分このブログの中にも違和感のある文章が多いと存じます。 その問題を改善する為にはやはり日本語が母国語である方の文章を丸で覚えた方が良いですよね。 私の辞書は単語とその単語が使われた文書も一緒に保存していて、そういうところに関しては楽だと思います。

これからもこのプロジェクトをガンガン発展して行きたいと思いながら本記事を纏めます。