Как найти индексы всех вхождений одной строки в другую в JavaScript?


105

Я пытаюсь найти позиции всех вхождений строки в другой строке без учета регистра.

Например, учитывая строку:

Я научился играть на укулеле в Ливане.

и строку поиска le, я хочу получить массив:

[2, 25, 27, 33]

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

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

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

Ответы:


166
var str = "I learned to play the Ukulele in Lebanon."
var regex = /le/gi, result, indices = [];
while ( (result = regex.exec(str)) ) {
    indices.push(result.index);
}

ОБНОВИТЬ

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

function getIndicesOf(searchStr, str, caseSensitive) {
    var searchStrLen = searchStr.length;
    if (searchStrLen == 0) {
        return [];
    }
    var startIndex = 0, index, indices = [];
    if (!caseSensitive) {
        str = str.toLowerCase();
        searchStr = searchStr.toLowerCase();
    }
    while ((index = str.indexOf(searchStr, startIndex)) > -1) {
        indices.push(index);
        startIndex = index + searchStrLen;
    }
    return indices;
}

var indices = getIndicesOf("le", "I learned to play the Ukulele in Lebanon.");

document.getElementById("output").innerHTML = indices + "";
<div id="output"></div>


2
Как здесь может leбыть переменная строка? Даже при использовании new Regexp(str);специальных символов таится опасность, например поиск $2.50. regex = new Regexp(dynamicstring.replace(/([\\.+*?\\[^\\]$(){}=!<>|:])/g, '\\$1'));ИМХО было бы что-то вроде бы поближе. Я не уверен, есть ли в js встроенный механизм экранирования регулярных выражений.
Wrikken 04

new RegExp(searchStr)будет так, и да, в общем случае вам придется избегать специальных символов. На самом деле этого не стоит делать, если вам не нужен такой уровень общности.
Tim Down

1
Отличный ответ и очень полезный. Большое спасибо, Тим!
Bungle

1
Если строка поиска является пустой строкой, вы получите бесконечный цикл ... выполните проверку.
HelpMeStackOverflowMyOnlyHope

2
Допустим searchStr=aaaи что str=aaaaaa. Тогда вместо того, чтобы найти 4 случая, ваш код найдет только 2, потому что вы делаете пропуски searchStr.lengthв цикле.
вспыхивает

18

Вот бесплатная версия регулярных выражений:

function indexes(source, find) {
  if (!source) {
    return [];
  }
  // if find is empty string return all indexes.
  if (!find) {
    // or shorter arrow function:
    // return source.split('').map((_,i) => i);
    return source.split('').map(function(_, i) { return i; });
  }
  var result = [];
  for (i = 0; i < source.length; ++i) {
    // If you want to search case insensitive use 
    // if (source.substring(i, i + find.length).toLowerCase() == find) {
    if (source.substring(i, i + find.length) == find) {
      result.push(i);
    }
  }
  return result;
}

indexes("I learned to play the Ukulele in Lebanon.", "le")

РЕДАКТИРОВАТЬ : и если вы хотите сопоставить такие строки, как 'aaaa' и 'aa', чтобы найти [0, 2], используйте эту версию:

function indexes(source, find) {
  if (!source) {
    return [];
  }
  if (!find) {
      return source.split('').map(function(_, i) { return i; });
  }
  var result = [];
  var i = 0;
  while(i < source.length) {
    if (source.substring(i, i + find.length) == find) {
      result.push(i);
      i += find.length;
    } else {
      i++;
    }
  }
  return result;
}

7
+1. Я провел несколько тестов для сравнения с решением, использующим Regex. Самым быстрым методом был метод с использованием Regex: jsperf.com/javascript-find-all
StuR,

1
Самый быстрый метод - использовать indexOf jsperf.com/find-o-substrings
Итан Яньцзя Ли

@LiEthan это будет иметь значение только в том случае, если эта функция является узким местом и, возможно, если входная строка длинная.
jcubic

@jcubic Ваше решение кажется хорошим, но есть небольшая путаница. Что, если я вызову такую ​​функцию var result = indexes('aaaa', 'aa')? Ожидаемый результат должен быть [0, 1, 2]или [0, 2]?
Cao Mạnh Quang

@ CaoMạnhQuang смотрит на код первый результат. Если вам нужен второй, вам нужно создать цикл while и внутри, если вы i+=find.length;i++
вставите

15

Вы точно можете это сделать!

//make a regular expression out of your needle
var needle = 'le'
var re = new RegExp(needle,'gi');
var haystack = 'I learned to play the Ukulele';

var results = new Array();//this is the results you want
while (re.exec(haystack)){
  results.push(re.lastIndex);
}

Изменить: научиться писать RegExp

Кроме того, я понял, что это не совсем то , что вы хотите, поскольку это lastIndexговорит нам о конце иглы, а не о начале, но это близко - вы можете вставить re.lastIndex-needle.lengthв массив результатов ...

Изменить: добавление ссылки

Ответ @Tim Down использует объект результатов из RegExp.exec (), и все мои ресурсы Javascript затушевывают его использование (кроме предоставления вам совпадающей строки). Так что когда он использует result.index, это какой-то безымянный Match Object. В MDC-описании exec этот объект на самом деле описан достаточно подробно.


Ха! В любом случае спасибо за участие - я ценю это!
Bungle

9

Один лайнер с использованием String.protype.matchAll(ES2020):

[...sourceStr.matchAll(new RegExp(searchStr, 'gi'))].map(a => a.index)

Используя ваши ценности:

const sourceStr = 'I learned to play the Ukulele in Lebanon.';
const searchStr = 'le';
const indexes = [...sourceStr.matchAll(new RegExp(searchStr, 'gi'))].map(a => a.index);
console.log(indexes); // [2, 25, 27, 33]

Если вы беспокоитесь о том, чтобы сделать разворот и map()в одну строку, я запустил его с for...ofциклом для миллиона итераций (используя ваши строки). Один лайнер в среднем составляет 1420 мс, а for...ofна моей машине - 1150 мс. Это немаловажная разница, но один лайнер будет работать нормально, если вы проведете только несколько совпадений.

Смотрите matchAllна канюсе


3

Если вы просто хотите найти положение всех совпадений, я хотел бы указать вам на небольшой прием:

var haystack = 'I learned to play the Ukulele in Lebanon.',
    needle = 'le',
    splitOnFound = haystack.split(needle).map(function (culm)
    {
        return this.pos += culm.length + needle.length
    }, {pos: -needle.length}).slice(0, -1); // {pos: ...} – Object wich is used as this

console.log(splitOnFound);

Это может быть неприменимо, если у вас есть RegExp с переменной длиной, но для некоторых это может быть полезно.

Это чувствительно к регистру. Для нечувствительности к регистру используйте String.toLowerCaseфункцию before.


Думаю, ваш ответ лучший, потому что использование RegExp опасно.
Бхарата

1

Вот простой фрагмент кода:

function getIndexOfSubStr(str, searchToken, preIndex, output) {
    var result = str.match(searchToken);
    if (result) {
        output.push(result.index +preIndex);
        str=str.substring(result.index+searchToken.length);
        getIndexOfSubStr(str, searchToken, preIndex, output)
    }
    return output;
}

var str = "my name is 'xyz' and my school name is 'xyz' and my area name is 'xyz' ";
var searchToken ="my";
var preIndex = 0;

console.log(getIndexOfSubStr(str, searchToken, preIndex, []));


0

Следуйте ответ @jcubic, его решение вызвало небольшие путает для моего случая
К примеру var result = indexes('aaaa', 'aa')он будет возвращать [0, 1, 2]вместо [0, 2]
Так что я обновил прикусило решение , как показано ниже , чтобы соответствовать моему случаю

function indexes(text, subText, caseSensitive) {
    var _source = text;
    var _find = subText;
    if (caseSensitive != true) {
        _source = _source.toLowerCase();
        _find = _find.toLowerCase();
    }
    var result = [];
    for (var i = 0; i < _source.length;) {
        if (_source.substring(i, i + _find.length) == _find) {
            result.push(i);
            i += _find.length;  // found a subText, skip to next position
        } else {
            i += 1;
        }
    }
    return result;
}

0

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

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

function findRegexIndices(text, needle, caseSensitive){
  var needleLen = needle.length,
    reg = new RegExp(needle, caseSensitive ? 'gi' : 'g'),
    indices = [],
    result;

  while ( (result = reg.exec(text)) ) {
    indices.push([result.index, result.index + needleLen]);
  }
  return indices
}

0

Проверьте это решение, которое также сможет найти такую ​​же строку символов, дайте мне знать, если что-то отсутствует или не так.

function indexes(source, find) {
    if (!source) {
      return [];
    }
    if (!find) {
        return source.split('').map(function(_, i) { return i; });
    }
    source = source.toLowerCase();
    find = find.toLowerCase();
    var result = [];
    var i = 0;
    while(i < source.length) {
      if (source.substring(i, i + find.length) == find)
        result.push(i++);
      else
        i++
    }
    return result;
  }
  console.log(indexes('aaaaaaaa', 'aaaaaa'))
  console.log(indexes('aeeaaaaadjfhfnaaaaadjddjaa', 'aaaa'))
  console.log(indexes('wordgoodwordgoodgoodbestword', 'wordgood'))
  console.log(indexes('I learned to play the Ukulele in Lebanon.', 'le'))


