Как читать несколько файлов XML, а затем выводить в несколько файлов CSV с одинаковыми именами файлов XML - PullRequest
1 голос
/ 11 июня 2019

Я пытаюсь проанализировать несколько файлов 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

I'read memory': no implicit conversion of Array into String (TypeError) и другие ошибки.

Ответы [ 2 ]

2 голосов
/ 16 июня 2019

Вот краткий рецензирование вашего кода, что-то похожее на то, что вы получите в корпоративной среде ...

Вместо того, чтобы писать:

input_folder = "H:/input"

input_folder[input_folder.length-1,1] == '/' # => false

Подумайте об этом, используя-1 смещение от конца строки для доступа к символу:

input_folder[-1] # => "t"

Это упрощает вашу логику, делая ее более читабельной, поскольку в ней отсутствует лишний визуальный шум:

input_folder[-1] == '/' # => false

См. [] и []= в документации по String.


Для меня это выглядит как ошибка:

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 голос
/ 12 июня 2019

Вы должны использовать Ruby's 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) }
...