Puppeteer † 
- パズル認証や2段階認証は、正面から突破するより、1回は手動で補助してログインし、cookieをやりとりすれば、たいていはしばらく追加認証を回避できる。(毎回、追加認証を課すサイトには通用しない。)cookieはPuppeteerのコードで明示的に取得・保存する必要がある。headlessブラウザに(自動的に)保存されるものは助けにならないようだ。(サイトによって違うかも。)
- (毎回の)パズル認証を(簡単に)回避できたサイト:
- headlessだとうまく巡回できない(headlessを無効にすれば順調な)サイトもある。例えば、三井住友カードはheadlessだといまのところ巡回に成功していない。headlessで動かないと、VPSのCentOSで動かすのが面倒だ(普通、クラウドのCentOSにX Windowは入れない。)。cronで定期的に実行するには、VPSが望ましい。次善の策として、新たに、自宅サーバーにGNOME Desktop付きのCentOS 7を入れた。(CentOS 6ではheadless Chromeは動かないようだ。)これでいまのところ三井住友カードも自動巡回できている。
- cronで動かすならtry-catchを書かないと、時間切れなど何か予期せぬことが起きる度にメモリに貯まっていく。自動終了しない。
Error: Node is either not visible or not an HTMLElement † 
単に、指定したセレクタが複数あるだけかも。例えばinput[name="password"]が2つあるときに、await page.typeやawait page.focusなどは上記エラーが出る(ことがある)。そういうときはclass: ElementHandleでオブジェクトのリストを取得し、リストの番号で目的のオブジェクトを操作すればよい。例えば2つ目を扱いたいなら、
let items = await page.$$('input[name="password"]');
items[1].click();
pageクラスと書式が同じものもあれば違うものもある。例えばelementHandle.typeは、セレクタを引数に取らない。puppeteer/api.md at master · GoogleChrome/puppeteer · GitHub
sample † 
新電力 あしたでんき † 
スマートメーターの情報をグラフで表示するページを電力各社は提供している。私が契約している「あしたでんき」は、データをCSV等でダウンロードできないので、画面から取得している。当初、Greasemonkeyで試したが失敗したので、Puppeteerに切り替えた。
このサンプルでは、前月のデータをTSVで書き出している。当月を取得したいなら前月に移動するコードを削除すればよい。
const puppeteer = require('puppeteer');
const fs = require('fs');
const cheerio = require('cheerio');
const path = require('path');
let tsvfile = path.join(__dirname, 'ashitadenki.tsv');
(async() => {
const browser = await puppeteer.launch({
headless: false,
args: [
'--no-sandbox',
]
});
const page = await browser.newPage();
await page.setViewport({width: 1024, height: 2048});
try {
await page.goto('https://ashita-denki.jp/login/', { waitUntil: 'networkidle2'});
await page.waitForSelector('input#username', {waitUntil: 'networkidle2', timeout: 10000});
await page.focus("input#username");
await page.type("input#username", 'your ID');
await page.focus("input#password");
await page.type("input#password", 'your password');
await page.waitFor(1000);
await page.click('input[type=submit]');
await page.waitForNavigation({waitUntil: 'load'});
await page.waitForSelector('#monthly-report-table', {waitUntil: 'networkidle2', timeout: 2500});
var now = new Date();
var month = now.getMonth(); //1月は0
// 前の月を表示する
await page.waitForSelector('#month > form > div:nth-child(1) > select:nth-child(2)');
await page.select('#month > form > div:nth-child(1) > select:nth-child(2)', month.toFixed());
await page.waitFor(3000);
await page.waitForSelector('#monthly-report-table', {waitUntil: 'networkidle2', timeout: 2500});
let html = await page.$eval('#monthly-report-table > ul', e => e.outerHTML);
$ = cheerio.load(html);
let lines = [];
$('li').each(function(){
let row = [];
// #monthly-report-table > ul > li:nth-child(1) > time
row.date = $(this).find('time').text().trim();
if (row.date == '') {
return true; //$().eachのnext
}
row.date = row.date.replace(/日/,'');
row.date = month.toFixed() + '/' + row.date;
// #monthly-report-table > ul > li:nth-child(1) > ul > li.kwh > span
row.kw = $(this).find('ul > li.kwh > span').text().trim();
// #monthly-report-table > ul > li:nth-child(1) > ul > li.cost > span
row.cost = $(this).find('ul > li.cost > span').text().trim();
lines.push(Object.values(row).join("\t"));
});
fs.writeFile(tsvfile, lines.join("\n"),(err) => {
if (err) throw err;
});
} catch (e) {
console.log('caught');
process.stderr.write(e.toString());
await browser.close();
process.exit(1);
}
await browser.close();
})();
Node.js † 
cheerio † 
文字コード † 
Perl † 
- (先ず簡単な方から。)URLエンコードされている文字列を取り扱う(例えばApacheのログファイルの解析)には、単にURLデコードしただけでは駄目。URLデコード後、直ちにPerlの内部表現に「デコード」する。さもないと他の文字列と整合性がとれなくなる。例えば、一般に全ての文字列は、出力の際、エンコードする。URLデコードした文字列について、デコードされる前にエンコードされると二重エンコードになり、文字化けする。
- (これは意外だった。)URLエンコードされている文字列を含むファイル(例えばApacheのログファイル)を読み取るとき、「open(IN, '<:utf8'...」や「use open IN => ":utf8";」でデコードすると、(その後、Encode::decode_utf8しようがしまいが)スクリプトのリテラル(use utf8;前提)と(正規表現や文字列比較演算子で)マッチしなくなる。リテラルによる正規表現や文字列比較演算子が使えない。文字化けはないので、上記より気付きにくい。use open INやbinmode INを便利なプラグマとして、雛形として使っていることは少なくないのでは。
- 「<:utf8」ではなく「<:encoding(UTF-8)」でも変わらず。「:utf8 は、さらなるチェックなしにデータが UTF-8 としてマークしますが、 :encoding(UTF-8) はデータが実際に有効な UTF-8 かどうかをチェックします。 」perlfunc - Perl 組み込み関数 - perldoc.jp
- 「binmode IN, ":utf8";」でも同様に駄目になる。
- 下記のコードで試せる。
use utf8;
use strict;
use warnings;
use Encode;
use Devel::Peek;
my $str = '%E6%97%85%E8%A1%8C'; # 旅行
my $file = 'test.bin';
my $str_decoded = decode_utf8( &urldecode ($str));
print "basis:$str_decoded\n";
Devel::Peek::Dump($str_decoded);
print qq(\n);
open (OUT, ">$file");
binmode OUT;
print OUT $str;
close OUT;
# -------------------------------------------------------
# デコードせず読み込み
open (IN, "<$file");
#binmode IN;
my $str1_from_file = <IN>;
close IN;
&compare($str,$str1_from_file);
print qq(\n);
# -------------------------------------------------------
# デコードして読み込み
open(IN, '<:utf8', $file);
#binmode IN;
my $str2_from_file = <IN>;
close IN;
&compare($str,$str2_from_file);
# -------------------------------------------------------
# URLデコード
sub urldecode{
my $uri = shift(@_);
$uri =~ tr/+/ /;
$uri =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('H2', $1)/eg;
return $uri;
}
# 比較
sub compare{
my @sample = @_;
# URLデコード前
&sameornot(@sample);
# URLデコード
my @sample_urldecoded = map {urldecode($_)} @sample;
&sameornot(@sample_urldecoded);
# 内部表現へのデコード
my @sample_decoded = map {decode_utf8($_)} @sample_urldecoded;
&sameornot(@sample_decoded);
sub sameornot {
$_[0] eq $_[1] ? print "same\n" : print "differnet\n";
}
# 正規表現
if ($sample_decoded[1] =~ /旅行/) {
print "1) regex works\n";
}
# 念のためdecode_utf8前で試す
if ($sample_urldecoded[1] =~ /旅行/) {
print "2) regex works\n";
}
print "compared:$sample_decoded[1]\n";
print "before decode_utf8\n";
Devel::Peek::Dump($sample_urldecoded[1]);
print "after decode_utf8\n";
Devel::Peek::Dump($sample_decoded[1]);
}
- 途中「Wide character in print」と言われるが、それが正しい。むしろ3番目の出力箇所で出ないことの方が意外だった。
- このような結果になる。
$ perl test2.pl
Wide character in print at test2.pl line 12.
basis:旅行
SV = PV(0x1018ee0) at 0x1037980
REFCNT = 1
FLAGS = (PADMY,POK,pPOK,UTF8)
PV = 0x110a940 "\346\227\205\350\241\214"\0 [UTF8 "\x{65c5}\x{884c}"]
CUR = 6
LEN = 16
same
same
same
1) regex works
Wide character in print at test2.pl line 73.
compared:旅行
before decode_utf8
SV = PVMG(0x10d7a20) at 0x1037188
REFCNT = 1
FLAGS = (POK,pPOK)
IV = 0
NV = 0
PV = 0x1122220 "\346\227\205\350\241\214"\0
CUR = 6
LEN = 16
after decode_utf8
SV = PV(0x1019740) at 0x1037218
REFCNT = 1
FLAGS = (POK,pPOK,UTF8)
PV = 0x1097030 "\346\227\205\350\241\214"\0 [UTF8 "\x{65c5}\x{884c}"]
CUR = 6
LEN = 16
same
same
differnet
compared:旅行
before decode_utf8
SV = PVMG(0x10d7a50) at 0x10370e0
REFCNT = 1
FLAGS = (POK,pPOK,UTF8)
IV = 0
NV = 0
PV = 0x110acb0 "\303\246\302\227\302\205\303\250\302\241\302\214"\0 [UTF8 "\x{e6}\x{97}\x{85}\x{e8}\x{a1}\x{8c}"]
CUR = 12
LEN = 16
after decode_utf8
SV = PVMG(0x10d7960) at 0x10371e8
REFCNT = 1
FLAGS = (POK,pPOK,UTF8)
IV = 0
NV = 0
PV = 0x1030240 "\303\246\302\227\302\205\303\250\302\241\302\214"\0 [UTF8 "\x{e6}\x{97}\x{85}\x{e8}\x{a1}\x{8c}"]
CUR = 12
LEN = 16
メモ † 
- printfの記法は気が滅入る。表で値を与えてみた。
- 普通の書き方
printf "<tr><td $tdclr_host>%s<td>%s<td>%s<td>%.60s<td $tdclr_stts>%s<td>%.50s</tr>\n",substr($host,-30),"$mon $date",$clock,$uri,$stts,$ua;
- 表で
my @printform = split /\n/, <<END;
<tr>
<td $tdclr_host>%s @{[substr($host,-30)]} %h host, 式展開の記法
<td>%s $mon $date 日付
<td>%s $clock 時刻
<td>%.60s $uri %rからページ名切り出し
<td $tdclr_stts>%s $stts status code
<td>%.50s $ua User-Agent
</tr>
END
for (@printform) {
@_ = split /\t+/;
if (defined $_[1]) {
printf qq($_[0]),$_[1];
} else {
print qq($_[0]);
}
}
- CGIプログラムをroot名義で動かしたいことがある。sudoを使えば可能。当初誤解していたのだが、sudoersに適切な記載をすれば、その条件で、自動的に実行ユーザーが変わるのだと思っていた。(mod_rewriteのように。)正しくはsudoコマンドが必要。sudoコマンドで、対象コマンド等を呼んで初めて実行ユーザーが変わる。
- CGIスクリプトそれ自体をsudoで呼び出し実行する(CGIとして)ことは非現実的なので、CGIスクリプトの中で本命のスクリプトないしコマンドをsudoで呼び出すことになる。sudoersに登録する対象コマンド等は、その本命スクリプトないしコマンドになる。ただし、/bin/cpや/sbin/iptablesのようなコマンドを登録するのは影響範囲が広すぎ、危険だ。反面、スクリプトを守るのが要になるので、「chown root.root スクリプト」しておくとよい。