Подтвердить что ты не робот

PHP Regex Проверьте, имеют ли две строки два общих символа

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

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

Вот проблема. Я получил большой файл, каждая строка которого имеет ровно 4 символа.

Это регулярное выражение, которое определяет "правильные" строки:

"/^[AB][CD][EF][GH]$/m" 

На английском языке каждая строка имеет либо A, либо B в позиции 0, либо C, либо D в позиции 1, либо E или F в позиции 2, либо G или H в позиции 3. Я могу предположить, что каждая строка будет ровно 4 символа.

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

В приведенном ниже примере предполагается следующее:

  • $line всегда является допустимым форматом
  • BigFileOfLines.txt содержит только допустимые строки

Пример:

// Matches all other lines in string that share 2 or more characters in common
// with "$line"
function findMatchingLines($line, $subject) {
    $regex = "magic regex I'm looking for here";
    $matchingLines = array();
    preg_match_all($regex, $subject, $matchingLines);
    return $matchingLines;
}

// Example Usage
$fileContents = file_get_contents("BigFileOfLines.txt");
$matchingLines = findMatchingLines("ACFG", $fileContents);

/*
 * Desired return value (Note: this is an example set, there 
 * could be more or less than this)
 * 
 * BCEG
 * ADFG
 * BCFG
 * BDFG
*/

Один из способов, которым я знаю, что будет работать, - иметь регулярное выражение, подобное следующему (следующее регулярное выражение будет работать только для "ACFG":

"/^(?:AC.{2}|.CF.|.{2}FG|A.F.|A.{2}G|.C.G)$/m"

Это работает хорошо, производительность приемлема. Меня беспокоит то, что мне приходится генерировать это на основе $line, где я предпочел бы, чтобы он не знал, что такое конкретный параметр. Кроме того, это решение плохо масштабируется, если позже код будет изменен, чтобы соответствовать словам, 3 или более символов, или если размер каждой строки увеличивается от 4 до 16.

Просто кажется, что там что-то замечательное, что я пропускаю. Также кажется, что это может быть дублирующим вопросом, но ни один из других вопросов, на которые я смотрел, по-видимому, не затрагивает эту конкретную проблему.

Спасибо заранее!

Update:

Кажется, что норма с ответами Regex предназначена для пользователей SO просто публиковать регулярное выражение и сказать: "Это должно работать для вас".

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

  • A. Работает
  • В. Является наиболее эффективным (я считаю, что существует достаточное количество предположений, которые могут быть сделаны относительно строки темы, что можно сделать достаточную оптимизацию).

Конечно, если вы дадите ответ, который работает, и никто не отправит ответ * с * решением, я помечаю его как ответ:)

Обновление 2:

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

Причины моего одобрения:

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

Тем не менее, многие из них относятся к приведенным ниже ответам, а также за то, что они очень тщательно объясняют, почему их решение является лучшим. Если вы столкнулись с этим вопросом, потому что это то, что вы пытаетесь выяснить, пожалуйста, дайте им все прочитать, мне очень помогли.

4b9b3361

Ответ 1

Почему бы вам просто не использовать это регулярное выражение $regex = "/.*[$line].*[$line].*/m";?

В вашем примере это означает $regex = "/.*[ACFG].*[ACFG].*/m";

Ответ 2

Это регулярное выражение, которое определяет "правильные" строки:

/^[A|B]{1}|[C|D]{1}|[E|F]{1}|[G|H]{1}$/m

На английском языке каждая строка имеет либо A, либо B в позиции 0, либо C, либо D в положении 1, либо E, либо F в положении 2, либо G или H в 3. Я могу предположить, что каждая строка будет ровно 4 символа долго.

Это не то, что означает это регулярное выражение. Это регулярное выражение означает, что каждая строка имеет либо A, либо B, либо трубу в позиции 0, C или D или трубу в положении 1 и т.д.; [A|B] означает "либо" A ", либо" | "или" B ". '|' только означает" или" за пределами классов символов.

