Python + tweepyにて、Video URLを取得する

TwitterのVideo URLの取得方法を調べる機会がありましたので、メモとして残します。

なお、Twitter用ライブラリは、 tweepy を使います。

 
目次

 

環境

 

tweepyでTwitter APIを操作する

各種トークンを取得します。

auth = tweepy.OAuthHandler(CONSUMER_TOKEN, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)
api = tweepy.API(auth)

 
tweepyにはCursorオブジェクトがあります。これを使うと、指定した数のツイートを取得できるため、ページングなどを気にする必要がないです。
Cursor Tutorial — tweepy 3.6.0 documentation

for status in tweepy.Cursor(api.user_timeline, 
    id='Twitter', since_id=560070183650213887,
    tweet_mode='extended').items(34):
        i += 1
print(i)

 
実行すると、

34

が出力されます。

 

ツイートとVideo URLが140文字に収まる場合

Video URLはどこに含まれているかを調べたところ、 extended_entities にあるようです。
Twitter REST APIを使って30秒動画の取得。$tweet->extended_entities->media[0]->video_info->variants[0]->url – エコテキブログ

以下のツイートで試してみます。
Twitterさんのツイート: "You can now shoot, edit and share video on Twitter. Capture life's most moving moments from your perspective. http://t.co/31JoMS50ha"

TWEET_ID = 560070183650213889
status = api.get_status(TWEET_ID)
print(status.text)
if hasattr(status, 'extended_entities'):
    for media in status.extended_entities.get('media', [{}]):
        if media.get('type', None) == 'video':
            print('video url: ' + media['video_info']['variants'][0]['url'])

実行結果です。

You can now shoot, edit and share video on Twitter. Capture life's most moving moments from your perspective. http://t.co/31JoMS50ha
video url: https://video.twimg.com/ext_tw_video/560070131976392705/pu/vid/640x360/vmLr5JlVs2kBLrXS.mp4

 

ツイートとVideo URLが140文字を超える場合

ツイートとVideo URLが140文字を超える場合は、少し動作が変わります。

別のツイート*1で実行してみると、

<長いツイート>やまな… https://t.co/xxx

みたいに、「…」でツイートが省略された上、extended_entitiesを取得できません。

 
この場合はどうするか調べたところ、APIを呼ぶ時に tweet_mode='extended' をつければ良さそうです。
Twitter Amplify Videos do not contain extended_entities · Issue #731 · tweepy/tweepy

 
また、 tweet_mode='extended'とした時は、 status.text がなくなり、 status.full_text になります。

# status = api.get_status(TWEET_ID)
# print(status.text)
status = api.get_status(TWEET_ID, tweet_mode='extended')
print(status.full_text)

 
実行してみます。

<長いツイート>やまない https://t.co/yyyy
video url: https://video.twimg.com/ext_tw_video/xxx.mp4

取得できました。また、短縮URLも先ほどのとは異なっていました。

 

【未解決】Videoが30秒より長い場合

こんな感じのツイートの場合、どう取得すればよいか不明です...
Twitter Videoさんのツイート: "Now, everyone can post videos up to 140 seconds long! We can’t wait to see the amazing videos you create and share. https://t.co/DFsuvnXkuL"

 

ソースコード

GitHubに上げました。 e.g._video_url ディレクトリの中身が今回のファイルです。
thinkAmi-sandbox/tweepy-sample thinkAmi-sandbox/tweepy-sample

*1:依頼された方のプライベートな感じなので、伏せ字にしておきます

Pythonで、Seleniumのfind_element系メソッドとPageObjectsを試してみた

PythonSeleniumには

  • find_element_by_xxx()
  • find_elements_by_xxx()
  • find_element()
  • find_elements()

などのメソッドが用意されています。

今回は、それらを試してみた時のメモを残します。

なお、途中からSeleniumのPage Objectsパターンが出てきます。

ただ、今回使うPage Objectsは公式ドキュメントよりは簡易的なPage Objectsになっています。
6. Page Objects — Selenium Python Bindings 2 documentation

 
目次

 

環境

  • Python 3.6.3
  • Selenium 3.6.0
  • pytest 3.2.3
    • テストランナーとして使用

 

Seleniumの対象とするHTML

以下のHTMLを用意しました。

なお、id属性が重複していますが、これは意図的にやっています。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ol>
        <li id="id1">Text1</li>
        <li id="id1">Text2</li>
        <li id="id1">Text3</li>
    </ol>

    <ul>
        <li class="post1">Post1-1</li>
        <li class="post1">Post1-2</li>
        <li class="post2">Post2</li>
    </ul>
</body>
</html>

 
あとは、上記のindex.htmlファイルがあるディレクトリにて、

$ python -m http.server

を実行し、 http://localhost:8000 でHTMLファイルが表示されるようにします。
21.22. http.server — HTTP サーバ — Python 3.6.3 ドキュメント

 

find_element_by_xxx()

HTMLから

などで要素があるかを検索し、WebElement(selenium.webdriver.remote.webelement.WebElement)を返します。
WebElement - 7. WebDriver API — Selenium Python Bindings 2 documentation

 
また、複数の要素がHTMLにあった場合は、そのうち最初に見つかったものを返します。

