かみやんの技術者ブログ

主にプログラムの話です

iTunesのAppStoreレビューのスクレイピング

ibisMail for iPhoneの開発・販売に利用している社内ツールの1つのレビュー報告用のスクレイピングプログラムを公開します。 この社内ツールを作った目的は、PCにインストールしたiTunesが非常に重く、かつ、言語を切り替えてレビューを読むという作業が恐ろしく面倒であり、開発メンバー、サポートメンバーみんなが全部を読むというのは、手間がかかりすぎて不可能というレベルだったので、毎日1回バッチを実行して、新しいレビューが世界のどこかで書かれたら、メールで社内メンバーにお知らせするというスクレイピングプログラムを書いた。書いたのはちょいと前だが、昨日、Twitterの @iphone_dev_jp に「世界中のレビューを読むのが大変」とあったので、公開します。

必要な環境は、WindowsまたはLinuxまたはMacOSJDKApache HttpClient。MySQL

普段、スクレイピングをするときは、ウエブブラウザのふりをする訳だが、今回はiTunesのふりをしなければならないため、Wiresharkというパケットキャプチャ兼アナライザを用いてハックした。

雑談

私の仕事に、チーム全体の効率アップやモチベーションアップも含まれる。以前このブログで、「iTunes Connectにログインし、売り上げのcsv.gzファイルをダウンロードし、展開しDB登録するスクレイピングプログラム」を公開(このエントリ)したが、こういったものも効率アップのための仕掛け。
今回のIPhoneAppReviewバッチも開発チームのモチベーション、製品の改善には非常に役に立つ。
ibisMailはリリースから1年5ヶ月たった。ver.1.0リリース直後は総合1位を取りましたが、その後ビジネス25位近辺と低迷していました。その間ずっと改善を続け、ついにビジネス2位になりました。感謝感激です。ユーザの皆様ありがとうございます。

この情報が参考になったと思った方は、ibisMail をダウンロードいただけると幸いです URL:ibisMailのダウンロード、レビューはこちら
※以下、プログラム

IPhoneAppReview.java
/**
 * AppStoreのレビューを収集して社内へメールで通知するバッチ
 * Copyright ibis inc. Eiji Kamiya
 * License: GPL License 
 */
package batch;

import iphone.ResponseData;
import iphone.ScrapingClient;
import iphone.beans.ReviewBean;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Properties;

import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;


import db.DBConnectIPhone;

import util.Util;
/**
 * AppStoreのレビューをスクレイピングして社内へメールで通知するバッチ
 * Wiresharkでパケットキャプチャし、URLを解析
 * Wiresharkのフィルタ「http and tcp.port==80」
 * @author kamiya
 */
public class IPhoneAppReview {
    static String BASE_URL="http://ax.itunes.apple.com";
    static String LOG_DIRECTORY="WEB-INF/log/iPhone/";
    static int MAX_RETRY=3;//iTunes Connectがときどきエラーのためリトライ
    static String SMTP_SERVER="smtp.example.com";
    static String FROM_ADDRESS="iPhoneAppReview@example.com";//エラー報告メール
    static String TO_ADDRESS="foo@example.com";//エラー報告メールあて先
    static String REVIEW_FROM_ADDRESS="iPhoneAppReview@example.com";//レビュー報告メール
    static String REVIEW_TO_ADDRESS="foo@example.com";//レビュー報告メールあて先
    static String[] appTable = {//appIDは、iTunesでアプリDLページを開いて、右クリックでURLをコピーで、URLの一部にIDがあるので分かる
    	"ibisMail","999999999999",//ibisMail for iPhone
    };
    static String[] countryTable = {
    	"アメリカ合衆国 143441",
    	"アルゼンチン 143505",
    	"オーストラリア 143460",
    	"ベルギー 143446",
    	"ブラジル 143503",
    	"カナダ 143455",
    	"チリ 143483",
    	"中国 143465",
    	"コロンビア 143501",
    	"コスタリカ 143495",
    	"クロアチア 143494",
    	"チェコ 143489",
    	"デンマーク 143458",
    	"ドイツ 143443",
    	"エルサルバドル 143506",
    	"スペイン 143454",
    	"フィンランド 143447",
    	"フランス 143442",
    	"ギリシャ 143448",
    	"グァテマラ 143504",
    	"香港 143463",
    	"ハンガリー 143482",
    	"インド 143467",
    	"インドネシア 143476",
    	"アイルランド 143449",
    	"イスラエル 143491",
    	"イタリア 143450",
    	"韓国 143466",
    	"クウェート 143493",
    	"レバノン 143497",
    	"ルクセンブルク 143451",
    	"マレーシア 143473",
    	"メキシコ 143468",
    	"オランダ 143452",
    	"ニュージーランド 143461",
    	"ノルウェイ 143457",
    	"オーストリア 143445",
    	"パキスタン 143477",
    	"パナマ 143485",
    	"ペルー 143507",
    	"フィリピン 143474",
    	"ポーランド 143478",
    	"ポルトガル 143453",
    	"カタール 143498",
    	"ルーマニア 143487",
    	"ロシア 143469",
    	"サウジアラビア 143479",
    	"スイス 143459",
    	"シンガポール 143464",
    	"スロバキア 143496",
    	"スロベニア 143499",
    	"南アフリカ 143472",
    	"スリランカ 143486",
    	"スウェーデン 143456",
    	"台湾 143470",
    	"タイ 143475",
    	"トルコ 143480",
    	"アラブ首長国連邦 143481",
    	"イギリス 143444",
    	"ベネズエラ 143502",
    	"ベトナム 143471",
    	"日本 143462",	
    };
    private ScrapingClient client;//Httpクライアント
    private Hashtable<Integer,String> appMap;//アプリ番号とアプリ名の表
    private Hashtable<Integer,String> countryMap;//国番号と国名の表
    
