Trova l'ennesima occorrenza di sottostringa in una stringa


118

Sembra che dovrebbe essere piuttosto banale, ma sono nuovo in Python e voglio farlo nel modo più pitonico.

Voglio trovare l'indice corrispondente all'ennesima occorrenza di una sottostringa all'interno di una stringa.

Deve esserci qualcosa di equivalente a quello che VOGLIO fare, che è

mystring.find("substring", 2nd)

Come puoi ottenere questo risultato in Python?


7
Trova l'ennesima occorrenza della stringa? Presumo che significhi l'indice dell'ennesima occorrenza?
Mark Byers

2
Sì, l'indice dell'ennesima occorrenza
prestomation

9
Cosa dovrebbe succedere se ci sono corrispondenze sovrapposte? Find_nth ('aaaa', 'aa', 2) dovrebbe restituire 1 o 2?
Mark Byers

Sì! ci deve essere qualcosa per trovare l'ennesima occorrenza di una sottostringa in una stringa e per dividere la stringa all'ennesima occorrenza di una sottostringa.
Reman

Risposte:


69

L'approccio iterativo di Mark sarebbe il solito modo, credo.

Ecco un'alternativa con la suddivisione delle stringhe, che spesso può essere utile per la ricerca di processi correlati:

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)

Ed ecco un veloce (e un po 'sporco, in quanto devi scegliere un po' di pula che non può corrispondere all'ago) one-liner:

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

7
Il primo suggerimento sarà molto inefficiente per stringhe di grandi dimensioni quando la corrispondenza che ti interessa è vicina all'inizio. Guarda sempre l'intera stringa. È intelligente ma non lo consiglierei a qualcuno che è nuovo in Python e vuole solo imparare un buon modo per farlo.
Mark Byers

3
Grazie, mi piace la tua battuta. Non penso che sia la cosa più immediatamente leggibile al mondo, ma non è molto peggio della maggior parte degli altri sotto
prestomation

1
+1 per la battuta, questo dovrebbe aiutarmi adesso. Stavo pensando di fare l'equivalente di .rfind('XXX'), ma sarebbe andato in pezzi se 'XXX'apparisse comunque più tardi nell'input.
Nikhil Chelliah

Questa funzione assume n = 0, 1, 2, 3, ... Sarebbe bello che tu assumessi n = 1, 2, 3, 4, ...
Buon

75

Ecco una versione più pitonica della semplice soluzione 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

Esempio:

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

Se vuoi trovare l'ennesima occorrenza sovrapposta di needle, puoi incrementare di 1invece di len(needle), in questo modo:

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

Esempio:

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

È più facile da leggere rispetto alla versione di Mark e non richiede la memoria aggiuntiva della versione suddivisa o l'importazione del modulo di espressioni regolari. Inoltre aderisce ad alcune delle regole nello Zen di python , a differenza dei vari reapprocci:

  1. Semplice è meglio che complesso.
  2. Flat è meglio che annidato.
  3. La leggibilità conta.

Può essere fatto in una stringa? Come find_nth (df.mystring.str, ('x'), 2) per trovare la posizione della seconda istanza di 'x'?
Arthur D. Howland

36

Questo troverà la seconda occorrenza di sottostringa in string.

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

Modifica: non ho pensato molto alla performance, ma una rapida ricorsione può aiutare a trovare l'ennesima occorrenza:

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)

Può essere esteso in generale per trovare l'elemento n-esimo?
ifly6

Questa è la migliore risposta IMHO, ho fatto una piccola aggiunta per il caso speciale in cui n = 0
Jan Wilmans,

Non volevo modificare il post per brevità. Sono d'accordo con te però, che n = 0 dovrebbe essere trattato come un caso speciale.
Sriram Murali

Questo dovrebbe essere regolato per gestire il caso in cui ci sono meno di noccorrenze della sottostringa. (In questo caso il valore di ritorno scorrerà periodicamente attraverso tutte le posizioni di occorrenza).
coldfix

29

Capendo che la regex non è sempre la soluzione migliore, probabilmente ne userei una qui:

>>> 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
Il rischio qui ovviamente è che la stringa da cercare conterrà caratteri speciali che indurranno la regex a fare qualcosa che non volevi. L'uso di re.escape dovrebbe risolvere questo problema.
Mark Byers

