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

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

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

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

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

Встречайте: info.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. Пользуйтеся на здоровье! Инструкции прилагаются.
И автор не погнушается попросить донату.
Автор с благодарностью примет любые средства на поддержку сервиса postindexapi.ru.
PayPal: envek@envek.name
WebMoney: R157729290120
С не меньшею благодарностью принимаются так же pull request'ы и bug report'ы, а так же любые советы и пожелания в гитхаб-репозитории: github.com/Envek/postindexapi.ru


Спасибо за внимание.
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 46
  • +8
    К моему несчастью, я нередко сталкиваюсь с тем, что поле индекса является обязательным.
    Я довольно часто переезжаю. Вот и сейчас — что бы узнать свой индекс мне надо тратить время на поиски сайта с необходимой информацией, а потом и на нём искать свой индекс.
    Было бы намного удобнее, если бы, в случае отсутствия заполненного индекса, он заполнялся бы автоматически по данным остальной части адреса.
    Что-то мне подсказывает, что я не один такой =)
    • +1
      Теоретически можно эту информацию «выдирать» из этой базы сконструировав запрос вида
      SELECT index FROM post_indices WHERE region ILIKE 'Ваш регион' AND city ILIKE 'Ваш город' AND ops_type = 'П';
      

      Но чтобы избежать опечаток, названия областей, районов, городов пользователю нужно подсказывать при вводе, тем более, что в этой базе все республики, например, названы со словом «республика» на конце, отчего звучат непривычно (Адыгея республика, блинский блин).
      • +4
        Но практически в крупных городах даже на одной небольшой улице могут быть разные индексы. То есть с помощью этой базы можно лишь проверить, что нет грубых ошибок, что указав индекс /190.../, я указал на Петербург, но 190000 или 190999 сервису всё равно.
        • +3
          Да, так и есть. База Почты России детализирует только до населённого пункта (редко — до района в городе).
          Хотите точности до дома — вам нужен ФИАС.
          Есть и сервисы предоставляющие его: например раз и два
          • 0
            Ух-ты. Ребята по ссылкам молодцы!
            Сколько времени было убито на войну сначала с КЛАДРом, а потом и с ФИАС. Разбор структуры, конвертация в нормальный формат, оптимизация, построение индексов… Теперь все это сидит где-то в темных глубинах одной малоизвестной облачно ERP. Эх, и видимо вместе с ней тихо помрет…
        • +2
          Для морфологического поиска используют Sphinx
        • +2
          В 2GIS можно узнать индекс по адресу, просто кликнув по зданию…
          • 0
            К сожалению, одной и той же улице и населенному пункту может соответствовать несколько индексов. Поэтому можно уменьшить их число, но все равно придется задавать вопрос: «Выберите, какой из следующих индексов ваш».

            Вот например, здесь вбейте «москва» и «ленинградский». Там 8 индексов.
            • 0
              Еще дополню. В прицнипе, неправильно, если Интернет-магазин требует жестко индекс. Он должен требовать заведения либо индекса, либо адреса. И во втором варианте уже менеджер должен через Интернет или по телефону позвонить в почтовое отделение и выяснить этот индекс. клиента эта проблема касаться не должна. По моему опыту, свой индекс не знают примерно 30% клиентов, это не так много.
            • +1
              Я привык при заполнении адресов писать только цифры, отвечающие за город (3-4 первые), остальное дополняю нулями. Нули — это почтамт, почта через него и так проходит.

              Почему так делаю: последние цифры — это номер отделения уже внутри города (вообще, адресуемой единицы), и ошибка в номере отделения затягивает доставку просто очень сильно. Пока в не том отделении посылку отложат, пока решат, что делать (иногда пересылают в правильное, иногда отправляют телеграмму получателю, что зайдите в такое-то отделение), пока отправят телеграмму, а ее пока доставят — ой много воды утечет. Грубо, с ошибкой в номере отделения надо отправлять, если вместо персиков действительно хочется получить курагу :)
              • 0
                В больших городах может не так элегантно сработать.
                • 0
                  Это в насколько больших? Как Москва? В Питере вроде все отделения в диапазоне 190000-190999 и то с поопусками
                  • 0
                    Хотя вполне быть что только у Москвы есть несколько базовых трёх цифр.
            • +2
              Вообще, идея отличная, но, как сказал товарищ выше, куда лучше после выбора региона подставлять основной индекс. Если человек захочет — исправит на более точный. Все равно основного индекса предостаточно — досылают в отделение по адресу.

              И да, что делать с этим DBF-то? Мне бы в MySQL базу загрузить это дело — дальше сам уже сделаю.

              P. S.: Нет, меня не забанили в гугле :-)
              • 0
                в самой же статье указано. и для мускула тоже. O_O
                • 0
                  В смысле, что конвертировать можно?
                  • +1
                    Ну, конвертер в убунтовских репозиториях есть: dbf2mysql. Попробуйте его. Есть ли под винду — не знаю.
                    • 0
                      Я не очень хорошо пользуюсь шелом и совсем никогда не пользовался репозиториями. Но у меня есть сервер на линуксе и доступ к шелу, соответственно. Я в нем какие-то простейшие задачи только делал всегда.

                      Не подскажете подробнее, как мне этот конвертер скачать/поставить? Буду спасибо.
                      • +1
                        Если у вас Debian или Ubuntu: apt-get install dbf2mysql (предварить sudo для Ubuntu). Как пользоваться — сможете почитать по команде man dbf2mysql.

                        На форуме sql.ru её вызывют так:
                        dbf2mysql -d test -t dbftable -h localhost -U root -P pass -vv DOMA.DBF
                        

                        SQL-код при этом пойдёт на стандартный вывод (на монитор), добавьте перенаправление после команды для вывода в файл:
                        dbf2mysql -d test -t dbftable -h localhost -U root -P pass -vv DOMA.DBF > file.sql
                        
                        • 0
                          У меня просто линукс (по край ней мере, так сообщает команда uname -a). Как я понимаю, в этом случае репозиторий для дебиана/убунты мне не очень подходит.

                          Ладно, я попробую, наверное, под винду найти конвертер, а то вопросами замучаю :-)
              • +1
                Если указывать индекс почтового хаба или города (с тремя нулями на конце), то посылка дойдет, но чуть позже. Для Интернет-магазинов это может быть критичным, поэтому они стараются указывать как-можно более точный индекс.
                • 0
                  Чаще просто извещение никто не доставит, где посылка не знает, и она через 30 дней отправится обратно. Инфа 146%, проверено на большом количестве.
                  • +1
                    Да, такой риск есть. В целом, обсуждая вопрос доставки товара, корректность заполнения адреса многие знакомые магазины называют критически важным. Некоторые вообще считают, что нормальной практикой может являться запрос только телефона, обратный звонок, и детальное уточнение адреса доставки. В противном случае — расходы на обслуживание возврата товара могут быть существенны.
              • +1
                Эти индексы, будь они не ладны. Я, как человек довольно молодой, отправлял обычные письма всего несколько раз (и то — документы). Всегда приходится гуглить свой индекс. А вот в последний раз, несколько дней назад, я загуглил неверный индекс. Так что, идея с автоматическим проставлением индекса выглядит крайне круто. Надеюсь, когда-нибудь это станет обыденным делом.
                • +1
                  Обычно, когда приходишь в почтовое отделение, его индекс где-нибудь написан крупными буквами. Можно прямо в отделении и заполнить.
                  • 0
                    Не, я товар заказывал в интернет магазине. Там нужно было ввести индекс.
                    • +1
                      Ну тогда в следующий раз идите не в гугл, а на сайт Почты России, там есть форма для поиска индекса по адресу. :)
                      • 0
                        Ага, как раз этой формой и воспользовался. Правда, сделал я это уже после заказа — для проверки индекса :)
                        Ну, ничего, почта говорит, что посылка нормально дойдёт, правда, немного позже.
                  • 0
                    В 2GIS можно узнать индекс по адресу, просто кликнув по зданию…
                    • +3
                      Жаль, 2GIS не всегда пишет индекс ближайшего почтового отделения (которое ответственно за это здание), а только адрес центрального почтового отделения (которое на весь населённый пункт).
                    • 0
                      В США zip коды повсеместно используются и это очень удобно. Zip код написан в ID (=> всегда можно посмотреть), По нему легко найти ближайшие магазины, примерно оценить как далеко находится тот или иной район, получить предложение о скидках и прочих услугах, google maps рисует район и прочее прочее. Жаль что в России с этим большая беда.
                      • 0
                        А что такое ID?
                        • 0
                          Есть 2 термина: DL и ID.
                          DL — driver's license, по-нашему водительское удостоверение. Удостоверяет личность и даёт право управления транспортным средством.
                          ID — identification card. Это карточка, которая выглядит как DL и используется только для удостоверения личности.

                          DL и ID являются полноценными удостоверениями личности, с ними и в банк можно прийти, и на самолёте полететь. Паспорт при этом лежит дома и отдыхает.

                          В обоих прописан полный адрес проживания, включая zip код.
                          • 0
                            Спасибо за интересную информацию! Всегда приятно узнать что-то о США :)
                    • 0
                      У «Триколор-ТВ» на техподдержке используется определение нас.пункта по индексу.
                      • +1
                        Чем не устроило Получение региона и города по индексу?

                        И оно не пишет, как блондинка ;)
                        • 0
                          Эх, когда я весной искал не нашлось оно мне. И покатились велосипеды. :-)
                        • 0
                          Привет, народ. Мне для одного проекта потребовалось прикрутить эту базу, но ВСЕ НАЗВАНИЯ КАПСОМ!
                          Делюсь функцией на php, которая принимает строку (хоть капсом, хоть как) и делает каждое слово с большой буквы, кроме слов исключений, типа «район», «область» и т.д.
                          Особенности: первая буква слова в скобках тоже будет увеличена, слова могут быть разделены пробелом или тире.

                          Примеры работы, первая строка до обработки, вторая после обработки
                          • МОСКОВСКАЯ ОБЛАСТЬ, ОДИНЦОВО
                          • Московская область, Одинцово

                          • САРАТОВСКАЯ ОБЛАСТЬ, КРАСНОАРМЕЙСКИЙ РАЙОН, САДОВОЕ
                          • Саратовская область, Красноармейский район, Садовое

                          • ТЮМЕНСКАЯ ОБЛАСТЬ, ХАНТЫ-МАНСИЙСКИЙ-ЮГРА АВТОНОМНЫЙ ОКРУГ
                          • Тюменская область, Ханты-Мансийский-Югра автономный округ

                          • САХА (ЯКУТИЯ) РЕСПУБЛИКА, ЯКУТСК
                          • Саха (Якутия) Республика, Якутск

                          Код
                          /* 
                          Функция предназначена для нормализации написания названий населенных пунктов из базы индексов Почты России,
                          http://info.russianpost.ru/database/ops.html
                          Функция возвращает строку, где каждое слово пишется с большой буквы, кроме слов исключений
                          Пример:
                          МОСКОВСКАЯ ОБЛАСТЬ, ОДИНЦОВО -> Московская область, Одинцово
                          САРАТОВСКАЯ ОБЛАСТЬ, КРАСНОАРМЕЙСКИЙ РАЙОН, САДОВОЕ -> Саратовская область, Красноармейский район, Садовое
                          ТЮМЕНСКАЯ ОБЛАСТЬ, ХАНТЫ-МАНСИЙСКИЙ-ЮГРА АВТОНОМНЫЙ ОКРУГ -> Тюменская область, Ханты-Мансийский-Югра автономный округ
                          САХА (ЯКУТИЯ) РЕСПУБЛИКА, ЯКУТСК -> Саха (Якутия) Республика, Якутск
                          */
                          function RussianPost_NormalizeWords($string, $encoding='utf-8') {
                          	//Слова исключения, они не будут написаны с большой буквы
                          	$exceptions = array('автономный','автономная','округ','район','область','край','немецкий','национальный');
                          
                          	$string = mb_strtolower(trim($string), $encoding); //перевод всех букв в нижний регистр
                          	$word = preg_split('/[\s-]+/', $string, -1, PREG_SPLIT_OFFSET_CAPTURE); //Разделяем строку на отдельные слова (разделители: пробел, тире)
                          
                          	$resultstring = ''; //Результирующая строка
                          	for($i=0; $i<count($word); $i++) {
                          
                          		$substr = $word[$i][0]; //Отдельное слово из строки
                          
                          		$substr_nothooks = str_replace(array(')','(',',','.'),'',$substr); //Удаляем скобки, запятые, точки (для проверки слова на исключение)
                          		if(!in_array($substr_nothooks, $exceptions)) { //Если слова нет в исключениях, 
                          			$offset1 = ($substr[0]=='(') ? 2 : 1; //Смещение первой буквы, нужно для того, чтобы правило применялось к словам в скобках
                          			$first = mb_substr($substr, 0, $offset1, $encoding); //первая буква
                          			$rest  = mb_substr($substr, $offset1, mb_strlen($substr, $encoding), $encoding); //все кроме первой буквы 
                          			$first = mb_strtoupper($first, $encoding); //Переводим первую букву в верхний регистр
                          			$substr = $first.$rest;
                          		}
                          
                          		$splitter = (isset($word[$i+1])) ? substr($string, $word[$i+1][1]-1, 1) : ''; //Определяем разделитель между словами (пробел или тире)
                          		$resultstring .= $substr.$splitter; //Склеиваем строку из отдельных слов, устанавливаем разделители между словами (пробел или тире)
                          	}
                          	return $resultstring; //Возвращаем строку.
                          }
                          

                          • +1
                            Спасибо!
                            Я промазал и поставил вашему комментарию минус вместо плюса. Извините.
                          • 0
                            Подключил свой магазинчик к вам года с полтора назад. Даже чего-то донатил и багтречил помнится :)
                            Однако все чаще сталкиваюсь с тем, что юзер не знает своего индекса. А вот свой адрес — сюрприз! — знает всегда. То есть назревает вопрос: а можно ли как-то «повернуть процесс вспять»?
                            • 0
                              Посмотрите в сторону сервисов «Кладр в облаке» и «Дадата», они клёвые. Я на них наткнулся уже после создания этого приложения.
                              • 0
                                Спасибо, «Дадата» вроде интересные.
                                Кладр в облаке видел, он годится разве что на побаловаться.
                                • 0
                                  Не скажите, я в своё время адаптировал пример с картой и был весьма доволен, а недавно они сделали и поиск в одно поле, см. примеры. Дадата новее, выглядит интереснее, но я лично ей ещё не пользовался.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.