Exif 信息的读取与删除(Pillow、Piexif、ExifTool)

我想要删除约百 G 图片中的一些信息,具体是:标题、主题、标记、备注、图像 ID。

虽然可以选中若干图片,属性 - 删除属性和个人信息,然后进行删除。

但是这种方式删除不了图像 ID,并且图片也不在同一个文件夹之中,因此还是要考虑写一个 Python 脚本来处理。


一番搜索后,考虑尝试的库有 PillowPiexif

首先需要对 imghdr 打一个补丁,在文件遍历中显然是需要判断图片类型的,但是 imghdr 对 jpg 的识别有些问题。

imghdr / python - Can’t detec type of some images (image extension)


def test_jpeg1(h, f):
    """ JPEG data in JFIF format """
    if b'JFIF' in h[:23]:
        return 'jpeg'


JPEG_MARK = b'\xff\xd8\xff\xdb\x00C\x00\x08\x06\x06' \
            b'\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f'


def test_jpeg2(h, f):
    """ JPEG with small header """
    if len(h) >= 32 and 67 == h[5] and h[:32] == JPEG_MARK:
        return 'jpeg'


def test_jpeg3(h, f):
    """ JPEG data in JFIF or Exif format """
    if h[6:10] in (b'JFIF', b'Exif') or h[:2] == b'\xff\xd8':
        return 'jpeg'


imghdr.tests.append(test_jpeg1)
imghdr.tests.append(test_jpeg2)
imghdr.tests.append(test_jpeg3)

此后使用的 imghdr 均为打过补丁的 imghdr。



这里补充一下我想要删除的 exif 信息对应的 tag 信息:

https://exiftool.org/TagNames/EXIF.html

tags 0x9c9b-0x9c9f are used by Windows Explorer;
special characters in these values are converted to UTF-8 by default, or Windows Latin1 with the -L option.

名称 Tag ID Tag Name Values / Notes
标题 0x9c9b (40091) XPTitle ignored by Windows Explorer if ImageDescription exists
备注 0x9c9c (40092) XPComment
作者 0x9c9d (40093) XPAuthor ignored by Windows Explorer if Artist exists
标记 0x9c9e (40094) XPKeywords
主题 0x9c9f (40095) XPSubject
作者 0x013b (315) Artist
图像ID 0xa420 (42016) ImageUniqueID



Pillow 读取 exif 信息简单示例。

def exif_pillow(dir_path):
    """
    Pillow
    https://github.com/python-pillow/Pillow
    https://pillow.readthedocs.io/en/stable/?badge=latest
    """
    for root, dirs, files in os.walk(dir_path):
        for file in files:
            img_path = os.path.join(root, file)
            img_type = imghdr.what(img_path)
            if img_type is None:
                print(f'path = {img_path} is not graphic')
            else:
                print(f'img_path = {img_path}, img = {img_type}')
                with Image.open(img_path) as img:
                    exif = img.getexif()
                    for key, val in exif.items():
                        if key in ExifTags.TAGS:
                            tag = ExifTags.TAGS[key]
                            if isinstance(val, bytes):
                                print(chardet.detect(val))
                            print(f'{key} - {tag}: {val}')
                        else:
                            print(f'{key} not in ExifTags.TAGS')
            print('-------------------------------------------')

tag id 和 tag name 的对应关系是在字典 ExifTags.TAGS 中存储的,需要注意的是,有些 tag id 不存在于字典中。

另外在对应信息是 bytes 且内容存在中文时,编码上有些问题,不论是按照 UTF-8、GBK 或是 chardet 检测的编码来 encode,中文内容都无法正确获取。

Exif 实例可以当作字典一样去设值,然后通过 save 方法保存,但是在实际操作中会出现错误,或者保存后图片体积变化明显存在异常的情况。



Piexif 读取 exif 信息简单示例。

def exif_piexif(dir_path):
    """
    Piexif
    https://github.com/hMatoba/Piexif
    https://piexif.readthedocs.io/en/latest/
    https://piexif.readthedocs.io/en/latest/sample.html#with-pil-pillow
    """
    for root, dirs, files in os.walk(dir_path):
        for file in files:
            img_path = os.path.join(root, file)
            img_type = imghdr.what(img_path)
            if img_type is None:
                print(f'path = {img_path} is not graphic')
            else:
                print(f'img_path = {img_path}, img = {img_type}')
                with Image.open(img_path) as img:
                    exif = piexif.load(img.info["exif"])
                    for tag, val in exif.items():
                        print(f'{tag}:')
                        if isinstance(val, dict):
                            for k, v in val.items():
                                if k in ExifTags.TAGS:
                                    tag = ExifTags.TAGS[k]
                                    print(f'{k} - {tag}: {v}')
                                else:
                                    print(f'{k} not in ExifTags.TAGS')
                        else:
                            print(f'val: {val}')
            print('-------------------------------------------')

piexif 自己的文档中就给出了结合 pillow 修改并保存 exif 信息的示例

可以发现和 pillow 有些区别,pillow 是通过 PIL.Image.Image.getexif 获取 exif 实例。

而 piexif 的 load 返回的是字典,dump 返回的是 exif 的 bytes。

在实际保存中,还是会出现错误。

不过值得一提的是,piexif 配合 pillow 读取 exif 信息时,中文明显为 UTF-8 的编码形式。

piexif 的 remove 可以非常简单的移除 exif 信息,但是不包括标题(XPTitle)和标记(XPKeywords)这些 IPTC 信息。



综上,因为不满足我的需求或者遇到暂时无法解决的错误问题。我开始考虑使用 ExifTool。



ExifTool

项目地址:https://github.com/exiftool/exiftool

项目主页:https://exiftool.org/

安装说明:https://exiftool.org/install.html

EXIF Tags:https://exiftool.org/TagNames/EXIF.html

程序文档:https://exiftool.org/exiftool_pod.html


这里仅简单介绍几个参数选项使用,全部选项请参照文档。

如果执行中遇到经典的 无法将“exiftool”项识别为 cmdlet、函数、脚本文件或可运行程序的名称 提示,那就认真看一遍安装说明。


  • -r[.] (-recurse),递归处理子目录
  • -g[NUM…] (-groupHeadings),按照标签组输出
  • -P (-preserve),保留文件修改日期/时间
  • -overwrite_original,直接覆盖,默认生成 _original 备份文件
  • -TAG[+-^]=[VALUE],向标签写入新值


查看文件信息:

exiftool [file_path]

递归分组查看文件信息:

exiftool -g -r [dir_path]

递归清空指定信息,保留文件修改日期/时间,不生成备份文件:

exiftool -XPTitle= -XPComment= -XPKeywords= -XPAuthor= -XPSubject= -ImageUniqueID= -LastKeywordXMP= -LastKeywordIPTC= -ImageDescription= -Subject= -Title= -Description= -Keywords= -Artist= -overwrite_original -P -r [dir_path]


实际测试后可以发现 ExifTool 可以满足我的需求,操作简便,功能强大。

另外,ExifTool 也提供了 ExifToolGUI,如果使用,注意查看 Requirements and preparations 部分的说明。