Как разбить поле «ФИО» на имя, фамилию и отчество в PHP

Бывают случаи, когда ФИО пользователей на сайте задавалось с помощью единственного поля «ФИО», а после какого-то времени, в ходе рефакторинга базы, решили сделать 3 поля — Фамилия, Имя, Отчество. Как же быть со старыми данными? Ведь пользователи вводят туда всё что угодно. Попадаются такие ФИО как «Иванов К.М.», «пЕтрова ивгения», «Константин пАВЛОВ директор». Как же выделить из этих «ФИО» осмысленную информацию, настоящие имя, фамилию и отчество, чтобы заполнить соответствующие поля?

Когда я столкнулся с этой проблемой, я не нашел готового решения, поэтому написал инструмент сам.

Библиотека анализа и нечёткого поиска в строке фамилии, имени, отчества.

Библиотека написана на PHP и использует базу русских имен, фамилий, и отчеств (женских и мужских), взятых из википедии, и дополненных еще несколькими десятками вручную. Если вам нужно определять также другие ФИО (например, грузинские, таджикские, казахские), нужно будет дополнить базы слов в соответствующих файлах.

Взять библиотеку можно здесь: https://github.com/MihanEntalpo/FIO-Analyzer

Установка с помощью composer:

В вашем файле composer.json нужно прописать:

{
    "require": {
        "mihanentalpo/fio-analyzer": "*"
    }
}

Установить:

composer.phar install

Для подключения достаточно написать:

<?php 
require_once("../vendor/autoload.php");
$analyzer = new \Mihanentalpo\FioAnalyzer\FioAnalyzer();

А для выполнения анализа, натравить метод break_apart на ваши входные данные:

<?php
//Намеренно добавлены лишние слова и знаки, размер букв также специально такой. 
$src = "Иванов иван Петрович главный инженер.,"; 
$fio = $analyzer->break_apart($src);
print_r($fio);

В результате будет возвращена следующая структура:

Array
(
    [first_name] => Array
        (
            [src] => иван
            [found] => иван
            [percent] => 1
        )
    [second_name] => Array
        (
            [src] => Петрович
            [found] => петрович
            [percent] => 1
        )
    [last_name] => Array
        (
            [src] => Иванов
            [found] => иванов
            [percent] => 1
        )
)

где поля «first_name», «second_name» и «last_name» — это данные соответственно о имени, отчестве, фамилии, найденных в переданной строке. Если чего-то из них найти не удалось, тогда этого поля просто не будет в возвращаемом массиве.
Поле «src» — этот тот текст в исходной строке, по которому проводилось сравнение.
Поле «found» — найденное значение из базы имен/фамилий/отчеств, в нижнем регистре.
Поле «percent» — процент соответствия между src и found. В данном примере он равен 1, но может быть и меньше, если есть отличия в буквах.

Функционал класса

Основная функция break_apart:

/**
* @param string $fio Строка с ФИО
* @param float $edge граница совпадения имени, фамилии, и отчества, число от 0 до 1
**/
function break_apart( $fio, $edge = 0.75)

Как работает алгоритм?

1. Переданная фраза, например «Иванов иван Петрович главный инженер.,», очищается от мусора, и разбивается на массив слов.
2. По каждому слову производится поиск среди всего множества имён, фамилий и отчеств, и выбирается наиболее похожее имя, фамилия и отчество, с учётом порогового значения, то есть совпадение менее заданного процента будут считаться недостоверными.
3. Поскольку имя, фамилия и отчество в строке ФИО должны встречаться только по одному разу, собираются все возможные комбинации значений слов, когда, например, строка представляет собой «имя,фамилия,отчество, шум» (где «шум» — это просто постороннее слово, не являющееся ни именем, ни фамилией, ни отчеством), «имя, шум, отчество, фамилия», «фамилия, шум, имя, шум, отчество» и так далее, и выбирается тот вариант, где суммарный процент совпадения между словами, и их найденными «именами»,»фамилиями» и «отчествами» максимален.
4. Этот набор и возвращается в качестве результата

Нечёткий поиск

Для нечёткого поиска по именам, фамилиям, отчествам используется класс FastFuzzySearch, описанный мной в другой статье.

Оптимизация инициализации

Первая инициализация класса FioAnalyzer может занять некоторое время, поскольку проводится инициализация объектов нечёткого поиска FastFuzzySearch. Каждый из них загружает массив имён, фамилий и отчеств соответственно. После инициализации будут созданы файлы first_names.php.index, second_names.php.index и last_names.php.index, из которых в следующий раз будет проводиться быстрая загрузка данных для FastFuzzySearch


