如何在JavaScript中查找一个字符串在另一个字符串中的所有出现位置的索引?

159
我想要查找一个字符串在另一个字符串中出现的所有位置,而且不区分大小写。
例如,给定字符串:
I learned to play the Ukulele in Lebanon.
和搜索字符串le,我想要得到数组:
[2, 25, 27, 33]

两个字符串都将是变量 - 即不能硬编码它们的值。

我认为这对于正则表达式来说应该很容易,但是在苦苦挣扎一段时间后,我没有找到可行的解决方法。

我发现了这个示例,展示了如何使用.indexOf()实现,但肯定有更简洁的方法吧?

19个回答

209
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>


4
在这里,“le”如何成为一个变量字符串?即使使用“new Regexp(str);”,特殊字符的危险仍然潜伏着,例如搜索“$2.50”。像“regex = new Regexp(dynamicstring.replace(/([\ .+*?\ [^\ \] $(){}=!<>|: ])/ g,'\ $ 1'));”这样的代码更接近我的想法。我不确定js是否有内置的正则表达式转义机制。 - Wrikken
1
啊,我知道了:我没有在问题中注意到原帖确实需要那种普遍性水平。需要重写一下... - Tim Down
1
如果搜索字符串为空字符串,则会进入无限循环...应该对其进行检查。 - HelpMeStackOverflowMyOnlyHope
5
假设 searchStr=aaa,并且 str=aaaaaa。那么你的代码只能找到2个匹配项,而不是4个,因为在循环中你正在跳过 searchStr.length - blazs
1
@blazs:没错,但这可能是期望的行为。从问题中无法确定。 - Tim Down
显示剩余4条评论

58

使用String.prototype.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]

如果你担心在一行中执行spread和map()操作,我使用for...of循环对使用您的字符串进行了100万次迭代。这个一行代码的平均时间是1420ms,而for...of的平均时间是1150ms。这不是一个微不足道的差异,但如果你只需要做几个匹配,一行代码就可以正常工作。 请参见caniuse上的matchAll

23

这是一个不需要正则表达式的版本:

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
我进行了一些测试以与使用正则表达式的解决方案进行比较。速度最快的方法是使用正则表达式的方法:http://jsperf.com/javascript-find-all - StuR
1
最快的方法是使用indexOf https://jsperf.com/find-o-substrings - Ethan Yanjia Li
@LiEthan 只有当该函数成为瓶颈并且输入字符串很长时才会有影响。 - jcubic
@jcubic,你的解决方案看起来很好,但是有一个小疑问。如果我像这样调用函数“var result = indexes('aaaa', 'aa')”,期望的结果应该是“[0, 1, 2]”还是“[0, 2]”? - Cao Mạnh Quang
@CaoMạnhQuang 查看代码第一个结果。如果你想要第二个结果,你需要创建while循环,在其中用i+=find.length;代替if语句,否则使用i++ - jcubic
显示剩余2条评论

22

你肯定可以做到这件事!

//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);
}

编辑:学会拼写正则表达式

另外,我意识到这并不完全是你想要的,因为lastIndex告诉我们针的结尾而不是开头,但它很接近——你可以把 re.lastIndex-needle.length 推入结果数组中...

编辑:添加链接

@Tim Down的答案使用了RegExp.exec()的results对象,而我所有的JavaScript资源都忽略了它的用法(除了给出匹配的字符串)。所以当他使用result.index时,那是某种未命名的Match Object。在MDC exec描述中,他们实际上很详细地描述了这个对象。


哈!无论如何感谢您的贡献 - 我很感激! - Bungle

8

我有点晚(差不多晚了10年2个月)来参加这个聚会,但未来的编码人员可以使用while循环和indexOf()来完成。

let haystack = "I learned to play the Ukulele in Lebanon.";
let needle = "le";
let pos = 0; // Position Ref
let result = []; // Final output of all index's.
let hayStackLower = haystack.toLowerCase();

// Loop to check all occurrences 
while (hayStackLower.indexOf(needle, pos) != -1) {
  result.push(hayStackLower.indexOf(needle , pos));
  pos = hayStackLower.indexOf(needle , pos) + 1;
}

console.log("Final ", result); // Returns all indexes or empty array if not found

1
这是一个不错的解决方案,运行良好。谢谢! - Rog

