пʼятниця, 28 грудня 2012 р.

Історія одного тупняку, або як я конвертував DBF в SQLite3

Нещодавно до мене потрапив файл у форматі dbf з досить цікавою інформацією. Але сьогодні не про це.

Для роботи з цією інформацією, я вирішив імпортувати таблицю DBF у SQLite3. Використовувати сторонній софт для такої тривіальної задачки — це не наш варіант. Ми не шукаємо легких шляхів. Напишемо скрипт самі. Писати, звичайно, будемо на Ruby.

DBF — застаріваючий формат зберігання даних, який (був?) широко розповсюджений на пострадянському просторі.

Недовгі пошуки навели на бібліотеку dbf. Як описує її автор, це невелика швидка бібліотека для читання DBase, Xbase, Clipper і FoxPro файлів баз даних.

Відкриваємо DBF файл:
require 'dbf'
dbf_table = DBF::Table.new(dbf_file, nil)
dbf_columns = dbf_table.columns
І відразу отримуємо помилку:
dbf-2.0.3/lib/dbf/column/base.rb:19:in `initialize': column name cannot be empty (DBF::Column::NameError)

Проблема виникає через те, що при ініціалізації поля, у його назві відсікаються всі не-ASCII символи. А такий порядок речей в моєму випадку недопустимий, оскільки
імена полів у DBF таблиці записані кирилицею у кодуванні cp1251.

Тут стане у нагоді "monkey patching".
Ініціалізуємо таблицю у кодуванні cp1251 і конверуємо імена полів в utf-8:

module DBF
  module Column
    class Base
      attr_reader :name, :type, :length, :decimal

      def initialize(name, type, length, decimal, version, encoding = nil)
        @name, @type, @length, @decimal, @version, @encoding = clean(name, encoding), type, length, decimal, version, encoding

        raise LengthError, "field length must be greater than 0" unless length > 0
        raise NameError, "column name cannot be empty" if @name.length == 0
      end

      def clean(value, encoding)
        value.force_encoding(encoding).encode('utf-8')
      end

    end
  end
end

dbf_table = DBF::Table.new(dbf_file, nil, 'cp1251')
dbf_columns = dbf_table.columns

Не вдаючись в подробиці створення схеми SQLite3 (повний код скрипта Ви знайдете в кінці статті), перейдемо відразу до імпорту. Задачка звичайно зовсім тривіальна і написання рішення займає лічені хвилини.
dbf_table.each do |record|
  sqlite_db.execute("INSERT INTO the_table VALUES (?,?,?,?,?,?)", record.attributes.values)
end
Тут можна було б піти покурити, попити чаю або кави чи навіть подивитися фільм. Даний процес зайняв досить багато часу.
Час роботи такого рішення мене не вдовольняв зовсім.
Бувалі програмісти мабуть відразу зрозуміють де собака зарита. Але цей тупняк поглинув мене на кілька годин.
Спочатку я грішив на повільність бібліотеки dbf. Але цей варіант був швидко відкинутий. Каменем спотикання була саме бібліотека SQLite3.
Наступну годину я курив, пив пиво і дивився "Ruby Sparks". Між іншим красивий і милий романтичний фільм, але настирливий продакт-плейсмент Apple вже бісить. Паралельно ґвалтува ґуґл в пошуках рішення. І ось воно зійшло:
sql_db.transaction do |db|
  dbf_table.each do |record|
    db.execute("INSERT INTO the_table VALUES (?,?,?,?,?,?)", record.attributes.values)
  end
end

Несподівано скрипт, який виконувався годинами, виконався за 10 секунд. Підвищення продуктивності over 9000%. Я отетерів(!) від такого результату.

Мораль цієї історії проста... Комусь здається, що це тривіально, але мені просто хотілось поділитися з кимось цими думками.

Нище ви знайдете повну версію скрипта для конвертування DBF у SQLite3.

Немає коментарів: