查找字符串中子字符串的第n次出现


117

这似乎应该是微不足道的,但是我是Python的新手,并且希望以最Python的方式进行操作。

我想找到对应于字符串中第n个子字符串的索引。

一定有什么我想做的事情是

mystring.find("substring", 2nd)

如何在Python中实现?


7
查找字符串的第n个出现位置?我认为这意味着第n次出现的索引?
Mark Byers

2
是的,第n个事件的索引
储存时间为

9
如果比赛重叠,该怎么办?find_nth('aaaa','aa',2)应该返回1还是2?
Mark Byers

是! 必须找到在字符串中出现的第n个子字符串,并在出现第n个子字符串时拆分字符串。
Reman

Answers:


69

我认为,Mark的迭代方法将是通常的方法。

这是字符串拆分的替代方法,通常可用于查找相关过程:

def findnth(haystack, needle, n):
    parts= haystack.split(needle, n+1)
    if len(parts)<=n+1:
        return -1
    return len(haystack)-len(parts[-1])-len(needle)

这是一种快速(有点脏,因为您必须选择一些无法与针头相匹配的谷壳)的单缸套:

'foo bar bar bar'.replace('bar', 'XXX', 1).find('bar')

7
当您感兴趣的匹配即将开始时,第一个建议对于大型字符串将非常低效。它总是查看整个字符串。这很聪明,但是我不会推荐给刚接触Python并只想学习一种好的方法的人。
Mark Byers

3
谢谢,我喜欢你的一支内胆。我不认为这是世界上最容易阅读的东西,但与下面的大多数其他事物相比,它并没有比这糟得多
记忆式的

1
单线+1,这应该对我有帮助。我一直在想做等效的.rfind('XXX'),但是如果'XXX'以后无论如何在输入中出现,那都会崩溃。
Nikhil Chelliah 2010年

该函数假设n = 0、1、2、3,... ...最好假设n = 1、2、3、4 ...
快乐

75

这是简单的迭代解决方案的更多Pythonic版本:

def find_nth(haystack, needle, n):
    start = haystack.find(needle)
    while start >= 0 and n > 1:
        start = haystack.find(needle, start+len(needle))
        n -= 1
    return start

例:

>>> find_nth("foofoofoofoo", "foofoo", 2)
6

如果要查找的第n个重叠出现needle,可以用1代替,增加len(needle),如下所示:

def find_nth_overlapping(haystack, needle, n):
    start = haystack.find(needle)
    while start >= 0 and n > 1:
        start = haystack.find(needle, start+1)
        n -= 1
    return start

例:

>>> find_nth_overlapping("foofoofoofoo", "foofoo", 2)
3

这比Mark的版本更容易阅读,并且不需要拆分版本或导入正则表达式模块的额外内存。与各种方法不同,它还遵守python Zen中的一些规则re

  1. 简单胜于复杂。
  2. 扁平比嵌套更好。
  3. 可读性很重要。

可以用字符串完成吗?像find_nth(df.mystring.str,('x'),2)来查找'x'的第二个实例的位置?
亚瑟·豪兰

36

这将在字符串中找到子字符串的第二次出现。

def find_2nd(string, substring):
   return string.find(substring, string.find(substring) + 1)

编辑:我对性能没有考虑太多,但是快速递归可以帮助找到第n个出现的情况:

def find_nth(string, substring, n):
   if (n == 1):
       return string.find(substring)
   else:
       return string.find(substring, find_nth(string, substring, n - 1) + 1)

可以将其扩展为找到第n个元素吗?
ifly6

这最好的答案恕我直言,我做了一个小除了对于特殊情况,其中n = 0
扬Wilmans

为了简洁起见,我不想编辑该帖子。不过,我同意您的看法,认为n = 0应该视为特例。
Sriram Murali

应该对此进行调整,以处理n子字符串少于发生的情况。(在这种情况下,返回值将周期性地遍历所有发生位置)。
coldfix '19

28

了解正则表达式并不总是最好的解决方案,我可能在这里使用一个:

>>> import re
>>> s = "ababdfegtduab"
>>> [m.start() for m in re.finditer(r"ab",s)]
[0, 2, 11]
>>> [m.start() for m in re.finditer(r"ab",s)][2] #index 2 is third occurrence 
11

4
当然,这里的风险是要搜索的字符串将包含特殊字符,这将导致正则表达式执行您不想要的操作。使用re.escape应该可以解决这个问题。
Mark Byers

1
这很聪明,但是真的是Pythonic吗?仅仅找到子字符串的第n次出现就显得有点过头了,而且阅读起来也不容易。另外,就像您说的那样,您必须为此输入所有相关信息
Todd Gamblin,2009年