6
const findAllOccurrences = (str, substr) => {
  str = str.toLowerCase();
  
  let result = [];

  let idx = str.indexOf(substr)
  
  while (idx !== -1) {
    result.push(idx);
    idx = str.indexOf(substr, idx+1);
  }
  return result;
}

console.log(findAllOccurrences('I learned to play the Ukulele in Lebanon', 'le'));

<3老派js! - joe

4

如果您只想查找所有匹配项的位置,我想给您指一条小技巧:

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);

如果您使用的是具有可变长度的正则表达式,则可能不适用,但对于某些人来说可能会有所帮助。

此处区分大小写。如需不区分大小写,请在使用String.toLowerCase函数之前进行转换。


我认为你的答案是最好的,因为使用RegExp是危险的。 - Bharata

3

I would recommend Tim's answer. However, this comment by @blazs states "Suppose searchStr=aaa and that str=aaaaaa. Then instead of finding 4 occurences your code will find only 2 because you're making skips by searchStr.length in the loop.", which is true by looking at Tim's code, specifically this line here: startIndex = index + searchStrLen; Tim's code would not be able to find an instance of the string that's being searched that is within the length of itself. So, I've modified Tim's answer:

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 searchStr = prompt("Enter a string.");
var str = prompt("What do you want to search for in the string?");
var indices = getIndicesOf(str, searchStr);

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

将其更改为+ 1而不是+ searchStrLen,如果我有一个字符串aaaaaa和一个搜索字符串aaa,则允许索引1在索引数组中。
附言:如果有人想要在代码中添加注释以解释代码的工作原理,请说出来,我很乐意回应请求。

1

这是我通常用来根据位置获取字符串索引的方法。

我传递以下参数:

search:要搜索的字符串

find:要查找的字符串

position(默认为“all”):查找字符串在搜索字符串中出现的位置

(如果是“all”,则返回完整的索引数组)

(如果是“last”,则返回最后一个位置)

function stringIndex (search, find, position = "all") {
    
    var currIndex = 0, indexes = [], found = true;
    
    while (found) {        
        var searchIndex = search.indexOf(find);
        if (searchIndex > -1) {
            currIndex += searchIndex + find.length; 
            search = search.substr (searchIndex + find.length);
            indexes.push (currIndex - find.length);
        } else found = false; //no other string to search for - exit from while loop   
    }
    
    if (position == 'all') return indexes;
    if (position > indexes.length -1) return [];
    
    position = (position == "last") ? indexes.length -1 : position;
    
    return indexes[position];        
}

//Example:
    
var myString = "Joe meets Joe and together they go to Joe's house";
console.log ( stringIndex(myString, "Joe") ); //0, 10, 38
console.log ( stringIndex(myString, "Joe", 1) ); //10
console.log ( stringIndex(myString, "Joe", "last") ); //38
console.log ( stringIndex(myString, "Joe", 5) ); //[]

1

大家好,这是使用reduce和辅助方法查找匹配短语索引的另一种方式。当然,RegExp更方便,也许在内部实现上有类似的方法。希望你们觉得有用。

function findIndexesOfPhraseWithReduce(text, phrase) {
      //convert text to array so that be able to manipulate.
          const arrayOfText = [...text];

      /* this function takes the array of characters and
      the search phrase and start index which comes from reduce method
      and calculates the end with length of the given phrase then slices
      and joins characters and compare it whith phrase.
      and returns True Or False */

         function isMatch(array, phrase, start) {
         const end = start + phrase.length;
         return (array.slice(start, end).join('')).toLowerCase() === 
               phrase.toLowerCase();
         }

    /* here we reduce the array of characters and test each character
    with isMach function which takes "current index" and matches the phrase
    with the subsequent character which starts from current index and
    ends at the last character of phrase(the length of phrase). */

        return arrayOfText.reduce((acc, item, index) => isMatch(arrayOfText, phrase, 
        index) ? [...acc, index] : acc, []);
}

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

function findIndexesOfPhraseWithReduce(text, phrase) {
     
         const arrayOfText = [...text];
         function isMatch(array, phrase, start) {
         const end = start + phrase.length;
         return (array.slice(start, end).join('')).toLowerCase() === 
               phrase.toLowerCase();
         }
        return arrayOfText.reduce((acc, item, index) => isMatch(arrayOfText, phrase, 
        index) ? [...acc, index] : acc, []);
}

console.log(findIndexesOfPhraseWithReduce("I learned to play the Ukulele in Lebanon.", "le"));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接