極楽とんぼのロボット製作記

情報工学系大学院生がロボットとその周辺技術や身の回りの出来事について紹介するブログ

最強のJupyterNotebook環境(vim-bind)を構築する

この記事で行うこと

  • 「jupyter-vim-binding」を使って、jupyter notebookにvim-bindのキー設定をする
  • 「xkeysnail」を使ってctrl+nなどのvim-bindで操作した際に邪魔になるブラウザのデフォルトのショートカットキーを別の設定に割り当てる

f:id:gokuraku104robot:20180810180518p:plain:w500

jupyter notebookのinstall

以下のコマンドからjupyter notebookをインストールします。

$ pip install jupyter

jupyter notebookを

$ jupyter notebook

で起動します。

vim bindの設定をする

jupyter-vim-bindingのインストール

以下のコマンドからjupyter-vim-bindingをインストールします。

$ pip install jupyter_contrib_nbextensions
$ jupyter contrib nbextension install --user
$ mkdir -p $(jupyter --data-dir)/nbextensions
$ cd $(jupyter --data-dir)/nbextensions
$ git clone https://github.com/lambdalisue/jupyter-vim-binding vim_binding
$ jupyter nbextension enable vim_binding/vim_binding

これによりj,kなどを使ってセルを移動したり、iでインサートモードに入って編集したりできます。

vim-bindの編集

<ESC>がデフォルトのままで使いづらければ、custom.js<ESC>jjなどにremapすることができます。 まず~/.jupyter/custom/custom.jsファイルを作成します。

$ mkdir -p ~/.jupyter/custom/
$ cd ~/.jupyter/custom
$ vim custom.js

custom.jsには以下のようにキーバインドを設定します。
custom.js

// Configure CodeMirror Keymap
require([
  'nbextensions/vim_binding/vim_binding',   // depends your installation
], function() {
  // Map jj to <Esc>
  CodeMirror.Vim.map("jj", "<Esc>", "insert");
  // Swap j/k and gj/gk (Note that <Plug> mappings)
  CodeMirror.Vim.map("j", "<Plug>(vim-binding-gj)", "normal");
  CodeMirror.Vim.map("k", "<Plug>(vim-binding-gk)", "normal");
  CodeMirror.Vim.map("gj", "<Plug>(vim-binding-j)", "normal");
  CodeMirror.Vim.map("gk", "<Plug>(vim-binding-k)", "normal");
});

// Configure Jupyter Keymap
require([
  'nbextensions/vim_binding/vim_binding',
  'base/js/namespace',
], function(vim_binding, ns) {
  // Add post callback
  vim_binding.on_ready_callbacks.push(function(){
    var km = ns.keyboard_manager;
    // Allow Ctrl-2 to change the cell mode into Markdown in Vim normal mode
    km.edit_shortcuts.add_shortcut('ctrl-2', 'vim-binding:change-cell-to-markdown', true);
    // Update Help
    km.edit_shortcuts.events.trigger('rebuild.QuickHelp');
  });
});

ブラウザのデフォルトのショートカットキーを無効化

jupyter notebookをvim-bindするとctrl+nctrl+pなどブラウザのデフォルトのショートカットキーが邪魔になってきます。これらを別のショートカットキーとして設定し直します。

アドオンを使う方法

shortkeys,hotkeysなどのアドオンを使ってショートカットキーを編集する方法ですが、ブラウザのバージョンアップの影響のためか、まともに動作するものはありませんでした。いつかブラウザがデフォルトのキーマッピングを編集できる仕様になってくれたらいいと思います。

OS側のショートカットキーを変更する方法

ブラウザの進化を待っている時間はないので、OS側からショートカットキーを変更することでブラウザのショートカットキーを上書きします。 MACの場合、OS側でアプリケーションのショートカットキーを編集できるらしいです。 fabrec.jp Ubuntuの場合はxkeysnailを使ってこれを実現します。

xkeysnailのインストール

$ sudo pip3 install xkeysnail

xkeysnailを使ってみる

以下のようにpythonでconfigファイルを作成します。
xkeysnail_config.py

import re
from xkeysnail.transform import *
define_keymap(re.compile("Firefox|Google"), {
    K("C-p"): with_mark(K("up")),
    K("C-n"): with_mark(K("down")),
}, "Firefox and Chrome")

作成したPythonプログラムをxkeysnailから実行します。

$ xhost +SI:localuser:root
$ sudo xkeysnail xkeysnail_config.py

FirefoxやChrome上でctrl+nやctrl+pをしても新しいウィンドウや印刷画面がでなければ成功です。

自動起動の設定

xkeysnailを自動で起動する設定を行います。先程はrootで実行しましたが、ここではxkeysnail用にsudoが可能なユーザーxkeysnailを作成することにします。

自動起動用のユーザを作成

以下のコマンドからユーザーグループとユーザーを作成します。

$ sudo groupadd uinput
$ sudo useradd -G input,uinput -s /sbin/nologin xkeysnail

以下のファイルを作成して、保存します。
/etc/udev/rules.d/40-udev-xkeysnail.rules

KERNEL=="uinput", GROUP="uinput"


/etc/modules-load.d/uinput.conf

uinput


/etc/sudoers.d/10-installer

username ALL=(ALL) ALL, (xkeysnail) NOPASSWD: /opt/xkeysnail/bin/xkeysnail

usernameには自分のユーザー名を入力してください。sudoersの設定は間違えると修正が面倒なので慎重に行ってください。

自動起動用のスクリプトの作成

xkeysnailをubuntuで自動起動するための設定を行います。 先ほど作ったPythonのconfigファイルを/etc/opt/xkeysnail/にコピーします。

$ sudo mkdir -p /etc/opt/xkeysnail/
$ cp xkeysnail_config.py /etc/opt/xkeysnail/

同じく/etc/opt/xkeysnail/にxkeysnailを起動するシェルスクリプトを作成します。 /etc/opt/xkeysnail/start_xkeysnail.sh

