前提

承接上一篇《如何批量导出Google相册所有数据》
根据上一篇的方法导出的归档数据,往往许多信息都被抹除了,也就是Meta信息丢失,其中包括但不限于照片的定位信息(经纬度)拍摄时间拍照设备光圈等一大堆信息。如果你默认下载了所有相册集,那么可能会有大量重复照片等着你,最可气的是如果你没有调整IOS设备的拍照格式的话,默认拍出的都是HEIC/HEVC格式的内容,而Google恰恰又把IOS设备默认的HEIC格式照片直接处理成了一个jpg加一个2到3秒左右的MOV短视频,如果你使用HEIC拍摄了大量照片,那可能只能一个个手动在相册选择删除。

所以一般来讲,通过归档批量导出的数据,可能会遇到以下几种情况:

  1. Meta信息丢失
  2. 重复
  3. 时间混乱
  4. 多出大量的短视频

所以我一直在思考要如何处理这些问题。
首先是Meta信息丢失,直接导致了我把照片直接导入相册后时间线混乱,可能我昨天拍的照片会出现在2007年那一栏中,其次往往许多照片旁边伴随着一个2秒短视频,相册一眼望过去全是重复内容,让人苦恼不堪。

用Google search了一圈,发现网上有人提出问题,但是没人解决,痛定思痛,我决定写个小脚本批量处理,然后再导入手机。(最底下有完整代码,也已经放在Github上)

最终实现了

  1. 视频时长短于2s的,放在了under2文件夹下,短于3s的,全部放在了under3文件夹下
  2. 重复文件默认被删除,包括.json和视频文件,如果代码中dealDuplicate(False),则会归类到Duplicate文件夹下
  3. 根据所有.json文件修复了照片的Exif数据和日期
  4. 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_namefileList.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 丢了,有些照片则没丢,不清楚是否和勾选原图上传有关,所以脚本里要提前判断。

我这里用了piexifPIL库,安装方式pip3 install piexifpip3 install Pillow

#计算lat/lng信息
def format_latlng(latlng):degree = int(latlng)res_degree = latlng - degreeminute = int(res_degree * 60)res_minute = res_degree * 60 - minuteseconds = 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':   #   单文件测试#     continueif not os.path.exists(scanDir + '/json/' + file_name + '.json'):continueexifJson = 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)continueexcept 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_namefileList.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 - degreeminute = int(res_degree * 60)res_minute = res_degree * 60 - minuteseconds = 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':   #   单文件测试#     continueif not os.path.exists(scanDir + '/json/' + file_name + '.json'):continueexifJson = 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)continueexcept 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相册元数据修复相关推荐

  1. 黑客盯上了Google相册漏洞

    2019独角兽企业重金招聘Python工程师标准>>> 研究人员在Google相册应用上发现了一个已修复的漏洞.有了这个漏洞,黑客可以使用Google相册来跟踪他们的位置历史记录. ...

  2. 多亏了Google相册,如何一键释放Android手机上的空间

    Let's be real here: modern smartphones have limited storage. While they're coming with a lot more th ...

  3. 技术实操丨HBase 2.X版本的元数据修复及一种数据迁移方式

    摘要:分享一个HBase集群恢复的方法. 背景 在HBase 1.x中,经常会遇到元数据不一致的情况,这个时候使用HBCK的命令,可以快速修复元数据,让集群恢复正常. 另外HBase数据迁移时,大家经 ...

  4. 笔记本电脑与台式机同步连接_如何将台式机与Google云端硬盘(和Google相册)同步...

    笔记本电脑与台式机同步连接 Google has been doing its part to make sure everyone has a backup of important data, a ...

  5. 如何防止某些照片显示在Android的图库或Google相册中

    Look, we get it: you don't want every picture showing up in your gallery app on your Android phone. ...

  6. 谷歌云端硬盘 转存_如何合并多个Google云端硬盘和Google相册帐户

    谷歌云端硬盘 转存 It isn't possible to merge Google accounts directly, making it tricky to move your data fr ...

  7. vue查看本地相册_使用Vue.js构建的Google相册相册查看器

    vue查看本地相册 google-photos-vue (google-photos-vue) Google Photos album viewer built with Vue.js. 使用Vue. ...

  8. google相册数据导出

    网上搜了下,有不少人问怎么下载,别问我为什么知道这个东西,请叫我雷锋!我也不知道O(∩_∩)O~,有需求就有code... 言归正传,很久以前可以用google相册,毕竟用的很舒服,现在不行了,所以要 ...

  9. 如何在Android的相机应用程序中添加Google相册快捷方式

    Google Photos is arguably the best photo management app on the Play Store. It's intuitive and easy t ...

最新文章

  1. DotNet生成随机数的一些方法
  2. 《疯狂Java讲义》2
  3. Python中字符串切片详解
  4. 【Python】利用 Python 分析了一波月饼,我得出的结论是?
  5. Array Splitting
  6. MyEclipse web项目导入Eclipse,详细说明
  7. 2018.7.28 二叉树的遍历规则(前序遍历、后序遍历、中序遍历)
  8. python教案 md文件_python操作pdf文件.md
  9. 毕业答辩之毕业设计答辩问题有哪些?
  10. python如何下载os库_python下载os库的方法
  11. MyEclipse10破解过程
  12. 解密为何 Golang 能从众多语言中脱颖而出
  13. 【微信小程序学习笔记02理解与初始准备】【实战天气微信小程序】
  14. Chrome 翻译插件规避代码块
  15. vue table表格中身份证隐藏中间几位
  16. 《人工智能原理》读书笔记:第1章 绪论
  17. java 统一日志_基于log4j实现统一日志管理
  18. 音质好的蓝牙耳机有哪些?盘点四款好音质蓝牙耳机
  19. linux 增加交换空间,在linux上增加swap交换空间
  20. cufflinks 介绍使用

热门文章

  1. VS2013/MFC 自绘控件获取系统CPU和物理内存使用率
  2. javaweb JSP JAVA 仓库库存管理系统(仓库进销存管理系统jsp服装库存管理系统仓库管理系统)
  3. FileUnhider for mac(一键文件隐藏和显示工具)
  4. Amaze UI 入门引导
  5. Uncaught SyntaxError: Invalid shorthand property initialize
  6. 两个对象List根据属性取交集和差集
  7. linux命令行界面颜色配置,如何配置Linux命令行的字体和背景颜色
  8. hge养成类游戏《见习小恶魔威力加强版》源代码
  9. java实现将指定字符串替换为制定长度的空格
  10. 满满的干货分享!!!广电网络的那些事~