Найдите n-е вхождение подстроки в строке

118

Кажется, это должно быть довольно тривиально, но я новичок в Python и хочу сделать это самым питоническим способом.

Я хочу найти индекс, соответствующий n-му вхождению подстроки в строке.

Должно быть что-то эквивалентное тому, что Я ХОЧУ делать, а именно

mystring.find("substring", 2nd)

Как этого добиться в Python?

prestomation
источник
7
Найти n-е вхождение строки? Я полагаю, это означает индекс n-го вхождения?
Марк Байерс,
2
Да, индекс n-го вхождения
prestomation
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')
bobince
источник
7
Первое предложение будет очень неэффективным для больших строк, когда интересующее вас совпадение находится в самом начале. Он всегда смотрит на всю строку. Это умно, но я бы не рекомендовал это тем, кто плохо знаком с Python и просто хочет узнать, как это сделать.
Марк Байерс,
3
Спасибо, мне нравится твой лайнер. Я не думаю, что это самая легко читаемая вещь в мире, но она не намного хуже, чем большинство других ниже
prestomation
1
+1 за однострочник, это должно мне помочь прямо сейчас. Я думал о том, чтобы сделать эквивалент .rfind('XXX'), но это развалится, если 'XXX'все равно появится позже во входных данных.
Nikhil Chelliah 07
Эта функция предполагает, что n = 0, 1, 2, 3, ... Было бы неплохо, если бы вы приняли n = 1, 2, 3, 4, ...
Happy
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

Если вы хотите найти n-е перекрывающееся вхождение 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подстрок встречается меньше, чем вхождений. (В этом случае возвращаемое значение будет периодически проходить через все позиции появления).
coldfix
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
Это умно, но действительно ли это Pythonic? Кажется излишним просто найти n-е вхождение подстроки, и это не совсем легко читать. Кроме того, как вы говорите, для этого вам нужно импортировать все re
Тодд Гамблин
Когда вы используете квадратные скобки, вы говорите Python создать весь список. Круглые скобки будут повторять только первые элементы, что более эффективно:(m.start() for m in re.finditer(r"ab",s))[2]
emu
1
@emu Нет, то, что вы разместили, не сработает; вы не можете взять индекс генератора.
Марк Эмери
@MarkAmery, прости! Я очень удивлен, почему я разместил этот код. Тем не менее, подобное и уродливое решение возможно с использованием itertools.isliceфункции:next(islice(re.finditer(r"ab",s), 2, 2+1)).start()
emu
17

Я предлагаю результаты сравнительного анализа наиболее известных подходов, представленных на данный момент, а именно: @bobince findnth()(на основе str.split()) и @ tgamblin или @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 нет разницы между случаями in-memory и mmapped. Также интересно отметить , что _find_nth2(), в основе которой лежит string.h«S memchr()библиотечной функции, теряет против простой реализации в _find_nth(): Дополнительному„оптимизации“вmemchr() по- видимому , отражающиеся ...

В заключение, реализация в findnth()(на основе str.split()) - действительно плохая идея, так как (а) она ужасно работает для больших строк из-за необходимого копирования, и (б) она вообще не работает с 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)
forbzie
источник
Я могу представить, что это тоже довольно эффективно по сравнению с другими решениями.
Rotareti
7

Я бы, наверное, сделал что-то подобное, используя функцию поиска, которая принимает параметр индекса:

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)

Я думаю, это не особенно Pythonic, но все просто. Вместо этого вы можете сделать это с помощью рекурсии:

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)

Это функциональный способ решить эту проблему, но я не знаю, делает ли это его более Pythonic.

Марк Байерс
источник
1
for _ in xrange(n):можно использовать вместоwhile n: ... n-=1
jfs
@JF Себастьян: Да, я думаю, это немного больше Python. Я обновлю.
Марк Байерс,
Кстати: 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)). Ничего страшного, но экономит время вычислений.
Дэн Лёвенгерц,
@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)
modle13
источник
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версия, которая должна работать при поиске либо a, 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]
Zv_oDD
источник
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
источник
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 02
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 и упрощено для случая одного символа.

coldfix
источник
Это прекрасно.
Хафиз Хильман Мохаммад Софиан,
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
yarz-тек
источник
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