    /**
	 * エントリポイント
	 * @param args
	 */
	public static void main(String[] args) {
        for(int i=0;i<MAX_RETRY;i++){
            boolean isErr=false;
            try {
                //メイン処理
                IPhoneAppReview app=new IPhoneAppReview();
                app.run();
            } catch (Exception e) {
                e.printStackTrace();
                isErr=true;
                System.out.println("Retry:"+(i+1));
                if(i==MAX_RETRY-1){//最大リトライ数に達したとき
                    System.out.println("GIVE UP!!!");
                    StringWriter errSW=new StringWriter();
                    PrintWriter err=new PrintWriter(errSW);
                    e.printStackTrace(err);
                    Util.sendMail(SMTP_SERVER, FROM_ADDRESS, TO_ADDRESS, 
                            "【iPhone】IPhoneAppReview Exception",
                            "IPhoneAppReviewバッチでExceptionが発生しました。\n" +
                            errSW.toString());
                    break;
                }
            }
            if(isErr==false){//成功したとき
                break;
            }
        }
   	}

    /**
     * コンストラクタ
     * @throws Exception 
     */
    public IPhoneAppReview() throws Exception{
    	client = new ScrapingClient();
        client.setUserAgent("iTunes/9.2 (Windows; Microsoft Windows 7 "
        		+"Home Premium Edition (Build 7600)) AppleWebKit/533.16");
        //アプリ番号表を作成する
        appMap = new Hashtable<Integer, String>();
        for(int i=0;i<appTable.length;i+=2){
        	appMap.put(Integer.valueOf(appTable[i+1]), appTable[i]);
        }
        //国番号表を作成する
        countryMap = new Hashtable<Integer, String>();
        for(int i=0;i<countryTable.length;i++){
        	int idx=countryTable[i].lastIndexOf(' ');
        	if(idx==-1){
        		throw new Exception("Can't found ' '.");
        	}
        	String key = countryTable[i].substring(idx+1);
        	String value = countryTable[i].substring(0,idx);
        	int keyI = Integer.parseInt(key);
        	countryMap.put(Integer.valueOf(keyI),value);
        }
    }
    
    /**
     * メイン
     * @throws Exception 
     */
    private void run() throws Exception{
    	Enumeration<Integer> appIds=appMap.keys();
    	while(appIds.hasMoreElements()){
    		Integer appId = appIds.nextElement();
	        Enumeration<Integer> countryCodes=countryMap.keys();
	        while(countryCodes.hasMoreElements()){
	        	Integer countryCode=countryCodes.nextElement();
	        	String countryName = countryMap.get(countryCode);
	        	System.out.println("Country="+countryName);
	            openReviewPage(appId.toString(), countryCode.intValue());
	        }
    	}
    }
    
