Python 如何在空白处拆分字符串并保留单词的偏移量和长度

Python 如何在空白处拆分字符串并保留单词的偏移量和长度,python,string,Python,String,我需要将字符串拆分为单词,但也需要得到单词的开始和结束偏移量。例如,如果输入字符串为: input_string = "ONE ONE ONE \t TWO TWO ONE TWO TWO THREE" 我想得到: [('ONE', 0, 2), ('ONE', 5, 7), ('ONE', 9, 11), ('TWO', 17, 19), ('TWO', 21, 23), ('ONE', 25, 27), ('TWO', 29, 31), ('TWO', 33, 35), ('THR

我需要将字符串拆分为单词,但也需要得到单词的开始和结束偏移量。例如,如果输入字符串为:

input_string = "ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"
我想得到:

[('ONE', 0, 2), ('ONE', 5, 7), ('ONE', 9, 11), ('TWO', 17, 19), ('TWO', 21, 23),
 ('ONE', 25, 27), ('TWO', 29, 31), ('TWO', 33, 35), ('THREE', 37, 41)]
我有一些工作代码,可以使用input_string.split和调用.index来实现这一点,但速度很慢。我试图通过手动迭代字符串来编写代码,但速度仍然较慢。有人对此有快速算法吗

以下是我的两个版本:

def using_split(line):
    words = line.split()
    offsets = []
    running_offset = 0
    for word in words:
        word_offset = line.index(word, running_offset)
        word_len = len(word)
        running_offset = word_offset + word_len
        offsets.append((word, word_offset, running_offset - 1))

    return offsets

def manual_iteration(line):
    start = 0
    offsets = []
    word = ''
    for off, char in enumerate(line + ' '):
        if char in ' \t\r\n':
            if off > start:
                offsets.append((word, start, off - 1))
            start = off + 1
            word = ''
        else:
            word += char

    return offsets

通过使用timeit,“使用分割”是最快的,其次是“手动迭代”,那么到目前为止最慢的是使用re.finditer,如下所示。

以下方法可以做到这一点:

def split_span(s):
    for match in re.finditer(r"\S+", s):
        span = match.span()
        yield match.group(0), span[0], span[1] - 1
import re
s = 'ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE'
ret = [(m.group(0), m.start(), m.end() - 1) for m in re.finditer(r'\S+', s)]
print(ret)
这将产生:

[('ONE', 0, 2), ('ONE', 5, 7), ('ONE', 9, 11), ('TWO', 17, 19), ('TWO', 21, 23),
 ('ONE', 25, 27), ('TWO', 29, 31), ('TWO', 33, 35), ('THREE', 37, 41)]

以下运行速度稍快-节省约30%。我所做的只是提前定义函数:

def using_split2(line, _len=len):
    words = line.split()
    index = line.index
    offsets = []
    append = offsets.append
    running_offset = 0
    for word in words:
        word_offset = index(word, running_offset)
        word_len = _len(word)
        running_offset = word_offset + word_len
        append((word, word_offset, running_offset - 1))
    return offsets

以下想法可能会加快速度:

  • 使用deque而不是列表来存储偏移量,并仅在返回时转换为列表。Deque追加不会像列表追加那样导致内存移动
  • 如果已知单词短于某个长度,请在索引函数中提供此项
  • 在本地词典中定义函数
  • 注意:我没有测试这些,但这里有一个例子

    from collections import deque
    
    def using_split(line):
        MAX_WORD_LENGTH = 10
        line_index = line.index
    
        words = line.split()
    
        offsets = deque()
        offsets_append = offsets.append
    
        running_offset = 0
    
        for word in words:
            word_offset = line_index(word, running_offset, running_offset+MAX_WORD_LENGTH)
            running_offset = word_offset + len(word)
            offsets_append((word, word_offset, running_offset - 1))
    
        return list(offsets)
    

    这里有一些面向c的方法,只在整个字符串上迭代一次。 您还可以定义自己的分隔符。 测试和工作,但可能更清洁

    def mySplit(myString, mySeperators):
        w = []
        o = 0
        iW = False
        word = [None, None,None]
        for i,c in enumerate(myString):
            if not c in mySeperators:
                if not iW:
                    word[1]=i
                    iW = True
            if iW == True and c in mySeperators:
                word[2]=i-1
                word[0] = myString[word[1]:i]
                w.append(tuple(word))
                word=[None,None,None]
                iW = False
        return w
    
    mySeperators = [" ", "\t"]
    myString = "ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"
    splitted = mySplit(myString, mySeperators)
    print splitted
    

    这似乎很快就能奏效:

    tuple_list = [(match.group(), match.start(), match.end()) for match in re.compile("\S+").finditer(input_string)]
    

    警告,此解决方案的速度受光速限制:

    def get_word_context(input_string):
        start = 0
        for word in input_string.split():
            c = word[0] #first character
            start = input_string.find(c,start)
            end = start + len(word) - 1
            yield (word,start,end)
            start = end + 2
    
    print list(get_word_context("ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"))
    
    [('1',0,2),('1',5,7),('1',9,11),('2',17,19),('2',21,23),('1',25,27),('2',29,31),('2',33,35),('3',37,41)]


    以下是一些想法,您可以分析它们是否足够快:

    input_string = "".join([" ","ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"," "])
    
    #pre processing
    from itertools import chain
    stuff = list(chain(*zip(range(len(input_string)),range(len(input_string)))))
    print stuff
    stuff = iter(stuff)
    next(stuff)
    
    #calculate
    switches = (i for i in range(0,len(input_string)-1) if (input_string[next(stuff)] in " \t\r\n") ^ (input_string[next(stuff)] in " \t\r\n"))
    print [(word,next(switches),next(switches)-1) for word in input_string.split()]
    
    
    #pre processing
    from itertools import chain
    stuff = list(chain(*zip(range(len(input_string)),range(len(input_string)))))
    print stuff
    stuff = iter(stuff)
    next(stuff)
    
    
    #calculate
    switches = (i for i in range(0,len(input_string)-1) if (input_string[next(stuff)] in " \t\r\n") ^ (input_string[next(stuff)] in " \t\r\n"))
    print [(input_string[i:j+1],i,j-1) for i,j in zip(switches,switches)]
    

    通过直接作弊,我在几分钟内获得了大约35%的加速:我使用cython将您的using_split()函数转换为基于C的python模块。这是我尝试cython的第一个借口,我发现这很容易,也很有价值——见下文

    使用C语言是万不得已的选择:首先,我花了几个小时胡思乱想,试图找到一种比使用_split()版本更快的算法。问题是,本机python str.split()速度惊人,比我使用numpy或re尝试的任何东西都快。因此,即使要扫描字符串两次,str.split()的速度也足够快,似乎不重要,至少对于这个特定的测试数据来说不重要

    为了使用cython,我将您的解析器放在一个名为parser.pyx的文件中:

    ===================== parser.pyx ==============================
    def using_split(line):
        words = line.split()
        offsets = []
        running_offset = 0
        for word in words:
            word_offset = line.index(word, running_offset)
            word_len = len(word)
            running_offset = word_offset + word_len
            offsets.append((word, word_offset, running_offset - 1))
        return offsets
    ===============================================================
    
    然后我运行这个来安装cython(假设是debian ish Linux系统):

    然后我从这个python脚本调用了解析器:

    ================== using_cython.py ============================
    
    #!/usr/bin/python
    
    import pyximport; pyximport.install()
    import parser
    
    input_string = "ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"
    
    def parse():
        return parser.using_split(input_string)
    
    ===============================================================
    
    为了测试,我运行了以下命令:

    python -m timeit "import using_cython; using_cython.parse();"
    
    在我的机器上,使用_split()函数的纯python大约 8.5USEC运行时,而我的cython版本平均约为5.5USEC


    更多详情参见

    我意识到python循环在这里是一个缓慢的操作,因此我开始使用位图,我走了这么远,它仍然很快,但我无法找到一种无循环的方法来获得开始/停止索引:

    import string
    table = "".join([chr(i).isspace() and "0" or "1" for i in range(256)])
    def indexed6(line):
        binline = string.translate(line, table)
        return int(binline, 2) ^ int(binline+"0", 2)
    
    返回的整数为每个开始位置和每个停止+1位置设置位



    p.S.zip()的速度相对较慢:快到可以使用一次,慢到不能使用三次。

    回答很好,很优雅。但事实证明速度较慢:(@xorsyst,此解决方案与使用_split的
    之间的速度差异有多大?此解决方案的速度大约是使用_split@xorsyst:你用编译的
    re
    表达式试过了吗?优雅比…其他任何东西都好!哇-我很惊讶!对我来说,CPython只打了15%的折扣,而PyPy it ma更糟一点。我会暂缓悬赏,给其他人一些时间做一些更令人印象深刻的事情:)它很可能是特定于实现的,pypy并不是为速度而设计的。我的例子是0.020秒,你的例子是0.013秒。最大的开销是for循环,这是目前最短的。根据你使用它的方式,你可以通过把它变成一个生成器来提高整体性能,但如果你真的这样做的话我想从中找出一个只会让它慢下来的列表。我喜欢使用生成器的想法,因为调用代码可能不会使用所有结果,但我需要更长的时间来测试。如果这只是一个更大问题的一部分,我建议使用探查器,并尝试找出问题所在。使函数更快会有帮助,但也会减少对它的调用次数。如果有任何可能它不会使用所有的结果,那么我肯定会建议一个生成器!好主意,但它们没有帮助我担心这与我上面的手动迭代方法类似,只是出于某些原因,速度要慢得多。但是谢谢你的尝试。可能是贝卡使用我使用了一个列表作为分隔符。考虑到这一点,在python中迭代可能不是一个好主意,因为更高级别的字符串函数是用高效的C代码实现的。如果您有任何带有重复字符的长单词,line.index(word[0],running_offset)比line.index(word,running_offset)快(除非你有很多空白)。你可以(在c==word[0]的enumerate(word)中,i代表i,c。)下一步()恐怕这个测试比我的使用_split()稍微慢一点我可以看一下您的测试数据吗?我正在使用给定的字符串,并使用计时器进行测试。您是否尝试过对代码进行Cythonization?我会的,当cython.org从当前发生的任何不好的事情中恢复过来时。:-}
    import string
    table = "".join([chr(i).isspace() and "0" or "1" for i in range(256)])
    def indexed6(line):
        binline = string.translate(line, table)
        return int(binline, 2) ^ int(binline+"0", 2)