Znajdź n-te wystąpienie podciągu w ciągu

118

Wydaje się, że powinno to być dość trywialne, ale jestem nowy w Pythonie i chcę to zrobić jak najbardziej w Pythonie.

Chcę znaleźć indeks odpowiadający n-temu wystąpieniu podciągu w ciągu.

Musi być coś równoważnego temu, co CHCĘ zrobić, czyli

mystring.find("substring", 2nd)

Jak możesz to osiągnąć w Pythonie?

prestomation
źródło
7
Znajdź n-te wystąpienie łańcucha? Zakładam, że oznacza to indeks n-tego wystąpienia?
Mark Byers
2
Tak, indeks n-tego wystąpienia
prestomowa
9
Co powinno się stać, jeśli są nakładające się mecze? Czy find_nth ('aaaa', 'aa', 2) powinno zwrócić 1 czy 2?
Mark Byers
Tak! musi być coś, co znajdzie n-te wystąpienie podciągu w ciągu znaków i podzieli go przy n-tym wystąpieniu podciągu.
Reman

Odpowiedzi:

69

Myślę, że iteracyjne podejście Marka byłoby typowym sposobem.

Oto alternatywa z dzieleniem ciągów, która często może być przydatna do znajdowania procesów powiązanych:

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)

A oto szybki (i nieco brudny, ponieważ musisz wybrać plewy, które nie pasują do igły):

'foo bar bar bar'.replace('bar', 'XXX', 1).find('bar')
bobince
źródło
7
Pierwsza sugestia będzie bardzo nieefektywna w przypadku dużych ciągów, gdy interesujący Cię mecz jest blisko początku. Zawsze patrzy na cały ciąg. Jest to sprytne, ale nie polecałbym tego komuś, kto jest nowy w Pythonie i po prostu chce się nauczyć, jak to robić.
Mark Byers
3
Dzięki, podoba mi się twoja jedna wkładka. Nie sądzę, że jest to najbardziej czytelna rzecz na świecie, ale nie jest dużo gorsza niż większość innych poniżej
prestom
1
+1 dla jednej linijki, to powinno mi teraz pomóc. Myślałem o zrobieniu odpowiednika .rfind('XXX'), ale to by się rozpadło, gdyby i tak 'XXX'pojawiło się później na wejściu.
Nikhil Chelliah,
Ta funkcja zakłada n = 0, 1, 2, 3, ... Byłoby miło, gdybyś założył, że n = 1, 2, 3, 4, ...
Happy
75

Oto bardziej Pythonic wersja prostego rozwiązania iteracyjnego:

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

Przykład:

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

Jeśli chcesz znaleźć n-te nakładające się wystąpienie needle, możesz zwiększyć o 1zamiast len(needle), na przykład:

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

Przykład:

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

Jest to łatwiejsze do odczytania niż wersja Marka i nie wymaga dodatkowej pamięci wersji dzielącej lub importowania modułu wyrażeń regularnych. W przeciwieństwie do różnych podejść, przestrzega również kilku zasad Zen Pythonare :

  1. Proste jest lepsze niż złożone.
  2. Płaskie jest lepsze niż zagnieżdżone.
  3. Liczy się czytelność.
Todd Gamblin
źródło
Czy można to zrobić w ciągu? Na przykład find_nth (df.mystring.str, ('x'), 2), aby znaleźć pozycję drugiej instancji 'x'?
Arthur D. Howland
36

To znajdzie drugie wystąpienie podciągu w ciągu.

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

Edycja: Nie myślałem dużo o wydajności, ale szybka rekurencja może pomóc w znalezieniu n-tego wystąpienia:

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
źródło
Czy można to ogólnie rozszerzyć, aby znaleźć n-ty element?
ifly6
To najlepsza odpowiedź IMHO, zrobiłem mały dodatek dla specjalnego przypadku, w którym n = 0
Jan Wilmans
Nie chciałem edytować posta ze względu na zwięzłość. Zgadzam się jednak z Tobą, że n = 0 należy traktować jako przypadek szczególny.
Sriram Murali
Powinno to być dostosowane do obsługi w przypadku, gdy jest mniej niż nwystąpień podciągu. (W tym przypadku wartość zwracana będzie cyklicznie przechodzić przez wszystkie pozycje występowania).
coldfix
29

Rozumiejąc, że regex nie zawsze jest najlepszym rozwiązaniem, prawdopodobnie użyłbym jednego tutaj:

>>> 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
źródło
4
Oczywiście istnieje ryzyko, że szukany ciąg będzie zawierał znaki specjalne, które spowodują, że wyrażenie regularne zrobi coś, czego nie chcesz. Użycie re.escape powinno rozwiązać ten problem.
Mark Byers
1
To sprytne, ale czy to naprawdę Pythonic? Wydaje się, że to przesada za znalezienie n-tego wystąpienia podciągu i nie jest to łatwe do odczytania. Ponadto, jak mówisz, musisz zaimportować wszystko do tego
Todd Gamblin,
Używając nawiasów kwadratowych, mówisz Pythonowi, aby utworzył całą listę. Nawiasy okrągłe będą iterować tylko przez pierwsze elementy, co jest bardziej efektywne:(m.start() for m in re.finditer(r"ab",s))[2]
emu
1
@emu Nie, to, co opublikowałeś, nie zadziała; nie możesz wziąć indeksu generatora.
Mark Amery,
@MarkAmery sorry! Jestem dość zaskoczony, dlaczego opublikowałem ten kod. Mimo to możliwe jest podobne i brzydkie rozwiązanie przy użyciu itertools.islicefunkcji:next(islice(re.finditer(r"ab",s), 2, 2+1)).start()
emu
17

Przedstawiam wyniki testów porównawczych, porównujące najbardziej znaczące podejścia zaprezentowane do tej pory, a mianowicie @ bobince's findnth()(na podstawie str.split()) vs. @ tgamblin's lub @Mark Byers ' find_nth()(na podstawie str.find()). Porównam również z rozszerzeniem C ( _find_nth.so), aby zobaczyć, jak szybko możemy jechać. Oto 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

Oczywiście wydajność ma największe znaczenie, jeśli łańcuch jest duży, więc przypuśćmy, że chcemy znaleźć 1000001. znak nowej linii („\ n”) w pliku o nazwie „bigfile” o wielkości 1,3 GB. Aby zaoszczędzić pamięć, chcielibyśmy popracować nad mmap.mmapreprezentacją obiektową pliku:

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)

Jest już pierwszy problem findnth(), ponieważ mmap.mmapobiekty nie obsługują split(). Więc właściwie musimy skopiować cały plik do pamięci:

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

Auć! Na szczęście snadal mieści się w 4 GB pamięci mojego Macbooka Air, więc zróbmy benchmark findnth():

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

Najwyraźniej okropny występ. Zobaczmy, jak działa podejście oparte na str.find():

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

Dużo lepiej! Najwyraźniej findnth()problem polega na tym, że jest on zmuszony do skopiowania ciągu w trakcie split(), co jest już drugim razem, gdy kopiowaliśmy 1,3 GB danych dookoła s = mm[:]. Tu pojawia się druga zaleta find_nth(): Możemy go używać mmbezpośrednio, tak że nie są wymagane żadne kopie pliku:

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

Wydaje się, że działanie na mmvs. jest niewielki spadek wydajności s, ale to pokazuje, że find_nth()możemy uzyskać odpowiedź w 1,2 sekundy w porównaniu do findnth47 sekund.

Nie znalazłem przypadków, w których str.find()podejście oparte było znacznie gorsze niż str.split()podejście oparte, więc w tym miejscu argumentowałbym, że odpowiedź @ tgamblin lub @Mark Byers powinna zostać zaakceptowana zamiast @ bobince.

W moich testach find_nth()powyższa wersja była najszybszym czystym rozwiązaniem Pythona, jakie mogłem wymyślić (bardzo podobnym do wersji @Mark Byers). Zobaczmy, o ile lepiej możemy zrobić z modułem rozszerzającym C. Oto _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);
}

Oto setup.pyplik:

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

Zainstaluj jak zwykle z python setup.py install. Kod C odgrywa tutaj przewagę, ponieważ ogranicza się do znajdowania pojedynczych znaków, ale zobaczmy, jak szybko to jest:

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

Najwyraźniej jeszcze trochę szybciej. Co ciekawe, na poziomie C nie ma różnicy między obudowami in-memory i mmapped. Warto również zauważyć, że _find_nth2(), który opiera się na string.h„s memchr()funkcja biblioteki, traci się przeciwko zwykłej realizacji w _find_nth(): dodatkowy«optymalizacje»w memchr()widocznie mści ...

Podsumowując, implementacja w findnth()(oparta na str.split()) jest naprawdę złym pomysłem, ponieważ (a) działa strasznie w przypadku większych ciągów z powodu wymaganego kopiowania i (b) w ogóle nie działa na mmap.mmapobiektach. Implementacja w find_nth()(oparta na str.find()) powinna być preferowana we wszystkich okolicznościach (i dlatego powinna być akceptowaną odpowiedzią na to pytanie).

Wciąż jest sporo miejsca na ulepszenia, ponieważ rozszerzenie C działało prawie 4 razy szybciej niż czysty kod Pythona, co wskazuje, że może istnieć argument za dedykowaną funkcją biblioteczną Pythona.

Stefan
źródło
8

Najprostszy sposób?

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

firstTest = text.find('test')

print text.find('test', firstTest + 1)
Forbzie
źródło
Mogę sobie wyobrazić, że jest to również całkiem wydajne w porównaniu z innymi rozwiązaniami.
Rotareti
7

Prawdopodobnie zrobiłbym coś takiego, używając funkcji find, która przyjmuje parametr indeksu:

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)