    /**
     * 製品ページ内のレビューの取得
     * 参考:http://www.doedoegames.com/blog/scraping-the-itunes-store
     * 参考:http://blogs.oreilly.com/iphone/2008/08/scraping-appstore-reviews.html 国コード一覧
     * @param appId
     * @return
     * @throws Exception 
     */
    private void openReviewPage(String appId,int countryCode) throws Exception {
    	String url = BASE_URL+"/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id="+
    		appId+"&pageNumber=0&sortOrdering=4&type=Purple+Software";
    	client.setHeader("X-Apple-Store-Front",""+countryCode+"-1");
    	ResponseData res = client.doGet(url);
    	//HTMLをパースしてBeanを作成する
    	String html = res.getHtml();
    	//client.saveToFile("IPhoneAppReview.txt", html);//DEBUG用
    	int idx = 0;
    	ArrayList<ReviewBean> list = new ArrayList<ReviewBean>();
    	while(true){
    		ReviewBean bean = new ReviewBean();
    		bean.setAppId(appId);
    		bean.setCountryCode(countryCode);
	    	//タイトルを探す
	    	StringBuffer title = new StringBuffer();
	    	idx = client.findValue(html,idx,"styleSet=\"basic13\" textJust=\"left\" "
	    			+"maxLines=\"1\"><SetFontStyle normalStyle=\"textColor\"><b>","</b>",title);
	    	if(idx==-1){
	    		break;
	    	}
	    	bean.setTitle(title.toString());
	    	//星を探す
	    	StringBuffer star = new StringBuffer();
	    	idx = client.findValue(html, idx, "<HBoxView topInset=\"1\" alt=\"", " star", star);
	    	if(idx==-1){
	    		break;
	    	}
	    	bean.setStar(Util.parseInt(star.toString(), 0));
	    	//ユーザIDを探す
	    	StringBuffer userId = new StringBuffer();
	    	idx = client.findValue(html, idx, "userReviewId=", "\">", userId);
	    	if(idx==-1){
	    		break;
	    	}
	    	bean.setUserProfileId(userId.toString());
	    	//ユーザ名を探す
	    	StringBuffer userName = new StringBuffer();
	    	idx = client.findValue(html, idx, appId+"\">", "</GotoURL>", userName);
	    	if(idx==-1){
	    		break;
	    	}
	    	String userNameStr = convertHtmlToText(userName.toString());
	    	bean.setUserName(userNameStr.trim());
	    	//バージョンを探す
	    	StringBuffer version=new StringBuffer();
	    	idx = client.findValue(html, idx, "- ", "-", version);
	    	if(idx==-1){
	    		break;
	    	}
	    	bean.setAppVersion(version.toString().trim());
	    	//更新日を探す
	    	StringBuffer dateString = new StringBuffer();
	    	idx = client.findValue(html, idx, " ", "</SetFontStyle></TextView>", dateString);
	    	if(idx==-1){
	    		break;
	    	}
	    	String dateStr = dateString.toString().trim();
	    	SimpleDateFormat dateFormat= new SimpleDateFormat("dd-MMM-yyyy",Locale.US);
	    	if(countryCode==143441){//USのとき
	    		dateFormat = new SimpleDateFormat("MMM d, yyyy",Locale.US);
	    	} else if(countryCode==143443){//Deutschlandのとき
	    		dateFormat = new SimpleDateFormat("dd.MM.yyyy",Locale.GERMAN);
	    	} else if(countryCode==143442){//Franceのとき
	    		dateFormat = new SimpleDateFormat("dd MMM yyyy",Locale.FRANCE);
	    	} else if(countryCode==143450){//Italiaのとき
	    		dateFormat = new SimpleDateFormat("dd-MMM-yyy",Locale.ITALY);
	    	}
	    	java.util.Date date;
	    	try {
	    		date = dateFormat.parse(dateStr);
	    	} catch(ParseException e){
	    		break;
	    	}
	    	bean.setReviewDate(new java.sql.Date(date.getTime()));
	    	//本文を探す
	    	StringBuffer body=new StringBuffer();
	    	idx = client.findValue(html, idx, "styleSet=\"normal11\" textJust=\"left\">"
	    			+"<SetFontStyle normalStyle=\"textColor\">", "</SetFontStyle></TextView>", 
	    			body);
	    	if(idx==-1){
	    		break;
	    	}
	    	bean.setBody(convertHtmlToText(body.toString()));
	    	list.add(bean);
    	}
    	//ソートにする
    	sortReviewBean(list);
    	//メールで報告とDBへ挿入
    	insertToDb(list);
    }

