Seleniumを使ってみる(Cygin64 + Python)

近年ますますリッチになっていくWebコンテンツ、たいていのことは単純なスクレイピングでできるけど、Javascriptやら何やらを駆使したサイトは扱いづらい。

ので、いつかはブラウザ自動化を試してみたいなあと思っていた、が調べるのが面倒でChromeの拡張を作ったりしてお茶を濁していたけど、そろそろちゃんと使ってみよう。

ということでSeleniumを使ってみる。まずは動かすとこまで、

予習

Seleniumと言っても色々種類があってややこしい。この辺のサイトが参考になる。

要するにSelenium WebDriver(Selenium2)を使うのがスタンダードということですな。

ダウンロード

Selenium - Downloads

Perl用のbindingがあればよかったのだけど、公式のものは無さそう。

サードパーティーでもいいんだろうけど、せっかくなので公式のものを使おう。この中ならPythonかな・・・。使ったことないけど。

WebDriver

Selenium自体は各言語からWebDriverを動かすためのライブラリ群(だと思う、WebDriverも含めてSeleniumなのかな)ので、それぞれのブラウザに対応したWebdriverというものをダウンロードしてくる。

上のSeleniumのサイトにリンクが紹介されているので適当にダウンロードして、パスの通ったディレクトリに保存する。

パスが通ってなくても、プログラム上でパスを指定できるっぽい。

インストール

準備がそろったのでSeleniumのライブラリーをインストールしよう。

If you have pip on your system, you can simply install or upgrade the Python bindings::


pip install -U selenium


Alternately, you can download the source distribution from PyPI(e.g. selenium-3.6.0.tar.gz), unarchive it, and run::


python setup.py install

pipとは。よく分からないからパッケージをダウンロードしてsetyp.pyを実行するヤツにしよう。

$ which python
/usr/bin/python -> python2.7.exe

$ which python3
/usr/bin/python3 -> python3.6m.exe

$ ln -sf /usr/bin/python3.6m.exe /usr/bin/python

よく分からんがCygwinだとpythonはバージョン2にリンクされているようなので、バージョン3の方にリンクさせておく。

$ python setup.py
Traceback (most recent call last):
  File "setup.py", line 22, in <module>
    from setuptools import setup
ModuleNotFoundError: No module named 'setuptools'

setuptoolsって何やねん。面倒くさいなあ・・・。

何かくっそややこしいな。心が折れてきた。

要するに現在はpipを使ってパッケージ管理をするのが主流で、pipはsetuptoolsを使ったツールということか。

PerlでいうとcpanmとCPANモジュールみたいなものかな?知らんけど。

ちらのうら - Cygwin上でpipとsetuptoolsをインストールする方法

なるほど。

Cygwinにpython3-pipとpython3-setuptoolsがあったのでインストールする。

binutils、libuuid-develは元々インストールしてあった。

$ pip3 install -U selenium
Collecting selenium
  Downloading selenium-3.6.0-py2.py3-none-any.whl (924kB)
    100% |################################| 931kB 585kB/s
Installing collected packages: selenium
Successfully installed selenium-3.6.0

なるほど、あっさりできた。えらい遠回りしたけど。。

テスト

from selenium import webdriver

browser = webdriver.Firefox()
browser.get('http://seleniumhq.org/')

おおー動いた。

import time
from selenium import webdriver

driver = webdriver.Chrome()  # Optional argument, if not specified will search path.
driver.get('http://www.google.com/xhtml');
time.sleep(5) # Let the user actually see something!
search_box = driver.find_element_by_name('q')
search_box.send_keys('ChromeDriver')
search_box.submit()
time.sleep(5) # Let the user actually see something!
driver.quit()

Chromeのdriverをダウンロードして試してみたが、こちらも動いた。しゅごい。

Smart::CommentsとかData::DumperのUTF-8文字列をエスケープしない

調べてみると色々でてくる。

