RSS로 한 번에 백업하기
📼
내 Ghost 블로그는 서버리스하지 않다. 지속적인 관리가 필요함에도 서버를 유지하는 이유는 서버를 통한 블로그 운영에 수많은 장점들이 있기 때문이다. 하지만 서버를 통해 블로그를 관리하게 되면 아주 큰 단점이 한 가지 생긴다. 서버가 터질 경우 안에 담긴 글들을 복원하기 매우 어려워진다는 것이다. 앞으로 글과 사진이 훨씬 많아질텐데 매번 일일이 복사하여 백업하기는 너무 귀찮을 것 같다는 생각이 들었다. 이를 개선해볼 대책을 세워보고 싶었다.
Ghost 내장 백업의 문제점
Ghost는 .json
형태의 블로그 백업 파일을 내려 받을 수 있는 기능을 제공한다. 정말 블로그의 영혼 그대로 복사된다. 작가 이름, 사용한 태그들, 글의 내용 및 양식, 글을 업로드한 시간과 HTML 메타태그 내 summary까지, Ghost 내에서 설정할 수 있는 모든 것들은 그대로 백업된다.
하지만 두 가지 문제점이 있다.
- Ghost 내장 백업 파일은 사람이 읽기 어렵다. Minified JSON일 뿐만 아니라, 수많은 정보들을 담고 있기 때문에 파일 구조도 복잡하며 글도 압축되어 있다.
- 또 Ghost 내장 백업은 사진을 백업하지 않는다. 때문에 블로그를 복원하면 사진 파일들은 모두 "찾을 수 없음"(일명 엑박)이 뜨게 된다. 블로그 서버가 살아있거나 복사해둔 사진들이 있으면 다행이지만 사진을 복원하지 못하는 경우도 생길 수 있다는 뜻이다.
목표
주 목표
- 글과 사진을 모두 백업해야 한다.
보너스 목표
- 사람이 읽을 수 있는 형태여야 한다. (Human-Readable Medium)
- 블로그를 복원할 때를 대비하여 어느 글의 어느 위치에 어느 사진이 들어가는지 명확하게 알 수 있어야 한다.
- 백업이 편리해야 한다.
- 블로그 외부에서 복제본을 만들 수 있어야 한다.
구상
바로 RSS이다. RSS는 2000년대 초반 블로그 붐이 일었을 때 등장한 기술로 마치 "구독"의 역할을 한다. RSS를 지원하는 사이트나 블로그들은 RSS 피드 주소를 제공한다. RSS 피드 주소에는 그 사이트에서 업데이트 되는 내용들이 기계가 읽을 수 있는 형태로 정리되어 올라간다. 사용자들이 RSS 리더에 RSS 피드 주소를 입력하면 리더가 RSS 피드 주소에서 새로운 컨텐츠를 매번 긁어오는 것이다.
현대에는 SNS가 활성화되며 RSS 기술이 사장되었지만 나의 목표를 달성하기에는 충분한 기술이다. RSS 피드는 글을 받아오는 API 역할을 하는 것이다. Ghost는 기본적으로 RSS를 지원하니 이를 이용하기로 했다.
개략적인 아이디어
- 블로그 RSS 주소를 입력하여 RSS 피드 전체를 복사해온다.
- RSS를 파싱하여 글의 HTML을 추출한다.
- 각 글마다 폴더를 하나씩 생성하여 글의 HTML을 저장한다.
- 글의 HTML에 포함된
img
태그의src
주소에 접속하여 사진을 내려 받는다. - 사진이 존재하는 글은 글 폴더마다
images
폴더를 만들어 사진을 저장하고, HTML 내img
태그의src
를 저장된 이미지의 상대 경로로 변경한다.
Development
참고
아래의 모든 예시는 [anaclumos/backup-with-rss](https://github.com/anaclumos/backup-with-rss)
의 v1을 기준으로 작성되었다. 이 글을 읽을 시점에는 무언가 새로운 기능이나 버그 수정이 추가되었을지 모른다.
또한 이 글에 첨부된 코드는 개략적인 전개를 보여주기 위한 것이지 전체 코드가 아니다. 이 글대로 복사해서 실행하려 하면 아마 실행되지 않을 것이다! 전체 코드는 GitHub 저장소에 공개되어 있다.
1. Feedparser를 이용해 RSS 피드 복 사
Python의 Feedparser라는 모듈을 통해 RSS 피드를 복사한다.
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
import feedparser
class RSSReader:
origin = ""
feed = ""
def __init__(self, URL):
self.origin = URL
self.feed = feedparser.parse(URL)
def parse(self):
return self.feed.entries
RSSReader는 RSS 피드를 불러와 entries
항목을 넘기는데 사용된다.
이 코드가 하는 역할은,
- RSSReader Object가 생성될 때
self.origin
에 RSS 주소를 저장하고 RSS 주소를 파싱하여self.feed
에 저장한다. - parse 함수가 실행될 시
self.feed
에 저장된 값 중entries
를 반환한다.
이 중 entries
에는 RSS 피드의 글들이 list
의 형식으로 들어있다. 다음 예시는 이 글의 RSS이다.
parse()
의 self.feed.entries
의 구조
// 일부 생략
{
"bozo": 0,
"encoding": "utf-8",
"entries": [],
"feed": {
"generator": "Ghost 3.13",
"generator_detail": {
"name": "Ghost 3.13"
},
"image": {
"href": "https://blog.chosunghyun.com/favicon.png",
"link": "https://blog.chosunghyun.com/",
"links": [
{
"href": "https://blog.chosunghyun.com/",
"rel": "alternate",
"type": "text/html"
}
],
"title": "Sunghyun Cho",
"title_detail": {
"base": "https://blog.chosunghyun.com/rss/",
"language": "None",
"type": "text/plain",
"value": "Sunghyun Cho"
}
},
"link": "https://blog.chosunghyun.com/",
"links": [
{
"href": "https://blog.chosunghyun.com/",
"rel": "alternate",
"type": "text/html"
},
{
"href": "https://blog.chosunghyun.com/rss/",
"rel": "self",
"type": "application/rss+xml"
}
],
"subtitle": "Sunghyun Cho's Blog",
"subtitle_detail": {
"base": "https://blog.chosunghyun.com/rss/",
"language": "None",
"type": "text/html",
"value": "Sunghyun Cho;s Blog"
},
"title": "Sunghyun Cho",
"title_detail": {
"base": "https://blog.chosunghyun.com/rss/",
"language": "None",
"type": "text/plain",
"value": "Sunghyun Cho"
},
"ttl": "60",
"href": "https://blog.chosunghyun.com/rss/",
"namespaces": {
"": "http://www.w3.org/2005/Atom",
"content": "http://purl.org/rss/1.0/modules/content/",
"dc": "http://purl.org/dc/elements/1.1/",
"media": "http://search.yahoo.com/mrss/",
"status": 200,
"version": "rss20"
}
}
}