wordpressからbloggerへ画像つきで引っ越したい。

要約

  • blog引っ越すとき本文はexport / importできるけど画像は困るよね
  • 画像は別途DLしてきて引越し先の画像共有サービスにうpしなおせばよくね?
  • ツール作って実際にやってみたよ
  • せっかくなのでソースとか残しとくよ
以下詳細+ソースコード



背景・動機

2004年あたりから引越しを繰り返しながらひっそり続けてきたブログも twitter / Facebook に拠点を移して以来さっぱり更新しなくなり、いい加減サーバ代金払うのもしんどくなってきた。けど消すのもなんか忍びないので、せめて無料サーバに移行して解約したいなぁ・・。てのが動機。
ドメインは変えたくないので、以下の条件でブログサービスを探してみた。

  • 無料・広告なし
  • 独自ドメイン受け入れ
  • フォトシェアサービスとの連携が可能
  • Wordpressからのimport可能
  • デザインテンプレートあり
  • 携帯orスマートフォンテンプレートあり
で、結局blogger一択。


条件

  • 移転元:Wordpress
  • 移転先:Blogger
  • 画像共有サービス:Picasa
  • 記事件数:約3000件
  • 写真点数:約4500枚


引越し作業方針

  1. exportした記事ファイルから画像のurlを抜き出す
  2. 抜き出したurlの画像をDLし、Picasaにアップロードし、PicasaでのURLを記録
  3. exportファイルの元画像urlをPicasaのURLに置換
  4. bloggerにimport

制限等

  • Picasaの1アルバム1000枚の制限を考慮する
  • Picasaの連続アップロード規制を考慮する
  • Picasaのアルバムをプログラムから生成・管理すると(中断・再開等が)大変なので事前に手動で作成し、あらかじめalbumIDを取得しておく
  • export元の記事ファイルのurlはフルパスを前提にする
  • 画像ファイルはjpg,jpegのみを対象とする
  • エラー処理は極力簡素化し、通信エラー等の発生

ツール仕様

上記1,2,3に必要なツールを作ってみた。

  • 1. urlpicker.pl
exportしたxmlから単純に正規表現で画像urlを引っ掛けるだけ。
csvでファイルに出力する際に1000件ごとに別ファイルを作成する。
#! /usr/bin/perl
#

if(($ARGV[0] eq "")or($ARGV[1] eq "")){
 print "usage: pickjpg.pl inputfile outputfile\n";
 exit(-1);
}
if(!(-e $ARGV[0])){
 print "file not found\n";
 exit(-1);
}

$inputfile = $ARGV[0];
$outputfile = $ARGV[1];

open(INFILE, "< $inputfile") or die "cannot open file :$!\n";