class TestFindElement:
    def setup_method(self):
        self.chrome = Chrome()
        self.chrome.get('http://localhost:8000')

    def teardown_method(self):
        self.chrome.quit()

    def test_find_element_by_xxx(self):
        id1 = self.chrome.find_element_by_id('id1')
        assert id1.text == 'Text1'

        post1_1 = self.chrome.find_element_by_class_name('post1')
        assert post1_1.text == 'Post1-1'

        post1_2 = self.chrome.find_element_by_css_selector('.post1')
        assert post1_2.text == 'Post1-1'

        post1_3 = self.chrome.find_element_by_xpath('//li[@class="post1"]')
        assert post1_3.text == 'Post1-1'

 
なお、要素が見つからない場合は、例外 selenium.common.exceptions.NoSuchElementException を送出します。

def test_find_elemnt_by_xxx_without_element(self):
    try:
        self.chrome.find_element_by_id('id2')
    except NoSuchElementException:
        # エラー内容をprintする
        print(f'\n{traceback.format_exc()}')

 

find_elements_by_xxx()

find_element_by_xxx()と同じく、HTMLから

などで要素があるかを検索します。

戻り値は、WebElementのリストとなります。

def test_find_elements_by_xxx(self):
    id1 = self.chrome.find_elements_by_id('id1')
    assert len(id1) == 3
    assert id1[0].text == 'Text1'

    post1_1 = self.chrome.find_elements_by_class_name('post1')
    assert len(post1_1) == 2
    assert post1_1[0].text == 'Post1-1'

    post1_2 = self.chrome.find_elements_by_css_selector('.post1')
    assert len(post1_2) == 2
    assert post1_2[0].text == 'Post1-1'

    post1_3 = self.chrome.find_elements_by_xpath('//li[@class="post1"]')
    assert len(post1_3) == 2
    assert post1_3[0].text == 'Post1-1'

 
なお、find_element_by_xxx()と異なり、要素が見つからない場合は、長さ0のリストを返します。

そのため、要素の有無をテストする場合、例外の送出がないこちらを使うのが良いかもしれません。

def test_find_elements_by_xxx_without_element(self):
    post3 = self.chrome.find_elements_by_class_name('post3')
    assert len(post3) == 0

 

find_element()

最初、find_element_by_xxx()があるならいらないのでは?と思っていました。

しかし、SeleniumでPageObjectsパターンを使う場合は、これを使うのが良さそうだと分かりました。公式ドキュメントにも記載されています。
6. Page Objects — Selenium Python Bindings 2 documentation

 
今回は、簡易的なPageObjectsを実装してみます。

まず、index.html向けのクラス IndexPageObject を用意します。そのクラスには

  • ページを操作するため、WebDriverのインスタンスを渡す
  • Seleniumで検索する要素を、クラス定数のタプルとして定義

を実装します。

class IndexPageObject:
    # Seleniumで探す要素をクラス定数として定義する
    ID1 = (By.ID, 'id1')
    CLASS1 = (By.CLASS_NAME, 'post1')
    CLASS2 = (By.CLASS_NAME, 'post2')
    CLASS3 = (By.CLASS_NAME, 'post3')  # これは存在しない要素
    CSS = (By.CSS_SELECTOR, '.post1')
    XPATH = (By.XPATH, '//li[@class="post1"]')

    def __init__(self, driver: WebDriver):
        # WebDriverをインスタンスに渡す
        self.driver = driver

 
次に、ページに対する操作をメソッドとして実装します。

ページの要素を探す場合は、find_element()メソッドを使います*1
find_element() - 7. WebDriver API — Selenium Python Bindings 2 documentation

また、find_element()メソッドの引数には、クラス定数として定義したタプルを展開して渡します。

これにより、もしHTML構造が変わっても、クラス定数のタプルの内容を変更するだけです。テストコードには影響しません。

def get_post_using_find_element(self):
    # IDで探す場合
    id1 = self.driver.find_element(*IndexPageObject.ID1)

 
あとは、テストコードの方で

します。

def test_find_element(self):
    index_page = IndexPageObject(self.chrome)
    post = index_page.get_post_using_find_element()
    assert post == 'Post1-1'

   
もし、要素が存在しない場合は、find_element_by_xxx()と同じく例外を送出します。

def get_post_using_find_element_without_element(self):
    try:
        # find_element()の場合、要素が存在しないと例外が送出される
        return self.driver.find_element(*IndexPageObject.CLASS3)
    except NoSuchElementException:
        print(f'\n{traceback.format_exc()}')
        raise

 
テストコードはこんな感じです。

def test_find_element_without_element(self):
    with pytest.raises(NoSuchElementException):
        index_page = IndexPageObject(self.chrome)
        index_page.get_post_using_find_element_without_element()

 

find_elements()

こちらは、find_elements_by_xxx()をPageObjectsで実装する場合に使います。

def get_post_count_using_find_elements(self):
    # XPATHで探す場合
    xpath1 = self.driver.find_elements(*IndexPageObject.XPATH)
    return len(xpath1)

 
テストコードです。

def test_find_elements(self):
    index_page = IndexPageObject(self.chrome)
    count = index_page.get_post_count_using_find_elements()
    assert count == 2

 
要素が存在しない場合は、find_elements_by_xxx()と同様長さ0のリストが返ってきます。

def get_post_count_using_find_elements_without_element(self):
    # find_elements()の場合は、要素が存在しなくても例外とならず、長さ0のリストが返る
    elements = self.driver.find_elements(*IndexPageObject.CLASS3)
    return len(elements)

 
