fias 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -16,33 +16,66 @@ gem 'fias', git: 'https://github.com/evilmartians/kladr.git'
16
16
 
17
17
  2. Скачать и распаковать базу ФИАС в tmp/fias (по умолчанию)
18
18
 
19
- # Создание таблиц в базе данных
19
+ # Импорт структуры данных
20
20
 
21
21
  Возможны два варианта:
22
22
 
23
23
  ```
24
- rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=...]
24
+ rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=... ONLY=...]
25
25
  ```
26
26
 
27
27
  Либо:
28
28
 
29
29
  ```
30
- rails g fias:migration [--path=... --prefix=...]
30
+ rails g fias:migration [--path=... --prefix=... --only=...]
31
31
  ```
32
32
 
33
33
  Первый вариант - для использования гема вне рельсов, либо для случая, когда
34
34
  актуальная база ФИАС будет использоваться только для локального мапинга, и
35
35
  на продакшн попасть не должна.
36
36
 
37
+ В параметре only можно передать имена нужных таблиц, houses - все
38
+ таблицы домов.
39
+
37
40
  # Импорт данных
38
41
 
39
42
  ```
40
- rake fias:import:placements fias:import:houses PREFIX=fias PATH=tmp/fias
43
+ rake fias:import PREFIX=fias PATH=tmp/fias ONLY=address_objects
44
+ rake fias:import PREFIX=fias PATH=tmp/fias ONLY=houses
41
45
  ```
42
46
 
43
- Импорт данных разделен на два шага, по скольку, не всегда нужны данные
44
- о строениях, а их почти в 20 раз больше, чем остальных. В большинстве случаев
45
- достаточно городов и улиц.
47
+ Первый пример импортирует только адресные объекты (без справочников),
48
+ второй - только дома.
49
+
50
+ Поддерживается импорт в Postgres и SQLite (нужно для :memory: баз)
51
+
52
+ # Импорт данных в память (для рейк тасок)
53
+
54
+ ```ruby
55
+ ActiveRecord::Base.configurations['fias'] = {
56
+ :adapter => 'sqlite3',
57
+ :database => ':memory:'
58
+ }
59
+ Fias::AddressObject.establish_connection :fias
60
+
61
+ fias = Fias::DbfWrapper.new('tmp/fias')
62
+ importer = Fias::Importer.build(
63
+ adapter: 'sqlite3', connection: Fias::AddressObject.connection.raw_connection
64
+ )
65
+ tables = fias.tables(:address_objects)
66
+
67
+ # Мигрейт. В таком виде так как стандартный мигратор всегда открывает новое
68
+ # соединение с БД, а в случае с SQLite это означает пересоздание базы :memory:.
69
+ Fias::AddressObject.connection.instance_exec do
70
+ eval(importer.schema(tables))
71
+ end
72
+
73
+ importer.import(tables) do |name, record, index|
74
+ record[:aclevel] == 1 # Только активные
75
+ end
76
+
77
+ Fias::AddressObject.count # И дальше импорт
78
+ ```
46
79
 
47
80
  # Некоторые замечания про ФИАС
48
81
 
@@ -55,24 +88,123 @@ rake fias:import:placements fias:import:houses PREFIX=fias PATH=tmp/fias
55
88
  образования хранится в поле AOGUID. Из внешних таблиц логично ссылаться
56
89
  на AOGUID, а не на AOID.
57
90
 
