【免费工具】一键将Excel通讯录批量导入手机!支持VCF格式,超简单!

【免费工具】一键将Excel通讯录批量导入手机!支持VCF格式,超简单!

笔者开发了一款【Excel转VCF通讯录工具】,支持一键将Excel表格批量转换为VCF格式,轻松导入到手机、邮箱、企业微信等通讯录!
链接:https://pan.quark.cn/s/e9563a4149bd
主要亮点:

  • 支持批量导入,省时省力
  • 支持姓名、单位、手机、座机、邮箱、备注等字段
  • 支持自定义模板下载,表格填写更规范
  • 支持合并为一个VCF或每人一个VCF,适配各种导入场景
  • 现代化美观界面,操作零门槛
  • 纯本地运行,数据安全无忧

适用场景:

  • 企业/学校/社群批量导入通讯录
  • 家庭成员、同学录、客户名单管理
  • 需要将Excel联系人导入手机/邮箱/微信/钉钉等

使用方法:

  1. 下载并运行本工具
  2. 按提示下载空白模板,填写联系人信息
  3. 一键导出VCF,导入到手机或通讯录应用即可

更多详细关注公众号:

import ttkbootstrap as ttk
from ttkbootstrap.constants import *
import pandas as pd
import os
from tkinter import filedialog, messagebox, StringVar
import vobject
import subprocess
import sys