テストコードです。

def test_find_elements_without_element(self):
    index_page = IndexPageObject(self.chrome)
    count = index_page.get_post_count_using_find_elements_without_element()
    assert count == 0

 

テストの実行

いずれのテストもパスしました。

$ python -m pytest . -v -s
==== test session starts ====
platform darwin -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- /path/to/bin/python
cachedir: .cache
rootdir: /path/to/python_selenium_sample/e.g._find_element, inifile:
collected 8 items                                                                                                                                                         

test_selenium_find_element.py::TestFindElement::test_find_element_by_xxx PASSED
test_selenium_find_element.py::TestFindElement::test_find_elemnt_by_xxx_without_element 
Traceback (most recent call last):
  File "/path/to/python_selenium_sample/e.g._find_element/test_selenium_find_element.py", line 33, in test_find_elemnt_by_xxx_without_element
    self.chrome.find_element_by_id('id2')
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 341, in find_element_by_id
    return self.find_element(by=By.ID, value=id_)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 843, in find_element
    'value': value})['value']
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 308, in execute
    self.error_handler.check_response(response)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"id","selector":"id2"}
  (Session info: chrome=62.0.3202.75)
  (Driver info: chromedriver=2.33.506106 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2),platform=Mac OS X 10.11.6 x86_64)


PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements_by_xxx PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements_by_xxx_without_element PASSED
test_selenium_find_element.py::TestFindElement::test_find_element PASSED
test_selenium_find_element.py::TestFindElement::test_find_element_without_element 
Traceback (most recent call last):
  File "/path/to/python_selenium_sample/e.g._find_element/index_page_object.py", line 41, in get_post_using_find_element_without_element
    return self.driver.find_element(*IndexPageObject.CLASS3)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 843, in find_element
    'value': value})['value']
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 308, in execute
    self.error_handler.check_response(response)
  File "/path/to/python_selenium_sample/env363/lib/python3.6/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"class name","selector":"post3"}
  (Session info: chrome=62.0.3202.75)
  (Driver info: chromedriver=2.33.506106 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2),platform=Mac OS X 10.11.6 x86_64)


PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements PASSED
test_selenium_find_element.py::TestFindElement::test_find_elements_without_element PASSED

==== 8 passed in 20.19 seconds ====

 

ソースコード

GitHubに上げました。e.g._find_elementディレクトリ以下が、今回のファイルです。
thinkAmi-sandbox/python_selenium-sample

 
なお、説明用にPageObjectsでassertしていますが、本来は不要です。

*1:公式ドキュメントには、「‘Private’ method used by the find_element_by* methods. Use the corresponding find_element_by* instead of this.」と書いてあるので、使ってよいか不安ですが...

#pycamp Python Boot Camp in 長野八ヶ岳にTAとして参加しました

10/28に富士見森のオフィスにて開催された「Python Boot Camp in 長野八ヶ岳」にTAとして参加しました。

 
会場の外見はこんな感じで、中もキレイに整っていました。

 

勉強会本編

講師は鈴木たかのりさんでした。Python Boot Camp Textに色々なネタを交えつつ、進行していきました。
Python Boot Camp Text — Python Boot Camp Text 2016.04.28 ドキュメント

自分は、6月のPython Boot Camp in 長野のTAの時の反省を頭に入れながら、参加者の方々のサポートを行いました。

冒頭で参加者の自己紹介がありました。Web・組込・データサイエンス系など、幅広い分野を背景に持つ方々が集っていました。

特に、中学生の参加者には驚きました。3月のみんなのPython勉強会 in 長野#1に引き続き、最近の中学生はすごいと改めて感じました。

 
お話の中では、このあたりが印象に残りました。

 
参加者の方からは、PythonVisual Studioで書く方法について質問を受けました。

最近さわっていなかったため調べてみたところ、Microsoftのサイトに色々とまとまっていました。自分がさわっていた頃よりもさらに充実しているという印象です。
Visual Studio での Python | Microsoft Docs

 
また、鈴木たかのりさんにはサインもいただきました。ありがとうございました。

 

懇親会

その後の懇親会も同じ場所にて開催されました。地元のお酒や地元の野菜、おでん、地元のおばあさんのお漬物などをおいしくいただきました。

アットホームな雰囲気ということもあり、参加されたみなさんといろいろと交流できました。技術的なバックグラウンドが様々であったこともあり、楽しかったです。

Hogmap (パラグライダーのフライトログデータベースと、GPS解析システム) のお話では、パラグライダーに関する認識が変わりました。
Hogmap

Pythonが動くボードPYNQの実物もありました。

 
また、長野県の勉強会についてよく聞かれたため、

をしていました。ただ、データサイエンス系の勉強会は分からなかったため、回答できませんでした...

会場となった富士見森のオフィスさんでもいろいろとあるようで、まずは11/4,5のイベント(月夜の下のハッカソン)があることをうかがいました。
月夜の下のハッカソン – Moon Light Hack

 
最後になりましたが、「Python Boot Camp in 長野八ヶ岳」を企画・運営・参加されたみなさま、ありがとうございました。

Python + pytestにて、pytestに独自のコマンドラインオプションを追加する