Кроме того, {1} является no-op; без какого-либо квантификатора, все должно появиться ровно один раз. Поэтому правильное регулярное выражение для вышеприведенного английского:

/^[AB][CD][EF][GH]$/

или, альтернативно:

/^(A|B)(C|D)(E|F)(G|H)$/

В этом случае у второго есть побочный эффект захвата буквы в каждой позиции, так что первая захваченная группа сообщит вам, был ли первый символ A или B и так далее. Если вы не хотите захвата, вы можете использовать группировку без захвата:

/^(?:A|B)(?:C|D)(?:E|F)(?:G|H)$/

Но версия символьного класса, безусловно, является обычным способом записи.

Что касается вашей проблемы, она не подходит для регулярных выражений; к тому моменту, когда вы деконструируете строку, верните ее обратно в соответствующем синтаксисе regex, скомпилируйте регулярное выражение и выполните тест, вам, вероятно, было бы намного лучше, чем просто сравнение персонажа по символу.

Я бы переписал ваше регулярное выражение ACFG таким образом: /^(?:AC|A.F|A..G|.CF|.C.G|..FG)$/, но это просто внешний вид; Я не могу придумать лучшего решения, использующего регулярное выражение. (Хотя, как указал Майк Райан, было бы лучше, чем /^(?:A(?:C|.E|..G))|(?:.C(?:E|.G))|(?:..EG)$/, но это все же то же самое решение, только в более эффективно обработанной форме.)

Ответ 3

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

function findMatchingLines($line) {
    static $file = null;
    if( !$file) $file = file("BigFileOfLines.txt");

    $search = str_split($line);
    foreach($file as $l) {
        $test = str_split($l);
        $matches = count(array_intersect($search,$test));
        if( $matches > 2) // define number of matches required here - optionally make it an argument
            return true;
    }
    // no matches
    return false;
}

Ответ 4

Люди могут быть смущены вашим первым регулярным выражением. Вы даете:

"/^[A|B]{1}|[C|D]{1}|[E|F]{1}|[G|H]{1}$/m" 

И затем скажите:

На английском языке каждая строка имеет либо A, либо B в позиции 0, либо C, либо D в позиции 1, либо E или F в позиции 2, либо G или H в позиции 3. Я могу предположить, что каждая строка будет ровно 4 символа.

Но это не то, что означает это регулярное выражение.

Это связано с тем, что оператор | имеет наивысший приоритет здесь. Итак, на самом деле то, что на самом деле говорит это регулярное выражение, на английском языке: Либо A, либо | или B в первой позиции, OR C или | или D в первой позиции, OR E или | или F в первой позиции, OR G или '| or H` в первой позиции.

