如何拆分包含多个 iCalendar 事件的文本文件?

How do I split a text file containing multiple iCalendar events?

我有一个来自 Google 日历的文本文件。日历上的每个事件都有 14 个不同的字段,但所有事件都像这样堆叠在一起:

BEGIN:VEVENT
DTSTART:20160304T093000Z
DTEND:20160304T143000Z
DTSTAMP:20160417T141329Z
UID:
CREATED:20160228T142659Z
DESCRIPTION:For assembler
LAST-MODIFIED:20160304T133208Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Richmond
TRANSP:OPAQUE
END:VEVENT

BEGIN:VEVENT
DTSTART;VALUE=DATE:20160312
DTEND;VALUE=DATE:20160313
DTSTAMP:20160417T1413
........etc, etc.

我想将文本文件拆分为事件,每个事件都有 14 个字段,并将其保存为数组。我一直在尝试打开文件并按行阅读,但困扰我的是拆分成字段。

假设您使用File.read(fname)将"gulp"文件放入变量str,其中:

str =<<_
BEGIN:VEVENT
DTSTART:20160304T093000Z
DTEND:20160304T143000Z
DTSTAMP:20160417T141329Z
CREATED:20160228T142659Z
DESCRIPTION:For assembler
LAST-MODIFIED:20160304T133208Z
SEQUENCE:0
STATUS:CONFIRMED
END:VEVENT

BEGIN:VEVENT
DTSTART:20160314T093000Z
DTEND:20160314T143000Z
DTSTAMP:20160427T141329Z
CREATED:20160228T142659Z
DESCRIPTION:For assembler
LAST-MODIFIED:20160314T133208Z
SEQUENCE:0
STATUS:CONFIRMED
END:VEVENT
_

如果文件很大,您可以逐行阅读,例如使用 IO::foreach.

您现在可以按如下方式分解字符串。

arr = str.split(/\n{2,}/).map { |s| s.split(/\n/) }
  #=> [["BEGIN:VEVENT", "DTSTART:20160304T093000Z", "DTEND:20160304T143000Z",
  #     "DTSTAMP:20160417T141329Z", "CREATED:20160228T142659Z",
  #     "DESCRIPTION:For assembler", "LAST-MODIFIED:20160304T133208Z",
  #     "SEQUENCE:0", "STATUS:CONFIRMED", "END:VEVENT"
  #    ],
  #    ["BEGIN:VEVENT", "DTSTART:20160314T093000Z", "DTEND:20160314T143000Z", 
  #      "DTSTAMP:20160427T141329Z", "CREATED:20160228T142659Z",
  #      "DESCRIPTION:For assembler", "LAST-MODIFIED:20160314T133208Z",
  #      "SEQUENCE:0", "STATUS:CONFIRMED", "END:VEVENT"
  #    ]
  #   ] 

如果您不想以 BEGIN:END: 开头的行,请将 s.split(/\n/) 更改为:

s.split(/\n/).reject { |t| t.start_with?("BEGIN:", "END:") } }

接下来,我想您会希望将此数组转换为更有用的数据结构,例如哈希数组。您可以按如下方式进行(认识到可能需要进行一些修改以满足您的要求)。

 arr.map do |a|
   a.each_with_object({}) do |b,h|
     key, value = b.split(':')
     begin
       dt = DateTime.iso8601(value)
     rescue ArgumentError
       nil
     end
     h[key.to_sym] = dt ? dt : value
   end
 end
   #=> [{:BEGIN=>"VEVENT",
   #     :DTSTART=>#<DateTime: 2016-03-04T09:30:00+00:00 (...)>,
   #     :DTEND=>#<DateTime: 2016-03-04T14:30:00+00:00 (...)>,
   #     :DTSTAMP=>#<DateTime: 2016-04-17T14:13:29+00:00 (...)>,
   #     :CREATED=>#<DateTime: 2016-02-28T14:26:59+00:00 (...)>,
   #     :DESCRIPTION=>"For assembler",
   #     :"LAST-MODIFIED"=>#<DateTime: 2016-03-04T13:32:08+00:00 (...)>,
   #     :SEQUENCE=>"0",
   #     :STATUS=>"CONFIRMED",
   #     :END=>"VEVENT"
   #    },
   #    {:BEGIN=>"VEVENT",
   #     :DTSTART=>#<DateTime: 2016-03-14T09:30:00+00:00 (...)>,
   #     :DTEND=>#<DateTime: 2016-03-14T14:30:00+00:00 (...)>,
   #     :DTSTAMP=>#<DateTime: 2016-04-27T14:13:29+00:00 (...)>,
   #     :CREATED=>#<DateTime: 2016-02-28T14:26:59+00:00 (...)>,
   #     :DESCRIPTION=>"For assembler",
   #     :"LAST-MODIFIED"=>#<DateTime: 2016-03-14T13:32:08+00:00 (...)>,
   #     :SEQUENCE=>"0",
   #     :STATUS=>"CONFIRMED",
   #     :END=>"VEVENT"
   #    }
   #   ]

