Skip to main content
 首页 » 编程设计

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

2025年05月04日14daizhj

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



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

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

请您参考如下方法:

派对晚了一点,但让我们看看我能为我们做些什么。

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

我认为除了其他答案中已经提出的以外,Python中没有其他简单快速的解决方案(我对其中的一些问题进行了稍微修改,以获得与double- 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时发生了变化)





为了完整性,以下是其他经过测试的功能:

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注释中提取,Malayalam字母ഊ在 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()所示;
在原始输入中添加任何非换行的ASCII 7位字符(例如 unl_join()),并删除 \0之后的最后一个元素(看起来比前一个更安全,更快),如 join()所示。