Google Home Mini と LINE の連携

想定シナリオ(やりたいこと)

以下の状況で

  • 子どもはスマホをもっていない
  • 学校から帰宅した時、家に両親が不在で子ども1人になる

子どもが帰宅して、スマートスピーカーGoogle Home Mini)に呼びかけると両親のLINEグループに メッセージが送信される

これがやりたいことになります。

参考になった記事

この2つの記事がとても参考になりました。

ただ、2017年という古い記事だったせいか画面の表示内容が違っていたり、記事通りではうまくいかない箇所もありました。

うまく動作させるのに多少試行錯誤が必要だったのでその情報を書いてみます。

同じことをしたい人にとって少しでも参考になればと思います。

大事なポイント

参考記事の中では

  1. スマートスピーカーとIFTTTを連携させる
  2. その後にIFTTTのアプレットを作成

という順序でしたが 先にIFTTTアプレットを作る必要があります。これがハマりどころでした。

なので、(今時点での)正しい順番は

  1. IFTTTアプレットを作成する
  2. Google Home Mini と IFTTTを連携させる

です。

これについては以下のフォーラムの投稿が解決に役立ちました。

Answered: Can't link IFTTT to Google home - Google Nest Community

Dhomasさんの 1 RECOMMENDED ANSWER

Instead, I created the applet first in IFTTT, THEN I linked Google Assistant to IFTTT. It then found "devices" to attach to my Assistant.

がそれです。

IFTTTアプレットの作成

IFTTTで Google Assistant(Google Home Mini)が特定のメッセージを受けたらLINE にメッセージを送るというアプレットを作成します。

IFTTTのアカウント作成をしてダッシュボードに入ったら、My Applets から Create をクリック

If This の Add をクリック、Choose a service で Google Assistant を選びます

続いて、トリガーを作成します

参考記事の中ではトリガーが何種類かあるようでしたが、"Activate scene" の1個しか選択肢はありませんでした

Scene name を入力します。

OK Google, アクティベート {入力したシーン名}」でトリガーが発動されるようになります

続いて、Then Thatのパート。Add をクリックし、

LINE を選択します

選択できるのは Send message だけです。これを選びます

LINEアカウントとの連携後、宛先(Recipient)を選びます。 プルダウンを開くとLINEグループのリストが表示されるので該当するグループ(今回のシナリオでは両親のグループ)を選択します

Continueをクリックし、

レビューをしたらFinishを押して完了となります

Google Home Mini と IFTTTの連携

まず、Google Homeのアプリをスマートホンにインストールしておきます。

アプリを開き、画面右下の設定ボタンをタップ

サービスのセクションの「Googleと連携させる」 をタップ

虫眼鏡の検索ボタンからIFTTTを選択し、

次へをタップ

IFTTTにログインしてAuthorizeをタップしたら

リンク中のメッセージが出て、その後設定完了となります

大事なポイントで書いてますが、最初にGoogle Homeの設定を進めてしまうと、ここで以下のエラーメッセージが出てリンク失敗となります

IFTTT no devices were found in your ifttt account you may need to setup these devices ifttt first. if you already set up devices, they may not be available due to temproary error. please wait a few minutes and try again

成功すると以下のようにスマートスピーカーへの呼びかけに従ってLINEグループにメッセージが飛んでくるようになります

Make(旧Integromat)でWebサービスにログインした後の遷移先ページを取得する方法

前回の記事の中で Makeを使って、Webサービス(サークルスクエア)からイベントリストを抜き出して、Googleカレンダーに登録するということをやりました。

Webサービスにログインしてログイン後に遷移するページのHTMLを取得することはいちおう可能だったのですが その後のイベントリスト取得を Text parser のみで処理するのは気が遠くなるのでMakeのみでWebサービスからのスクレイピングをすることを断念しました。

ただ、ログイン後の遷移先ページのHTML取得のやり方は苦労したので今回紹介します。

【つぶやき】Makeはやりたいことに対しての詳しいドキュメントが見つからず、思ったよりつまずきが多かったです。

他のサイトで全く同じようにいくかはわかりませんが少しでも参考になればと思います

やりたいこと