使用方括号时,您告诉Python创建整个列表。:圆括弧将迭代仅通过第一元件,这是更有效(m.start() for m in re.finditer(r"ab",s))[2]
鸸鹋

1
@emu不,您发布的内容无效;您无法获取生成器的索引。
Mark Amery 2014年

@MarkAmery对不起!我很惊讶为什么发布了该代码。尽管如此,使用以下itertools.islice功能还是可以实现类似的丑陋解决方案:next(islice(re.finditer(r"ab",s), 2, 2+1)).start()
emu 2014年

17

我提供了一些基准测试结果,以比较到目前为止介绍的最著名的方法,即@bobince findnth()(基于str.split())与@tgamblin find_nth()(或基于@Mark Byers)(基于str.find())。我还将与C扩展名(_find_nth.so)进行比较,以了解我们可以走多快。这里是find_nth.py

def findnth(haystack, needle, n):
    parts= haystack.split(needle, n+1)
    if len(parts)<=n+1:
        return -1
    return len(haystack)-len(parts[-1])-len(needle)

def find_nth(s, x, n=0, overlap=False):
    l = 1 if overlap else len(x)
    i = -l
    for c in xrange(n + 1):
        i = s.find(x, i + l)
        if i < 0:
            break
    return i

当然,如果字符串很大,性能最重要,因此假设我们要在1.3 GB的文件“ bigfile”中找到第1000001个换行符('\ n')。为了节省内存,我们希望处理mmap.mmap文件的对象表示形式:

In [1]: import _find_nth, find_nth, mmap

In [2]: f = open('bigfile', 'r')

In [3]: mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)

findnth()由于mmap.mmap对象不支持,因此已经存在第一个问题split()。因此,我们实际上必须将整个文件复制到内存中:

In [4]: %time s = mm[:]
CPU times: user 813 ms, sys: 3.25 s, total: 4.06 s
Wall time: 17.7 s

哎哟! 幸运的是s,我的Macbook Air仍可容纳4 GB内存,因此让我们进行基准测试findnth()

In [5]: %timeit find_nth.findnth(s, '\n', 1000000)
1 loops, best of 3: 29.9 s per loop

显然表现糟糕。让我们看看基于的方法是如何str.find()做到的:

In [6]: %timeit find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 774 ms per loop

好多了!显然,findnth()问题在于它被迫在期间复制字符串split(),这已经是我们第二次在after之后复制1.3 GB的数据了s = mm[:]。这里有第二个优点find_nth():我们可以mm直接使用它,因此文件的副本是必需的:

In [7]: %timeit find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 1.21 s per loop

mmvs. 上似乎有一些小的性能损失s,但这表明find_nth()与1.2 s findnth的总和(47 s)相比,可以在1.2 s内获得答案。

我发现没有任何str.find()一种方法比基于方法的性能明显差于str.split()基于方法的情况,因此,在这一点上,我认为应该接受@tgamblin或@Mark Byers的答案,而不是@bobince的答案。

在我的测试中,上述版本find_nth()是我能想到的最快的纯Python解决方案(非常类似于@Mark Byers的版本)。让我们看看使用C扩展模块可以做的更好。这里是_find_nthmodule.c

#include <Python.h>
#include <string.h>

off_t _find_nth(const char *buf, size_t l, char c, int n) {
    off_t i;
    for (i = 0; i < l; ++i) {
        if (buf[i] == c && n-- == 0) {
            return i;
        }
    }
    return -1;
}

off_t _find_nth2(const char *buf, size_t l, char c, int n) {
    const char *b = buf - 1;
    do {
        b = memchr(b + 1, c, l);
        if (!b) return -1;
    } while (n--);
    return b - buf;
}

/* mmap_object is private in mmapmodule.c - replicate beginning here */
typedef struct {
    PyObject_HEAD
    char *data;
    size_t size;
} mmap_object;

typedef struct {
    const char *s;
    size_t l;
    char c;
    int n;
} params;

int parse_args(PyObject *args, params *P) {
    PyObject *obj;
    const char *x;

    if (!PyArg_ParseTuple(args, "Osi", &obj, &x, &P->n)) {
        return 1;
    }
    PyTypeObject *type = Py_TYPE(obj);

    if (type == &PyString_Type) {
        P->s = PyString_AS_STRING(obj);
        P->l = PyString_GET_SIZE(obj);
    } else if (!strcmp(type->tp_name, "mmap.mmap")) {
        mmap_object *m_obj = (mmap_object*) obj;
        P->s = m_obj->data;
        P->l = m_obj->size;
    } else {
        PyErr_SetString(PyExc_TypeError, "Cannot obtain char * from argument 0");
        return 1;
    }
    P->c = x[0];
    return 0;
}

