読者です 読者をやめる 読者になる 読者になる

たまには日記の一つでも。

28にしてはじめたバイオリンの記録と、ときどき日曜ハッキング

auoneブログからはてなダイアリーへ引っ越し の続き


完了。はてなダイアリーはてなフォトライフの投稿をAtomでがんばってみた。結局、Abderaはなんだかいうことを聞いてくれないので早々に捨てた。AuoneブログのRSS処理はROMEを使用し、はてなへのAtomのPOSTは生XMLApache HttpClientを直接蹴って送信。
ちなみに、Apache HttpClientは4.0.1を使用。ぐぐって出てくるサンプルは3.x系が多いが、どうも細かいところがちょいちょい違うみたい。
それにしても・・・久々にコーディングしたなぁ・・・


AuoneBlogExporter.java

なんてことはない。ROMEを使ってRSS経由でAuoneブログの全エントリを取得。

import java.util.List;
import java.util.Date;
import java.util.ArrayList;
import java.net.URL;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

public class AuoneBlogExporter {
	private static final String FEED_URL
		= "http://blog.auone.jp/igarashitm/?disp=rss&pageID=";
	
	public List<SyndEntry> fetchAll() throws Exception {
		SyndFeedInput sfi = new SyndFeedInput();
		String prevTitle = null;
		Date prevDate = null;
		int page = 1;
		List<SyndEntry> allEntries = new ArrayList<SyndEntry>();
		while(true) { 
			SyndFeed sf = sfi.build(new XmlReader(new URL(FEED_URL+page)));
			@SuppressWarnings("unchecked")
			List<SyndEntry> entries = sf.getEntries();
			SyndEntry topEntry = entries.get(0);
			if( topEntry.getTitle().equals(prevTitle)
					&& topEntry.getPublishedDate().equals(prevDate) ) {
				// auoneブログでは(他は知らね)、最終ページより大きいページを指定しても
				// 最終ページが取得されるので、 これを利用して最終ページを判定する。
				break;
			}else {
				prevTitle = topEntry.getTitle();
				prevDate = topEntry.getPublishedDate();
				allEntries.addAll(entries);
				page++;
			}
		}
		return allEntries;
	}
}

HatenaDiaryImporter.java

postEntry()とpostPhoto()がキモ。それぞれ、はてなダイアリーはてなフォトライフにAtomでPOSTする。なお、サブルーチンのgenerateWsseHeader()は、yohei-y:weblog: 2005-04こちらを参考にさせていただきました。というかまんまぱちってきました。こんなところで恐縮ですがありがとうございます。

//import java.util.Date;
import java.util.List;
import java.util.Calendar;
import java.util.TimeZone;
import java.text.SimpleDateFormat;
import java.io.InputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.security.MessageDigest;
import java.security.SecureRandom;

import org.jdom.Element;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;

public class HatenaDiaryImporter {
	private final static String D_ATOM_BASE_URL
		= "http://d.hatena.ne.jp/igarashitm/atom/blog";
	private final static String F_ATOM_POST_URL
		= "http://f.hatena.ne.jp/atom/post";
	private static final String USERNAME = "hoge";
	private static final String PASSWORD = "fuga";
	private DefaultHttpClient httpClient;
	
	public HatenaDiaryImporter() {
		this.httpClient = new DefaultHttpClient();
	}
	
	public void postEntry( String _title, String _content, String _updated) throws Exception {
		StringBuilder outxml = new StringBuilder();
		outxml.append("<entry xmlns=\"http://www.w3.org/2005/Atom\">\r\n");
		outxml.append("<title>");
		outxml.append(_title);
		outxml.append("</title>\r\n");
		outxml.append("<content type=\"text\">");
		outxml.append(_content);
		outxml.append("</content>\r\n");
		outxml.append("<updated>");
		outxml.append(_updated);
		outxml.append("</updated>\r\n");
		outxml.append("</entry>");
				
		HttpPost post = new HttpPost(D_ATOM_BASE_URL);
		post.addHeader("X-Wsse", generateWsseHeader(USERNAME,PASSWORD));
		StringEntity entity = new StringEntity(outxml.toString(),"UTF-8");
		post.setEntity(entity);
		HttpResponse res = httpClient.execute(post);
		
		System.out.println("post entry["+_title+"] >> " + res.getStatusLine().toString());
		if( res.getStatusLine().getStatusCode() >= 400 ) {
			System.err.println(outxml.toString());
		}
		res.getEntity().getContent().close();
	}

