如何拆分包含多个 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 没有描述所需的实际输出,因此超出这一点您的里程可能会有所不同。
我有一个来自 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 没有描述所需的实际输出,因此超出这一点您的里程可能会有所不同。