0

Вот мое бесплатное решение с регулярными выражениями.

const findOccurrences = (fullStr, searchStr) => {
    const fullString = fullStr.toLowerCase();
    const searchString = searchStr.toLowerCase();
    const possibleOccurrences = [];
    const occurrenceIndices = [];

    for (let i = 0; i <= fullString.length - searchString.length; i++) {
        possibleOccurrences.push(fullString.slice(i, i + searchString.length));
    }
    for (let k = 0; k < possibleOccurrences.length; k++) {
        if (possibleOccurrences[k] === searchString) {
            occurrenceIndices.push(k);
        }
    }

    return occurrenceIndices;
}

0

Я бы порекомендовал ответ Тима. Однако @blazs утверждает: «Предположим, что searchStr = aaa и что str = aaaaaa. Тогда вместо поиска 4 вхождений ваш код найдет только 2, потому что вы делаете пропуски по searchStr.length в цикле», что верно, если посмотреть на Код Тима, а именно эта строка здесь: startIndex = index + searchStrLen; Код Тима не сможет найти экземпляр строки, в которой выполняется поиск, которая находится в пределах самой длины. Итак, я изменил ответ Тима:

function getIndicesOf(searchStr, str, caseSensitive) {
    var startIndex = 0, index, indices = [];
    if (!caseSensitive) {
        str = str.toLowerCase();
        searchStr = searchStr.toLowerCase();
    }
    while ((index = str.indexOf(searchStr, startIndex)) > -1) {
        indices.push(index);
        startIndex = index + 1;
    }
    return indices;
}

var indices = getIndicesOf("le", "I learned to play the Ukulele in Lebanon.");

document.getElementById("output").innerHTML = indices + "";
<div id="output"></div>

Изменение его на «+ 1» вместо «+ searchStrLen» позволит индексу 1 быть в массиве индексов, если у меня есть str «aaaa» и searchStr «aa».

Второй ответ

У меня есть еще один фрагмент кода, который тоже работает. Он имитирует ответ, предоставленный @YuJie, за исключением того, что этот более компактный, чем ответ Ю Джи:

function getIndicesOf(searchStr, str, caseSensitive) {
    var startIndex = 0, index, indices = [];
    if (!caseSensitive) {
        str = str.toLowerCase();
        searchStr = searchStr.toLowerCase();
    }
    for (var i=0; i<str.length-1; i++) {
        if (str.substr(i, searchStr.length) == searchStr) {
            indices.push(i);
        }
    }
    return indices;
}

var indices = getIndicesOf("le", "I learned to play the Ukulele in Lebanon.");

document.getElementById("output").innerHTML = indices + "";
<div id="output"></div>

Однако недостатком этого фрагмента является то, что он может занять немного больше времени, чем первый, поскольку первый использует встроенную функцию JavaScript indexOf (), тогда как второй немного похож на старую поговорку "re -изобретая колесо ". Итак, в целом, я бы порекомендовал свой первый ответ вместо этого. PS Если кому-то нужны комментарии в коде, объясняющие, как он работает, скажите об этом, и я буду рад ответить на запрос.


-1
function countInString(searchFor,searchIn){

 var results=0;
 var a=searchIn.indexOf(searchFor)

 while(a!=-1){
   searchIn=searchIn.slice(a*1+searchFor.length);
   results++;
   a=searchIn.indexOf(searchFor);
 }

return results;

}

Это ищет вхождения строки внутри другой строки, а не регулярных выражений.

-1

приведенный ниже код сделает эту работу за вас:

function indexes(source, find) {
  var result = [];
  for(i=0;i<str.length; ++i) {
    // If you want to search case insensitive use 
    // if (source.substring(i, i + find.length).toLowerCase() == find) {
    if (source.substring(i, i + find.length) == find) {
      result.push(i);
    }
  }
  return result;
}

indexes("hello, how are you", "ar")

-2

Используйте String.prototype.match .

Вот пример из самой документации MDN:

var str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
var regexp = /[A-E]/gi;
var matches_array = str.match(regexp);

console.log(matches_array);
// ['A', 'B', 'C', 'D', 'E', 'a', 'b', 'c', 'd', 'e']

Это довольно просто.
igaurav

11
Вопрос в том, как найти индексы явлений, а не самих событий!
Luckylooke

1
несмотря на то, что этот ответ не соответствует вопросу, но это то, что я искал :)
AlexNikonov
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.