Encuentre la enésima aparición de subcadena en una cadena

118

Esto parece que debería ser bastante trivial, pero soy nuevo en Python y quiero hacerlo de la manera más Pythonic.

Quiero encontrar el índice correspondiente a la n-ésima aparición de una subcadena dentro de una cadena.

Tiene que haber algo equivalente a lo que QUIERO hacer, que es

mystring.find("substring", 2nd)

¿Cómo puedes lograr esto en Python?

prestomación
fuente
7
¿Encontrar la enésima aparición de la cadena? ¿Supongo que significa el índice de la enésima aparición?
Mark Byers
2
Sí, el índice de la enésima ocurrencia
prestomation
9
¿Qué debería suceder si hay coincidencias superpuestas? ¿Debería find_nth ('aaaa', 'aa', 2) devolver 1 o 2?
Mark Byers
¡Si! tiene que haber algo para encontrar la n-ésima aparición de una subcadena en una cadena y dividir la cadena en la n-ésima aparición de una subcadena.
Reman

Respuestas:

69

El enfoque iterativo de Mark sería la forma habitual, creo.

Aquí hay una alternativa con la división de cadenas, que a menudo puede ser útil para procesos relacionados con la búsqueda:

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)

Y aquí hay un resumen rápido (y algo sucio, en el que debes elegir un poco de paja que no puede coincidir con la aguja) de una sola línea:

'foo bar bar bar'.replace('bar', 'XXX', 1).find('bar')
bobince
fuente
7
La primera sugerencia será muy ineficaz para cadenas grandes cuando la coincidencia que le interesa esté cerca del comienzo. Siempre mira toda la cadena. Es inteligente, pero no se lo recomendaría a alguien que sea nuevo en Python y solo quiera aprender una buena manera de hacerlo.
Mark Byers
3
Gracias, me gusta tu única línea. No creo que sea la cosa más legible al instante en el mundo, pero no es mucho peor que la mayoría de los demás a continuación
prestomation
1
+1 para el one-liner, esto debería ayudarme ahora mismo. Había estado pensando en hacer el equivalente de .rfind('XXX'), pero eso se desmoronaría si de 'XXX'todos modos aparece más adelante en la entrada.
Nikhil Chelliah
Esta función asume n = 0, 1, 2, 3, ... Sería bueno que asumiera n = 1, 2, 3, 4, ...
Feliz
75

Aquí hay una versión más Pythonic de la sencilla solución iterativa:

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

Ejemplo:

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

Si desea encontrar la enésima aparición superpuesta de needle, puede incrementar en en 1lugar de len(needle), así:

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

Ejemplo:

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

Es más fácil de leer que la versión de Mark y no requiere la memoria adicional de la versión de división o el módulo de importación de expresiones regulares. También se adhiere a algunas de las reglas del Zen de Python , a diferencia de los diversos reenfoques:

  1. Mejor es simple que complejo.
  2. Plano es mejor que anidado.
  3. La legibilidad cuenta.
Todd Gamblin
fuente
¿Se puede hacer esto en una cadena? ¿Como find_nth (df.mystring.str, ('x'), 2) para encontrar la posición de la segunda instancia de 'x'?
Arthur D. Howland
36

Esto encontrará la segunda aparición de subcadena en cadena.

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

Editar: no he pensado mucho en el rendimiento, pero una recursividad rápida puede ayudar a encontrar la enésima aparició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)
Sriram Murali
fuente
¿Se puede extender esto generalmente para encontrar el n-ésimo elemento?
ifly6
Esta es la mejor respuesta en mi humilde opinión, hice una pequeña adición para el caso especial donde n = 0
Jan Wilmans
No quería editar la publicación por brevedad. Sin embargo, estoy de acuerdo con usted en que n = 0 debe tratarse como un caso especial.
Sriram Murali
Esto debe ajustarse para manejar el caso donde hay menos nocurrencias de la subcadena. (En este caso, el valor de retorno circulará periódicamente a través de todas las posiciones de ocurrencia).
Coldfix
29

Entendiendo que la expresión regular no siempre es la mejor solución, probablemente usaría una aquí:

