Поиск совпадающих частей двух строк в PHP

Я ищу простой способ найти совпадающие части двух строк в PHP (особенно в контексте URI)

Например, рассмотрим две строки:

http://2.2.2.2/~machinehost/deployment_folder/

а также

/~machinehost/deployment_folder/users/bob/settings

Мне нужно отрезать совпадающую часть этих двух строк от второй строки, в результате чего:

пользователи/боб/настройки

перед добавлением первой строки в качестве префикса, образуя абсолютный URI.

Есть ли простой способ (в PHP) сравнить две произвольные строки для сопоставления подстрок внутри них?

РЕДАКТИРОВАТЬ: как указано, я имел в виду самую длинную совпадающую подстроку, общую для обеих строк


person ubermensch    schedule 25.11.2010    source источник
comment
Какие тут критерии? Потому что технически h в http будет соответствовать h в machinehost. Вам нужно будет быть намного более конкретным, чем сопоставление подстрок.   -  person cdhowie    schedule 26.11.2010
comment
Извините, вы совершенно правы. Я имел в виду соответствие максимально длинной подстроке.   -  person ubermensch    schedule 26.11.2010


Ответы (5)


Предполагая, что ваши строки $a и $b соответственно, вы можете использовать это:

$a = 'http://2.2.2.2/~machinehost/deployment_folder/';
$b = '/~machinehost/deployment_folder/users/bob/settings';

$len_a = strlen($a);
$len_b = strlen($b);

for ($p = max(0, $len_a - $len_b); $p < $len_b; $p++)
    if (substr($a, $len_a - ($len_b - $p)) == substr($b, 0, $len_b - $p))
        break;

$result = $a.substr($b, $len_b - $p);

echo $result;

Этот результат равен http://2.2.2.2/~machinehost/deployment_folder/users/bob/settings.

person cambraca    schedule 25.11.2010

Поиск самого длинного общего совпадения также можно выполнить с помощью регулярного выражения.

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

/**
 * Determine the longest common match within two strings
 *
 * @param string $str1
 * @param string $str2 Two strings in any order.
 * @param boolean $case_sensitive Set to true to force
 * case sensitivity. Default: false (case insensitive).
 * @return string The longest string - first match.
 */
function get_longest_common_subsequence( $str1, $str2, $case_sensitive = false ) {
    // First check to see if one string is the same as the other.
    if ( $str1 === $str2 ) return $str1;
    if ( ! $case_sensitive && strtolower( $str1 ) === strtolower( $str2 ) ) return $str1;

    // We'll use '#' as our regex delimiter. Any character can be used as we'll quote the string anyway,
    $delimiter = '#';

    // We'll find the shortest string and use that to check substrings and create our regex.
    $l1 = strlen( $str1 );
    $l2 = strlen( $str2 );
    $str = $l1 <= $l2 ? $str1 : $str2;
    $str2 = $l1 <= $l2 ? $str2 : $str1;
    $l = min( $l1, $l2 );

    // Next check to see if one string is a substring of the other.
    if ( $case_sensitive ) {
        if ( strpos( $str2, $str ) !== false ) {
            return $str;
        }
    }
    else {
        if ( stripos( $str2, $str ) !== false ) {
            return $str;
        }
    }

    // Regex for each character will be of the format (?:a(?=b))?
    // We also need to capture the last character, but this prevents us from matching strings with a single character. (?:.|c)?
    $reg = $delimiter;
    for ( $i = 0; $i < $l; $i++ ) {
        $a = preg_quote( $str[ $i ], $delimiter );
        $b = $i + 1 < $l ? preg_quote( $str[ $i + 1 ], $delimiter ) : false;
        $reg .= sprintf( $b !== false ? '(?:%s(?=%s))?' : '(?:.|%s)?', $a, $b );
    }
    $reg .= $delimiter;
    if ( ! $case_sensitive ) {
        $reg .= 'i';
    }
    // Resulting example regex from a string 'abbc':
    // '#(?:a(?=b))?(?:b(?=b))?(?:b(?=c))?(?:.|c)?#i';

    // Perform our regex on the remaining string
    $str = $l1 <= $l2 ? $str2 : $str1;
    if ( preg_match_all( $reg, $str, $matches ) ) {
        // $matches is an array with a single array with all the matches.
        return array_reduce( $matches[0], function( $a, $b ) {
            $al = strlen( $a );
            $bl = strlen( $b );
            // Return the longest string, as long as it's not a single character.
            return $al >= $bl || $bl <= 1 ? $a : $b;
        }, '' );
    }

    // No match - Return an empty string.
    return '';
}

