# 用Python开发本地图片EXIF信息查看器:轻松揭秘照片背后的故事


背景介绍

摄影爱好者拍摄的照片中,隐藏着丰富的元数据——EXIF信息。它记录了拍摄时间、相机型号、光圈/快门等参数,甚至包含GPS位置。这些信息能帮助我们还原拍摄场景、分析参数设置,但手动查看十分繁琐。本文将带大家用Python打造一个本地图片EXIF信息查看器,通过Tkinter GUI和exifread库,实现图片EXIF数据的可视化解析。

思路分析

要实现这个工具,我们需要解决四个核心问题:

  1. GUI交互:用Tkinter构建界面,支持“选择图片”按钮、文件选择对话框和信息展示区域。
  2. EXIF读取:借助exifread库解析图片的元数据标签(如拍摄时间、GPS坐标)。
  3. 数据转换:将原始EXIF数据(如度分秒格式的GPS坐标)转换为人类可读的格式(如十进制经纬度、带单位的时间)。
  4. 错误处理:应对图片无EXIF数据、格式不兼容或解析失败的情况。

代码实现

下面是完整的代码实现,包含详细注释。首先确保安装依赖:

pip install exifread pillow

完整代码

import tkinter as tk
from tkinter import filedialog
import exifread
from PIL import Image
import os
from datetime import datetime

def parse_gps(tags):
    """解析GPS信息:纬度、经度、海拔(度分秒转十进制)"""
    latitude = None
    longitude = None
    altitude = None

    # 处理纬度(GPSLatitude + GPSLatitudeRef)
    if "GPS GPSLatitude" in tags and "GPS GPSLatitudeRef" in tags:
        lat = tags["GPS GPSLatitude"].values  # 度、分、秒(Ratio对象)
        lat_ref = str(tags["GPS GPSLatitudeRef"]).strip()  # N/S
        # 度分秒转十进制:度 + 分/60 + 秒/3600
        deg = lat[0].num / lat[0].den
        min = lat[1].num / lat[1].den
        sec = lat[2].num / lat[2].den
        lat_dec = deg + min/60 + sec/3600
        latitude = f"{lat_dec:.4f}° {lat_ref}"

    # 处理经度(GPSLongitude + GPSLongitudeRef)
    if "GPS GPSLongitude" in tags and "GPS GPSLongitudeRef" in tags:
        lon = tags["GPS GPSLongitude"].values  # 度、分、秒(Ratio对象)
        lon_ref = str(tags["GPS GPSLongitudeRef"]).strip()  # E/W
        deg = lon[0].num / lon[0].den
        min = lon[1].num / lon[1].den
        sec = lon[2].num / lon[2].den
        lon_dec = deg + min/60 + sec/3600
        longitude = f"{lon_dec:.4f}° {lon_ref}"

    # 处理海拔(GPSAltitude)
    if "GPS GPSAltitude" in tags:
        alt = tags["GPS GPSAltitude"].values[0]  # Ratio对象
        alt_m = alt.num / alt.den  # 转换为米
        altitude = f"{alt_m:.1f}m"

    return latitude, longitude, altitude

