amazonの商品ページから画像一覧を取得するブックマークレット

2017年1月18日 追記
アマゾンの画像を置いているサーバーのドメインが変わったみたい?
ドメイン名のところを修正しています(m´・ω・`)m ゴメン…


ひさしぶりにブックマークレット・ネタ。

会社でたまに、商品紹介の版下データをIllustratorで作成することがあるのですが・・・商品画像を集める作業は一苦労してました。あるときは、紙のカタログからスキャンしたり、PDFのページをリーダーで表示させた状態をスクリーンショット撮ったり(笑)、まぁ結構なんぎしてました。
でも最近は違います! アマゾンのサイトに行くと、ほぼ全ての画像が!!!(^^;;; ま、当たり前だけど、amazonに載ってある画像を勝手に版下データに流用するのはチョシャッケンを侵害!!! ってのは理解しているんですが、商品メーカーの販売会社の営業さんとかに問い合わせもらっても、いちいち対応がメンドクサイのか、アマゾンの画像を使えと(笑) 末端の下っ端なんてそんなもんですよ。

ってなわけで、ブラウザでいちいち右クリック・画像保存・・・とかいうルーチンは、パソコンが得意。

商品ページのHTMLコードをザラッと解析して(< オーバーだよ・・・)、ブックマークレットを作った。
だいたい、↓ こんな感じ。

/* 
amazonの任意の商品ページでF12キーでDevツールを出し、
コンソールにて以下をコピペして実行して試せます。
2017/1/18 画像サーバーのドメイン名をマッチするための正規表現を修正してます。
*/
(function($,undefined)
{
  var multi = {};
  var m = $.each($('body').html().match(/https?:\/\/images\-\w+\.ssl\-images\-amazon\.com\/images\/I\/(?:[\w%\-]+)(?:\._S[A-Z]\d+_)?\.jpg/g),
                function(i,v)
                {
                  var url = v.replace(/\._S[A-Z]\d+_/,'');
                  if(url in multi)
                    return;
                  multi[url] = null;
                });

  var urls = [];
  $.each(multi,
         function(url,v)
         {
           urls.push(url);
         });

  var len = urls.length;
  var doc = window.open("","image").document;
  doc.open("text/html");
  doc.writeln("<html><head><title>Imaging</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><p><a href=\"javascript:window.close();\">ウィンドウを閉じる。</a></p><p>");
  for(var i=0;i<len;i++)
    {
      doc.writeln("<a target=\"_blank\" href=\"",urls[i],"\"><img width=\"100\" border=\"0\" src=\"",urls[i],"\"></a>");
    }
  doc.writeln("</p></body></html>");
  doc.close();

})(jQuery);

ただ、これだと、window.openメソッドを使っているので、ブラウザの設定によってはポップアップブロックされるかも。amazon.co.jpだけ例外設定してやればヨシ。

上のコードをコピペしてブラウザのデベロッパーツール>console にて確認してみてください。
ポップアップされたウィンドウまたはタブに画像がデデデと列挙されるはず・・・されなかったらごめんなさい。
画像は幅100ピクセルに縮小して表示されてますが、画像をクリックしてやると大きくなるはず・・・されなかったらごめんなさい。

wordpress.comでは javascript: なリンクは貼れないので、ミニファイしたコードを↓に書いときますので、適当なブックマークを作って、編集して、URLに、↓のコードをコピペして保存してください。

javascript:(function($,undefined){var multi={};var m=$.each($('body').html().match(/https?:\/\/images\-\w+\.ssl\-images\-amazon\.com\/images\/I\/(?:[\w%\-]+)(?:\._S[A-Z]\d+_)?\.jpg/g),function(i,v){var url=v.replace(/\._S[A-Z]\d+_/,'');if(url in multi)return;multi[url]=null});var urls=[];$.each(multi,function(url,v){urls.push(url)});var len=urls.length;var doc=window.open("","image").document;doc.open("text/html");doc.writeln("<html><head><title>Imaging</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><p><a href=\"javascript:window.close();\">ウィンドウを閉じる。</a></p><p>");for(var i=0;i<len;i++){doc.writeln("<a target=\"_blank\" href=\"",urls[i],"\"><img width=\"100\" border=\"0\" src=\"",urls[i],"\"></a>")}doc.writeln("</p></body></html>");doc.close()})(jQuery);

使い方は・・・分かりますよね? amazonって、頻繁に表示方法を変えているので、たぶんそのうち使えなくなるかもですね。
あ、エッチな商品の画像を取っちゃだめですよ(笑) あくまでお仕事の一助に(^^;;;

ブックマークレットって何? どうやって登録するの? どうやって使うの? おいしいの? とかいうメンドクサイ人は・・・適当にググってください(_ _)

twitterのお気に入りの画像を一括ダウンロード

追記 2017/07/15
C#に書き直した修正版 → https://ptsv.jp/2016/04/20/tweet-image-download-agent/


追記 2015/11/10
修正版 → スクリプトを修正したものを投稿していますm(_ _)m


ツイッターで、検索したりタイムラインに流れる画像で気になったのは片っ端から「お気に入り」に入れてました。で、とある日曜の夜中にふと、「ファボッったツイートの画像を全部ダウンロードしたい!」欲求に駆られ、ググってみたら下記ブログ記事を見つけた。

dyama.org/2014/10/berryjack-simple-twitter-media-downloader-by-shellscript/

ここで書かれていたtimelineに直接リクエストを投げる方法を、ブラウザで確認しながら調べてみると、できそうだったので、シェルスクリプトを流用させてもらいました。(m_m)
・・・Windowsなので、perlに書き直して完了。Win32版のwget.exeが必要ですが・・・。

ただ、「お気に入り」の画像を一括ダウンロードするにはログインが必要なんで、その部分を調べて書くのが面倒だったんで、Internet Explorerでログインした後、クッキーをエクスポートし、twitter.comドメインの部分だけ抜き出し cookies.txt と保存し、それをwgetに食わせることで強引に解決(^^;;; とりあえずなんで目をつむる。

Twitter APIとか使わないといけないのかなー、と思ってたんですが、上記ブログ記事のシェルスクリプトのおかげで随分簡単にゲトできました。ありがとうございました(^.^)

探せば便利なソフトがあると思うんですが・・・簡単にできたんで別にいいか(^^;
また、時間があれば、C#に書き直してGUIつくろー。

#!/usr/bin/perl
# 使い方
#
# > perl twitter.pl 画像タイムラインのURL(下記参照)
#
# 【任意のTwitterIDの画像の場合】
# https://twitter.com/i/profiles/show/(TwitterID)/media_timeline

# 【自分のお気に入りの場合】
# https://twitter.com/(自分のTwitterID)/favorites/timeline
# 別途ログイン後のクッキーが必要。IEなどでログインしてエクスポート。
# twitter.comドメインだけ抜き出して 同じディレクトリにcookies.txt として保存する必要あり。
#
# tmpというディレクトリがカレントディレクトリに作成され、画像がダウンロードされます。
# ダウンロードエージェントに GNU Wgetを利用しています。
# おいらのようにwindowsな人は別途 Win32版 GUN WGETが必要。

# 【追記】 wgetを使用せず、LWP::UserAgentを使用するように加筆したものを一番下に追加

use bigint;
mkdir 'tmp' unless(-e './tmp');
$param = '';
$timeline = $ARGV[0] || die "input timeline url...\n";

@options = ('--no-check-certificate',
            '-O -',
            "-a ./tmp/wget_$$.log");
push @options,'--load-cookies=cookies.txt' if(-e 'cookies.txt');

$options = join(' ',@options);
do
{
  $wget = sprintf('wget %s "%s%s"',$options,$timeline,$param);
  print $wget,"\n";
  $result = `$wget`;
  $result =~ s/\\\//\//g;

  @result = $result =~ m!https://pbs\.twimg\.com/media/\w+\.\w{3,4}\:large!g;
  foreach(@result)
    {
      $out = '';
      if(m/(\w+\.\w{3,4})\:large$/)
        {
          $filename = "tmp/$1";
          $out = "-O $filename";

          unless(-e $filename)
            {
              system("wget --no-check-certificate -a ./tmp/wget_$$.log -P tmp -nd $out $_");
              print "saved $1\n";
            }
        }
    }

  @result = $result =~ m/data-tweet-id=\\\"([0-9]+)/g;
  @result = sort @result;

  $cxt_id = shift @result;
  $max_id = $cxt_id - 1;

  $param = "?contextual_tweet_id=$cxt_id&max_id=$max_id";
} while( $cxt_id );

【追記】

wget を使う代わりに、LWP::UserAgent を使用するように改変。使い方は上と一緒。
・・・なんか無駄にコードが増えた(ーー;;;

#!/usr/bin/perl

# 使い方は上記wgetを使用するものと一緒

use strict;
use warnings;
use bigint;
use LWP::UserAgent;
use HTTP::Cookies;
use Time::Piece;

#クッキーファイル名
my $COOKIE = 'cookies.txt';

#作業用クッキーファイルパス
my $TMP_COOKIE = './tmp/libwww.'.$COOKIE;

#エージェント
my $UserAgent = LWP::UserAgent->new;

&{sub
{
  #arguments
  my @argv = @_;
  mkdir 'tmp' unless(-e './tmp');

  my $tmp = "./tmp/$$.media";
  my $param = '';
  my $timeline = $argv[0] || die "input timeline url...\n";
  my ($cxt_id,$max_id);

  #cookies.txtがカレントディレクトリにあればフォーマット変換して作業用ファイルに保存
  if(-e $COOKIE)
    {
      &convert_cookie;
      $UserAgent->cookie_jar(HTTP::Cookies->new(file => $TMP_COOKIE,autosave => 1));
    }

  do
    {
      $timeline = $argv[0].$param;

      print "---- getting and parsing\n$timeline ...\n----\n";
      my $result = &lwp_agent($timeline,'-') || die "can not get timeline. may be wrong url.\n";
      $result =~ s/\\\//\//g;

      foreach my $url_($result =~ m!https://pbs\.twimg\.com/media/\w+\.\w{3,4}\:large!g)
        {
          my $out = '';
          if($url_ =~ m/(\w+\.\w{3,4})\:large$/)
            {
              my $filename = "tmp/$1";
              unless(-e $filename)
                {
                  &lwp_agent($url_,$filename);
                  print "saved $1\n";
                }
            }
        }

      my @result = $result =~ m/data-tweet-id=\\\"([0-9]+)/g;
      @result = sort @result;

      $cxt_id = shift @result;
      $max_id = $cxt_id - 1;

      $param = "?contextual_tweet_id=$cxt_id&max_id=$max_id" if(defined $cxt_id);

    } while( $cxt_id );

  print "done!\n";

  #作業用のクッキー削除
  unlink $TMP_COOKIE if(-e $TMP_COOKIE);

}}(@ARGV);

sub lwp_agent
{
  my ($url,$ofile) = @_;
  my %options = ();

  if($ofile ne '-')
    {
      return $UserAgent->get($url,':content_file' => $ofile);
    }
  else
    {
      if(my $response = $UserAgent->get($url))
        {
          return $response->decoded_content;
        }
    }
  0;
}

sub convert_cookie
{
  my $fin = IO::File->new($COOKIE);
  my $fout = IO::File->new('>'.$TMP_COOKIE);

  $fout->print("#LWP-Cookies-1.0\n");
  foreach my $line_($fin->getlines)
    {
      chomp $line_;
      next if($line_ =~ /^$/ || $line_ =~ /^\#/);
      my ($domain,$flag,$path,$secure,$expires,$name,$value) = split(/\s+/,$line_);
      my $t = gmtime($expires);
      $expires = sprintf('%s, %d-%s-%04d %02d:%02d:%02d GMT',
                         $t->day,
                         $t->mday,
                         $t->month,
                         $t->year,
                         $t->hour,
                         $t->min,
                         $t->sec);
      $secure = $secure eq 'TRUE' ? 'secure' : '';

      $fout->print(qq(Set-Cookie3: $name=$value; path="$path"; domain=$domain; path_spec; expires="$expires"; $secure\n));
    }

  $fin->close;
  $fout->close;
}
__END__

PHP配列の悪夢、再び

おれって、相当アホ!!! ってな備忘録。

こんなコードを書いた。(実際に書いたコードはもっと複雑ですが・・・)

&lt;?php
/******************************

 配列の中身を出力する。だけ。

******************************/
function array_wrong($files)
{
  $num = count($files);
  for($i=0;$i&lt;$num;$i++)
    {
      printf(&quot;%d = %sn&quot;,$i,@$files[$i]);
    }
}

もうこの時点で、「お前の書くコードなんて一切使わない」宣言されるだろう。
はい、そうですね。僕もそう思います。(T-T)

こんな基本的な間違いを平気で犯す自分に自己嫌悪して数時間立ち直れませんでした・・・。

たぶん、次のような引数を渡すと期待するように動作するでしょう。

&lt;?php
$files = array('abc.txt',
               'xyz.pdf',
               'def.jpg');

//たぶん上手くいく。オール、オッケー
array_wrong($files);

しかし・・・次のようにしたら・・・

&lt;?php
$files = array('abc.txt',
               'xyz.pdf',
               'def.jpg');

unset($files[1]);

$files[10] = 'zzzz.gif';
$files[7] = 'bbbbb.ai';

array_wrong($files);

僕が期待した出力には当然なりません。とほほ。

こういうバカ・コードが原因だと気づくのに、半日要してしまったよ・・・・。
PHPの配列とcount関数の挙動が全然理解してない証拠っすね。。。恥ずかしい。。。

原因が分かれば・・・foreachもしくは、count関数を使わず・・・ループすればいい。

&lt;?php
function array_right($files)
{
  ksort($files);
  foreach($files as $i =&gt; $file)
    {
      printf(&quot;%d = %sn&quot;,$i,@$files[$i]);
    }
}

//もしくは・・・count()関数でループ回数を決定するのではなく配列キーの最大値を利用する。
function array_right($files)
{
  ksort($files);
  $num = max(array_keys($files));
  for($i=0;$i&lt;$num;$i++)
    {
      if(isset($files[$i]))
        printf(&quot;%d = %sn&quot;,$i,@$files[$i]);
    }
}

はぁ・・・。いつになったら使える人間になれるんだろう・・・。

MySQL + UPDATE + PDOStatement::rowCount の罠

MySQLのセットアップはテーブル作成とかも含めて、非常に面倒なので、作成途中(開発中)はSQLiteで作って動作確認して、作成大詰めの段階でMySQLに切り替え完成・・・という工程はわりかし一般的?だと思う。ファイル一個でバックアップ・リストアも簡単で開発効率も上がります、僕は。

で、SQLite + PDO で何の問題もなくある程度コーディングが終わり、MySQLに移行して検証していると、おかしな挙動の解決に半日かかってしまった備忘録のエントリです。

データを空更新、特定の行を取得して、編集画面表示、その後、内容を変えずに同じデータで更新すると、update文は成功するのに、作用した行数が0を返す・・・。だいたい下のような感じ。

&lt;?php
/*************************************************************

  あらかじめ下記mysqlクライアントで実行

  &gt;&gt; CREATE TABLE test_table(id INTEGER,data CHAR(255));
  &gt;&gt; INSERT INTO test_table values(1,'Kenji Nakagawa');

**************************************************************/
$pdo = new PDO('sqlite:log.sqlite');

if(modify_name(1,'Kenji Nakagawa'))
{
  header('location: list.php');
}

function modify_name($id,$name)
{
  global $pdo;

  $rv = false;
  $sql = 'update test_table set name = ? where id = ?';

  if(false !== ($stmt = $pdo-&gt;prepare($sql)))
    {
      if(false !== ($result = $stmt-&gt;execute(array($name,$id))))
        {
          $rv = $stmt-&gt;rowCount();
        }
    }

  return $rv;
}

この一連のコーディングでの最大の失敗は、rowCount()メソッドが返す行数で、update文の成功・失敗を判断したところ。分かってしまえば、何でもないことだけど、はまってしまった。

分かったことは、

MySQL + UPDATE文の実行では、PDOStatement::rowCount() は「実際に変更した(あった?)行数」(←ここ重要)を返す

・・・・ということ。

元々のレコードと同じデータをupdateすると、update文の実行はfalseは返さない(成功する)が、rowCount()は1ではなく、0を返す・・・。PHPサイトのドキュメントを検索したらちゃんと書いてありました・・・・とほほ(T-T)

UPDATE を使用する場合、MySQL では新旧の値が同じときには更新処理を行いません。 このことから、必ずしも mysql_affected_rows() の返す値が マッチする行の数と一致するとは限りません。返す値は実際に更新処理が行われた 行の数です。

そんな・・・・僕が勉強したときにちょろっと読んだMySQL入門書には書いてない!(笑)

ってわけで、解説書なんかで勉強するときは、いかに良い本に巡り会うことの重要性を改めて痛感しました。。。

コマンドバーにボタンを追加

Windows7のコマンドバーネタ(備忘録)です(^^ゞ

会社のPCがやっとXPからWindows7になりました。ええ、やっとです。XPのサポートが完全に終了したので、たぶん、仕方なくです(笑) どうせなら、Windows8.1にして欲しいものです。

さて、Windows8以降はエクスプローラシェルにもリボンUIが採用され、Vista/7のフォルダウィンドウの上部にくっついていたコマンドバーが消えてしまいました。Windows8以降のフォルダウィンドウはなかなか使い勝手よく、色んな設定をわざわざコントロールパネルを辿らずとも変更できるのですが・・・Windows7ではその辺使い勝手が悪いです。まぁ、見た目は圧倒的にWindows7の方が好きなのですが・・・。

あ、そうそう、フォルダウィンドウ内で、CTRLキーを押しながらマウスホイールを回すとアイコンサイズが変わるって知ってました? おいら、今日初めて知った・・・。ちょー便利。

と、そんなことは、どーでもよく。。。

隠し属性のファイルの表示・非表示を簡単に切り替えるようにしたくて、コマンドバーにスクリプトを登録しました。
toggle

コマンドバーへのボタン追加は、3年ぐらい前にコマンドバーの記事を書いていたので、隠し属性のファイルの表示・非表示を行うスクリプトを書いて、レジストリに登録するだけ。

/*
  隠しファイルの表示・非表示トグル スクリプト
  ちょいと変更 at 2014/4/28
*/
(function()
{
  var wShell = WScript.CreateObject("WScript.Shell")

  try
    {
      var Key="HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced\\Hidden";
      var Value = parseInt(wShell.RegRead(Key));

      // レジストリ・キー(Key)の値(Value)が、1の時は非表示、2の時は表示なので入れ替え。
      // ・・・逆だったっけ?ま、いいや。
      wShell.RegWrite(Key,(Value & 1) + 1,"REG_DWORD");

      //現在開いているフォルダウィンドウのコレクションを得る。
      var sWindows = WScript.CreateObject("Shell.Application").Windows();

      /*
      現在開いているフォルダの数だけ、ループを回して更新する。
      Itemメソッドで得られるオブジェクトはInternetExplorerオブジェクトなので、
      Refresh()メソッドで表示を更新させる(F5キーを押すのと同じ)。
      */ 
      var i = sWindows.Count - 1;
      while(i >= 0)
        sWindows.Item(i--).Refresh();
    }
  catch(e)
    {
      WScript.Echo(e);
    }
})(); 

レジストリへの登録は「エクスプローラー(Windows7)のコマンドバーにボタンを追加する」をご参考に。