1
Questo è intelligente, ma è davvero pitonico? Sembra eccessivo solo per trovare l'ennesima occorrenza di una sottostringa e non è esattamente facile da leggere. Inoltre, come dici tu, devi importare tutto il re per questo
Todd Gamblin

Quando usi le parentesi quadre, dici a Python di creare l'intero elenco. Le parentesi tonde itererebbero solo attraverso i primi elementi, che è più efficace:(m.start() for m in re.finditer(r"ab",s))[2]
emu

1
@emu No, quello che hai pubblicato non funzionerà; non puoi prendere un indice di un generatore.
Mark Amery

@MarkAmery scusa! Sono abbastanza sorpreso del motivo per cui ho pubblicato quel codice. Tuttavia, una soluzione simile e brutta è possibile utilizzando la itertools.islicefunzione:next(islice(re.finditer(r"ab",s), 2, 2+1)).start()
emu

17

Sto offrendo alcuni risultati di benchmarking confrontando gli approcci più importanti presentati finora, vale a dire @ bobince findnth()(basato su str.split()) contro @ tgamblin o @Mark Byers find_nth()(basato su str.find()). Metterò a confronto anche un'estensione C ( _find_nth.so) per vedere quanto velocemente possiamo andare. Ecco 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

Ovviamente, le prestazioni sono più importanti se la stringa è grande, quindi supponiamo di voler trovare il 1000001 ° newline ('\ n') in un file da 1.3 GB chiamato 'bigfile'. Per risparmiare memoria, vorremmo lavorare su una mmap.mmaprappresentazione dell'oggetto del file:

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)

C'è già il primo problema con findnth(), poiché gli mmap.mmapoggetti non supportano split(). Quindi dobbiamo effettivamente copiare l'intero file in memoria:

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

Ahia! Fortunatamente srientra ancora nei 4 GB di memoria del mio Macbook Air, quindi facciamo un benchmark findnth():

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

Chiaramente una prestazione terribile. Vediamo come funziona l'approccio basato su str.find():

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

Molto meglio! Chiaramente, findnth()il problema è che è costretto a copiare la stringa durante split(), che è già la seconda volta che copiamo gli 1.3 GB di dati in giro dopo s = mm[:]. Ecco il secondo vantaggio di find_nth(): Possiamo usarlo mmdirettamente, in modo tale che siano necessarie zero copie del file:

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

Sembra esserci una piccola penalizzazione delle prestazioni operando su mmvs. s, ma questo dimostra che find_nth()può darci una risposta in 1,2 s rispetto al findnthtotale di 47 s.

Non ho trovato casi in cui l' str.find()approccio basato fosse significativamente peggiore dell'approccio str.split()basato, quindi a questo punto, direi che la risposta di @ tgamblin o di @Mark Byers dovrebbe essere accettata invece di @ bobince.

Nei miei test, la versione di cui find_nth()sopra era la soluzione Python pura più veloce che potessi trovare (molto simile alla versione di @Mark Byers). Vediamo quanto meglio possiamo fare con un modulo di estensione C. Ecco _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);
}

Ecco il setup.pyfile:

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

Installa come al solito con python setup.py install. Il codice C gioca un vantaggio qui poiché è limitato alla ricerca di singoli caratteri, ma vediamo quanto è veloce:

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

Chiaramente ancora un po 'più veloce. È interessante notare che non c'è differenza a livello C tra i casi in memoria e quelli mappati. E 'anche interessante notare che _find_nth2(), che si basa sul string.h's memchr()funzione di libreria, perde contro l'attuazione immediata di _find_nth(): Le ulteriori 'ottimizzazioni' in memchr()apparentemente ritorni di fiamma ...

In conclusione, l'implementazione in findnth()(basata su str.split()) è davvero una cattiva idea, poiché (a) si comporta in modo terribile per stringhe più grandi a causa della copia richiesta e (b) non funziona affatto sugli mmap.mmapoggetti. L'implementazione in find_nth()(basata su str.find()) dovrebbe essere preferita in tutte le circostanze (e quindi essere la risposta accettata a questa domanda).