Python + pytestにて、コマンドラインオプションを追加して、コマンドラインから値を受け取った時のメモを残します。

 
目次

 

環境

 

pytestのコマンドラインオプションの追加方法

公式ドキュメントに情報がありました。
Basic patterns and examples — pytest documentation

とすれば良いようです。

 

conftest.pyの実装

公式ドキュメントによると、コマンドラインオプションを実装するための引数は標準モジュールの argparse と似ていました。

 
そこで今回は、

の2つを実装してみます。

まずは、pytest_addoption()関数に、追加するオプションの内容を記述します。 action='store'なものが前者、action='store_true'なものが後者となります。

def pytest_addoption(parser):
    """add commandline options"""
    parser.addoption('--fruit', action='store', default='ham',
                     help='fruit name: apple, grape, banana, etc')
    parser.addoption('--season', action='store_true',
                     help='fruit season now')

 
続いて、コマンドラインオプションから渡された値を返す、@pytest.fixtureデコレータの付いた関数を用意します。

request.config.getoption()メソッドの引数に、コマンドラインオプション名(parser.addoption()の第一引数と同じ名前)を指定します。

@pytest.fixture
def favorite_fruit(request):
    return request.config.getoption('--fruit')


@pytest.fixture
def is_season(request):
    return request.config.getoption('--season')

 

テストコードの実装

テストコードの引数に、conftext.pyで指定したfixtureデコレータのある関数の名前を指定します。

favorite_fruitis_seasonが該当します。

def test_fruit(favorite_fruit, is_season):
    print(f'\nfruit:{favorite_fruit}, is_season:{is_season}')
    assert True

 

テストの実行

ヘルプを見ると、オプションが追加されていました。

$ python -m pytest e.g._add_commandline_option/ --help
...
custom options:
  --fruit=FRUIT         fruit name: apple, grape, banana, etc
  --season              fruit season now
...

 
実行してみます。

なお、テストをパスした時はprint()の結果が出力されないため、 -sオプションを付けて実行します。

# 独自オプション無し:fruit・is_seasonともデフォルト値
$ python -m pytest e.g._add_commandline_option/ -s
...
fruit:ham, is_season:False

# fruitオプションあり:渡したbananaが表示
$ python -m pytest e.g._add_commandline_option/ -s --fruit banana
...
fruit:banana, is_season:False

# seasonオプションあり:is_seasonがTrueになった
$ python -m pytest e.g._add_commandline_option/ -s --fruit banana --season
...
fruit:banana, is_season:True

テストコードに、コマンドラインオプションの値が引き渡されていました。

 

応用:コマンドラインオプションで指定したテストコード以外はスキップする

pytestでは

  • pytest.mark.skipでテストをスキップ
  • pytest.mark.skipifで、条件に一致した場合にテストをスキップ

などの制御ができます。

ただ、上記の方法では、「コマンドラインオプションで指定したテストコード以外はスキップする」を実現できませんでした。コマンドラインオプションの値をマーカーに渡すことができなかったためです。

そこで公式ドキュメントを見たところ、

を組み合わせれば良さそうでした。
Working with custom markers — pytest documentation

独自コマンドラインオプションを追加したときと同様、conftest.pyとテストコードに実装します。

 

conftest.pyの実装

conftest.pyには

  • pytest_addoption()関数にて、独自コマンドラインオプションを追加 (今回は、targetオプション)
  • pytest_configure()関数にて、独自マーカーを追加 (今回は、pytest.mark.test_number)
  • @pytest.fixtureデコレータの付いた関数にて、独自コマンドラインオプションの値を取得
  • pytest_runtest_setup()関数にて、スキップするかどうかを判定

の4つを実装しました。

 

def pytest_addoption(parser):
    """add commandline options"""
    parser.addoption('--target', action='store',
                     help='if commandline args match test decorator, run test. if not, skip it')


def pytest_configure(config):
    """add custom marker"""
    config.addinivalue_line('markers', 'test_number(number): test case number')


@pytest.fixture
def my_target(request):
    return request.config.getoption('--target')


def pytest_runtest_setup(item):
    """decide skip or run testcase"""
    marker = item.get_marker('test_number')
    if marker is None:
        return

    opt = item.config.getoption('target')
    if opt is None:
        return

    targets = opt.split(',')
    test_number = str(marker.args[0])
    if test_number not in targets:
        pytest.skip('it is non-target testcase. skip it.')

 

テストコードの実装

今回はテストコードとして、

の3パターンを用意しました。

def test_no_marker():
    print('apple')
    assert True


@pytest.mark.test_number(1)
def test_with_marker():
    print('grape')
    assert True


@pytest.mark.parametrize('kind, code', [
    pytest.param('banana', '123', marks=pytest.mark.test_number(2)),
    pytest.param('pear', '456', marks=pytest.mark.test_number(3)),
])
def test_parameterize_with_marker(kind, code):
    print(f'\n{kind}: {code}')
    assert True

 

テストの実行

独自コマンドラインオプションのtargetを指定しない場合、すべてのテストが実行されました。

$ python -m pytest e.g._add_commandline_option/ -s
...
.apple
.grape
.
banana: 123
.
pear: 456
.

 
target=1とした場合は、独自マーカーの引数に1を渡しているテストケースが実行されました。

また、独自マーカーがないテストも実行されています。

