Наш чатик

Телеграм чат начинающих программистов. Общаемся и помогаем друг другу

Если ссылка не открывается, можно найти нас в поиске по чатам @rubyrush или пойти другими путем

Хранение данных, запись в XML

В прошлом уроке мы научились писать XML-файлы руками и читать XML в программах Ruby. Теперь научимся записывать XML-данные в файл с использованием нашего любимого gem-а: REXML.

План урока

  1. Пишем XML в Ruby
  2. Программа учета финансов v. 2.0
  3. Ошибки в XML

Почему нельзя просто писать текст в файлы

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

  • Данные постоянно обновляются разными программами
  • Файлы с данными могут содержать большие объемы информации, которые неудобно просматривать глазами
  • Часто файлы, предназначенные для программ, вообще нельзя понять без дополнительных программ (JPEG, MP3, AVI)
  • XML изначально придуман легко читаемым и простым по структуре именно для простоты его обработки в программах
  • Человек может легко ошибиться в синтаксисе (об этом в конце)

Как писать XML с помощью Ruby

Для записи XML в файлы мы воспользуемся тем же парсером, что и для чтения, только теперь будем действовать наоборот. Мы передаем XML-документ и объект файла нашему парсеру и он сам пишет в него данные как надо.

Запись XML-файла также использует XML-парсер

Пишем программу для записи расходов

Итак, давайте поставим задачу. Мы хотим программу, которая позволит нам ввести в консоли трату (описание, категорию, дату и количество потраченных денег), а потом допишет её в наш файл expenses.xml. Вспомним, заодно, как он у нас устроен.

<?xml version='1.0' encoding='UTF-8'?>
<expenses>
  <expense amount='900' category='Образование' date='9.7.2015'>
     Книжка по Ruby on Rails
  </expense>
  <expense amount='400' category='Хороший программист' date='1.7.2015'>
     Петличка и провода
  </expense>
  <expense amount='3500' category='Хороший программист' date='23.6.2015'>
     Софтбокс + штатив
  </expense>
</expenses>

Давайте писать программу для записи расходов в наш файл expenses.xml: для этого мы будем использовать всё тот же gem REXML. Создайте в вашей папке rubytut2/lesson9/expense_tracker файл expense_writer.rb:

require "rexml/document" # подключаем парсер
require "date" # будем использовать операции с данными

# Спросим у пользователя, на что он потратил деньги и сколько
puts "На что потратили деньги?"
expense_text = STDIN.gets.chomp

puts "Сколько потратили?"
expense_amount = STDIN.gets.chomp.to_i

# Спросим у пользователя, когда он потратил деньги
puts "Укажите дату траты в формате ДД.ММ.ГГГГ, например 12.05.2003 (пустое поле - сегодня)"
date_input = STDIN.gets.chomp

# Для того, чтобы записать дату в удобном формате, воспользуемся методом parse класса Time
expense_date = nil

# Если пользователь ничего не ввёл, значит он потратил деньги сегодня
if date_input == ''
  expense_date = Date.today
else
  begin 
    expense_date = Date.parse(date_input)
  rescue ArgumentError # если дата введена неправильно, перехватываем исключение и выбираем "сегодня"
    expense_date = Date.today
  end
end

# Наконец, спросим категорию траты
puts "В какую категорию занести трату"
expense_category = STDIN.gets.chomp

# Сначала получим текущее содержимое файла
# И построим из него XML-структуру в переменной doc
current_path = File.dirname(__FILE__)
file_name = current_path + "/my_expenses.xml"

file = File.new(file_name, "r:UTF-8")
doc = nil 

begin
  doc = REXML::Document.new(file)
rescue REXML::ParseException => e # если парсер ошибся при чтении файла, придется закрыть прогу :(
  puts "XML файл похоже битый :("
  abort e.message
end

file.close

# Добавим трату в нашу XML-структуру в переменной doc

# Для этого найдём элемент expenses (корневой)
expenses = doc.elements.find('expenses').first

# И добавим элемент командой add_element
 # Все аттрибуты пропишем с помощью параметра, передаваемого в виде АМ
expense = expenses.add_element 'expense', {
    'amount' => expense_amount,
    'category' => expense_category,
    'date' => expense_date.strftime('%Y.%m.%d') # or Date#to_s
}
# А содержимое элемента меняется вызовом метода text
expense.text = expense_text

# Осталось только записать новую XML-структуру в файл методов write
# В качестве параметра методу передаётся указатель на файл
# Красиво отформатируем текст в файлике с отступами в два пробела
file = File.new(file_name, "w:UTF-8")
doc.write(file, 2)
file.close

puts "Информация успешно сохранена"

Отлично, осталось запустить этот файл и создать новую трату:

$ ruby expenses_writer.rb

На что потратили деньги?
Компот
Сколько потратили?
200
Укажите дату траты в формате ДД.ММ.ГГГГ, например 12.05.2003 (пустое поле - сегодня)
12.07.2015
В какую категорию занести трату
Развлечения
Информация успешно сохранена

Можно посмотреть содержимое файла my_expenses.xml и убедиться, что информация действительно успешно сохранена.

<?xml version='1.0' encoding='UTF-8'?>
<expenses>
  <expense amount='900' category='Образование' date='9.7.2015'>
     Книжка по Ruby on Rails
  </expense>
  <expense amount='400' category='Хороший программист' date='1.7.2015'>
     Петличка и провода
  </expense>
  <expense amount='3500' category='Хороший программист' date='23.6.2015'>
     Софтбокс + штатив
  </expense>
  <expense amount='200' category='Развлечения' date='2015.07.12'>
     Компот
  </expense>
</expenses>

Действительно, новая трата добавилась. Поиграйтесь с программой, добавьте несколько новых трат и посмотрите, как можно с помощью двух программ: expense_reader.rb и expense_writer.rb вообще обойтись без открывания файла my_expenses.xml в текстовом редакторе.

Ошибки в синтаксисе XML-файлов

Если вы работаете с внешними данными, которые непонятно кто готовил, помните о возможности ошибки. Например, незакрытый тег или забытая треугольная скобка. Парсер сообщит вам о таких ошибках.

Пример XML-файла с ошибками

Давайте попробуем сломать файл my_expenses.xml и, например, удалим закрывающий тег коревого контейнера </expenses>.

$ ruby expenses_reader.rb
.../ruby-2.1.4/lib/ruby/2.1.0/rexml/parsers/treeparser.rb:27:in `parse': No close tag for /expenses (REXML::ParseException)

Парсер сразу же ругнулся и всё нам прямо так и написал: "Нет закрывающего тега у expenses". Чтобы в этой ситуации программа не вылетала с ошибкой, можно написать обработчик.

begin
  doc = REXML::Document.new(file) # создаем новый документ REXML, построенный из открытого XML файла
rescue REXML::ParseException => e
  puts "Похоже, файл #{file_name} испорчен:"
  abort e.message
end

Теперь можно ломать наш файл как угодно и смотреть, что за сообщение напишет нам наш expense_reader.rb. Программа для записи также будет спотыкаться о битые XML-файлы, но обработчик туда допишите уже сами.

Итак, мы научились не только читать XML-файлы но и с лёгкостью писать свои (или обновлять уже имеющиеся, что полезнее).