Encontre a enésima ocorrência de substring em uma string

118

Parece que deve ser bem trivial, mas sou novo em Python e quero fazer isso da maneira mais pitônica.

Quero encontrar o índice correspondente à enésima ocorrência de uma substring dentro de uma string.

Deve haver algo equivalente ao que EU QUERO fazer, que é

mystring.find("substring", 2nd)

Como você pode fazer isso em Python?

prestomação
fonte
7
Encontrar a enésima ocorrência da string? Presumo que significa o índice da enésima ocorrência.
Mark Byers
2
Sim, o índice da enésima ocorrência
prestomação
9
O que deve acontecer se houver correspondências sobrepostas? Deve find_nth ('aaaa', 'aa', 2) retornar 1 ou 2?
Mark Byers
Sim! tem que haver algo para encontrar a enésima ocorrência de uma substring em uma string e dividir a string na enésima ocorrência de uma substring.
Reman

Respostas:

69

A abordagem iterativa de Mark seria a maneira usual, eu acho.

Esta é uma alternativa com divisão de string, que muitas vezes pode ser útil para encontrar processos relacionados:

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)

E aqui está uma linha rápida (e um tanto suja, em que você tem que escolher uma palha que não combina com a agulha):

'foo bar bar bar'.replace('bar', 'XXX', 1).find('bar')
bobince
fonte
7
A primeira sugestão será muito ineficiente para strings grandes quando a combinação de seu interesse está perto do início. Sempre olha para a string inteira. É inteligente, mas eu não recomendaria isso para alguém que é novo em Python e só quer aprender uma boa maneira de fazer isso.
Mark Byers
3
Obrigado, gosto do seu forro. Não acho que seja a coisa mais legível instantaneamente do mundo, mas não é muito pior do que a maioria das outras abaixo
pré
1
1 para o one-liner, isso deve me ajudar agora. Eu estava pensando em fazer o equivalente a .rfind('XXX'), mas isso iria desmoronar se 'XXX'aparecer mais tarde na entrada de qualquer maneira.
Nikhil Chelliah
Esta função assume n = 0, 1, 2, 3, ... Seria bom você assumir n = 1, 2, 3, 4, ...
Feliz
75

Aqui está uma versão mais pitônica da solução iterativa direta:

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

Exemplo:

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

Se você deseja encontrar a enésima ocorrência de sobreposição de needle, você pode incrementar por em 1vez de len(needle), assim:

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

Exemplo:

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

É mais fácil de ler do que a versão de Mark e não requer a memória extra da versão de divisão ou módulo de expressão regular de importação. Ele também segue algumas das regras do Zen do python , ao contrário das várias reabordagens:

  1. Simples é melhor que complexo.
  2. Plano é melhor do que aninhado.
  3. A legibilidade conta.
Todd Gamblin
fonte
Isso pode ser feito em uma string? Como find_nth (df.mystring.str, ('x'), 2) para encontrar a posição da 2ª instância de 'x'?
Arthur D. Howland
36

Isso encontrará a segunda ocorrência de substring na string.

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

Edit: Não pensei muito sobre o desempenho, mas uma recursão rápida pode ajudar a encontrar a enésima ocorrência:

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)
Sriram Murali
fonte
Isso pode ser estendido geralmente para encontrar o enésimo elemento?
ifly6 de
Esta é a melhor resposta IMHO, fiz uma pequena adição para o caso especial em que n = 0
Jan Wilmans
Eu não queria editar o post por brevidade. Eu concordo com você, porém, que n = 0 deve ser tratado como um caso especial.
Sriram Murali
Isso deve ser ajustado para lidar com o caso em que há menos do que nocorrências da substring. (Neste caso, o valor de retorno percorrerá periodicamente todas as posições de ocorrência).
coldfix de
29

Entendendo que regex nem sempre é a melhor solução, provavelmente usaria uma aqui:

>>> 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
Mark Peters
fonte
4
O risco aqui, é claro, é que a string a ser pesquisada contenha caracteres especiais que farão com que a regex faça algo que você não deseja. Usar re.escape deve resolver isso.
Mark Byers
1
Isso é inteligente, mas é realmente Pythônico? Parece um exagero apenas encontrar a enésima ocorrência de uma substring e não é exatamente fácil de ler. Além disso, como você disse, você deve importar todos os dados para isso
Todd Gamblin
Ao usar colchetes, você diz ao Python para criar a lista inteira. Os colchetes iterariam apenas através dos primeiros elementos, o que é mais eficaz:(m.start() for m in re.finditer(r"ab",s))[2]
emu
1
@emu Não, o que você postou não funciona; você não pode obter um índice de um gerador.
Mark Amery
@MarkAmery desculpe! Estou bastante surpreso por ter postado esse código. Ainda assim, uma solução semelhante e feia é possível usando a itertools.islicefunção:next(islice(re.finditer(r"ab",s), 2, 2+1)).start()
emu
17

Estou oferecendo alguns resultados de benchmarking comparando as abordagens mais proeminentes apresentadas até agora, nomeadamente @bobince's findnth()(baseado em str.split()) vs. @tgamblin's ou @Mark Byers ' find_nth()(baseado em str.find()). Também vou comparar com uma extensão C ( _find_nth.so) para ver o quão rápido podemos ir. Aqui está 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

Claro, o desempenho é mais importante se a string for grande, então suponha que queremos encontrar a 1000001ª nova linha ('\ n') em um arquivo de 1,3 GB chamado 'bigfile'. Para economizar memória, gostaríamos de trabalhar em uma mmap.mmaprepresentação de objeto do arquivo:

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)

Já existe o primeiro problema com findnth(), uma vez que os mmap.mmapobjetos não suportam split(). Portanto, temos que copiar todo o arquivo para a memória:

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

Ai! Felizmente, sainda cabe nos 4 GB de memória do meu Macbook Air, então vamos avaliar findnth():

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

Claramente um desempenho terrível. Vamos ver como a abordagem baseada em str.find():

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

Muito melhor! Claramente, findnth()o problema de é que ele é forçado a copiar a string durante split(), que já é a segunda vez que copiamos 1,3 GB de dados depois s = mm[:]. Aí vem a segunda vantagem de find_nth(): Podemos usá-lo mmdiretamente, de forma que nenhuma cópia do arquivo seja necessária:

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

Parece haver uma pequena penalidade de desempenho operando em mmvs. s, mas isso ilustra que find_nth()pode nos dar uma resposta em 1,2 s em comparação com findntho total de 47 s.

Não encontrei nenhum caso em que a str.find()abordagem baseada fosse significativamente pior do que a str.split()abordagem baseada, então, neste ponto, eu diria que a resposta de @tgamblin ou @Mark Byers deve ser aceita em vez da de @bobince.

Em meus testes, a versão find_nth()acima foi a solução Python puro mais rápida que eu poderia criar (muito semelhante à versão de @Mark Byers). Vamos ver o quanto podemos fazer melhor com um módulo de extensão C. Aqui está _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);
}

Aqui está o setup.pyarquivo:

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

Instale normalmente com python setup.py install. O código C tem uma vantagem aqui, pois se limita a encontrar caracteres únicos, mas vamos ver o quão rápido isso é:

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

Claramente um pouco mais rápido ainda. Curiosamente, não há diferença no nível C entre os casos in-memory e mmapped. Também é interessante ver que _find_nth2(), que é baseado no string.h's memchr()função de biblioteca, perde-se contra a implementação direta em _find_nth(): Os 'otimizações' adicionais memchr()estão aparentemente frustrada ...

Concluindo, a implementação em findnth()(com base em str.split()) é realmente uma má ideia, uma vez que (a) ela tem um desempenho péssimo para strings maiores devido à cópia necessária e (b) não funciona em mmap.mmapobjetos. A implementação em find_nth()(com base em str.find()) deve ser preferida em todas as circunstâncias (e, portanto, ser a resposta aceita para essa pergunta).

Ainda há bastante espaço para melhorias, já que a extensão C rodou quase um fator de 4 mais rápido do que o código Python puro, indicando que pode haver um caso para uma função de biblioteca Python dedicada.

Stefan
fonte
8

Maneira mais simples?

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

firstTest = text.find('test')

