Python
Scrapy

Scrapy を使ってTumblrの記事をエクスポートしてWordPressにインポートする

最初 Tumblr で作っていたウェブサイトを、WordPress に引っ越しさせたのですが、Tumblrにはエクスポート機能が無いので、自前でスクレイピングして WordPress でインポートできる形式の csv ファイルを作ってインポートしました。

ただそれだけの話なのですが、たまには Qiita も更新しておこうと思ったので公開しておきます。

scrapy は、Python 製のスクレイピング用フレームワークです。

https://scrapy.org/

今回は、こちらを利用して、Tumblr のウェブサイトから記事のcsvファイルを作ります。

Kobito.jvOfAP.png
(こちらが引越し元のサイト)

移行に際して、画像も取ってきたいので、記事中に参照している画像もダウンロードして、img タグの中の src 属性を、ダウンロードした画像の名前に変換しています。

完成版のソースコードはgithubで公開しています。
https://github.com/codeforjapan/scrape_c4j_tumblr

まず、scrapy をインストールします。

pip install scrapy

プロジェクトを作ります。

scrapy startproject [プロジェクト名]

以下のようなディレクトリができます。

[プロジェクト名]/
    scrapy.cfg            # 全体の設定ファイル

    [プロジェクト名]/       # プロジェクトの場所
        __init__.py

        items.py          # アイテムの定義を書くファイル

        pipelines.py      # パイプラインの処理を書くファイル

        settings.py       # プロジェクトの設定ファイル

        spiders/          # スクレイピングの処理を置く場所
            __init__.py

まず、プロジェクト配下のsettings.pyに以下の設定を追加します。

#ダウンロード間隔を3秒空ける
DOWNLOAD_DELAY = 3
# エクスポートの文字コードをutf-8に
FEED_EXPORT_ENCODING='utf-8'
# エクスポートフォーマットは csv
FEED_FORMAT='csv'
# 出力するフィールド(items.py に対応)
FEED_EXPORT_FIELDS = ["post_id","post_name","post_author","post_date","post_type","post_status","post_title","post_content","post_category","post_tags","custom_field"]
# イメージダウンロードのための設定
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'scrapy.pipelines.images.ImagesPipeline': 1,
    'c4jtumblr.pipelines.C4JtumblrPipeline': 300,
}
# イメージ画像のダウンロード先
IMAGES_STORE = './out/images'
# csv ファイルの出力先
FEED_URI = './out/export.csv'

今回は、spiders 配下に、以下のようなコードを書きました。

spiders/c4j.py
# coding: utf-8

from datetime import datetime

from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.selector import Selector

from c4jtumblr.items import NewsItem


class C4jSpider(CrawlSpider):
    name = 'c4j'
    allowed_domains = ['archive.code4japan.org']
    start_urls = [
        #エントリーポイント(スクレイピングのスタート地点)
        'http://archive.code4japan.org/',
    ]
    # 一覧ページのパース処理
    def parse(self, response):
        # <article> 以下が各記事になっているので、全記事を取得
        for article in response.css('article'):
            article_page = article.css('.text a::attr("href")').extract_first() 
            # テキストの記事と、写真記事の場合でマークアップが違うので、失敗していたらもう一つを試す
            if article_page == None:
                article_page = article.css('.photo-hover a::attr("href")').extract_first()
            # response.follow で、指定したURLをフェッチしてparse_newsに渡す
            yield response.follow(article_page, self.parse_news)
        # 次のページへ    
        next_page = response.css('#pagination a#older::attr("href")').extract_first()

        if next_page is not None:
            yield response.follow(next_page, self.parse)

    # 個別のページのパース処理
    def parse_news(self, response):
        # NewsItem クラスには、csv 出力に必要なカラムがフィールドとして入っている
        item = NewsItem()
        sel = Selector(response)
        article = response.css("article")
        # 各フィールドの中身を設定。Tumblr に無い情報は固定の文字列にしている。
        item['article_id'] = article.css("::attr(id)").extract_first()
        item['post_id'] = ""
        item['post_date'] = article.css("::attr(date)").extract_first()
        item['post_name'] = response.url.split("/")[-1]
        item['post_title'] = article.css("div.text h2::text").extract_first()
        item['post_author'] = 'hal'
        item['post_type'] = 'story'
        item['post_status'] = 'draft'
        item['post_category'] = ""
        item['post_tags'] = " ".join(article.css("a.tag::text").extract())
        if item['post_title'] == None:
            item['post_title'] = article.css('div.captions p ::text').extract_first()
        item['post_content'] = article.css('div.text').extract_first()
        if (item['post_content']) == None:
            item['post_content'] = article.css('.photo').extract_first() + article.css('div.text-post div.captions').extract_first()
        # 画像を発見したら、ダウンロードキューに入れておく
        item['image_urls'] = article.css('img::attr("src")').extract()

        # 出力
        yield item

items.py は以下のようになっています。

items.py
# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class C4JtumblrItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass

class NewsItem(scrapy.Item):
    article_id = scrapy.Field()
    post_id = scrapy.Field()
    post_author = scrapy.Field()
    post_name = scrapy.Field()
    post_title = scrapy.Field()
    post_content = scrapy.Field()
    post_date = scrapy.Field()
    post_category = scrapy.Field()
    post_type = scrapy.Field()
    post_status = scrapy.Field()
    post_tags = scrapy.Field()
    custom_field = scrapy.Field()

    image_urls = scrapy.Field()
    images = scrapy.Field()

今回、画像を発見したらそれもダウンロードしておくようにしています。
c4j.py の下の方にある、 item['image_urls'] = article.css('img::attr("src")').extract() の部分ですね。

なんと、scrapy のデフォルトでは、imge_urls プロパティにURLを突っ込んでおくと、勝手にダウンロードをしてくれます。便利ですね。
ただ、ダウンロードした画像のファイル名は、画像のあったURLをSHA1ハッシュ化した名前に変換されます。ローカルでの置き場所も変わるので、記事側の src も変更しなくてはいけません。
その処理を実行するのが、pipeline.py です。

pipeline.py
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html


class C4JtumblrPipeline(object):
    def process_item(self, item, spider):
        # replace image src to downloaded image path
        for image in item['images']:
            item['post_content'] = item['post_content'].replace(image['url'], '/images/' + image['path'])
        return item 

あとは、ディレクトリに移動してコマンドを叩くだけです。

% cd scrape_c4j_tumblr #プロジェクト名のディレクトリ
% scrapy crawl c4j

これを実行すると、out/export.csv が完成します。画像ファイルは out/images/full 以下に保存されていきます。
あとは、できあがったcsvファイルをWordPressに食わせて、画像ファイルをしかるべき場所に配置すればOKでした。やりましたね!
僕としては、画像パスの書き換え部分がとても便利でした!
それでは!