91
+ # Маппинг, он же итерирование
92
+
93
+ Задачи:
94
+
95
+ 1. Сопоставить базу приложения с ФИАСом.
96
+ 2. Уведомлять приложение об изменениях в связанных объектах ФИАСа.
97
+ 3. Уведомлять приложения о новых данных в ФИАСе.
98
+
99
+ В таблице, с которой устанавливаются соответствия нужно создать поле для
100
+ хранения UUID записи ФИАСа.
101
+
102
+ На примере регионов:
103
+
104
+ ```ruby
105
+ # Будем искать соответствия регионов приложения регионам в ФИАС
106
+ scope = Region
107
+ fias_scope = Fias::AddressObject.leveled(:region)
108
+
109
+ # Обработчик событий итератора
110
+ matcher = ->(action, record, *objects) {
111
+ match = objects.first
112
+
113
+ case action
114
+
115
+ # Появилась новая запись в ФИАСе
116
+ when :created
117
+ puts "Region #{match.abbrevated}"
118
+ Region.create!(
119
+ fias_reference: match.id,
120
+ title: match.abbrevated
121
+ )
122
+
123
+ # Элемент в ФИАСе обновлен, нужно (?) обновить базу приложения
124
+ when :updated
125
+ record.update_attributes(
126
+ title: match.abbrevated,
127
+ fias_reference: match.id
128
+ )
129
+ puts "#{record.title} became #{match.abbrevated}"
130
+
131
+ # Объект в ФИАСе был, но из актуального состояния перешел в неактуальное.
132
+ # Типа, Припять: умерший город.
133
+ # Скорее всего, такой элемент в базе приложения нужно скрыть.
134
+ when :deleted
135
+ raise NotImplementedError, "#{record.title} #{objects.first.abbrevated}"
136
+
137
+ # Элемент в ФИАСе разделился: например, поселение Черто-Полохово
138
+ # было преобразовано в две деревни: Чертово и Полохово
139
+ # В этом случае параметр objects содержит объекты ФИАСа, на которые
140
+ # разделился старый.
141
+ when :split
142
+ raise NotImplementedError
143
+
144
+ # Элемент в ФИАСе объединился
145
+ # Типа, присоединили Московскую область к Москве
146
+ # В этом случае objects.first это запись ФИАС о новом объекте, а остальные
147
+ # элементы objects - записи ФИАС объединившихся объектов.
148
+ when :joined
149
+ raise NotImplementedError
150
+
151
+ # Элементу базы приложения нет соответствия в ФИАСе. Пытаемся сопоставить
152
+ # и зафиксировать, если найшли соответствующий вариант.
153
+ when :match
154
+ match = objects.detect { |b| b.actual? || b.name == record.title }
155
+ if match.present?
156
+ record.update_attributes(
157
+ title: match.abbrevated,
158
+ fias_reference: match.id
159
+ )
160
+ puts "#{record.title} became #{match.abbrevated} (new)"
161
+ else
162
+ puts "Record not found #{record.title}"
163
+ end
164
+
165
+ else
166
+ raise 'Unknown action!'
167
+ end
168
+ }
169
+
170
+ # Итерирует существующие в базе приложения элементы
171
+ # fias_reference - название колонки с UUID соответствующей региону записи
172
+ # ФИАСа
173
+ # title - название региона
174
+ fias_scope.match_existing(scope, :fias_reference, :title, &matcher)
175
+
176
+ # Итерирует отсутствующие в базе приложения, но имеющиеся в скоупе
177
+ # ФИАСа адреса.
178
+ fias_scope.actual.match_missing(scope, :fias_reference, &matcher)
179
+ ```
180
+
181
+ Примечания:
182
+
183
+ 1. В случае неоднозначного совпадения ФИАС и приложения, нужно ставить поиск
184
+ соответствия на модерацию.
185
+ 2. В случае :split, :joined - тоже на модерацию. Хотя, разделения происходят
186
+ и нечасто, однако, в ФИАСе таких случаев больше полутора тысяч.
187
+ 3. При начальном импорте данных из ФИАСа, #match_missing лучше начать вызывать
188
+ только после того как соответствия старой базы ФИАСу будут полностью разрулены.
189
+
58
190
  # Работа с данными
59
191
 
60
192
  Существующие регионы:
61
193
 
62
- ```
194
+ ```ruby
63
195
  Fias::AddressObject.actual.leveled(:region).all
64
196
  ```
65
197
 
66
198
  Подчиненные объекты в регионе (области, районы, столица региона):
67
199
 
68
- ```
200
+ ```ruby
69
201
  region = Fias::AddressObject.actual.leveled(:region).first
70
202
  region.children.actual
71
203
  ```
72
204
 
73
205
  # TODO
74
206
 
75
- 1. Индексы.
207
+ 1. Индексы. Индексы. ИНДЕКСЫ.
76
208
 
77
209
  # Полезные ссылки
78
210
 
data/fias.gemspec CHANGED
@@ -21,4 +21,7 @@ Gem::Specification.new do |gem|
21
21
  gem.add_dependency 'rake'
22
22
  gem.add_dependency 'activerecord', '> 3'
23
23
  gem.add_dependency 'progress_bar'
24
+ gem.add_development_dependency 'pg'
25
+ gem.add_development_dependency 'sqlite3'
26
+ gem.add_development_dependency 'hirb'
24
27
  end
data/lib/fias.rb CHANGED
@@ -3,8 +3,11 @@ require 'dbf'
3
3
  require 'active_record'
4
4
 
5
5
  require 'fias/version'
6
- require 'fias/import/dbf_wrapper'
7
- require 'fias/import/pg'
6
+ require 'fias/dbf_wrapper'
7
+ require 'fias/importer'
8
+ require 'fias/importer/base'
9
+ require 'fias/importer/pg'
10
+ require 'fias/importer/sqlite'
8
11
 
9
12
  require 'fias/railtie' if defined?(Rails)
10
13
 
@@ -1,9 +1,18 @@
1
+ # encoding: utf-8
2
+ # TODO: Поделить на файлы
1
3
  module Fias
2
4
  class AddressObject < ActiveRecord::Base
3
- self.table_name = "#{Rails.application.config.fias.prefix}_address_objects"
5
+ # TODO: Тут надо понять как префикс передать
6
+ if defined?(Rails)
7
+ self.table_name = "#{Rails.application.config.fias.prefix}_address_objects"
8
+ else
9
+ self.table_name = "fias_address_objects"
10
+ end
11
+
4
12
  self.primary_key = 'aoid'
5
13
 
6
14
  alias_attribute :name, :formalname
15
+ alias_attribute :id, :aoid
7
16
 
8
17
  # Родительские объекты (Ленобласть для Лодейнопольского района)
9
18
  # Для "проезд 1-й Конной Лахты 2-й" - Санкт-Петербург и Ленинград.
@@ -20,17 +29,25 @@ module Fias
20
29
  primary_key: 'aoguid',
21
30
  foreign_key: 'parentguid'
22
31
 
23
- # Предыдущая историческая версия названия (Ленинград для Питера)
24
- belongs_to :next_version,
32
+ # Предыдущие исторические версии названия (Ленинград для Питера)
33
+ # Может быть несколько, если произошло слияние
34
+ has_many :previous_versions,
25
35
  class_name: 'AddressObject',
