LoginSignup
17
2

More than 1 year has passed since last update.

Google Docs APIを使って、索引を作成する

Last updated at Posted at 2021-12-04

はじめまして、記事を読んでいただいてありがとうございます。
この記事は『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など、他のツールも合わせて、索引をまとめることもできそうですよね。

予告

来年は、心理学とベイズ統計のネタか、対話型インターフェースのネタを書きたいです。

17
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
2