かみやんの技術者ブログ

主にプログラムの話です

IbisAppSales

前のエントリ(これ)で、書いたiTunes Connectに接続してスクレイピングして、iPhoneのダウンロード数をダウンロードするプログラムが書けました。
GPLで公開するので、使ってください。

インストール

  • Apache HttpClientのダウンロード
    • このページから、HttpClient 4.0 (GA)Binary with dependenciesをダウンロード
  • Eclipseで下記IbisAppSales.javaをソースとしたJavaアプリケーションプロジェクトを作成
  • プロジェクトの設定で、コンパイラを1.6に設定
  • プロジェクトの設定のビルドパスのライブラリで、外部JARとして、HttpClientを展開したフォルダのlibフォルダにある、httpcore-4.0.1.jarhttpclient-4.0.jarcommons-logging-1.1.1.jarの3つを追加
  • ビルド

使い方

コマンドライン引数で、UserIDとPasswordを渡すと、iTunes ConnectのSales/Trend Reportの最新のDailyレポートがダウンロードされて、gz展開されます。

IbisAppSales.java

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

/* ファイル概要:
 * Created on 2009/07/10 by ibis inc. Eiji Kamiya
 * Licence : GPL
 */

/**
 * クラス概要:
 * iTunes Connectに接続してSales/Trend Reportの最新のDailyレポートを
 * ダウンロードするコマンド.
 * @author ibis inc. Eiji Kamiya
 */
public class IbisAppSales {
    static final String BASE_URL="https://itunesconnect.apple.com";
    static final String LOGIN_PAGE_URL="/WebObjects/iTunesConnect.woa";
    DefaultHttpClient httpClient;
    HttpContext httpContext;

    /**
     * @param args
     */
    public static void main(String[] args) {
        if(args.length<2){
            System.err.println("Usage : java IbisAppSales [UserID] [Password]");
            return;
        }
        IbisAppSales app=new IbisAppSales();
        app.run(args[0],args[1]);
    }