Wydaje mi się, że nie jest to specjalnie Pythonic, ale jest proste. Zamiast tego możesz to zrobić używając rekurencji:

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)

To funkcjonalny sposób na rozwiązanie tego problemu, ale nie wiem, czy to czyni go bardziej Pythonowym.

Mark Byers
źródło
1
for _ in xrange(n):można użyć zamiastwhile n: ... n-=1
jfs
@JF Sebastian: Tak, myślę, że to trochę bardziej Pythonic. Zaktualizuję.
Mark Byers
BTW: xrange nie jest już potrzebne w Pythonie 3: diveintopython3.org/…
Mark Byers
1
return find_nth(s, x, n - 1, i + 1)powinno być return find_nth(s, x, n - 1, i + len(x)). Nie jest to wielka sprawa, ale oszczędza trochę czasu obliczeń.
Dan Loewenherz
@dlo: Właściwie to może dać różne wyniki w niektórych przypadkach: find_nth ('aaaa', 'aa', 2). Mój daje 1, twój daje 2. Domyślam się, że twój jest tym, czego chce plakat. Zaktualizuję mój kod. Dziękuję za komentarz.
Mark Byers
3

To da ci tablicę indeksów początkowych dla dopasowań do yourstring:

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

Wtedy twój n-ty wpis wyglądałby tak:

n = 2
nth_entry = indices[n-1]

Oczywiście musisz uważać na granice indeksu. Możesz uzyskać liczbę takich wystąpień yourstring:

num_instances = len(indices)
model13
źródło
2

Oto inne podejście wykorzystujące re.finditer.
Różnica polega na tym, że zagląda to do stogu siana tylko wtedy, gdy jest to konieczne

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
źródło
2

Oto kolejna wersja re+, itertoolsktóra powinna działać podczas wyszukiwania a strlub a RegexpObject. Przyznam, że jest to prawdopodobnie przesadzone, ale z jakiegoś powodu bawiło mnie to.

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
źródło
2

Opierając się na odpowiedzi modle13 , ale bez rezależności od modułu.

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

Chciałbym, żeby to była wbudowana metoda ciągów.

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

Dostarczenie innego „podstępnego” rozwiązania, które wykorzystuje spliti join.

W Twoim przykładzie możemy użyć

len("substring".join([s for s in ori.split("substring")[:2]]))
Ivor Zhou
źródło
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
źródło
potrzebuje wyjaśnienia
Ctznkane525
find_nth('aaa', 'a', 0)zwraca, 1podczas gdy powinien powrócić 0. Potrzebujesz czegoś takiego, i = s.find(substr, i) + 1a potem wróć i - 1.
a_guest
1

Rozwiązanie bez używania pętli i rekurencji.

Użyj wymaganego wzorca w metodzie kompilacji i wprowadź żądane wystąpienie w zmiennej 'n', a ostatnia instrukcja wypisze indeks początkowy n-tego wystąpienia wzorca w podanym ciągu. Tutaj wynik działania finditera, czyli iteratora, jest konwertowany na listę i uzyskuje bezpośredni dostęp do n-tego indeksu.

import re
n=2
sampleString="this is history"
pattern=re.compile("is")
matches=pattern.finditer(sampleString)
print(list(matches)[n].span()[0])
Karthik
źródło
1

W szczególnym przypadku, w którym szukasz n-tego wystąpienia znaku (tj. Podłańcuch o długości 1), następująca funkcja działa poprzez zbudowanie listy wszystkich pozycji wystąpień danego znaku:

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]

Jeśli będzie mniej niż nwystąpień danej postaci, to da IndexError: list index out of range.

To pochodzi od @ Zv_oDD za odpowiedź i uproszczone dla przypadku pojedynczego znaku.

coldfix
źródło
To jest piękne.
Hafiz Hilman Mohammad Sofian
0

Wymiana jednej wkładki jest świetna, ale działa tylko dlatego, że XX i kierownica mają tę samą długość

Dobra i ogólna def to:

def findN(s,sub,N,replaceString="XXX"):
    return s.replace(sub,replaceString,N-1).find(sub) - (len(replaceString)-len(sub))*(N-1)
Charles Doutriaux
źródło
0

Oto odpowiedź, której naprawdę chcesz:

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
źródło
0

Oto moje rozwiązanie do znalezienia nwystąpienia bw łańcuchu 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)

Jest to czysty Python i iteracyjny. W przypadku 0 lub nzbyt dużej wartości zwraca -1. Jest jednoliniowy i można go używać bezpośrednio. Oto przykład:

>>> reduce(lambda x, y: -1 if y > x + 1 else 'bibarbobaobaotang'.find('b', x + 1), range(4), -1)
7
黄锐铭
źródło
0

Def:

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

Używać:

get_first_N_words('  One Two Three Four ' , 3)

Wynik:

'One Two Three'
Cierń Fouad
źródło