C'è ancora un bel po 'di margine di miglioramento, poiché l'estensione C ha funzionato quasi un fattore 4 più velocemente del puro codice Python, indicando che potrebbe esserci un caso per una funzione di libreria Python dedicata.


8

Il modo più semplice?

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

firstTest = text.find('test')

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

Posso immaginare che anche questo sia abbastanza performante, rispetto ad altre soluzioni.
Rotareti

7

Probabilmente farei qualcosa di simile, usando la funzione find che accetta un parametro index:

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)

Non è particolarmente Pythonic credo, ma è semplice. Puoi farlo usando invece la ricorsione:

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)

È un modo funzionale per risolverlo, ma non so se questo lo renda più Pythonic.


1
for _ in xrange(n):può essere utilizzato al posto diwhile n: ... n-=1
jfs

@ JF Sebastian: Sì, immagino che sia un po 'più Pythonic. Io aggiornerò.
Mark Byers

BTW: xrange non è più necessario in Python 3: diveintopython3.org/…
Mark Byers

1
return find_nth(s, x, n - 1, i + 1)dovrebbe essere return find_nth(s, x, n - 1, i + len(x)). Non è un grosso problema, ma consente di risparmiare un po 'di tempo di calcolo.
Dan Loewenherz

@dlo: In realtà questo può dare risultati diversi in alcuni casi: find_nth ('aaaa', 'aa', 2). Il mio dà 1, il tuo 2. Immagino che il tuo sia effettivamente ciò che vuole il poster. Aggiornerò il mio codice. Grazie per il commento.
Mark Byers

3

Questo ti darà una serie di indici di partenza per le partite a yourstring:

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

Quindi la tua ennesima voce sarebbe:

n = 2
nth_entry = indices[n-1]

Ovviamente devi stare attento con i limiti dell'indice. Puoi ottenere il numero di istanze di yourstringquesto tipo:

num_instances = len(indices)

2

Ecco un altro approccio che utilizza re.finditer.
La differenza è che questo guarda solo nel pagliaio per quanto necessario

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

Ecco un'altra re+ itertoolsversione che dovrebbe funzionare durante la ricerca di a stro a RegexpObject. Ammetterò liberamente che questo è probabilmente troppo ingegnerizzato, ma per qualche motivo mi ha intrattenuto.

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

Basandosi sulla risposta di modle13 , ma senza la redipendenza dal modulo.

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

Mi piacerebbe che questo fosse un metodo di stringa incorporato.

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

Fornire un'altra soluzione "complicata", che utilizza splite join.

Nel tuo esempio, possiamo usare

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

ha bisogno di una spiegazione
Ctznkane525

find_nth('aaa', 'a', 0)ritorna 1mentre dovrebbe tornare 0. Hai bisogno di qualcosa di simile i = s.find(substr, i) + 1e poi torna i - 1.
a_guest

1

Soluzione senza usare loop e ricorsione.

Usa il modello richiesto nel metodo di compilazione e inserisci l'occorrenza desiderata nella variabile 'n' e l'ultima istruzione stamperà l'indice iniziale dell'ennesima occorrenza del modello nella stringa data. Qui il risultato di finditer ie iterator viene convertito in list e accede direttamente all'ennesimo indice.

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

1

Per il caso speciale in cui si cerca l'ennesima occorrenza di un carattere (ovvero la sottostringa di lunghezza 1), la seguente funzione funziona costruendo un elenco di tutte le posizioni delle occorrenze del carattere dato:

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 ci sono meno di noccorrenze del carattere dato, darà IndexError: list index out of range.

Questo è derivato dalla risposta di @ Zv_oDD e semplificato per il caso di un singolo carattere.


Questo è bellissimo.
Hafiz Hilman Mohammad Sofian

0

La sostituzione di una fodera è ottima ma funziona solo perché XX e la barra hanno la stessa lunghezza

Una buona definizione generale sarebbe:

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

0

Questa è la risposta che vuoi veramente:

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

Ecco la mia soluzione per trovare la nricorrenza di bin 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. Per 0 o ntroppo grande, restituisce -1. È un rivestimento e può essere utilizzato direttamente. Ecco un esempio:

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

0

Def:

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

Usare:

get_first_N_words('  One Two Three Four ' , 3)

Produzione:

'One Two Three'

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.