26
- primary_key: 'aoid',
27
- foreign_key: 'nextid'
36
+ foreign_key: 'aoid',
37
+ primary_key: 'previd'
28
38
 
29
- # Следующая историческая версия названия (Питер для Ленинграда)
30
- belongs_to :previous_version,
39
+ # Следующая исторические версии названия (Питер для Ленинграда)
40
+ # Может быть несколько, если произошло разделение
41
+ has_many :next_versions,
31
42
  class_name: 'AddressObject',
32
- primary_key: 'aoid',
33
- foreign_key: 'previd'
43
+ foreign_key: 'aoid',
44
+ primary_key: 'nextid'
45
+
46
+ # Полное наименование типа объекта (город, улица)
47
+ belongs_to :address_object_type,
48
+ class_name: '::Fias::AddressObjectType',
49
+ foreign_key: 'shortname',
50
+ primary_key: 'scname'
34
51
 
35
52
  # Актуальные записи (активные в настоящий момент)
36
53
  # Проверено, что livestatus уже достаточен для идентификации
@@ -45,18 +62,33 @@ module Fias
45
62
  }
46
63
 
47
64
  scope :sorted, order('formalname ASC')
65
+ scope :with_types, includes(:address_object_type)
66
+ scope :matching, ->(name) {
67
+ scope = if self.connection.adapter_name
68
+ where(%{ "formalname" @@ ? OR ? @@ "formalname"}, name, name)
69
+ else
70
+ where('formalname LIKE ?', "%#{name}%")
71
+ end
48
72
 
49
- # Полное наименование типа объекта (город, улица)
50
- belongs_to :address_object_type,
51
- class_name: 'Fias::AddressObjectType',
52
- primary_key: 'shortname',
53
- foreign_key: 'scname'
73
+ # Первыми идут центральные и крупные поселения
74
+ scope.order('aolevel ASC, centstatus DESC')
75
+ }
76
+
77
+ scope :same_region, ->(address_object) {
78
+ where(regioncode: address_object.regioncode)
79
+ }
80
+
81
+ # Значимые поселения
82
+ scope :central, where('centstatus > 0')
54
83
 
55
- # Есть ли исторические варианты записи?
56
84
  def has_history?
57
85
  previd.present?
58
86
  end
59
87
 
88
+ def actual?
89
+ livestatus == 1
90
+ end
91
+
60
92
  # Актуальный родитель. Для 1-го проезда 2-й Конной Лахты - только Питер.
61
93
  def parent
62
94
  parents.actual.first
@@ -66,11 +98,134 @@ module Fias
66
98
  AOLEVELS.key(aolevel)
67
99
  end
68
100
 
101
+ # Название с сокращением / полным наименованием города
102
+ # "г. Санкт-Петербург" / "город Санкт-Петербург"
103
+ #
104
+ # Для работы метода требуется загрузить таблицу :address_object_types
105
+ #
106
+ # Параметр - режим:
107
+ # :obviously - очевидный режим". В этом режиме "Дагестан" вместо
108
+ # "Республика Дагестан", "АО" вместо "Автономная область",
109
+ # но всегда "Краснодарский край"
110
+ # :short - дописываются "г." и "респ."
111
+ # :long - дописываются "город" и "Республика"
112
+ #
113
+ # Пример:
114
+ # .abbrevated(:obviously) # Тульская область, Хакасия, Еврейская Аобл.
115
+ # .abbrevated(:short) # Тульская область, Респ. Хакасия, Еврейская Аобл.
116
+ # .abbrevated(:long) # Тульская область, Республика Хакасия, Еврейская автономная область
117
+ def abbrevated(mode = :obviously)
118
+ return name if address_object_type.blank? || shortname.blank?
119
+
120
+ case aolevel_sym
121
+ when :region
122
+ # "Ханты-Мансийский Автономный округ - Югра"
123
+ return name if regioncode == '86'
124
+
125
+ ending = name[-2..-1]
126
+
127
+ # Если название кончается на -ая -ий - по правилам русского языка
128
+ # нужно дописать в конец "область", "край"
129
+ must_append = SHN_MUST_APPEND_TO_ENDINGS.include?(ending) ||
130
+ shortname == 'Чувашия'
131
+
132
+ must_abbrevate = must_append ||
133
+ shortname == 'Чувашия' ||
134
+ mode != :obviously
135
+
136
+ return name unless must_abbrevate
137
+
138
+ abbr = case mode
139
+ when :short
140
+ shortname
141
+ when :long
142
+ address_object_type.name
143
+ when :obviously
144
+ if SHN_LEAVE_SHORTS_INTACT.include?(shortname)
145
+ shortname
146
+ else
147
+ address_object_type.name
148
+ end
149
+ end
150
+
151
+ # Точка не ставится для АО, края, Чувашии и длинных названий
152
+ abbr = "#{abbr}." if mode == :short && not(shortname.in?(SHN_MUST_NOT_APPEND_DOT))
153
+
154
+ if not(must_append) && must_abbrevate
155
+ "#{abbr} #{name}"
156
+ else
157
+ # "Республика" => "республика", но "АО" остается
158
+ abbr = abbr.mb_chars.downcase if not(shortname.in?(SHN_LEAVE_SHORTS_INTACT))
159
+ "#{name} #{abbr}"
160
+ end
161
+ else
162
+ abbr = if mode == :long
163
+ address_object_type.try(:name)
164
+ else
165
+ shortname
166
+ end
167
+ "#{abbr} #{name}"
168
+ end
169
+ end
170
+
171
+ class << self
172
+ # Подробное описание см. в README
173
+ # TODO: OPERSTATUS
174
+ def match_existing(scope, fias_key_accessor, title_field_accessor, &block)
175
+ scope.find_each do |record|
176
+ aoid = record.send(fias_key_accessor)
177
+ title = record.send(title_field_accessor)
178
+
179
+ if aoid.present?
180
+ match = scoped.find_by_aoid(aoid)
181
+
182
+ if match.present?
183
+ unless match.actual?
184
+ next_versions = match.next_versions
185
+
186
+ if next_versions.empty?
187
+ yield(:deleted, record, match)
188
+ elsif next_versions.count == 1
189
+ next_version = next_versions.first
190
+ previous_versions_of_current = next_version.previous_versions
191
+
192
+ if previous_versions_of_current.count == 1
193
+ yield(:updated, record, next_version)
194
+ else
195
+ yield(:joined, record, next_version, previous_versions_of_current)
196
+ end
197
+ elsif next_versions.count > 1
198
+ yield(:split, record, next_versions)
199
+ end
200
+ end
201
+ end
202
+ else
203
+ matches = scoped.matching(title)
204
+ yield(:match, record, *matches)
205
+ end
206
+ end
207
+ end
208
+
209
+ def match_missing(scope, address_object_key_field, &block)
210
+ scoped.each do |address_object|
211
+ unless scope.where(address_object_key_field => address_object.aoid).exists?
212
+ yield(:created, nil, address_object)
213
+ end
214
+ end
215
+ end
216
+ end
217
+
69
218
  # Коды уровня адресного объекта