$ python -m pytest e.g._add_commandline_option/ -s --target 1
...
.apple
.grape
.ss

 
@pytest.mark.parametrizeなテストコードもうまく制御されています。

# parametrizeなテストのうち、前者のもの
$ python -m pytest e.g._add_commandline_option/ -s --target 2
...
.apple
.s
banana: 123
.s


# parametrizeなテストのうち、後者のもの
$ python -m pytest e.g._add_commandline_option/ -s --target 3
...
.apple
.ss
pear: 456
.

 

conftest.pyについて

そもそも conftest.py は何だろうと思い調べたところ、以下の解説が詳しかったです。ディレクトリレベルのプラグインと考えれば良さそうです。
python - In py.test, what is the use of conftest.py files? - Stack Overflow

 

ソースコード

GitHubにあげました。e.g._add_commandline_option ディレクトリが今回のものです。
thinkAmi-sandbox/python_pytest-sample

#stapy 第29回みんなのPython勉強会に参加しました & LTしました

10/11にクリーク・アンド・リバー社で開催された、第29回みんなのPython勉強会に参加しました。
みんなのPython勉強会#29 - connpass

いつもはギークラボ長野にて中継を見ていますが、今回は初の現地参加でした。

今回のテーマはデータサイエンスとPythonでした。データサイエンス方面はあまりさわっていないため、いろいろと参考になる話が聞けました。

以下、簡単なメモです。誤っていたらご指摘ください。

目次

 

データサイエンスとPythonの役割

辻真吾さん(Start Python Club)

データサイエンスとそのまわりのライブラリのお話が印象に残りました。

マニアックな関数についてはRの方が強いなど、RとPythonでの機能面の違いについて疑問に思ってたことが分かりました。

 

PySparkで始める並列分散処理

林田千瑛さん(Retty.Inc)

Apache Sparkの概要を知ることができて良かったです。

以下、印象に残ったところです。

  • Apache Sparkは、一台から使える
  • RDDとDataFrameの違い
    • RDDは柔軟、DataFrameの方が速い
  • Apache Sparkを使うなら、データ規模が10GBを超えるあたりから
    • アドテクや異常検知などに向く
  • AWS EMRでは、ノートブックとしてApache Zeppelinが使える
  • AWS EMRからGoogle BigQueryにデータを渡すと転送量が多いため、費用がかかる
  • Apache Sparkには論文があるので、設計思想を知ることができる

 

データサイエンティストを目指す全てのPythonistaのために

