Madogiwa Blog

主に技術系の学習メモに使っていきます。

v3.0が出たのでfeedjiraがどう動いているか、なんとなくコードを読んでみた🦖

いろいろなファイル形式のパース処理を実装するときに便利なfeedjiraですが、最近メジャーバージョンがあがってv3.0.0になりました🎉

github.com

feedjiraを使ってxmlをパースする処理のサンプルは下記の通りです。

xml = HTTParty.get(url).body
feed = Feedjira.parse(xml)
feed.entries.first.title

Feedjira.parsexmlをパースしてオブジェクトを取得することが出来ます。

では実際に実装を見ていきます。

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の定義を見てみると、xmlon_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とか他のを使っても良さそうな気もしました👀

docs.ruby-lang.org