print text.find('test', firstTest + 1)
Proibzie
fonte
Posso imaginar que isso também tem um bom desempenho, em comparação com outras soluções.
Rotareti
7

Eu provavelmente faria algo assim, usando a função find que usa um parâmetro de índice:

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)

Não é particularmente Pythônico, eu acho, mas é simples. Você poderia fazer isso usando recursão em vez disso:

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)

É uma forma funcional de resolver, mas não sei se isso o torna mais pitônico.

Mark Byers
fonte
1
for _ in xrange(n):pode ser usado em vez dewhile n: ... n-=1
jfs
@JF Sebastian: Sim, acho que é um pouco mais pitônico. Eu vou atualizar.
Mark Byers
BTW: xrange não é mais necessário no Python 3: diveintopython3.org/…
Mark Byers
1
return find_nth(s, x, n - 1, i + 1)deveria ser return find_nth(s, x, n - 1, i + len(x)). Não é grande coisa, mas economiza algum tempo de computação.
Dan Loewenherz
@dlo: Na verdade, isso pode dar resultados diferentes em alguns casos: find_nth ('aaaa', 'aa', 2). O meu dá 1, o seu dá 2. Acho que o seu é realmente o que o autor da postagem deseja. Vou atualizar meu código. Obrigado pelo comentário.
Mark Byers
3

Isso lhe dará uma matriz dos índices iniciais para correspondências com yourstring:

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

Então, sua enésima entrada seria:

n = 2
nth_entry = indices[n-1]

Claro, você deve ter cuidado com os limites do índice. Você pode obter o número de instâncias yourstringcomo este:

num_instances = len(indices)
modle13
fonte
2

Aqui está outra abordagem usando re.finditer.
A diferença é que isso só olha para o palheiro na medida do necessário

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() 
John La Rooy
fonte
2

Aqui está outra versão re+ itertoolsque deve funcionar ao pesquisar por a strou a RegexpObject. Admito francamente que provavelmente isso é um excesso de engenharia, mas por algum motivo me divertiu.

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
Hank Gay
fonte
2

Com base na resposta do modle13 , mas sem a redependência do módulo.

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

Eu meio que gostaria que este fosse um método de string embutido.

>>> iter_find("http://stackoverflow.com/questions/1883980/", '/')
[5, 6, 24, 34, 42]
Zv_oDD
fonte
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
ghostdog74
fonte
1

Fornecendo outra solução "complicada", que usa splite join.

No seu exemplo, podemos usar

len("substring".join([s for s in ori.split("substring")[:2]]))
Ivor Zhou
fonte
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
Jason
fonte
precisa de uma explicação
Ctznkane525
find_nth('aaa', 'a', 0)retorna 1enquanto deveria retornar 0. Você precisa de algo parecido i = s.find(substr, i) + 1e depois voltar i - 1.
a_guest 02 de
1

Solução sem usar loops e recursão.

Use o padrão requerido no método de compilação e insira a ocorrência desejada na variável 'n' e a última instrução imprimirá o índice inicial da enésima ocorrência do padrão na string dada. Aqui, o resultado de finditer, isto é, iterador, está sendo convertido em lista e acessando diretamente o enésimo índice.

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

Substituir um forro é ótimo, mas só funciona porque XX e barra têm o mesmo lentgh

Uma boa definição geral seria:

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

Esta é a resposta que você realmente deseja:

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
yarz-tech
fonte
0

Aqui está minha solução para encontrar na ocorrência de bna string 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)

É puro Python e iterativo. Para 0 ou nmuito grande, retorna -1. É de uma linha e pode ser usado diretamente. Aqui está um exemplo:

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

Para o caso especial em que você procura a enésima ocorrência de um caractere (ou seja, substring de comprimento 1), a seguinte função funciona construindo uma lista de todas as posições de ocorrências do caractere fornecido:

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]

Se houver menos de nocorrências do personagem dado, ele dará IndexError: list index out of range.

Isso é derivado da resposta de @Zv_oDD e simplificado para o caso de um único caractere.

coldfix
fonte
0

Def:

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

Usar:

get_first_N_words('  One Two Three Four ' , 3)

Resultado:

'One Two Three'
Chadee Fouad
fonte