This is my Ruby class which takes the output from the User Search XML Ticker, and creates from it an array of E2XMLTickerParser::Writeup objects, each of which contains the requisite info E2 gives you about your nodes.

Basic operation is quite simple. You create a new parser by calling E2XMLTickerParser.new, then call its "parse" method with a signal argument of the XML ticker output (a String). After this is done, the parser object will contain an experience method which returns total XP, and the parser's writeups method will return the Array of E2XMLTickerParser::Writeup objects.

Each of these Writeup objects responds to the methods: name, node_id, reputation, createtime, cooled, parent_e2node and cooledby_user. These methods each will return what you expect them to, in native Ruby object format rather than simply the text output E2 generates. For example, createtime will be a Time object, reputation an Integer, and cooled will be true/false.

Here's the parser's implementation itself! It requires, of course, Ruby 1.6 or later and XML::Parser (the Ruby expat bindings).


#!/usr/local/bin/ruby
# Copyright (c) 2001 Brian Fundakowski Feldman
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
require 'xmlparser'

class E2XMLTickerParser < XML::Parser
	class Writeup
		[:node_id, :reputation, :createtime, :cooled, :parent_e2node,
		  :cooledby_user, :name].each {|member|
			attr_reader(member)
		}
		def node_id=(val)
			@node_id = Integer(val)
		end
		def reputation=(val)
			@reputation = Integer(val)
		end
		def parent_e2node=(val)
			@parent_e2node = Integer(val)
		end
		def createtime=(val)
			time =
			  /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.match(val)
			unless time
				raise 'invalid "createtime": #{val}'
			end
			@createtime = Time.utc(*time[1..6].collect {|t| t.to_i})
		end
		def cooled=(val)
			@cooled = Integer(val) != 0
		end
		def cooledby_user=(val)
			@cooledby_user = if val.empty? then nil else val end
		end
		def name=(val)
			@name = val
		end
		def [](element)
			method(element).call
		end
		def []=(element, val)
			method("#{element}=").call(val)
		end
	end

	attr_reader :experience
	attr_reader :writeups
	def initialize
		@writeups = []
		@context = [[nil, nil]]
	end
	## visitors for each Element
	@@tagdeps = {
		"USERSEARCH" => nil,
		"INFO" => "USERSEARCH",
		"writeup" => "USERSEARCH"
	}
	def startElement(tag, args)
		# assertions regarding tag nesting
		if !@@tagdeps.has_key?(tag) || @@tagdeps[tag] != @context[-1][0]
			raise "invalid E2 XML Ticker (#{tag} in #{@context[-1][0]}"
		end
		@context.push([tag, args])
		# lazy-parse "writeup", but not "INFO"
		if tag == "INFO"
			@experience = Integer(args["experience"])
		end
	end
	def endElement(tag)
		@context.pop
	end
	## visitor for "text"
	def default(text)
		if @context[-1][0] == "writeup"
			wu = Writeup.new
			wu.name = text
			@context[-1][1].each {|key, val|
				wu[key] = val
			}
			@writeups.push(wu)
		end
	end
end