如何在Python中用`\ n`替换通用换行符?

时间:2019-06-22 11:39:29

标签: python python-3.x

b'\n' Python3对象中,用bytes替换所有“通用换行符”的最佳方式(最干净,最快)是什么?


编辑:我最终使用了b'\n'.join(bytestr.splitlines()),因为它似乎是最安全的,而且我不介意在最后放一个潜在的换行符。

但是请注意以下@norok2的出色答案,以了解警告,时间安排和更快的解决方案。

4 个答案:

答案 0 :(得分:13)

参加聚会有点晚了,但让我们看看我能为我们做些什么。

首先,一个披露:我最喜欢的是@JohnHennig的double-replace()方法,因为它相当快,而且很清楚发生了什么。

我认为Python内除了其他答案中已经提出的解决方案外,没有其他简单 fast 解决方案(我对其中的一些问题进行了稍微修改,以获得完全相同的解决方案结果为双{replace())。

但是,可能可以加快速度。我最喜欢的加速方法numba无法轻松应用,因为对bytes()和/或bytearray()的支持有限。接下来的事情是使用Cython并在C / C ++中使用它。 由于使用C字符串令人讨厌,所以我求助于C ++字符串。 这样,可以通过一个循环编写此任务。 根据我的测试,这是最快的方法。 为简单起见,我是使用Cython魔术师用IPython编写的。

%load_ext Cython
%%cython --cplus -c-O3 -c-march=native -a


from libcpp.string cimport string


cdef extern from *:
    """
    #include <string>
    std::string & erase(
            std::string & s,
            std::size_t pos,
            std::size_t len) {
        return s.erase(pos, len); }
    """
    string& erase(string& s, size_t pos, size_t len)


cdef string unl_prealloc(string s):
    cdef char nl_lf = b'\n'
    cdef char nl_cr = b'\r'
    cdef char null = b'\0'
    cdef size_t s_size = s.size()
    cdef string result = string(s_size, null)
    cdef size_t i = 0
    cdef size_t j = 0
    while i + 1 <= s_size:
        if s[i] == nl_cr:
            result[j] = nl_lf
            if s[i + 1] == nl_lf:
                i += 1
        else:
            result[j] = s[i]
        j += 1
        i += 1
    return erase(result, j, i - j)


def unl_cython(string b):
    return unl_prealloc(b)

这些是我的计算机上的基准测试图:

(由于在其他计算机上运行,​​实际时间在EDIT上发生了变化)

benchmarks


出于完整性考虑,以下是其他经过测试的功能:

import re


def unl_replace(s):
    return s.replace(b'\r\n', b'\n').replace(b'\r', b'\n')


# EDIT: was originally the commented code, but it is less efficient
# def unl_join(s):
#     nls = b'\r\n', b'\r', b'\n'
#     return b'\n'.join(s.splitlines()) + (
#         b'\n' if any(s.endswith(nl) for nl in nls) else b'')
def unl_join(s):
    result = b'\n'.join(s.splitlines())
    nls = b'\r\n', b'\r', b'\n'
    if any(s.endswith(nl) for nl in nls):
        result += b'\n'
    return result


# Following @VPfB suggestion
def unl_join_new(s):
    return b'\n'.join((s + b'\0').splitlines())[:-1]


def unl_re(s, match=re.compile(b'\r\n?')):
    return match.sub(b'\n', s)


def unl_join_naive(s):  # NOTE: not same result as `unl_replace()`
    return b'\n'.join(s.splitlines())

这是用于生成输入的函数:

def gen_input(num, nl_factor=0.10):
    nls = b'\r\n', b'\r', b'\n'
    words = (b'a', b'b', b' ')
    random.seed(0)
    nl_percent = int(100 * nl_factor)
    base = words * (100 - nl_percent) + nls * nl_percent
    return b''.join([base[random.randint(0, len(base) - 1)] for _ in range(num)])

以及用于生成数据和绘图(来自here的脚本)的调用方式:

funcs = (
    unl_replace,
    unl_cython, 
    unl_join,
    unl_join_new,
    unl_re,
    unl_join_naive,
)

runtimes, input_sizes, labels, results = \
    benchmark(funcs, gen_input=gen_input)
plot_benchmarks(runtimes, input_sizes, labels)

注意

我还用显式循环测试了其他两种可能的实现,但是由于它们比拟议的解决方案要慢几个数量级,因此我从比较中省略了它们,但我在此报告以供将来参考:

def unl_loop(b):
    nl_cr = b'\r'[0]
    nl_lf = b'\n'[0]
    result = b''
    i = 0
    while i + 1 <= len(b):
        if b[i] == nl_cr:
            result += b'\n'
            i += 2 if b[i + 1] == nl_lf else 1
        else:
            result += b[i:i + 1]
            i += 1
    return result


def unl_loop_bytearray(b):
    nl_cr = b'\r'[0]
    nl_lf = b'\n'[0]
    result = bytearray()
    i = 0
    while i + 1 <= len(b):
        if b[i] == nl_cr:
            result.append(nl_lf)
            i += 2 if b[i + 1] == nl_lf else 1
        else:
            result.append(b[i])
            i += 1
    return bytes(result)


def unl_loop_bytearray2(b):
    nl_cr = b'\r'[0]
    nl_lf = b'\n'[0]
    result = bytearray(len(b))
    i = j = 0
    while i + 1 <= len(b):
        if b[i] == nl_cr:
            result[j] = nl_lf
            i += 2 if b[i + 1] == nl_lf else 1
        else:
            result[j] = b[i]
            i += 1
        j += 1
    return bytes(result[:j])


def unl_loop_bytearray3(b):
    nl_cr = b'\r'[0]
    nl_lf = b'\n'[0]
    b = bytearray(b)
    i = 0
    while i + 1 <= len(b):
        if b[i] == nl_cr:
            if b[i + 1] == nl_lf:
                del b[i]
            else:
                b[i] = nl_lf
        i += 1
    return bytes(b)

(编辑:对假设/潜在问题的评论)

假设/潜在问题

对于“混合换行符”文件,例如b'alpha\nbravo\r\ncharlie\rdelta',将\r\n视为1或2换行符在理论上总是存在歧义。 上面实现的所有方法将具有相同的行为,并将\r\n视为单个换行符。

此外,所有这些方法都将出现\r和/或\r\n的虚假存在以及复杂的编码问题,例如,从@JohnHennig注释中提取马拉雅拉姆字母UTF-16中编码为b'\ r \ n',bytes.splitlines()似乎不知道它,并且所有经过测试的方法似乎行为均相同:

s = 'ഊ\n'.encode('utf-16')
print(s)
# b'\xff\xfe\n\r\n\x00'

s.splitlines()
[b'\xff\xfe', b'', b'\x00']

for func in funcs:
    print(func(s))
# b'\xff\xfe\n\n\x00'
# b'\xff\xfe\n\n\x00'
# b'\xff\xfe\n\n\x00'
# b'\xff\xfe\n\n\x00'
# b'\xff\xfe\n\n\x00'
# b'\xff\xfe\n\n\x00'

最后,unl_join_naive()仅依赖于Python实现行拆分,这意味着将要发生的事情不太明显,但将来可能会更好地支持此类问题。 如果此方法位于字符串的末尾,则该方法还将删除最后一个换行符,因此需要一些额外的代码(这将在时序中添加一个通常很小的常量偏移量)来克服此问题。解决该问题的一些建议包括:

  • 最后检查最后一个字符是否存在换行标记(鉴于当前的bytes.splitlines()实现,这现在不是问题,但如果发生虚假的\r\n,将来可能会成为问题)成为最后一个字符,并且bytes.splitlines()的行为对此变得敏感),如unl_join()
  • 在原始输入中添加任何非换行的ASCII 7位字符(例如\0),并删除join()之后的最后一个元素(看起来比前一个更安全,更快),如下所示: unl_join_new()