#!/usr/bin/env bash
if [ -x /usr/local/bin/xkeysnail ]; then
    xhost +SI:localuser:xkeysnail
    sudo -u xkeysnail DISPLAY=:1 /usr/local/bin/xkeysnail /etc/opt/xkeysnail/xkeysnail_config.py &
fi

さらにUbuntuが起動時に自動でシェルスクリプトを実行するための設定を~/.config/autostart/に作成します。
~/.config/autostart/xkeysnail.desktop

[Desktop Entry]
Type=Application
Version=1.0
Name=xkeysnail
GenericName=Keymapper
Exec=/etc/opt/xkeysnail/start_xkeysnail.sh
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true

コンピュータを再起動後、FirefoxやChrome上でctrl+nやctrl+pをしても新しいウィンドウや印刷画面がでなければ成功です。

参考サイト
jupyter上でvimを使う
GitHub - lambdalisue/jupyter-vim-binding: Jupyter meets Vim. Vimmer will fall in love.
Vimの手癖が出ても安心!Firefoxのアドオン「Shortkeys」 | atominux
xkeysnail - もうひとつの Linux 向けキーリマッパ
GitHub - mooz/xkeysnail: Yet another keyboard remapping tool for X environment
xremap の代わりに xkeysnail を使ってみる - @tmtms のメモ
xmodmapとxcapeで消耗するのはもうやめよう - ぽよメモ
xkeysnailでキーリマップする

PythonからGoogle Cloud Vision APIを使ってみた。

GoogleのCloud Vision API使ってみましたが思ったよりも良い精度でした。Pythonを使ったrequestの方法と出力結果のjsonデータを見るためのjson viewerを紹介します。

Google Cloud Vision API の使い方

API keyの取得

まずはCloud Vision APIを使うためのAPI keyを取得します。 以下にアクセスします。 console.cloud.google.com f:id:gokuraku104robot:20180728173321p:plain:w500

「vision」と検索してCloud Vision APIを選択します。 f:id:gokuraku104robot:20180728173357p:plain:w500

「有効」ボタンを押します。有効ボタンが押せない場合はGoogle Cloud Platformの無料トライアルの登録がまだなので、右上の「無料トライアルに登録」ボタンから登録します。 f:id:gokuraku104robot:20180728173649p:plain:w300

Cloud Vision APIを有効にできたら、「管理」を押します。 f:id:gokuraku104robot:20180728174018p:plain:w500

「認証情報」を押します。 f:id:gokuraku104robot:20180728174254p:plain:w500

「認証情報を作成」をクリックしてAPI keyを作成します。 f:id:gokuraku104robot:20180728174634p:plain:w500

API にrequestするプログラム

API keyを取得できたら、Cloud Vision APIにrequestするプログラムを作成します。 str_api_keyにAPI keyを入力してください。 Cloud Vision APIは画像をbase64形式にエンコードする必要があります(pil_image_to_base64(pil_image))。 エンコードした画像、API keyをrequestsでGoogle Cloud Platformに投げます。 認識に成功するとjsonデータが送られてきます。出力されたjsonデータはjson.loads(json_data)で辞書型として格納できます。

import base64
import json
from requests import Request, Session
from io import BytesIO
from PIL import Image

#PILで開いた画像をbase64形式に変換します
def pil_image_to_base64(pil_image):
    buffered = BytesIO()
    pil_image.save(buffered, format="PNG")
    str_encode_file = base64.b64encode(buffered.getvalue()).decode("utf-8")
    return str_encode_file

#PILで開いた画像をCloud Vision APIに投げます
def recognize_image(pil_image):
        str_encode_file = pil_image_to_base64(pil_image)
        str_url = "https://vision.googleapis.com/v1/images:annotate?key="
        str_api_key = "Key"
        str_headers = {'Content-Type': 'application/json'}
        str_json_data = {
            'requests': [
                {
                    'image': {
                        'content': str_encode_file
                    },
                    'features': [
                        {
                            'type': "TEXT_DETECTION",
                            'maxResults': 10
                        }
                    ]
                }
            ]
        }

        obj_session = Session()
        obj_request = Request("POST",
                              str_url + str_api_key,
                              data=json.dumps(str_json_data),
                              headers=str_headers
                              )
        obj_prepped = obj_session.prepare_request(obj_request)
        obj_response = obj_session.send(obj_prepped,
                                        verify=True,
                                        timeout=60
                                        )

        if obj_response.status_code == 200:
            with open('data.json', 'w') as outfile:
                json.dump(obj_response.json(), outfile)
                text = get_fullTextAnnotation(obj_response.text)
            return text

        else:
            return "error"

#返ってきたjsonデータの"fullTextAnnotation"部分のテキストを抽出します。
def get_fullTextAnnotation(json_data):
    text_dict = json.loads(json_data)
    try:
        text = text_dict["responses"][0]["fullTextAnnotation"]["text"]
        return text
    except:
        print(None)
        return None

if __name__ == '__main__':
    image_path = "./test.png"
    pil_image = Image.open(image_path)
    print(recognize_image(pil_image))

f:id:gokuraku104robot:20180729112342p:plain:w300
入力した画像

出力として以下のようなjsonデータが出力されます。長過ぎるので途中を省略します。

