ファイル名に使えない文字

C# 備忘ログ。

テキストボックスに入力したワードをそのまま ディレクトリの作成やファイル作成とかのメソッドに渡すと、例外が飛んでくることがある。ほとんどの場合、?,:,/ などの文字をエスケープしてない!のが原因。めんどくさいのう! と思いながら、それらの文字をフィルタリングするメソッドを書いてたんですが・・・結構気の利くメソッドを見つけた。

Path.GetInvalidFileNameChars()とPath.GetInvalidPathChars()メソッド。

単純にコールすると、ファイルシステム上で使えない文字をchar[]配列に格納して返してくれる! ・・・が、ドキュメントを見ると、頼りないお言葉が・・・

このメソッドから返される配列は、ファイルおよびディレクトリの名前に無効な文字の完全なセットが含まれているとは限りません。

おいおい!仕事放棄するなよぅっっっッ!
ってなわけで、結局、”CON”やら”PRN”とかは自前でフィルタリングしないといけない・・・と。

//using System;
//using System.Text.RegularExpressions;
//using System.IO;
//using System.Linq;
//...
/// <summary>
/// ファイルシステムで使用できない文字をエスケープする。
/// CON,PRNなどの文字列の場合は、before,afterを前後に付けて返す。
/// 制御文字などの文字は、16進文字コードに変換する。
/// </summary>
/// <param name="str">検査する文字</param>
/// <returns>エスケープされた文字列</returns>
public static string EscapeInvalidPathChars(string str,string before="x",string after = "x")
  {
    string rv = Regex.Replace(str,
                              string.Format(@"([{0}])",Regex.Escape(new String(Path.GetInvalidFileNameChars()))),
                              m => Uri.HexEscape(m.Value.First()));

    if(Regex.Match(rv,@"^(CON|PRN|AUX|NULL|COM\d|LPT\d)$",RegexOptions.IgnoreCase).Success)
      rv = string.Format("{1}{0}{2}",rv,before,after);

    return rv;
  }

そういえば・・・CONCON問題とかあったのう。。。ブルースクリーンで一喜一憂してた時代でほのぼのしてたなー

Tweet image download agent.

