【问题标题】:How to split a string on whitespace and retain offsets and lengths of words如何在空格上拆分字符串并保留单词的偏移量和长度
【发布时间】:2012-03-01 15:20:30
【问题描述】:

我需要将一个字符串拆分成单词,还要获取单词的开始和结束偏移量。因此,例如,如果输入字符串是:

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,“using_split”是最快的,其次是“manual_iteration”,到目前为止最慢的是使用 re.finditer,如下所示。

【问题讨论】:

  • 如果你有任何重复字符的长单词,line.index(word[0], running_offset) 比 line.index(word, running_offset) 快(除非你有很多空白) .你可以去 (i for i,c in enumerate(word) of c==word[0]).next()

标签: python string


【解决方案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)]

【讨论】:

  • 不错,优雅的答案。结果是慢了:(
  • @xorsyst,这个方案和using_split的速度差距有多大?
  • 这个进来的时间大约是 using_split 的 1.6 倍
  • @xorsyst:你用编译过的re 表达式试过了吗?
  • 优雅胜过……其他一切!
【解决方案2】:

以下运行速度稍快 - 节省约 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

【讨论】:

  • 哇 - 我很惊讶!对我来说,CPython 只获得 15% 的折扣,而使用 PyPy 则稍微差了一点。我会保留一点赏金,给其他人一些时间来做一些更令人印象深刻的事情:)
  • 它很可能是特定于实现的,而 pypy 并不是为速度而设计的。我的例子是 0.020s,我的例子是 0.013s。
  • 最大的开销是 for 循环,目前它可能很短。根据你使用它的方式,你可以通过将它变成一个生成器来整体提高性能,但如果你真的想要一个列表,它只会减慢它的速度。
  • 我喜欢使用生成器的想法,它们可能会节省一些,因为调用代码可能不会使用所有结果,但我需要更长的时间来测试。
  • 如果这只是更大问题的一部分,我建议使用分析器并尝试查明问题所在。使函数更快会有所帮助,但也会减少对它的调用次数。如果有可能它不会使用所有结果,那么我肯定会建议使用生成器!
【解决方案3】:
def split_span(s):
    for match in re.finditer(r"\S+", s):
        span = match.span()
        yield match.group(0), span[0], span[1] - 1

【讨论】:

    【解决方案4】:

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

    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"))
    

    [('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)]

    【讨论】:

    • 恐怕这个测试比我的 using_split() 稍慢
    • 我可以看看你的测试数据吗?
    • 我正在使用给定的字符串,并使用 Timer 来测试它。
    【解决方案5】:

    通过彻底的作弊,我能够在几分钟内获得大约 35% 的加速:我使用 cython 将您的 using_split() 函数转换为基于 C 的 python 模块。这是我不得不尝试 cython 的第一个借口,我发现它非常简单且有益 - 见下文。

    使用 C 是最后的手段:首先,我花了几个小时试图找到比您的 using_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 机器):

    sudo apt-get install cython
    

    然后我从这个 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();"
    

    在我的机器上,你的纯 python using_split() 函数平均约为 8.5 微秒运行时间,而我的 cython 版本平均大约 5.5 微秒。

    更多详情http://docs.cython.org/src/userguide/source_files_and_compilation.html

    【讨论】:

    【解决方案6】:

    以下想法可能会导致加速:

    1. 使用双端队列而不是列表来存储偏移量并仅在返回时转换为列表。双端队列追加不会像列表追加那样导致内存移动。
    2. 如果已知单词比某个长度短,请在索引函数中提供。
    3. 在本地字典中定义您的函数。

    注意:我没有测试过这些,但这里有一个例子

    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)
    

    【讨论】:

    • 好主意,但恐怕没有帮助
    【解决方案7】:

    这里你有一些面向 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
    

    【讨论】:

    • 这类似于我上面的手动迭代方法,只是由于某种原因慢得多。但感谢您的尝试。
    • 可能是因为我使用了分隔符列表。想一想,在python中迭代可能不是一个好主意,因为更高级别的字符串函数是用高效的C代码实现的。
    【解决方案8】:

    这似乎工作得很快:

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

    【讨论】:

      【解决方案9】:

      您可以分析以下几个想法,看看它们是否足够快:

      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)]
      

      【讨论】:

        【解决方案10】:

        在我看来,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 位置设置了位。

        附: zip() 比较慢:快用一次,太慢用不了3次。

        【讨论】:

          猜你喜欢
          • 2017-07-08
          • 1970-01-01
          • 2014-11-28
          • 2011-05-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-02-14
          • 1970-01-01
          相关资源
          最近更新 更多