前提承接上一篇《如何批量导出Google相册所有数据》根据上一篇的方法导出的归档数据，往往许多信息都被抹除了，也就是Meta信息丢失，其中包括但不限于照片的定位信息（经纬度）、拍摄时间、拍照设备、光圈等一大堆信息。如果你默认下载了所有相册集，那么可能会有大量重复照片等着你，最可气的是如果你没有调整IOS设备的拍照格式的话，默认拍出的都是HEIC/HEVC格式的内容，而Google恰恰又把IOS设备默认的HEIC格式照片直接处理成了一个jpg加一个2到3秒左右的MOV短视频，如果你使用HEIC拍摄了大量照片，那可能只能一个个手动在相册选择删除。所以一般来讲，通过归档批量导出的数据，可能会遇到以下几种情况：Meta信息丢失重复时间混乱多出大量的短视频所以我一直在思考要如何处理这些问题。首先是Meta信息丢失，直接导致了我把照片直接导入相册后时间线混乱，可能我昨天拍的照片会出现在2007年那一栏中，其次往往许多照片旁边伴随着一个2秒短视频，相册一眼望过去全是重复内容，让人苦恼不堪。用Google search了一圈，发现网上有人提出问题，但是没人解决，痛定思痛，我决定写个小脚本批量处理，然后再导入手机。（最底下有完整代码，也已经放在Github上）最终实现了视频时长短于2s的，放在了under2文件夹下，短于3s的，全部放在了under3文件夹下重复文件默认被删除，包括.json和视频文件，如果代码中dealDuplicate(False)，则会归类到Duplicate文件夹下根据所有.json文件修复了照片的Exif数据和日期HEIC格式相片统一放在了同名文件夹下json文件统一放在json文件夹里​ 脚本是python写的，没怎么用过这个语言，本着实用主义原则，代码可能并不优雅重复照片我仔细观察了一下，发现大量重复照片和视频的下载名称都相同，那就直接扫描文件夹，把重复文件剔除即可def dealDuplicate(delete=True): fileList = {} dg = os.walk(scanDir) for path,dir_list,file_list in dg: for file_name in file_list: full_file_name = os.path.join(path, file_name) if file_name == '元数据.json': continue #处理重复文件 if file_name in fileList.keys(): DupDir = scanDir + '/Duplicate/' if not os.path.exists(DupDir): os.makedirs(DupDir) if delete: os.remove(full_file_name) #这里可以直接删除 else: if not os.path.exists(DupDir + file_name): shutil.move(full_file_name, DupDir) print('重复文件：' + full_file_name + ' ------ ' + fileList[file_name]) else: fileList[file_name] = full_file_name fileList.clear()重复短视频另一个就是大量HEIC转换出来的大量短视频，都是.MOV格式文件，这里我选择通过ffmpeg判断视频时长，进而把时长在3s以内的视频过滤出来，最终全部删除有选择地分门别类。这里需要安装一下ffmpeg的扩展，pip3 install ffmpeg-python即可还有一点是需要提前安装好ffmepg可执行文件并配置好环境变量，否则有可能会报找不到ffprobe错误#文件分类 def dealClassify(): #部分文件变了，重新扫描 g = os.walk(scanDir) for path, dir_list, file_list in g: for file_name in file_list: full_file_name = os.path.join(path, file_name) #处理时长低于3s的视频 if os.path.splitext(file_name)[-1] == '.MOV': print('根据时长分类文件：' + full_file_name) info = ffmpeg.probe(full_file_name) #print(info) duration = info['format']['duration'] #时长 if float(duration) <= 2: under2Dir = scanDir + '/under2/' if not os.path.exists(under2Dir): print('创建文件夹：' + under2Dir) os.makedirs(under2Dir) if not os.path.exists(under2Dir + file_name): shutil.move(full_file_name, under2Dir) elif 2 < float(duration) <= 3: under3Dir = scanDir + '/under3/' if not os.path.exists(under3Dir): print('创建文件夹：' + under3Dir) os.makedirs(under3Dir) if not os.path.exists(under3Dir + file_name): shutil.move(full_file_name, under3Dir) #处理HEIC文件 elif os.path.splitext(file_name)[-1] == '.HEIC': heicDir = scanDir + '/HEIC/' if not os.path.exists(heicDir): os.makedirs(heicDir) if not os.path.exists(heicDir + file_name): shutil.move(full_file_name, heicDir) #单独存储json文件 elif os.path.splitext(file_name)[-1] == '.json': jsonDir = scanDir + '/json/' if not os.path.exists(jsonDir): os.makedirs(jsonDir) if not os.path.exists(jsonDir + file_name): shutil.move(full_file_name, jsonDir) #print('处理json文件：' + full_file_name) #print('json文件：' + os.path.splitext(file_name)[-2] + '.json')这里顺便筛选剔除了HEIC格式的照片，并把所有json文件单独放到一个文件夹备用修复Exif数据谷歌把每一张照片原本的 Exif 数据（e.g. 地点、日期）抹掉，然后提取出来放到了对应的 JSON 里，另外目前版本看到的格式只有xx.扩展和xx.文件扩展.json这种命名方式的Meta文件，其他格式命名的没有做处理。我这里的格式大概如下：{ "title": "IMG_4093.jpg", "description": "", "imageViews": "0", "creationTime": { "timestamp": "1525150106", "formatted": "2018年5月1日UTC 上午4:48:26" }, "modificationTime": { "timestamp": "1607202343", "formatted": "2020年12月5日UTC 下午9:05:43" }, "photoTakenTime": { "timestamp": "1485071348", "formatted": "2017年1月22日UTC 上午7:49:08" }, "geoData": { "latitude": 34.18444722222222, "longitude": 116.92279722222223, "altitude": 48.648960739030024, "latitudeSpan": 0.0, "longitudeSpan": 0.0 }, "geoDataExif": { "latitude": 34.18444722222222, "longitude": 116.92279722222223, "altitude": 48.648960739030024, "latitudeSpan": 0.0, "longitudeSpan": 0.0 }, "googlePhotosOrigin": { "mobileUpload": { "deviceType": "IOS_PHONE" } } }导出的文件中有部分是不存在.json元数据的，有些照片我的 Exif 丢了，有些照片则没丢，不清楚是否和勾选原图上传有关，所以脚本里要提前判断。我这里用了piexif和PIL库，安装方式pip3 install piexif，pip3 install Pillow。#计算lat/lng信息 def format_latlng(latlng): degree = int(latlng) res_degree = latlng - degree minute = int(res_degree * 60) res_minute = res_degree * 60 - minute seconds = round(res_minute * 60.0,3) return ((degree, 1), (minute, 1), (int(seconds * 1000), 1000)) #读json def readJson(json_file): with open(json_file, 'r') as load_f: return json.load(load_f) #处理照片exif信息 def dealExif(): g = os.walk(scanDir) for path,dir_list,file_list in g: for file_name in file_list: full_file_name = os.path.join(path, file_name) ext_name = os.path.splitext(file_name)[-1] if ext_name.lower() in ['.jpg','.jpeg','.png']: # if file_name != 'ee7db1e41afc9fd342e42e0a5034006b.JPG': # 单文件测试 # continue if not os.path.exists(scanDir + '/json/' + file_name + '.json'): continue exifJson = readJson(scanDir + '/json/' + file_name + '.json') print('处理Exif：' + full_file_name) try: img = Image.open(full_file_name) # 读图 exif_dict = piexif.load(img.info['exif']) except UnidentifiedImageError: print("图片读取失败：" + full_file_name) continue except KeyError: print("图片没有exif数据，尝试创建：" + full_file_name) exif_dict = {'0th':{},'Exif': {},'GPS': {}} # 修改exif数据 exif_dict['0th'][piexif.ImageIFD.DateTime] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime( int(exifJson['photoTakenTime']['timestamp']))).encode('utf-8') exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime( int(exifJson['creationTime']['timestamp']))).encode('utf-8') exif_dict['Exif'][piexif.ExifIFD.DateTimeDigitized] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime( int(exifJson['modificationTime']['timestamp']))).encode('utf-8') exif_dict['GPS'][piexif.GPSIFD.GPSLatitude] = format_latlng(exifJson['geoDataExif']['latitude']) exif_dict['GPS'][piexif.GPSIFD.GPSLongitude] = format_latlng(exifJson['geoDataExif']['longitude']) # exif_dict['GPS'][piexif.GPSIFD.GPSLongitudeRef] = 'W' # exif_dict['GPS'][piexif.GPSIFD.GPSLatitudeRef] = 'N' exif_bytes = piexif.dump(exif_dict) img.save(full_file_name, None, exif=exif_bytes) #修改文件时间（可选） # photoTakenTime = time.strftime("%Y%m%d%H%M.%S", time.localtime(int(exifJson['photoTakenTime']['timestamp']))) # os.system('touch -t "{}" "{}"'.format(photoTakenTime, full_file_name)) # os.system('touch -mt "{}" "{}"'.format(photoTakenTime, full_file_name)) 修改下对应路径依次运行即可if __name__ == '__main__': scanDir = r'/Users/XXX/Downloads/Takeout' #TODO 这里修改归档的解压目录 dealDuplicate() dealClassify() dealExif() print('终于搞完了，Google Photos 辣鸡')总结7K多个照片视频运行了大概2分钟跑完了，最终运行一遍下来之后，多余的照片和视频已经处理掉了，那些HEIC已经被分成JPG+MOV的，程序把MOV视频剔除，所有照片已有的Exif已经修复了，代码中有一段修改文件时间的被我注释了，有条件的可以参考各自系统修改下文件时间就更好了。把一路运行下来的坑都踩了一遍，如果有什么问题我再补充好了。完整代码！注意是Python3环境也可以直接在Github上查看#!/usr/bin/python3 # -*- coding:utf-8 -*- # @Author : gty0211@foxmail.com import json import os import shutil import time import ffmpeg import piexif from PIL import Image, UnidentifiedImageError #归档zip解压目录 scanDir = '' #处理重复 def dealDuplicate(delete=True): fileList = {} dg = os.walk(scanDir) for path,dir_list,file_list in dg: for file_name in file_list: full_file_name = os.path.join(path, file_name) if file_name == '元数据.json': continue #处理重复文件 if file_name in fileList.keys(): DupDir = scanDir + '/Duplicate/' if not os.path.exists(DupDir): os.makedirs(DupDir) if delete: os.remove(full_file_name) #这里可以直接删除 else: if not os.path.exists(DupDir + file_name): shutil.move(full_file_name, DupDir) print('重复文件：' + full_file_name + ' ------ ' + fileList[file_name]) else: fileList[file_name] = full_file_name fileList.clear() #文件分类 def dealClassify(): #部分文件变了，重新扫描 g = os.walk(scanDir) for path, dir_list, file_list in g: for file_name in file_list: full_file_name = os.path.join(path, file_name) #处理时长低于3s的视频 if os.path.splitext(file_name)[-1] == '.MOV': print('根据时长分类文件：' + full_file_name) info = ffmpeg.probe(full_file_name) #print(info) duration = info['format']['duration'] #时长 if float(duration) <= 2: under2Dir = scanDir + '/under2/' if not os.path.exists(under2Dir): print('创建文件夹：' + under2Dir) os.makedirs(under2Dir) if not os.path.exists(under2Dir + file_name): shutil.move(full_file_name, under2Dir) elif 2 < float(duration) <= 3: under3Dir = scanDir + '/under3/' if not os.path.exists(under3Dir): print('创建文件夹：' + under3Dir) os.makedirs(under3Dir) if not os.path.exists(under3Dir + file_name): shutil.move(full_file_name, under3Dir) #处理HEIC文件 elif os.path.splitext(file_name)[-1] == '.HEIC': heicDir = scanDir + '/HEIC/' if not os.path.exists(heicDir): os.makedirs(heicDir) if not os.path.exists(heicDir + file_name): shutil.move(full_file_name, heicDir) #单独存储json文件 elif os.path.splitext(file_name)[-1] == '.json': jsonDir = scanDir + '/json/' if not os.path.exists(jsonDir): os.makedirs(jsonDir) if not os.path.exists(jsonDir + file_name): shutil.move(full_file_name, jsonDir) #计算lat/lng信息 def format_latlng(latlng): degree = int(latlng) res_degree = latlng - degree minute = int(res_degree * 60) res_minute = res_degree * 60 - minute seconds = round(res_minute * 60.0,3) return ((degree, 1), (minute, 1), (int(seconds * 1000), 1000)) #读json def readJson(json_file): with open(json_file, 'r') as load_f: return json.load(load_f) #处理照片exif信息 def dealExif(): g = os.walk(scanDir) for path,dir_list,file_list in g: for file_name in file_list: full_file_name = os.path.join(path, file_name) ext_name = os.path.splitext(file_name)[-1] if ext_name.lower() in ['.jpg','.jpeg','.png']: # if file_name != 'ee7db1e41afc9fd342e42e0a5034006b.JPG': # 单文件测试 # continue if not os.path.exists(scanDir + '/json/' + file_name + '.json'): continue exifJson = readJson(scanDir + '/json/' + file_name + '.json') print('处理Exif：' + full_file_name) try: img = Image.open(full_file_name) # 读图 exif_dict = piexif.load(img.info['exif']) except UnidentifiedImageError: print("图片读取失败：" + full_file_name) continue except KeyError: print("图片没有exif数据，尝试创建：" + full_file_name) exif_dict = {'0th':{},'Exif': {},'GPS': {}} # 修改exif数据 exif_dict['0th'][piexif.ImageIFD.DateTime] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime( int(exifJson['photoTakenTime']['timestamp']))).encode('utf-8') exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime( int(exifJson['creationTime']['timestamp']))).encode('utf-8') exif_dict['Exif'][piexif.ExifIFD.DateTimeDigitized] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime( int(exifJson['modificationTime']['timestamp']))).encode('utf-8') exif_dict['GPS'][piexif.GPSIFD.GPSLatitude] = format_latlng(exifJson['geoDataExif']['latitude']) exif_dict['GPS'][piexif.GPSIFD.GPSLongitude] = format_latlng(exifJson['geoDataExif']['longitude']) # exif_dict['GPS'][piexif.GPSIFD.GPSLongitudeRef] = 'W' # exif_dict['GPS'][piexif.GPSIFD.GPSLatitudeRef] = 'N' exif_bytes = piexif.dump(exif_dict) img.save(full_file_name, None, exif=exif_bytes) #修改文件时间（可选） # photoTakenTime = time.strftime("%Y%m%d%H%M.%S", time.localtime(int(exifJson['photoTakenTime']['timestamp']))) # os.system('touch -t "{}" "{}"'.format(photoTakenTime, full_file_name)) # os.system('touch -mt "{}" "{}"'.format(photoTakenTime, full_file_name)) if __name__ == '__main__': scanDir = r'/Users/XXX/Downloads/Takeout' #TODO 这里修改归档的解压目录 dealDuplicate() dealClassify() dealExif() print('终于搞完了，Google Photos 辣鸡')
修复Google相册元数据请看这里手机刷机 突然照片就没了 于是想把之前备份在 Google相册 的照片全部导出来从Google相册下载但是打开之后发现照片只能一张张勾选，不能批量下载整个相册。就很痛苦，总不能真的让我一个个勾选吧。后来发现 Google有个存档的功能 ，可以下载所有Google相关的信息数据，其中就包含Google相册的照片信息。然后就只选需要的就行了只选择相册存档接下来需要选归档方式，因为需要打包成压缩包下载，所以一般是选择压缩包格式和每个压缩包大小，以及保存方式等待完成然后等一会 处理完成之后，就会根据你的选择的保存方式，下载或者转存都可以了Google存档管理地址https://takeout.google.com/settings/takeout不过，我的照片又从手机里找出来了。