中林紀彦さん(SOMPOホールディングス

事業会社における、データサイエンティストの活躍する環境づくりが印象に残りました。

以下、印象に残ったところです。

  • AWSGCPのマルチクラウドで構築
  • 社内データ・社外データ・オープンデータをまとめたメタデータテーブルを用意
    • どこに生データがあるかの検索を容易に
  • ビジネス経験を積むのも重要
    • データをビジネスにどうやって持っていくか、考えることができるため
  • 現場で使ってもらいやすくなるよう、結果をDjangoに組み込んで表現
  • データサイエンティストにとって良い会社
    • データを容易に入手できる会社
    • 分析結果を容易にシステムに組み込めること
    • 「自分は、この会社の何にコミットしているのか」をお互いに説明できる環境

 

LTのスライド(Robot FrameworkでSeleniumを扱いやすくする)

Robot FrameworkでSeleniumを扱いやすくする - slideship.com

プレゼンをする時の準備に手間取ってしまい、余裕なくLTしていたような気がします。事前準備重要ですね。

Robot Frameworkに興味を持っていただけた方もいらっしゃったようで、ありがたいです。

また、使った時のソースコード全体は、GitHubに上げました。
thinkAmi-sandbox/stapy29-sample

 
他にもLTされる方々が多く、いろいろと活発でした。

 
また、今回、Slideshipで初めてスライドを作ってみました。

作った時に感じたことは、

  • シンタックスハイライトが便利
  • ブラウザで動作が、ネットワークがつながってなくても、スライドを使ったプレゼンができる
  • SlideShareやSpeakerDeckのように、ブログへ埋め込む方法はどこかにあるのかな...

です。

 
最後になりましたが、関係者のみなさまありがとうございました。

RobotFramework + SeleniumLibrary + Appiumにて、Genymotion上のChromeを使ってテストする

以前、AndroidChromeを使うテストをRobot Frameworkで書いてみました。
RobotFramework + SeleniumLibraryにて、Android実機上のChromeを使ってテストする - メモ的な思考的な

この時は実機上のChromeを使っていました。

今回は、Androidエミュレータの一種であるGenymotion上のChromeを使って、テストしてみます。

 
目次

 

環境

  • Mac OS X 10.11.6
  • Python 3.6.2
  • RobotFramework 3.0.2
  • SeleniumLibrary 3.0.0b1
  • Node.js 8.4.0
  • Xcode 8.2.1
  • Appium 1.7.1
  • Genymotion 2.10.0

 
上記の環境はすべて構築済とします。

なお、Appiumのインストールは、以下の記事で行ったものを流用しています。
RobotFramework + SeleniumLibrary + Appiumで、iOSシミュレータ上のSafariブラウザでテストする - メモ的な思考的な

 

環境構築

Genymotion上に仮想デバイスを作成

今回は Google Nexus5 - 5.1.0 - API22 - 1080x1920 をセットアップします。

セットアップ終了後、仮想デバイスを起動したままにしておきます。

 

OpenGAppsのChromeをインストール

今回はGenymotoinoのサイトでも紹介されている通り、 OpenGApps を使ってChromeをインストールします。
Genymotion 2.10 – Google Play Services and Play Store Are There!

Google Playを使うにはVariantは nano で良いのですが、Chromeは含まれていません。

そのため、以下の内容でダウンロードします。

Platform Android Variant
x86 5.1 full

 
zipファイルのダウンロードが終わったら、そのzipファイルをGenymotionへとドラッグ&ドロップします。

しばらくすると仮想デバイスへのインストールが終わります。

インストール後、仮想デバイスを再起動すると、Chromeがインストールされています。

 

Robot Frameworkのテストコード実装

実機上でテストしたものと同じソースコードとなります。

*** Settings ***

Library  SeleniumLibrary


*** Keywords ***
GoogleでPythonを検索してスクリーンショットを撮り、結果を出力する
    # 以下のコードをRobot Framework風にした
    # http://qiita.com/orangain/items/db4594113c04e8801aad

    # 以下を参考に、Chromeのオプションを追加して、Chromeを起動する
    # https://sites.google.com/a/chromium.org/chromedriver/getting-started/getting-started---android
    ${options} =  evaluate  sys.modules['selenium.webdriver'].ChromeOptions()  sys
    call method  ${options}  add_experimental_option  androidPackage  com.android.chrome
    create webdriver  Chrome  chrome_options=${options}

    # Googleのトップ画面を開く
    go to  https://www.google.co.jp/

    # タイトルにGoogleが含まれていることを確認する
    ${page_title} =  get title
    should contain  ${page_title}  Google

    # 検索後を入力してEnter
    input text  name=q  Python
    # Robot FrameworkではEnterキーは\\13になる
    # https://github.com/robotframework/Selenium2Library/issues/4
    press key  name=q  \\13

    # Ajax遷移のため、適当に2秒待つ
    sleep  2sec

    # タイトルにPythonが含まれていることを確認する
    ${result_title} =  get title
    should contain  ${result_title}  Python

    # スクリーンショットを撮る
    capture page screenshot  filename=result_google_python_android.png

    # ログを見やすくするために改行を入れる
    log to console  ${SPACE}

    # 検索結果を表示する
    @{web_elements} =  get webelements  css=h3 > a
    :for  ${web_element}  in  @{web_elements}
    \  ${text} =  get text  ${web_element}
    \  log to console  ${text}
    \  ${href} =  call method  ${web_element}  get_attribute  href
    \  log to console  ${href}

    # ブラウザを終了する
    close browser


*** TestCases ***

GoogleでPythonを検索するテスト
    GoogleでPythonを検索してスクリーンショットを撮り、結果を出力する

 

テスト実行

Genymotionの仮想デバイスを起動

シャットダウンしているとテストできないため、起動しておきます。

 

Appiumを起動

ターミナルより起動します。

$ appium
[Appium] Welcome to Appium v1.7.1
[Appium] Appium REST http interface listener started on 0.0.0.0:4723

 

テストを実行

ターミナルより実行します。

$ robot android_chrome.robot 
================
Android Chrome         
================
GoogleでPythonを検索するテスト                                         
Python - ウィキペディア
https://ja.m.wikipedia.org/wiki/Python
Python基礎講座(1 Pythonとは) - Qiita
http://qiita.com/Usek/items/ff4d87745dfc5d9b85a4
Pythonとは - python.jp
https://www.python.jp/about/
Top - python.jp
https://www.python.jp/
【入門者必見】Pythonとは?言語の特徴やシェア、仕事市場を徹底解説 | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト
http://www.sejuku.net/blog/7720
Python 3を使うべきでない場合(なんてない) | プログラミング | POSTD
http://postd.cc/case-python-3/
Pythonに咬まれるな : 注意すべきセキュリティリスクのリスト | プログラミング | POSTD
http://postd.cc/a-bite-of-python/
GoogleでPythonを検索するテスト    | PASS |

テストがパスしました。

また、スクリーンショットも撮影されています。

f:id:thinkAmi:20170928214013p:plain:w400

 

ソースコード

GitHubに上げました。 selenium_appium_sample/android_chrome.robot ファイルが今回のテストファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

SeleniumGrid + RobotFramework + SeleniumLibraryで、VMのWindowsのInternet Explorerを使ってテストする

今まで、

など、Mac上で動作するものに対してテストコードを書いてきました。

 
そんな中、VMWindowsInternet Explorerを使ってテストする機会がありました。

そこで、

を行うよう、SeleniumGrid + RobotFramework + SeleniumLibraryを使ってテストしてみたため、その時のメモを残します。

なお、Selenium Gridについては以下が詳しいです。
» Selenium Gridで複数の実機ブラウザで自動テスト TECHSCORE BLOG

 
目次

 

環境

今回は

という構成にしました。

なお、Selenium Grid自体は、HubとNodeを一つの端末にしても動作します。ターミナルが3つ必要になるくらいです。(Hub用、Node用、テスト実行用)

 

Mac

 

Windows

 

Mac側のSelenium Gridの環境構築

Selenium Grid用のjarファイルのダウンロード

後日AppiumでもSelenium Grid環境を構築するため、Seleniumは3.3.1を使います (現時点の最新は3.5.3)。

PythonライブラリのバージョンとSelenium Gridのバージョンが異なる場合エラーが起きることがあるため、同じバージョンのものをダウンロードします。

今回は過去のバージョンのため、ダウンロードページのprevious releasesより selenium-server-standalone-3.3.1.jar をダウンロードします。

ダウンロードしたzipを展開し、出てきたjarファイルをテスト用ディレクトリに置きます。

 

Selenium Grid Host用の設定ファイルを作成

コマンドラインに設定内容を記述してもよいのですが、今回は設定ファイルを使います。

今回使用するMacにはVM用のネットワークアダプタなど、IPが振られているネットワークアダプタが複数あります。何も設定しないと、LANのネットワークアダプタが使われないことがあります。その場合、Selenium Grid NodeからHubへ接続できず動作しません。

そのため、今回の設定ファイルには、HubのIPアドレスを記載します。

hub_config.json

{
  "host": "192.168.10.103"
}

 

Selenium Grid Hubの起動

Macのターミナルを開き、jarファイルのあるディレクトリで以下を実行します。

$ java -jar selenium-server-standalone-3.3.1.jar -role hub -hubConfig hub_config.json 

21:54:58.285 INFO - Selenium build info: version: '3.3.1', revision: '5234b32'
...
21:54:59.918 INFO - Nodes should register to http://192.168.10.103:4444/grid/register/
21:54:59.918 INFO - Selenium Grid hub is up and running

Hubが起動しました。

http://192.168.10.103:4444/grid/console へアクセスすると、以下のようになりました。

f:id:thinkAmi:20170917215832p:plain

 

VMwareのゲストOSのIP設定をブリッジへと変更

もし、VMwareの設定画面にネットワークアダプタがない場合は、新規に追加します。

その上で、ネットワークアダプタの設定を Macを共有 から ブリッジされたネットワーキング > 自動検出 へと変更します。

これでMacとゲストOSのWindowsIPアドレスが別となりました *1

 

VMのWindows10側のSelenium Gridの環境構築

jarとIEDriverServerのダウンロードと配置

NodeとなるWindowsにも、Hubと同じバージョンのjarファイルおよびIEDriverServerを任意のディレクトリに用意します。

今回用意したものは

  • selenium-server-standalone-3.3.1.jar
    • Hubと同じバージョンのもの
  • IEDriverServer.exe
    • IEDriverServer_Win32_3.3.0.zipを展開したもの
    • Windows10のOSは64bitだが、Downloadsページに32bit IEが推奨されていたため、32bit版のものを使った
    • Internet Explorer 用 WebDriver (Windows)

を同じディレクトリに入れます。

なお、jarと同じディレクトリに置けば、IEDriverServer.exeには特にPATHを通さなくても動作しました。

 

Selenium Grid Node用の設定ファイルを作成

Internet Explorerを動作させるための設定ファイルを記載します。

なお、Selenium3からは configuration というキーを付けるとエラーとなります。
Selenium 3.0.1 Chrome Node configuration - Stack Overflow

また、設定ファイルのデフォルト値は以下に記載されています。 https://github.com/SeleniumHQ/selenium/blob/master/java/server/src/org/openqa/grid/common/defaults/DefaultNodeWebDriver.json

今回はその差分を記載します。

node.json

{
  "capabilities": [
    {
      "browserName": "internet explorer",
      "platform": "WINDOWS",
      "maxInstances": 1,
      "cleanSession": true ,
      "version": "11",
      "browser-version": "11",
      "seleniumProtocol": "WebDriver"
    }
  ],
  "hub": "http://192.168.10.103:4444/grid/register",
  "host": "192.168.10.107",
  "port": 5054,
  "register": true
}

 

Internet Explorerの設定を変更

SeleniumInternet Explorerを使う場合、いくつかの設定が必要になります。

今回は以下のWikiにあるRequired Configurationに従い、設定を行います。
Required Configuration - InternetExplorerDriver · SeleniumHQ/selenium Wiki

設定内容は

  • インターネットオプションのセキュリティタブにある、 保護モードを有効にする にチェックを入れる
  • インターネットオプションの詳細設定にある 拡張保護モードを有効にするチェックを外す
  • IEを起動し、ズームを100%にする
  • Windows10の場合、IEのバージョンは11なので、レジストリに以下を追加する
    • キー:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BFCACHE
    • 値: 種類 DWORD(32ビット値) 、名前 iexplore .exe 、 データ 0
    • 今回は32bitのIEを使ったが、64bitの場合は別のなのでWikiを参照のこと

です。

 

Selenium Grid Nodeの起動

コマンドプロンプトから起動します。

>java -jar selenium-server-standalone-3.3.1.jar -role node -nodeConfig node.json
22:00:16.200 INFO - Selenium build info: version: '3.3.1', revision: '5234b32'
22:00:17.466 INFO - Registering the node to the hub: http://192.168.10.103:4444/grid/register
22:00:17.497 INFO - The node is registered to the hub and ready to use

Nodeが起動し、Hubに登録されました。

 
この結果、Macのコンソールには

21:59:07.276 INFO - Registered a node http://192.168.10.107:5054
22:00:13.005 INFO - I/O exception (java.net.SocketException) caught when processing request to {}->http://192.168.10.107:5054: Connection reset
22:00:13.006 INFO - Retrying request to {}->http://192.168.10.107:5054
22:00:17.512 INFO - Registered a node http://192.168.10.107:5054
22:00:17.512 WARN - Cleaning up stale test sessions on the unregistered node http://192.168.10.107:5054

と表示されます。

また、Selenium Grid HubのConsoleをブラウザで開くと

f:id:thinkAmi:20170917221316p:plain

に変わりました。

 

Robot Frameworkのテストコードを作成

今回もGooglePythonを検索するテストコードになります。

以前と異なる点は

  • Create Webdriverキーワードで、ブラウザ名を Remote にする
  • command_executor に、Selenium GridのHubの待ち受けURLを指定
  • desired_capabilities に、Selenium GridでIEを動かすためのDesiredCapabilitiesを指定

となります。

なお、Remoteに対するDesiredCapabilitiesの書き方は以下が参考になりました。 robotframework - Opening Chrome browser in Android device using Robot Framework script and chromedriver? - Stack Overflow

 
internet_explorer.robot

*** Settings ***

# Libraryは大文字小文字の区別があるようで、libraryとしてしまうとIDEが認識しない
Library  SeleniumLibrary


*** Keywords ***
GoogleでPythonを検索してスクリーンショットを撮り、結果を出力する
    ${caps} =  create dictionary  browserName=internet explorer  platform=WINDOWS
    create webdriver  Remote  command_executor=http://192.168.10.103:4444/wd/hub  desired_capabilities=${caps}

    # Googleのトップ画面を開く
    go to  https://www.google.co.jp/

    # タイトルにGoogleが含まれていることを確認する
    ${page_title} =  get title
    should contain  ${page_title}  Google

    # 検索後を入力して送信する
    input text  name=q  Python
    # Robot FrameworkではEnterキーは\\13になる
    # https://github.com/robotframework/Selenium2Library/issues/4っz1
    press key  name=q  \\13

    # Ajax遷移のため、適当に2秒待つ
    sleep  2sec

    # タイトルにPythonが含まれていることを確認する
    ${result_title} =  get title
    should contain  ${result_title}  Python

    # スクリーンショットを撮る
    capture page screenshot  filename=result_google_python.png

    # ログを見やすくするために改行を入れる
    log to console  ${SPACE}

    # 検索結果を表示する
    @{web_elements} =  get webelements  css=h3 > a
    :for  ${web_element}  in  @{web_elements}
    \  ${text} =  get text  ${web_element}
    \  log to console  ${text}
    \  ${href} =  call method  ${web_element}  get_attribute  href
    \  log to console  ${href}

    # ブラウザを終了する
    close browser


*** TestCases ***

GoogleでPythonを検索するテスト
    GoogleでPythonを検索してスクリーンショットを撮り、結果を出力する

 

テストの実行

準備が整ったため、Macのターミナルよりテストを実行します。

$ robot internet_explorer.robot

を実行すると、VMWindowsでIE11が起動し、GooglePythonを検索します。

テストが進むにつれMacのターミナルに

==============================
Internet Explorer                                                             
==============================
GoogleでPythonを検索するテスト                                         
Python - ウィキペディア
https://ja.wikipedia.org/wiki/Python
Pythonとは - python.jp
https://www.python.jp/about/
Python チュートリアル — Python 3.6.1 ドキュメント
https://docs.python.jp/3/tutorial/index.html
【入門者必見】Pythonとは?言語の特徴やシェア、仕事市場を徹底解説 | 侍 ...
http://www.sejuku.net/blog/7720
Pythonとは?何に使えるの?Pythonの特徴や使い道を…|Udemy メディア
https://udemy.benesse.co.jp/development/python.html
Pythonに関する12300件の投稿 - Qiita
https://qiita.com/tags/Python
Python基礎講座(1 Pythonとは) - Qiita
http://qiita.com/Usek/items/ff4d87745dfc5d9b85a4
初心者でもほぼ無料でPythonを勉強できるコンテンツ10選 - paiza開発日誌
http://paiza.hatenablog.com/entry/2015/04/09/%E5%88%9D%E5%BF%83%E8%80%85%E3%81%A7%E3%82%82%E3%81%BB%E3%81%BC%E7%84%A1%E6%96%99%E3%81%A7Python%E3%82%92%E5%8B%89%E5%BC%B7%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%8410
Python入門
http://www.tohoho-web.com/python/
Pythonとは - はてなキーワード - はてなダイアリー
http://d.hatena.ne.jp/keyword/Python
GoogleでPythonを検索するテスト    | PASS |

と表示されていきます。

ターミナルにPASSが表示されたところで、VMWindowsのIE11が閉じます。

また、実行ディレクトリに、IEの画面のスクリーンショットも保存されています。

f:id:thinkAmi:20170917222158p:plain

*画像は一部を切り抜いています。

 
以上より、

ができました。

 

参考

ソースコード

GitHubに上げました。 selenium_grid_sample/internet_explorer.robot ファイルが今回のテストファイルです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples

なお、 hub_config.jsonnode.json も含めています。もし使う場合は、 node.jsonSelenium Grid Nodeに渡し、IPアドレスなどを変更すると良いです。

また、 selenium-server-standalone-3.3.1.jarIEDriverServer.exe は含めていないため、それらはダウンロードしてください。

*1:Macと共有してもSelenium Grid環境を構築できるかもしれませんが、未検証です