6 комментариев

  • Ответить Андрей |

    Хорошая, полезная библиотека.
    Со своей стороны хочу предложить подход, несколько убыстряющий процесс разбора, да и менее ресурсоемкий.

    Подготовка:
    — Определяем, что строка состоит из 3-х слов.
    — Ищем, какое из них заканчивается на -вич или -вна
    — Предполагаем, что это отчество (выводим пока за скобки фамилии типа Войнович)
    — Если «отчество» на 2-ом месте, то первым будет «имя», или если «отчество» на 3-ем месте, то на 2-ом «имя».

    Если «отчества» получилось 2 (например Войнович Тамара Васильевна), то исходим из русской грамматики, что отчество обычно пишется или на 2-ом или на 3-ем месте. (Тамара Васильевна Войнович)

    Мне кажется, что такой логический разбор можно вставить перед прогоном по массиву имен-фамилий-отчеств

    • Ответить mihanentalpo |

      Разумеется, данную методику можно ускорить, однако сужается её применимость.
      Приведу примеры:

      > Определяем, что строка состоит из 3-х слов.

      Что делать есть слов не 3 ? Проблема в том, что, если пользователю дана полная свобода самовыражения в полях ввода, он обязательно введёт там всё что угодно 🙂

      > Ищем, какое из них заканчивается на -вич или -вна

      Могут быть опечатки, например вместо «-вич» будет окончание «-вчи», или «-вч». В этом случае наверное предполагается возвращаться к стандартному алгоритму?

      > Предполагаем, что это отчество (выводим пока за скобки фамилии типа Войнович)
      > Если «отчество» на 2-ом месте, то первым будет «имя», или если «отчество» на 3-ем месте, то на 2-ом «имя».

      Места расположения, к сожалению, вообще ни о чём ни говорят, на каком месте что записано — абсолютно случайная величина 🙂

      Приведу примеры ФИО, с которыми доводилось столкнуться данной библиотеке:

      Это ИМЕННО ТО что люди вписывали в поле ФИО.

      «Звоните по многоканальным телефонам, skype или ICQ.»
      «Начальник отдела Марина Иванова, менеджер отдела оптовых продаж Светлана.»
      «Барасов Николай (добавляйте в SkyPe и ICQ отвечу на любые вопросы)»
      «Арман (Просьба звонить с 7- до 20-00 Москв.время)»
      «менеджер по работе с клиентами Андрей»
      «Марк.Сергей.Руслан. Павел. Влад.Иван.»
      «Любовь Павловна, почта: camillashop @ mail . ru»
      «Денис Викторович (отдел продаж)»

      да, было много и нормально введённых ФИО, на которых предложенное вами улучшение сработает, но разве это интересно 🙂

  • Ответить Евгений |

    Не стабильно работающее решение,
    Например Наталья Захаровна Ходаковская путает отчество и фамилию.
    Да и завязываться на справочники можно только в именах я думаю, остальное анализировать другими способами

    • Ответить mihanentalpo |

      Возможно фамилия или отчество из приведенного примера как раз отсутствует в справочнике.
      Если же присутствует, тогда можно сделать доработку позволяющую для определенных фамилии и отчества стоящих вместе правильно расставлять значения.
      «Другие способы» — это например какие?

  • Ответить Денис |

    Неверно работает алгоритм. Получил следующий результат от библиотеки:
    ——
    array(3) {
    ‘second_name’ =>
    array(3) {
    ‘src’ =>
    string(8) «Осипович»
    ‘found’ =>
    string(10) «Осиповичев»
    ‘percent’ =>
    double(0.75)
    }
    ‘first_name’ =>
    array(3) {
    ‘src’ =>
    string(7) «Татьяна»
    ‘found’ =>
    string(7) «Татьяна»
    ‘percent’ =>
    int(1)
    }
    ‘last_name’ =>
    array(3) {
    ‘src’ =>
    string(10) «Викторовна»
    ‘found’ =>
    string(10) «Викторовна»
    ‘percent’ =>
    int(1)
    }
    }
    ——
    По документации last_name — это фамилия, а библиотека выдает отчество со 100%-ым совпадением)

    У вас кажется перепутаны местами last_name и second_name в реализации.

    • Ответить mihanentalpo |

      Да, похоже в коде сделана правка из-за которой последовательность перепутана.

      $typeNames = array( ‘first_name’, ‘second_name’, ‘last_name’ );
      ……
      $found[0] = $this->searchIn( «first_names», $p, $min, $max, $edge );
      $found[1] = $this->searchIn( «last_names», $p, $min, $max, $edge );
      $found[2] = $this->searchIn( «second_names», $p, $min, $max, $edge );
      ……

      То есть, поле last_name имеющее индекс 2, получает значение из массива second_names.

      Надо будет поправить.

Оставить комментарий