我试图解析多个XML文件,然后将它们输出到CSV文件中,以列出正确的行和列。
我能够通过定义文件名一次处理一个文件,并将其具体输出到定义的输出文件名来实现:
File.open('H:/output/xmloutput.csv','w')
我想写入多个文件并使它们的名称与XML文件名相同,而无需对其进行硬编码。我尝试过多种方式,但是到目前为止还没有运气。
示例XML:
<?xml version="1.0" encoding="UTF-8"?>
<record:root>
<record:Dataload_Request>
<record:name>Bob Chuck</record:name>
<record:Address_Data>
<record:Street_Address>123 Main St</record:Street_Address>
<record:Postal_Code>12345</record:Postal_Code>
</record:Address_Data>
<record:Age>45</record:Age>
</record:Dataload_Request>
</record:root>
这是我尝试过的:
require 'nokogiri'
require 'set'
files = ''
input_folder = "H:/input"
output_folder = "H:/output"
if input_folder[input_folder.length-1,1] == '/'
input_folder = input_folder[0,input_folder.length-1]
end
if output_folder[output_folder.length-1,1] != '/'
output_folder = output_folder + '/'
end
files = Dir[input_folder + '/*.xml'].sort_by{ |f| File.mtime(f)}
file = File.read(input_folder + '/' + files)
doc = Nokogiri::XML(file)
record = {} # hashes
keys = Set.new
records = [] # array
csv = ""
doc.traverse do |node|
value = node.text.gsub(/\n +/, '')
if node.name != "text" # skip these nodes: if class isnt text then skip
if value.length > 0 # skip empty nodes
key = node.name.gsub(/wd:/,'').to_sym
if key == :Dataload_Request && !record.empty?
records << record
record = {}
elsif key[/^root$|^document$/]
# neglect these keys
else
key = node.name.gsub(/wd:/,'').to_sym
# in case our value is html instead of text
record[key] = Nokogiri::HTML.parse(value).text
# add to our key set only if not already in the set
keys << key
end
end
end
end
# build our csv
File.open('H:/output/.*csv', 'w') do |file|
file.puts %Q{"#{keys.to_a.join('","')}"}
records.each do |record|
keys.each do |key|
file.write %Q{"#{record[key]}",}
end
file.write "\n"
end
print ''
print 'output files ready!'
print ''
end
我一直遇到'read memory': no implicit conversion of Array into String (TypeError)
和其他错误。
答案 0 :(得分:2)
这里是您的代码的快速同行评审,就像您在公司环境中一样...
代替写作:
input_folder = "H:/input"
input_folder[input_folder.length-1,1] == '/' # => false
考虑使用距字符串结尾的-1
偏移量来访问字符:
input_folder[-1] # => "t"
这简化了您的逻辑,使其更具可读性,因为它没有不必要的视觉干扰:
input_folder[-1] == '/' # => false
这对我来说似乎是个错误:
files = Dir[input_folder + '/*.xml'].sort_by{ |f| File.mtime(f)}
file = File.read(input_folder + '/' + files)
files
是文件名数组。 input_folder + '/' + files
正在将数组附加到字符串:
foo = ['1', '2'] # => ["1", "2"]
'/parent/' + foo # =>
# ~> -:9:in `+': no implicit conversion of Array into String (TypeError)
# ~> from -:9:in `<main>'
您要如何处理它是程序员的一项练习。
doc.traverse do |node|
令人讨厌,因为它回避了Nokogiri能够使用访问器搜索特定标签的功能。我们很少需要逐个标签地遍历一个文档,通常仅在我们查看其结构和布局时才需要。 traverse
较慢,因此只能作为最后的手段。
length
很不错,但在检查字符串是否包含内容时不需要:
value = 'foo'
value.length > 0 # => true
value > '' # => true
value = ''
value.length > 0 # => false
value > '' # => false
来自Java的程序员喜欢使用访问器,但我喜欢懒惰,这可能是因为我的C和Perl背景。
请注意sub
和gsub
,因为它们没有按照您认为的去做。两者都希望有一个正则表达式,但是在开始扫描之前,它们都将使用一个经过escape
处理的字符串。
您要传递正则表达式,在这种情况下可以这样做,但是如果您不记得所有模式匹配规则并且gsub
会扫描到最后,则可能会导致意外问题。字符串:
foo = 'wd:barwd:' # => "wd:barwd:"
key = foo.gsub(/wd:/,'') # => "bar"
通常,我建议人们在使用正则表达式之前要三思而后行。我已经看到了相当高级的程序员在逻辑上打开了一些空白,因为他们不知道引擎将要做什么。它们功能强大,但需要通过外科手术使用,而不是作为通用解决方案。
字符串也会发生相同的情况,因为gsub
不知道何时退出:
key = foo.gsub('wd:','') # => "bar"
因此,如果您只想更改第一个实例,请使用sub
:
key = foo.sub('wd:','') # => "barwd:"
我会做一些不同的事情。
foo = 'wd:bar'
我可以检查一下前三个字符是什么
foo[0,3] # => "wd:"
或者我可以使用字符串索引将它们替换为其他内容:
foo[0,3] = ''
foo # => "bar"
还有更多,但我认为现在已经足够了。
答案 1 :(得分:1)
您应该使用Ruby的CSV类。另外,您不需要执行任何字符串匹配或正则表达式的操作。使用Nokogiri定位元素。如果您知道XML中的节点名称是一致的,则应该非常简单。我不确定这是否是您想要的输出,但这应该可以使您朝正确的方向前进:
require 'nokogiri'
require 'csv'
def xml_to_csv(filename)
xml_str = File.read(filename)
xml_str.gsub!('record:','') # remove the record: namespace
doc = Nokogiri::XML xml_str
csv_filename = filename.gsub('.xml', '.csv')
CSV.open(csv_filename, 'wb' ) do |row|
row << ['name', 'street_address', 'postal_code', 'age']
row << [
doc.xpath('//name').text,
doc.xpath('//Street_Address').text,
doc.xpath('//Postal_Code').text,
doc.xpath('//Age').text,
]
end
end
# iterate over all xml files
Dir.glob('*.xml').each { |filename| xml_to_csv(filename) }