	/** リストの中身をソート
	 * @param list
	 * @return
	 */
	private void sortReviewBean(ArrayList<ReviewBean> list) {
		Collections.sort(list,new Comparator<ReviewBean>(){
			public int compare(ReviewBean o1, ReviewBean o2) {
				long a1 = o1.getReviewDate().getTime()/100000;
				long a2 = o2.getReviewDate().getTime()/100000;
				return (int)(a1 - a2);
			}
		});
	}

	/**
	 * 指定したレビューを1通のメールとして報告
	 * @param db
	 * @param con
	 * @param bean
	 * @throws SQLException
	 */
	private void reportByMail(DBConnectIPhone db, Connection con,
			ReviewBean bean) throws SQLException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
		ArrayList<ReviewBean> list=db.getReview(con, bean.getAppId(), 
				bean.getCountryCode(), bean.getUserProfileId());
		list.add(0, bean);//先頭に自分を追加
		String countryName = countryMap.get(Integer.valueOf(bean.getCountryCode()));
		String subject=bean.getUserName()+"さん("+countryName+")のレビュー";
		StringBuffer body=new StringBuffer();
		body.append(bean.getUserName()+"さん("+countryName+")のレビューが更新されました。\n");
		body.append("--------------------------------------------------\n");
		for (ReviewBean bean2 : list) {
			body.append("日付:"+sdf.format(bean2.getReviewDate())+" ");
			body.append(bean2.getAppVersion()+"\n");
			body.append("タイトル:"+bean2.getTitle()+" "
					+makeStarMark(bean2.getStar())+"\n\n");
			body.append(bean2.getBody()+"\n");
			body.append("\n");
			body.append("--------------------------------------------------\n");
		}
		body.append("portalWeb/IPhoneAppReview batch\n");
		sendMail(SMTP_SERVER, REVIEW_FROM_ADDRESS, REVIEW_TO_ADDRESS, 
		        subject,body.toString());
	}

	/** 指定した数から星文字列を得る
	 * @param star
	 * @return
	 */
	private String makeStarMark(int star) {
		switch(star){
		case 1:return "★☆☆☆☆";
		case 2:return "★★☆☆☆";
		case 3:return "★★★☆☆";
		case 4:return "★★★★☆";
		case 5:return "★★★★★";
		default:return "星不明";
		}
	}

	/**
     * 入力レビュー列をDBに挿入
     * @param list
     * @throws ClassNotFoundException
     * @throws SQLException
     */
    private void insertToDb(ArrayList<ReviewBean> list) throws ClassNotFoundException, SQLException {
        DBConnectIPhone db=null;
        try{
            db=new DBConnectIPhone();
            Connection con=Common.getIPhoneConnection();
            for (ReviewBean bean : list) {
            	if(db.isExistReview(con, bean)==false){//DBにないとき
        	    	System.out.println(bean.toString());
            		reportByMail(db, con, bean);
	        		db.insertReview(con, bean);
            	}
			}
        } finally {
        	if(db!=null){
        		db.close();
        	}
        }
	}

	/** HTMLをテキストへ変換(' '+'\n'または'\n'の行をカット)
     * \r\nの改行は考慮されていない
     * @param str
     * @return
     */
    public static String convertHtmlToText(String str) {
		StringBuffer sb = new StringBuffer();
		for(int i=0;i<str.length();i++){
			char ch=str.charAt(i);
			if(ch=='\n' && i<str.length()-2 && str.charAt(i+1)==' ' 
				&& str.charAt(i+2)=='\n'){//"\n \n"のとき
				sb.append(ch);
				i+=2;
			} else if(ch=='\n' && i<str.length()-1 && str.charAt(i+1)=='\n'){//"\n\n"のとき
				sb.append(ch);
				i++;
			} else if(ch=='&'){//&のとき(実態参照解決)
				int idx = str.indexOf(';',i+1);
				if(idx==-1){//;がないとき
					sb.append(ch);
				} else {//;があるとき
					String mark=str.substring(i+1,idx);
					if(mark.equals("lt")){//<のとき
						sb.append('<');
					} else if(mark.equals("gt")){//>のとき
						sb.append('>');
					} else if(mark.equals("amp")){//&のとき
						sb.append('&');
					} else if(mark.length()>0 && mark.charAt(0)=='#'){//#999;のとき
						String num = mark.substring(1);
						int n = Util.parseInt(num, -1);
						if(n!=-1){//数字に変換できたとき
							sb.append((char)n);
						}
					} else {
						sb.append(ch);
						sb.append(mark);
						sb.append(';');
					}
					i+=mark.length()+1;
				}
			} else if(ch=='<'){//<のとき
				int idx=str.indexOf('>',i+1);
				if(idx==-1){//>がないとき
					sb.append(ch);
				} else {//>があるとき
					i=idx;
				}
			} else {
				sb.append(ch);
			}
		}
		return sb.toString();
	}

    public static void sendMail(String smtp, String from, String to, String subject, String body){
		try {
			Properties props = System.getProperties();
			// SMTPサーバーのアドレスを指定
			props.put("mail.smtp.host", smtp);// 設定ファイルから読み込み
			Session session = Session.getDefaultInstance(props, null);
			MimeMessage mimeMessage = new MimeMessage(session);
			// 送信元メールアドレスと送信者名を指定
			mimeMessage.setFrom(new InternetAddress(from, null, "utf-8"));
			// 送信先メールアドレスを指定
			mimeMessage.setRecipients(Message.RecipientType.TO, to);
			// メールのタイトルを指定
			mimeMessage.setSubject(subject, "utf-8");
			// メールの内容を指定
			mimeMessage.setText(body, "utf-8");
			// 送信日付を指定
			mimeMessage.setSentDate(new Date());
			
			Transport.send(mimeMessage);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
ResponseData.java
/* ファイル概要:
 * Copyright ibis inc. Eiji Kamiya
 * License: GPL License
 * Created on 2009/08/18 by kamiya
 */
package iphone;

import org.apache.http.HttpResponse;

/**
 * クラス概要:IPhoneApp〜バッチから利用されるクラス
 * @author kamiya
 */
public class ResponseData {
    private String html;//HTML
    private String statusLine;//ステータスライン
    private String contentType;//Content-Type(すべて小文字化されている)
    private String fileName;//Contents-Dispositionがあったときのファイル名
    private String location;//302などのmoved系の遷移先URL
    private HttpResponse response;//HTTPのレスポンス
    /**
     * @return htmlの取得
     */
    public String getHtml() {
        return html;
    }
    /**
     * @param html htmlの設定
     */
    public void setHtml(String html) {
        this.html = html;
    }
    /**
     * @return statusLineの取得
     */
    public String getstatusLine() {
        return statusLine;
    }
    /**
     * @param statusLine statusLineの設定
     */
    public void setStatusLine(String statusLine) {
        this.statusLine = statusLine;
    }
    /**
     * @return fileNameの取得
     */
    public String getFileName() {
        return fileName;
    }
    /**
     * @param fileName fileNameの設定
     */
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
    /**
     * @return locationの取得
     */
    public String getLocation() {
        return location;
    }
    /**
     * @param location locationの設定
     */
    public void setLocation(String location) {
        this.location = location;
    }
    /**
     * @return contentTypeの取得
     */
    public String getContentType() {
        return contentType;
    }
    /**
     * @param contentType contentTypeの設定
     */
    public void setContentType(String contentType) {
        this.contentType = contentType;
    }
	/**
	 * HttpResponseの取得
	 * @return
	 */
	public HttpResponse getResponse() {
		return response;
	}
	/**
	 * HttpResponseの設定
	 * @param response
	 */
	public void setResponse(HttpResponse response) {
		this.response = response;
	}
}
ScrapingClient.java
/**
 * Copyright ibis inc. Eiji Kamiya
 * License: GPL License
 */
package iphone;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
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.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.AbstractHttpMessage;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;


/**
 * スクレイピング用ユーティリティ
 * @author kamiya
 * 2010/7/4
 */
public class ScrapingClient {
    private DefaultHttpClient httpClient;//Webクライアント
    private HttpContext httpContext;//コンテキスト
    private Hashtable<String,String> headerMap;//追加するリクエストヘッダ

    /**
     * コンストラクタ
     */
    public ScrapingClient(){
		HttpParams params = new BasicHttpParams();
        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
        HttpProtocolParams.setContentCharset(params, "UTF-8");
        HttpProtocolParams.setUseExpectContinue(params, true);
		SchemeRegistry registry = new SchemeRegistry();
		registry.register(new Scheme("http",
				PlainSocketFactory.getSocketFactory(), 80));
		registry.register(new Scheme("https",
				SSLSocketFactory.getSocketFactory(),443));
		ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);
		httpClient = new DefaultHttpClient(ccm, params);
        httpContext=new BasicHttpContext();
        httpClient.getParams().setParameter(
                ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
        headerMap = new Hashtable<String, String>();
    }
    
    /** User-Agentの設定
     * @param ua
     */
    public void setUserAgent(String ua){
    	httpClient.getParams().setParameter(HttpProtocolParams.USER_AGENT, ua);
    }
    
    /**
     * GETリクエストを投げる
     * @param url GET先URL
     * @return レスポンスデータ
     * @throws Exception
     */
    public ResponseData doGet(String url) throws Exception {
        ResponseData res;
        do {
            System.out.println("GET "+url);
            HttpGet httpGet = new HttpGet(url);
            addHeader(httpGet);
            HttpResponse response = httpClient.execute(httpGet,httpContext);
            res=receiveResponse(response);
        } while(res.getLocation()!=null);
        return res;
    }

	/**
     * POSTリクエストを投げる
     * @param url POST先URL
     * @param param POSTするパラメータ
     * @return レスポンスデータ
     * @throws Exception
     */
    public ResponseData doPost(String url,List<NameValuePair> param) throws 
        Exception {
        ResponseData res;
        do{
            System.out.println("POST "+url);
            HttpPost httpPost = new HttpPost(url);
            addHeader(httpPost);
            httpPost.setEntity(new UrlEncodedFormEntity(param, HTTP.UTF_8));
            HttpResponse response = httpClient.execute(httpPost,httpContext);
            res=receiveResponse(response);
            if(res.getLocation()!=null){//リダイレクトのとき
                url=res.getLocation();
            }
        } while(res.getLocation()!=null);
        return res;
    }

    /**
     * レスポンスを受け取った後の処理
     * @param response レスポンス
     * @return レスポンスデータ
     * @throws Exception
     */
    private ResponseData receiveResponse(HttpResponse response) throws Exception{
        ResponseData res=new ResponseData();
        res.setResponse(response);
        System.out.println("Response Status: " + response.getStatusLine());
        res.setStatusLine(response.getStatusLine().toString());
        String contentType=response.getFirstHeader("Content-Type").getValue();
        contentType=contentType.toLowerCase();
        res.setContentType(contentType);
        Header location=response.getFirstHeader("Location");
        if(location!=null){//Locationヘッダがあるとき
            res.setLocation(location.getValue());
        }
        HttpEntity entity = response.getEntity();
        if(entity==null){
            return res;
        }
        if(contentType.startsWith("text/html") ||
        		contentType.startsWith("text/xml")){//HTMLまたはxmlのとき
            res.setHtml(EntityUtils.toString(entity,"utf-8"));
            //System.out.println(res.getHtml());
        }
        return res;
    }
    
    /**
     * HTMLをスクレイピングして、targetの次の文字から"までを取得
     * @param html 入力HTML
     * @param target 探す文字列
     * @return 得られた値
     * @throws IOException
     */
    public 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);
    }
    
    /**
     * HTMLをスクレイピングして、afterを探し、それ以降のtargetの次の文字から"までを取得
     * @param html
     * @param target
     * @param after
     * @return 得られた値
     * @throws IOException
     */
    public String findValue(String html,String after, String target) throws IOException {
    	int idx=html.indexOf(after);
    	if(idx==-1){
    		throw new IOException("Can't find "+after);
    	}
        int idx1=html.indexOf(target,idx+after.length());
        if(idx1==-1){
            throw new IOException("Can't find "+target);
        }
        int idx2=html.indexOf("\"", idx1+target.length());
        if(idx2==-1){
            throw new IOException("Can't find \"");
        }
        return html.substring(idx1+target.length(),idx2);
    }
    
    /** HTMLをスクレイピングして検索の文字から終端文字までの間の文字を得る(検索開始位置指定付き)
     * @param html 検索対象の文字列
     * @param start 検索開始位置
     * @param target 検索の文字列
     * @param terminater 検索の文字列の次の文字から検索する終端文字列
     * @param ret 検索の文字列と終端文字列の間の文字列(このメソッド内でappend()する、事前にnewしておくこと)
     * @return 見つかった場合は、終端文字列の次の文字の位置、見つからないときは-1
     * @throws IOException 
     */
    public int findValue(String html, int start, String target, String terminater, 
    		StringBuffer ret) throws IOException{
    	int idx = html.indexOf(target,start);
    	if(idx==-1){//検索の見つからないとき
    		return -1;
    	}
    	idx += target.length();
    	int idx2=html.indexOf(terminater,idx);
    	if(idx2==-1){//終端文字列が見つからないとき
    		return -1;
    	}
    	ret.append(html.substring(idx, idx2));
    	idx2+=terminater.length();
    	return idx2;
    }

    /** httpGetまたはhttpPostにヘッダを追加
     * @param getOrPost
     */
    private void addHeader(AbstractHttpMessage getOrPost){
    	Enumeration<String> e = headerMap.keys();
    	while(e.hasMoreElements()){
    		String key = e.nextElement();
    		String value = headerMap.get(key);
    		getOrPost.addHeader(key,value);
    	}
    }
    
    /** ヘッダを追加(すでに同じヘッダ名が登録されているときは上書き)
     * @param key
     * @param value
     */
    public void setHeader(String key, String value){
    	if(headerMap.contains(key)){//すでに登録済みのとき
    		headerMap.remove(key);
    	}
    	headerMap.put(key, value);
    }
    
    /**
     * 追加するヘッダ情報をクリア
     */
    public void clearHeader(){
    	headerMap.clear();
    }
    
    /**
     * HTTPコンテキストの属性の取得
     * @param key
     * @return
     */
    public String getAttribute(String key){
    	return httpContext.getAttribute(key).toString();
    }

	/**
	 * 文字列をファイルに出力する
	 * @param fname
	 * @param str
	 * @throws IOException
	 */
	public void saveToFile(String fname, String str) throws IOException {
		BufferedWriter br=null;
		try {
			br = new BufferedWriter(new FileWriter(fname));
			br.write(str);
		} finally {
			if(br!=null){
				try {
					br.close();
				} catch(Exception e){
				}
			}
		}
	}
}
ReviewBean.java
/**
 * Copyright ibis inc. Eiji Kamiya
 * License: GPL License
 */
