CustomFeed::config - 指定した URL から RSS item を抜すプラグイン(実験中)
ちょいと実験で、CustomFeed::config というのを作成中。
config には、URL だけを指定し、抜き出すパラメータの条件の詳細は
別ファイルで定義しというもの。条件には、xpath もしくは正規表現
が使える。xpath で抜き出す処理は hpricot を使っているから、
CSSセレクタとかを指定できるかも。
- module: CustomFeed::config config: link: - 'http://www.onsen.ag/'
前にも同じようなプラグインを作っていたけれども、未完成だったので、
今回はそれなりに形にしてみようかと。Plagger にあったものを参考に
してみた。Pragger で使えれば、僕の中ではそれなりに使えるツールに
なると思う。
HTTPCache クラスは、一度アクセスした URL のコンテンツをローカルに
保存するもので、それ以降はその URL に対しては HTTP アクセスをしない
ようにします。プラグインのテスト中何度も HTTP リクエストが飛ぶのは
良くないと思ってね。前に作ったものだけれど、若干修正しているかも。
require 'rubygems' require 'hpricot' class Extract def Extract.match(opt, &block) record = Extract::Item.new(opt) record.instance_eval(&block) return record.data end class Item attr_accessor :data def initialize(opt) @text = opt[:text] @data = {:summary => {}, :item => []} end def record(opt, &block) if opt.key?(:xpath) doc = Hpricot(@text) records = doc.search(opt[:xpath]).collect {|r| r.to_s} elsif opt.key?(:regexp) records = @text.scan(%r{#{opt[:regexp]}}m).collect {|r| r.to_s} end records.each {|r| prop = Extract::Property.new(r) prop.instance_eval(&block) @data[:item] << prop.data } return self end end class Property attr_accessor :text, :data def initialize(text) @text = text @data = {} end def item(opt) xpath = Hash[*opt.map {|k, v| [k, v] if v.key?(:xpath)}.compact.flatten] const = Hash[*opt.map {|k, v| [k, v] if v.key?(:const)}.compact.flatten] regexp = Hash[*opt.map {|k, v| [k, v] if v.key?(:regexp)}.compact.flatten] if regexp.size regexp.each_pair {|k, v| @data[k] = @text.match(%r{#{v[:regexp]}}m).to_a[1] implement_method(@data, k.to_s, @data[k]) } end if xpath.size doc = Hpricot(@text) xpath.each {|k, v| elem = doc.at(v[:xpath]) elem_val = '' case elem when Hpricot::Text elem_val = elem.to_s when Hpricot::Elem case v[:expr] when /inner_text/ elem_val = elem.inner_text when /text/ elem_val = elem.search('/').map{|e| e if e.class == Hpricot::Text }.compact.join('') when /@(.+)/ elem_val = elem[$1] else elem_val = elem.inner_text end else elem_val = '' end @data[k] = elem_val implement_method(@data, k.to_s, elem_val) } end if const.size end end private def implement_method(obj, name, value = nil) obj[name] = value obj.instance_eval <<-EOD def #{name} self['#{name}'] end def #{name}=value self['#{name}'] = value end EOD end end end require 'md5' require 'fileutils' require 'open-uri' class HTTPCache def initialize(dir) @cache_dir = dir end def get(uri) text = '' unless uri =~ %r{^http://} open(url) {|f| text = f.read } return text.toutf8 end if File.exist?(@cache_dir) && File.directory?(@cache_dir) cache_file = "#{@cache_dir}/#{Digest::MD5.new.update(uri.to_s)}.html" if File.file?(cache_file) && File.readable?(cache_file) File::open(cache_file) {|f| text = f.read } else open(uri) {|f| text = f.read } self.set(uri, text) end else open(uri) {|f| text = f.read } self.set(uri, text) end text.toutf8 end def set(uri, text) FileUtils.mkdir_p(@cache_dir) unless File.exist?(@cache_dir) return 0 unless File.exist?(@cache_dir) && File.directory?(@cache_dir) cache_file = "#{@cache_dir}/#{Digest::MD5.new.update(uri.to_s)}.html" File::open(cache_file, "w") {|f| f.write(text) } return 0 end end require 'kconv' require 'yaml' def load_assets(path) $stderr. puts "assets path: #{path}" assets = {} Dir::glob(path + '/*.yaml') {|file| $stderr.puts "assets file: #{file}" assets[File.basename(file)] = YAML.load(File.read(file).toutf8) } assets end def match_assets(assets, url) assets.each_value {|asset| $stderr.puts "assets handle: #{asset['handle']}" return asset if asset.key?('handle') && url =~ %r{#{asset['handle']}} } nil end require 'rss' @assets_path = File.expand_path(File.dirname($0) + '/assets/plugin/CustomFeed-config') def config(config, data) assets = load_assets(@assets_path) $stderr.puts "number of assets: #{assets.size}" rss = RSS::Maker.make("2.0") {|maker| maker.channel.title = '' maker.channel.description = '' maker.channel.link = '' config['link'].each {|link| asset = match_assets(assets, link) if asset.nil? $stderr.puts "skip item; not match handle; url=#{link}" next end $stderr.puts "fetch url; #{link}" text = HTTPCache.new('./cache').get(link) if asset.key?('capture') record_config = {:regexp => asset['capture']} elsif asset.key?('capture_xpath') record_config = {:xpath => asset['capture_xpath']} else $stderr.puts "skip item; not capture paremeter; url=#{link}" next end items = Extract.match :text => text do record record_config do |r| if asset.key?('extract') asset['extract'].each_pair {|k, v| item({ k => { :regexp => v }}) } end if asset.key?('extract_xpath') asset['extract_xpath'].each_pair {|k, v| item({ k => { :xpath => v, :expr => 'inner_text'}}) } end end end if asset.key?('after_hook') items[:item].map! {|item| # TODO: eval(asset['after_hook']) item } end items[:item].each {|i| next if i.nil? rss_item = maker.items.new_item i.each_pair {|k, v| rss_item.instance_variable_set("@#{k}", v) } rss_item.title = '' if rss_item.title.nil? } } } $stderr.puts "number of item: #{rss.items.size}" data if rss.items.size == 0 rss.items end
試しに作った音泉用に作ったものを、載せておく。
handle: 'http://www\.onsen\.ag/' capture_xpath: '//td[@bgcolor=#000000]' extract: title: '<td width="135"[^>]?>(?:(?:<.+?>)*)\s*(.+?)\s*(?:<.+?>)' link: '(http://[^\s]+?\.asx)' author: '<td align="center"><img .+?alt="([^"]+)"' date: '(http://[^\s]+?\.asx)' description: '^' after_hook: | next if item.title.nil? || item.link.nil? if item.date =~ /([0-9]{2}[0-9]{2}).asx/ item.date = Time.parse($1) else item.date = Time.now end item.description = item.title + ' ' + item.author