이전에는 구글의 블로거닷컴에 둥지를 틀고 블로그를 운영했는데요. 블로거닷컴이 여러 가지로 우리나라 인터넷 사용자의 요구를 반영하지 못하기에 호시탐탐 이사할 기회를 노리고 있었습니다. 블로거닷컴이 불편한 점의 예를 들자면, 태그는 달 수 있지만 카테고리 별 분류를 지원하지 않아서 게시물을 큰 단위로 묶는 게 안되고, 우리나라 블로그들이 즐겨 쓰는 트랙백도 안되고, 댓글 달기도 불편하게 되어있고, 미묘하게 느린데다 레이아웃이 큼지막해서 우리나라 사용자들이 좋아하는 스타일도 아니구요.
맘에 드는 이사처-_-를 찾지 못하고 시간을 보내다 텍스트큐브로 옮기기로 정하고 계정을 만든게 2009년 5월인가 그랬습니다. 근데 막상 이사하려고 보니 게시물을 옮겨오는게 안된다는 치명적인 문제점이 드러났습니다. 블로거닷컴이 게시물을 파일로 묶어주는 형식과 텍스트큐브가 게시물을 파일에서 복원하는 형식이 서로 호환되지 않아서 옮겨올 수가 없는 거였습니다. 새 글만 텍스트큐브쪽에 올리고 이전 글은 블로거닷컴쪽을 유지하는 방법도 있었지만, 그렇게 하기엔 좀 지저분한 느낌이고, 이전 글에는 얽힌 트랙백도 없는데다 댓글도 별로 없으니 아쉽지도 않아서 과감히 다 옮기기로 정했는데, 막상 이사가 안 되는 거에요. 그래서 일단 보류했죠.
그리고 또 몇 달이 지나 2010년 3월이 됐는데, 아무래도 이사를 해야 할 것 같아서 아예 제가 변환프로그램을 만들기로 결심을 한 겁니다. -_-
자세한 개발 과정을 적는건 좀 귀찮기도 하고(소스코드에 주석 한줄도 안 달았는데!!) 어차피 알아들을 사람만 알아들을테니 그냥 프로그램을 공개할까 합니다.
블로거닷컴은 Formatted ATOM 형식으로 블로그를 내보내거나 불러올 수 있고, 텍스트큐브는 TTXML 형식으로 블로그를 내보내거나 불러오게 되어 있습니다. 따라서 Formatted ATOM 형식으로 블로거닷컴 블로그를 받아온 후에 TTXML 형식으로 변환한 다음 텍스트큐브에서 읽어오면 블로그를 이사할 수 있게 됩니다. 제가 작성한 변환기는 Formatted ATOM을 TTXML로 변환합니다. 다만 게시물의 제목과 본문, 작성일만 변환하기 때문에 댓글이나 각종 설정은 모조리 날아가게 됩니다. 한 3시간 정도 걸려서 대충 만들었기 때문에 일어난 일로, 제대로 구조를 잡아 만들면 댓글도 변환할 수 있을 것 같지만, 제가 필요가 없어서 구현을 안 했습니다. -_-;;
구현하다 깨달은 건데, TTXML은 설계가 좀 이상한 것 같습니다. 게시물의 작성일을 UNIX Timestamp 형식으로 저장하는데, 이건 좀 아닌 것 같아요. 플랫폼에 종속적이지 않도록 XML을 사용하면서 UNIX Timestamp를 그대로 쓰다니..
여하튼 첨부파일로 프로그램의 소스코드와 실행파일을 공개합니다. Visual C# 2008 Express에서 작성했으므로 빌드 시에는 Visual C# 2008 이상의 버전이 필요합니다. 수정할 필요 없이 사용만 하고 싶다면 첨부파일에 들어있는 실행파일을 실행해보시고, 안되면
Microsoft .NET Framework 3.5 SP1을 깔면 실행이 가능합니다. 실행하면 이렇게 나옵니다.
보시다시피 간단합니다. 블로거닷컴에서 생성한 XML 파일을 읽어와서 다른 이름으로 저장하면 프로그램이 이를 변환하고 결과를 화면 하단에 표시해 줍니다. 이렇게 해서 새로 생성한 파일을 텍스트큐브에서 읽어오면 됩니다. ^^;;
* 추가 *
블로거닷컴이 파일 첨부를 지원하지 않기 때문에 소스 코드 본문을 올립니다. UI 부분은 생략하고 XML 파싱하는 부분만 올립니다만, C#에 대하여 약간이라도 아시는 분이라면 별 어려움 없이 이용하실 수 있을 겁니다. 요청이 있고 파일을 올릴만한 곳이 있다면 전체 소스를 올리겠습니다. 그렇지만 텍스트큐브가 서비스를 접니 마니 하는 지금에 와서 그런 게 필요한 분이 있으실 리가..
//
// Formatted ATOM to TTXML Converter
//
// Copyright(C) 2010 Sei Zong Myoung
//
// Revision :
// 2010. 03. 16. Initial version.
//
using System;
using System.Windows.Forms;
using System.Xml;
namespace AtomToTtxml
{
public struct Post
{
public string title;
public string content;
public DateTime date;
};
public partial class AtomToTtxml : Form
{
XmlDocument atom;
XmlDocument ttxml;
public AtomToTtxml()
{
InitializeComponent();
}
private void Log(string str)
{
textBoxLog.Text += str;
textBoxLog.Text += "\r\n";
}
private void buttonLoad_Click(object sender, EventArgs e)
{
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
try
{
if ( atom == null ) atom = new XmlDocument();
atom.Load(openFileDialog.FileName);
}
catch (XmlException ex)
{
Log(ex.Message);
}
if (atom.DocumentElement != null)
{
Log("ATOM parsed without any problem.");
textBoxAtom.Text = openFileDialog.FileName;
}
}
}
private void buttonSave_Click(object sender, EventArgs e)
{
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
textBoxTtxml.Text = saveFileDialog.FileName;
if (ConvertAtomToTtxml())
{
ttxml.Save(saveFileDialog.FileName);
Log("TTXML saved without any problem.");
}
}
}
private bool ConvertAtomToTtxml()
{
try
{
int found = 0;
if (atom == null || atom.DocumentElement == null)
return false;
if (ttxml == null) ttxml = new XmlDocument();
ttxml.LoadXml("<?xml version='1.0' encoding='UTF-8'?>");
Post post = new Post();
foreach (XmlNode child in atom.DocumentElement.ChildNodes)
{
bool ret = false;
if (child.Name == "entry")
ret = ParseAtomEntry(child, ref post);
if (ret == true)
{
found++;
XmlElement elem = ttxml.CreateElement("post");
XmlElement title = ttxml.CreateElement("title");
XmlElement content = ttxml.CreateElement("content");
XmlElement published = ttxml.CreateElement("published");
TimeSpan ts = (post.date.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0));
title.InnerText = post.title;
content.InnerText = post.content;
published.InnerText = ts.TotalSeconds.ToString();
ttxml.DocumentElement.AppendChild(elem);
elem.AppendChild(title);
elem.AppendChild(content);
elem.AppendChild(published);
}
}
Log(found + " posts found.");
//ttxml.Save(Console.Out);
}
catch (XmlException ex)
{
Log(ex.Message);
}
return true;
}
private bool ParseAtomEntry(XmlNode node, ref Post newPost)
{
string id = "";
string title = "";
string content = "";
string date = "";
bool post = false;
foreach (XmlNode child in node.ChildNodes)
{
switch (child.Name)
{
case "title":
title = child.InnerText;
break;
case "content":
content = child.InnerText;
break;
case "published":
date = child.InnerText;
break;
case "id":
id = child.InnerText;
break;
case "thr:total":
post = true;
break;
}
}
if (!id.Contains("post") || post == false)
return false;
newPost.title = title;
newPost.content = content;
newPost.date = DateTime.Parse(date);
return true;
}
}
}