Wunderlist APIを使ってみた

Wunderlistの完了済みタスクをコピーしてEvernoteに保存してタスクを削除、というのを毎週するようにしたのだけど、どうにも面倒くさい。

Documentation | Wunderlist Developer

APIが提供されているから自動化できそう。

検索するとPerlではAPI::Wunderlistというモジュールが公開されているのだけど、インストールがどうも上手く行かなくて、強制インストールするとコードがエラーになる。

http://deps.cpantesters.org/?module=API%3A%3AWunderlist;perl=latest

どうやらType::Tinyのテストが失敗するようなのだけど、コードが抽象的すぎてよく分からんし諦めた。

そもそもX-Client-IDとX-Access-Tokenを付けたHTTPを投げるだけなんだから、curlを使ったシェルスクリプトでもいいんだよね。PerlならLWPをそのまま使ってもいいし。何でインストールに四苦八苦してまでモジュールを入れんといかんねん。

こういうマイクロな実装ってライブラリの導入とか依存関係とかで困るから結局モノリシックなものが便利なんじゃないかという気になってきた(老化)

余談はともかく。完了したタスク+ノートをリストアップするスクリプトを作ってみた。

LWPでもいいけど、REST::ClientといのがLWPベースのシンプルな実装っぽいので使うことにした。

use strict;
use warnings;
use Carp qw/croak/;

use REST::Client;
use HTTP::Response;
use JSON::XS;
use YAML::Syck;

my $JSON   = JSON::XS->new->utf8;
my $API    = 'a.wunderlist.com/api/v1';
my $client = REST::Client->new();
my $res;

$client->addHeader('X-Client-ID', $CLIENT_ID);
$client->addHeader('X-Access-Token', $ACCESS_TOKEN);

# 1. get lists
$client->GET("$API/lists");
$res = res_with_check($client);
### $res

# 2. get completed tasks and notes
my @ctasks;
foreach my $l (@$res){
  ### $l
  $client->GET(sprintf("$API/tasks?completed=true&list_id=%s", $l->{id}));
  $res = res_with_check($client);
  ### $res
  foreach my $t (@$res){
    ### $t
    $client->GET(sprintf("$API/notes?task_id=%s", $t->{id}));
    $res = res_with_check($client);
    ### $res
    push(@ctasks, {
      id           => $t->{id},
      list         => $l->{title},
      title        => $t->{title},
      created_at   => $t->{created_at},
      completed_at => $t->{completed_at},
      note         => ($$res[0]->{content} || ''),
    });
  }
}
### @ctasks

DumpFile("wl_completed.yml", \@ctasks);

sub res_with_check{
  my $client = shift;
  my $code = $client->responseCode();
  if($code ne '200'){
    my $err = sprintf("Server Error: %s\n", HTTP::Response->new($code)->status_line);
    $err .= $client->responseContent() . "\n" if $client->responseContent();
    croak $err;
  }
  my $data = $client->responseContent();
  my $tree = $JSON->decode($data);
  return $tree;
}
  • ドキュメントによるとnotesにはlist_idも指定できるんだけど、未完了タスクだけで完了済みタスクのnoteを取ってこないから、個別のtaskごとにリクエストしないとダメっぽい
    • そもそもWLのタスクに付けてるメモは "Note" ではなく "Task comment" だと思ってて「なんで空のコンテンツが返ってくるんや〜」って数時間悩んでいた。説明が足りない。Documentation | Wunderlist Developer
  • REST::Client->GETがいい感じにエラーハンドリングしてくれて直接contentを返してくれたらコードがスッキリするかなあ
  • JSONで受け取ったデータをYAMLで保存するのもアレだなあと思うんだけど、見やすいし手動で加工しやすいので
  • REST APIをいい感じに扱えるライブラリってなんだろ。rubyとかpythonの方がいいのかなあ。

追記 2018-11-07

http - Perl: Programatically set POST param using REST::Client module - Stack Overflow

POSTでJSONデータを渡すときは明示的に "Content-type: application/json" を付けないといけないらしい。気づかなくて小一時間悩んでしまった・・・。

あと、JSONのencode/decode周りの日本語処理がクソ訳わからなくて心が折れそうになった。

こんな感じでモジュール化したので、今後使う機会があればもっと簡単にできそう。

sub new{
  my $this  = shift;
  my $class = ref($this) || $this;
  my $self  = {};
  bless $self, $class;

  $self->{api}    = 'a.wunderlist.com/api/v1';
  $self->{client} = REST::Client->new;
  $self->{client}->addHeader('X-Client-ID', $client_id);
  $self->{client}->addHeader('X-Access-Token', $access_token);
  return $self;
}

sub post{
  my $self = shift;
  my ($op, $data) = @_;
  my $uri = sprintf("%s/%s", $self->{api}, $op);
  $self->{client}->POST($uri, JSON::XS->new->ascii->encode($data), {"Content-type" => 'application/json'});
  return $self->res_with_check;
}

10年遅いPerlのourの話

10年ぶりにPerlを書いて色々調べていたら超懐かしい話題を見かけたので。
ourの挙動は分かったけど、一番重要なのはそれで何が嬉しいの?という事じゃないのかなあ。
要するにuse strict環境でグローバル変数(完全修飾名?)を簡単に作れる方法というだけ。

use strict;

package VERY::LONG::NAME::MODULE{
  # $var = 1; これはエラー
  # $VERY::LONG::NAME::MODULE::var = 1; これはOKだけど面倒くさい
  # my $var = 1; スコープを外れると見えなくなる(undef)
  our $var = 1;
}
print "$VERY::LONG::NAME::MODULE::var\n";