Smart::Commentsで調べると上のサイトが出てくる。$SIG{__WARN__}をフックする方法らしい。これは毎回このコードを書かないとダメそうなので面倒っぽい。

Dat::Dumperのqquote関数を上書きする方法。

今回はSmart::Commentsさえどうにかなれば良いので、Smart/Comments.pmを書き換える。

use Data::Dumper 'Dumper';
{
    package Data::Dumper;
    no warnings 'redefine' ;
    sub qquote { return wantarray? @_ : shift; }
}
$Data::Dumper::Useperl = 1;
binmode STDERR, ":utf8";

色々マージしてこんな感じかなあ。

以前も同じようなことをしたはずだけど、Cygwinを再インストールしたら変更がもとに戻ってやり方を忘れてしまったのでメモしておく。

twitterをなんかするやつ

検索すると色々出てくるけど、シンプルにシェルスクリプトというのがよさげ。

基本的なアイデアは生きてるけど、今のtwitterの仕様では動画がダウンロードできないらしい。

GIF動画についてはこちらのページのやり方でいける。

mp4の動画はどうしようかと思って色々と悩んだ結果、youtube-dlに任せることにした。名前からしYoutube専用なのかと思ったら色々なサイトから動画がダウンロードできて便利ね。

youtube-dl

あとは不具合ではないんだけど、-oオプション(:orig)を付けた際にファイル名が "*.jpg%3Aorig" みたいになって見にくいのを直した。

全体的にはこんな感じか。時間があったら後で備忘録として実装の説明を書いておきたい。

youtube-dlにcookieを渡してないので、ログインしないとダメなヤツは保存できない。ま、必要になったら追加しよう・・・

$ diff -u berryjack berryjack.org
--- berryjack   2017-10-18 02:12:01.985022700 +0900
+++ berryjack.org       2017-10-17 23:34:22.090356500 +0900
@@ -4,9 +4,6 @@
 SCRIPT_DIR=$(cd $(dirname $0);pwd)
 source "$SCRIPT_DIR/common.sh"