【追記】2017/09/19 現在「いいね」画像のTL取得に失敗しているみたい。原因究明中(デバッグ中)使用しているアカウントがロックされたので、ちょっとお手上げ。ログインするととたんにロックされるのでビルド済みのファイルは公開中止。アカウントを永久凍結されると困るので、これにて終了(^^;; チキンハートw

ちょうどGUIをWinFomsからWPFに移行中でそろそろ完了しつつあったのでちょい残念。でもまぁ、WPFのバインディングやら非同期やら勉強できたので、いい教材だったかなと。


【追記】 2017/08/27 二段階認証に対応。画面をちょっと変更。ファイル拡張子の判別がちゃんと機能してなかった・・・他色々修正しまくり。


【追記】 2017/08/20 set-cookieヘッダの取得方法を若干変更。 現在2段階認証には対応しておりません。今度対応させます。


【追記】 2017/08/19 ログイン後の画像取得が失敗する不具合を修正。


【追記】2017/08/18 ↓の原因をほぼ特定。暫定的に修正したものを更新予定。


【追記】2017/08/17 ログインのプロセスが変更になった関係で、現在、ログインが成功しても画像の取得に失敗します。ログイン自体が失敗している模様です。また調査次第修正できるようであれば、修正します m(__)m


【追記】2017/07/09 取得するファイルタイプ(拡張子)、ファイルサイズの上下限を設定できるようにした。コードのリファクタリング実施。


【追記】2016/09/06 テキストボックスのところをコンボボックスに張り替え。新しいものから10個まで入力履歴を記憶できるように修正。他、諸々修正。


【追記】2016/04/27 GW前に、ちまちま修正他、検索ワードに対応。これで画像収集が捗るわい(^^;


ちょっと前に書いたツイッターの画像を一括ダウンロードするやつ、のGUI版を 非同期タスクのお勉強も兼ねて日曜プログラミング(^^;

コンソールでコマンドを打つ方が慣れているのでわざわざGUIみたいなクソめんどくさいもん書く必要もなかったんですが・・・。

とくに最近、C#でアシンクやらアウェイトやらウェイタブルとか今まで見たこともないキーワードがガンガン出てくるようなコードを見る機会が多くなったので、勉強がてら練習で作ってみました。・・・・というレベルです(m_m)
とはいいつつ、awaitとかasyncとかまだよく分からないので、Task.Run()をふんだんにちりばめてみました(^_^;;; ま、自分で使うツールだから動けばいいや。
検索とかはまだ実装していません、というか、自分には必要ないので、やるつもり無し。時間があればまたブラウザで解析して・・・時間があれば・・・。
そもそもこの手のツールは他にもっと便利なソフトが窓の杜とかVectorとか探せばあると思うので。。。
動画とかも落とせればいいかなー、とか思って昼休みにchromeの開発者ツールでちょっと調べてみたけど、結構めんどくさそうですね・・・。

twitterimageagent-1 twitterAgent2

Twitter APIじゃないので、ブラウザでの表示方法が変更されると誤動作するでしょう。

ビルド済みのファイルとあまり綺麗では無いソースファイルは、OSDN.JPの作業部屋 ↓↓↓から。(使い方よく分かってません・・・)
https://osdn.jp/users/earlgreyx/pf/TwitterImageDownloadAgent/wiki/FrontPage

twitterの「いいね」画像をダウンロード C#版

2017年7月15日
こっちに移動 → https://ptsv.jp/2016/04/20/tweet-image-download-agent/

2016年4月29日 Windows フォームを利用したGUI版追加
検索ワード対応とスレッドプールにダウンロード処理を投げるように、ソース本体も改編した版

2015年11月27日・29日 コード修正・他
コード修正 画像ファイルの正規表現の間違い修正及びcommandを追加。
コード修正 URL を favorites => likes に変更。文章をちょろっと変更。

2015年11月23日 コード修正
2回目以降のtimeline URLの取得方法が間違ってました。ごめんなさい。(m_m)


perl で組んだものを C# に書き直したコードです。perlで組んだもので僕がやりたかったことは全部できた訳ですが・・・ま、今仕事暇なんで(^^;

エラーチェックとか全然してません。このコードは参考テスト程度のものなので、あしからず。
間違ったtargetとか入力してサーバーが404エラー返すと例外吐いて死にます(^_^;; エラー処理をコードに盛り込むとコードの要点が見えずらくなるので・・・いないとは思いますが、コードを使う時は、エラー処理(例外捕捉とか)を入れてね。

また、Twitter API使った方法ではないので、twitter側がブラウザ表示の方法を変更してしまうと、たぶんエラーで落ちると思う。まぁ、いつまでこの方法で使えんのかな、という感じです。

コンソールプログラムで良ければ、コンパイルしたものを置いときます。[ getTweetImage.exe ]
※使用方法は下記コードのコメントを参照してください。
(EXEファイルをダウンロードすると、たぶんセキュリティーチェックで引っかかると思いますが、信用できない方は、下記コードをご自身でコンパイルしてください。 )

たぶん Windows以外のOSでも、monoが入っていたら 上記 exe ファイルがそのまま動くんじゃないかなー。
[追記]
CentOS6 + mono で実行してみましたが、https通信でセキュリティ?かなんかの例外が発生してしまいました。
デフォルトでインストールしたまんまのmonoの場合はルート証明書をインポートする等しないといけないみたいですね。mozroots.exeをインストールして解決しました。下記ページを参考にさせていただきましたm(__)m。
~Monoで、Secure Socket Layer (SSL) / Transport Layer Security (TLS)通信を行うには~
http://qiita.com/takanemu/items/129acc13d8b7ce088b92

[追記ここまで]

これにて終了。

/*******************************************************************************

  saving image file on twitter.(getTweetImage.cs)

   build: Visual Studio 2015 Community.( also Express Edition) or Windows SDK
     IDE使うほどでもないので、コマンドラインツールでcsc.exeでバイトコンパイルしてください。
     >> csc.exe getTweetImage.cs

  howto: Console command
     >> getTweetImage.exe command target [username password]
     
     command  : ターゲットが"いいね"した画像 => 'favo' 
                ターゲットがアップした画像   => 'profile'
                ターゲットのTLの画像        => 'timeline'
     target   : ターゲットとなる取得したい画像のアカウント名 
     username : 自分のログインアカウント
     password : 自分のパスワード

     usernameとpasswordは、「いいね」画像の取得及び、
     ターゲットとなるアカウントが非公開アカの場合に必要かと思います。

*******************************************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Net;
using System.Linq;

namespace ptsv.jp
{
  public class ConsoleApplication1
    {
      private static Dictionary<string,string> TIMELINEURL = new Dictionary<string,string>
        {
          { "profile","https://twitter.com/i/profiles/show/{0}/media_timeline"},
          { "favo","https://twitter.com/{0}/likes/timeline"},
          { "timeline","https://twitter.com/i/profiles/show/{0}/timeline"}
        };

      /// <summary>
      /// スタートアップ
      /// </summary>
      /// <param name="args">コマンドライン引数</param>
      public static void Main(string [] args)
        {
          if(args.Length < 2)
            {
              Console.WriteLine("USAGE: getTweetImage.exe command target [username password]");
              Console.WriteLine("USAGE: command is 'favo' or 'profile'");
              Console.WriteLine("require username and password\n when command is 'favo',or target is private account.");
              return;
            }
          string command  = args[0];
          string target = args[1];
          string username_or_email = null;
          string password = null;

          if(args.Length == 4)
            {
               username_or_email = args[2];
               password = args[3];
            }

          if(command == "favo" && (string.IsNullOrEmpty(username_or_email) || string.IsNullOrEmpty(password)))
            {
               Console.WriteLine("when command is 'favo', username and password is required.");
               Console.WriteLine("USAGE: getTweetImage.exe command target [username password]");
               return;
            }

          string timeline_url = TIMELINEURL.ContainsKey(command) ? string.Format(TIMELINEURL[command],target) : null;

          var twitter = new TwitterDownload((type,v) =>
                                            {
                                              switch(type)
                                                {
                                                case "get-timeline":
                                                  Console.WriteLine("---- getting and parsing\n{0} ...\n----",v);
                                                  break;
                                                case "getting-image":
                                                  Console.WriteLine("fetching: {0}",v);
                                                  break;
                                                case "saved-image":
                                                  Console.WriteLine("saved {0}",v);
                                                  break;
                                                case "file-exists":
                                                  Console.WriteLine("{0} is already exists.",v);
                                                  break;
                                                case "fail-login":
                                                  Console.WriteLine("Auth code not found.","");
                                                  break;
                                                case "fail-auth":
                                                  Console.WriteLine("username or password were refused.","");
                                                  break;
                                                case "try-login":
                                                  Console.WriteLine("Trying to login by '{0}' for twitter.com", v);
                                                  break;
                                                }
                                            });

          if(!string.IsNullOrEmpty(timeline_url))
            {
              twitter.Agent(timeline_url,username_or_email,password);
            }
          else
            {
              Console.WriteLine("[INVALID COMMAND] command should be 'favo' or 'profile'");
            }
        }
    }

  /// <summary>
  /// 画像ダウンロード本体クラス
  /// </summary>
  public class TwitterDownload
    {
      /*------------------------------------------------------------------------------

        Const

      ------------------------------------------------------------------------------*/
      public const string URL_LOGIN    = "https://twitter.com/login";
      public const string URL_SESSIONS = "https://twitter.com/sessions";
      public const string USER_AGENT   = "Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.0; Trident/5.0)"; //適当
      public const string REFERER      = "https://twitter.com/";
      private const string RE_MEDIA_URL = @"https://pbs\.twimg\.com/media/([\w\-]+\.\w{3,4})(:large)?";
      private const string RE_TWEET_ID  = @"data-tweet-id=\\""(\d+)\\""";
      private const string RE_AUTH_CODE = @"<input type=""hidden"" value=""([\d\w]+?)"" name=""authenticity_token""(?:\s*/)?>";

      /*------------------------------------------------------------------------------

        Instance

      ------------------------------------------------------------------------------*/
      private string OutputDirectory = Path.Combine(".", "tmp");
      private TwitterWebClient webClient;
      private Action<string,string> Callback;

      public TwitterDownload(Action<string,string> callback, string cookieFile)
        {
         this.Callback = callback;
         this.webClient =  new TwitterWebClient(cookieFile);
        }

      public TwitterDownload(Action<string,string> callback)
        {
          this.Callback = callback;
          this.webClient = new TwitterWebClient();
         }

      public string directory
        {
          set
            {
              this.OutputDirectory = value;
            }
          get
            {
              return this.OutputDirectory;
            }
        }

      public void Agent(string timelineURL,string username,string password)
        {
          if(String.IsNullOrEmpty(timelineURL))
            return;

          if(!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
            {
                if (!GetLogin(username, password))
                 {
                    Callback("fail-auth", "");
                    return;
                 }
            }

          if (!Directory.Exists(OutputDirectory))
            Directory.CreateDirectory(OutputDirectory);

          string param = "?include_available_features=1&include_entities=1";
          string url;
          do
            {
              url = timelineURL + param;
              Callback("get-timeline",url);

              string result = webClient.DownloadString(url);
              param = GetImage(result);

            } while(!string.IsNullOrEmpty(param));
        }

      private bool GetLogin(string username,string password)
        {
          Callback("try-login", username);
          webClient.Headers[HttpRequestHeader.UserAgent] = USER_AGENT;
          webClient.Headers[HttpRequestHeader.Referer] = REFERER;

          string loginResult = webClient.DownloadString(URL_LOGIN);
          Match authMatch = Regex.Match(loginResult,RE_AUTH_CODE);
          if(!authMatch.Success)
            return false;

          var param = new NameValueCollection();
          param.Add("session[username_or_email]",username);
          param.Add("session[password]",password);
          param.Add("authenticity_token",authMatch.Groups[1].Value);
          param.Add("remember_me","1");
          param.Add("redirect_after_login","/");

          return Encoding.UTF8.GetString(webClient.UploadValues(URL_SESSIONS,param)).IndexOf("error") < 0;
        }

      private string GetImage(string json)
        {
          json = json.Replace(@"\/", "/");

          foreach(Match match in Regex.Matches(json,RE_MEDIA_URL))
            {
              string url = match.Value;
              string basename = match.Groups[1].Value;
              string filename = Path.Combine(OutputDirectory,basename);

              url = string.IsNullOrEmpty(match.Groups[2].Value) ? url + ":orig" : url.Replace(":large",":orig");

              if(File.Exists(filename))
                {
                  Callback("file-exists",filename);
                }
              else
                {
                  Callback("getting-image",url);
                  webClient.DownloadFile(url,filename);
                  Callback("saved-image",filename);
                }
            }

          var ids = new List<string>();
          foreach(Match match in Regex.Matches(json,RE_TWEET_ID))
            {
              ids.Add(match.Groups[1].Value);
            }

          return ids.Count > 0 ? string.Format("?max_position={0}&include_available_features=1&include_entities=1",ids.Last()) : null;
        }
    }

  /// <summary>
  /// クッキーの読書に対応するためだけのWebClientクラス。
  /// </summary>
  internal class TwitterWebClient : WebClient
    {
      private CookieContainer cookieContainer = new CookieContainer();
      private bool isAutoRedirectEnabled = false;

      public TwitterWebClient() : this("") {}

      /// <summary>
      /// もしクッキーファイルが存在したらクッキーファイルを読み込んでCookieContainerに追加する。
      /// </summary>
      /// <param name="cookieFile">ブラウザからエクスポートしたクッキーファイル</param>
      public TwitterWebClient(string cookieFile)
        {
          if(File.Exists(cookieFile))
            {
              using(var sr = new StreamReader(cookieFile))
              {
                string line;
                var reCRLF = new Regex(@"[\r\n]$");
                var reEmpty = new Regex("^$");
                var reComment = new Regex("^#");
                var reSpace = new Regex(@"\s+");

                while ((line = sr.ReadLine()) != null) 
                  {
                    line = reCRLF.Replace(line,"");
                    if(reEmpty.Match(line).Success || reComment.Match(line).Success)
                      continue;
                    string[] ar =  reSpace.Split(line);

                    cookieContainer.Add(new Cookie(ar[5],ar[6],ar[2],ar[0]));
                  }
              }
            }
        }

      /// <summary>
      /// WebClient.GetWebRequestメソッドをオーバーライド
      /// </summary>
      protected override WebRequest GetWebRequest(Uri address)
        {
          WebRequest request = base.GetWebRequest(address);

          if (request is HttpWebRequest)
            {
              var httpWebRequest = request as HttpWebRequest;
              if(httpWebRequest != null)
                {
                  httpWebRequest.CookieContainer = this.cookieContainer;
                  httpWebRequest.AllowAutoRedirect = this.isAutoRedirectEnabled;
                }
            }

          return request;
        }
      /// <summary>
      /// 自動リダイレクトするか?
      /// </summary>
      public bool AutoRedirect
         {
            set { isAutoRedirectEnabled = value; }
            get { return isAutoRedirectEnabled; }
         }
    }

}

mysqldumpの出力をメールで送りつける

備忘録です。

MySQLデービーを使うサイトが増えて、そろそろデービーのバックアップを自動化しようと目論む。

ま、データベースと言っても小規模の小さいデータばっかりなので、どんなに多くても10MBを超すデータベースはないので、mysqldumpをcronで月一回程度実行してその出力を暗号化ZIPに圧縮して1つのメールアドレスに飛ばせば、複数サイトのDBバックアップが出来る!

ま、perlで書けばいいや、と思って書き出したんだけど、Archive::Zipでは暗号化ZIPを作成できないとのこと。使っているサーバーではPHPのバージョンが古くて、これも使えず・・・。zipコマンドを使えば・・・と思うんですが出来ればperl内で完結させたい。

しょうがないのでmysqldumpの出力をそのままCrypt::CBCとかで暗号化してしまえ、ということで。暗号化して送信するスクリプト(cron用)と、複合化するスクリプトを作成。

mysqldump-mail.pl を cronに月イチ夜中の3時ぐらいにサイト毎に5分ぐらいづつずらして登録。後は待っとけばダンプファイルがバックアップされる。

まぁ、でも、小さいデータ量だから可能なわけで・・・、当たり前ですが、数百メガバイトからギガバイト級のデータベースとかだと、絶対やっちゃだめっすけどね(^^;;;

※Crypt::CBCへのコンストラクタ引数は、とりあえず適当に指定しているので、これをそのままコピペして実運用はできません。いえ、しちゃだめです。また mysqldumpは環境毎に異なると思うのでコピペして使える代物ではありません。あしからず、ご了承くださいませ。ないとは思いますが、一応注意書きです。

○mysqldumpを暗号化して送信

#!/usr/bin/perl
##############################################################################
=comment

  'mysqldump' を実行した結果を暗号化して指定したメールアドレスへ送信します。

  ex.
    ./mysqldump-mail.pl --sendto=hoge@hoge.com --user=DBユーザー \
                     --password=DBパスワード --database=データベース名

=cut
##############################################################################
use strict;
use warnings;
use Getopt::Long qw/:config no_ignore_case/;
use MIME::Base64 qw/encode_base64/;
use Crypt::CBC;
use IO::File;
use IO::String;

my $SENDMAIL = '/usr/lib/sendmail';

my %CONFIG = ('user'     => '',
              'password' => '',
              'sendto'   => 'xxx@yyy.com',
              'from'     => 'xxx@zzz.com',
              'database' => '',
              'subject'  => 'MySQL dump at %04d/%02d/%02d',
              'filename' => '',
              'key'      => 'private key',
              'cipher'   => 'DES_EDE3');

#Startup code
&{sub
{
  #arguments
  my @argv = @_;
  my ($y,$m,$d) = (localtime)[5,4,3];
  ($y,$m) = ($y-100,$m+1);

  my %config = %CONFIG;

  my $result = Getopt::Long::GetOptionsFromArray(\@argv,
                                                 'user=s'     => \$config{user},
                                                 'password=s' => \$config{password},
                                                 'from=s'     => \$config{from},
                                                 'sendto=s'   => \$config{sendto},
                                                 'database=s' => \$config{database},
                                                 'key=s'      => \$config{key},
                                                 'cipher=s'   => \$config{cipher});

  $config{subject} = sprintf($config{subject},$y,$m,$d);

  # ファイル名を定義(※ファイル名で日付がわかるようにしておきます)
  my $basename = "mysqldump-$y$m$d";

  $config{filename} = "$basename.sql.enc";

  die "no database...\n" unless $config{database};
  die "no user...\n" unless $config{user};

  my $commandline = sprintf('mysqldump --opt --skip-extended-insert --user=%s --password=%s %s',
                            $config{user},
                            $config{password},
                            $config{database});

  # 暗号化する
  my $sqldump = IO::File->new("$commandline |") || die "can not open\n";

  return &sendmail($sqldump,%config);

}}(@ARGV);

sub encode_stream
{
  my ($in,$out,$key,$cipher) = @_;

  my $cbc = Crypt::CBC->new(-key    => $key,
                            -cipher => $cipher);

  #読込バッファ,暗号化バッファ
  my ($buf,$enc) = ('','');

  #バッファサイズ
  my $buf_size = 57*71;

  $cbc->start('encrypting');
  while(0 < $in->read($buf,$buf_size))
    {
      # 詠み込まれたバッファを暗号化して暗号化バッファに追加
      $enc .= $cbc->crypt($buf);

      while(length $enc > $buf_size)
        {
          $out->print(encode_base64(substr($enc,0,$buf_size,'')));
        }
      $buf = '';
    }
  $out->print(encode_base64($enc)) if($enc);

  $cbc->finish();

  1;
}

sub sendmail
{
  my $ref = shift;
  my %config = @_;

  my $bound = '_xkdjfsdkjfsafdskfjsa_';
  my $boundary = "--$bound";
  my $boundary_end = "--$bound--";

  my $sm = IO::File->new("| $SENDMAIL -i -t");

  $sm->print(<<__MAIL__);
From: $config{from}
To: $config{sendto}
Subject: $config{subject}
Content-Type: Multipart/Mixed; boundary="$bound"
MIME-Version: 1.0

$boundary
Content-Transfer-Encoding: 8bit
Content-type: text/plain; charset=UTF-8

MySQLデータベースダンプファイルです。

$boundary
Content-type: application/octed-stream
Content-Transfer-Encoding:Base64
Content-disposition: attachment; filename=$config{filename}

__MAIL__

  &encode_stream($ref,$sm,$config{key},$config{cipher});

  $sm->print("\n$boundary_end\n");
  $sm->close;

  return 0;
}

 
 

○ファイルを復号化する

#!/usr/bin/perl
##############################################################################
=comment

  Crypt::CBCで暗号化したファイルを復号化します。

  ex.
  ./cbc-decrypt.pl --key=秘密キー --in=入力ファイル

  その他)
  --out=出力ファイル(標準出力) --cipher=アルゴリズム(DES_EDE3)

=cut
##############################################################################
use strict;
use warnings;
use Getopt::Long qw/:config no_ignore_case/;
use Crypt::CBC;
use IO::File;

my %CONFIG = (key    => '',
              in     => '',
              out    => '-',
              cipher => 'DES_EDE3');

#Startup code
&{sub
{
  #arguments
  my @argv = @_;
  my %config = %CONFIG;

  my $result = Getopt::Long::GetOptionsFromArray(\@argv,
                                                 'key=s'    => \$config{key},
                                                 'cipher=s' => \$config{cipher},
                                                 'in=s'     => \$config{in},
                                                 'out=s'    => \$config{out});


  my $encfile = IO::File->new($config{in}) or die "can not open file\n";
  my $fout = $config{out} eq '-' ? IO::File->new_from_fd(fileno STDOUT,'>') : IO::File->new(">$config{out}") or die "can not open output file\n";

  return &decode_stream($encfile,$fout,$config{key},$config{cipher});

}}(@ARGV);


sub decode_stream
{
  my ($in,$out,$key,$cipher) = @_;

  my $cbc = Crypt::CBC->new(-key => $key,-cipher => $cipher);

  my ($buf,$len) = ('',0);
  my $read_size = 4096;

  $in->binmode;
  $out->binmode;

  $cbc->start('decrypting');
  while(0 < ($len = $in->read($buf,$read_size)))
    {
      $out->print($cbc->crypt($buf));
      $buf = '';
    }
  $cbc->finish();

  1;
}

twitterの「いいね」画像をダウンロード 修正版

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


2015年11月27日・29日 コード修正
コード修正 画像ファイルの正規表現の間違い修正・ちょこっと追加。
コード修正 URL を favorites => likes に変更

2015年11月23日 コード修正
2回目以降のtimelineのURLの決定方法が間違っておりましたので修正しました。ごめんなさい(m_m)

2015年11月17日 コード修正
ログインを行うコードを追加。これにより、ブラウザからクッキーファイルをエクスポートする手間を省くようにした。ログインコードはこれでいいのかどうかわからん。twitterってrails使ってんだっっけ? よくわからんが、適当。
本来はTwitter APIを使わないといけないと思うけど・・・API経由は正直メンドクサイ。勉強する気なし。(m_m)

C#で書き直したプロトタイプをさっき書いたので、これもデバッグが終わり次第また書こっと。
『perlなんかインストールしてねーよ、ボケ!』って方は、C#で書き直した方に ビルドしたやつを置いてます。


今年の2月ぐらいに書いたtwitter画像ダウンロードスクリプトが動かなくなったので修正・修正(^_^;;

2回目以降のタイムライン取得のURLが変更になったみたい?。昼休みにブラウザでアクセスしてデベロッパーツールでちょこっと解析したんですが・・・どのパラメーターで読めばいいのか、いまいち分かんないです。適当です。あくまで、自分用の備忘録なんで、すみません。取れればいいんです(m_m)

twitterにログインするところまで書きなおそうと一瞬思いましたが、また今度にします(・・;
ブラウザでログインしてクッキーエクスポート、twitter.comドメインだけ抜き出して、カレントディレクトリへcookie.txtというファイル名で保存。テキトーですまない。

使い方は下記参考。ログインしたcookie.txtが必要なのは、いいね(旧称:お気に入り)の取得のみ。他人のIDの「いいね」も同様にログインが必要です。この辺よくわからん。自分もしくは他人のIDのタイムラインにログインクッキーは必要ない。です。

#!/usr/bin/perl
=pod

=head1 ツイッターでのタイムラインから、画像のみダウンロードするスクリプト

使い方 targetに画像を取得したいアカウント名を指定する。

(いいね 画像の場合は type に favo をセット(デフォルト) 
(※ いいね 取得は自分以外のアカウントでも必ずログインが必要みたいです。)

> twitter.pl --username=xxxxx --password=yyyyy --type=favo --target=zzzz


(単に任意のアカウントのタイムラインの画像が欲しい場合は type に profileをセット)
(TLに流れているリツイートも含めた画像を取得したい場合は、timelineをセット)
(※通常公開されているアカウントのタイムラインではusename,passwordは必要ありません。
(※非公開アカウントではフォローを許可されたアカウントのusername/passwordが必要です。)

> twitter.pl --type=profile --target=公開アカウント名


ディレクトリ<tmp>に画像が吐き出されます。

=cut

use strict;
use warnings;
use LWP::UserAgent;
use Getopt::Long qw/:config no_ignore_case/;

my %CONFIG = (username => '', password => '', dir => './tmp', type => 'favo', target => '');
my %TIMELINE = (favo    => 'https://twitter.com/%s/likes/timeline',
                profile => 'https://twitter.com/i/profiles/show/%s/media_timeline',
                timeline => 'https://twitter.com/i/profiles/show/%s/timeline');

#エージェント
my $UserAgent = LWP::UserAgent->new(cookie_jar => {});

&{sub
{
  my @argv = @_;
  my $result = Getopt::Long::GetOptionsFromArray(\@argv,
                                                 'username=s' => \$CONFIG{username},
                                                 'password=s' => \$CONFIG{password},
                                                 'type=s'     => \$CONFIG{type},
                                                 'dir=s'      => \$CONFIG{dir},
                                                 'target=s'   => \$CONFIG{target});

  exists $TIMELINE{$CONFIG{type}} or die "invalid type....\n";
  $CONFIG{dir} || die "specified output directory...\n";
  $CONFIG{target} || die "specified target account name...\n";

  mkdir $CONFIG{dir} unless(-e $CONFIG{dir});

  &use_lwp_agent;

}}(@ARGV);

sub use_lwp_agent
{
  my ($param,$result) = ('?include_available_features=1&include_entities=1','');

  if($CONFIG{username} && $CONFIG{password})
    {
      my $response = $UserAgent->get('https://twitter.com/login');
      $result = $response->decoded_content;

      $result =~ /<input type="hidden" value="([\d\w]+?)" name="authenticity_token"(?:\s*\/)?>/ or die "can not detect authenticity_token...\n";

      my $auth = $1;
      $response = $UserAgent->post('https://twitter.com/sessions',
                                   {'session[username_or_email]' => $CONFIG{username},
                                    'session[password]'          => $CONFIG{password},
                                    'authenticity_token'         => $auth,
                                    'remember_me'                => '1',
                                    'redirect_after_login'       => '/' }) or die "can not access login page... \n";

      $result = $response->decoded_content;
      die "failed to login...\n" if($result =~ /error/);
    }

  my $url = sprintf($TIMELINE{$CONFIG{type}},$CONFIG{target});
  do
    {
      my $timeline = $url.$param;

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

      $param = &get_images($result,\&lwp_agent);

    } while( $param );

  print "done!\n";
}

#タイムラインのJSONデータから画像を取得して、次のタイムラインのパラメータを返す。
sub get_images
{
  my ($json,$agent) = @_;

  $json =~ s/\\\//\//g;
  foreach my $url_($json =~ m!https://pbs\.twimg\.com/media/[\w\-]+\.\w{3,4}(?::large)?!g)
    {
      if($url_ =~ m/([\w\-]+\.\w{3,4})(:large)?$/)
        {
          my $basename = $1;
          my $filename = "$CONFIG{dir}/$basename";
          if($2)
            {
              $url_ =~ s/:large/:orig/;
            }
          else
            {
              $url_ .= ':orig';
            }

          unless(-e $filename)
            {
              print "fetching: $url_\n";
              &$agent($url_,$filename);
              print "saved $basename\n";
            }
        }
    }

  my @ids = $json =~ m/data-tweet-id=\\\"([0-9]+)/g;
  my $max_id = @ids > 0 ? pop @ids : '';

  return $max_id ? "?max_position=${max_id}&include_available_features=1&include_entities=1" : undef;
}

#URLを取得して返す。
sub lwp_agent
{
  my ($url,$ofile) = @_;
  my %options = ();

  if($ofile ne '-')
    {
      if($ofile eq ':src')
        {
          if($url =~ /([\w\-\.%]+?\.\w)$/)
            {
              $ofile = "$CONFIG{dir}/$1";
            }
          else
            {
              goto cleanup;
            }
        }

      return $UserAgent->get($url,':content_file' => $ofile);
    }

cleanup:
  if(my $response = $UserAgent->get($url))
    {
      return $response->decoded_content;
    }

  0;
}

__END__