文字列内でn番目に出現する部分文字列を見つける


118

これはかなり些細なことのように思えますが、私はPythonに不慣れで、最もPython的な方法でそれを実行したいと考えています。

文字列内のn番目の部分文字列に対応するインデックスを検索したい。

私がやりたいことと同等のものがあるはずです

mystring.find("substring", 2nd)

Pythonでこれをどのように実現できますか?


7
文字列のn番目の出現を見つけますか?それはn番目の発生のインデックスを意味すると思いますか?
Mark Byers、

2
はい、n番目の発生のインデックス
prestomation 2009

9
重複する一致がある場合はどうなりますか?find_nth( 'aaaa'、 'aa'、2)は1または2を返す必要がありますか?
Mark Byers、

はい!文字列内でn番目に出現する部分文字列を見つけて、n番目に出現する部分文字列で文字列を分割する必要があります。
Reman

回答:


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を初めて使用する人に、この方法を学びたいだけの人にはお勧めしません。
Mark Byers、

3
おかげで、私はあなたのワンライナーが好きです。私はそれが世界で最もすぐに読めることだとは思わないが、それは、他のほとんどの下にはるかに悪いではありません
prestomation

1
ワンライナーの+1、これは今私に役立つはずです。私はと同等のことをすることを考えていましたが.rfind('XXX')'XXX'とにかく入力に後で現れるとそれはばらばらになります。
Nikhil Chelliah

この関数は、n = 0、1、2、3、...と仮定します。n= 1、2、3、4、...と仮定するとよいでしょう
Happy

75

これは、単純な反復ソリューションのよりPython的なバージョンです。

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インクリメントできます。1len(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

これはMarkのバージョンよりも読みやすく、分割バージョンやインポートする正規表現モジュールの追加のメモリを必要としません。また、さまざまなアプローチとは異なり、Zen of pythonのいくつかのルールに準拠していますre

  1. シンプルは複雑よりも優れています。
  2. ネストよりもフラットの方が適しています。
  3. 読みやすさが重要です。

これは文字列で実行できますか?find_nth(df.mystring.str、( 'x')、2)のようにして、 'x'の2番目のインスタンスの位置を見つけますか?
アーサーD.ハウランド2018年

36

これにより、string内で2番目に出現するsubstringが検出されます。

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の特別な場合に少し追加しました
Jan Wilmans

簡潔にするために、投稿を編集したくありませんでした。ただし、n = 0は特殊なケースとして扱われるべきだと私は同意します。
Sriram Murali、

これは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を使用すると、これを解決できます。
Mark Byers、

1
これは賢いですが、本当にPythonicですか?部分文字列のn番目のオカレンスを見つけるだけの場合は、やり過ぎのようで、正確に読み取るのは簡単ではありません。また、あなたが言うように、このためにはすべてのreをインポートする必要があります
Todd Gamblin

大括弧を使用する場合、リスト全体を作成するようにPythonに指示します。丸括弧は、より効果的であり、最初の要素を反復あろう:(m.start() for m in re.finditer(r"ab",s))[2]
EMU

1
@emuいいえ、投稿したものは機能しません。ジェネレータのインデックスを取得することはできません。
Mark Amery

@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

もちろん、文字列が大きい場合はパフォーマンスが最も重要なので、 'bigfile'と呼ばれる1.3 GBのファイルで1000001番目の改行( '\ n')を検索するとします。メモリを節約するため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、私のMacbook Airの4 GBのメモリに収まるので、ベンチマークを行ってみましょう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 GBのデータを後でコピーした2回目s = mm[:]です。ここに2番目の利点があります:ファイルのコピーが必要ないようfind_nth()mm直接使用できます。

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

そこには小さなパフォーマンスペナルティ動作しているように見えるmmsが、これはそれが示すfind_nth()に比べて、私たちに1.2秒で答えを得ることができるfindnth47秒のの合計。

str.find()ベースのアプローチがベースのアプローチよりも著しく悪いケースは見つからなかったstr.split()ので、現時点では、@ bobinceではなく@tgamblinまたは@Mark Byersの回答を受け入れる必要があると主張します。

私のテストでは、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

明らかにまだかなり速いです。興味深いことに、メモリ内のケースとmmappedケースの間でCレベルに違いはありません。のライブラリ関数に_find_nth2()基づいているが、の簡単な実装に負けていることも興味深いです。追加の「最適化」は明らかにバックファイアしています...string.hmemchr()_find_nth()memchr()

結論として、findnth()(に基づくstr.split())での実装は、(a)必要なコピーのために大きな文字列に対してひどく実行され、(b)mmap.mmapオブジェクトに対してまったく機能しないため、本当に悪い考えです。find_nth()(に基づくstr.find())での実装は、すべての状況で優先される必要があります(したがって、この質問への回答として受け入れられます)。

C拡張機能は、純粋なPythonコードよりも4倍近い速度で実行され、専用のPythonライブラリ関数の場合があることを示しているため、まだ改善の余地はかなりあります。


8

最も簡単な方法は?

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

firstTest = text.find('test')

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

他のソリューションと比較して、これもかなりパフォーマンスが高いと想像できます。
ロタレティ2017

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)

それは私が推測する特に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セバスチャン:ええ、それはもう少しPythonicだと思います。更新します。
Mark Byers、

ところで、Python 3ではxrangeは不要になりました。diveintopython3.org
Mark Byers

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を与えます。私はあなたが実際にポスターが望むものだと思います。コードを更新します。コメントをありがとう。
Mark Byers、

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

次に、a またはのいずれかを検索するときに機能する別のre+ itertoolsバージョンがstrあり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、つまりiteratorの結果はリストに変換され、n番目のインデックスに直接アクセスしています。

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

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出現を見つけるための私の解決策です:ba

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

文字の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

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'

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.