Знайдіть 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.