{"responses": [{"textAnnotations": [{"locale": "ja", "description": .... "text": "Bellmart Kiosk\n\u9818\u53ce\u66f8\n\u30d9\u30eb\u30de\u30fc\u30c8\u516b\u91cd\u6d32\u5357\nTEL: 03-3281-6388\n2018\u5e747\u670825\u65e5(\u6c34) 14:59 No:0002\n4901777300446\n\u4f0a\u53f3\u885b\u9580\n#151\n\u5c0f\u8a08\n\u5185\u7a0e\u5bfe\u8c61\u984d\n\u5185\u7a0e\n\u5408\u8a08\n\u00a5151\n\u00a5151\n#11\n\u00a5151\n8\u00b700%\n8.00%\n\u4ea4\u901a\u7cfbIC\u652f\u6255\n\u00a5151\n#11)\n(\u6d88\u8cbb\u7a0e\u7b49\n250\n\u5229\u7528\u65e5\u6642 2018/07/25 14:59:47\n\u4ea4\u901a\u7cfbIC\u30ab\u30fc\u30c8\u3001\u652f\u6255\n\u4ea4\u901a\u7cfbIC\u30ab\u30fc\u30c8\u3001\u6b8b\u984d\n(\u30ab\u30fc\u30c8\u3001\u756a\u53f7: JE 5401)\n#151\n\u00a52\u3001716\n\u53d6\u5f15No7137\n1\u70b9\u8cb7\n"}}]}

このjsonデータ内のfullTextAnnotationのtext部分に全ての認識された文字列が格納されています。内容は以下のとおりです。

Bellmart Kiosk
領収書
ベルマート八重洲南
TEL: 03-3281-6388
2018年7月25日(水) 14:59 No:0002
4901777300446
伊右衛門
#151
小計
内税対象額
内税
合計
¥151
¥151
#11
¥151
8·00%
8.00%
交通系IC支払
¥151
#11)
(消費税等
250
利用日時 2018/07/25 14:59:47
交通系ICカート、支払
交通系ICカート、残額
(カート、番号: JE 5401)
#151
¥2、716
取引No7137
1点買

おすすめjson viewer

出力結果のjsonデータですが、非常に見づらいためjson viewerを使用することをおすすめします。 おすすめはjsoneditoronlineです。左側にjsonデータを入れて右矢印をクリックするとjsonを階層構造で確認することができます。辞書型でアクセスするときにも参考になります。 jsoneditoronline.org

f:id:gokuraku104robot:20180729113949p:plain:w500

参考サイト
凄すぎ!Google Cloud Vision APIをつかって簡単高精度にOCR – Arehamahoudarouka
機械学習×Webアプリ診断:Cloud Vision APIでCAPTCHAを認識する

seleniumで始めるウェブスクレイピング(Steamで遊ぶ)

Seleniumでウェブスクレイピングをする方向けの記事です。

この記事で分かること

  1. seleniumの環境構築
  2. XPATHでの要素の指定方法
  3. seleniumでページ上の要素を取得する方法
  4. seleniumを使う上でのTIps

実行環境は以下の通りです。

Python 3.6.3 |Anaconda custom (64-bit)
>>> selenium.__version__
'3.12.0'

ウェブドライバーにはChromeDriver(2.4.0)を使用します。

環境構築

seleniumのインストール

Pythonは基本的にPython3の環境であれば問題ありません。 pipでseleniumのインストールを行います。

$ pip install selenium

インストールが完了したらimportできるかも確認しましょう。

$ python
>>> import selenium
>>> selenium.__version__
'3.12.0'

WebDriverのダウンロード

以下のリンクから最新のChromeDriverをダウンロードします。ダウンロードする際は自分のOSに合ったものを選びます。
Downloads - ChromeDriver - WebDriver for Chrome

ダウンロードしたらzipファイルを展開して適当な場所に置きます。
例)

$ unzip chromedriver_linux64.zip
$ mkdir ~/webdrivers
$ mv chromedriver ~/webdrivers/

ダウンロードできたらウェブドライバーをロードしてみましょう。 Pythonで以下のコードを書いて実行します。

from selenium import webdriver
chrome_driver_path = "/home/username/webdrivers/chromedriver"
driver = webdriver.Chrome(chrome_driver_path)

ここで以下のようなエラーがでる場合はGoogleChromeをアップデートする必要があります。

selenium.common.exceptions.SessionNotCreatedException: 
Message: session not created exception: Chrome version must be >= 66.0.3359.0

以下のコマンドを実行してGoogleChromeのアップデートしましょう。

$ sudo apt --only-upgrade install google-chrome-stable

Steamをウェブスクレイピングしてみよう!

SteamのSummerSaleが終わってしまい、これからまたSteamの瞬間的なSaleを追いかけることになりそうです。せっかくですのでSteamを題材にウェブスクレイピングしてみようと思います。

Steamで検索する

キーワードをSteamで検索するプログラムを作成します。プログラムの流れは検索窓を見つける、検索窓に入力する、検索ボタンをクリックするという3段階です。

要素を見つける

まず検索窓を指定してやる必要があります。ブラウザで Welcome to Steam を開いて、検索窓を右クリックして検証(Firefoxの場合は「要素を調査」)をクリックします。 f:id:gokuraku104robot:20180705205454p:plain:w500
するとウェブページのソースコードが見れます。ハイライトされているのが検索欄のソースコードです。ソースコードのハイライトされている部分を右クリックしてCopy→Copy elementをクリックします。 検索窓のソースは以下のようになっています。

<input id="store_nav_search_term" name="term" type="text" class="default" placeholder="ストアを検索する" size="22" autocomplete="off">
XPATHで要素を指定する

今、見つけた要素を使って検索窓に文字を入力します。 要素を指定するにはclassやidなどで指定したり、cssセレクタを使う方法などがありますが、今回は汎用性の高いXPATHを使う方法で指定します。

XPATHとは

XML Path Language(XPATH)はマークアップ言語XMLに準拠した文書の特定の部分を指定する言語構文である。 XML Path Language - Wikipedia

例えば次のようなhtmlの文書があった時、

<html>
...
  <body>
    <h1>test</h1>
    <div class="hoge">
      <span class="piyo">foo</span>
    </div>
  </body>
</html>

<span class="piyo">foo</span>の部分を指定するには

/html/body/div/span[@class='piyo']

のように階層を指定します。毎回、フルのパスを書くのは面倒なので通常は省略して表記します。//を頭につけると<html>の中の全てのタグを表すことができます。

//span[@class='piyo']

この指定方法ではspanタグでclassの要素がpiyoであるものが指定されます。場合によっては一意に定まらないので、その際はもう1つ上の階層から指定したり、複数の要素を指定したりします。

今回の検索窓は<input id="store_nav_search_term" name="term" type="text" class="default" placeholder="ストアを検索する" size="22" autocomplete="off">ですので、これを指定するXPATHは

//input[@id='store_nav_search_term'

となります。 同じように検索ボタンのコードをブラウザの検証から探します。検索ボタンは

<img src="https://steamstore-a.akamaihd.net/public/images/blank.gif">

ですので、XPATHは

//img[@src='https://steamstore-a.akamaihd.net/public/images/blank.gif']

となります。

では以下のコードを実行して、steamで検索させてみましょう。

from selenium import webdriver
chrome_driver_path = "/home/tombo/webdrivers/chromedriver"  #chromedriverへのパス
driver = webdriver.Chrome(chrome_driver_path)     #chromedriverの読み込み
steam_url = "https://store.steampowered.com/"    #SteamのURL
search_word = "Undertale"    #検索キーワード

driver.get(steam_url)    #SteamのURLを開く
search_element = driver.find_element_by_xpath("//input[@id='store_nav_search_term']")    #検索窓を指定
search_element.send_keys(search_word)    #要素にキーワードを入力
search_button = driver.find_element_by_xpath("//img[@src='https://steamstore-a.akamaihd.net/public/images/blank.gif']")   #検索ボタンを指定
search_button.click()    #要素をクリック
input()    #すぐに閉じてしまうのでキー入力を待機させます
driver.quit()

売上上位のゲームのリストを得る

Steamの売上上位のゲームのリストを作成します。下記のリンクのページにあるゲームのリストからゲームタイトル、価格、ゲームへのリンクを取得します。
Steam Search
手始めにゲームのリストが載っているソースをコピーしてきます。

<div id="search_result_container">

    <div class="search_rule"></div>

    

        <!-- List Items -->
        <div>
            <!-- Extra empty div to hack around lame IE7 layout bug -->
            <div></div>
            <!-- End Extra empty div -->
                                                        <!---リストの1つ目--->
                            <a href="https://store.steampowered.com/app/381210/Dead_by_Daylight/?snr=1_7_7_topsellers_150_1" data-ds-appid="381210" onmouseover="GameHover( this, event, 'global_hover', {&quot;type&quot;:&quot;app&quot;,&quot;id&quot;:381210,&quot;public&quot;:1,&quot;v6&quot;:1} );" onmouseout="HideGameHover( this, event, 'global_hover' )" class="search_result_row ds_collapse_flag app_impression_tracked">
                    <div class="col search_capsule"><img src="https://steamcdn-a.akamaihd.net/steam/apps/381210/capsule_sm_120.jpg?t=1528898731"></div>
                    <div class="responsive_search_name_combined">
                        <div class="col search_name ellipsis">
                            <span class="title">Dead by Daylight</span>
                            <p>
                                <span class="platform_img win"></span>                           </p>
                        </div>
                        <div class="col search_released responsive_secondrow">2016年6月14日</div>
                        <div class="col search_reviewscore responsive_secondrow">
                                                            <span class="search_review_summary positive" data-tooltip-html="ほぼ好評<br>このゲームのユーザーレビュー 101,107 件中 77% が好評です">
                                </span>
                                                    </div>


                        <div class="col search_price_discount_combined responsive_secondrow">
                            <div class="col search_discount responsive_secondrow">
                                
                            </div>
                            <div class="col search_price  responsive_secondrow">
                                ¥ 1,980                            </div>
                        </div>
                    </div>


                    <div style="clear: left;"></div>
                </a>
                                                        <!---リストの2つ目--->
                            <a href="....

ページの構造としては<div id="search_result_container"> <div>...の中に<a href="...>として商品のURLやタイトルが入っています。(trで作ってくれればスクレイピングしやすかったのですが...)まずは商品の情報が入っている<a>で囲まれている部分をまとめて取得します。このときのXPATHは

//div[@id='search_result_container']/div/a

となります。このXPATHを使ってブラウザで検索してみると25件ヒットすると思われます。 f:id:gokuraku104robot:20180707150720p:plain:w500
このように複数の要素を指定して読み込みたいときはfind_elements関数を使用します。

driver.find_elements_by_xpath("//div[@id='search_result_container']/div/a")

find_elementsは指定に該当する要素のリストを返します。

以下のコードを実行してsteamの売上上位の25個の商品のURLが取得できているか確認してみてください。

from selenium import webdriver

chrome_driver_path = "/home/tombo/webdrivers/chromedriver"
driver = webdriver.Chrome(chrome_driver_path)
steam_url = "https://store.steampowered.com/search/?filter=topsellers"

driver.get(steam_url)
topseller_elements = driver.find_elements_by_xpath("//div[@id='search_result_container']/div/a")
print(len(topseller_elements))
for top_element in topseller_elements:
    print(top_element.get_attribute("href"))

driver.quit()

URLが取得できたので、次はゲームのタイトルを取得します。ゲームタイトルまでのXPATHは今、取得したURLを利用すると指定しやすいです。XPATHのhrefの部分に今取得したURLを使います。

game_url = top_element.get_attribute("href")
game_title_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/span[@class='title']")

次にゲームの価格を取得します。ゲームの価格はセールになっている場合となっていない場合とでhtmlのコードが異なっているため、それに対応するように書きます。 セールでない時のゲーム価格は

<div class="col search_price  responsive_secondrow">
¥ 1,980
</div>

のようなコードで、セール時には

<div class="col search_price discounted responsive_secondrow">
<span style="color: #888888;"><strike>¥ 1,520</strike></span><br>¥ 1,368
</div>

のようなコードになります。今回は例外処理でそれぞれの処理を書きました。

    try:    #セールでない時
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price  responsive_secondrow']")
        game_regular_price = int(re.sub(r'\D','',game_price_element.text))    #正規表現を用いて数字のみを抜き出し、int型にキャスト
        game_discount_price = None
    except:    #セールの時
        #通常価格とセール価格の両方が文字列として入ってしまっている
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']")

        #通常価格のみを抜き出す
        game_regular_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']/span/strike")
        game_regular_price = int(re.sub(r'\D','',game_regular_price_element.text))    #正規表現を用いて数字のみを抜き出し、int型にキャスト

        #2つの価格が入っている文字列から通常価格を消す
        discount_price_text = game_price_element.text.replace(game_regular_price_element.text,"")
        game_discount_price = int(re.sub(r'\D','',discount_price_text))    #正規表現を用いて数字のみを抜き出し、int型にキャスト
    print(game_regular_price)
    print(game_discount_price)

最後に売上上位のゲームのリストをcsvに保存するプログラムです。

from selenium import webdriver
import re
import csv

chrome_driver_path = "/home/tombo/webdrivers/chromedriver"
driver = webdriver.Chrome(chrome_driver_path)
steam_url = "https://store.steampowered.com/search/?filter=topsellers"
driver.get(steam_url)

topseller_elements = driver.find_elements_by_xpath("//div[@id='search_result_container']/div/a")

fileobj = open("steam_topsale_games.csv","w")
dataWriter = csv.writer(fileobj)

for top_element in topseller_elements:
    game_url = top_element.get_attribute("href")
    game_title_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/span[@class='title']")
    game_title = game_title_element.text
    try:
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price  responsive_secondrow']")
        game_regular_price = int(re.sub(r'\D','',game_price_element.text))
        game_discount_price = None
    except:
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']")
        game_regular_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']/span/strike")
        game_regular_price = int(re.sub(r'\D','',game_regular_price_element.text))
        discount_price_text = game_price_element.text.replace(game_regular_price_element.text,"")
        game_discount_price = int(re.sub(r'\D','',discount_price_text))
    game_data_list = list()
    game_data_list.append(game_title)
    game_data_list.append(game_url)
    game_data_list.append(game_regular_price)
    game_data_list.append(game_discount_price)

    dataWriter.writerow(game_data_list)


driver.quit()
fileobj.close()

Selenium TIps

seleniumのwait

明示的な待機

次のようにして要素を取得してボタンをクリックできるようになるまで待機することができます。

from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, 'someid')))
暗黙的な待機

