Знайдіть n-е виникнення підрядків у рядку


118

Це здається, що це має бути досить тривіально, але я новачок у Python і хочу зробити це самим піфонічним способом.

Я хочу знайти індекс, відповідний п’ятому виникненню підрядки всередині рядка.

Повинно бути щось еквівалентне тому, що Я ХОЧУ робити, що є

mystring.find("substring", 2nd)

Як можна досягти цього в Python?


7
Знайдіть n-е виникнення струни? Я припускаю, що це означає індекс п’ятої появи?
Марк Байєрс

2
Так, індекс п’ятої зустрічі
престомація

9
Що має статися, якщо є збіги, що перетинаються? Чи повинен find_nth ('aaaa', 'aa', 2) повернути 1 або 2?
Марк Байєрс

Так! повинно бути щось, щоб знайти n-е виникнення підрядки в рядку та розділити рядок при n-му появі підрядки.
Реман

Відповіді:


69

Думаю, ітеративний підхід Марка був би звичайним способом.

Ось альтернатива розбиття рядків, яка часто може бути корисною для пошуку пов'язаних процесів:

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 і просто хоче навчитися хорошому способу це зробити.
Марк Байєрс

3
Дякую, мені подобається ваш один лайнер. Я не думаю, що це найбільш читабельна річ у світі, але це не набагато гірше, ніж більшість інших нижче
передчуття

1
+1 для однолінійного, це має мені зараз допомогти. Я думав зробити еквівалент .rfind('XXX'), але це розвалиться, якщо все-таки 'XXX'з’явиться пізніше у введенні.
Нікхіл Челлі

Ця функція припускає n = 0, 1, 2, 3, ... Було б добре, якщо ви припускаєте n = 1, 2, 3, 4, ...
Щасливий

75

Ось більш піфонічна версія прямого ітеративного рішення:

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

Якщо ви хочете знайти NTH перекривається виникнення з 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

Це легше для читання, ніж версія Марка, і для нього не потрібна додаткова пам'ять версії для розщеплення або імпорту модуля регулярного вираження. Він також дотримується деяких правил у дзен пітона , на відміну від різних reпідходів:

  1. Простий - краще, ніж складний.
  2. Квартира краще, ніж вкладена.
  3. Читання рахується.

Чи можна це зробити в рядку? Як find_nth (df.mystring.str, ('x'), 2), щоб знайти позицію 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
Ян Вільманс

Я не хотів коротко редагувати публікацію. Я згоден з вами, що n = 0 слід розглядати як особливий випадок.
Шрірам Муралі

Це слід скорегувати, щоб обробити той випадок, коли кількість nпідрядок є меншою, ніж кількість випадків. (У цьому випадку повернене значення періодично буде циклічно проходити через усі положення виникнення).
виправлення

29

Розуміючи, що регулярний вираз не завжди є найкращим рішенням, я, мабуть, використовую його тут:

>>> 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 повинно це вирішити.
Марк Байєрс

1
Це розумно, але чи це насправді піфонічно? Здається, надмірність лише для того, щоб знайти n-е виникнення підрядка, і читати це не зовсім просто. Крім того, як ви кажете, вам доведеться імпортувати все для цього
Todd Gamblin

Використовуючи квадратні дужки, ви скажете Python створити весь список. Круглі дужки перейдуть лише через перші елементи, що є більш ефективним:(m.start() for m in re.finditer(r"ab",s))[2]
ему

1
@emu Ні, те, що ви розмістили, не працюватиме; ви не можете прийняти індекс генератора.
Марк Амеррі

@MarkAmery вибачте! Я дуже здивований, чому я розмістив цей код. Тим не менш, подібне і потворне рішення можливе за допомогою itertools.isliceфункції:next(islice(re.finditer(r"ab",s), 2, 2+1)).start()
ему

17

Я пропоную деякі результати бенчмаркінгу, порівнюючи найвизначніші підходи, представлені до цього часу, а саме: @ bobince's findnth()(на основі str.split()) проти @ tgamblin's або @Mark Byers ' find_nth()(на основі 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