Cary 的回答很好,但我只想指出,您也应该能够使用 vformat gem 加载数据。 gem 提供了访问事件属性的好方法。

不幸的是,这个 gem 似乎不能简单地由 gem install 'vformat' 安装,它必须直接从 github 安装(使用捆绑器很容易)。下面是读取两个事件的例子:

正在安装 gem:

# Gemfile
gem 'vformat', git: "https://github.com/martinpovolny/vformat-ruby.git"

$ bundle
...

读取两个事件:

str =<<_
BEGIN:VEVENT
DTSTART:20160304T093000Z
DTEND:20160304T143000Z
DTSTAMP:20160417T141329Z
CREATED:20160228T142659Z
DESCRIPTION:For assembler
LAST-MODIFIED:20160304T133208Z
SEQUENCE:0
STATUS:CONFIRMED
END:VEVENT

BEGIN:VEVENT
DTSTART:20160314T093000Z
DTEND:20160314T143000Z
DTSTAMP:20160427T141329Z
CREATED:20160228T142659Z
DESCRIPTION:For assembler
LAST-MODIFIED:20160314T133208Z
SEQUENCE:0
STATUS:CONFIRMED
END:VEVENT
_

require 'vformat/icalendar'

events = VFormat.decode(str)
events.count
# => 2

events.first.DESCRIPTION.value
# => "For assembler"

events.second.STATUS.value
# => "CONFIRMED"

TL;DR

您真的应该使用 iCalendar parser for this, but either your data is malformed or the icalendar 2.3.0 解析器目前已损坏。但是,您可以使用正则表达式解析格式良好的 iCal 事件数据,然后修改您的数据结构以适合您的用例。

使用正则表达式解析 iCal 数据

虽然成熟的解析器更好,但作为一种快速而肮脏的替代方法,您可以扫描文件中的事件,然后将它们拆分为数组数组:

ics = File.read '/tmp/foo.ics'
events = ics.scan(/^BEGIN:VEVENT.*?END:VEVENT/m).map { |e| e.split ?\n }

在此示例中,events.first 将生成 "BEGIN:VEVENT""DTSTART:20160304T093000Z" 等元素。这是您在问题中要求的,但可能不是您真正需要的。如果您不直接使用 iCalendar 事件对象,您可能需要将事件数据放入更灵活的数据结构中(例如 Hash or OpenStruct)。

将事件数组转换为散列

获得事件数组后,您可以使用 String#split or String#partition 将单个事件转换为散列或其他 key/value 数据结构。例如,使用上一节中相同的 events 变量:

event_hash = Hash[*events.first.flat_map { |e| e.split ?: }]

event_hash 上使用 awesome_print 从我们的变量中显示以下格式良好的内容:

{
               "BEGIN" => "VEVENT",
             "DTSTART" => "20160304T093000Z",
               "DTEND" => "20160304T143000Z",
             "DTSTAMP" => "20160417T141329Z",
                 "UID" => "CREATED",
    "20160228T142659Z" => "DESCRIPTION",
       "For assembler" => "LAST-MODIFIED",
    "20160304T133208Z" => "LOCATION",
            "SEQUENCE" => "0",
              "STATUS" => "CONFIRMED",
             "SUMMARY" => "Richmond",
              "TRANSP" => "OPAQUE",
                 "END" => "VEVENT"
}

然后可以按您喜欢的任何方式操作此散列,或用于创建更合适的对象,例如 Icalendar::Event。原始 post 没有描述所需的实际输出,因此超出这一点您的里程可能会有所不同。