答案 1 :(得分:6)

这是我过去使用的:

>>> bytestr = b'A sentence\rextending over\r\nmultiple lines.\n'
>>> bytestr.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
b'A sentence\nextending over\nmultiple lines.\n'

我不知道这是否是最好的方式,但是这种方式简单易懂。例如,关键是首先replace两字节序列,然后是其余的孤立\r个字符。

即使上面的示例混合了不同类型的换行符字节序列,也存在一个隐含的假设,即该方法仅适用于在整个过程中都使用相同方法的输入。不管是哪个换行符,它都是不可知的。恰当的例子:b'\r\r\n\n'没有唯一的解释,如果允许混合使用换行符,因为它可能代表3或4个空行。

答案 2 :(得分:5)

正则表达式也可以与bytes对象一起使用。怎么样:

import re

data = b"hello\r\n world\r\naaaaa\rbbbbbb"

print(re.sub(b"\r\n?",b"\n",data))

结果:

b'hello\n world\naaaaa\nbbbbbb'

正则表达式查找\r(可选),后跟\n,然后将其替换为\n。如您所见,它涵盖了所有情况。它也只需要1次通过。在我的替补席上,看来像John's answer这样的两倍bytes.replace快得多。

答案 3 :(得分:4)

b'\n'.join(bytestr.splitlines())

内置bytes.splitlines()似乎比多次bytes.replace()调用更安全,更快:

bytestr = b'A sentence\rextending over\r\nmultiple lines.\n'

timeit b'\n'.join(bytestr.splitlines())
385 ns ± 21.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

timeit bytestr.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
457 ns ± 14.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

它的另一个优点是,具有更强的前瞻性,以防将来的Python版本中“通用换行”行为再次发生变化。

不过,它会在末尾(如果有的话)删除最后的换行符。