暗黙的な待機は一度設定すれば、すべてに適応されます。要素が利用できるまで待ったり、Javascriptの実行終了まで待ったり、ページのロードを待ったりすることができます。

driver.implicitly_wait(10)    #すぐに利用できない要素が利用できるようになるまで10秒待つ
driver.set_script_timeout(10)   #javascript実行が終了するまで10秒待つ
driver.set_page_load_timeout(10)    #ページが完全にロードされるまで最大で10秒待つ

ボタンを押すときはスクロール

ボタンを押そうとしているのに

selenium.common.exceptions.WebDriverException: Message: unknown error: Element is not clickable at point

などと表示されて押せないことはありませんか? seleniumは画面外にあるボタンを押すにはその場所までスクロールして画面内にもってくる必要があります。location_once_scrolled_into_vieを使うことで要素の場所までスクロールすることができます。

driver.find_element_by_xpath(next_page_xpath).location_once_scrolled_into_vie
driver.find_element_by_xpath(next_page_xpath).click()

headlessモード

いちいちブラウザが立ち上がるのが面倒な場合はheadlessモードがおすすめです。ブラウザを立ち上げずにスクレイピングができます。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(chrome_driver_path,chrome_options=options)

便利なのですが、一部javascriptが動かない時もあり、スクレイピングしたい要素が見えなくなってしまうという場合もあります。そんな時はvirtualdisplayを使いましょう。 まずはxvfbとpyvirtualdisplayをインストールします。

