Pull to refresh

Используем почтовые индексы в своём приложении во благо

Reading time 6 min
Views 57K
Я думаю, что на многих сайтах пользователя спросят его физический адрес. Для доставки ли, для отсылки бумажного спама уведомлений ли. И, в общем-то — это мелочь. Вбил индекс, Москва, область, район, село, улица, дом, квартира. Казалось бы, что тут упрощать, каждый вроде помнит свой адрес, трудно ли его вбить? Но дьявол, как всегда, кроется в мелочах: пользователь опечатывается в адресе, посылка уходит не туда, лучи «добра» идут вам в обратную связь и вообще жизнь плохеет.

Приглядитесь к первой части адреса — индексу. В этом наборе из шести цифр уже есть область, район и город/село. Их можно подставить автоматом. Этим мы убъём сразу двух зайцев:
  • Убережём пользователя от ошибок (при вводе неверного индекса он сразу заметит, что город-то не его), что, в случае доставки письма (а то и посылки), может здорово её ускорить (пока её по неверному индексу зашлют, да пока разберутся, что не туда заслали, да отправят туда — пользователь вам всю плешь проест)
  • пользователю будет приятно, что о нём заботятся :-)

Дело за малым: нам нужна база почтовых индексов.

И она есть! Всамделишняя, электронная и, главное, официальная база индексов от Почты России.

Встречайте: vinfo.russianpost.ru/database/ops.html

База доступна в уже диковинном для молодых разработчиков формате DBF и регулярно (два раза в месяц) обновляется.

Конечно, по подробности эта база до ФИАС недотягивает, но, стоит отдать должное, она гораздо проще (всего одна таблица!), поэтому, если вам не нужна точность до улицы и дома, а хватит только населённого пункта — вам сюда.

Прикручиваем счастье к… ну, давайте к сайту.


Итак, радостно качаем базу и думаем, как же её впихнуть в используемый нами КакойТамУНасСовременныйSQL (а то и НеSQL).

Ищем в гугле, ищем в яндексе, ищем в apt-cache, последний нам радостно и выдаёт:

envek@envek-work:~$ apt-cache search dbf
pgdbf - converter of XBase / FoxPro tables to PostgreSQL
dbf2mysql - xBase <--> MySQL

Здорово-то как! Я использую Postgres и конвертировать буду в него. В базе используется ещё досовская кодировка, так что призовём на помощь iconv. Кстати, самые свежие версии pgdbf (>= 0.6.2) сами шаманством владеют и iconv призывают, но до убунтовского репозитория они ещё не добрались.

mv {PIndx08,post_indices}.dbf # Переименовываем файл, как будет называться таблица
pgdbf -u post_indices.dbf | iconv -f CP866 > post_indices.sql # Конвертируем

Что же, теперь надо заставить это работать.

Я использую Ruby on Rails, на её примере и покажу. Кто рельсы не понимает, может пролистать.

Cоздаём модель, которая будет нашу информацию из базы данных и представлять в приложении
rails g model PostIndex

В миграцию вдумчиво копируем структуру таблицы из оригинальной базы, делаем индекс первичным ключом:
class CreatePostIndices < ActiveRecord::Migration
  def change
    create_table :post_indices, id: false do |t|
      t.string :index,     limit:  6
      t.string :ops_name,  limit: 60
      t.string :ops_type,  limit: 50
      t.string :ops_subm,  limit:  6
      t.string :region,    limit: 60
      t.string :autonom,   limit: 60
      t.string :area,      limit: 60
      t.string :city,      limit: 60
      t.string :city_1,    limit: 60
      t.date   :act_date
      t.string :index_old, limit:  6
      t.index  :index_old
    end
    reversible do |to|
      to.up do
        execute 'ALTER TABLE post_indices ADD PRIMARY KEY (index);'
      end
    end
  end
end

Слегка настраиваем модель:
class PostIndex < ActiveRecord::Base
  self.primary_key = 'index'
end

Делаем простенький контроллер, который нам почтовый индекс в json-формате отдаст:
# В консоли: rails generate controller PostIndices
class PostIndicesController < ApplicationController
  def get
    @index = PostIndex.where(index: params[:index]).first
    @index = PostIndex.where(index_old: params[:index]).order(:index).first! unless @index
    respond_to do |format|
      format.json { render json: @index.to_json(only: [:index, :region, :area, :city]) }
    end
  end
end

Прописываем в config/routes.rb маршрут, по которому приложение нам отдаст желанные индексы:
get '/post_index/:index(.:format)', controller: :post_indices, action: :get

И, главное: html и javascript, которые и сделают всю магию для пользователя.

HTML-форма:
<form id='address_form'>
  <table>
    <tr>
      <td><label for='address_postcode'>Почтовый индекс</label></td>
      <td>
        <input class='postcode_field' id='address_postcode' name='address[postcode]'>
        <p class='description'>После ввода почтового индекса, поля «область», «район» и «город» заполняются автоматически.</p>
      </td>
    </tr>
    <tr>
      <td><label for='address_region'>Область/край/республика</label></td>
      <td><input class='region_field' id='address_region' name='address[region]'></td>
    </tr>
    <tr>
      <td><label for='address_area'>Район</label></td>
      <td><input class='area_field' id='address_area' name='address[area]'></td>
    </tr>
    <tr>
      <td><label for='address_city'>Город/село</label></td>
      <td><input class='city_field' id='address_city' name='address[city]'></td>
    </tr>
  </table>
</form>