def parse_exif(file_path, text_area):
    """解析图片EXIF信息并显示"""
    try:
        # 清空之前的内容
        text_area.delete(1.0, tk.END)

        # 1. 文件基本信息
        file_name = os.path.basename(file_path)
        file_size = os.path.getsize(file_path) / (1024 * 1024)  # 转换为MB
        # 获取分辨率(借助PIL)
        img = Image.open(file_path)
        width, height = img.size
        text_area.insert(tk.END, "【文件基本信息】\n")
        text_area.insert(tk.END, f"文件名:{file_name}\n")
        text_area.insert(tk.END, f"文件大小:{file_size:.2f}MB\n")
        text_area.insert(tk.END, f"分辨率:{width}×{height}\n\n")

        # 2. 读取EXIF数据(二进制模式打开)
        with open(file_path, 'rb') as f:
            tags = exifread.process_file(f, details=False)  # details=False提高速度

        # 3. 拍摄设备与参数
        text_area.insert(tk.END, "【拍摄设备与参数】\n")
        # 拍摄时间(EXIF DateTimeOriginal)
        if "EXIF DateTimeOriginal" in tags:
            time_str = str(tags["EXIF DateTimeOriginal"]).strip()
            # 转换为标准时间格式:2023:08:20 09:45:32 → 2023-08-20 09:45:32
            time_obj = datetime.strptime(time_str, "%Y:%m:%d %H:%M:%S")
            text_area.insert(tk.END, f"拍摄时间:{time_obj.strftime('%Y-%m-%d %H:%M:%S')}\n")
        else:
            text_area.insert(tk.END, "拍摄时间:未找到\n")

        # 相机型号(Image Model)
        if "Image Model" in tags:
            text_area.insert(tk.END, f"相机型号:{str(tags['Image Model']).strip()}\n")
        else:
            text_area.insert(tk.END, "相机型号:未找到\n")

        # 镜头型号(EXIF LensModel)
        if "EXIF LensModel" in tags:
            text_area.insert(tk.END, f"镜头型号:{str(tags['EXIF LensModel']).strip()}\n")
        else:
            text_area.insert(tk.END, "镜头型号:未找到\n")

        # 光圈值(EXIF FNumber)
        if "EXIF FNumber" in tags:
            f_num = tags["EXIF FNumber"].values[0]  # 分数对象(如 56/10 → f/5.6)
            f = f_num.num / f_num.den
            text_area.insert(tk.END, f"光圈值:f/{f:.1f}\n")
        else:
            text_area.insert(tk.END, "光圈值:未找到\n")

        # 快门速度(EXIF ExposureTime)
        if "EXIF ExposureTime" in tags:
            exp_time = tags["EXIF ExposureTime"].values[0]
            if exp_time.num == 1:  # 如 1/250 → 250s?不,1/250的num是1,den是250 → 1/250s
                text_area.insert(tk.END, f"快门速度:1/{exp_time.den}s\n")
            else:  # 如 5/1000 → 1/200s(den/num=200)
                s = exp_time.den / exp_time.num
                text_area.insert(tk.END, f"快门速度:1/{s}s\n")
        else:
            text_area.insert(tk.END, "快门速度:未找到\n")

        # ISO感光度(EXIF ISOSpeedRatings)
        if "EXIF ISOSpeedRatings" in tags:
            text_area.insert(tk.END, f"ISO感光度:{tags['EXIF ISOSpeedRatings'].values[0]}\n")
        else:
            text_area.insert(tk.END, "ISO感光度:未找到\n")

        # 焦距(EXIF FocalLength)
        if "EXIF FocalLength" in tags:
            focal = tags["EXIF FocalLength"].values[0]
            f = focal.num / focal.den
            text_area.insert(tk.END, f"焦距:{f:.1f}mm\n")
        else:
            text_area.insert(tk.END, "焦距:未找到\n")

        text_area.insert(tk.END, "\n")

        # 4. GPS位置信息
        text_area.insert(tk.END, "【GPS位置信息(若存在)】\n")
        latitude, longitude, altitude = parse_gps(tags)
        if latitude:
            text_area.insert(tk.END, f"纬度:{latitude}\n")
        else:
            text_area.insert(tk.END, "纬度:未找到\n")
        if longitude:
            text_area.insert(tk.END, f"经度:{longitude}\n")
        else:
            text_area.insert(tk.END, "经度:未找到\n")
        if altitude:
            text_area.insert(tk.END, f"海拔高度:{altitude}\n")
        else:
            text_area.insert(tk.END, "海拔高度:未找到\n")

    except Exception as e:
        text_area.insert(tk.END, f"解析出错:{str(e)}\n")

def select_image():
    """选择图片文件并解析EXIF"""
    file_path = filedialog.askopenfilename(
        title="选择图片文件",
        filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp")]
    )
    if file_path:
        parse_exif(file_path, text_area)

# 主窗口设置
root = tk.Tk()
root.title("图片EXIF信息查看器")
root.geometry("800x600")

# 文本显示区域
text_area = tk.Text(root, wrap=tk.WORD, font=("SimHei", 10))
text_area.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

# 选择图片按钮
btn_select = tk.Button(root, text="选择图片", command=select_image)
btn_select.pack(pady=10)

if __name__ == "__main__":
    root.mainloop()

代码解析

  1. GUI搭建:使用Tkinter的Text组件展示信息,Button触发文件选择,filedialog实现本地文件浏览。
  2. EXIF读取:通过exifread.process_file()以二进制模式读取图片的EXIF标签,details=False跳过冗余信息,提高解析速度。
  3. 数据转换
    • 时间格式化:将EXIF的DateTimeOriginal(如2023:08:20 09:45:32)转换为YYYY-MM-DD HH:MM:SS格式。
    • GPS坐标转换:将度分秒格式(如30°21'34.2" N)转换为十进制(如30.3595° N),公式为 度 + 分/60 + 秒/3600
    • 参数格式化:光圈、快门、焦距等参数从分数形式(如56/10)转换为人类可读的格式(如f/5.61/250s)。
  4. 错误处理:用try-except捕获解析过程中的异常(如文件损坏、无EXIF数据),保证程序健壮性。

总结

通过这个项目,我们实践了GUI设计文件操作第三方库调用数据格式化的综合能力:
– 从Tkinter的界面搭建,到exifread的元数据解析,再到GPS坐标的转换,每一步都锻炼了Python开发的核心技能。
– 工具实用性强:摄影爱好者可快速分析参数,普通用户可还原拍摄地点,是学习GUI和元数据处理的绝佳案例。

如果你想扩展功能,可尝试:
– 支持批量解析图片的EXIF信息。
– 可视化GPS轨迹(结合地图API)。
– 导出EXIF信息为JSON/CSV格式。

快来运行代码,看看你的照片隐藏了哪些故事吧! 📷

(注:代码兼容JPG、PNG等格式的图片,但PNG的EXIF支持度低于JPG,建议用JPG图片测试。)


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注