	public String postPhoto( String _title, String _photoUrl ) throws Exception {
		String base64encoded = null;
		HttpGet get = new HttpGet(_photoUrl);
		HttpResponse res = httpClient.execute(get);
		System.out.println("get img["+_photoUrl+"] >> " + res.getStatusLine().toString());
		System.out.println("get img >> " + res.getEntity().getContentType());
		InputStream in = res.getEntity().getContent();
		byte[] bufB = new byte[1024], imgB = new byte[0];
		int count=0, total=0;
		while( (count = in.read(bufB)) != -1 ) {
			total += count;
			byte[] newImgB = new byte[total];
			System.arraycopy(imgB, 0, newImgB, 0, imgB.length);
			System.arraycopy(bufB, 0, newImgB, imgB.length, count);
			imgB = newImgB;
		}
		in.close();
		base64encoded = new String(Base64.encodeBase64(imgB));
		
		StringBuilder outxml = new StringBuilder();
		outxml.append("<entry xmlns=\"http://purl.org/atom/ns#\">\r\n");
		outxml.append("<title>");
		outxml.append(_title);
		outxml.append("</title>\r\n");
		outxml.append("<content mode=\"base64\" type=\"");
		outxml.append(res.getEntity().getContentType().getValue());
		outxml.append("\">");
		outxml.append(base64encoded);
		outxml.append("</content>\r\n");
		outxml.append("</entry>\r\n");

		HttpPost post = new HttpPost(F_ATOM_POST_URL);
		post.addHeader("X-Wsse", generateWsseHeader(USERNAME,PASSWORD));
		StringEntity entity = new StringEntity(outxml.toString(),"UTF-8");
		post.setEntity(entity);
		res = httpClient.execute(post);
		System.out.println("post img >> " + res.getStatusLine().toString());
		if( res.getStatusLine().getStatusCode() >= 400 ) {
			System.err.println(outxml.toString());
		}
		
		in = res.getEntity().getContent();
		SyndFeedInput sfi = new SyndFeedInput();
		SyndFeed sf = sfi.build(new XmlReader(in));
		//System.out.println("post img >> " + sf.toString());
		in.close();
		
		@SuppressWarnings("unchecked")
		List<Element> fm = (List<Element>)sf.getForeignMarkup();
		String output = null;
		for( Element e : fm ) {
			if( e.getName().equals("syntax")) {
				output = e.getText();
			}
		}
		return output;
	}
	
	private String generateWsseHeader( String _username, String _password ) 
			throws Exception {
		byte[] nonceB = new byte[8];
		SecureRandom.getInstance("SHA1PRNG").nextBytes(nonceB);
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
		sdf.setTimeZone(TimeZone.getTimeZone("JST"));
		Calendar now = Calendar.getInstance();
		now.setTimeInMillis(System.currentTimeMillis());
		String created = sdf.format(now.getTime());
		byte[] createdB = created.getBytes("utf-8");
		byte[] passwordB = _password.getBytes("utf-8");
	    
		byte[] v = new byte[nonceB.length + createdB.length + passwordB.length];
		System.arraycopy(nonceB, 0, v, 0, nonceB.length);
		System.arraycopy(createdB, 0, v, nonceB.length, createdB.length);
		System.arraycopy(passwordB, 0, v, nonceB.length + createdB.length,
				passwordB.length);

		MessageDigest md = MessageDigest.getInstance("SHA1");
		md.update(v);
		byte[] digest = md.digest();

		StringBuilder wsse = new StringBuilder();
		wsse.append("UsernameToken Username=\"");
		wsse.append(_username);
		wsse.append("\", PasswordDigest=\"");
		wsse.append(new String(Base64.encodeBase64(digest)));
		wsse.append("\", Nonce=\"");
		wsse.append(new String(Base64.encodeBase64(nonceB)));
		wsse.append("\", Created=\"");
		wsse.append(created);
		wsse.append('"');
		return wsse.toString();
	}
}

MoveAuoneToHatena.java

こんなもん貼っておいても誰も喜ばないとは思うけど・・・タイトルとか本文の整形をしている、泥臭いとこ。上の二つも決して綺麗なものではないけども。

import java.util.List;
import java.util.TimeZone;
import java.util.regex.Pattern;
import java.text.SimpleDateFormat;

import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.module.DCModuleImpl;

