如何在Rails中将XML转换为哈希?

7

如何在Ruby中将XML正文转换为哈希?

我有一个XML正文,我想将其解析为哈希

<soap:Body>
    <TimesInMyDAY>
        <TIME_DATA>
            <StartTime>2010-11-10T09:00:00</StartTime>
            <EndTime>2010-11-10T09:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:20:00</StartTime>
            <EndTime>2010-11-10T09:40:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:40:00</StartTime>
            <EndTime>2010-11-10T10:00:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:00:00</StartTime>
            <EndTime>2010-11-10T10:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:40:00</StartTime>
            <EndTime>2010-11-10T11:00:00</EndTime>
        </TIME_DATA>
    </TimesInMyDAY>
</soap:Body>

我想将它转换成这样的哈希:

{ :times_in_my_day => { 
    :time_data = > [
        {:start_time=>"2010-11-10T09:00:00", :end_time => "2010-11-10T09:20:00" },
        {:start_time=>"2010-11-10T09:20:00", :end_time => "2010-11-10T09:40:00" },
        {:start_time=>"2010-11-10T09:40:00", :end_time => "2010-11-10T10:00:00" },
        {:start_time=>"2010-11-10T10:00:00", :end_time => "2010-11-10T10:20:00" },
        {:start_time=>"2010-11-10T10:40:00", :end_time => "2010-11-10T11:00:00" }
        ]
    } 
}

理想情况下,标签将转换为snake_case符号,并成为哈希中的键。

此外,日期时间缺少时区偏移量。它们在本地时区(而不是UTC)中。因此,我想解析它以显示本地偏移量,然后将xml日期时间字符串转换为Rails DateTime对象。结果数组可能如下所示:

{ :times_in_my_day => { 
    :time_data = > [
        {:start_time=>Wed Nov 10 09:00:00 -0800 2010, :end_time => Wed Nov 10 9:20:00 -0800 2010 },
        {:start_time=>Wed Nov 10 09:20:00 -0800 2010, :end_time => Wed Nov 10 9:40:00 -0800 2010 },
        {:start_time=>Wed Nov 10 09:40:00 -0800 2010, :end_time => Wed Nov 10 10:00:00 -0800 2010 },
        {:start_time=>Wed Nov 10 10:00:00 -0800 2010, :end_time => Wed Nov 10 10:20:00 -0800 2010 },
        {:start_time=>Wed Nov 10 10:40:00 -0800 2010, :end_time => Wed Nov 10 11:00:00 -0800 2010 }
        ]
    } 
}

我能够使用parsein_time_zone方法将单个日期时间进行转换,具体操作如下:

Time.parse(xml_datetime).in_time_zone(current_user.time_zone)

但我不太确定在将XML转换为哈希时解析时间的最佳方法。

我会感激任何建议。谢谢!

编辑

将日期时间字符串转换为Rails DateTime对象的代码是错误的。这将把xml日期时间字符串解析为系统的时区偏移量,然后将该时间转换为用户的时区。正确的代码是:

Time.zone.parse(xml_datetime)

如果用户的时区与系统不同,则会将用户的时区偏移量添加到原始日期时间字符串中。这里有一个关于如何启用用户时区偏好的Railscast:http://railscasts.com/episodes/106-time-zones-in-rails-2-1

Time.zone.parse(xml_datetime) <- 太棒了!谢谢。 - William Denniss
5个回答

15

Hash.from_xml(xml) 是解决这个问题的简单方法。它是 ActiveSupport 的方法。


6

我曾经使用Perl中的XML::Simple,因为使用Perl解析XML非常麻烦。

后来我转到了Ruby,开始使用Nokogiri,发现它非常适合解析HTML和XML。它非常容易使用,以至于我会用CSS或XPath选择器来思考问题,而不再需要XML-to-hash转换器。

require 'ap'
require 'date'
require 'time'
require 'nokogiri'

xml = %{
<soap:Body>
    <TimesInMyDAY>
        <TIME_DATA>
            <StartTime>2010-11-10T09:00:00</StartTime>
            <EndTime>2010-11-10T09:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:20:00</StartTime>
            <EndTime>2010-11-10T09:40:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:40:00</StartTime>
            <EndTime>2010-11-10T10:00:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:00:00</StartTime>
            <EndTime>2010-11-10T10:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:40:00</StartTime>
            <EndTime>2010-11-10T11:00:00</EndTime>
        </TIME_DATA>
    </TimesInMyDAY>
</soap:Body>
}

time_data = []

