tags:

views:

306

answers:

4

How can i easily parse a document which has this structure

description
some line of text
another line of text
more lines of text

quality
3 47 88 4 4 4  4

text: type 1
stats some funny stats

description
some line of text2
another line of text2
more lines of text2

quality
1 2  4 6 7

text: type 1
stats some funny stats

.
.
.

Ideally i would want an array of hash structures where each hash represents a 'section' of the document and probably should look like this:

{:description => "some line of text another line of text more lines of text", :quality => "3 47 88 4 4 4 4", :text =>type 1, :stats => "some funny stats"}

+3  A: 

You should look for the indicator lines (description, quality, text and stats) in a loop and fill the hash while processing the document line by line.

Another option would be to use regular expressions and parse the document at once, but you don't really need regular expressions here, and if you're not familiar with them, I'd have to recommend against regexes.

UPDATE:

sections = []

File.open("deneme") do |f|
  current = {:description => "", :text => "", :quality => "", :stats => ""}
  inDescription = false
  inQuality = false

  f.each_line do |line|
    if inDescription
      if line.strip == ""
        inDescription = false
      else
        current[:description] += line
      end
    elsif inQuality
      current[:quality] = line.strip
      inQuality = false
    elsif line.strip == "description"
      inDescription = true
    elsif line.strip == "quality"
      inQuality = true
    elsif line.match(/^text: /)
      current[:text] = line[6..-1].strip
    elsif line.match(/^stats /)
      current[:stats] = line[6..-1].strip
      sections.push(current)
      current = {:description => "", :text => "", :quality => "", :stats => ""}
    end
  end
end
Can Berk Güder
Thank you so much for this! However,The only issue i found with this approach is that it is not generic enough. Trying to use it with modifications to a similarly structured document did not work. Maybe it is slightly not very straight forward to see what is happening behind the code.
eastafri
you're right, it's not meant to be generic. it only works if the document has the same structure as your example.if the document has even a slightly different structure, the parser fails miserably, but you can say that this is by design.
Can Berk Güder
+1  A: 

Your input looks pretty close to YAML, so I would convert the input to valid YAML (using method like Can's) then use standard ruby library to load it. Then when your user runs into something they didn't think of in their brilliant markup, tell them to just use YAML :)

chris
+2  A: 

The regex version:

ary = str.scan(/description\n(.*?)\n\nquality\n(.*?)\n\ntext:([^\n]+)\nstats([^\n]+)/m).inject([]) do |n, (desc, qual, text, stats)|
  n << { :description => desc.gsub("\n", ' '), :quality => qual, :text => text, :stats => stats }
end
kejadlen
+1  A: 

One parsing trick is to read data in paragraph mode -- a chunk at a time. If your sub-sections are consistently delimited by 2 newlines (or if you can use a pre-process to impose such consistency), paragraph-reading might be useful.

Aside from the special handling needed for the 'text' sub-section, the example below is fairly general, requiring only that you declare the name of the last sub-section.

# Paragraph mode.
$/ = "\n\n"

last_subsection = 'text'
data = []

until DATA.eof
    data.push({})
    while true
        line = DATA.readline

        # Determine which sub-section we are in.
        ss = nil
        line.sub!( %r"\A(\w+):?\s*" ) { ss = $1; '' }

        # Special handling: split the 'text' item into 2 subsections.
        line, data[-1]['stats'] = line.split(/\nstats +/, 2) if ss == 'text'

        data[-1][ss] = line
        break if ss == last_subsection
    end

    # Cleanup newlines however you like.
    data[-1].each_key { |k| data[-1][k].gsub!(/\n/, ' ') }
end

# Check
data.each { |d| puts; d.each { |k,v| puts k + ' => ' + v } }

__END__
# Data not shown here...
FM