仕事を楽しくする【AWSを使わない】はてぶの記事をサムネイル付きでJSON配信する3ステップ
はじめに
類似するネタは、オンライン上に色々な方が書いて下さっているのですが、"サムネイルはAWS S3に保存しよう!"的なネタが多く、VPS一台で処理を完結したい自分にとっては、そのものズバリの記事がありませんでした。需要があるかどうかわかりませんが、書いてみたいと思います。
おおまかに実装方針を立てると、このようになります。
- はてぶのAPIから記事を収集する(XMLをフェッチしてパース)
- はてぶの記事URLからサムネイルを生成してDBに保存(dragonflyもしくはcarrierwaveを用います。どちらでも可能です)
- 記事とサムネイルをJSON形式で配信する
今回は主要部分を抜粋してご紹介します。ソースコードはこちらに公開しております。
tjnet/geeknews-rails · GitHub
(1)はてぶのAPIから記事を収集する(XMLをフェッチしてパース)
まずはとにかく記事データが必要ということで、railsを用いて記事を収集します。データ収集に利用したのは以下のコードです。
#coding: utf-8 require 'pp' require 'net/http' require 'uri' require 'rexml/document' module Tasks class ArticlePicker # initialize article data def self.execute ActiveRecord::Base.connection.execute("TRUNCATE TABLE articles") feed_list = Feed.all articles = [] feed_list.each do |feed| next if feed.url.blank? articles = fetch_and_parse(feed.url) save_articles(articles, feed.category_id) end end # save articles def self.save_articles(articles, category_id) return if articles.empty? pp '--' pp Time.now articles.each do |article| #pp sprintf('save %s of %d', article['title'], category_id) pp sprintf('save %s of %d', article['link'], category_id) Article.create!( :category_id => category_id, :title => article['title'], :link => article['link'], :description => article['description'], ) end end # return parsed articles def self.fetch_and_parse(url) res = fetch(url) articles = xml_to_array(res) pp articles articles end # fetch response from url def self.fetch(url) pp URI.escape(url) uri = URI.parse URI.escape(url) res = Net::HTTP.get uri res end # return parsed XML def self.xml_to_array(xml) items_rx = [] doc = REXML::Document.new xml doc.elements.each('//item') do |e| i = Hash.new i["title"] = e.elements['title'].text i["link"] = e.elements['link'].text i["description"] = e.elements['description'].text items_rx << i end return items_rx end end end
リトライ処理などは、別のスクリプトに切り出しています。
begin pp "updating article ..." Tasks::ArticlePicker.execute rescue Exception => e pp "[ERROR] update_article #{e.message}" retry_count += 1 retry if retry_count <= 5 pp "retried 5 times so exit" exit end pp "updated article"
(2)はてぶの記事URLからサムネイルを生成してDBに保存(dragonflyもしくはcarrierwaveを用います。どちらでも可能です)
URLからサムネイルを作成するためにwkhtmltoimageを実行しています。
DBへの保存はcarrierwaveを用いています。
# Generate and assign an image or set a validation error begin tempfile = temp_thumbnail_path cmd = "wkhtmltoimage --quality 40 --width 50 --height 50 \"#{self.link}\" \"#{tempfile}\"" p "*** grabbing thumbnail: #{cmd}" system(cmd) # sometimes returns false even if image was saved self.photo = File.new(tempfile) # will throw if not saved self.save rescue => e p "*** thumbnail error: #{e}" self.errors.add(:base, "Cannot generate thumbnail. Is your URL valid?") ensure end
DBへのデータ保存が終われば、wkhtmltoimageで生成した一時ファイルは不要となりますので、modelのコールバックで後始末します。
#Cleanup temp files when we are done after_save :cleanup_temp_thumbnail # Cleanup the temporary thumbnail image def cleanup_temp_thumbnail File.delete(temp_thumbnail_path) rescue 0 end
(3)記事とサムネイルをJSON形式で配信する
ここまででDBに画像データを保存できました。後は、これをcontrollerからJSONとして配信します。
class Api::V1::ArticleController < ApplicationController # return articles def index render json: Article.list(request) end end
# #= return articles as array of hash # def self.list(request) res = Array.new Article.all.each do |article| res.push({ :category_id => article.category_id, :title => article.title, :link => article.link, :description => article.description, :image_url =>(article.photo.blank?) ? '' : "#{request.protocol}#{request.host_with_port}#{article.photo.url}", }) end res end
記事URLを生成するために、controllerからrequestを受け取っています。
もう少し簡潔に書けそうですが。。。
はまりどころ
うまく動かないときは、このあたりをチェックしてみて下さい。
- サムネイルを一時的に保存するディレクトリのパーミッションなど
- public/以下のディレクトリ構造
- 本番環境だけで動かないときは、デプロイ設定やrackサーバの再起動が正常になされているか
TODO
次は、このあたりに取り組みたいですね。
- cronの設定
- サムネイルのクオリティを向上させる
- 静的ファイルの配信を高速化