因为之前个人主力机一直在锤子和 iPhone 之前反复横跳,导致除了 iCloud 外,不得不使用跨平台兼容性良好的 Google Photos 作为两台手机之间的相册同步工具。后来锤子宣布不再出新手机,于是我又用回了 iPhone。

众所周知,在 iPhone 上无法使用第三方 app 作为图片选择器,只能使用系统自带的。因此,仅在 Google Photos 中存在的图片无法在 iPhone 上方便的被使用,而只能在 Google Photos 内部浏览,比较不便。于是我萌生了将 Google Photos 中的图片去重导入 iCloud 相册的想法。

由于同时开启了 iCloud 相册和 Google Photos 备份功能,理论上我的 Google Photos 相册应该是 iCloud 相册的超集(即 iCloud 相册中有的照片一定在 Google Photos 相册中,而 Google Photos 相册中可能有 iCloud 相册没有的照片)。因此大概的思路如下:

  1. 获取到所有的 iCloud 相册照片,给每张图片计算摘要值(一串字符,可以理解为图片的特征),得到一个摘要值的集合
  2. 获取到所有的 Google Photos 相册照片,给每张图片计算摘要值,检查是否在第一步计算得到的集合中。如果存在,则不进行任何处理;如果不存在,则将照片复制到一个临时文件夹中
  3. 将临时文件夹中的照片批量导入到 iCloud 相册

那么就开始吧!


下载 iCloud 相册到本地

首先我们需要下载所有 iCloud 相册照片到本地,以便用于后续的摘要值计算。我们可以在 Mac 上打开“照片”App,进入偏好设置,选择“将原片下载到此 Mac”。

“照片”App 设置界面

然后在“照片”App 中的底部会提示“正在下载”。慢慢等它下载完成。与此同时,我们可以进行第二步。


下载 Google Photos 相册到本地

来到 Google Photos 的设置界面,在“导出您的数据”这一栏点击展开,然后点击“备份”。

Google Photos 设置界面

在新打开的“Google 导出”页面中,点击“下一步”。

新建导出作业

选择“导出一次”,底下的文件格式选择 tgz,Mac 天然可以支持此格式的解压缩。大小看自己网络情况和相册总容量。如果带宽特别高、并且希望下载的时候可以并发下载,可以适当减小 size。不过因为每一个分片都需要手动点击下载,因此 size 太小的话会点击非常多次。然后点击“创建导出作业”。特别提一下:Google 很良心,实测下来似乎没有限制下载的带宽。

创建导出作业

提交之后就是漫长的等待了,由于需要将所有相册文件打包,因此耗时会比较长。我的 50G 文件大概花了几十分钟,如果文件数量特别多的话,几个小时甚至几天都是有可能的。完成之后会往你的邮箱发送一封包含下载链接的邮件,点击就可以下载打包好的文件。

多提一句,我们能如此方便的下载自己在一个互联网服务中产生的所有数据(专业上称为“互操作性”),多亏了各国颁布的信息安全相关法案(如欧洲的 GDPR)。如果没有法律的约束,各个互联网服务肯定希望把用户捆绑在自己的生态里,会大量使用专有格式,并限制用户导出数据。没有法律的约束,资本是不会让科技向善的。

在本地对比两个相册

我们将从 Google Photos 下载下来的归档文件进行解压,并将解压后的文件放置在同一个目录中,称为目录 A。

然后我们来到“相册”App,在偏好设置中找到图库的位置(它应当以 .photoslibrary 结尾),点击“在访达中显示”。此目录我们称为目录 B。

多说一句,像“照片图库.photoslibrary”这样的“文件名”,看似是单个文件,而如果你在命令行中查看,则会发现它其实是一个文件夹(称之为“包”),只不过在 macOS 的界面上得到了特殊处理。在访达中点击右键“显示包内容”就可以像打开普通文件夹一样打开它了。

现在我们需要对比目录 A 比目录 B 多出来的文件列表,并且由于存在很多子文件夹,需要展开进行对比。这就到了 Python 出场的时候了(以下代码需要在 Python 3.8+ 版本中运行,可自行 Google 安装方法,或在评论中提问):

import os
import hashlib
import shutil

photo_library_dir = "/Users/username/Pictures/照片图库.photoslibrary" # 目录 B
google_takeout_dir = "/Users/username/Downloads/untar" # 目录 A


def get_md5(file_path: str):
    with open(file_path, "rb") as f:
        h = hashlib.md5()
        while block := f.read(64 * (1 << 20)):
            h.update(block)
    return h.hexdigest()


def copy_with_renaming(filename: str, fullpath: str, target_dir: str):
    final_filename = filename
    renamed = False
    if os.path.exists(os.path.join(target_dir, final_filename)):
        sp = os.path.splitext(filename)
        i = 1
        while True:
            final_filename = f"{sp[0]} ({i}){sp[1]}"
            if not os.path.exists(os.path.join(target_dir, final_filename)):
                renamed = True
                break
            i += 1
    if renamed:
        print(f"name {filename} already used, use {final_filename} instead")
    shutil.copy(fullpath, os.path.join(target_dir, final_filename))


originals_dir = photo_library_dir + "/originals"
files_hash_set = set()
for root, _, files in os.walk(originals_dir):
    for file in files:
        files_hash_set.add(get_md5(os.path.join(root, file)))
print(f"total file count in iCloud photo library: {len(files_hash_set)}")

diff_directory_name = "diff"
os.makedirs(diff_directory_name)
for root, _, files in os.walk(google_takeout_dir):
    for file in files:
        if file.startswith("."):
            continue  # ignore hidden files
        full_path = os.path.join(root, file)
        if get_md5(file_path=full_path) not in files_hash_set:
            print(f"{os.path.relpath(full_path)} not in iCloud photo lib")
            copy_with_renaming(file, full_path, diff_directory_name)

通过简单的 50 行代码,我们将 Google Photos 中存在、但 iCloud 相册中不存在的照片找了出来,并复制到了“diff”这个文件夹中,并且还处理了重复文件名的情况。我们在访达中打开这个文件夹,然后将其拖拽到“照片”App 中,就可以将这部分内容导入到 iCloud 相册了。接下来等待完成同步即可。

你也许会喜欢
阅读更多

Mac 根据应用程序决定Fn键(功能键)的功能

使用键盘顶部的 Fn 键用作 macOS 功能键可以非常方便的调整亮度、控制音乐播放、以及显示“启动台”(Launchpad),但有的时候(必须写代码)我们需要临时将 Fn 键给应用程序用作标准功能键,不然的话需要多按一个 Fn 键,很不顺手,有没有办法自动切换呢?
阅读更多

我是如何管理网络书签和个人知识库的

对于网络书签和个人知识库的需求,相信很多人还是有的。只是很多人因为整理“历史遗留问题”的成本偏高、又或者是因为没有发现合适的工具,而越来越懒得去整理书签和知识库,导致要找资料的时候总是找不到。作为一个希望事情变得有条理的个人知识管理初烧者,我今天准备写一篇文章,向大家介绍一下如何妥善的建立自己的电子化知识搜寻体系。