%jpghash;
while(){
 chomp($_);
 while($_ =~ /(s?https?:\/\/[-_\.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)/g){
  my $tmp = $1;
  if($tmp =~ /\.jpg$|\.jpeg$/i){
      $jpghash{$tmp} = "";
  }
 }
}
close(INFILE);

$count = 0;
$n = 0;
$of = $outputfile.$n;
open(OFILE, "> $of") or die "cannot open file :$!\n";

foreach $jpg (keys %jpghash){
    if($count > 999){
       $count = 0;
       $n++;
       $of = $outputfile.$n;
       close(OFILE);
       open(OFILE, "> $of") or die "cannot open file :$!\n";
       next;
    }
    print OFILE "$jpg,\n";
}
close(OFILE);

  • 2. UploadPicasa
1.で出力したcsvファイルを読み込み、指定されたアカウントの指定されたアルバムに逐次アップロードする
アップロード完了したファイルのPicasaURLをcsvの対応する行に書き込んでいく。
後述の規制に対応するため200ファイルアップロードごとに10分間のインターバルを設けている。
package jp.go1.blogconv2;

import java.io.IOException;

import org.jargp.ArgumentProcessor;
import org.jargp.ParameterDef;
import org.jargp.IntDef;
import org.jargp.BoolDef;
import org.jargp.StringDef;

import com.google.gdata.util.ServiceException;

public class BlogConv {
    private static String inputFile = "";
    private static String outputFile = "";
    private static String albumId = "";
    private static String user = "";
    private static String userId = "";
    private static String passwd = "";       

    private static ParameterDef[] parameterDefs = {
        new StringDef('i', "inputFile", "input File name"),
        new StringDef('o', "outputFile", "output File name"),
        new StringDef('u', "user", "user name"),
        new StringDef('d', "userId", "user id"),
        new StringDef('p', "passwd", "password"),
        new StringDef('a', "albumId", "album id")
    };
    
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int totalCount = 0;
        int uploadCount = 0;
        int c = 0;
        
        BlogConv application = new BlogConv();        
        ImgFileList fl = new ImgFileList();

        try {
            ArgumentProcessor.processArgs(args, parameterDefs, application);
   
            UploadPicasa p = new UploadPicasa(user, userId, passwd, albumId);
            fl.readList(inputFile);
            
            for(UrlSet us : fl.getUsList()){
                totalCount++;
                if(c >= 200){
                    System.out.println("Uploaded : " + uploadCount + "/" + totalCount);
                    System.out.println("waiting.. ");
                    Thread.sleep(600000);
                    c = 0;
                }
                if(us.getPicasaUrl() == null){
                    String s = p.uploadFromFile(us.getBlogUrl());
                    if(s != null){
                        uploadCount++;
                        c++;
                        us.setPicasaUrl(s);
                        System.out.println("Uploaded : " + s);
                    }
                }
            }

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ServiceException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            System.out.println("Uploaded : " + uploadCount + "/" + totalCount);
            try {
                fl.writeList(outputFile);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
package jp.go1.blogconv2;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import com.google.gdata.client.photos.*;
import com.google.gdata.data.*;
import com.google.gdata.data.media.*;
import com.google.gdata.data.photos.*;
import com.google.gdata.util.ServiceException;

public class UploadPicasa { 
    private static String user;
    private static String userId;
    private static String passwd;
    private static String albumId;
  
    public UploadPicasa() throws MalformedURLException{
    }

    public UploadPicasa(String u, String d, String p, String a) throws MalformedURLException{
      this.user = u;
      this.userId = d;
      this.passwd = p;
      this.albumId = a;
    }
 public String uploadFromUrl(String srcUrl) throws IOException, ServiceException{
        PicasawebService service = new PicasawebService("service1");
        service.setUserCredentials(user, passwd);
        URL dst = new URL("https://picasaweb.google.com/data/feed/api/user/" + userId + "/albumid/" + albumId);
     
     URL src = new URL(srcUrl);
     URLConnection conn = src.openConnection();
     InputStream in = conn.getInputStream();

        PhotoEntry photo = new PhotoEntry();
        photo.setTitle(new PlainTextConstruct(srcUrl.substring(srcUrl.lastIndexOf("/")+1,srcUrl.length())));
        photo.setClient("clientName");
        byte[] byteArray = IOUtils.toByteArray(in);
         
        MediaByteArraySource media = new MediaByteArraySource(byteArray, "image/jpeg");
        photo.setMediaSource(media);
        
        PhotoEntry entry = service.insert(dst, photo);
        String uploadUrl = entry.getMediaContents().get(0).getUrl();
        return uploadUrl;
 }
}
package jp.go1.blogconv2;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;

public class ImgFileList {
    public ArrayList usList;

    public ArrayList getUsList() {
        return usList;
    }

    public void setUsList(ArrayList usList) {
        this.usList = usList;
    }
    
    public void addUrlSet(UrlSet us){
        this.usList.add(us);
    }

    public ImgFileList(){
    }
    
    public void readList(String fileName) throws IOException{
        File csv = new File(fileName); // CSVデータファイル
        usList = new ArrayList();

        BufferedReader br = new BufferedReader(new FileReader(csv));

        // 最終行まで読み込む
        String line = "";
        while ((line = br.readLine()) != null) {

          // 1行をデータの要素に分割
          StringTokenizer st = new StringTokenizer(line, ",");
          UrlSet us = new UrlSet();
          if(st.hasMoreTokens()){
              String s = st.nextToken();
              if(s.equals("null")){
              }else{
                  us.setBlogUrl(s);
              }
          }
          if(st.hasMoreTokens()){
              String s = st.nextToken();
              if(s.equals("null")){
              }else{
                  us.setPicasaUrl(s);
              }
          }
          this.addUrlSet(us);

        }
        br.close();   
    }
    
    public void writeList(String fileName) throws IOException{
        File csv = new File(fileName); // CSVデータファイル
        // 追記モード
        BufferedWriter bw = new BufferedWriter(new FileWriter(csv, true));
        // 新たなデータ行の追加
        for(UrlSet us : usList){
            bw.write(us.getBlogUrl() +  "," + us.getPicasaUrl());
            bw.newLine();
        }
        bw.close();        
    }    
    
}
package jp.go1.blogconv2;

public class UrlSet {
    public String blogUrl;
    public String picasaUrl;

    public UrlSet(){
        this.blogUrl = null;
        this.picasaUrl = null;
    }
    
    public UrlSet(String blog, String picasa){
        this.blogUrl = blog;
        this.picasaUrl = picasa;
    }    
    
    public String getBlogUrl() {
        return blogUrl;
    }
    public void setBlogUrl(String blogUrl) {
        this.blogUrl = blogUrl;
    }
    public String getPicasaUrl() {
        return picasaUrl;
    }
    public void setPicasaUrl(String picasaUrl) {
        this.picasaUrl = picasaUrl;
    }
 }

  • 3. urlreplacer.pl
2.で完成したcsvで単純に置換するだけ。
#! /usr/bin/perl
#

if(($ARGV[0] eq "")or($ARGV[1] eq "")){
 print "usage: convjpg.pl jpglist inputfile outputfile\n";
 exit(-1);
}
if(!(-e $ARGV[0])){
 print "file not found\n";
 exit(-1);
}

$jpglist = $ARGV[0];
$inputfile = $ARGV[1];
$outputfile = $ARGV[2];

%jpghash;

open(JPGLISTFILE, "< $jpglist") or die "cannot open file :$!\n";
while(){
    chomp($_);
    my @a = split(/,/,$_);
    $jpghash{$a[0]} = $a[1];
}
close(JPGLISTFILE);
print "\n";

$found = 0;
$notfount = 0;
$alldata = "";

open(INFILE, "< $inputfile") or die "cannot open file :$!\n";
while(){
    $alldata .= $_;
}
close(INFILE);
foreach $jpgurl (keys %jpghash){
    $alldata =~ s/$jpgurl/$jpghash{$jpgurl}/g;
}

open(OFILE, "> $outputfile") or die "cannot open file :$!\n";
print OFILE $alldata;
close(OFILE);

やってみた

  • wordpress2blogger
  • urlpicker.pl
  • UploadPicasa.jar
  • urlreplacer.pl
で。たまにCaptchaRequiredExceptionやBadGateway、Internal Server Errorを食らいながらのんびり4時間ほどでアップロード完了。見ての通り無事importできている模様。
試行錯誤中にわかったんだけど連続で200ファイル程度をアップロードするとChaptchaを要求されるという規制があるようだ。

所感

  • GAEの日本語ドキュメント少ない・・。
  • Java初めて使ったけどeclipseこんなに親切丁寧なのね。
  • 載せてないけどJUnitも使ってみた。便利。
  • ていうか公式ツールでこんなんあればいいのに。


QA

Q: なんでperl?
A: 大昔に作ったperlでurl抜き出す正規表現が残ってたから

Q: なんでjava?
A: perlで全部書こうとして挫折してちょっと新しい言語やってみたかった

Q: なんで全自動じゃないの?
A: 扱うファイル増えるしalbumの状態管理せなならんくなるし通信エラー見て再送せなんなるしどうせ一度しか使わないし。

Q: セキュリティry
A: 自家用ツールなのでry

コメント

このブログの人気の投稿

iOS 4.0