70
219
  AOLEVELS = {
71
220
  region: 1, autonomy: 2, district: 3, city: 4,
72
221
  territory: 5, settlement: 6, street: 7,
73
222
  additional_territory: 90, additional_territory_slave: 91
74
223
  }
224
+
225
+ # Дописывать сокращения обязательно, "Самарская" выглядит странно,
226
+ # всегда должно быть "Самарская область", а "Дагестан" понятно и так.
227
+ SHN_MUST_APPEND_TO_ENDINGS = %w(ая ий)
228
+ SHN_MUST_NOT_APPEND_DOT = %w(край АО Чувашия) # Не дописывать точку к сокращению
229
+ SHN_LEAVE_SHORTS_INTACT = %w(АО Аобл Чувашия) # В очевидном режиме не разворачивать "АО" в "Автономная область"
75
230
  end
76
231
  end
@@ -1,8 +1,15 @@
1
+ # encoding: utf-8
1
2
  module Fias
2
3
  class AddressObjectType < ActiveRecord::Base
3
- self.table_name = "#{Rails.application.config.fias.prefix}_fias_address_object_types"
4
+ # TODO: Тут надо понять как префикс передать
5
+ if defined?(Rails)
6
+ self.table_name = "#{Rails.application.config.fias.prefix}_address_object_types"
7
+ else
8
+ self.table_name = "fias_address_object_types"
9
+ end
4
10
  self.primary_key = 'scname'
5
11
 
6
12
  alias_attribute :name, :socrname
13
+ alias_attribute :abbrevation, :scname
7
14
  end
8
15
  end
