Нещодавно до мене потрапив файл у форматі dbf з досить цікавою інформацією. Але сьогодні не про це.
Для роботи з цією інформацією, я вирішив імпортувати таблицю DBF у SQLite3. Використовувати сторонній софт для такої тривіальної задачки — це не наш варіант. Ми не шукаємо легких шляхів. Напишемо скрипт самі. Писати, звичайно, будемо на Ruby.
Недовгі пошуки навели на бібліотеку dbf. Як описує її автор, це невелика швидка бібліотека для читання DBase, Xbase, Clipper і FoxPro файлів баз даних.
Відкриваємо DBF файл:
І відразу отримуємо помилку:
Проблема виникає через те, що при ініціалізації поля, у його назві відсікаються всі не-ASCII символи. А такий порядок речей в моєму випадку недопустимий, оскільки
імена полів у DBF таблиці записані кирилицею у кодуванні cp1251.
Тут стане у нагоді "monkey patching".
Ініціалізуємо таблицю у кодуванні cp1251 і конверуємо імена полів в utf-8:
Не вдаючись в подробиці створення схеми SQLite3 (повний код скрипта Ви знайдете в кінці статті), перейдемо відразу до імпорту. Задачка звичайно зовсім тривіальна і написання рішення займає лічені хвилини.
Тут можна було б піти покурити, попити чаю або кави чи навіть подивитися фільм. Даний процес зайняв досить багато часу.
Час роботи такого рішення мене не вдовольняв зовсім.
Бувалі програмісти мабуть відразу зрозуміють де собака зарита. Але цей тупняк поглинув мене на кілька годин.
Спочатку я грішив на повільність бібліотеки dbf. Але цей варіант був швидко відкинутий. Каменем спотикання була саме бібліотека SQLite3.
Наступну годину я курив, пив пиво і дивився "Ruby Sparks". Між іншим красивий і милий романтичний фільм, але настирливий продакт-плейсмент Apple вже бісить. Паралельно ґвалтува ґуґл в пошуках рішення. І ось воно зійшло:
Несподівано скрипт, який виконувався годинами, виконався за 10 секунд. Підвищення продуктивності over 9000%. Я отетерів(!) від такого результату.
Мораль цієї історії проста... Комусь здається, що це тривіально, але мені просто хотілось поділитися з кимось цими думками.
Нище ви знайдете повну версію скрипта для конвертування DBF у SQLite3.
Для роботи з цією інформацією, я вирішив імпортувати таблицю 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding: utf-8 | |
require 'dbf' | |
require 'ruby-progressbar' | |
require 'sqlite3' | |
require 'getoptlong' | |
module DBF | |
module Column | |
class Base | |
attr_reader :name, :type, :length, :decimal | |
# Not strip non-ascii characters from column names | |
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 | |
def trans | |
{ | |
0 => { ua: 'Телефон', eng: 'telephone' }, | |
1 => { ua: 'Назва', eng: 'name' }, | |
2 => { ua: 'Місто', eng: 'city' }, | |
3 => { ua: 'Вулиця', eng: 'street' }, | |
4 => { ua: 'Будинок', eng: 'house' }, | |
5 => { ua: 'Квартира', eng: 'apartment' }, | |
} | |
end | |
if __FILE__ == $0 | |
dbf_file = nil | |
sqlite3_file = nil | |
table = 'phones' | |
opts = GetoptLong.new( | |
[ '--help', '-h', GetoptLong::NO_ARGUMENT ], | |
[ '--output', '-o', GetoptLong::REQUIRED_ARGUMENT ], | |
) | |
opts.each do |opt, arg| | |
case opt | |
when '--help' | |
puts "#{File.basename(__FILE__)} [dbf file] -o [sqlite3 file] | |
" | |
exit | |
when '--output' | |
sqlite3_file = arg | |
end | |
end | |
dbf_file = ARGV.shift | |
abort "You must supply a DBF filename on the command line" unless dbf_file | |
dbf_table = DBF::Table.new(dbf_file, nil, 'cp1251') | |
dbf_columns = dbf_table.columns | |
sql = "create table #{table} (\n" | |
sql << "\tid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n" | |
sql << (0..5).map do |i| | |
"\t#{trans[i][:eng]} varchar(#{dbf_columns[i].length})" | |
end.join(",\n") | |
sql << "\n);" | |
puts sql | |
File.delete(sqlite3_file) if File.exist?(sqlite3_file) | |
sql_db = SQLite3::Database.new(sqlite3_file) | |
sql_db.execute_batch(sql) | |
pbar = ProgressBar.create(title: 'data processing', total: dbf_table.record_count, format: '%a |%b>>%i| %p%% (%c of %C) %t') | |
columns = trans.values.map{ |h| h[:eng] }.join(',') | |
sql_db.transaction do |db| | |
dbf_table.each do |record| | |
db.execute("INSERT INTO #{table} (#{columns}) VALUES (?,?,?,?,?,?)", record.attributes.values) | |
pbar.increment | |
end | |
end | |
end |
Немає коментарів:
Дописати коментар