    //コンストラクタ
    public IbisAppSales(){
        httpClient = new DefaultHttpClient();
        httpContext=new BasicHttpContext();
        httpClient.getParams().setParameter(
                ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
    }
    
    //メイン
    void run(String userId, String password){
        try {
            String html=openLoginPage();
            html=clickSignIn(html,userId,password);
            html=clickTrendReport(html);
            html=refreshToReport(html);
            html=selectDailyOption(html);
            html=clickDownload(html);
            System.out.println("Success!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //ログインページ表示
    private String openLoginPage() throws IOException {
        return doGet(BASE_URL+LOGIN_PAGE_URL);
    }
    
    //ログインページ
    //ログインする
    private String clickSignIn(String html,String userId,String password) throws IOException {
        //FROMのaction先を探す
        String action=findValue(html,"<form name=\"appleConnectForm\" method=\"post\" action=\"");        
        List<NameValuePair> param = new ArrayList <NameValuePair>();
        param.add(new BasicNameValuePair("theAccountName", userId));
        param.add(new BasicNameValuePair("theAccountPW", password));
        return doPost(BASE_URL+action,param);
    }

    //メインメニューページ
    //Trend Reportリンクをクリックする
    private String clickTrendReport(String html) throws IOException {
        String link=findValue(html, "<td valign=\"top\" align=\"center\"><a href=\"");
        return doGet(BASE_URL+link);    
    }
    
    //Trend Reportクリック後のクッションページ
    //リフレッシュする
    private String refreshToReport(String html) throws IOException{
        String link=findValue(html,"<META HTTP-EQUIV=\"refresh\" Content=\"0;URL=");
        return doGet(link);
    }

    //レポートオプション選択ページ
    //Report PeriodプルダウンでDailyを選択
    private String selectDailyOption(String html) throws IOException {
        //FROMのaction先を探す
        String action=findValue(html,"<form method=\"post\" name=\"frmVendorPage\" action=\"");
        //input hidden wosidを探す
        String wosid=findValue(html,"<input type=\"hidden\" name=\"wosid\" value=\"");

        List<NameValuePair> param = new ArrayList <NameValuePair>();
        param.add(new BasicNameValuePair("wosid", wosid));          //hidden wosid
        param.add(new BasicNameValuePair("11.7","Summary"));        //Report Typeプルダウン=Summary
        param.add(new BasicNameValuePair("11.9", "Daily"));         //Report Periodプルダウン=Daily
        param.add(new BasicNameValuePair("hiddenDayOrWeekSelection","Daily"));
        param.add(new BasicNameValuePair("hiddenSubmitTypeName", "ShowDropDown"));
        String baseUrl=httpContext.getAttribute("http.target_host").toString();
        return doPost(baseUrl+action,param);
    }
    
    //レポートオプション選択ページ
    //ダウンロードボタンクリック
    private String clickDownload(String html) throws IOException {
        //FROMのaction先を探す
        String action=findValue(html,"<form method=\"post\" name=\"frmVendorPage\" action=\"");
        //input hidden wosidを探す
        String wosid=findValue(html,"<input type=\"hidden\" name=\"wosid\" value=\"");
        //日付選択プルダウンの先頭の日付を得る
        String date=findValue(html,"name=\"11.11.1\"><option value=\"");
        
        List<NameValuePair> param = new ArrayList <NameValuePair>();
        param.add(new BasicNameValuePair("wosid", wosid));          //hidden wosid
        param.add(new BasicNameValuePair("11.7","Summary"));        //Report Typeプルダウン=Summary
        param.add(new BasicNameValuePair("11.9", "Daily"));         //Report Periodプルダウン=Daily
        param.add(new BasicNameValuePair("11.11.1", date));         //Dayプルダウン
        param.add(new BasicNameValuePair("hiddenDayOrWeekSelection","Daily"));
        param.add(new BasicNameValuePair("hiddenSubmitTypeName", "Download"));
        param.add(new BasicNameValuePair("download", "Download"));  //Submitボタン Download
        String baseUrl=httpContext.getAttribute("http.target_host").toString();
        return doPost(baseUrl+action,param);
    }
    
    //GETリクエストを投げる
    private String doGet(String url) throws IOException {
        System.out.println("GET "+url);
        HttpGet httpGet = new HttpGet(url);        
        HttpResponse response = httpClient.execute(httpGet,httpContext);
        return receiveResponse(response);
    }

    //POSTリクエストを投げる
    private String doPost(String url,List<NameValuePair> param) throws 
        ClientProtocolException, IOException {
        System.out.println("POST "+url);
        HttpPost httpPost = new HttpPost(url);
        httpPost.setEntity(new UrlEncodedFormEntity(param, HTTP.UTF_8));
        HttpResponse response = httpClient.execute(httpPost,httpContext);
        return receiveResponse(response);
    }
    
    //レスポンスを受け取った後の処理
    private String receiveResponse(HttpResponse response) throws IOException{
        String ret="";
        HttpEntity entity = response.getEntity();
        System.out.println("Response Status: " + response.getStatusLine());
        if(entity==null){
            return "";
        }
        String contentType=response.getFirstHeader("Content-Type").getValue();
        contentType=contentType.toLowerCase();
        if(contentType.startsWith("text/html")){//HTMLのとき
            ret=EntityUtils.toString(entity);
            System.out.println(ret);
        } else {
            Header disposition=response.getFirstHeader("Content-Disposition");
            if(disposition!=null){//Content-Dispositionがあるとき
                String fileName=disposition.getValue();
                if(fileName.startsWith("filename=")){
                    fileName=fileName.substring(9);//filename=をカット
                }
                InputStream in=null;
                if(contentType.equalsIgnoreCase("application/x-gzip")){//GZIPのとき
                    in=new GZIPInputStream(entity.getContent());//展開する
                    if(fileName.endsWith(".gz")){//拡張子が.gzのとき
                        fileName=fileName.substring(0,fileName.length()-3);
                    }
                } else {
                    in=entity.getContent();
                }
                System.out.println("Download "+fileName);
                save(in,fileName);
            }
        }
        return ret;        
    }
    
    //InputStreamを読んで、ファイルとして保存する
    private void save(InputStream in, String fileName) throws IOException{
        BufferedOutputStream bos=null;
        byte[] buf=new byte[4096];
        int len;
        try {
            bos=new BufferedOutputStream( new FileOutputStream(fileName));
            while((len=in.read(buf))!=-1){
                bos.write(buf,0,len);
            }
        } finally {
            if(bos!=null){
                try{
                    bos.close();
                } catch(IOException e){}
            }
        }
    }
    
    //HTMLをスクレイピングして、targetの次の文字から"までを取得
    private String findValue(String html,String target) throws IOException{
        int idx=html.indexOf(target);
        if(idx==-1){
            throw new IOException("Can't find "+target);
        }
        int idx2=html.indexOf("\"", idx+target.length());
        if(idx2==-1){
            throw new IOException("Can't find \"");
        }
        return html.substring(idx+target.length(),idx2);
    }
}

IbisAppSales.bat

REM USAGE IbisAppSales.bat [UserId] [Password]
java -classpath ../../lib/httpcomponents-client-4.0/lib/httpclient-4.0.jar;../../lib/httpcomponents-client-4.0/lib/httpcore-4.0.1.jar;../../lib/httpcomponents-client-4.0/lib/commons-logging-1.1.1.jar;. IbisAppSales %1 %2

私は、この後、ダウンロードしたtxtファイルをパースして、DBサーバにINSERTするコードを書く予定。これはアイビス社内のバックヤードシステムに強く依存するのでそのコードは公開しません。ダウンロードしたレポートをメールで送るなども便利でしょう。各自テキトーに改良してください。