@@ -0,0 +1,90 @@
1
+ module Fias
2
+ # Класс для доступа к DBF-файлам ФИАС.
3
+ #
4
+ # Пример:
5
+ # wrapper = Fias::DbfWrapper.new('tmp/fias')
6
+ # wrapper.address_objects.record_count
7
+ # wrapper.address_objects.each { |record| record.attributes }
8
+ #
9
+ # TODO: Добавить в инишилайзер tables, чтобы при создании проверять
10
+ # их наличие на диске
11
+ class DbfWrapper
12
+ # Открывает DBF-файлы ФИАСа
13
+ def initialize(pathspec)
14
+ unless Dir.exists?(pathspec)
15
+ raise ArgumentError, 'FIAS database path does not exists'
16
+ end
17
+ self.pathspec = pathspec
18
+
19
+ DBF_ACCESSORS.each do |accessor, dbf_name|
20
+ filename = File.join(pathspec, dbf_name)
21
+
22
+ if File.exists?(filename)
23
+ dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
24
+ send("#{accessor}=", dbf)
25
+ end
26
+ end
27
+ end
28
+
29
+ # Возвращает хеш {аксессор dbf-таблицы => таблица}
30
+ # На входе массив названий таблиц (строки), без параметров - все
31
+ # Макрос houses вернет все таблицы домов
32
+ #
33
+ # Пример:
34
+ # wrapper = Fias::DbfWrapper.new('tmp/fias')
35
+ # wrapper.tables(%w(address_objects address_object_types houses))
36
+ # ... {
37
+ # ... address_objects: Dbf::Table(...),
38
+ # ... address_object_types: Dbf::Table(...),
39
+ # ... house1: Dbf::Table(...),
40
+ # ... ...
41
+ # ... house99: Dbf::Table(...)
42
+ # ... }
43
+ def tables(*only)
44
+ only = only.first if only.first.is_a?(Array)
45
+ only = only.map(&:to_s)
46
+
47
+ hash = DBF_ACCESSORS.keys.map do |accessor|
48
+ accessor_s = accessor.to_s
49
+ is_houses = only.include?('houses') && accessor_s.starts_with?('house')
50
+
51
+ if only.include?(accessor_s) || is_houses
52
+ [accessor, send(accessor)]
53
+ end
54
+ end
55
+ Hash[*hash.compact.flatten]
56
+ end
57
+
58
+ # { house01: "HOUSE01"..house99: "HOUSE99" }
59
+ HOUSES_ACCESSORS = Hash[*(1..99).map { |n|
60
+ [("house%0.2d" % n).to_sym, "HOUSE%0.2d.dbf" % n]
61
+ }.flatten]
62
+
63
+ # Таблица соответствий аттрибутов класса DBF-файлам
64
+ DBF_ACCESSORS = {
65
+ address_object_types: 'SOCRBASE.DBF',
66
+ current_statuses: 'CURENTST.DBF',
67
+ actual_statuses: 'ACTSTAT.DBF',
68
+ operation_statuses: 'OPERSTAT.DBF',
69
+ center_statuses: 'CENTERST.DBF',
70
+ interval_statuses: 'INTVSTAT.DBF',
71
+ estate_statues: 'ESTSTAT.DBF',
72
+ structure_statuses: 'STRSTAT.DBF',
73
+ address_objects: 'ADDROBJ.DBF',
74
+ house_intervals: 'HOUSEINT.DBF',
75
+ landmarks: 'LANDMARK.DBF'
76
+ }.merge(
77
+ HOUSES_ACCESSORS
78
+ )
79
+
80
+ DEFAULT_ENCODING = Encoding::CP866
81
+
82
+ attr_reader *DBF_ACCESSORS.keys
83
+ attr_reader :houses
84
+
85
+ private
86
+ attr_accessor :pathspec
87
+ attr_writer *DBF_ACCESSORS.keys
88
+ attr_writer :houses
89
+ end
90
+ end
@@ -0,0 +1,30 @@
1
+ module Fias
2
+ module Importer
3
+ # Фактори-метод, возвращает ссылку на объект импортера
4
+ # Принимает параметры:
5
+ # :adapter - название адаптера
6
+ # :connection - прямое соединение с базой данных (connection.raw_connection)
7
+ def self.build(options = {})
8
+ adapter = options.delete(:adapter) ||
9
+ ActiveRecord::Base.connection_config[:adapter]
10
+
11
+ connection = options.delete(:connection) ||
12
+ ActiveRecord::Base.connection.raw_connection
13
+
14
+ case adapter
15
+ when 'postgresql'
16
+ Pg.new(
17
+ connection,
18
+ options
19
+ )
20
+ when 'sqlite3'
21
+ Sqlite.new(
22
+ connection,
23
+ options
24
+ )
25
+ else
26
+ raise 'Only postgres & sqlite supported now, fork'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,59 @@
1
+ module Fias
2
+ module Importer
3
+ class Base
4
+ def initialize(connection, options = {})
5
+ self.prefix = options.delete(:prefix) || 'fias'
6
+ self.connection = connection
7
+ end
8
+
9
+ # На входе - хеш (имя таблицы => dbf)
10
+ def schema(tables)
11
+ "".tap do |s|
12
+ tables.each do |name, dbf|
13
+ if dbf.present?
14
+ s << schema_for(name, table_name(name), dbf)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def schema_for(name, table_name, dbf)
21
+ "".tap do |s|
22
+ s << %{create_table "#{table_name}", id: false do |t|\n}
23
+ s << schema_columns(name, dbf)
24
+ s << "end\n"
25
+ end
26
+ end
27
+
28
+ def import(tables, &block)
29
+ tables.each do |name, dbf|
30
+ if dbf
31
+ import_table(name, table_name(name), dbf, &block)
32
+ end
33
+ end
34
+ end
35
+
36
+ def import_table(name, table_name, dbf, &block)
37
+ raise NotImplementedError, 'Implement this in concrete class'
38
+ end
39
+
40
+ protected
41
+ attr_accessor :prefix, :connection
42
+
43
+ private
44
+ def table_name(name)
45
+ "#{prefix}_#{name}"
46
+ end
47
+
48
+ def schema_columns(accessor, table)
49
+ "".tap do |s|
50
+ table.columns.each do |column|
51
+ column_name = column.name.downcase
52
+ column_def = column.schema_definition
53
+ s << " t.column #{column_def}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,81 @@
1
+ module Fias
2
+ module Importer
3
+ # Класс для импорта данных из ФИАС в PostgreSQL.
4
+ # Используется COPY FROM STDIN.
5
+ class Pg < Base
6
+ def schema_for(name, table_name, dbf)
7
+ super + alter_table_to_pg_uuids(name, table_name)
8
+ end
9
+
10
+ private
11
+ def alter_table_to_pg_uuids(name, table_name)
12
+ "".tap do |s|
13
+ columns = CONVERT_TO_UUID[name]
14
+ if columns.present?
15
+ columns.each do |column|
16
+ s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def import_table(name, table_name, dbf, &block)
23
+ fields = table_fields(dbf)
24
+ columns = table_columns(fields)
25
+
26
+ truncate_table(table_name)
27
+ copy_from_stdin(table_name, columns)
28
+
29
+ dbf.each_with_index do |record, index|
30
+ should_import = yield(name, record.attributes, index) if block_given?
31
+
32
+ unless should_import === false
33
+ data = record.to_a
34
+ put_data(data)
35
+ end
36
+ end
37
+
38
+ put_copy_end
39
+ end
40
+
41
+ def truncate_table(table_name)
42
+ connection.exec "TRUNCATE TABLE #{table_name};"
43
+ end
44
+
45
+ def table_fields(table)
46
+ table.columns.map(&:name).map(&:downcase)
47
+ end
48
+
49
+ def table_columns(fields)
50
+ fields.join(', ')
51
+ end
52
+
53
+ def copy_from_stdin(table_name, columns)
54
+ sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
55
+ connection.exec(sql)
56
+ end
57
+
58
+ def put_data(data)
59
+ data.map! { |item| item == "" ? '-nil-' : item }
60
+ line = data.join("\t") + "\n"
61
+ connection.put_copy_data(line)
62
+ end
63
+
64
+ def put_copy_end
65
+ connection.put_copy_end
66
+
67
+ while res = connection.get_result
68
+ result_status = res.res_status(res.result_status)
69
+ unless result_status == 'PGRES_COMMAND_OK'
70
+ raise "Import failure: #{result_status}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # Эти поля нужно отконвертировать в тип UUID после создания
77
+ CONVERT_TO_UUID = {
78
+ address_objects: %w(aoguid aoid previd nextid parentguid)
79
+ }
80
+ end
81
+ end
@@ -0,0 +1,38 @@
1
+ module Fias
2
+ module Importer
3
+ # Нужно для :memory: баз в первую очередь
4
+ class Sqlite < Base
5
+ def import_table(name, table_name, dbf, &block)
6
+ truncate_table(table_name)
7
+
8
+ qmarks = ['?'] * dbf.columns.keys.size
9
+ qmarks = qmarks.join(', ')
10
+
11
+ dbf.each_with_index do |record, index|
12
+ data = record.attributes
13
+
14
+ should_import = yield(name, data, index) if block_given?
15
+
16
+ unless should_import === false
17
+ columns = data.keys.join(', ')
18
+
19
+ values = data.values.map do |value|
20
+ if value.is_a?(Date)
21
+ value.to_s
22
+ else
23
+ value
24
+ end
25
+ end
26
+
27
+ connection.execute("INSERT INTO #{table_name} (#{columns}) VALUES (#{qmarks});", values)
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+ def truncate_table(table_name)
34
+ connection.execute "DELETE FROM #{table_name} WHERE 1"
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/fias/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fias
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -4,14 +4,18 @@ module Fias
4
4
 