static PyObject* py_find_nth(PyObject *self, PyObject *args) {
    params P;
    if (!parse_args(args, &P)) {
        return Py_BuildValue("i", _find_nth(P.s, P.l, P.c, P.n));
    } else {
        return NULL;    
    }
}

static PyObject* py_find_nth2(PyObject *self, PyObject *args) {
    params P;
    if (!parse_args(args, &P)) {
        return Py_BuildValue("i", _find_nth2(P.s, P.l, P.c, P.n));
    } else {
        return NULL;    
    }
}

static PyMethodDef methods[] = {
    {"find_nth", py_find_nth, METH_VARARGS, ""},
    {"find_nth2", py_find_nth2, METH_VARARGS, ""},
    {0}
};

PyMODINIT_FUNC init_find_nth(void) {
    Py_InitModule("_find_nth", methods);
}

这是setup.py文件:

from distutils.core import setup, Extension
module = Extension('_find_nth', sources=['_find_nthmodule.c'])
setup(ext_modules=[module])

像往常一样安装python setup.py install。C代码在这里发挥了优势,因为它仅限于查找单个字符,但是让我们看一下它有多快:

In [8]: %timeit _find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 218 ms per loop

In [9]: %timeit _find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 216 ms per loop

In [10]: %timeit _find_nth.find_nth2(mm, '\n', 1000000)
1 loops, best of 3: 307 ms per loop

In [11]: %timeit _find_nth.find_nth2(s, '\n', 1000000)
1 loops, best of 3: 304 ms per loop

显然还快很多。有趣的是,内存中情况和映射情况之间的C级别没有差异。有趣的是_find_nth2(),它基于string.hmemchr()库函数,相对于以下简单的实现方式有所失落_find_nth():额外的“优化” memchr()显然是后退式的...

总而言之,findnth()(基于str.split())中的实现确实是一个坏主意,因为(a)由于需要进行复制,因此它对于较大的字符串表现出极大的性能,(b)根本不适用于mmap.mmap对象。在find_nth()(基于str.find())中的实现在所有情况下都应优先考虑(因此是该问题的公认答案)。

还有很大的改进空间,因为C扩展比纯Python代码快将近4倍,这表明可能存在专用Python库函数的情况。


8

最简单的方法?

text = "This is a test from a test ok" 

firstTest = text.find('test')

print text.find('test', firstTest + 1)

我可以想象,与其他解决方案相比,它的性能也很高。
Rotareti

7

我可能会使用带有索引参数的find函数来做这样的事情:

def find_nth(s, x, n):
    i = -1
    for _ in range(n):
        i = s.find(x, i + len(x))
        if i == -1:
            break
    return i

print find_nth('bananabanana', 'an', 3)

我猜这不是特别的Pythonic,但是很简单。您可以使用递归来代替:

def find_nth(s, x, n, i = 0):
    i = s.find(x, i)
    if n == 1 or i == -1:
        return i 
    else:
        return find_nth(s, x, n - 1, i + len(x))

print find_nth('bananabanana', 'an', 3)

这是解决该问题的一种实用方法,但是我不知道这是否使其更具有Python风格。


1
for _ in xrange(n):可以代替while n: ... n-=1
jfs

@JF Sebastian:是的,我想这有点Pythonic。我会更新。
Mark Byers

BTW:x范围不再需要在Python 3:diveintopython3.org/...
马克·拜尔斯

1
return find_nth(s, x, n - 1, i + 1)应该是return find_nth(s, x, n - 1, i + len(x))。没什么大不了的,但是节省了一些计算时间。
Dan Loewenherz,2009年

@dlo:实际上,在某些情况下可以给出不同的结果:find_nth('aaaa','aa',2)。我的给1,你的给2。我想你的实际上就是海报想要的。我将更新代码。感谢您的评论。
Mark Byers

3

这将为您提供与匹配的起始索引数组yourstring

import re
indices = [s.start() for s in re.finditer(':', yourstring)]

那么您的第n个条目将是:

n = 2
nth_entry = indices[n-1]

当然,您必须小心索引范围。您可以获得这样的实例数yourstring

num_instances = len(indices)

2

这是使用re.finditer的另一种方法。
所不同的是,这只会尽可能地调查大海捞针

from re import finditer
from itertools import dropwhile
needle='an'
haystack='bananabanana'
n=2
next(dropwhile(lambda x: x[0]<n, enumerate(re.finditer(needle,haystack))))[1].start() 

2

这是搜索a 或a 时应该工作的另一个re+ itertools版本。我会自由地承认这可能是过度设计的,但是出于某种原因,它使我感到很开心。strRegexpObject

import itertools
import re

