在Python中将带有BOM的UTF-8转换为无BOM的UTF-8

110

这里有两个问题。我有一组通常带BOM的UTF-8文件。我想将它们(最好是就地)转换为没有BOM的UTF-8。似乎codecs.StreamRecoder(stream, encode, decode, Reader, Writer, errors)可以处理这个问题。但是我没有看到任何使用示例。这是否是处理此问题的最佳方法?

source files:
Tue Jan 17$ file brh-m-157.json 
brh-m-157.json: UTF-8 Unicode (with BOM) text
此外,如果我们无需显式地知道不同的输入编码(已看到ASCII和UTF-16),那将是理想的。似乎所有这些都是可行的。是否有一种解决方案可以将任何已知的Python编码作为输入,并以UTF-8(没有BOM)输出?

编辑1下面提出的解决方案(谢谢!)
fp = open('brh-m-157.json','rw')
s = fp.read()
u = s.decode('utf-8-sig')
s = u.encode('utf-8')
print fp.encoding  
fp.write(s)

这给我返回以下错误:

IOError: [Errno 9] Bad file descriptor

最新消息

在评论中,有人告诉我错误在于使用 'rw' 模式打开文件,而不是 'r+'/'r+b' 模式,所以我应该最终重新编辑我的问题并删除已解决部分。


2
你需要以“r+”模式打开文件进行读取和更新。同时添加“b”选项,这样它也可以在Windows上运行而不会出现任何奇怪的行尾问题。最后,你需要将文件指针回到文件开头并截断文件末尾 - 请参考我的更新答案。 - Martin Geisler
目前的第二个Python 3答案需要点赞。 - Josiah Yoder
7个回答

161

这个答案适用于Python 2

只需使用"utf-8-sig"编解码器

fp = open("file.txt")
s = fp.read()
u = s.decode("utf-8-sig")

这将给你一个没有BOM的Unicode字符串。然后你可以使用它。
s = u.encode("utf-8")

要获取一个正常的UTF-8编码的字符串,可以使用s。如果你的文件很大,那么应该避免将它们全部读入内存中。BOM只是文件开头的三个字节,因此可以使用以下代码将其从文件中删除:
import os, sys, codecs

BUFSIZE = 4096
BOMLEN = len(codecs.BOM_UTF8)

path = sys.argv[1]
with open(path, "r+b") as fp:
    chunk = fp.read(BUFSIZE)
    if chunk.startswith(codecs.BOM_UTF8):
        i = 0
        chunk = chunk[BOMLEN:]
        while chunk:
            fp.seek(i)
            fp.write(chunk)
            i += len(chunk)
            fp.seek(BOMLEN, os.SEEK_CUR)
            chunk = fp.read(BUFSIZE)
        fp.seek(-BOMLEN, os.SEEK_CUR)
        fp.truncate()

它打开文件,读取一块数据,并将其写入比读取位置早3个字节的位置。文件在原地重写。更简单的解决方案是将较短的文件写入一个新文件,例如newtover's answer。这样做会更简单,但在短时间内会使用两倍的磁盘空间。
至于猜测编码方式,你可以按照从最具体到最不具体的顺序循环遍历编码方式。
def decode(s):
    for encoding in "utf-8-sig", "utf-16":
        try:
            return s.decode(encoding)
        except UnicodeDecodeError:
            continue
    return s.decode("latin-1") # will always work

一个UTF-16编码的文件无法解码为UTF-8,所以我们首先尝试使用UTF-8。如果失败了,那么我们再尝试使用UTF-16。最后,我们使用Latin-1 — 因为在Latin-1中所有256个字节都是合法值,所以这总是有效的。在这种情况下,您可能希望返回None,因为它实际上是一个备用方案,您的代码可能需要更加小心地处理它(如果可以的话)。

1
嗯,我在第一次编辑中更新了问题,并提供了示例代码,但出现了错误的文件描述符。感谢任何帮助。试图找出这个问题的所在。 - timpone
2
似乎出现了AttributeError: 'str' object has no attribute 'decode'的错误。所以我最终使用了这段代码with open(filename,encoding='utf-8-sig') as f_content:,然后doc = f_content.read(),这样就可以正常工作了。 - clement116
优秀的解决方案,运行得很好!非常感谢,你帮我节省了几个小时。 - brillenheini

90

在Python 3中,很容易做到:读取文件并使用utf-8编码重新编写它:

s = open(bom_file, mode='r', encoding='utf-8-sig').read()
open(bom_file, mode='w', encoding='utf-8').write(s)

8
import codecs
import shutil
import sys

s = sys.stdin.read(3)
if s != codecs.BOM_UTF8:
    sys.stdout.write(s)

shutil.copyfileobj(sys.stdin, sys.stdout)

1
你能解释一下这段代码是如何工作的吗?$ remove_bom.py < input.txt > output.txt。我说得对吗? - guneysus
1
@guneysus,是的,完全正确。 - newtover

6
我发现这个问题是因为使用 configparser.ConfigParser().read(fp) 打开 UTF8 BOM 头文件时出现了问题。
对于那些正在寻找解决方案以删除头部,使 ConfigPhaser 能够打开配置文件而不是报告错误:File contains no section headers 的人,请按照以下方式打开文件:
configparser.ConfigParser().read(config_file_path, encoding="utf-8-sig")

这可以帮助你省去删除文件BOM头的大量工作。
(我知道这听起来与主题无关,但希望这能帮助像我一样苦苦挣扎的人们。)

2
当我第一次使用try-except时,它也可以轻松打开UTF-8“非BOM”编码的文件,没有任何问题。 - flipSTAR
1
secedit /export 存在类似的问题,它会创建 UTF-16 LE BOM 文件。以下方法适用于这种情况:config.read('sec.cfg', encoding="utf-16") - jetnet

5

这是我实现的将任何编码转换为无BOM UTF-8并将Windows换行符替换为通用格式的方法:

def utf8_converter(file_path, universal_endline=True):
    '''
    Convert any type of file to UTF-8 without BOM
    and using universal endline by default.

    Parameters
    ----------
    file_path : string, file path.
    universal_endline : boolean (True),
                        by default convert endlines to universal format.
    '''

    # Fix file path
    file_path = os.path.realpath(os.path.expanduser(file_path))

    # Read from file
    file_open = open(file_path)
    raw = file_open.read()
    file_open.close()

    # Decode
    raw = raw.decode(chardet.detect(raw)['encoding'])
    # Remove windows end line
    if universal_endline:
        raw = raw.replace('\r\n', '\n')
    # Encode to UTF-8
    raw = raw.encode('utf8')
    # Remove BOM
    if raw.startswith(codecs.BOM_UTF8):
        raw = raw.replace(codecs.BOM_UTF8, '', 1)

    # Write to file
    file_open = open(file_path, 'w')
    file_open.write(raw)
    file_open.close()
    return 0

4
您可以使用编解码器。
import codecs
with open("test.txt",'r') as filehandle:
    content = filehandle.read()
if content[:3] == codecs.BOM_UTF8:
    content = content[3:]
print content.decode("utf-8")

无法使用的片段(文件句柄?还有codecs.BOM_UTF8会返回语法错误) - Max

1
在Python3中,您应该添加encoding='utf-8-sig'
with open(file_name, mode='a', encoding='utf-8-sig') as csvfile:
    csvfile.writelines(rows)

那就这样吧。

这个答案与2015年的这个答案完全相同。 - bfontaine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接