Webサービスのログイン画面

ログイン画面

アカウントとパスワードを入力するとホームに飛びます。

このホームのHTMLを取得することが目標になります。

流れとしては以下になります。

  1. ログインページをGETメソッドで取得
  2. アカウントとパスワードをPOSTで送信
  3. 認証成功したらhomeへ自動遷移する
  4. homeのHTMLを取得する

シナリオ全体像

シナリオは以下のようなモジュールで構成されます。

Makeのみでスクレイピングを完結しようとすればこの4つのモジュールのあとに Text parser が鬼のように続くことになると思います

TL;DR

先に大事なポイントをまとめます

  • 前段後段の2つのHTTPモジュールで共にShare cookies with other HTTP modules を Yes にする
  • 前段のGETの応答の中のCookie headers の値を後段のPOSTするときに再利用する
  • POSTする後段のリクエストで Follow redirectを Yes、Follow all redirect も Yes にする

あとは、前段のGETの応答のbodyからCSRFトークンを抽出し、後段のPOSTするときにアカウント、パスワードとともにCSRFトークンをパラメータとしてセットすればokです

各モジュールの説明

HTTP: Make a request (#1)

ログインページを取得します。

ログインページ取得後、やることは2つになります。

  1. CSRFトークンを抽出する(Text parserでパターンマッチして抽出する)
  2. 応答のCookieヘッダからセッション情報の値を抽出する

1つ目は次のText parserのモジュールで対応します。

2つ目は以下の赤枠で囲った部分になりますが、後続のモジュールから直接参照できます。ただ、なぜか circlesquare_session のほうは参照できなかったので一工夫必要になります

Text parser:Match pattern (#1)

正規表現にてCSRFトークンを抽出します

Text parser:Replace (#2)

1番目のHTTPリクエストの応答で含まれているCookieヘッダ(XSRF_TOKENとcirclesquare_session)のうち、circlesquare_sessionが後段のHTTPモジュールからなぜか参照できないので、値をコピーします

HTTP: Make a request (#2)

ここでやっていること

  • セッション情報の値をCookieヘッダとしてセット
  • CSRFトークンをPOSTパラメータに追加

気をつけることは 2つのHTTPモジュールで共にShare cookies with other HTTP modules を Yes にすることです。

これをしてないと、419 エラーになります。

419 はこれまで出会ったことなかったですが 以下の定義により Laravel固有のステータスコードCSRF検証が失敗したときに出るみたいです。

HTTP response status code 419 Page Expired is an unofficial client error that is Laravel-specific and returned by the server to indicate that the Cross-Site Request Forgery (CSRF) validation has failed.

また、 Follow all redirect を No にしていると 以下のようなHTMLとともに 302 が返ってくるので Follow all redirect を Yes にします。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='https://www.c-sqr.net/home'" />

        <title>Redirecting to https://www.c-sqr.net/home</title>
    </head>
    <body>
        Redirecting to <a href="https://www.c-sqr.net/home">https://www.c-sqr.net/home</a>.
    </body>
</html>

Follow all redirect を Yes にしていると、200のレスポンスコードが返ってきて遷移後のページのHTMLが応答として得られます。

以上で無事にログイン後の遷移先ページのHTMLを取得できました。

ただ、ここからText Parserを何個も用いてイベント情報のリストを抽出するのは大変そうだったので結局は自前のWebサービスを実装して情報抽出しています。

Make(旧Integromat)でサークルスクエアのイベントをGoogleカレンダーに登録する

モチベーション

子どものラグビースクールのスケジュールがサークルスクエアで管理されているのですが、サークルスクエアは度々セッションが切れてしまいその度にログインを求められ、イベントリストを確認するときに煩わしさを感じていました。

そのため、ノーコード自動化ツールのMake(旧Integromat)を使ってサークルスクエアのイベント情報をGoogleカレンダーに転記する仕組みを作ったので紹介します。

構成

2つのシナリオから構成されます。

前半シナリオの各モジュールの詳細

イベントリストの取得

Makeのみでサークルスクエアにログインしてイベントリストを取得するのが難儀だったのですべてをMakeで完結させることは諦め、ここは自前のWebサービスを作ることにしました。

将来、サークルスクエアがAPI公開してくれればMakeのみで完結できるのでそれを期待したいところ

コードは以下 github.com

Heroku等にデプロイすればWebサービスとして動きます。

なお、アカウントとパスワードをこのWebサービスに送信することになるのでコード流用するときはそれを留意してお使いください。(アカウント情報をこっそり保存したりするmalwareではありませんがコードご確認の上ご利用ください)

HTTP Make a request モジュールを使って自前のイベントリスト取得用WebサービスAPIアクセスします

入力は上記のアカウント情報と取得したいイベントの日付(対象月)になります。

HTTPパラメータは以下のような感じです。

応答は以下のようなJSON文字列が返ってきます

[
  {"name":"イベント1","start_time":"2023/9/3 09:00","end_time":"2023/9/3 11:30","url":"https://www.c-sqr.net/events/xxx"},
  {"name":"イベント2","start_time":"2023/9/4 09:00","end_time":"2023/9/4 11:30","url":"https://www.c-sqr.net/events/yyy"}
]

前月に翌月分のイベント情報を取得したい場合と当月分のイベント情報を取得したい場合があるので リクエストパラメータのdateの値を自動的に決めることができず、HTTPモジュールのパラメータを適宜、手動設定する必要がありそこは残課題です。

JSONパース

Webサーバーからの出力されたJSON文字列をパースします

Iterator

イベント1個ずつ、Googleスプレッドシートに記録済みかを判定するので Iteratorを置きます。なお、Iterator は Flow Control の中にあります。

名前(name)でループさせます

Googleスプレッドシート

記録用のスプレッドシートはあらかじめ作っておき、それを指定します。

スプレッドシートのヘッダ行は

  • A列: Event
  • B列: Starttime
  • C列: Endtime
  • D列: URL

とします。

新規イベントかのチェック

Google Sheetsの Search Rows モジュールを使って新規イベントかどうかを判定します。

スプレッドシートの各列の情報とイベントリストのJSONの対応するキーの情報がすべてマッチするかどうかで判定します。

すべてマッチする場合は既存イベントとなり、1つでも一致しない列があれば新規データとしてみなします。

なので、サークルスクエア側でイベント更新があった場合は新規データとみなされます。つまり、イベント更新があった場合、元のカレンダーイベントも残っちゃうのでそこは今後の課題です。

新規データがあったかの判定

フィルターを作ります。

Googleスプレッドシートへの記録

Google Sheets の Add a row モジュールを使います。

後半シナリオの各モジュールの詳細

Googleスプレッドシートの新規データのトリガー

Google Sheets のWatch New Rows を使います。 新規の行が追加されていたらその行を抽出します。

SpreadSheet IDでスプレッドシートを指定し、Sheet Name でシート名を指定します。Limit は適当な値をセットします。

Iterator

Iterator を追加し、行ごとに処理させます。

時刻情報があるイベントとそうでないイベントの判別

Router を追加し、時刻情報があるイベントと時刻情報がないイベントに分岐させます。 それぞれにフィルターを設定します

  • 時刻情報があるイベントのフィルター
  • 時刻情報がないイベントのフィルター

Googleカレンダーへの記載

Google Calendar の Create an Event モジュールを分岐の先にそれぞれ追加します。

時刻情報がある場合は、All Day EventをNo、 時刻情報がない場合は All Day EventをYes にします。

あとは

  • Event Name: 1. Event (A)
  • Start Date: 1. Starttime (B)
  • End Date: 1. Endtime (C)
  • Description: 1. URL (D)

とし、スプレッドシートの記載内容をイベント情報に反映させます。

使ってみて

今のところ、適当なタイミングでMakeのダッシュボードから前半シナリオと後半シナリオを手動で実行して使っています。 スケジュールを確認したいとき、Googleカレンダーだけを確認すればよいのでそこはやはり便利です。

イベントリストの取得タイミングを月末などあるタイミングで固定すれば、取得したいイベントの日付(対象月)をMake上で自動で設定して定期実行できそうです。

NILMのパブリックデータセットの入手方法

前回の記事で紹介したNILMTKを使ってNILMのパブリックデータセットを扱う方法の記事です

データセットの入手先まとめ

NILMTKで使うためのデータ変換方法

NILMTK の nilmtk/dataset_converters によりNILMTKで取り扱う共通データ形式(.h5)に変換します。

例えば、REDDの場合だと

from nilmtk.dataset_converters import convert_redd
convert_redd('low_freq', 'redd.h5') 

というようにh5ファイルをあらかじめ生成し、その後は以下のようにそのファイルを読み込むことでNILMTK上でデータセットとして扱うことができます

train = DataSet('/data/redd.h5')
test = DataSet('/data/redd.h5')

REDD

入手先

http://redd.csail.mit.edu/

ベーシック認証のパスワードは redd / disaggregatetheenergy

前処理

データの解凍

$ tar jxvf low_freq.tar.bz2
コンバート
from nilmtk.dataset_converters import convert_redd
convert_redd('/path/to/low_freq', 'redd.h5')  # low_freqはlow_freq.tar.gz2を解凍したときのディレクトリ

REFIT

入手先

https://pureportal.strath.ac.uk/en/datasets/refit-electrical-load-measurements-cleaned

Access Dataset の CLEAN_REFIT_081116.7z をダウンロード

前処理

7zを解凍するツールのインストール

$ sudo apt-get install p7zip

解凍

$ 7zr x -orefit CLEAN_REFIT_081116.7z  # 解凍ディレクトリを指定 (refitとする)
コンバート
from nilmtk.dataset_converters import convert_refit
convert_refit('/path/to/refit', 'refit.h5')  # 解凍したディレクトリのrootパスを指定する

UK-DALE

入手先

https://data.ukedc.rl.ac.uk/browse/edc/efficiency/residential/EnergyConsumption/Domestic/UK-DALE-2017/UK-DALE-FULL-disaggregated

ukdale.zipをダウンロード

コンバート
from nilmtk.dataset_converters import convert_ukdale
convert_ukdale("/path/to/ukdale", "ukdale.h5")  # 解凍したディレクトリのrootパスを指定する

AMPds

入手先

http://ampds.org/

AMPds Version 2 ではなく、以下の Version 1: The original AMPds (R2013) をダウンロードする必要がある。

https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/MXB7VO

Access Dataset をクリックして Original Format ZIP を選択し、dataverse_files.zipをダウンロード

前処理

以下の4ファイルは削除する必要がある。 TSのキーがないためコンバート時にエラーになってしまう

$ rm ampds/FRG.csv
$ rm ampds/WHG.csv
$ rm ampds/WHW.csv
$ rm ampds/HTW.csv
コンバート
from nilmtk.dataset_converters import convert_ampds
convert_ampds("/path/to/ampds", "ampds.h5")  # 解凍したディレクトリのrootパスを入力する

iAWE

入手先

https://iawe.github.io/

Downloadのリンクをクリックし、

https://drive.google.com/drive/folders/1c4Q9iusYbwXkCppXTsak5oZZYHfXPmnp

からelectricity.tar.gzをダウンロード

コンバート
from nilmtk.dataset_converters import convert_iawe
convert_iawe("/path/to/electricity", "iawe.h5")  # 解凍したディレクトリのrootパスを指定

DRED

入手先

https://www.st.ewi.tudelft.nl/~akshay/dred/

DownloadsからNILMTK support の DRED H5 をクリック https://www.st.ewi.tudelft.nl/~akshay/dred/data/

All_data.csv をダウンロードする

コンバート
from nilmtk.dataset_converters import convert_dred
convert_dred("/path/to/All_data.csv", "dred.h5") # ディレクトリ名を指定ではなくファイル名を指定しないといけない

ECO

入手先

http://www.vs.inf.ethz.ch/res/show.html?what=eco-data

Downloadsのリンクをクリックし、続いて、 I accept termsをクリック

ダウンロードのアイコン(右上の2つのアイコンの左側)をクリックして Downloads All

前処理
unzip 01_plugs_csv.zip -d 01_plugs_csv
unzip 02_plugs_csv.zip -d 02_plugs_csv
unzip 03_plugs_csv.zip -d 03_plugs_csv
unzip 04_plugs_csv.zip -d 04_plugs_csv
unzip 05_plugs_csv.zip -d 05_plugs_csv
unzip 06_plugs_csv.zip -d 06_plugs_csv
unzip 01_sm_csv.zip -d 01_sm_csv
unzip 02_sm_csv.zip -d 02_sm_csv
unzip 03_sm_csv.zip -d 03_sm_csv
unzip 04_sm_csv.zip  # これだけそのまま
unzip 05_sm_csv.zip -d 05_sm_csv
unzip 06_sm_csv.zip -d 06_sm_csv
rm *.zip
rm -r __MACOSX/
コンバート
from nilmtk.dataset_converters import convert_eco
convert_eco("/path/to/IE594964/REP594965", "eco.h5", "Asia/Tokyo")

timezoneの引数として何のタイムゾーンを指定すべきなのか不明.. とりあえずAsia/Tokyoで変換成功することだけは確認

GREEND

入手先

https://sourceforge.net/projects/greend/

Downloadボタンをクリック

コンバート
from nilmtk.dataset_converters import convert_greend
convert_greend("/path/to/greend/GREEND_0-2_300615", "greend.h5", use_mp=False)

HIPE

入手先

https://www.energystatusdata.kit.edu/hipe.php

1 week または 3 months のリンクをクリックしてダウンロード

コンバート
$ cd nilmtk/nilmtk/dataset_converters/hipe
$ python convert_hipe.py /path/to/hipe hipe.h5  # hipeディレクトリの下に解凍した複数個のcsvがある

ちなみに、他のデータセットと同様のやり方

from nilmtk.dataset_converters.hipe.convert_hipe import convert_hipe
convert_hipe("/path/to/hipe", "hipe.h5") 

だとAssesionErrorで止まりました

IDEAL

入手先

https://datashare.ed.ac.uk/handle/10283/3647

から household_sensors.zip (14.77Gb) と room_and_appliance_sensors.zip (9.317Gb) をダウンロード

前処理
# ideal_datasetの下に2つのzipがある

$ unzip household_sensors.zip
$ mv sensordata/ household_sensordata
$ unzip room_and_appliance_sensors.zip
$ mv sensordata/ rooms_appliance_sensordata
コンバート
from nilmtk.dataset_converters import convert_ideal
convert_ideal("/path/to/ideal_dataset", "ideal.h5")

SMART

入手先

https://traces.cs.umass.edu/index.php/Smart/Smart

UMass Smart* Dataset - 2017 release の HomeA-electrical.tar.gz, HomeB-electrical.tar.gz, HomeC-electrical.tar.gz をダウンロード(Formに名前、所属、国名を入力する必要あり)

converterのコードを読む限り、HomeA, HomeB, HomeCにのみ対応しているっぽい

コンバート
from nilmtk.dataset_converters import convert_smart
convert_smart("/path/to/smart", "smart.h5")  # 絶対パスで指定する必要あり。smartの下に展開したHomeA, HomeB, HomeCディレクトリ

COMBED

入手先

https://combed.github.io/

Download RAW CSVをクリック

コンバート
from nilmtk.dataset_converters import convert_combed
convert_ideal("/path/to/combed", "combed.h5")  # combedの下にzipを展開したiiitdディレクトリがある

Dataport

入手先

https://dataport.pecanstreet.org/

ただし、学生じゃないとダウンロードするためのアカウントが発行してもらえない

商用ライセンスもあるがお高い.. https://www.pecanstreet.org/wp-content/uploads/2020/05/Pecan-Street-Data-Sheet-May-2020.pdf

DEDDIAG

入手先

https://figshare.com/articles/dataset/DEDDIAG_a_domestic_electricity_demand_dataset_of_individual_appliances_in_Germany/13615073

から house_08.zip, import.sh, create_tables_0.sql, create_tables_1.sql をダウンロード(?)

手元の環境では後続の処理の $ pip install deddiag-loader

でパッケージをインストールできず未確認

HES

ダウンロード先見つからず...

参考記事

NILMTKのインストール

NILMTKのインストール方法の記事です。 ニッチなジャンルの話でここに辿り着いた方はNILMが何かを知っていると思うのでNILMが何かについては省略です。。

うまくいかなかった方法

公式のドキュメントではAnacondaでのインストールを推奨していますがうまくいきませんでした..

nilmtkとnilmtk-contribをインストールしようとすると次のようなコマンドになりますが

$ conda install -c conda-forge -c nilmtk nilmtk nilmtk-contrib

Solving environment: がくるくるとなっている状態から一向に処理が進みません.. (MacBook Pro メモリ16GB)

結局、MacBook Proでのインストールは諦め、クラウドインスタンス上でメモリ40GBぐらいまで増やすとインストールができました。ただ、数時間単位で時間がかかりました

うまくいった方法

setup.pyを使ってインストールをするのが大量のメモリを必要とせず時間も短く良さそうでした。

ただ、そのままだと最新のtensorflowがインストールされてしまい、NILMTKの一部のコードが動かないのでsetup.pyを編集してやる必要があります。 その他、いくつかのパッケージもバージョン指定しないとエラーが出ました。

以下、順を追って手順を紹介します。

今回、試したPythonのバージョンは3.8でした。

$ conda create -n nilmtk-env python=3.8

1. nilmtkのインストール

対象リポジトリをcloneしてきて、

$ git clone https://github.com/nilmtk/nilmtk.git
$ cd nilmtk

setup.pyを次のように編集して、

    install_requires=[
        "pandas==0.25.3",
        "numpy >= 1.13.3, < 1.20.0",
        "networkx==2.1",
        "scipy",
        "tables",
        "scikit-learn>=0.21.2",
        "hmmlearn>=0.2.1",
        "pyyaml",
        "matplotlib==3.1.3",
        "jupyterlab",
        "nbconvert==6.5.0",  # 追加
    ],

インストール

$ python setup.py develop
$ cd ..

2. nilm_metadataのインストール

同様にインストールします。これはsetup.pyを編集しなくても問題なくインストールできます。

$ git clone https://github.com/nilmtk/nilm_metadata.git
$ cd nilm_metadata
$ python setup.py develop
$ cd ..
$ python -c "import nilmtk"

のコマンドでエラーが出なければここまでのインストールokです

3. nilmtk-contribのインストール

まず、リポジトリの取得

$ git clone https://github.com/nilmtk/nilmtk-contrib.git
$ cd nilmtk-contrib

nilmtkのとき同様に、setup.pyの編集をする必要があります。

    install_requires=[
        # 'nilmtk>=0.4',  # コメントアウト
        # 'tensorflow>=2.0', # コメントアウト
        'tensorflow==2.4.1',  # 追加
        'keras==2.4.0',  # 追加
        'protobuf==3.9.2',  # 追加
        'six==1.15.0',  # 追加
        'h5py==2.10.0',  # 追加
        'cvxpy>=1.0.0'
    ],

インストール

$ python setup.py develop

次のコードでエラーが出なければ無事にすべてがインストール完了です

$ python -c "import nilmtk_contrib"

コードの修正が必要な箇所

nilmtk-contribのアルゴリズムを動かすのにいくつかコード修正が必要だったので紹介しておきます

nilmtk-contrib/nilmtk_contrib/disaggregate/WindowGRU.py

 48             filepath = self.file_prefix + "-{}-epoch{}.h5".format(
 49                     # "_".join(appliance_name.split()),  # コメントアウトする
 50                     "_".join(app_name.split()),   # 変更後
 51                     current_epoch,
 52             )

nilmtk-contrib/nilmtk_contrib/disaggregate/resnet_classification.py

277                 ##################
278                 #PLOTTING
279                 # self.classification_output_plot(prediction_classification,appliance)   # コメントアウトする

参考記事

インストールに詰まってしまった人は実は少ない?!のかNILMTKのインストールの記事はWeb上でほとんど見つかりませんでした。。 私の場合、これらの記事内容をそのまま真似するだけではうまくいきませんでしたがもしかしたら役に立つかもしれないので参考記事としてリンクを載せておきます

seleniumをdockerで動かす

モチベーション

sanshonoki.hatenablog.com

約1年前、ジョブカンの工数入力するツールをseleniumを使って実装しました。

自分自身で今現在も使っていてかなり役立っていますが一つだけ課題がありました。

それは、chromeブラウザのバージョンがいつの間にか上がっていてchrome driverのバージョンと不適合ということでプログラムがエラーになり、その度にchrome driverをダウンロードし直す必要があるということでした。

ということで、その煩わしさから解放されるべくdockerコンテナを使ってseleniumを動かすことにしました。

最新版のchromeブラウザをインストールする場合

Dockerfileに下記2行でok

RUN apt-get -y update
RUN apt-get install -y chromium chromium-driver

chromedriver は /usr/bin/chromedriver にインストールされます

特定バージョンのchromeブラウザをインストールする場合

特定バージョンのchromeブラウザをインストールしたい場合は先程のように簡単にはいきません。 なぜなら apt-get install -y chromium だと最新版のchromeがインストールされてしまうからです

ただし、以下の記事で紹介されている Download older versions of Google Chrome for Windows, Linux and Mac というサイトから古いchromeブラウザのパッケージを入手して、インストール可能です。

qiita.com

Dockerfileは次のようになります。

RUN apt-get -y update

# Google Chrome dependencies
RUN apt-get install -y libasound2 libatk-bridge2.0-0 libatspi2.0-0 libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 libxkbcommon0 libxshmfence1 xdg-utils fonts-liberation

# Google Chrome (specific version)
RUN wget -O /tmp/google-chrome-stable_current_amd64.deb https://www.slimjet.com/chrome/download-chrome.php?file=files%2F90.0.4430.72%2Fgoogle-chrome-stable_current_amd64.deb
RUN dpkg -i /tmp/google-chrome-stable_current_amd64.deb && rm /tmp/google-chrome-stable_current_amd64.deb

# ChromeDriver (specific version)
ADD https://chromedriver.storage.googleapis.com/90.0.4430.24/chromedriver_linux64.zip /opt/chrome/
RUN cd /opt/chrome/ && unzip chromedriver_linux64.zip

基本的に最新のセキュリティ対策がなされている最新バージョンを使うべきですが特定バージョンのchromeブラウザを使いたい場合は参考にしてください

headlessブラウザを使ったときのジョブカン固有の問題

これはジョブカンサイト固有の問題だと思われますが、headlessブラウザを使ったときになぜか挙動が違うという問題が発生しました。

通常ブラウザでのログイン後

headlessブラウザでのログイン後

画面から分かるようにheadlessブラウザだとメニューが畳まれた状態になってしまいます..

想定しているelementが見つからず element not interactable エラーが出ちゃいます

ググると以下のような記事が見つかったのでウインドウサイズの大きさを変えてみましたが変わりませんでした。 yuki.world

仕方ないのでメニューが畳まれているかを調べて、畳まれていたらメニューを開く操作をするようにしています。

コード

とりあえず、dockerベースでseleniumが動くようになり煩わしさから解放されました

github.com

別解?

毎回、MacOSchromeブラウザのバージョンを調べて、そのバージョンに合ったchromedriverを 公式ダウンロードサイト から自動でダウンロードしてくれば Docker使わなくてもいいんじゃ? と考えましたが難しそうでした

# chromeのバージョンを調べる
$ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version
Google Chrome 101.0.4951.64

ただし、すべてのchromeのバージョンに対応したdriverがあるわけではありません...

https://chromedriver.storage.googleapis.com/101.0.4951.64/chromedriver_mac64.zip

にブラウザでアクセスすると NoSuchKey のエラー画面になります

chromedriveのダウンロードページは

となっていて、パッチバージョンまでピッタリなリンクは存在しません

ダウンロードページをWebスクレイピングしてメジャーバージョンだけがマッチするリンクを取り出せば何とかやれそうですがとても面倒くさそうです

ミニマルな論文管理ツールを作る

年始休暇中に(自分向け)論文管理ツールを実装しました。

github.com

モチベーション

1年前ぐらいに Mendeley という論文管理ウェブサービスを試してみましたが 同じ論文が重複に登録されたりして何かイマイチだなぁと感じてその後は使っていませんでした

具体的には、フォルダ(Collection)間をドラッグアンドドロップして論文移動させると重複に存在するようになってしまうという現象が特にイマイチな感じでした。

デスクトップ版は今では重複チェック機能が実装されているらしい(参考記事)ですが、Web版は確認する限りではそのような機能はありません

Mendeley以外の選択肢としては、Google製の Paperpile というのが良いようなのですが有料サブスクリプションで $3/月 かかります。

$3/月なので高いとまでは思いませんがお金払ってまでまじめに論文管理したいわけでもないんだよなと思って自作ツールを開発することにしました

要件と仕様

要件

  • ファイル名から重複チェックができる
  • タイトル抽出してキーワードサーチができる(ファイル名だけだとpdfファイルを開くまで何の論文か分からないため)
  • 同様に、Abstract抽出して キーワードサーチができる

そして、仕様は「論文からファイル名、タイトル抽出、Abstract抽出してcsvファイルに保存する」というミニマム仕様です。

PDFファイルからの文字抽出

Pythonでpdfファイルから文字抽出するのは何種類かパッケージがあります。

上の記事の中で取り上げられてた PyPDF2 と PDFMiner を試してみましたが PDFMinerが良さそうです。 Qiitaで参考にした記事でも PDFMiner を使ってました。

タイトル抽出とAbstract抽出

基本的には タイトル抽出は 2番目のQiita記事で書かれているように Bouding Boxのheightが一番高い行を抽出してくれば大半の論文では抽出できます。

Abstract抽出は "Abstract" という行を抽出して、"Introduction" の行までを切り出せばokです。

f:id:sanshonoki:20220202222811p:plain

手こずったところ(タイトル抽出編)

Title行抽出で単純にはうまくいかずちょっと苦労したのがいくつかあったので紹介しておきます

Author行が一番大きいheightになっているパターン

上付き文字があると見た目の高さ以上のheightになっていて、そこがTitle行として判定されてしまいます

以下の図では数字がその行のheightを表しています

f:id:sanshonoki:20220202224819p:plain

Author行っぽいか / 所属機関行っぽいか を判定するルールを追加して対処しました。

複数行に渡るタイトルで heightの高さが揃ってない

ほとんどの論文では同じheightが複数行にわたるのでheightが同じ行が続くかどうかで判定すればよいのですがそうはならない論文も中にはあります。

f:id:sanshonoki:20220202230107p:plain

仕方ないので完全に同一のheightが続くという条件を少し緩和しました

手こずったところ(Abstract抽出編)

基本的には Abstract という行を抽出して、Introductionの行までを切り出せばokですがTitle抽出と同様にいくつかそれが当てはまらないケースがあり苦労しました。

Abstractの文字がないパターン

Abstractの文字をサーチして、Abstract行の先頭を特定できません

f:id:sanshonoki:20220202223348p:plain

先にIntroductionの行を抽出してそこからエリア推定します。

2カラム形式になっているパターン

Abstract行の直後にIntroductionが来るのでAbstractブロックを正しく抽出できません

下の図のような論文で BoundingBox抽出すると、Abstractの先頭行の後にIntroduction行が続き、その後にAbstractの本文が続きます。

f:id:sanshonoki:20220202231213p:plain

改行されていることを判定する(上のブロックとの位置関係から分かる)ことでうまく抽出できるようになりました。

開発してみて

もっと簡単にいくのかなと思っていたので意外と課題はありましたがそれらに対応したルールを1個ずつ追加してうまくできるようになりました。

とりあえず手元の420個程度の論文で試してみる限りは全体として99%程度うまくいっていて満足です。

1997年の古い論文とか日本語の論文とか、一部の論文ではうまく情報抽出できてませんでしたが2010年あたり以降の新しい論文は基本うまくいっています。

自分の手持ちの論文に完全にチューニングされているルール/パラメータになってますがarxiv機械学習系の論文だったらそれなりの精度で動く気はしてます。

Author行かどうかの判定に苦労したので 今後はBERTの固有表現抽出等を組み込んで判定できると面白いと思っています。