Звичайно, продуктивність має найбільше значення, якщо рядок є великим, тому припустимо, що ми хочемо знайти 1000001-й новий рядок ('\ n') у файлі 1,3 ГБ під назвою "bigfile". Щоб зберегти пам'ять, ми хотіли б працювати над 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все ще входить в 4 Гб пам'яті мого Macbook Air, тому давайте орієнтир 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(), що вже вдруге ми скопіювали 1,3 ГБ даних після 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 с порівняно findnthз загальною кількістю 47 с.

Я не знайшов жодного випадку, коли 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 немає різниці між випадками пам'яті та mmapped. Також цікаво відзначити , що _find_nth2(), в основі якої лежить string.h«S memchr()бібліотечної функції, втрачає проти простої реалізації в _find_nth(): Додаткового" оптимізації "в по- memchr()видимому , й це позначається ...

На закінчення, реалізація в findnth()(на основі str.split()) - це дійсно погана ідея, оскільки (a) вона спрацьовує жахливо для великих рядків через необхідне копіювання, і (b) вона взагалі не працює на mmap.mmapоб'єктах. Реалізація find_nth()(заснована на str.find()) повинна віддавати перевагу в будь-яких обставинах (і тому є прийнятою відповіддю на це питання).

Є ще багато можливостей для вдосконалення, оскільки розширення C працює майже на 4 рази швидше, ніж чистий код Python, що вказує на те, що може бути випадок для спеціальної бібліотеки Python.


8

Найпростіший спосіб?

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

firstTest = text.find('test')

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

Я можу собі уявити, що це досить добре, порівняно з іншими рішеннями.
Ротарети

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)

Думаю, це не особливо піфонічно, але просто. Ви можете зробити це замість рекурсії:

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)

Це функціональний спосіб її вирішити, але я не знаю, чи це робить його більш пітонічним.


1
for _ in xrange(n):можна використовувати замістьwhile n: ... n-=1
jfs

@JF Себастьян: Так, я думаю, це трохи більше пітонічне. Я оновлю.
Марк Байєрс

BTW: xrange більше не потрібен в 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

@dlo: насправді це може давати різні результати в деяких випадках: find_nth ('aaaa', 'aa', 2). Моє дає 1, твоє дає 2. Я думаю, твоє насправді те, чого хоче плакат. Я оновлю код. Дякуємо за коментар
Марк Байєрс

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

Ось ще одна версія re+, itertoolsяка повинна працювати при пошуку strабо a RegexpObject. Я вільно визнаю, що це, мабуть, надто інженерно, але чомусь це мене розважало.

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

find_nth('aaa', 'a', 0)повертається, 1поки має повернутися 0. Вам потрібно щось на кшталт, i = s.find(substr, i) + 1а потім повертайтеся i - 1.
a_guest

1

Рішення без використання циклів і рекурсії.

Використовуйте необхідний шаблон у методі компіляції та введіть потрібну подію у змінну 'n', і остання операція надрукує початковий індекс n-го появи шаблону у заданому рядку. Тут результат Finditer, тобто ітератор, перетворюється на список і безпосередньо отримує доступ до n-го індексу.

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

1

Для особливого випадку, коли ви шукаєте 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.

Це відбувається від @ Zv_oDD в відповідь і спрощені для випадку одного символу.



0

Замінити один вкладиш чудово, але працює лише тому, що XX і бар мають однакову довжину

Хорошим і загальним визначенням було б:

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

Ось моє рішення для пошуку nвиникнення 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

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'

-2

Як щодо:

c = os.getcwd().split('\\')
print '\\'.join(c[0:-2])

це не відповідь на початкове запитання
Jerzyk

Це не дає відповіді на запитання. Коли у вас буде достатня репутація, ви зможете коментувати будь-яку публікацію ; натомість надайте відповіді, які не потребують уточнення від запитувача .
Єжик
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.