5
5
  class_option :prefix, type: :string, default: :fias, desc: 'Table names prefix'
6
6
  class_option :path, type: :string, default: 'tmp/fias', desc: 'Path to FIAS dbfs'
7
+ class_option :only, type: :string, default: '', desc: 'Only tables'
7
8
 
8
9
  source_root File.expand_path("../templates", __FILE__)
9
10
 
10
11
  def generate_migration
11
- wrapper = Fias::Import::DbfWrapper.new(options.path)
12
- importer = wrapper.build_importer(prefix: options.prefix)
13
- @schema = importer.schema
14
- @schema.gsub!("\n", "\n\t\t")
12
+ only = options.only.split(',').map(&:strip)
13
+ wrapper = Fias::DbfWrapper.new(options.path)
14
+ importer = Fias::Importer.build(prefix: options.prefix)
15
+
16
+ tables = wrapper.tables(only)
17
+ @schema = importer.schema(tables)
18
+ @schema.gsub!("\n", "\n ")
15
19
 
16
20
  migration_template 'create_fias_tables.rb', 'db/migrate/create_fias_tables'
17
21
  end
data/tasks/fias.rake CHANGED
@@ -25,46 +25,43 @@ namespace :fias do
25
25
  end
26
26
 
27
27
  fias_path = ENV['FIAS'] || 'tmp/fias'
28
- wrapper = Fias::Import::DbfWrapper.new(fias_path)
29
- importer = wrapper.build_importer(prefix: ENV['PREFIX'])
28
+ wrapper = Fias::DbfWrapper.new(fias_path)
29
+ importer = Fias::Importer.build(prefix: ENV['PREFIX'])
30
30
 
31
31
  yield(wrapper, importer)
32
32
  end
33
+
34
+ def only
35
+ only = ENV['ONLY'].to_s.split(',').map(&:strip)
36
+ end
33
37
  end
34
38
 
35
- desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL)'
39
+ desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL, EXCLUDE or ONLY tables)'
36
40
  task may_be_rails(:create_tables) do
37
41
  within_connection do |wrapper, importer|
38
- ActiveRecord::Schema.define { eval(importer.schema) }
42
+ tables = wrapper.tables(only)
43
+ # TODO: Добавить во враппер tables, это убрать
44
+ raise "DBF file not found for: #{key}" if tables.keys.any? { |key| key.nil? }
45
+ ActiveRecord::Schema.define do
46
+ eval(importer.schema(tables))
47
+ end
39
48
  end