>>> 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
fuente
4
El riesgo aquí, por supuesto, es que la cadena a buscar contendrá caracteres especiales que harán que la expresión regular haga algo que usted no deseaba. El uso de re.escape debería resolver esto.
Mark Byers
1
Esto es inteligente, pero ¿es realmente Pythonic? Parece una exageración por solo encontrar la enésima aparición de una subcadena, y no es exactamente fácil de leer. Además, como dices, debes importar todos los archivos para esto
Todd Gamblin
Cuando usa corchetes, le dice a Python que cree la lista completa. Los corchetes iterarían solo a través de los primeros elementos, lo cual es más efectivo:(m.start() for m in re.finditer(r"ab",s))[2]
emu
1
@emu No, lo que ha publicado no funcionará; no puedes tomar un índice de un generador.
Mark Amery
@MarkAmery lo siento! Estoy bastante sorprendido de por qué publiqué ese código. Aún así, una solución similar y fea es posible usando la itertools.islicefunción:next(islice(re.finditer(r"ab",s), 2, 2+1)).start()
emu
17

Estoy ofreciendo algunos resultados de evaluación comparativa que comparan los enfoques más destacados presentados hasta ahora, a saber, @ bobince findnth()(basado en str.split()) frente a @ tgamblin o @Mark Byers ' find_nth()(basado en str.find()). También compararé con una extensión C ( _find_nth.so) para ver qué tan rápido podemos ir. Aqui esta 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

Por supuesto, el rendimiento es más importante si la cadena es grande, así que suponga que queremos encontrar la línea nueva 1000001 ('\ n') en un archivo de 1.3 GB llamado 'bigfile'. Para ahorrar memoria, nos gustaría trabajar en una mmap.mmaprepresentación de objeto del archivo:

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)

Ya existe el primer problema con findnth(), ya que los mmap.mmapobjetos no son compatibles split(). Entonces, en realidad, tenemos que copiar todo el archivo en la memoria:

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

¡Ay! Afortunadamente, stodavía cabe en los 4 GB de memoria de mi Macbook Air, así que hagamos una evaluación comparativa findnth():

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

Claramente una actuación terrible. Veamos cómo funciona el enfoque basado en str.find():

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

¡Mucho mejor! Claramente, findnth()el problema es que se ve obligado a copiar la cadena durante split(), que ya es la segunda vez que copiamos los 1.3 GB de datos después s = mm[:]. Aquí entra en juego la segunda ventaja de find_nth(): Podemos usarlo en mmforma directa, de tal manera que cero se requieren copias del archivo:

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

Parece haber una pequeña penalización de rendimiento operando en mmvs. s, pero esto ilustra que find_nth()puede darnos una respuesta en 1.2 s en comparación con findnthel total de 47 s.

No encontré casos en los que el str.find()enfoque basado fuera significativamente peor que el str.split()enfoque basado, por lo que en este punto, diría que la respuesta de @ tgamblin o @Mark Byers debería aceptarse en lugar de la de @ bobince.

En mis pruebas, la versión find_nth()anterior fue la solución Python pura más rápida que se me ocurrió (muy similar a la versión de @Mark Byers). Veamos cuánto mejor podemos hacer con un módulo de extensión C. Aqui esta _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);
}

Aquí está el setup.pyarchivo:

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

Instale como de costumbre con python setup.py install. El código C tiene una ventaja aquí, ya que se limita a encontrar caracteres individuales, pero veamos qué tan rápido es esto:

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, aún un poco más rápido. Curiosamente, no hay diferencia en el nivel C entre los casos en memoria y mmapeados. También es interesante ver que _find_nth2(), que se basa en string.hla memchr()función de la biblioteca, pierde frente a la implementación sencilla en _find_nth(): Las "optimizaciones" adicionales en memchr()aparentemente son contraproducentes ...

En conclusión, la implementación en findnth()(basada en str.split()) es realmente una mala idea, ya que (a) funciona terriblemente para cadenas más grandes debido a la copia requerida, y (b) no funciona en los mmap.mmapobjetos en absoluto. La implementación en find_nth()(basado en str.find()) debe ser preferida en todas las circunstancias (y por lo tanto ser la respuesta aceptada a esta pregunta).

