はじめまして、記事を読んでいただいてありがとうございます。
この記事は『LITALICO Engineers Advent Calendar 2021』の4日目の記事です。
私は、2021年4月にLITALICOに入社をしまして、ドメイン知識が0の状態から、周りの方に助けていただいて、どうにかこうにかプロダクト開発に携わっています。
そんな私としまして、ドメイン知識について、効率的にinputを行いたいと常日頃考えております。
誤解がないように記載しておきますが、周囲からフォローがないという訳ではありません。むしろ、丁寧に教えてくださるのですが、誰かに教わるということは、その方の分のコストも使わせていただくわけで、自分一人で調べて、ちゃんと理解できるなら、コスト効率高いよねと考えた次第です。
(職場へのフォローを忘れない組織人の鏡)
他にも色々方法を考えたのですが、まずは、大量にあるドキュメントから、自分が必要としている情報を探すをゴールに、Google Drive上の索引を作成したいと思い至った次第です。
事前準備
- APIが有効になっているGoogleCloudPlatformプロジェクト
- 今回は、Google Drive APIと、Google Docs APIを使用するので、2つとも有効にしてください
- 設定の仕方は、ちょこちょこ、GoogleCloudPlatformのUI変わったりするので、公式のプロジェクトの設定の仕方を確認してくださいませ
- Google APIを利用する際の認証情報
- こちらも公式の認証情報の設定の仕方を参照してくださいませ
- また、この認証情報は他の方に教えてはいけません
- わかち書きエンジン
- この記事では、Mecabを使用しております
- プログラム言語の実行環境
- この記事では、Python 3系を使用しております
- 公式の実装サンプルからちょこちょこ変えてるだけなので、nodeでも、Javaでもお好きなものを
google-authを使用して OAuth 2.0 の認証を通す
import os.path
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
SCOPES = [
'https://www.googleapis.com/auth/drive.metadata.readonly',
'https://www.googleapis.com/auth/documents.readonly'
]
CLIENT_SECRETS_FILE = 'credentials.json'
TOKEN_JSON_FILE = 'token.json'
def get_credentials():
credentials = None
if os.path.exists(TOKEN_JSON_FILE):
credentials = Credentials.from_authorized_user_file(TOKEN_JSON_FILE, SCOPES)
if not credentials or not credentials.valid:
if credentials and credentials.expired and credentials.refresh_token:
credentials.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRETS_FILE, SCOPES)
credentials = flow.run_local_server(port=0)
with open(TOKEN_JSON_FILE, 'w') as token:
token.write(credentials.to_json())
return credentials
簡単な解説
SCOPES = [
'https://www.googleapis.com/auth/drive.metadata.readonly',
'https://www.googleapis.com/auth/documents.readonly'
]
使用するスコープのリストを定義しておきます。
事前準備で用意していただいた認証情報のスコープに、上記で定義したものを含めるようにしてくださいませ。
CLIENT_SECRETS_FILE = 'credentials.json'
TOKEN_JSON_FILE = 'token.json'
事前準備で用意していただいた認証情報は、CLIENT_SECRETS_FILEでパスを指定。
TOKEN_JSON_FILEは、2回目以降のために、認証トークンを保持します。
if os.path.exists(TOKEN_JSON_FILE):
credentials = Credentials.from_authorized_user_file(TOKEN_JSON_FILE, SCOPES)
TOKEN_JSON_FILEが存在する場合に、TOKEN_JSON_FILEから過去に通った認証を取得します
if not credentials or not credentials.valid:
過去に通った認証が有効ならば、そのまま、認証を返します。
以下、認証が無効な場合
if credentials and credentials.expired and credentials.refresh_token:
credentials.refresh(Request())
認証は無効だけど、有効期限切れ、かつ、リフレッシュトークンがある場合、認証をリフレッシュします
else:
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRETS_FILE, SCOPES)
credentials = flow.run_local_server(port=0)
そもそも、認証通ってない場合、最初に認証通すとき、
認証情報を元に、承認フローのインスタンスを生成して、ローカルサーバから承認フローを実行します
一連の処理は、以下、importしているGoogleのライブラリーを見ると、処理がわかりやすいと思います。
https://github.com/googleapis/google-auth-library-python
https://github.com/googleapis/google-auth-library-python-oauthlib
with open(TOKEN_JSON_FILE, 'w') as token:
token.write(credentials.to_json())
再利用するために、有効な認証を、TOKEN_JSON_FILEに保存します。
Drive APIで、Docsファイルを取得する
import os.environ
import pymysql.cursors
from googleapiclient.discovery import build
DRIVE_API = { 'service_name': 'drive', 'version': 'v3' }
connection = pymysql.connect(host = os.environ['DB_HOST'],
user = os.environ['DB_USER'],
password = os.environ['DB_PASSWORD'],
db = os.environ['DB_NAME'],
charset = os.environ['DB_CHARSET'])
INSERT_SQL = "INSERT INTO test.docs (document_id, name) values (%s,%s)"
...
def get_docs():
creds = get_credentials()
drive_service = build(DRIVE_API.service_name, DRIVE_API.version, credentials=creds)
while True:
values = []
response = drive_service.files().list(q="mimeType='application/vnd.google-apps.document'",
spaces='drive',
fields='nextPageToken, files(id, name)',
pageToken=page_token).execute()
for file in response.get('files', []):
values.append([file.get('id'), file.get('name')])
cursor = connection.cursor()
cursor.executemany(INSERT_SQL, values)
page_token = response.get('nextPageToken', None)
if page_token is None:
break
簡単な解説
drive_service = build(DRIVE_API.service_name, DRIVE_API.version, credentials=creds)
認証を元に、APIと対話するためのResourceオブジェクトを作成します。
ここも、以下、importしているGoogleのライブラリーを見ると、処理がわかりやすいと思います。
https://github.com/googleapis/google-api-python-client
response = drive_service.files().list(q="mimeType='application/vnd.google-apps.document'",
spaces='drive',
fields='nextPageToken, files(id, name)',
pageToken=page_token).execute()
Drive APIを用いて、必要な情報を取得します。
今回は、Docsファイルだけ取得したいので、mimeTypeが、'application/vnd.google-apps.document'のファイルの一覧を取得。Spreadsheetの場合は、mimeTypeが、'application/vnd.google-apps.spreadsheet'。
公式だと、このページにGoogle特有のmimeTypeがまとめてあります。
page_token = response.get('nextPageToken', None)
if page_token is None:
break
nextPageTokenが存在する場合、一覧に次ページが存在するのでループして、次ページについて、処理。
nextPageTokenがNoneの場合、最後のページまで処理が終わったので、break。
Drive APIから取得したdocumentIDで、Docs APIでドキュメントの本文取得
DOCS_API = { 'service_name': 'docs', 'version': 'v1' }
...
def get_doc_content_lines(document_id):
creds = get_credentials()
doc_service = build(DOCS_API.service_name, DOCS_API.version, credentials=creds)
document = doc_service.documents().get(documentId=document_id).execute()
return document.get('body')
簡単な解説
doc_service = build(DOCS_API.service_name, DOCS_API.version, credentials=creds)
Drive APIと同じように、認証を元に、APIと対話するためのResourceオブジェクトを作成します
document = doc_service.documents().get(documentId=document_id).execute()
return document.get('body')
Docs APIでドキュメントのデータ取得。
現在のAPI仕様だと、page数などは取得できないので、本文のみ取得する
Mecabで、文章中の名詞のみ取得
import MeCab
tagger = MeCab.Tagger()
tagger.parse('')
...
def extract_nouns(lines, docs_id):
nouns = {}
insert_sql = "INSERT INTO test.nonus (docs_id, word, count) values (" + docs_id + ", %s, %d)"
for line in lines.splitlines():
for chunk in tagger.parse(line).splitlines()[:-1]:
(surface, feature) = chunk.split('\t')
if feature.startswith('名詞'):
nouns[surface] = nouns.get(surface, 0) + 1
cursor = connection.cursor()
cursor.executemany(insert_sql, nouns.items())
簡単な解説
用途にもよるのですが、今回は、索引なので、名詞だけ抽出しております。
pythonから辞書への追加ができないのが残念ですが、ドメインのワードを辞書追加すると、よりハッピーになれます。(pythonのライブラリにないだけで、頑張ればできそうな気もします。)
for chunk in tagger.parse(line).splitlines()[:-1]:
(surface, feature) = chunk.split('\t')
if feature.startswith('名詞'):
nouns[surface] = nouns.get(surface, 0) + 1
line = '私はLITALICO のシステムエンジニア です。' という文章に対して、mecab.parseの実行結果は、下記のようになります。
私 名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
LITALICO 名詞,固有名詞,組織,*,*,*,*
の 助詞,連体化,*,*,*,*,の,ノ,ノ
システムエンジニア 名詞,一般,*,*,*,*,システムエンジニア,システムエンジニア,システムエンジニア
です 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。 記号,句点,*,*,*,*,。,。,。
タブ区切りの後段に品詞の情報が出るので、それを元に、名詞か、それ以外か判定しております。
索引を作る際に、一般名詞は無視しても良い場合も多いのですが、辞書に存在しない語、辞書未登録語の場合、一般名詞として処理されます。そのため、一般名詞を除外すると、辞書未登録語を除外する可能性があるため、注意が必要です。
例) 私は、キャラメルフラペチーノが大好きです、キャラメルフラペチーノが辞書未登録語として、一般名詞となる
私 名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
キャラメルフラペチーノ 名詞,一般,*,*,*,*,*
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
大好き 名詞,形容動詞語幹,*,*,*,*,大好き,ダイスキ,ダイスキ
です 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
キャラメルフラペチーノが一般的ではないという意味ではありません。固有名詞のはずなのに、Mecabの辞書に載っていないから、カテゴリー全体をさす単語ではないのに、一般名詞になるのはおかしいよねという意味です。キャラメルフラペチーノは人類史上最も偉大な発明の一つです。そのうち、一般名詞になるかもしれませんね。
終わりに
実は、Google Docsの検索機能は、かなり良くて、単に、キーワードが含まれている資料を探すだけだと、Google Docsの検索機能を使った方が早いです。それをわかっていながら、わざわざ、索引を作ろうとしたのは、ゆくゆく文章解析をして、この文章が仕様書であるとか、議事録であるとか、そういったこともやりたいと考えているのと、うまくユビキタス言語が拾えないかなと思ったからです。また、slackや、Backlogなど、他のツールも合わせて、索引をまとめることもできそうですよね。
予告
来年は、心理学とベイズ統計のネタか、対話型インターフェースのネタを書きたいです。