package iphone.beans;

import java.sql.Date;

/** レビュー 
 * @author kamiya 2010/07/05
 *
 */
public class ReviewBean {
	private String appId;//アプリ番号
	private int countryCode;//国番号
	private String userProfileId;//10桁ぐらいの数字
	private String userName;//ユーザ名
	private String appVersion;//アプリバージョン
	private Date reviewDate;//レビュー更新日
	private int star;//星の数
	private String title;//レビュータイトル
	private String body;//レビュー本文
	
	public String getAppId() {
		return appId;
	}
	public void setAppId(String appId) {
		this.appId = appId;
	}
	public int getCountryCode() {
		return countryCode;
	}
	public void setCountryCode(int countryCode) {
		this.countryCode = countryCode;
	}
	public String getUserProfileId() {
		return userProfileId;
	}
	public void setUserProfileId(String userProfileId) {
		this.userProfileId = userProfileId;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getAppVersion() {
		return appVersion;
	}
	public void setAppVersion(String appVersion) {
		this.appVersion = appVersion;
	}
	public Date getReviewDate() {
		return reviewDate;
	}
	public void setReviewDate(Date date) {
		this.reviewDate = date;
	}
	public int getStar() {
		return star;
	}
	public void setStar(int star) {
		this.star = star;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getBody() {
		return body;
	}
	public void setBody(String body) {
		this.body = body;
	}
	public String toString(){
		return "Date="+reviewDate.toString()+" User="+userName
		+" Version="+appVersion+" Title="+title+" Body="+body;
	}
}
DBConnectIPhone.java
	/** 指定したAppID,CountryCode,UserProfileID,ReviewDateの行があるか?
	 * @param con
	 * @param bean
	 * @return
	 * @throws SQLException
	 */
	public boolean isExistReview(Connection con, ReviewBean bean) throws SQLException {
		PreparedStatement ps = null;
        ResultSet rs = null;
        try {
			String sql = "select AppID from DT_REVIEW "
				+"where AppID=? and CountryCode=? and UserProfileID=? and ReviewDate=? limit 0,1";
			ps = con.prepareStatement(sql);
			ps.setString(1, bean.getAppId());
			ps.setInt(2, bean.getCountryCode());
			ps.setString(3, bean.getUserProfileId());
			ps.setDate(4, bean.getReviewDate());
			rs = ps.executeQuery();
			if(rs.next()){
				return true;
			}
			return false;
        } finally {
        	close(null, ps, rs);
        }
	}

	/** 指定したAppID,CountryCode,UserProfileID,ReviewDateの行があるか?
	 * @param con
	 * @param bean
	 * @return
	 * @throws SQLException
	 */
	public ArrayList<ReviewBean> getReview(Connection con, String appId, 
			int countryCode, String userProfileId) throws SQLException {
		PreparedStatement ps = null;
        ResultSet rs = null;
        ArrayList<ReviewBean> list=new ArrayList<ReviewBean>();
        try {
			String sql = "select AppID,CountryCode,UserProfileID,UserName,"
				+"AppVersion,ReviewDate,Title,Body,Star from DT_REVIEW "
				+"where AppID=? and CountryCode=? and UserProfileID=? "
				+"order by ReviewDate desc";
			ps = con.prepareStatement(sql);
			ps.setString(1, appId);
			ps.setInt(2, countryCode);
			ps.setString(3, userProfileId);
			rs = ps.executeQuery();
			while(rs.next()){
				ReviewBean bean = new ReviewBean();
				bean.setAppId(rs.getString(1));
				bean.setCountryCode(rs.getInt(2));
				bean.setUserProfileId(rs.getString(3));
				bean.setUserName(rs.getString(4));
				bean.setAppVersion(rs.getString(5));
				bean.setReviewDate(rs.getDate(6));
				bean.setTitle(rs.getString(7));
				bean.setBody(rs.getString(8));
				bean.setStar(rs.getInt(9));
				list.add(bean);
			}
			return list;
        } finally {
        	close(null, ps, rs);
        }
	}

	/** 指定したレビューを1件挿入する
	 * @param con
	 * @param bean
	 * @throws SQLException
	 */
	public void insertReview(Connection con, ReviewBean bean) throws SQLException {
		PreparedStatement ps = null;
        ResultSet rs = null;
        try {
        	String sql="insert DT_REVIEW set "
        		+"AppID=?,"
        		+"CountryCode=?,"
        		+"UserProfileID=?,"
        		+"UserName=?,"
        		+"AppVersion=?,"
        		+"ReviewDate=?,"
        		+"Title=?,"
        		+"Body=?,"
        		+"Star=?";
        	ps=con.prepareStatement(sql);
        	int i=1;
        	ps.setString(i++, bean.getAppId());
        	ps.setInt(i++, bean.getCountryCode());
        	ps.setString(i++, bean.getUserProfileId());
        	ps.setString(i++, bean.getUserName());
        	ps.setString(i++, bean.getAppVersion());
        	ps.setDate(i++, bean.getReviewDate());
        	ps.setString(i++, bean.getTitle());
        	ps.setString(i++, bean.getBody());
        	ps.setInt(i++, bean.getStar());
        	ps.execute();
        } finally {
        	close(null, ps, rs);
        }		
	}
TABLE定義
CREATE TABLE `IPhoneDB`.`DT_REVIEW` (
  `AppID` VARCHAR(20) NOT NULL,
  `CountryCode` INT NOT NULL,
  `UserProfileID` VARCHAR(20),
  `UserName` VARCHAR(255),
  `AppVersion` VARCHAR(30),
  `ReviewDate` DATE,
  `Title` VARCHAR(255),
  `Body` TEXT,
  `Star` int default 0,
  PRIMARY KEY USING BTREE(`AppID`,`CountryCode`,`UserProfileID`,`ReviewDate`)
) ENGINE = InnoDB,  DEFAULT CHARSET=utf8;
あとは、、
  • クラスパスにJavaMail(mail.jar)、httpclient-4.0.1.jar、httpcore-4.0.1.jar、mysql-connector-java-5.1.8-bin.jarを追加
  • 起動用のバッチファイルを作成、cronなり、Windowsならタスクスケジューラに登録
  • DBConnectIPhoneクラスが一部、弊社内セキュリティを守るため削られていますが、ソースを読めば何を補完すればよいか分かると思います。