Пользователь
0,0
рейтинг
15 августа 2013 в 05:20

Разработка → Используем почтовые индексы в своём приложении во благо 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


Спасибо за внимание.
Новиков Андрей @Envek
карма
81,3
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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
                  Если вы работаете с MySQL, то наверняка используете и PHP, для него существует расширение dBase для работы с dbf: http://php.net/manual/ru/ref.dbase.php
                • +2
                  image
                  • 0
                    У мея Красная Шапка 4.4.7-3
    • +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
          Не скажите, я в своё время адаптировал пример с картой и был весьма доволен, а недавно они сделали и поиск в одно поле, см. примеры. Дадата новее, выглядит интереснее, но я лично ей ещё не пользовался.

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