Javascript-код (очень подробный, с уведомлением пользователя, отловом ошибок и исправлением индекса)
jQuery(document).ready(function($){
  $('.postcode_field').on('keyup change', function () {
    // Найдём все поля
    var postcode_field = $(this);
    var form = postcode_field.parents("form");
    var region_field = $('.region_field', form);
    var area_field   = $('.area_field', form);
    var city_field   = $('.city_field', form);
    // Очистим все поля
    region_field.val('');
    area_field.val('');
    city_field.val('');
    // Если индекс введён полностью - загрузим информацию о нём
    var postcode = this.value;
    if (postcode.length == 6) {
      jQuery.ajax({ 
        dataType: "jsonp",
        url: 'http://postindexapi.ru/'+postcode+'.json?callback=?',
        beforeSend: function() { // Уведомим пользователя, что загрузка идёт
          $("td:last-child p.description.notice, td:last-child p.description.alert", postcode_field.parents("tr")).remove();
          $('<p class="description notice loading"></p>').text("Загрузка…").appendTo($("td:last-child", postcode_field.parents("tr")))
        },
        success: function(data){
          postcode_field.val(data.index);
          region_field.val(data.region);
          area_field.val(data.area);
          city_field.val(data.city);
          if (data.index != postcode) {
              var message = "Вы ввели устаревший почтовый индекс: "+postcode+", ваш текущий индекс: "+data.index;
              $('<p class="description notice"></p>').text(message).appendTo($("td:last-child", postcode_field.parents("tr")))
          }
        },
        error: function (jqxhr, status, e) {
          var message = 'Произошла ошибка при загрузке адреса по почтовому индексу!'+e;
          if (e == 'Not Found') message = 'Почте России такой почтовый индекс не известен';
          if (status == 'timeout') message = 'Сервер с почтовыми индексами не отвечает. Попробуйте ещё раз.';
          $('<p class="description alert"></p>').text(message).appendTo($("td:last-child", postcode_field.parents("tr")))
          console.debug(jqxhr, status, e);
        },
        complete: function () { // Уберём плашку
          $("td:last-child p.description.loading", postcode_field.parents("tr")).remove();
        }
      });     
    }
  });
});

И, вуаля, при вводе индекса нам автоматом подставляется область, город и так далее. Заодно, бонусом, мы можем исправлять устаревшие индексы на актуальные (очень часто у людей записаны адреса родственников с уже безнадёжно устаревшими индексами).



Ещё штришок: чтобы держать базу всегда свежей, создадим rake-таск, который будет запускаться по крону, скажем, раз в две недели и всё это делать за нас (в Gemfile у вас должен быть gem 'nokogiri', можно с require: false):

require 'open-uri'
require 'fileutils'
require 'nokogiri'

namespace :post_index do

  desc 'Update used post indices database to latest'
  task update: :environment do
    # Get info about post indices database
    url_prefix = 'http://info.russianpost.ru/database'
    doc  = Nokogiri::HTML(open("#{url_prefix}/ops.html"))
    file = doc.at_css('a[name=newdbdata]+table tr:last-child td:nth-child(4) a').attr :href
    FileUtils.mkdir_p "#{Rails.root}/tmp/post_indices"
    dir = Pathname.new("#{Rails.root}/tmp/post_indices")
    filepath = Pathname.new("#{dir}/#{file}")
    filepath_success = Pathname.new("#{dir}/#{file}.success")
    if filepath.exist? and filepath_success.exist?
      puts 'Already up-to-date.'
    else
      # Download, unzip, rename and convert post indices file
      sh "wget #{url_prefix}/#{file} -O #{filepath}"
      sh "unzip -o #{filepath} -d #{dir}"
      dbf_filename = filepath.to_s.gsub /\.zip$/, '.DBF'
      sh "cp -f #{dbf_filename} #{dir}/post_indices.dbf"
      sh "pgdbf -u #{dir}/post_indices.dbf | iconv -f CP866 > #{dir}/post_indices.sql"
      # Import in database
      config = Abitur::Application.config.database_configuration[::Rails.env]
      dbh, dbu, dbp, db = config['host'], config['username'], config['password'], config['database']
      sh "PGPASSWORD=#{dbp} psql -U #{dbu} -w -h #{dbh} #{db} < #{dir}/post_indices.sql"
      # Clean up
      FileUtils.rm [dbf_filename, "#{dir}/post_indices.dbf", "#{dir}/post_indices.sql"], force: true
      FileUtils.touch filepath_success
    end
  end

end

Резюме


Плюсы: простота внедрения, использования и поддержания в актуальном состоянии, малый вес
Минусы: невысокая подробность (только до населённого пункта), ВСЕ ГОРОДА КАПСОМ, ПОЧТА РОССИИ, ЗАЧЕМ?

И для самых ленивых


Ну, и напоследок. Если же вам такая мелочь понравилась, но вы яростно не желаете тащить эту информацию к себе в приложение, то специально для вас я сделал мини-сервис postindexapi.ru. Который делает как раз то, что я описал выше — отдаёт информацию об индексе в JSON. Пользуйтеся на здоровье! Инструкции прилагаются.

С благодарностью принимаются так же pull request'ы и bug report'ы, а так же любые советы и пожелания в гитхаб-репозитории: github.com/Envek/postindexapi.ru

Спасибо за внимание.

UPD: По состоянию на 2018-й год было потухший мини-сервис postindexapi.ru поддерживает в рабочем состоянии alexkbs, за что ему большое спасибо. API его сервиса отличается от приведённого в статье, поэтому используйте отдельный код на JavaScript.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+44
Comments 47
Comments Comments 47

Articles