Он сгенерирует регулярное выражение, используя более короткую из двух строк, хотя производительность, скорее всего, будет одинаковой в любом случае. Он может неправильно сопоставлять строки с повторяющимися подстроками, и мы ограничены сопоставлением строк из двух или более символов, если они не равны или не являются подстрокой другого. Например:

// Works as intended.
get_longest_common_subsequence( 'abbc', 'abc' ) === 'ab';

// Returns incorrect substring based on string length and recurring substrings.
get_longest_common_subsequence( 'abbc', 'abcdef' ) === 'abc';

// Does not return any matches, as all recurring strings are only a single character long.
get_longest_common_subsequence( 'abc', 'ace' ) === '';

// One of the strings is a substring of the other.
get_longest_common_subsequence( 'abc', 'a' ) === 'a';

Несмотря на это, он работает с использованием альтернативного метода, и регулярное выражение можно уточнить для решения дополнительных ситуаций.

person Shaun Cockerill    schedule 15.12.2017
comment
В исходном коде была ошибка, из-за которой, если две строки равны, он вернет пустую строку (я изменил это с помощью редактирования). Эта функция работает очень быстро благодаря движку PCRE. Это может быть более чем в 10 раз быстрее, чем решение на чистом PHP. Особенно на длинных струнах. - person Flip; 07.03.2019
comment
Я подозреваю, что здесь есть больше крайних случаев, которые могут пойти не так. Особенно с 1 или 2 строками символов. Вы были предупреждены. - person Flip; 07.03.2019
comment
Я обновил это, чтобы учесть подстроки, которые теперь могут принимать один символ и возвращать его, если он совпадает. Проблема все еще остается, когда, если стортест строк содержит 2 одинаковых символа, он легко игнорирует символы между ними. Приветствуются любые предложения по тонкой настройке сгенерированного регулярного выражения и предотвращению этой проблемы; они будут исследованы и включены в ответ, где это возможно. - person Shaun Cockerill; 11.04.2019

Я не уверен, что понял ваш запрос полностью, но идея такова:

Пусть A будет вашим URL-адресом, а B — вашим «/~machinehost/deployment_folder/users/bob/settings».

  • поиск B в A -> вы получаете индекс i (где i - позиция первого / из B в A)
  • пусть l = длина (A)
  • Вам нужно отрезать B от (l-i) до длины (B), чтобы получить последнюю часть B (/users/bob/settings)

Я еще не проверял, но если вам действительно нужно, я могу помочь вам заставить это блестящее (ироничное) решение работать.

Обратите внимание, что это может быть возможно с регулярными выражениями, такими как

$pattern = "$B(.*?)"
$res = array();
preg_match_all($pattern, $A, $res);

Изменить: я думаю, что ваш последний комментарий делает мой ответ недействительным. Но вам нужно найти подстроки. Таким образом, вы можете сначала начать с тяжелого алгоритма, пытающегося найти B[1:i] в A для i в {2, length(B)}, а затем использовать некоторые динамическое программирование.

person Aif    schedule 25.11.2010

это не похоже на готовый код для вашего требования. Итак, давайте искать простой способ.

В этом упражнении я использовал два метода: один для поиска самого длинного совпадения, а другой для отсечения совпадающей части.

Метод FindLongestMatch() разбирает путь, по частям ищет совпадение в другом пути, оставляя только одно совпадение, самое длинное (без массивов, без сортировки). Метод RemoveLongestMatch() принимает суффикс или «остаток» после позиции с самым длинным совпадением.

Здесь полный исходный код:

<?php

function FindLongestMatch($relativePath, $absolutePath)
{
    static $_separator = '/';
    $splitted = array_reverse(explode($_separator, $absolutePath));

    foreach ($splitted as &$value)
    {
        $matchTest = $value.$_separator.$match;
        if(IsSubstring($relativePath, $matchTest))
            $match = $matchTest;

        if (!empty($value) && IsNewMatchLonger($match, $longestMatch))
            $longestMatch = $match;
    }

    return $longestMatch;
}