40
49
  end
41
50
 
42
- namespace :import do
43
- desc 'Import FIAS data (without houses)'
44
- task may_be_rails(:placements) do
45
- within_connection do |wrapper, importer|
46
- total_record_count = wrapper.tables.sum { |accessor, dbf| dbf.record_count }
47
-
48
- puts 'Importing FIAS data...'
51
+ desc 'Import FIAS data'
52
+ task may_be_rails(:import) do
53
+ within_connection do |wrapper, importer|
54
+ tables = wrapper.tables(only)
49
55
 
50
- bar = ProgressBar.new(total_record_count)
51
- importer.import do
52
- bar.increment!
53
- end
56
+ total_record_count = tables.sum do |accessor, dbf|
57
+ dbf.present? ? dbf.record_count : 0
54
58
  end
55
- end
56
59
 
57
- desc 'Import FIAS data (houses)'
58
- task may_be_rails(:houses) do
59
- within_connection do |wrapper, importer|
60
- total_record_count = wrapper.houses.sum { |region, dbf| dbf.record_count }
60
+ puts 'Importing FIAS data...'
61
61
 
62
- puts 'Importing FIAS data (houses)...'
63
-
64
- bar = ProgressBar.new(total_record_count)
65
- importer.import_houses do
66
- bar.increment!
67
- end
62
+ bar = ProgressBar.new(total_record_count)
63
+ importer.import(tables) do
64
+ bar.increment!
68
65
  end
69
66
  end
70
67
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fias
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-21 00:00:00.000000000 Z
12
+ date: 2013-01-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: dbf
@@ -75,6 +75,54 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: pg
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: sqlite3
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: hirb
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
78
126
  description: Ruby wrapper to FIAS database
79
127
  email:
80
128
  - gzigzigzeo@evilmartians.com
@@ -91,8 +139,11 @@ files:
91
139
  - lib/fias.rb
92
140
  - lib/fias/active_record/address_object.rb
93
141
  - lib/fias/active_record/address_object_type.rb
94
- - lib/fias/import/dbf_wrapper.rb
95
- - lib/fias/import/pg.rb
142
+ - lib/fias/dbf_wrapper.rb
143
+ - lib/fias/importer.rb
144
+ - lib/fias/importer/base.rb
145
+ - lib/fias/importer/pg.rb
146
+ - lib/fias/importer/sqlite.rb
96
147
  - lib/fias/railtie.rb
97
148
  - lib/fias/version.rb
98
149
  - lib/generators/fias/migration.rb
@@ -112,7 +163,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
163
  version: '0'
113
164
  segments:
114
165
  - 0
115
- hash: -2160443642439180378
166
+ hash: 493497803985021140
116
167
  required_rubygems_version: !ruby/object:Gem::Requirement
117
168
  none: false
118
169
  requirements:
@@ -121,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
172
  version: '0'
122
173
  segments:
123
174
  - 0
124
- hash: -2160443642439180378
175
+ hash: 493497803985021140
125
176
  requirements: []
126
177
  rubyforge_project:
127
178
  rubygems_version: 1.8.24