$ sudo apt install xvfb
$ pip install pyvirtualdisplay

コードを書くときには以下のようにします。visibleが0で非表示、1で表示です。

from pyvirtualdisplay import Display
window_width = 1600
window_height = 900
display = Display(visible=0, size=(window_width,window_height))
display.start()
driver = webdriver.Chrome(chrome_driver_path)
driver.set_window_size(window_width,window_height)
#######何らかの処理#######
driver.quit()
display.stop()

参考サイト

PythonでWebスクレイピングする時の知見をまとめておく - Stimulator
Selenium API(逆引き)
selenium reference
seleniumのReference(日本語)
python - Selenium working with Chrome, but not headless Chrome - Stack Overflow seleniumにてButtonがクリックできない時の対処法

目指せ機械学習マスター[#01](機械学習の事始め:『人工知能は人間を超えるか』)

tani_AI_Academyさんの記事に影響を受けて、自分も機械学習の勉強をはじめました。機械学習の分野は多岐にわたっていて、初心者の私は何から手をつけて良いか分かりませんでした。 qiita.com 上の記事では機械学習をマスターするまでの道のりが示されています。

手始めに フェーズ1の参考図書として紹介されていた。松尾 豊 (著) の『人工知能は人間を超えるか ディープラーニングの先にあるもの (角川EPUB選書) 』を読んでみました。

人工知能とは何か

人工知能の定義については専門家の間でも意見が分かれています。

たとえば、公立はこだて未来大学学長の中島秀之氏は、人工知能を「人工的につくられた、知能をもつ実態。あるいはそれをつくろうとすることによって知能自体を研究する分野である」と定義している。
人工知能学会の元会長で京都大学大学教授の西田豊明氏は「『知能を持つメカ』ないしは『心を持つメカ』」と定義している。

自分はそもそも知能というものが定義できていない、あるいは定義することが難しいために様々な見解が出てきているのだと考えています。何を持って知能とするのかは人によって様々です。アリを例にすれば、自らや仲間が環境中に出したフェロモンをたどることで餌場にたどりつき、更にその経路の最適化まで行われます。このように言うとアリはものすごく賢く感じますが、1つ1つの個体が行っているのは餌にしろフェロモンにしろ匂いのする方へ向かうということだと考えると賢くないように感じてしまいます。iPoneのSiriも対話が成立している時は、ひょっとしたらこれはすごく賢いのでないかと思ってしまいますが、「私には〇〇の意味はわかりません。Webで検索してみましょうか?」と言われた瞬間にやっぱり賢くないなと感じてしまいます。ここから考えられるのは知能のあるなしは人間が対象の行動から類推するもので、例え優れた知能を持つものであっても外界に何の出力もしないものには知能を見ないということではないでしょうか。知能はあくまでそれを観測する側が知能のある、なし、高い、低いを決めるものなのではないかと考えています。
人工知能という言葉の解釈の違いから、現在、様々な製品が人工知能搭載を謳っています。このため人工知能搭載と書かれていても、その中で実際どのような技術が使われているのかわからなくなっています。著者は世間一般で言われている人工知能を搭載した製品というものを4つに区分しています。

レベル1:単純な制御プログラムを人工知能と称している
レベル2:古典的な人工知能
レベル3:機械学習と取り入れた人工知能
レベル4:ディープラーニングを取り入れた人工知能

人工知能の3つのブーム

第1次AIブーム

人工知能には3つのブームが存在しています。

第1次AIブームは1950年代後半〜1960年代。コンピュータで「推論・探索」をすることで特定の問題を解く研究が進んだ。しかし、いわゆる「トイ・プロブレム(おもちゃの問題)」は解けても、複雑な現実の問題は解けないことが明らかになった結果、ブームは急速に冷め、1970年代には人工知能研究は冬の時代を迎えた。

この時代の人工知能と読んでいる技術は現在の木探索などの探索アルゴリズムのことであることに驚きました。かつて人工知能と呼ばれた技術であっても時代が経って一般化されるとそれは人工知能技術ではなくなってしまう。ちょうど自律移動ロボットの技術が自動運転に利用されると自動運転技術になってしまうのと同じ流れを感じました。

第2次AIブーム

第2次AIブームは1980年代であり、コンピュータに「知識」を入れると賢くなるというアプローチが全盛を迎え、エキスパートシステムと呼ばれる実用的なシステムがたくさんつくられた。しかし、知識を記述、管理することの大変さが明らかになってくると、1995年ごろにはふたたびAIは冬の時代に突入してしまう。

専門家から様々な知識を抽出して、専門家のように判断するエキスパートシステムは専門家から知識を取り出すことが大変であることと、ルールの数が増えるにしたがって、それらを矛盾なく整理することが難しいという話を読んで、自分がロボコンなどでロボットの行動をルールベースで記述していたころを思い出しました。やはり単純な問題ならばif/thenで書けないこともないのですが、ルールが増えてくると、どこに変更を加えるとどこが変わるのかが不明瞭になってしまうという問題を抱えていました。
知識獲得のボトルネックだけでなく、フレーム問題やシンボルグラウンディング問題もこの時代からの人工知能が抱える課題です。著者はフレーム問題については

フレーム問題は、あるタスクを実行するのに「関係のある知識だけを取り出してそれを使う」という、人間ならごく当たり前にやっている作業がいかに難しいかを表している。

と書いています。例えば友達との待ち合わせに行くとして、大抵の人ならば待ち合わせの時間よりちょっと早めに着くように移動するでしょう。それは途中で電車なりバスなりが遅れることを想定していたり、初めての場所で迷ってしまうことを前提としていたりするからかもしれません。でももっと範囲を広げて考えれば、もし行く途中に誰かが道で倒れていて、その人を助ける必要があったとしたら、その時間も考えなければなりません。もしかしたら途中で交通事故にあうかもしれない。もしかしたら空から槍が降ってくるかもしれない。そのためにはヘルメットを買いに行かなければならない。というようにありとあらゆる想定をし始めると結局、待ち合わせの場所に行くことは不可能になってしまいます。こうした想定を常識の範囲内で行わせることが難しいというのがフレーム問題です。
シンボルグラウンディング問題は

コンピュータは記号の「意味」がわかっていないので、記号をその意味するものと結びつけることができない。
たとえば、シマウマを見たことがない人がいたとして、その人に「シマウマという動物がいて、シマシマのあるウマなんだ」と教えたら、本物のシマウマを見た瞬間、その人は「あれが話しに聞いていたシマウマかもしれない」とすぐに認識できるだろう。人間はウマの意味とシマの意味が分かっているからである。

と書いています。このたとえは少し分かりづらかったです。前に人工知能に関する授業を受けていた時に聞いた例え話としては、「リンゴについて説明しよう」というものがありました。リンゴと言えば、赤くて甘くて、木になっている果実と答えるでしょう。では赤とは、甘いとは、木とは何でしょう。こうした問答を続けていくと結局、それは自然(宇宙全体という意味での)とか、概念とか、非常に抽象的な言葉を使って説明することから逃げる他なくなってしまいます。記号を使って記号を説明することは難しく、私達は通常、言葉である記号と実態である物とが一対一対応がついているから言葉を使って認識を共有することができます。一方、人工知能に言葉を使って言葉を教えようとするとこのシンボルグラウンディング問題が発生してしまいます。 著者は最終的に

コンピュータがデータから特徴量を取り出し、それを使った「概念(シニフィエ:意味されるもの)」を獲得したあとに、そこに「名前(シニフィアン:意味するもの)」を与えれば、シンボルグラウンディング問題はそもそも発生しない。

と述べています。

第3次AIブーム

ここでは学習するとは分けることであるとして、様々な分類アルゴリズム(最近傍法、ナイーブベイズ法、決定木、サポートベクターマシン、ニューラルネットワーク)などの紹介がされています。この段階での機械学習では何を元に分類するかという特徴量を人が決める必要がありました。

機械学習の精度を上げるのは、「どんな特徴量を入れるか」にかかっているのに、それは人間が頭を使って考えるしかなかった。これが「特徴量設計」で機械学習の最大の関門だった。

ディープラーニングの登場で、それまで人手で行っていた特徴量設計を機械で行うことができるようになりました。画像の中に写っているものを答える問題や文字の認識など、様々なタスクで大幅に認識率が向上しました。

ディープラーニングは、データをもとに、コンピュータが自ら特徴量をつくり出す。人間が特徴量を設計するのではなく、コンピュータが自ら高次の特徴量を獲得し、それをもとに画像を分類できるようになる。

これからの人工知能

著者は人工知能が6つの段階を経て進化していくと述べています。

①画像特徴の抽象化ができるAI
②マルチモーダルな抽象化ができるAI
③行動と結果の抽象化ができるAI
④行動を通じた特徴量を獲得できるAI
⑤言語理解・自動翻訳ができるAI
⑥知識獲得ができるAI

私はロボットに機械学習を応用したいと考えているので③の行動と結果の抽象化ができるAIが気になっています。どんな行動をすればどんな結果が発生するかを予測できれば、ロボットにアームを取り付けて、何かをどこかに移動させるといったことが今よりも容易になるかもしれません。実際に人工知能がこの6つの段階を経ていくかは分かりませんが、人の代わりに何かの課題を実行するのに必要な要素が含まれていると感じました。

本を読んで考えたこと

大学時代の教授が言った「機械学習なんてただの写像だ」

大学時代に自律移動ロボットの研究をしている研究室に在籍していました。その研究室の教授は安易に機械学習に飛びつくことを良しとせず「機械学習なんてただの写像だ」とおっしゃっていました。今回、この本を読んで「ただの」というにはもったいないくらいの技術的ブレイクスルーを感じました。それは本の中で繰り返し述べられている特徴量を機械が見つけられるようになったことです。これから人工知能を使っていくことで人間が気づきもしなかった特徴が洗い出されることが楽しみでなりません。

人工知能が人類と敵対するか

最近の人工知能のめざましい発展の中で、人工知能がいつか人間と敵対するのではないかという話も出てきています。人工知能が人と敵対するには知能だけでなく生命も必要であると述べています。さらに人工知能が生命を獲得する方法として3つのシナリオが紹介されています。

①人工知能を生命化する方法(ロボット編)
②人工知能を生命化する方法(ウイルス編)
③人工的な生命に知能を持たせる方法

本ではこれら3つの方法がどれも上手くいかないという説明から、人工知能が実現しないと述べられています。これは私の意見ですが、たとえ人工知能が人間より優れた知能と生命を持ち得たとしても人類と真っ向から敵対するようなことはないのではないかと考えています。立場を変えて考えると人間より優れた知的生命体が人間に敵対するというのは、人間が人間以外の例えばネズミなり虫なりと本気で敵対するということで、それは小学生相手に本気でキレてる大人と変わらないような気がします。優れた知的生命体は人間と争うことすら馬鹿馬鹿しいと思うのではないでしょうか。

この本で分かること

  • 人工知能の大まかな歴史
  • 人工知能の代表的な手法
  • 人工知能が抱える課題
  • 著者によるこれからの人工知能の発展の予想

あとがき

初めて書評のようなものを書きましたが、意外と難しかったです。著作権に触れないように自分の意見と本の記述がしっかりと分けること、どの部分を紹介して、どの部分を紹介しないのかを決めることなど、まだまだ足りないところばかりだと感じました。

cronを使って自動でスクリーンショットを撮る

Ubuntu上で一定時間ごとにスクリーンショットを撮る必要に迫られ、試行錯誤した結果を載せます。

shutterのインストールと使い方

スクリーンショットを撮るツールとしてはgnome-screenshotやscrotなどがありますが、今回はファイル名の指定やウィンドウの指定などの細かな設定が行えるshutterを使用します。 以下のコマンドでshutterをインストールします。

$ sudo apt install shutter

$ shutterとすると下のようなウィンドウが表示されます。ここからGUI操作でスクリーンショットを撮ることもできます。

f:id:gokuraku104robot:20180609072918p:plain:w500
shutter
今回はCLIでの撮影を行うため

$ shutter -f

と入力します。デフォルトの設定では~/Pictures/にスクリーンショットが保存されます。 例えば

$ shutter -e -n --disable_systray --window=.*Chrome* -o '%y-%m-%d_%T.png'

とするとウィンドウ名にChromeという名前が入っているウィンドウを探して、そのウィンドウのスクリーンショットを撮ることができます。コマンドライン引数の詳しい内容についてはman shutterで確認できます。

cronでX-Windowを使うコマンドを使う際の注意

cronを使うと指定時刻にコマンドを実行させることができます。cronについては以下の記事が詳しいです。 qiita.com

スクリーンショットの撮影やブラウザなどGUIを使っているソフトの多くはX-Windowを使用しています。X-Windowを使用しているコマンドをcronから使用するには環境変数を教えてやる必要があります。まず普通のターミナルから以下のようなコマンドを打ちます。

$ export |grep DISPLAY
declare -x DISPLAY=":1"

私の環境ではDISPLAY変数は:1なので実行させたいシェルスクリプトにしたのように環境変数を追加してから実行したい処理を書きます。

#!/bin/bash
export DISPLAY=:1
###実行したい処理###
shutter -e -n --disable_systray --window=.*Chrome* -o '%y-%m-%d_%T.png'

これをshutter.shとして保存したら、下のようにしてシェルスクリプトに実行権限を与えます。

$ chmod 755 shutter.sh

最期にcronに登録します。

$ crontab -e

#1番下に以下を追加
* * * * * bash /home/username/pathtoscripts/shutter.sh

以上により毎分自動でスクリーンショットを撮ることができます。

参考サイト

linux - wmctrl not working in crontab - Stack Overflow Take periodic screenshots in Ubuntu with scrot and cron | existence, refactored クーロン(cron)をさわってみるお

【ROS】OpenCV2に依存に依存しているパッケージの利用

ROSのとあるパッケージを試しに動かしてみようとしたときに出会ったOpenCV関連のエラーです。 ちなみに動かそうとしたパッケージはこれです。 github.com

エラー内容

パッケージをビルドすると以下のようなエラーが出ます。

(find_package): Could not find a configuration file for package  
"OpenCV" that is compatible with requested version "2".  
The following configuration files were considered but not accepted:  
/opt/ros/kinetic/share/OpenCV-3.3.1-dev/OpenCVConfig.cmake, version: 3.3.1  

対処策

下記URLからOpenCV2.4をDownloadします。(Download Zipをクリック) github.com ダウンロードしたら以下を実行します。

$ unzip opencv-2.4.zip
$ cd libopencv2.4
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install

結局何に依存しているのか?

試しにエラーが出るプログラムのCMakeLists.txtの中身を find_package(OpenCV 2 REQUIRED) → find_package(OpenCV REQUIRED)
のように編集して、ビルドしてみるとコンパイルは通りますがリンクができなくなるようです。

fatal error: opencv2/gpu/gpu.hpp: そのようなファイルやディレクトリはありません  compilation terminated.  
external/spencer_people_tracking/detection/rgbd_detectors/pcl_people_detector/CMakeFiles/detect_people.dir/build.make:62:  
ターゲット 'external/spencer_people_tracking/detection/rgbd_detectors/pcl_people_detector/CMakeFiles/detect_people.dir/src/detect_people.cpp.o' のレシピで失敗しました  
make[2]: *** [external/spencer_people_tracking/detection/rgbd_detectors/pcl_people_detector/CMakeFiles/detect_people.dir/src/detect_people.cpp.o]  
エラー 1 CMakeFiles/Makefile2:11136: ターゲット 'external/spencer_people_tracking/detection/rgbd_detectors/pcl_people_detector/CMakeFiles/detect_people.dir/all' のレシピで失敗しました 

この見つからなかったと言われているopencv2/gpu/gpu.hppは何者なのか気になりますね。どうやらOpenCV3には実装されていないプログラムのようです。 answers.opencv.org OpenCV: gpu.hpp File Reference

参考サイト

GitHub - spencer-project/spencer_people_tracking: Multi-modal ROS-based people detection and tracking framework for mobile robots developed within the context of the EU FP7 project SPENCER.
Where to find /opencv2/gpu/gpu.hpp in Opencv 3.1.0 - OpenCV Q&A Forum
OpenCV: gpu.hpp File Reference

【Ubuntu】ジョイスティック(PS3コントローラ)がマウスとして認識されてしまう問題

ロボットを動かそうとしてPS3コントローラをPCに接続したところ、コントローラのジョイスティックがマウスとして認識され、ジョイスティックを動かす度にポインタが移動してしまう現象が発生しました。対処策は以下の通りです。

xserver-xorg-input-joystickの削除

xserver-xorg-input-joystickが入っている場合、これが悪さをしている可能性があります。下記コマンドから削除しましょう。
$ sudo apt purge xserver-xorg-input-joystick

xserver-xorg-input-joystickが入っていない、もしくは消しても直らない

xserver-xorg-input-joystickが入っていない、もしくは消しても直らない場合は以下を実行します。まずxinputを使って認識されているデバイスのリストを出します。

$ xinput -list
⎡ Virtual core pointer                          id=2    [master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                id=4    [slave  pointer  (2)]
⎜   ↳ SynPS/2 Synaptics TouchPad                id=12   [slave  pointer  (2)]
⎜   ↳ Logitech M545/M546                        id=9    [slave  pointer  (2)]
⎜   ↳ Sony PLAYSTATION(R)3 Controller           id=14   [slave  pointer  (2)]
⎣ Virtual core keyboard                         id=3    [master keyboard (2)]
    ↳ Virtual core XTEST keyboard               id=5    [slave  keyboard (3)]
    ↳ Power Button                              id=6    [slave  keyboard (3)]
    ↳ Video Bus                                 id=7    [slave  keyboard (3)]
    ↳ Power Button                              id=8    [slave  keyboard (3)]
    ↳ USB HD Webcam: USB HD Webcam              id=10   [slave  keyboard (3)]
    ↳ AT Translated Set 2 keyboard              id=11   [slave  keyboard (3)]
    ↳ Panasonic Laptop Support                  id=13   [slave  keyboard (3)]

問題のSony PLAYSTATION(R)3 Controllerが認識されていることがわかります(他のコントローラの場合は適宜読み替えてください)。再びxinputを使ってコントローラのプロパティを参照します。

$ xinput watch-props 'Sony PLAYSTATION(R)3 Controller' 
Device 'Sony PLAYSTATION(R)3 Controller':
        Device Enabled (141):   1
        Coordinate Transformation Matrix (143): 1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
        Device Accel Profile (273):     0
        Device Accel Constant Deceleration (274):       1.000000
        Device Accel Adaptive Deceleration (275):       1.000000
        Device Accel Velocity Scaling (276):    10.000000
        Device Product ID (262):        1356, 616
        Device Node (263):      "/dev/input/event17"
        Evdev Axis Inversion (277):     0, 0
        Evdev Axis Calibration (278):   <no items>
        Evdev Axes Swap (279):  0
        Axis Labels (280):      "Abs X" (391), "Abs Y" (392), "Abs Z" (604), "Abs Rotary X" (605), "Abs Rotary Y" (606), "Abs Rotary Z" (607)
        Evdev Scrolling Distance (282): 0, 0, 0

Device Enabledのフラグを1から0に変更します。 $ xinput set-prop 'Sony PLAYSTATION(R)3 Controller' 'Device Enabled' 0 以上でマウスとして認識されなくなっていれば成功です。

参考サイト
10.10 - Joystick acts as mouse; won't stop - Ask Ubuntu
Ubuntuでジョイスティックをつなぐとマウスポインターも動いてしまう: 電気羊の夢を見た
タッチパッドの有効無効を一発で@Ubuntu10.04 - 何にしても生っかじりの朴念仁