-YOUTUBE_DL=$SCRIPT_DIR/youtube-dl
-FFMPEG=$SCRIPT_DIR/ffmpeg
-
 function get_profile_image()
 {
   src="$1"
@@ -130,31 +127,6 @@
       #   skip=1
       # fi
     done
-
-    # video(2) - GIF
-    # cf. http://www.unknownengineer.net/entry/2017/06/03/193913
-    for video in $(grep -Eo 'https://pbs\.twimg\.com/tweet_video_thumb/[a-zA-Z0-9_\-]+\.(jpg|png)' $tmp |sort |uniq |sed -e "s/pbs.twimg.com\/tweet_video_thumb/video.twimg.com\/tweet_video/" | sed -e "s/jpg/mp4/")
-    do
-      exist "$dir" "$video"
-    done
-
-    # video(3) - MP4(HLS)
-    # save sumbnails
-    for image in $(grep -Eo 'https://pbs\.twimg\.com/ext_tw_video_thumb/[0-9]+/[a-z]+/[a-z]+/[a-zA-Z0-9_\-]+\.(jpg|png)' $tmp)
-    do
-      exist "$dir" "$image"
-    done
-    # save videos
-    for video in $(grep -Eo 'data-conversation-id="[0-9]+"|ext_tw_video_thumb' $tmp | grep -B 1 ext_tw_video_thumb | grep data-conversation-id | grep -Eo '[0-9]+' | sed -e "s/^/https\:\/\/twitter.com\/$id\/status\//")
-    do
-      m3u8url=$($YOUTUBE_DL -g $video)
-      video2=$(echo $(basename $m3u8url) | sed -e 's/\.m3u8/.mp4/')
-      exist "$dir" "$video2" > /dev/null
-      if [ $? -eq 0 ]; then
-        echo $m3u8url
-      fi
-    done
-
     # image
     for image in $( grep -Eo 'https://pbs\.twimg\.com/media/[a-zA-Z0-9_\-]+\.(jpg|png)' $tmp | sort | uniq | \
       (if [ $orig -eq 0 ]; then
@@ -164,18 +136,9 @@
       fi
       ) )
     do
-      if [ $orig -eq 0 ]; then
-        exist "$dir" "$image"
-        if [ $? -ne 0 ]; then
-          skip=1
-        fi
-      else
-        exist "$dir" "$(echo $(basename $image) | sed -e 's/:orig$//')" > /dev/null
-        if [ $? -ne 0 ]; then
-          skip=1
-        else
-          echo $image
-        fi
+      exist "$dir" "$image"
+      if [ $? -ne 0 ]; then
+        skip=1
       fi
     done
     if [ $force -ne 1 -a $skip -eq 1 ]; then
@@ -276,17 +239,7 @@
     fi
     for media_url in $(get_media_url $id $get_orig "$cookie_file" $userdir $force)
     do
-      echo $media_url | grep -E '\.m3u8' > /dev/null 2>&1
-      if [ $? -eq 0 ]; then
-        $YOUTUBE_DL -o "${userdir:+${userdir}/}%(id)s.%(ext)s" --ffmpeg-location $FFMPEG $media_url
-      else
-        echo $media_url | grep -E ':orig$' > /dev/null 2>&1
-        if [ $? -eq 0 ]; then
-          wget $cookie_opt -nc -nv -P "$userdir" -O ${userdir:+${userdir}/}$(echo $(basename $media_url) | sed -e 's/:orig$//') "$media_url"
-        else
-          wget $cookie_opt -nc -nv -P "$userdir" "$media_url"
-        fi
-      fi
+      wget $cookie_opt -nc -nv -P "$userdir" "$media_url"
     done
   else
     get_media_url $id $get_orig "$cookie_file" "" $force

return; と return undef; の違い

use strict;
use warnings;
use Smart::Comments;

my %n = (val=>return_nothing());
my %u = (val=>return_undef());
### %n
### %u

sub return_nothing{
  return;
}
sub return_undef{
  return undef;
}
$ perl a.pl
Odd number of elements in hash assignment at /home/tmp/a.pl line 5.

### %n: {
###       val => undef
###     }
### %u: {
###       val => undef
###     }

ただの return; だと、ハッシュの初期化に使った場合 "Odd number of elements in hash assignment" などという警告が出るみたい。

結果はどっちでも同じなんだけど、明示的にreturn undef;としておいたほうが良い(場合もある)みたい。

追記

調べてみると結構おなじようなこと考えてる人がいた。

Use a bare return to return failure
返り値で失敗を報告する時は、裸の return をもってせよ

404 Blog Not Found:perl - (undef) is true

ふーむ。

print "return_nothing is " . ((return_nothing()) ? "TRUE" : "FALSE") . "\n";
print "return_undef is " . ((return_undef()) ? "TRUE" : "FALSE") . "\n";
return_nothing is FALSE
return_undef is FALSE
$ perl -v
This is perl 5, version 22, subversion 2 (v5.22.2) built for cygwin-thread-multi

ふーむ。。

return undefしてもfalseになるっぽいけど、バージョン依存なんだろうか。

まあ何にせよ、成功・失敗を返り値で判断するような場合はただのreturn、値を返すときはreturn undefと使い分ける感じが良いということかな。

dropbox-api-commandのファイル更新判定を変更する

Perlで実装されたDropbox操作コマンドです。

ディレクトリのsyncのために定期的にcronで実行して便利に使わせていただいていたのですが、ファイルの更新を「サイズが違う」あるいは「タイムスタンプが違う」のどちらかで判定しているので、古いファイルに同期してしまう問題があります。

普段は古いファイルをダウンロード(sync)される前に手動でpushして対応していたのですが、先ほど油断した隙に1時間の作業を全部ロールバックされてしまったので、さすがに何とかすることにしました。

   1114 sub has_change ($$) {
   1115     my ($local_path, $content) = @_;
   1116
   1117     my $remote_epoch = $strp->parse_datetime($content->{client_modified})->epoch;
   1118     my $local_epoch = $local_path->stat->mtime;
   1119     my $remote_size = $content->{size};
   1120     my $local_size = $local_path->stat->size;
   1121
   1122     if ($debug) {
   1123         printf "remote: %10s %10s %s\n", $remote_epoch, $remote_size, $content->{path_display};
   1124         printf "local:  %10s %10s %s\n", $local_epoch, $local_size, decode('locale_fs', $local_path);
   1125     }
   1126
   1127     if (($remote_size != $local_size) || ($remote_epoch != $local_epoch)) {
   1128         return 1;
   1129     }
   1130
   1131     return;
   1132 }

問題の部分はここ。

   1114 sub has_change ($$$) {
...
   1127 #    if (($remote_size != $local_size) || ($remote_epoch != $local_epoch)) {
   1128     if ($remote_size != $local_size){
   1129         if($is_upload){
   1130             return 1 if $remote_epoch < $local_epoch;
   1131         }
   1132         else{ # download
   1133             return 1 if $remote_epoch > $local_epoch;
   1134         }
   1135     }
   1136     return;
   1137 }

has_changedはダウンロードとアップロードの2箇所呼ばれるので、「ファイルが新しい場合に更新」を実現させようとすると不等号の向きが逆になります。のでフラグを追加。

$ grep -nE "has_change|^sub sync_upload|sub sync_download" /usr/local/bin/dropbox-api
720:sub sync_download {
764:            if ((!-f $local_path) || has_change($local_path, $content, 0)) {
872:sub sync_download_file {
884:    if ((!-f $local_path) || has_change($local_path, $content, 0)) {
927:sub sync_upload {
976:                if (has_change($local_path, $content, 1)) {
1050:sub sync_upload_file {
1091:        if (has_change($local_path, $content, 1)) {

Perlで配列の値をハッシュにしたいような時

表現が難しいんだけど

my @value = (1, 2, 3, 4, 5);

みたいなデータがあって、それをハッシュとして初期化したいときはどうするか。

my %hash = (
  key1 => $value[0],
  key2 => $value[1],
..
..
);

とかするのだろうけど、配列の個数が何個もあると面倒くさいなぁってなる。

例では5個だけど、10個も20個も項目があるとちょっと厳しい。

実はもっと簡単に

my %hash;
@hash{qw/key1 key2 key3 key4 key5/} = @value;

みたいな感じで書ける。ただし代入はできるけど、初期化でやろうとするとコンパイルエラーになる。

数年前に見かけて何じゃそりゃって思ったけど便利な書き方なのでメモしておく。でもこれ、数年後にコード読み返したら分からなくなりそうだな。

bio監視する的な

推しのプロフィールの更新をチェックするツール、bioischanged.comみたいなサービスがあるみたいだけど、どこまで変更を細かくチェックするのか分からなかったので、自分でもちょっと作ってみた。

cronに登録して放っておくとDMが来る。ネトストぽい。。

別にJSONでなくても良かった気がする。まあいっか。

追記

100年ぶりにgistにアクセスしたらアカウントがロボット判定されたらしく強制プライベートモード?みたいな感じになってた。

フォームから連絡したら1時間も経たずにゴメンゴメン直しておいた的な返事がきてスゴイなって思った。

追記2

"This request looks like it might be automated. To protect our users from spam and other malicious activity, we can't complete this action right now. Please try again later." などというエラーが出てDMを送れなくなってしまった。どうすりゃいいのこれ。。

とりあえずDMじゃなくてメールを送る方式に変更した。

追記3


エモい(;ω;)(;ω;)(;ω;)

追記4


お疲れ様でした。

作成から1週間でcronから外される運命のスクリプトでしたが、プログラムが多少なりとも書けて良かったと思った。