ブクログの本棚を並べ替えるプログラムを作ったよ

背景

ブクログ(http://booklog.jp/)では、登録した本を本棚に並べることができます。また、ブクログブログパーツを使うと、自分のブログに本棚を載せることができます。この本棚ブログパーツでは、本は必ず投稿日時の順に並びます。
しかし、自分の気に入った順や、売上ランキングの高い順に本を並べたいこともあります。

そこで、今回はブクログの本棚を並べ替えるプログラムを作成しました。

概要

このプログラムは、ブクログの本の投稿日時を変更することで、本棚を並べ替えます。

並べ替えの方法は、以下の3つから選べます。

売上ランキング順
ブクログに表記されている売上ランキングの高い順に並べ替えます。
ランク順
自分で付けたランク(★の数)の高い順に並べ替えます。
出版日時順
出版日時の新しい順に並べ替えます。

実行環境

jdk1.5以上があれば動くと思います。以下の環境で動作確認しました。

OS jdk
Windows XP SP2 jdk 1.6.0_10
Ubuntu 8.10 jdk 1.6.0_10
SunOS 5.10 jdk 1.5.0_12

使い方

1. ソースコードコンパイル

ソースコードは以下の二つです。(後ろの方に全文掲載しています。)

javacコマンドでソースコードコンパイルします。

javac BooklogSorter.java BooklogAccess.java
2. プログラムの起動

javaコマンドで、プログラムを起動します。

java BooklogSorter

プログラムが起動されると、以下の内容が表示されます。

■■ブクログの情報を並べ替えます。■■
3. アカウント情報の入力

その後、アカウント情報の入力が求められるので、自分のアカウント情報を入力します。

アカウント情報を入力してください。
■ブクログ
ユーザID: ブクログのユーザIDを入力する。
パスワード: ブクログのパスワードを入力する。
3.X. ファイルからのアカウント情報の取得

もし、アカウント情報を手で入力するのが面倒な場合は、アカウント情報をファイルに記録しておくこともできます。
まず、以下の内容を書き込んだアカウントファイルを作成します。

ブクログのユーザID
ブクログのパスワード

プログラムの起動時に「-l」オプションでアカウントファイルの名前を指定すると、このファイルからアカウント情報が取得されます。

java DokushoMeterBooklogMatching -l アカウントファイルの名前

以下のように表示され、アカウント情報の入力が不要になります。

アカウント情報をファイルから取得しました。ファイル名=[アカウントファイルの名前]
4. 並べ替え方法の選択

次に、並べ替え方法を選択が求められるので、選択する並べ替え方法の番号を入力してください。

並べ替え方法を選んでください。
1. 売上ランキング順
2. ランク順
3. 出版日時順
bold;font-style:italic;">番号
5. 並べ替えの実行

並べ替え方法を選ぶと、プログラムはブクログの情報の並べ替えを開始します。この処理には時間がかかることがあります。並べ替えが終了すると、以下が表示されます。

ブクログの情報の並べ替えを終了しました。

並べ替えの例

このプログラムで、自分の本棚を並べ替えてみました。

1. 売上ランキング順:

2. ランク順:

3. 出版日時順:

ソースコード

以下に、ソースコードを全文掲載します。

BooklogSorter.java
import java.util.*;
import java.text.*;
import java.io.*;

public class BooklogSorter {

	private static final String NL = System.getProperty( "line.separator" );

	public static void main( String[] args ) throws Exception {
	
		System.out.println( "■■ブクログの情報を並べ替えます。■■" );
		System.out.println();

		//////////////////////////////////////////////////////////
		// アカウント情報を取得取得する。
		//////////////////////////////////////////////////////////

		String userid = "";			// ユーザID
		String password = "";		// パスワード
		
		boolean accountOK = false;		// アカウント情報取得成功フラグ

		// 引数を確認する。
		if ( args.length >= 2 && args[0].startsWith( "-l" ) ) {
			// 「-l」が指定されている:
		
			//////////////////////////////////////////////////////////
			// アカウント情報をアカウントファイルから取得する。
			//////////////////////////////////////////////////////////

			// アカウントファイルの名前を取得する。
			String accountFile = args[1];
			
			BufferedReader reader = null;
			try {
				// アカウントファイルを開く。
				reader = new BufferedReader( new FileReader( accountFile ) );
				
				// アカウントファイルからアカウント情報を読み取る。
				userid = reader.readLine();
				password = reader.readLine();

				// アカウント情報取得成功。
				accountOK = true;
				System.out.println( "アカウント情報をファイルから取得しました。ファイル名=[" + accountFile + "]" );
				
			} catch ( IOException e ) {
				System.out.println( "アカウント情報を正常に読み込めませんでした。ファイル名=[" + accountFile + "]"  );
				System.out.println();
			} finally {
				// ファイルを閉じる。
				if ( reader != null ) {
					try {
						reader.close();
					} catch ( IOException e ) {
						/* 無視 */
					}
				}
			}
		}

		if ( ! accountOK ) {
			
			//////////////////////////////////////////////////////////
			// アカウント情報を入力してもらう。
			//////////////////////////////////////////////////////////
			
			System.out.println( "アカウント情報を入力してください。" );
			System.out.println( "■ブクログ" );
			userid = getInput( "ユーザID: " );
			password = getInput( "パスワード: " );
		}
		
		System.out.println();
		
		//////////////////////////////////////////////////////////
		// 並べ替ええ方法を選択してもる。 
		//////////////////////////////////////////////////////////

		String prompt = "並べ替え方法を選んでください。" + NL
							+ "1. 売上ランキング順" + NL
							+ "2. ランク順" + NL
							+ "3. 出版日時順" + NL
							+ ": ";
		int order = getInputInt( prompt );
		
		System.out.println();
		
		// 選択された並べ替え方法に対応するコンパレータを作成する。
		Comparator<Map<String, String>> comparator;
		switch ( order ) {
			case 1:
				// 1. 売上ランキング順:
				comparator = new Comparator<Map<String, String>>() {
					public int compare( Map<String, String> info1, Map<String, String> info2 ) {
						int saleRank1;
						try {
							saleRank1 = Integer.parseInt( info1.get( "saleRank" ) );
						} catch ( NumberFormatException e ) {
							// 売上ランキングが未指定:
							saleRank1  = Integer.MAX_VALUE;
						}
						int saleRank2 = Integer.MAX_VALUE;
						try {
							saleRank2 = Integer.parseInt( info2.get( "saleRank" ) );
						} catch ( NumberFormatException e ) {
							// 売上ランキングが未指定:
							saleRank2  = Integer.MAX_VALUE;
						}
						
						return saleRank1 - saleRank2;
					}
				};
				break;
			
			case 2:
				// 2. ランク順:
				comparator = new Comparator<Map<String, String>>() {
					public int compare( Map<String, String> info1, Map<String, String> info2 ) {
						String rank1 = info1.get( "rank" );
						String rank2 = info2.get( "rank" );
						
						return rank2.compareTo( rank1 );
					}
				};
				break;
			
			case 3:
				// 3. 出版日時順
				comparator = new Comparator<Map<String, String>>() {
					public int compare( Map<String, String> info1, Map<String, String> info2 ) {
						String pubDate1 = info1.get( "pubDate" );
						String pubDate2 = info2.get( "pubDate" );
						
						return pubDate2.compareTo( pubDate1 );
					}
				};
				break;
			
			default:
				// その他:
				
				// 処理を終了する。
				System.out.println( "処理を終了します。" );
				return;
		}
		
		//////////////////////////////////////////////////////////
		// ブクログから本の情報を取得する。
		//////////////////////////////////////////////////////////

		// ブクログからクッキーを取得する。
		String cookie = BooklogAccess.getCookie();
		
		// ブクログにログインする。
		BooklogAccess.login( cookie, userid, password );
		
		// ログイン情報を取得し、ログインに成功したことを確認する。
		Map loginInfo = BooklogAccess.getLoginInfo( cookie );
		if ( ! userid.equals( loginInfo.get( "username" ) ) ) {
			// ログイン失敗:
			throw new Exception( "ログイン失敗" );
		}
		// ブクログから本の一覧を取得する。
		String[] bookList = BooklogAccess.getBookList( cookie );
		
		// 一覧中のすべての本について以下を繰り返す。
		List<Map<String, String>> bookInfo = new ArrayList<Map<String, String>>();
		for ( int i = 0; i < bookList.length; ++i ) {
			String isbn = bookList[i];
			// ブクログから本の情報を取得する
			Map<String, String> info = BooklogAccess.getBookInfo( cookie, isbn );
			
			bookInfo.add( info );
		}
		
		//////////////////////////////////////////////////////////
		// 本の情報を並べ替える。
		//////////////////////////////////////////////////////////

		// コンパレータで並べ替える。
		Collections.sort( bookInfo, comparator );
		
		//////////////////////////////////////////////////////////
		// 本の情報に投稿日時を設定する。
		// 先頭の本が一番新しい日時に、後ろの本ほど古い日時になるように設定する。
		//////////////////////////////////////////////////////////
		
		// 現在日時より、年・月・日・時を決める。
		Date now = new Date();
		String year = new SimpleDateFormat( "yyyy" ).format( now );
		String month = new SimpleDateFormat( "MM" ).format( now );
		String day = new SimpleDateFormat( "dd" ).format( now );
		String hour = new SimpleDateFormat( "HH" ).format( now );
		
		// 分・秒用のフォーマット。
		DecimalFormat df = new DecimalFormat( "00" );
		
		int s = 0;
		for ( int i = bookInfo.size() - 1; i >= 0; --i ) {
			Map<String, String> info = bookInfo.get( i );
			
			// 本ごとに分・秒を決定する。
			// ※3600冊を超えるとうまくいかない。
			String minute = df.format( s / 60 );
			String second = df.format( s % 60 );
			
			// 投光日時を設定する。
			info.put( "e_year", year );
			info.put( "e_month", month );
			info.put( "e_day", day );
			info.put( "e_hour",  hour );
			info.put( "e_minute", minute );
			info.put( "e_second", second );
			
			s = s + 1;
		}
		
		//////////////////////////////////////////////////////////
		// 本の情報を更新する。
		//////////////////////////////////////////////////////////

		for ( int i = 0; i < bookInfo.size(); ++i ) {
			Map<String, String> info = bookInfo.get( i );
			BooklogAccess.updateBookWithEditdate( cookie, info );
		}
		

		System.out.println( "ブクログの情報の並べ替えを終了しました。" );

	}
		
	/**
	 * ユーザの入力値をint型で取得する。
	 * @param prompt 表示するプロンプト
	 * @return 入力値
	 */
	private static int getInputInt( String prompt ) throws Exception {
		String line = getInput( prompt );

		try {
			return Integer.parseInt( line );
		} catch ( NumberFormatException e ) {
			return -1;
		}
	}
	
	/**
	 * ユーザの入力値を取得する。
	 * @param prompt 表示するプロンプト
	 * @return 入力値
	 */
	private static String getInput( String prompt ) throws Exception {
		System.out.print( prompt );
		BufferedReader reader = new BufferedReader( new InputStreamReader( System.in ) );
		return reader.readLine();
	}

}
BooklogAccess.java
import java.net.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;

/**
 * ブクログ接続
 * @author nattou_curry
 */
public class BooklogAccess {

	// ブクログのエンコード
	private static final String ENCODING = "EUC-JP";
	
	/**
	 * クッキーを取得する。
	 * @reurn クッキー
	 */
	public static String getCookie() throws Exception {
		
		// トップ画面への接続を開く。
		HttpURLConnection conn = openConnection( "/" );
		
		// クッキーを取得する。
		String cookie = conn.getHeaderField( "Set-Cookie" );
		
		// 接続を閉じる。
		closeConnection( conn );
		
		return cookie;
	}

	/**
	 * ログインする。
	 * @param userid ユーザID
	 * @param password パスワード
	 */
	public static void login( String cookie, String userid, String password ) throws Exception {
		
		// ログイン画面への接続を開く。
		HttpURLConnection conn = openConnection( "/uhome.php", cookie );
		
		// ログイン情報を送信する。
		String query = "account=" + userid + "&pw=" + password + "&st=1";
		conn.setDoOutput( true );
		conn.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
		conn.setRequestProperty( "Content-Length", query.length() + "" );
		OutputStreamWriter out = new OutputStreamWriter( conn.getOutputStream(), ENCODING );
		out.write( query );
		out.flush();
		out.close();

		// 接続を閉じる。
		closeConnection( conn );
	}
	
	/**
	 * ログイン情報を取得する。
	 * @param cookie クッキー
	 * @param ログイン情報を設定したマップ。username: ユーザ名。
	 * @return ログイン情報
	 */
	public static Map<String, String> getLoginInfo( String cookie ) throws Exception {
		
		// トップ画面への接続を開く。
		HttpURLConnection conn = openConnection( "/", cookie );
	
		// トップ画面のHTMLを取得する。
		String html = getHTML( conn );
		
		// 接続を閉じる。
		closeConnection( conn );
		
		// HTMLからログイン情報を見つける。
		Pattern p = Pattern.compile( "<a href=\"/setup.php\">ようこそ([^<]*)さん</a>" );
		Matcher m = p.matcher( html );
		if ( ! m.find() ) {
			// ログイン情報が見つからない:
			throw new Exception( "ログインしていません。" );
		}
		
		// ログイン情報をマップに設定する。
		Map<String, String> loginInfo = new HashMap<String, String>();
		loginInfo.put( "username", m.group( 1 ) );
		
		return loginInfo;
	}

	/**
	 * 本の一覧を取得する。
	 * @param cookie クッキー
	 * @return 本(isbn)の一覧
	 */ 
	public static String[] getBookList( String cookie ) throws Exception {
		
		List<String> itemList = new ArrayList<String>();	// 本(isbn)の一覧
		int page = 0;	// ページ番号
		
		Pattern p = Pattern.compile( "<td class=\"cell2\"><a href=\"http://booklog.jp/users/[^/]*/archives/([^\"]*)\" target=\"_blank\"><img src=\"[^\"]*\" width=\"50\"></a></td>" );

		// 「本の編集・削除画面」の全ページについて、以下を繰り返す。
		while ( true ) {
			// 本の編集・削除画面に接続する。
			HttpURLConnection conn = openConnection(  "/editbook.php?page=" + page, cookie );

			// 本の編集・削除画面のHTMLを取得する。
			String html = getHTML( conn );

			// 接続を閉じる。
			closeConnection( conn );
		
			// 本の情報を取得する。
			Matcher m = p.matcher( html );
			if ( !m.find() ) {
				break;
			}
			do {
				// 本(isbn)を一覧に追加する。
				itemList.add( m.group( 1 ) );
			} while ( m.find() );
		
			// 次のページに進む。
			++page;
		}
		
		return itemList.toArray( new String[0] );
	}
	
	/**
	 * 本の情報を取得する。
	 * @param cookie クッキー
	 * @param isbn 本のisbn
	 * @return 本の情報。title: タイトル、pubDate: 出版日、saleRank: 売上ランキング、comment、
	 *                   cate: カテゴリー、rank: ランク、comment: コメント、
	 *                   e_year: 年、e_month: 月、e_day: 日、e_hour: 時、e_minute: 分、e_second: 秒。
	 */ 
	public static Map<String, String> getBookInfo( String cookie, String isbn ) throws Exception {

		// 本の画面に接続する。
		HttpURLConnection conn = openConnection( "/addbook.php?mode=ItemLookup&asin=" + isbn, cookie );
	
		// 本の画面からHTMLを取得する。
		String html = getHTML( conn );

		// 接続を閉じる。
		closeConnection( conn );
		
		//////////////////////////////////////////////////////////////////
		// HTMLから本の情報を取得する。
		//////////////////////////////////////////////////////////////////
		
		String tmp;
		// タイトル
		String title = findGroup( html, "<h3 style=\"margin:5px;padding-bottom:0px;\">([^<]*)</h3>" );
		// 出版日
		String pubDate = findGroup( html, "<h3 style=\"margin:5px;padding-bottom:0px;\">[^<]*</h3>.*?<br />.*?\\(([^)]*)\\)<br />" );
		// 売上ランキング
		String saleRank = findGroup( html, "売り上げランキング:([^<]*)<br />" );
		// カテゴリ
		tmp = findGroup( html, "<select name=\"cate\" id=\"cate\">(.*?)</select>" );
		String cate = findGroup( tmp, "<option value=\"([^\"]*)\" selected>" );
		// ランク
		tmp = findGroup( html, "<select name=\"rank\">(.*?)</select>" );
		String rank = findGroup( tmp, "<option value=\"([^\"]*)\" selected>" );
		// コメント
		String comment = findGroup( html, "<textarea name=\"comment\" id=\"comment\" style=\"width: 100%; height: 200px;\">([^<]*)</textarea>" );
		// 年
		String e_year = findGroup( html, "<input type=\"text\" size=\"4\" maxlength=\"4\" style=\"width:4em;\" value=\"([^\"]*)\" id=\"e_year\" name=\"e_year\" />年" );
		// 月
		String e_month = findGroup( html, "<input type=\"text\" size=\"2\" maxlength=\"2\" style=\"width:2em;\" value=\"([^\"]*)\" id=\"e_month\" name=\"e_month\" />月" );
		// 日
		String e_day = findGroup( html, "<input type=\"text\" size=\"2\" maxlength=\"2\" style=\"width:2em;\" value=\"([^\"]*)\" id=\"e_day\" name=\"e_day\" />日" );
		// 時
		String e_hour = findGroup( html, "<input type=\"text\" size=\"2\" maxlength=\"2\" style=\"width:2em;\" value=\"([^\"]*)\" id=\"e_hour\" name=\"e_hour\" />時" );
		// 分
		String e_minute = findGroup( html, "<input type=\"text\" size=\"2\" maxlength=\"2\" style=\"width:2em;\" value=\"([^\"]*)\" id=\"e_minute\" name=\"e_minute\" />分" );
		// 秒。
		String e_second = findGroup( html, "<input type=\"text\" size=\"2\" maxlength=\"2\" style=\"width:2em;\" value=\"([^\"]*)\" id=\"e_second\" name=\"e_second\" />秒" );

		// 本の情報をマップに格納する。
		Map<String, String> info = new HashMap<String, String>();
		info.put( "isbn", isbn );
		info.put( "title", title );
		info.put( "pubDate", pubDate );
		info.put( "saleRank", saleRank );
		info.put( "cate", cate );
		info.put( "rank", rank );
		info.put( "comment", comment );
		info.put( "e_year", e_year );
		info.put( "e_month", e_month );
		info.put( "e_day", e_day );
		info.put( "e_hour", e_hour );
		info.put( "e_minute", e_minute );
		info.put( "e_second", e_second );
		
		return info;
	}
	
	/**
	 * 本を更新する。
	 * @param cookie クッキー
	 * @book 本の情報
	 * @comment コメント
	 */
	public static void updateBook( String cookie, Map<String, String> book ) throws Exception {
		
		String isbn = book.get( "isbn" );
		String cate = book.get( "cate" );
		String rank = book.get( "rank" );
		String comment = book.get( "comment" );

		// 本の更新画面に接続する。
		HttpURLConnection conn = openConnection( "/addbook.php?searchindex=&keywords=&mode=ItemLookup&pages=&asin=" + isbn, cookie );

		// 本の更新情報を送信する。
		String query = "cate=" + cate + "&cate_name=&cate_info=&rank=" + rank + "&comment=" + comment + "&st=1&book_asin=" + isbn;
		conn.setDoOutput( true );
		conn.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
		conn.setRequestProperty( "Content-Length", query.length() + "" );
		OutputStreamWriter out = new OutputStreamWriter( conn.getOutputStream(), ENCODING );
		out.write( query );
		out.flush();
		out.close();

		// 接続を閉じる。
		closeConnection( conn );
	}

	/**
	 * 本を更新する。更新時に投稿日時も更新する。
	 * @param cookie クッキー
	 * @book 本の情報
	 * @comment コメント
	 */
	public static void updateBookWithEditdate( String cookie, Map<String, String> book ) throws Exception {
		
		String isbn = book.get( "isbn" );
		String cate = book.get( "cate" );
		String rank = book.get( "rank" );
		String comment = book.get( "comment" );
		String e_year = book.get( "e_year" );
		String e_month = book.get( "e_month" );
		String e_day = book.get( "e_day" );
		String e_hour = book.get( "e_hour" );
		String e_minute = book.get( "e_minute" );
		String e_second = book.get( "e_second" );

		// 本の更新画面に接続する。
		HttpURLConnection conn = openConnection( "/addbook.php?searchindex=&keywords=&mode=ItemLookup&pages=&asin=" + isbn, cookie );

		// 本の更新情報を送信する。
		String query = "cate=" + cate + "&cate_name=&cate_info=&rank=" + rank + "&comment=" + comment + "&editdate=on&e_year=" + e_year + "&e_month=" + e_month + "&e_day=" + e_day + "&e_hour=" + e_hour + "&e_minute=" + e_minute + "&e_second=" + e_second + "&st=1&book_asin=" + isbn;
		conn.setDoOutput( true );
		conn.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
		conn.setRequestProperty( "Content-Length", query.length() + "" );
		OutputStreamWriter out = new OutputStreamWriter( conn.getOutputStream(), ENCODING );
		out.write( query );
		out.flush();
		out.close();

		// 接続を閉じる。
		closeConnection( conn );
	}

	/**
	 * ブクログへの接続を開く。
	 * @param requestURI リクエストURI
	 * @return 接続
	 */
	public static HttpURLConnection openConnection( String requestURI ) throws Exception {
		return openConnection( requestURI, null );
	}
	
	/**
	 * ブクログへの接続を開く。
	 * @param requestURI リクエストURI
	 * @param cookie クッキー
	 */
	public static HttpURLConnection openConnection( String requestURI, String cookie ) throws Exception {
		URL url = new URL( "http://booklog.jp" + requestURI );
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setInstanceFollowRedirects( false );
		conn.setRequestProperty( "Host", "booklog.jp" );
		conn.setRequestProperty( "User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6" );
		conn.setRequestProperty( "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" );
		conn.setRequestProperty( "Accept-Language", "ja,en-us;q=0.7,en;q=0.3" );
		conn.setRequestProperty( "Accept-Encoding", "gzip,deflate" );
		conn.setRequestProperty( "Accept-Charset", "Shift_JIS,utf-8;q=0.7,*;q=0.7" );
		conn.setRequestProperty( "Keep-Alive", "300" );
		conn.setRequestProperty( "Connection", "keep-alive" );
		conn.setRequestProperty( "Referer", "http://booklog.jp" );
		if ( cookie != null ) {
			conn.setRequestProperty( "Cookie", cookie );
		}
		
		return conn;
	}
	
	/**
	 * ブクログへの接続を切断する。
	 * @param conn 接続
	 */
	public static void closeConnection( HttpURLConnection conn ) {
		try {
			OutputStream out = conn.getOutputStream();
			out.close();
		} catch ( Exception e ) {}

		try {
			InputStream in = conn.getInputStream();
			in.close();
		} catch ( Exception e ) {}
	}
	
	/**
	 * 接続先のHTMLを取得する。
	 * @param conn 接続
	 * @return HTML
	 */
	public static String getHTML( HttpURLConnection conn ) throws Exception {
		BufferedReader in = new BufferedReader(
			new InputStreamReader( conn.getInputStream(), ENCODING ) );
		StringBuffer buf = new StringBuffer();

		int c;
		while ( ( c = in.read() ) != -1 ) {
			buf.append( (char) c );
		}
		in.close();
		
		return buf.toString();
	}

	/**
	 * 入力文字列中で、正規表現の一番目の括弧(グループ)にマッチする部分文字列を返す。
	 * @param regex 正規表現
	 * @param input 入力文字列
	 * @return マッチする部分文字列。マッチしない場合、null。
	 */
	public static String findGroup( String input, String regex ) {
		// 正規表現によるマッチングを行う。
		Pattern p = Pattern.compile( regex, Pattern.DOTALL );
		Matcher m = p.matcher( input );
		if ( ! m.find() ) {
			// マッチしない:
			return null;
		}
		
		// 一番目の括弧(グループ)にマッチする部分文字列を取得する。
		return m.group( 1 );
	}
}

注意

本プログラムに利用により、データ損失など何らかの問題が出た場合にも、当方では一切責任を持てません。個人の責任の範囲でご利用ください。