import piexif import os, json, re, time from tools import purge_file_name, get_album_list_data class PhotoExifRecover(object): def __init__(self, file_dir, floatview_info, raw_info): self.file_dir = file_dir self.floatview_info = floatview_info self.raw_info = raw_info self.exif_dict = piexif.load(self.file_dir) self.is_dirty = False def copy_exif(self, exif_dict_key, exif_value_key, info, info_dict_key, info_value_key, tag=""): if exif_dict_key not in self.exif_dict.keys() \ or exif_value_key not in self.exif_dict[exif_dict_key].keys() \ or exif_value_key not in self.exif_dict[exif_dict_key]: if info_dict_key in info.keys() \ and info_value_key in info[info_dict_key].keys() \ and info[info_dict_key][info_value_key]: if exif_dict_key not in self.exif_dict.keys(): self.exif_dict[exif_dict_key] = {} corvered = self.covert(info[info_dict_key][info_value_key], tag) self.exif_dict[exif_dict_key][exif_value_key] = corvered self.is_dirty = True print(exif_value_key, " is recoveried from ", info_value_key, corvered) return True def coyp_DateTimeOriginal_from_uploadtime(self): # if originalTime is missing if "Exif" not in self.exif_dict.keys() \ or piexif.ExifIFD.DateTimeOriginal not in self.exif_dict["Exif"].keys() \ or piexif.ExifIFD.DateTimeOriginal not in self.exif_dict["Exif"]: if "uploadtime" in self.raw_info.keys() and self.raw_info["uploadtime"]: if "Exif" not in self.exif_dict.keys(): self.exif_dict["Exif"] = {} self.exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal] = self.raw_info["uploadtime"].replace("-", ":") self.is_dirty = True print(piexif.ExifIFD.DateTimeOriginal, " is recoveried from ", "uploadtime") return True def add_exif(self, exif_dict_key, exif_value_key, value): if exif_dict_key not in self.exif_dict.keys() \ or exif_value_key not in self.exif_dict[exif_dict_key].keys() \ or exif_value_key not in self.exif_dict[exif_dict_key]: if value: if exif_dict_key not in self.exif_dict.keys(): self.exif_dict[exif_dict_key] = {} self.exif_dict[exif_dict_key][exif_value_key] = value self.is_dirty = True print(exif_value_key, " is added as ", value) return True def covert(self, s: str, tag): if s == "": return s elif tag == piexif.TYPES.Ascii: return s elif tag == piexif.TYPES.Rational or tag == piexif.TYPES.SRational: if '/' in s: (numerator, denominator) = s.split('/') return int(numerator), int(denominator) else: return int(float(s) * 10000), 10000 elif tag == piexif.TYPES.Short or tag == piexif.TYPES.Long: return int(s) elif tag == "GPSPos": decimal_degrees = float(s) degrees = int(decimal_degrees) minutes = int(60 * (decimal_degrees - degrees)) seconds = int(10000 * (3600 * (decimal_degrees - degrees) - 60 * minutes)) return (degrees, 1), (minutes, 1), (seconds, 10000) return s def recover(self): print("recovering:", self.file_dir) # 0th # self.copy_exif("0th", piexif.ImageIFD.Make, self.raw_info, "exif", "make", piexif.TYPES.Ascii) self.copy_exif("0th", piexif.ImageIFD.Model, self.raw_info, "exif", "model", piexif.TYPES.Ascii) # Exif # self.copy_exif("Exif", piexif.ExifIFD.ExposureBiasValue, self.raw_info, "exif", "exposureCompensation", piexif.TYPES.SRational) self.copy_exif("Exif", piexif.ExifIFD.ExposureMode, self.raw_info, "exif", "exposureMode", piexif.TYPES.Short) self.copy_exif("Exif", piexif.ExifIFD.ExposureProgram, self.raw_info, "exif", "exposureProgram", piexif.TYPES.Short) self.copy_exif("Exif", piexif.ExifIFD.ExposureTime, self.raw_info, "exif", "exposureTime", piexif.TYPES.SRational) self.copy_exif("Exif", piexif.ExifIFD.Flash, self.raw_info, "exif", "flash", piexif.TYPES.Short) self.copy_exif("Exif", piexif.ExifIFD.FNumber, self.raw_info, "exif", "fnumber", piexif.TYPES.Rational) self.copy_exif("Exif", piexif.ExifIFD.FocalLength, self.raw_info, "exif", "focalLength", piexif.TYPES.Rational) self.copy_exif("Exif", piexif.ExifIFD.ISOSpeed, self.raw_info, "exif", "iso", piexif.TYPES.Long) self.copy_exif("Exif", piexif.ExifIFD.LensModel, self.raw_info, "exif", "lensModel", piexif.TYPES.Ascii) self.copy_exif("Exif", piexif.ExifIFD.MeteringMode, self.raw_info, "exif", "meteringMode", piexif.TYPES.Short) # Exif: OriginalTime # if not self.copy_exif("Exif", piexif.ExifIFD.DateTimeOriginal, self.raw_info, "exif", "originalTime", piexif.TYPES.Ascii): self.coyp_DateTimeOriginal_from_uploadtime() # if originalTime is missing # GPS # if self.copy_exif("GPS", piexif.GPSIFD.GPSLongitude, self.floatview_info, "shootGeo", "pos_x", "GPSPos"): # 拍摄地点在东半球是参考东经的;在西半球如参考东经则经度是负数 self.add_exif("GPS", piexif.GPSIFD.GPSLongitudeRef, "E") if self.copy_exif("GPS", piexif.GPSIFD.GPSLatitude, self.floatview_info, "shootGeo", "pos_y", "GPSPos"): # 拍摄地点在北半球是参考北纬的;在南半球如参考北纬则维度是负数 self.add_exif("GPS", piexif.GPSIFD.GPSLatitudeRef, "N") if self.is_dirty: exif_bytes = piexif.dump(self.exif_dict) piexif.insert(exif_bytes, self.file_dir) class PhotoExifRecoverBatch(object): def __init__(self, target_uin): self.target_uin = target_uin self.e = [] def batch(self, should_rename=True, should_add_exif=True): # re constants p_date = re.compile(r"(\d{4})(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|3[0-1])[ _]([0-1]\d|2[0-3])([0-5]\d)([0-5]\d)") p_floatview_json = re.compile(r"^floatview_photo_\d{5}-\d{5}.json$") p_raw_json = re.compile(r"^photo_\d{5}-\d{5}.json$") target_dir = os.path.join(os.getcwd(), self.target_uin, "photo") if not os.path.exists(target_dir): print("路径不存在,请确认照片已下载,并在本文件尾部添加目标QQ号") # form album list album_info_dir = os.path.join(target_dir, "album_info.json") with open(album_info_dir, "r", encoding="utf-8") as album_info_f: album_info = json.load(album_info_f) album_list = get_album_list_data(album_info['data']) # No album at all! if len(album_list) <= 0: print("【json记录中无相册!】") return # do for every album for album in album_list: album_dir = "" files_in_target_dir = os.listdir(target_dir) album_id_purged = purge_file_name(album["id"]) # find album folder for file_name_in_target_dir in files_in_target_dir: if album_id_purged in file_name_in_target_dir: album_dir = os.path.join(target_dir, file_name_in_target_dir) # rename album folder if should_rename: if not re.search(p_date, file_name_in_target_dir): album_create_timestamp = int(album["createtime"]) # 取相册创建时间 album_create_date = time.strftime('%Y%m%d %H%M%S', time.localtime(album_create_timestamp)) file_name_in_target_dir_new = album_create_date + " " + file_name_in_target_dir album_dir_new = os.path.join(target_dir, file_name_in_target_dir_new) os.rename(album_dir, album_dir_new) album_dir = album_dir_new break if album_dir == "": print("相册文件夹缺失:", os.path.join(target_dir, album["name"])) continue # find floatview and raw json (500+ json文件会分裂。。) files_in_album_dir = os.listdir(album_dir) floatview_json_dir_list = [] raw_json_dir_list = [] for file_name_in_album_dir in files_in_album_dir: if re.search(p_floatview_json, file_name_in_album_dir): floatview_json_dir_list.append(os.path.join(album_dir, file_name_in_album_dir)) elif re.search(p_raw_json, file_name_in_album_dir): raw_json_dir_list.append(os.path.join(album_dir, file_name_in_album_dir)) # floatview or raw json is missing! if len(floatview_json_dir_list) == 0 or len(raw_json_dir_list) == 0: print("【相册中照片json数据缺失】:", album_dir) continue floatview_list = [] raw_list = [] for floatview_json_dir in floatview_json_dir_list: with open(floatview_json_dir, "r", encoding="utf-8") as floatview_json_f: floatview_json = json.load(floatview_json_f) for _floatview_info in floatview_json["data"]["photos"]: floatview_list.append(_floatview_info) for raw_json_dir in raw_json_dir_list: with open(raw_json_dir, "r", encoding="utf-8") as raw_json_f: raw_json = json.load(raw_json_f) for _raw_info in raw_json["data"]["photoList"]: raw_list.append(_raw_info) # find downloaded folder and file list within downloaded_dir = os.path.join(album_dir, "downloaded") # downloaded folder is missing! if not os.path.exists(downloaded_dir): print("【无downloaded文件夹】:", downloaded_dir) continue photos_in_album_downloaded_dir = os.listdir(downloaded_dir) # start to handle every photo within an album # floatview_info for floatview_info in floatview_list: lloc = floatview_info["lloc"] # find raw_info raw_info = None for _raw_info in raw_list: if _raw_info["lloc"] == lloc: raw_info = _raw_info break # find photo_dir photo_dir = "" lloc_purged = purge_file_name(lloc) for photo_name in photos_in_album_downloaded_dir: if lloc_purged in photo_name: photo_dir = os.path.join(downloaded_dir, photo_name) break if photo_dir == "": print("照片缺失:", os.path.join(downloaded_dir, lloc_purged)) continue # recover EXIF if should_add_exif: try: photoExifRecover = PhotoExifRecover(photo_dir, floatview_info, raw_info) photoExifRecover.recover() except Exception as e: error_message = "EXIF写入失败:" + photo_dir + "\n↘失败原因:" + str(e) print(error_message) self.e.append(error_message) continue # 对于EXIF写入发生异常的文件跳过重命名步骤 # rename photo if should_rename: [dir_name, photo_name] = os.path.split(photo_dir) if not re.search(p_date, photo_name): exif_in_file = piexif.load(photo_dir) if "Exif" in exif_in_file.keys() \ and piexif.ExifIFD.DateTimeOriginal in exif_in_file["Exif"].keys() \ and exif_in_file["Exif"][piexif.ExifIFD.DateTimeOriginal]: photo_create_date = \ bytes.decode(exif_in_file["Exif"][piexif.ExifIFD.DateTimeOriginal]).replace(":", "") photo_name_new = photo_create_date + " " + photo_name photo_dir_new = os.path.join(dir_name, photo_name_new) os.rename(photo_dir, photo_dir_new) photoExifRecover.file_dir = photo_dir_new photo_dir = photo_dir_new def show_error_list(self): e_count = len(self.e) if e_count <= 0: print("***EXIF写入过程未发生异常***") else: print("***EXIF写入失败数量:", e_count, "***") for e in self.e: print(e) if __name__ == "__main__": # 输入 target_uin = "" # 此处填入目标QQ号或在运行时输入 if target_uin == "": target_uin = input("请输入要批处理的QQ号:") should_rename = True # 是否需要将相册和照片文件名前加入时间标识,格式为 "YYYYMMDD HHMMSS 原文件名",便于排序整理 photoExifRecoverBatch = PhotoExifRecoverBatch(target_uin) # 文件完整性检查 print("开始文件完整性检查") photoExifRecoverBatch.batch(False, False) print("***如果照片文件缺失,可单独手工下载,或调高timeout,重新运行exporter.py下载;" "\n***如果相册文件夹缺失,请确认是否有空相册。\n") input("按回车开始批处理...") # 正式批处理 print("开始批处理") photoExifRecoverBatch.batch(should_rename) print("批处理完成\n") # 异常打印 input("按回车显示EXIF写入异常清单...") photoExifRecoverBatch.show_error_list() # by Yang-z # Ref # piexif Documen (piexif库官方文档): https://piexif.readthedocs.io/en/latest/ # official Exif standards(官方Exif标准): http://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf # 感谢greysign将时间写回照片文件的启发: https://github.com/greysign/QzoneExporter.git # 讲个笑话,我老婆用QQ空间备份照片:(