//Removes from the first string the longest match.
function RemoveLongestMatch($relativePath, $absolutePath)
{
    $match = findLongestMatch($relativePath, $absolutePath);
    $positionFound = strpos($relativePath, $match);     
    $suffix = substr($relativePath, $positionFound + strlen($match));

    return $suffix;
}

function IsNewMatchLonger($match, $longestMatch)
{
    return strlen($match) > strlen($longestMatch);
}

function IsSubstring($string, $subString)
{
    return strpos($string, $subString) > 0;
}

Это репрезентативное подмножество тестовых случаев:

//TEST CASES
echo "<br>-----------------------------------------------------------"; 
echo "<br>".$absolutePath = 'http://2.2.2.2/~machinehost/deployment_folder/';
echo "<br>".$relativePath = '/~machinehost/deployment_folder/users/bob/settings';
echo "<br>Longest match: ".findLongestMatch($relativePath, $absolutePath);
echo "<br>Suffix: ".removeLongestMatch($relativePath, $absolutePath);

echo "<br>-----------------------------------------------------------"; 
echo "<br>".$absolutePath = 'http://1.1.1.1/root/~machinehost/deployment_folder/';
echo "<br>".$relativePath = '/root/~machinehost/deployment_folder/users/bob/settings';
echo "<br>Longest match: ".findLongestMatch($relativePath, $absolutePath);
echo "<br>Suffix: ".removeLongestMatch($relativePath, $absolutePath);

echo "<br>-----------------------------------------------------------"; 
echo "<br>".$absolutePath = 'http://2.2.2.2/~machinehost/deployment_folder/users/';
echo "<br>".$relativePath = '/~machinehost/deployment_folder/users/bob/settings';
echo "<br>Longest match: ".findLongestMatch($relativePath, $absolutePath);
echo "<br>Suffix: ".removeLongestMatch($relativePath, $absolutePath);

echo "<br>-----------------------------------------------------------"; 
echo "<br>".$absolutePath = 'http://3.3.3.3/~machinehost/~machinehost/subDirectory/deployment_folder/';
echo "<br>".$relativePath = '/~machinehost/subDirectory/deployment_folderX/users/bob/settings';
echo "<br>Longest match: ".findLongestMatch($relativePath, $absolutePath);
echo "<br>Suffix: ".removeLongestMatch($relativePath, $absolutePath);

Выполнение предыдущих тестовых случаев дает следующий результат:

http://2.2.2.2/~machinehost/deployment_folder/
/~machinehost/deployment_folder/users/bob/settings
Longuest match: ~machinehost/deployment_folder/
Suffix: users/bob/settings

http://1.1.1.1/root/~machinehost/deployment_folder/
/root/~machinehost/deployment_folder/users/bob/settings
Longuest match: root/~machinehost/deployment_folder/
Suffix: users/bob/settings

http://2.2.2.2/~machinehost/deployment_folder/users/
/~machinehost/deployment_folder/users/bob/settings
Longuest match: ~machinehost/deployment_folder/users/
Suffix: bob/settings

http://3.3.3.3/~machinehost/~machinehost/subDirectory/deployment_folder/
/~machinehost/subDirectory/deployment_folderX/users/bob/settings
Longuest match: ~machinehost/subDirectory/
Suffix: deployment_folderX/users/bob/settings

Может быть, вы сможете взять идею этого фрагмента кода и превратить ее во что-то полезное для вашего текущего проекта. Дайте мне знать, если это сработало и для вас. Кстати, ответ мистера oreX тоже выглядит неплохо.

person Community    schedule 26.11.2010

Попробуй это.

http://pastebin.com/GqS3UiPD

person oreX    schedule 26.11.2010
comment
Привет! Ответы только по ссылкам - это не очень хорошо. Чтобы быть немедленно полезным для читателей (и избежать гниения ссылок), предоставьте хотя бы краткое изложение решения напрямую со ссылками, используемыми для предоставления дополнительной информации. Пожалуйста, обратитесь к stackoverflow.com/help/how-to-answer, если у вас есть вопросы о том, как написать хороший отвечать. - person Gustavo Straube; 30.04.2018