Firefox webdriverでcookieが読めなくなった話

取得したクッキーを保存して読み直すだけのコードである。

from selenium import webdriver
import json

driver = webdriver.Firefox()
#driver = webdriver.Chrome()
#driver = webdriver.PhantomJS()

try:
    driver.get("http://www.google.com")

    fw = open('cookies.json','w')
    json.dump(driver.get_cookies(),fw,indent=4)
    fw.close()
    
    f = open("cookies.json")
    cookies = json.load(f)
    f.close()

    for cookie in cookies:
        driver.add_cookie(cookie)

finally:
    driver.quit()

最近Firefoxのバージョンを最新(57)に上げたら、上のコードがエラーになった。

$ python test.py
Traceback (most recent call last):
  File "test.py", line 24, in <module>
    driver.add_cookie(cookie)
  File "/usr/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 760, in add_cookie
    self.execute(Command.ADD_COOKIE, {'cookie': cookie_dict})
  File "/usr/lib/python3.6/site-packages/selenium/webdriver/remote/webdriver.py", line 308, in execute
    self.error_handler.check_response(response)
  File "/usr/lib/python3.6/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.WebDriverException: Message: .google.co.jp

よく分からん。

ドメインの先頭にドットがついてるとエラーになる(InvalidCookieDomainException)という話もあるみたいだけど、generalなWebDriverExceptionとしか出てこないので、やっぱりよく分からん。

先頭のドットを消すと確かにExceptionを出さずに動くようになるのだが、そうすると目的のプログラムでcookieを期待通りに読んでくれなくなって詰む。

仕方ないのでChromeを使うか・・・と思うと、今度はswitch_to.windowでactiveにしたウインドウ(タブ)は必ずフォアグラウンドになってしまうというアレな仕様で詰む。(参考:ウインドウの最小化 - XXXannex

Seleniumを使うと、だいたいあちらを立てればこちらが立たぬ・・・みたいなことが多くてウンザリする。

iMacrosを自動起動する方向に変換した方がいいのかもしれない。まあ、あちらはあちらでwindows nativeなことができなくて困りそうだけど。

ま、いいや。とりあえず適当に53(53.0.3)くらいまで戻して事なきを得た。二度とアップグレードしないぞ。

追記

自動更新をOFFにし忘れたせいでまた57にアップグレードされてしまった。切れそう。

○○ソートみたいなやつ

ソートファクトリー
こういうの。

use strict;
use warnings;
use Path::Class;

my @list  = file('list.txt')->slurp(chomp=>1);
my $COUNT = 0;
print "$_\n" foreach sort { compare($a, $b) } @list;

sub compare{
  my($lhs, $rhs) = @_;
  printf("#%02d: which do you like?\n", ++$COUNT);
  print "1. $lhs\n";
  print "2. $rhs\n";
  print "3. Even\n";
  my $input = <>; $input =~ tr/\x0A\x0D//d;
  if($input == 1){
    return -1;
  }
  elsif($input == 2){
    return 1;
  }
  elsif($input == 3){
    return 0;
  }
  else{
    die;
  }
}

ウインドウの最小化

ルームを沢山開いてウインドウを放っておくと、全ルームの音がなって大変なことになるし、ブラウザがクソ重くなって大変なことになる。

ルームを開く前にウインドウを最小化しておけば大丈夫なので、実行時はブラウザを最小化しておきたい。

しかし、これが意外とできない。マジか。最大化はアッサリできるのに。

どうやらSeleniumAPIでは無理らしいので、キーボードショートカット(Alt+Space→n)を送るという原始的手段で実現するらしい。

  1. ActionChainsを使う
  2. win32comモジュールを使う
  3. ctypesを使ってwin32 dllをインポートする
  4. JavascriptからActiveXオブジェクトを使う

調べてみると、こんな感じの情報が色々出てくるが、どうやらCygwin+Python+Seleniumだと全滅のようで。素直にWindowsPythonを使っておけば良かったか・・・。

PerlだとCygin用でもWin32::GuiTestみたいなモジュールが使えるんですけどね・・・。

ので、苦肉の策として外部プログラムを実行することにした。

os.system('cscript close_window.vbs')
Option Explicit
On Error Resume Next

Dim objWshShell
Set objWshShell = WScript.CreateObject("WScript.Shell")
WScript.Sleep(2000)
objWshShell.SendKeys "% "
WScript.Sleep(500)
objWshShell.SendKeys "n"

Set objWshShell = Nothing

os.systemよりもsubprocess.callを使おう、みたいな情報が山ほど出てくるけど、さらにsubprocessの使い方を調べるの面倒くさいし動くからいいでしょ。(散々調べて全然できなかったので、ここに辿り着くまでに心が折れた)

なんかPythonを使うたびに「え、こんなこともできないの」みたいなことが多くてしんどい。

これなら最初からJavaで書けばよかったかもしれない。。

ま、ともかく必要な機能は無事に実装できた。後は細々とした使い勝手の調整とかリファクタリングとかをしていけばいいかなあ。

星投げと50カウントも自動化できれば便利なんだろうけど、そこまではいいかな。ツールもあるし手動でも何とかなるし。

そもそも、一番大きな動機が「配信が始まる前に★を貯めておくのを忘れる」「捨て★の時間を忘れる」といったものなので、始まってからの自動化はそこまで必要としてないのだ。

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を再インストールしたら変更がもとに戻ってやり方を忘れてしまったのでメモしておく。