doc = Nokogiri::XML(xml)
doc.search('//TIME_DATA').each do |t|
  start_time = t.at('StartTime').inner_text
  end_time = t.at('EndTime').inner_text
  time_data << {
    :start_time => DateTime.parse(start_time),
    :end_time   => Time.parse(end_time)
  }
end

puts time_data.first[:start_time].class
puts time_data.first[:end_time].class
ap time_data[0, 2]

输出结果如下:

DateTime
Time
[
    [0] {
        :start_time => #<DateTime: 2010-11-10T09:00:00+00:00 (19644087/8,0/1,2299161)>,
          :end_time => 2010-11-10 09:20:00 -0700
    },
    [1] {
        :start_time => #<DateTime: 2010-11-10T09:20:00+00:00 (22099598/9,0/1,2299161)>,
          :end_time => 2010-11-10 09:40:00 -0700
    }
]

时间值被有意解析为DateTime和Time对象,以显示两者都可以使用。

有道理,我在文档中还有其他级别和元素,我正在尝试将它们映射到数据库中,所以我认为将它们作为哈希来迭代可能是正确的方法。但是使用Nokogiri的搜索功能可能是一个不必要的步骤! - Chanpory
这只是一种不同的迭代方式。习惯使用Nokogiri进行操作后,你会发现从HTML页面中获取数据同样容易,前提是HTML不是病态的。 - the Tin Man
刚刚注意到你使用了 :start_time => Time.parse(start_time):end_time => DateTime.parse(end_time)... 你是不是想要用不同的方式来使用 Time 和 DateTime?只是想确认一下这个差异是否有原因。 - Chanpory
看起来我需要去掉 //,这样它就变成了 start_time = t.at('StartTime').inner_text……我得开始习惯xpath选择器了!对我来说还不直观。 - Chanpory
非常感谢,我已经疯狂地尝试弄清楚为什么它不起作用,最终在凌晨2:30得到了解决!希望你早日康复! - Chanpory
显示剩余6条评论

3

ActiveSupport增加了Hash.from_xml方法,可以在一次调用中将XML转换为哈希。另一个问题中有描述:https://dev59.com/CXM_5IYBdhLWcg3wyWWt#7488299

示例:

require 'open-uri'
remote_xml_file = "https://www.example.com/some_file.xml"
data = Hash.from_xml(open(remote_xml_file))

2

原始问题是一段时间前提出的,但我发现一个比使用Nokogiri和在XML中搜索特定名称更简单的解决方案。

Nori.parse(your_xml)将把XML解析为哈希表,键将与您的XML项具有相同的名称。


它的后端正在使用Nokogiri。那么为什么要在gem上使用gem呢? - Taimoor Changaiz
@TaimoorChangaiz 它也使用其他东西。您可以将其用于抽象化复杂性。 - srcspider

0

如果您不介意使用gem,crack可以很好地处理这个问题。

Crack将XML转换为哈希处理,然后您可以循环遍历结果哈希表以规范化日期时间。

编辑 使用REXML,您可以尝试以下方法(应该接近工作状态,但我无法访问终端,因此可能需要进行一些调整):

require 'rexml/document'
arr = []
doc = REXML::XPath.first(REXML::Document.new(xml), "//soap:Body/TimesInMyDAY").text
REXML::XPath.each(doc, "//TIME_DATA") do |el|
  start = REXML::XPath.first(el, "//StartTime").text
  end = REXML::XPath.first(el, "//EndTime").text
  arr.push({:start_time => Time.parse(start).in_time_zone(current_user.time_zone), :end_time => Time.parse(end).in_time_zone(current_user.time_zone)})
end

hash = { :times_in_my_day => { :time_data => arr } }

当然,这假定结构始终相同,并且您发布的示例并非为简单起见而编造(通常会有这样的例子)。

不介意使用gem,但我尝试使用包含to_hash方法的Savon gem,该方法使用Crack... 但是,我在日期解析方面遇到了问题。似乎Savon/Crack会假定没有偏移量的xml日期时间字符串处于UTC而不是本地用户的时区。因此,所有时间都会无意中发生偏移。所以当我真正想要的是Wed Nov 10 09:00:00 -0800 2010时,2010-11-10T09:00:00变成了Wed Nov 10 01:00:00 -0800 2010 :-( - Chanpory
当我尝试使用doc = REXML::XPath.first(REXML::Document.new(xml), "//soap:Body/TimesInMyDAY").text时,出现了一个奇怪的错误。错误是REXML::UndefinedNamespaceException: Undefined prefix soap found - Chanpory

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接