Todavía hay bastante margen de mejora, ya que la extensión C se ejecutó casi un factor 4 más rápido que el código Python puro, lo que indica que podría haber un caso para una función de biblioteca dedicada de Python.

Stefan
fuente
8

¿La forma más sencilla?

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

firstTest = text.find('test')

print text.find('test', firstTest + 1)
forbzie
fuente
Puedo imaginar que esto también es bastante eficaz, en comparación con otras soluciones.
Rotareti
7

Probablemente haría algo como esto, usando la función de búsqueda que toma un 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)

No es particularmente Pythonic, supongo, pero es simple. Podrías hacerlo usando la recursividad en su lugar:

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)

Es una forma funcional de resolverlo, pero no sé si eso lo hace más Pythonic.

Mark Byers
fuente
1
for _ in xrange(n):se puede utilizar en lugar dewhile n: ... n-=1
jfs
@JF Sebastian: Sí, creo que eso es un poco más Pythonic. Yo actualizaré.
Mark Byers
Por cierto: xrange ya no es necesario en Python 3: diveintopython3.org/…
Mark Byers
1
return find_nth(s, x, n - 1, i + 1)debería ser return find_nth(s, x, n - 1, i + len(x)). No es gran cosa, pero ahorra algo de tiempo de cálculo.
Dan Loewenherz
@dlo: En realidad, eso puede dar resultados diferentes en algunos casos: find_nth ('aaaa', 'aa', 2). El mío da 1, el tuyo da 2. Supongo que el tuyo es en realidad lo que quiere el cartel. Actualizaré mi código. Gracias por el comentario.
Mark Byers
3

Esto le dará una matriz de índices iniciales para coincidencias con yourstring:

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

Entonces tu enésima entrada sería:

n = 2
nth_entry = indices[n-1]

Por supuesto, debes tener cuidado con los límites del índice. Puede obtener el número de instancias de yourstringeste tipo:

num_instances = len(indices)
modle13
fuente
2

Aquí hay otro enfoque que usa re.finditer.
La diferencia es que esto solo mira hacia el pajar hasta donde sea necesario

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
fuente
2

Aquí hay otra versión re+ itertoolsque debería funcionar cuando se busca a stro a RegexpObject. Admitiré libremente que es probable que esto esté demasiado diseñado, pero por alguna razón me entretuvo.

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
fuente
2

Basado en la respuesta de modle13 , pero sin la redependencia del módulo.

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

Me gustaría un poco que este fuera un método de cadena incorporado.

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

Proporcionar otra solución "complicada", que utiliza splity join.

En su ejemplo, podemos usar

len("substring".join([s for s in ori.split("substring")[:2]]))
Ivor Zhou
fuente
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
fuente
necesita una explicación
Ctznkane525
find_nth('aaa', 'a', 0)regresa 1mientras debería regresar 0. Necesitas algo como i = s.find(substr, i) + 1y luego regresa i - 1.
a_guest
1

Solución sin usar bucles y recursividad.

Use el patrón requerido en el método de compilación e ingrese la ocurrencia deseada en la variable 'n' y la última declaración imprimirá el índice inicial de la enésima ocurrencia del patrón en la cadena dada. Aquí, el resultado del buscador, es decir, el iterador, se convierte en una lista y se accede directamente al índice n.

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

Reemplazar un forro es excelente, pero solo funciona porque XX y la barra tienen la misma longitud

Una definición buena y general sería:

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

Esta es la respuesta que realmente desea:

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
fuente
0

Aquí está mi solución para encontrar nla aparición de ben cadena 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)

Es Python puro e iterativo. Para 0 o ndemasiado grande, devuelve -1. Es de una sola línea y se puede utilizar directamente. Aquí hay un ejemplo:

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

Para el caso especial en el que busca la enésima aparición de un carácter (es decir, una subcadena de longitud 1), la siguiente función funciona construyendo una lista de todas las posiciones de las ocurrencias del carácter dado:

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]

Si hay menos nocurrencias del carácter dado, dará IndexError: list index out of range.

Esto se deriva de la respuesta de @ Zv_oDD y se simplifica para el caso de un solo carácter.

Coldfix
fuente
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)

Salida:

'One Two Three'
Chadee Fouad
fuente