class ExcelToVCFApp:
    def __init__(self):
        self.window = ttk.Window(themename="minty")
        self.window.title("Excel转VCF通讯录工具")
        self.window.geometry("1100x700")
        self.window.resizable(False, False)
        
        # 创建主框架(用grid布局)
        self.main_frame = ttk.Frame(self.window, padding="24")
        self.main_frame.pack(fill=BOTH, expand=YES)
        self.main_frame.rowconfigure(0, weight=1)
        self.main_frame.rowconfigure(1, weight=0)
        self.main_frame.columnconfigure(0, weight=1)
        self.main_frame.columnconfigure(1, weight=0)
        
        # 标题标签
        title_label = ttk.Label(
            self.main_frame,
            text="Excel转VCF通讯录工具",
            font=("微软雅黑", 22, "bold"),
            foreground="#2c3e50"
        )
        title_label.grid(row=0, column=0, sticky="w", pady=(10, 5))
        
        # 使用说明
        instruction = (
            "使用说明:\n"
            "1. Excel表格第一行必须包含表头。\n"
            "2. 必须有一列名为'姓名'。\n"
            "3. 电话列可为'电话'或'手机号码',邮箱、单位名称、备注可选。\n"
            "4. 建议表头无多余空格,内容不能为空。\n"
            "5. 支持批量转换,可选择合并或分开导出VCF文件。"
        )
        self.instruction_label = ttk.Label(
            self.main_frame,
            text=instruction,
            wraplength=1000,
            font=("微软雅黑", 11),
            foreground="#34495e",
            justify="left"
        )
        self.instruction_label.grid(row=1, column=0, sticky="w", pady=(0, 12))
        
        # 设置自定义按钮字体和颜色样式
        style = ttk.Style()
        style.configure("MyBlue.TButton", font=("微软雅黑", 13, "bold"), foreground="#fff", background="#3498db")
        style.map("MyBlue.TButton",
                  background=[('active', '#2980b9'), ('!active', '#3498db')])
        style.configure("MyGreen.TButton", font=("微软雅黑", 13, "bold"), foreground="#fff", background="#27ae60")
        style.map("MyGreen.TButton",
                  background=[('active', '#219150'), ('!active', '#27ae60')])
        style.configure("MyPurple.TButton", font=("微软雅黑", 13, "bold"), foreground="#fff", background="#8e44ad")
        style.map("MyPurple.TButton", background=[('active', '#6c3483'), ('!active', '#8e44ad')])
        
        # 下载空白模板按钮(紫色)
        self.template_btn = ttk.Button(
            self.main_frame,
            text="📝 下载空白模板",
            command=self.download_template,
            style="MyPurple.TButton",
            width=22,
            padding=(10, 8)
        )
        self.template_btn.grid(row=2, column=0, sticky="w", pady=(8, 0))
        # 鼠标悬停提示
        def show_tooltip(event):
            self.status_label.config(text="下载标准Excel模板,填写后可直接导入。")
        def hide_tooltip(event):
            self.status_label.config(text="")
        self.template_btn.bind("<Enter>", show_tooltip)
        self.template_btn.bind("<Leave>", hide_tooltip)
        # 选择Excel文件按钮(自定义蓝色)
        self.select_btn = ttk.Button(
            self.main_frame,
            text="📂 选择Excel文件",
            command=self.select_excel_file,
            style="MyBlue.TButton",
            width=22,
            padding=(10, 8)
        )
        self.select_btn.grid(row=3, column=0, sticky="w", pady=(8, 16))
        
        # 显示选择的文件路径
        self.file_label = ttk.Label(
            self.main_frame,
            text="未选择文件",
            wraplength=1000,
            font=("微软雅黑", 10),
            foreground="#888"
        )
        self.file_label.grid(row=4, column=0, sticky="w", pady=(0, 10))
        
        # 预览表格
        self.tree = None
        self.tree_frame = ttk.Frame(self.main_frame)
        self.tree_frame.grid(row=5, column=0, sticky="nsew", pady=(10, 0))
        self.main_frame.rowconfigure(5, weight=1)
        
        # 底部操作区
        self.bottom_frame = ttk.Frame(self.main_frame)
        self.bottom_frame.grid(row=6, column=0, sticky="ew", pady=(10, 0))
        self.main_frame.rowconfigure(6, weight=0)
        
        # 导出方式选择
        self.export_mode = StringVar(value="single")
        mode_frame = ttk.Frame(self.bottom_frame)
        mode_frame.pack(pady=10, anchor="w")
        ttk.Label(mode_frame, text="导出方式:", font=("微软雅黑", 11)).pack(side='left')
        ttk.Radiobutton(mode_frame, text="合并为一个VCF", variable=self.export_mode, value="single", bootstyle="success-round-toggle").pack(side='left', padx=10)
        ttk.Radiobutton(mode_frame, text="每人一个VCF", variable=self.export_mode, value="multi", bootstyle="warning-round-toggle").pack(side='left', padx=10)
        
        # 导出按钮(自定义绿色)
        self.export_btn = ttk.Button(
            self.bottom_frame,
            text="💾 开始导出",
            style="MyGreen.TButton",
            command=self.convert_to_vcf,
            state=DISABLED,
            width=22,
            padding=(10, 8)
        )
        self.export_btn.pack(pady=18, anchor="w")
        
        # 状态标签
        self.status_label = ttk.Label(
            self.bottom_frame,
            text="",
            wraplength=1000,
            font=("微软雅黑", 10),
            foreground="#16a085"
        )
        self.status_label.pack(pady=10, anchor="w")
        
        self.excel_file_path = None
        self.df = None
        
    def select_excel_file(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("Excel files", "*.xlsx *.xls")]
        )
        if file_path:
            self.excel_file_path = file_path
            self.file_label.config(text=f"已选择: {file_path}")
            try:
                df = pd.read_excel(self.excel_file_path)
                # 自动去除所有Unnamed列
                df = df.loc[:, ~df.columns.str.contains('^Unnamed')]
                df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
                if '姓名' not in df.columns:
                    messagebox.showerror("错误", "Excel表格必须包含'姓名'列,请检查表头!")
                    self.export_btn.config(state=DISABLED)
                    self.df = None
                    self.clear_tree()
                    return
                self.df = df
                self.refresh_tree(df)
                self.export_btn.config(state=NORMAL)
            except Exception as e:
                messagebox.showerror("错误", f"读取Excel文件失败:\n{str(e)}")
                self.status_label.config(text="读取Excel失败!")
                self.export_btn.config(state=DISABLED)
                self.df = None
                self.clear_tree()

    def clear_tree(self):
        if self.tree:
            self.tree.destroy()
            self.tree = None

    def refresh_tree(self, df):
        self.clear_tree()
        columns = list(df.columns)
        self.tree = ttk.Treeview(self.tree_frame, columns=columns, show='headings', height=20, bootstyle="info")
        style = ttk.Style()
        style.configure("Treeview.Heading", font=("微软雅黑", 11, "bold"), foreground="#2c3e50")
        style.configure("Treeview", font=("微软雅黑", 10), rowheight=28)
        for col in columns:
            self.tree.heading(col, text=col)
            self.tree.column(col, width=180, anchor='center')
        for _, row in df.iterrows():
            self.tree.insert('', 'end', values=[row.get(col, '') for col in columns])
        self.tree.pack(fill='both', expand=YES)

    def convert_to_vcf(self):
        if self.df is None:
            messagebox.showerror("错误", "请先选择并预览Excel文件")
            return
        try:
            df = self.df
            save_dir = filedialog.askdirectory(title="选择保存VCF文件的目录")
            if not save_dir:
                return
            total_contacts = len(df)
            converted = 0
            skipped = 0
            vcf_all = ""
            for index, row in df.iterrows():
                name = str(row['姓名']).strip() if not pd.isna(row['姓名']) else ''
                if not name or name.lower() == 'nan':
                    skipped += 1
                    continue
                vcard = vobject.vCard()
                vcard.add('fn')
                vcard.fn.value = name
                vcard.add('n')
                vcard.n.value = vobject.vcard.Name(family=name)
                # 单位名称
                if '单位名称' in df.columns and not pd.isna(row['单位名称']):
                    org = str(row['单位名称'])
                    if org and org.lower() != 'nan':
                        vcard.add('org')
                        vcard.org.value = [org]
                # 备注
                if '备注' in df.columns and not pd.isna(row['备注']):
                    note = str(row['备注'])
                    if note and note.lower() != 'nan':
                        vcard.add('note')
                        vcard.note.value = note
                # 添加电话(座机)
                if '电话' in df.columns and not pd.isna(row['电话']):
                    phone = str(row['电话']).strip()
                    if phone and phone.lower() != 'nan':
                        tel_home = vcard.add('tel')
                        tel_home.value = phone
                        tel_home.type_param = 'HOME'
                # 添加手机号码
                if '手机号码' in df.columns and not pd.isna(row['手机号码']):
                    mobile = str(row['手机号码']).strip()
                    if mobile and mobile.lower() != 'nan':
                        tel_cell = vcard.add('tel')
                        tel_cell.value = mobile
                        tel_cell.type_param = 'CELL'
                # 邮箱
                if '邮箱' in df.columns and not pd.isna(row['邮箱']):
                    email = str(row['邮箱']).strip()
                    if email and email.lower() != 'nan':
                        vcard.add('email')
                        vcard.email.value = email
                        vcard.email.type_param = 'INTERNET'
                if self.export_mode.get() == "single":
                    vcf_all += vcard.serialize()
                else:
                    filename = f"{name}_{index + 1}.vcf"
                    filepath = os.path.join(save_dir, filename)
                    with open(filepath, 'w', encoding='utf-8') as f:
                        f.write(vcard.serialize())
                converted += 1
                self.status_label.config(
                    text=f"正在转换: {converted}/{total_contacts}"
                )
                self.window.update()
            # 合并导出
            if self.export_mode.get() == "single" and converted > 0:
                filepath = os.path.join(save_dir, "contacts.vcf")
                with open(filepath, 'w', encoding='utf-8') as f:
                    f.write(vcf_all)
            msg = f"转换完成!\n共转换 {converted} 个联系人"
            if skipped > 0:
                msg += f"\n跳过无效数据 {skipped} 条(无姓名)"
            if self.export_mode.get() == "single" and converted > 0:
                msg += f"\n保存位置: {filepath}"
            else:
                msg += f"\n保存位置: {save_dir}"
            # 弹窗带"打开文件夹"按钮
            if messagebox.askyesno("成功", msg + "\n\n是否打开导出文件夹?"):
                self.open_folder(save_dir)
            self.status_label.config(text="转换完成!")
        except Exception as e:
            messagebox.showerror("错误", f"转换过程中出现错误:\n{str(e)}")
            self.status_label.config(text="转换失败!")

    def open_folder(self, path):
        if sys.platform.startswith('win'):
            os.startfile(path)
        elif sys.platform.startswith('darwin'):
            subprocess.Popen(['open', path])
        else:
            subprocess.Popen(['xdg-open', path])
    
    def download_template(self):
        # 定义模板内容
        columns = ['姓名', '单位名称', '手机号码', '电话', '邮箱', '备注']
        df = pd.DataFrame(columns=columns)
        save_path = filedialog.asksaveasfilename(
            defaultextension='.xlsx',
            filetypes=[('Excel文件', '*.xlsx')],
            title='保存空白模板',
            initialfile='通讯录模板.xlsx'
        )
        if save_path:
            try:
                df.to_excel(save_path, index=False)
                messagebox.showinfo("成功", f"空白模板已保存到:\n{save_path}")
            except Exception as e:
                messagebox.showerror("错误", f"保存模板失败:\n{str(e)}")

    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    app = ExcelToVCFApp()
    app.run() 
```
posted @ 2025-05-10 15:03  ghostdot  阅读(899)  评论(0)    收藏  举报