@@ -1,83 +0,0 @@
1
- module Fias
2
- module Import
3
- # Класс для доступа к DBF-файлам ФИАС
4
- # Пример:
5
- # wrapper = DbfWrapper.new('tmp/fias')
6
- # wrapper.address_objects.record_count
7
- # wrapper.address_objects.each { |record| record.attributes }
8
- class DbfWrapper
9
- def initialize(pathspec)
10
- unless Dir.exists?(pathspec)
11
- raise ArgumentError, 'FIAS database path does not exists'
12
- end
13
- self.pathspec = pathspec
14
-
15
- TABLES_ACCESSORS.each do |key, dbf_name|
16
- filename = File.join(pathspec, dbf_name)
17
- dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
18
-
19
- send("#{key}=", dbf)
20
- end
21
-
22
- self.houses = {}
23
- Dir[File.join(pathspec, HOUSE_DBF_MASK)].each do |filename|
24
- File.basename(filename) =~ /(\d+)/
25
- region_code = $1.to_i
26
-
27
- dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
28
- self.houses[region_code] = dbf
29
- end
30
- end
31
-
32
- def tables
33
- hash = TABLES_ACCESSORS.keys.map do |accessor|
34
- [accessor, send(accessor)]
35
- end
36
- end
37
-
38
- def build_importer(options)
39
- config = ActiveRecord::Base.connection_config
40
- if config[:adapter] == 'postgresql'
41
- import = Fias::Import::PgImporter.new(
42
- ActiveRecord::Base.connection.raw_connection,
43
- self,
44
- options
45
- )
46
- else
47
- raise 'Only postgres supported now, fork'
48
- end
49
- end
50
-
51
- # Таблица соответствий аттрибутов класса DBF-файлам
52
- TABLES_ACCESSORS = {
53
- address_object_types: 'SOCRBASE.DBF',
54
- current_statuses: 'CURENTST.DBF',
55
- actual_statuses: 'ACTSTAT.DBF',
56
- operation_statuses: 'OPERSTAT.DBF',
57
- center_statuses: 'CENTERST.DBF',
58
- interval_statuses: 'INTVSTAT.DBF',
59
- estate_statues: 'ESTSTAT.DBF',
60
- structure_statuses: 'STRSTAT.DBF',
61
- address_objects: 'ADDROBJ.DBF',
62
- house_intervals: 'HOUSEINT.DBF',
63
- landmarks: 'LANDMARK.DBF'
64
- }
65
- HOUSE_DBF_MASK = 'HOUSE??.DBF'
66
-
67
- # Эти поля нужно отконвертировать в тип UUID после создания
68
- CONVERT_TO_UUID = {
69
- address_objects: %w(aoguid aoid previd nextid parentguid)
70
- }
71
-
72
- DEFAULT_ENCODING = Encoding::CP866
73
-
74
- attr_reader *TABLES_ACCESSORS.keys
75
- attr_reader :houses
76
-
77
- private
78
- attr_accessor :pathspec
79
- attr_writer *TABLES_ACCESSORS.keys
80
- attr_writer :houses
81
- end
82
- end
83
- end
@@ -1,144 +0,0 @@
1
- module Fias
2
- module Import
3
- class PgImporter
4
- # Класс для импорта данных из ФИАС в PostgreSQL.
5
- # Используется COPY FROM STDIN.
6
- def initialize(raw_connection, wrapper, options = {})
7
- self.prefix = options.delete(:prefix) || 'fias'
8
- self.raw_connection = raw_connection
9
- self.wrapper = wrapper
10
- end
11
-
12
- # Генерирует схему базы для ActiveRecord в виде строки
13
- def schema
14
- "".tap do |s|
15
- wrapper.tables.each do |accessor, table|
16
- s << %{create_table "#{table_name(accessor)}", id: false do |t|\n}
17
- s << dump_schema_for(accessor, table)
18
- s << "end\n"
19
-
20
- s << alter_table_to_pg_uuids(accessor)
21
- end
22
-
23
- s << %{create_table "#{table_name('houses')}", id: false do |t|\n}
24
- s << dump_schema_for(:houses, wrapper.houses.values.first)
25
- s << " t.column :regioncode, :integer\n"
26
- s << "end\n"
27
- end
28
- end
29
-
30
- def import(&block)
31
- wrapper.tables.each do |accessor, table|
32
- import_table(accessor, table, &block)
33
- end
34
- end
35
-
36
- def import_houses(&block)
37
- wrapper.houses.each do |region, table|
38
- import_houses_for_region(region, table, &block)
39
- end
40
- end
41
-
42
- private
43
- def dump_schema_for(accessor, table)
44
- "".tap do |s|
45
- table.columns.each do |column|
46
- column_name = column.name.downcase
47
- column_def = column.schema_definition
48
- s << " t.column #{column_def}"
49
- end
50
- end
51
- end
52
-
53
- def alter_table_to_pg_uuids(accessor)
54
- "".tap do |s|
55
- columns = wrapper.class::CONVERT_TO_UUID[accessor]
56
- if columns.present?
57
- columns.each do |column|
58
- s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name(accessor)} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
59
- end
60
- end
61
- end
62
- end
63
-
64
- def import_table(accessor, table, &block)
65
- fields = table_fields(table)
66
- columns = table_columns(fields)
67
-
68
- tn = table_name(accessor)
69
-
70
- truncate_table(tn)
71
- copy_from_stdin(tn, columns)
72
-
73
- table.each_with_index do |record|
74
- data = record.to_a
75
- put_data(data)
76
- yield(accessor, data) if block_given?
77
- end
78
-
79
- put_copy_end
80
- end
81
-
82
- def import_houses_for_region(region, table, &block)
83
- fields = table_fields(table)
84
- fields << :regioncode
85
- columns = table_columns(fields)
86
-
87
- tn = table_name('houses')
88
-
89
- truncate_table(tn)
90
- copy_from_stdin(tn, columns)
91
-
92
- table.each_with_index do |record|
93
- data = record.to_a
94
- data << region
95
- put_data(data)
96
- yield('houses', data) if block_given?
97
- end
98
-
99
- put_copy_end
100
- end
101
-
102
- def table_name(table)
103
- "#{prefix}_#{table}"
104
- end
105
-
106
- def table_fields(table)
107
- table.columns.map(&:name).map(&:downcase)
108
- end
109
-
110
- def table_columns(fields)
111
- fields.join(',')
112
- end
113
-
114
- def truncate_table(table_name)
115
- raw_connection.exec "TRUNCATE TABLE #{table_name};"
116
- end
117
-
118
- def copy_from_stdin(table_name, columns)
119
- sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
120
- raw_connection.exec(sql)
121
- end
122
-
123
- def put_data(data)
124
- data.map! { |item| item == "" ? '-nil-' : item }
125
- line = data.join("\t") + "\n"
126
- raw_connection.put_copy_data(line)
127
- end
128
-
129
- def put_copy_end
130
- raw_connection.put_copy_end
131
-
132
- while res = raw_connection.get_result
133
- result_status = res.res_status(res.result_status)
134
- unless result_status == 'PGRES_COMMAND_OK'
135
- raise "Import failure: #{result_status}"
136
- end
137
- end
138
- end
139
-
140
- attr_accessor :raw_connection, :wrapper
141
- attr_accessor :prefix, :table_mappings
142
- end
143
- end
144
- end