I am trying to parse an XML file, however, am having all kinds of
trouble. I am using the REXML libraries and the sax2parser/listener.
In the sax2listener, I can use the character/text part of the method,
however, I cannot for the life of me figure out how to parse out JUST
WHAT I WANT. Here is what the file looks like as follows:
<B110><DNUM><PDAT> this is the text I need </PDAT></DNUM></B110>
If I use :character, %w{PDAT} {|text| puts text} ... I get the text
"this is the text I need" printed out. If I use the B110 or any
combination, I cannot get it to work. Anyone know how to get the
sax2parser/listener to parse the file and allow me to be selective
about what I parse out of the file? Thanks for any/all help in this
endeavor!!!!!!!!!!
-Bob Angell-
ang...@mac.com
What, exactly, do you want? To extract the text from the PDAT element?
How predictable is the XML?
Are the files as small as your example?
Are regular expressions an option? Or using a DOM and XPath?
How did you decide to use the listner?
James
--
http://www.ruby-doc.org - The Ruby Documentation Site
http://www.rubyxml.com - News, Articles, and Listings for Ruby & XML
http://www.rubystuff.com - The Ruby Store for Ruby Stuff
http://www.jamesbritt.com - Playing with Better Toys
require 'rexml/document'
doc = REXML::Document.new(File.open('someXMLFile.xml'))
info = doc.elements["//B110/DNUM/PDAT"].text
puts info
SA :)
For 2 Gig files?! Good luck!
James Edward Gray II
Hahahaha, I must agree with this. Of course the irony is that if those 2
gig XML files were in YAML, they would only be 5 megs ;)
Ryan
OK, I got the picture.
I would suggest the pull parser. Open up a file stream and keep pulling
events. When you get a start_element event, check the element name.
If it is B110, then, loop and pull events until the PDAT element.
Then pull until text event.
Grab text and store it or whatever.
Go back to main loop, looking again for that B110 element.
Something like this:
#!/usr/bin/env ruby
require 'rexml/parsers/pullparser'
include REXML::Parsers
$text = []
def pdat( parser )
while parser.has_next?
pull_event = parser.pull
$text.push( pull_event[0] ) if pull_event.text?
end
end
def get_text parser
while parser.has_next?
pull_event = parser.pull
b110( parser ) if pull_event.start_element? and
pull_event[0] =~ /B110/
end
end
def b110( parser )
while parser.has_next?
pull_event = parser.pull
pdat( parser ) if pull_event.start_element? and
pull_event[0] =~ /PDAT/
end
end
File.open( "pdat.xml", "r") { |f|
parser = PullParser.new( f )
b110( parser )
}
puts $text.join( "\n" )
How *does* one do stream parsing and partial reading of YAML files?
Is there a SAY parser?
Any time you're faced with a huge XML file and you only want to get
small pieces of it, you should think about using a stream parser, a la
Java's SAX2. The idea behind a stream parser is that the parser runs
through the entire file exactly once, and never has to seek forwards or
backwards; it throws the data to you in whatever size chunks are most
convenient for it. This means the parser does as little work as
possible, which means so long as your code is efficient, the end result
should be maximally efficient.
The downside is that you have to do a bit more work. For example,
depending on how the parser buffers things internally, it might send you
a piece of text inside an XML element in two or more pieces, and expect
you to glue them together. It's also up to you to deal with any
position-based restrictions on which elements you're interested in.
I assumed you wanted the text inside any PDAT element that was
*somewhere* inside a B110 element, so I simply track which elements are
currently "open" and by how many levels. Here's the code.
---
require 'rexml/document'
require 'rexml/parsers/streamparser'
class MyListener
def initialize
# Hash to record which elements we are inside at any given moment, and
# how many of them we are inside
@inside = Hash.new
@textbuffer = ''
end
def tag_start(name, attrs)
if @inside[name]
@inside[name] += 1
else
@inside[name] = 1
end
end
def text(text)
if @inside['B110'] and @inside['PDAT']
@textbuffer += text
end
end
def tag_end(name)
if name == 'PDAT'
# Output the text if we just closed a PDAT inside a B110
if @inside['B110'] and @inside['PDAT']
puts @textbuffer
end
# Clear the buffer any time we close a PDAT
@textbuffer = ''
end
# Decrement count, set to nil if zero
# so @inside['foo'] works as a boolean
if @inside[name] == 1
@inside[name] = nil
else
@inside[name] -= 1
end
end
end
listener = MyListener.new
source = File.new "mydoc.xml"
REXML::Document.parse_stream(source, listener)
---
Here's a sample file:
---
<FOO>
<B110><B110>
<SOMETHINGELSE>
<PDAT>This is the text you want</PDAT>This isn't.
</SOMETHINGELSE>
</B110>
<PDAT>This is sneaky good text</PDAT>
</B110>
<PDAT>This is bad text</PDAT>
</FOO>
---
Output:
---
This is the text you want
This is sneaky good text
---
Note that this uses the native REXML stream parser API, not the SAX2
clone, because the SAX2 clone is slower according to the documentation.
Disclaimers:
The above code is only lightly tested. Although a stream parser should
theoretically be the fastest option, I haven't actually benchmarked it
against (say) the pull parser. (Which is also documented as having an
unstable API, so personally I'd avoid it anyway.)
The above code will break if you have a PDAT somewhere inside a PDAT.
I'm assuming that's not allowed. If it is, you'll have to make your text
buffer be a stack of strings rather than a simple string, append to
@textbuffer[@inside['PDAT']], and take the performance hit.
Also, if you need to do more elaborate selection of which elements to
process, you'll obviously need to make changes to how the current
position is tracked... e.g. implementing "process PDAT elements only if
they are not buried more than 2 other elements deep inside a B110
element" is left as an exercise for the reader :-)
> (started doing this by parsing the file line by line, however,
> ran into malformed XML where I decided that I needed to use the database
> functionality of XML.
If by "malformed XML" you mean syntactically invalid XML, such as
unescaped < > characters, then you may be hosed, as REXML's parsers will
likely choke on it.
mathew
--
<URL:http://www.pobox.com/~meta/>