def find_nth(haystack, needle, n = 1):
    """
    Find the starting index of the nth occurrence of ``needle`` in \
    ``haystack``.

    If ``needle`` is a ``str``, this will perform an exact substring
    match; if it is a ``RegexpObject``, this will perform a regex
    search.

    If ``needle`` doesn't appear in ``haystack``, return ``-1``. If
    ``needle`` doesn't appear in ``haystack`` ``n`` times,
    return ``-1``.

    Arguments
    ---------
    * ``needle`` the substring (or a ``RegexpObject``) to find
    * ``haystack`` is a ``str``
    * an ``int`` indicating which occurrence to find; defaults to ``1``

    >>> find_nth("foo", "o", 1)
    1
    >>> find_nth("foo", "o", 2)
    2
    >>> find_nth("foo", "o", 3)
    -1
    >>> find_nth("foo", "b")
    -1
    >>> import re
    >>> either_o = re.compile("[oO]")
    >>> find_nth("foo", either_o, 1)
    1
    >>> find_nth("FOO", either_o, 1)
    1
    """
    if (hasattr(needle, 'finditer')):
        matches = needle.finditer(haystack)
    else:
        matches = re.finditer(re.escape(needle), haystack)
    start_here = itertools.dropwhile(lambda x: x[0] < n, enumerate(matches, 1))
    try:
        return next(start_here)[1].start()
    except StopIteration:
        return -1

2

基于modle13的答案,但没有re模块依赖性。

def iter_find(haystack, needle):
    return [i for i in range(0, len(haystack)) if haystack[i:].startswith(needle)]

我有点希望这是一个内置的字符串方法。

>>> iter_find("http://stackoverflow.com/questions/1883980/", '/')
[5, 6, 24, 34, 42]

1
>>> s="abcdefabcdefababcdef"
>>> j=0
>>> for n,i in enumerate(s):
...   if s[n:n+2] =="ab":
...     print n,i
...     j=j+1
...     if j==2: print "2nd occurence at index position: ",n
...
0 a
6 a
2nd occurence at index position:  6
12 a
14 a

1

提供另一个使用“ split和”的“棘手”解决方案join

在您的示例中,我们可以使用

len("substring".join([s for s in ori.split("substring")[:2]]))

1
# return -1 if nth substr (0-indexed) d.n.e, else return index
def find_nth(s, substr, n):
    i = 0
    while n >= 0:
        n -= 1
        i = s.find(substr, i + 1)
    return i

需要一个解释
Ctznkane525 '18

find_nth('aaa', 'a', 0)返回,1而应该返回0。您需要类似的东西i = s.find(substr, i) + 1,然后返回i - 1
a_guest

1

不使用循环和递归的解决方案。

在编译方法中使用所需的模式,然后在变量'n'中输入所需的出现位置,最后一条语句将在给定的字符串中打印该模式的第n个出现位置的起始索引。在这里,finditer的结果(即迭代器)将转换为list并直接访问第n个索引。

import re
n=2
sampleString="this is history"
pattern=re.compile("is")
matches=pattern.finditer(sampleString)
print(list(matches)[n].span()[0])

0

替换一根衬管很棒,但只能工作,因为XX和bar具有相同的长度

一个好的和一般的定义是:

def findN(s,sub,N,replaceString="XXX"):
    return s.replace(sub,replaceString,N-1).find(sub) - (len(replaceString)-len(sub))*(N-1)

0

这是您真正想要的答案:

def Find(String,ToFind,Occurence = 1):
index = 0 
count = 0
while index <= len(String):
    try:
        if String[index:index + len(ToFind)] == ToFind:
            count += 1
        if count == Occurence:
               return index
               break
        index += 1
    except IndexError:
        return False
        break
return False

0

这是我找到ninth出现b在字符串中的解决方案a

from functools import reduce


def findNth(a, b, n):
    return reduce(lambda x, y: -1 if y > x + 1 else a.find(b, x + 1), range(n), -1)

它是纯Python并且是迭代的。对于0或n太大,它将返回-1。它是单线的,可以直接使用。这是一个例子:

>>> reduce(lambda x, y: -1 if y > x + 1 else 'bibarbobaobaotang'.find('b', x + 1), range(4), -1)
7

0

对于搜索字符的第n个出现(即长度为1的子字符串)的特殊情况,以下功能通过构建给定字符出现的所有位置的列表来起作用:

def find_char_nth(string, char, n):
    """Find the n'th occurence of a character within a string."""
    return [i for i, c in enumerate(string) if c == char][n-1]

如果少于n给定字符的出现次数,它将给出IndexError: list index out of range

这是从@Z​​v_oDD的答案派生而来的,对于单个字符而言,它得到了简化。


0

Def:

def get_first_N_words(mytext, mylen = 3):
    mylist = list(mytext.split())
    if len(mylist)>=mylen: return ' '.join(mylist[:mylen])

使用方法:

get_first_N_words('  One Two Three Four ' , 3)

输出:

'One Two Three'

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.