Это потому, что [A|B] означает класс символов с одним из трех заданных символов (включая |. И поскольку {1} означает один символ (он также полностью лишний и может быть отброшен), а потому, что external | чередуется между всем вокруг. В моем выражении на английском языке над каждым заглавным OR стоит один из ваших чередующихся |. (И я начал подсчитывать позиции в 1, а не 0 - мне не хотелось набирать текст 0-я позиция.)

Чтобы получить описание на английском языке как регулярное выражение, вам нужно:

/^[AB][CD][EF][GH]$/

Регулярное выражение пройдет и проверит первую позицию для A или B (в классе символов), затем проверьте C или D в следующей позиции и т.д.

-

ИЗМЕНИТЬ:

Вы хотите протестировать только два из этих четырех символов.

Очень строго говоря, и, исходя из ответа @Mark Reed, самым быстрым регулярным выражением (после его анализа), вероятно, будет:

/^(A(C|.E|..G))|(.C(E)|(.G))|(..EG)$/

по сравнению с:

/^(AC|A.E|A..G|.CE|.C.G|..EG)$/ 

Это связано с тем, как реализация регулярного выражения проходит через текст. Сначала проверьте, находится ли A в первой позиции. Если это удастся, вы проверите подсерии. Если это не удастся, то вы закончите со всеми этими возможными случаями (или которые есть 3). Если у вас еще нет соответствия, вы затем проверяете, находится ли C во 2-й позиции. Если это удастся, вы проверите два подслучая. И если никто из них не достигнет успеха, вы проверите "EG на 3-й и 4-й позиции".

Это регулярное выражение специально создано для отказа как можно быстрее. Перечисление каждого случая отдельно, означает потерпеть неудачу, вы бы испытали 6 разных случаев (каждый из шести альтернатив) вместо трех случаев (как минимум). А в случаях, когда A не является первой позицией, вы должны немедленно перейти к проверке 2-й позиции, не нажимая ее еще два раза. Etc.

(Обратите внимание, что я точно не знаю, как PHP компилирует регулярное выражение - возможно, что они скомпилируются в одно и то же внутреннее представление, хотя я подозреваю, что нет.)

-

РЕДАКТИРОВАТЬ: В дополнительной точке. Быстрое регулярное выражение - несколько двусмысленный термин. Самый быстрый, чтобы потерпеть неудачу? Быстрее добиться успеха? И учитывая, какой возможный диапазон данных выборки последующих и неудачных строк? Все они должны быть уточнены, чтобы действительно определить, какие критерии вы имеете в виду самым быстрым.

Ответ 5

Здесь что-то, что использует расстояние Levenshtein вместо регулярного выражения и должно быть достаточно расширяемо для ваших требований:

$lines = array_map('rtrim', file('file.txt')); // load file into array removing \n
$common = 2; // number of common characters required
$match = 'ACFG'; // string to match

$matchingLines = array_filter($lines, function ($line) use ($common, $match) {
    // error checking here if necessary - $line and $match must be same length
    return (levenshtein($line, $match) <= (strlen($line) - $common));
});

var_dump($matchingLines);

Ответ 6

Существует 6 возможностей, по меньшей мере, двух символов, совпадающих с 4: MM.., MM, M..M,.MM.,.MM и..MM( "M" означает совпадение и "." что означает несоответствие).

Итак, вам нужно только преобразовать свой ввод в регулярное выражение, которое соответствует любой из этих возможностей. Для ввода ACFG вы должны использовать это:

"/^(AC..|A.F.|A..G|.CF.|.C.G|..FG)$/m"

Это, конечно же, вывод, к которому вы уже пришли - так хорошо до сих пор.

Основная проблема заключается в том, что Regex не является языком для сравнения two strings, это язык для сравнения строки с шаблоном. Таким образом, либо ваша строка сравнения должна быть частью шаблона (который вы уже нашли), либо он должен быть частью ввода. Последний метод позволит вам использовать совпадение общего назначения, но требует от вас блокировки ввода.

function findMatchingLines($line, $subject) {
  $regex = "/(?<=^([AB])([CD])([EF])([GH])[.\n]+)"
      + "(\1\2..|\1.\3.|\1..\4|.\2\3.|.\2.\4|..\3\4)/m";
  $matchingLines = array();
  preg_match_all($regex, $line + "\n" + $subject, $matchingLines);
  return $matchingLines;
}

Что делает эта функция, это предварительная подстановка вашей строки ввода с помощью строки, с которой вы хотите сопоставить, затем использует шаблон, который сравнивает каждую строку после первой строки (после + после [.\n]) назад к первая строка 4 символа.

Если вы также хотите проверить соответствующие строки на "правила", просто замените . в каждом шаблоне на соответствующий класс символов (\1\2[EF][GH] и т.д.).

Ответ 7

Вчера я поставил вопрос вчера вечером, чтобы опубликовать ответ, но кажется, что я немного опаздываю ^^ Вот мое решение в любом случае:

/^[^ACFG]*+(?:[ACFG][^ACFG]*+){2}$/m

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

Может быть сгенерирован с использованием:

function getRegexMatchingNCharactersOfLine($line, $num) {
    return "/^[^$line]*+(?:[$line][^$line]*+){$num}$/m";
}