いろいろなファイル形式のパース処理を実装するときに便利なfeedjiraですが、最近メジャーバージョンがあがってv3.0.0
になりました🎉
feedjiraを使ってxmlをパースする処理のサンプルは下記の通りです。
xml = HTTParty.get(url).body feed = Feedjira.parse(xml) feed.entries.first.title
Feedjira.parse
でxmlをパースしてオブジェクトを取得することが出来ます。
では実際に実装を見ていきます。
Feedjira.parse
Feedjira.parse
は、引数にparse対象のxmlとParserのオブジェクト、ブロックを引数にとるメソッドです。
引数にparserが無かった場合、parser_for_xml
を呼び出しparse可能なparserを探して返します。これにより複数のparserから変換可能なparserを探索できるみたいですね(便利)※引数でもparser_for_xml
でもparserが見つからなかったらraiseします。
その後はxmlとブロックを引数にparser.parse
を呼び出してParseします。
module Feedjira def parse(xml, parser: nil, &block) parser ||= parser_for_xml(xml) if parser.nil? raise NoParserAvailable, "No valid parser for XML." end parser.parse(xml, &block) end module_function :parse def parser_for_xml(xml) start_of_doc = xml.slice(0, 2000) Feedjira.parsers.detect { |klass| klass.able_to_parse?(start_of_doc) } end module_function :parser_for_xml
https://github.com/feedjira/feedjira/blob/master/lib/feedjira.rb#L57
ちなみにFeedjira.parsers
にはデフォルトで下記が設定されていて、parsers
に値を追加することで独自のparserもparser_for_xml
で取得する対象に含めることが出来ます。追加方法は、README(https://github.com/feedjira/feedjira#parsers)に記載されています。
module Feedjira module Configuration def default_parsers [ Feedjira::Parser::RSSFeedBurner, Feedjira::Parser::GoogleDocsAtom, Feedjira::Parser::AtomYoutube, Feedjira::Parser::AtomFeedBurner, Feedjira::Parser::AtomGoogleAlerts, Feedjira::Parser::Atom, Feedjira::Parser::ITunesRSS, Feedjira::Parser::RSS, Feedjira::Parser::JSONFeed, ] end
https://github.com/feedjira/feedjira/blob/master/lib/feedjira/configuration.rb#L58
Parser#parse
実際にparserに使われているClassは、下記のような実装になっています。elementでRSSの各要素の定義を記載しています。elements :item, class: RSSEntry
は、RSSEntryのオブジェクトの配列を持つことを表していて、RSSEntryも同様な形でfeedjira内で定義されています(https://github.com/feedjira/feedjira/blob/master/lib/feedjira/parser/rss_entry.rb)
parse
メソッドmodule FeedUtilities
に定義されているので、そちらも見てみます。
ちなみにelementとかの記法は、sax-machine(https://github.com/pauldix/sax-machine)というgemの機能によるものです。
module Feedjira module Parser # Parser for dealing with RSS feeds. # Source: https://cyber.harvard.edu/rss/rss.html class RSS include SAXMachine include FeedUtilities element :description element :image, class: RSSImage element :language element :lastBuildDate, as: :last_built element :link, as: :url element :rss, as: :version, value: :version element :title element :ttl elements :"atom:link", as: :hubs, value: :href, with: { rel: "hub" } elements :item, as: :entries, class: RSSEntry attr_accessor :feed_url def self.able_to_parse?(xml) (/\<rss|\<rdf/ =~ xml) && !(/feedburner/ =~ xml) end end end end
https://github.com/feedjira/feedjira/blob/master/lib/feedjira/parser/rss.rb
しかし、FeedUtilities
にも実際のparse処理はsuper
で呼び出されていて、feedjira上にはありません。。。
module Feedjira module FeedUtilities module ClassMethods def parse(xml, &block) xml = strip_whitespace(xml) xml = preprocess(xml) if preprocess_xml super xml, &block end
https://github.com/feedjira/feedjira/blob/master/lib/feedjira/feed_utilities.rb#L13
実際の処理はsax-machine上にありそうです。
SAXMachine#parse
SAXMachine#parse
の定義を見てみると、xmlとon_errorとon_warningを引数に、handler_klassをSAXMachine.handler
から動的に生成して、 オブジェクトを生成しxmlを引数にhandler_klass#sax_parse
を呼び出しています。
※SAXMachine.handlerは、nokogiri、Oga、Ox等がReaderが指定されています。
module SAXMachine def parse(xml_input, on_error = nil, on_warning = nil) handler_klass = SAXMachine.const_get("SAX#{SAXMachine.handler.capitalize}Handler") handler = handler_klass.new(self, on_error, on_warning) handler.sax_parse(xml_input) self end
https://github.com/pauldix/sax-machine/blob/master/lib/sax-machine/sax_document.rb#L7
今回は、nokogiriを使ったhandlerのsax_parse
の実装を見てみます。NokogiriのParserのオブジェクトを生成して、xmlを引数にparse
を実行しているだけのようです。実際のparse処理はnokogiri
側に定義されているようですね。
module SAXMachine class SAXNokogiriHandler < Nokogiri::XML::SAX::Document def sax_parse(xml_input) parser = Nokogiri::XML::SAX::Parser.new(self) parser.parse(xml_input) do |ctx| ctx.replace_entities = true end end
https://github.com/pauldix/sax-machine/blob/master/lib/sax-machine/handlers/sax_nokogiri_handler.rb
nokogiri側の実装は、下記に記載されていますが今回はfeedjiraの実装を知りたかっただけで、nokogiriのコードを読むと深みにハマりそうなので、やめておきます。。。 https://github.com/sparklemotion/nokogiri/blob/master/lib/nokogiri/xml/sax/parser.rb#L79
おわりに
今回は、feedjiraのコードを読んでみました。feedjiraはRSS等の主要なファイル形式をsax-machineというgemを使ってSAX形式で定義して、Nokogiri等のParseでparseするgemということがわかりました。 feedjiraの範囲では、sax-machineを使って主要形式のSAX形式の設定をSupportしているだけなので、中身は意外とシンプルでした。お手軽に主要な形式でparse処理を実装できるのは良い感じですね。
しかし独自のファイル定義メインで使うならfeedjiraで用意されているファイル形式が使えないので、変換のためのファイル定義を表すClassを新規に作らないといけないので、ruby標準のRSSとか他のを使っても良さそうな気もしました👀