public class MoveAuoneToHatena {
	public void execute() throws Exception {
		AuoneBlogExporter auone = new AuoneBlogExporter();
		List<SyndEntry> allEntries = auone.fetchAll();
		HatenaDiaryImporter hatena = new HatenaDiaryImporter();
		SimpleDateFormat photoSdf = new SimpleDateFormat("yyyyMMddHHmmss");
		photoSdf.setTimeZone(TimeZone.getTimeZone("JST"));
		SimpleDateFormat entrySdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
		entrySdf.setTimeZone(TimeZone.getTimeZone("JST"));
		for( SyndEntry entry : allEntries ) {
			// auoneブログでマイカテゴリとかいってたやつはここに入っている
			String category = ((DCModuleImpl)entry.getModules().get(0)).getSubject().getValue(); 
			// カテゴリ無指定は" no data "が入っている。大したこと書いてないので無視
			if( category.equals(" no data ") ) continue;
			
			StringBuilder title = new StringBuilder();
			title.append("[");
			title.append(category);
			title.append("]");
			if( category.equals("バイオリン") &&
					( entry.getTitle().matches(".*Lesson.*")
							|| entry.getTitle().matches(".*指摘事項.*") )) {
				title.append("[レッスン記録]");
			}
			title.append(" ");
			title.append(entry.getTitle().replace("\t", " "));
			entry.setTitle(title.toString());

			// 一応、Auoneの元記事へのリンクでも貼っておこうかな。
			StringBuilder desc = new StringBuilder();
			desc.append("***この記事は、こちらの元記事から引っ越しました →→→ [");
			desc.append(entry.getLink());
			desc.append("] \r\n \r\n \r\n");
			
			// 貧弱なauoneブログで全角スペースやらを駆使して整形していたやつを、
			// 可能な限りはてな記法に置き換える泥臭い仕事。
			String newDesc = entry.getDescription().getValue();
			newDesc = newDesc.replaceAll("<[bB][rR]>", "\r\n")
								.replaceAll("<[bB][rR] />", "")
								.replaceAll("&#160;", " ")
								.replaceAll("\\[", "**")
								.replaceAll("\\]", "")
								.replaceAll(" ・", "-")
								.replaceAll(" ", "-");
			Pattern ptn = Pattern.compile("^・",Pattern.MULTILINE);
			newDesc = ptn.matcher(newDesc).replaceAll("-");
			ptn = Pattern.compile("^ -",Pattern.MULTILINE);
			newDesc = ptn.matcher(newDesc).replaceAll("-");
			ptn = Pattern.compile("^スズキ$",Pattern.MULTILINE);
			newDesc = ptn.matcher(newDesc).replaceAll("***スズキ");
			ptn = Pattern.compile("^ホーマン$",Pattern.MULTILINE);
			newDesc = ptn.matcher(newDesc).replaceAll("***ホーマン");
			ptn = Pattern.compile("^小野アンナ音階$",Pattern.MULTILINE);
			newDesc = ptn.matcher(newDesc).replaceAll("***小野アンナ音階");

			// 本文から画像とリンクを抽出。画像ははてなフォトライフへアップしてリンク。
			String datetimeStr = photoSdf.format(entry.getPublishedDate());
			int anchors=0, imgAnchors=0; 
			int start=0, end=0;
			while( (start = newDesc.indexOf("<a ")) != -1 ) {
				end = newDesc.indexOf("</a>", start)+4;
				String anchor = newDesc.substring(start, end);
				int imgstart=0, urlstart=0, urlend=0;
				if( (imgstart = anchor.indexOf("<img ")) != -1 ) {
					urlstart = anchor.indexOf("src='",imgstart)+5;
					urlend = anchor.indexOf("'",urlstart);
					String url = anchor.substring(urlstart,urlend)
								.replaceAll("&amp;", "&");
					String fLink = hatena.postPhoto(datetimeStr+"_"+imgAnchors, url);
					newDesc = newDesc.substring(0,start)
							+ "[" + fLink + "]\r\n"
							+ newDesc.substring(end);
					imgAnchors++;
				}else {
					urlstart = anchor.indexOf("href=\"")+6;
					urlend = anchor.indexOf("\"",urlstart);
					String url = anchor.substring(urlstart,urlend);
					newDesc = newDesc.substring(0,start)
							+ url + "\r\n"
							+ newDesc.substring(end);
					anchors++;
				}
			}
			desc.append(newDesc);
			
			String updatedStr = entrySdf.format(entry.getPublishedDate());
			hatena.postEntry(title.toString(), desc.toString(), updatedStr);
		}
	}
	
	public static void main(String[] args) throws Exception {
		MoveAuoneToHatena mah = new MoveAuoneToHatena();
		mah.execute();
	}
}