文件转UTF8

有时候项目的文件是其他编码格式,在不同的编辑器下显示乱码,现在统一为UTF8,下面是python代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import chardet

# 文本文件扩展名白名单
TEXT_EXTENSIONS = {
    '.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx',  # C/C++
    '.py', '.pyw',  # Python
    '.java',  # Java
    '.cs',  # C#
    '.go',  # Go
    '.rs',  # Rust
    '.js', '.jsx', '.ts', '.tsx',  # JavaScript/TypeScript
    '.html', '.htm', '.css', '.scss', '.sass', '.less',  # 前端
    '.xml', '.json', '.yaml', '.yml', '.toml',  # 配置文件
    '.txt', '.log', '.md', '.markdown',  # 文档
    '.sql',  # SQL
    '.sh', '.bash', '.zsh',  # Shell
    '.bat', '.cmd', '.ps1',  # Windows脚本
    '.ini', '.cfg', '.conf', '.config',  # 配置文件
    '.cmake', '.make', '.mk',  # Makefile
    '.sln', '.vcxproj', '.filters',  # VS项目文件
    '.lua', '.rb', '.php', '.pl', '.pm',  # 其他语言
}

def is_text_file(file_path):
    """
    判断是否为文本文件
    先检查扩展名,如果不在白名单中,通过检测内容是否包含空字节来判断
    """
    # 1. 检查扩展名
    ext = os.path.splitext(file_path)[1].lower()
    if ext in TEXT_EXTENSIONS:
        return True
    
    # 2. 扩展名不在白名单中,检查内容是否包含空字节(二进制特征)
    try:
        with open(file_path, 'rb') as f:
            chunk = f.read(1024)
        
        # 空文件视为文本文件
        if not chunk:
            return True
        
        # 如果包含空字节,很可能是二进制文件
        if b'\x00' in chunk:
            return False
        
        # 没有空字节,使用chardet检测
        detected = chardet.detect(chunk)
        if detected.get('encoding') is None:
            return False
        
        # 常见的文本编码
        text_encodings = {'utf-8', 'ascii', 'gbk', 'gb2312', 'gb18030', 
                         'big5', 'shift-jis', 'euc-kr', 'utf-16', 'cp936'}
        return detected.get('encoding', '').lower() in text_encodings
        
    except Exception:
        return False

def convert_to_utf8_bom(file_path, verbose=True):
    """
    将文件转换为带BOM的UTF-8编码(Visual Studio可正确识别)
    
    Args:
        file_path: 文件的绝对路径
        verbose: 是否输出详细信息
    
    Returns:
        bool: 转换是否成功
    """
    # 检查文件是否存在
    if not os.path.exists(file_path):
        if verbose:
            print(f"❌ 错误:文件不存在 - {file_path}")
        return False
    
    # 检查是否为文件(而非目录)
    if not os.path.isfile(file_path):
        if verbose:
            print(f"❌ 错误:路径指向的是目录,不是文件 - {file_path}")
        return False
    
    try:
        # 读取原始文件的二进制内容
        with open(file_path, 'rb') as f:
            raw_data = f.read()
        
        # 空文件直接跳过
        if not raw_data:
            return True
        
        # 检查是否已经是带BOM的UTF-8
        if raw_data.startswith(b'\xef\xbb\xbf'):
            if verbose:
                print(f"⏭️  跳过(已是UTF-8-BOM): {os.path.basename(file_path)}")
            return True
        
        # 使用chardet检测文件编码
        detected = chardet.detect(raw_data)
        original_encoding = detected.get('encoding', 'unknown')
        confidence = detected.get('confidence', 0)
        
        # 格式化置信度为百分比
        confidence_percent = confidence * 100
        
        # 显示检测信息
        if verbose:
            encoding_display = original_encoding.upper() if original_encoding else 'UNKNOWN'
            print(f"🔍 检测到编码: {encoding_display} (置信度: {confidence_percent:.2f}%)")
        
        # 尝试解码
        content = None
        decoded_encoding = None
        
        # 优先使用chardet检测到的编码
        if original_encoding and original_encoding.lower() != 'unknown':
            try:
                content = raw_data.decode(original_encoding)
                decoded_encoding = original_encoding
            except (UnicodeDecodeError, TypeError):
                if verbose:
                    print(f"⚠️  使用 {original_encoding} 解码失败,尝试其他编码...")
        
        # 如果chardet解码失败,尝试常见编码
        if content is None:
            for enc in ['gbk', 'gb2312', 'gb18030', 'utf-8', 'big5', 
                       'cp936', 'latin-1', 'windows-1252']:
                if enc == original_encoding:
                    continue
                try:
                    content = raw_data.decode(enc)
                    decoded_encoding = enc
                    if verbose:
                        print(f"✅ 使用 {enc.upper()} 编码解码成功")
                    break
                except UnicodeDecodeError:
                    continue
        
        # 如果所有编码都失败
        if content is None:
            if verbose:
                print(f"❌ 错误:无法解码文件内容 - {os.path.basename(file_path)}")
            return False
        
        # 写回文件:UTF-8 with BOM
        with open(file_path, 'wb') as f:
            # 先写入BOM头 (EF BB BF)
            f.write(b'\xef\xbb\xbf')
            # 再写入UTF-8编码的内容
            f.write(content.encode('utf-8'))
        
        if verbose:
            original_display = decoded_encoding.upper() if decoded_encoding else 'UNKNOWN'
            print(f"✅ 转换成功:{file_path}")
            print(f"   原始编码: {original_display} → 目标编码: UTF-8 with BOM")
            print()
        
        return True
        
    except PermissionError:
        if verbose:
            print(f"❌ 错误:没有权限访问文件 - {file_path}")
        return False
    except Exception as e:
        if verbose:
            print(f"❌ 转换失败: {str(e)}")
        return False

def process_path(path, verbose=True, dry_run=False):
    """
    处理文件或目录
    
    Args:
        path: 文件或目录的绝对路径
        verbose: 是否输出详细信息
        dry_run: 是否仅预览(不实际转换)
    
    Returns:
        tuple: (成功数, 失败数, 跳过数)
    """
    if not os.path.exists(path):
        print(f"❌ 路径不存在: {path}")
        return (0, 0, 0)
    
    # 如果是文件,直接处理
    if os.path.isfile(path):
        if not is_text_file(path):
            print(f"⏭️  跳过(非文本文件): {os.path.basename(path)}")
            return (0, 0, 1)
        
        if dry_run:
            print(f"🔍 [预览] 将转换: {path}")
            return (1, 0, 0)
        
        success = convert_to_utf8_bom(path, verbose)
        return (1 if success else 0, 0 if success else 1, 0)
    
    # 如果是目录,递归处理
    if os.path.isdir(path):
        success_count = 0
        fail_count = 0
        skip_count = 0
        
        print(f"📂 扫描目录: {path}")
        print("=" * 70)
        
        for root, dirs, files in os.walk(path):
            # 跳过常见的版本控制目录和编译输出目录
            skip_dirs = {
                '.git', '.svn', '.hg', 'node_modules', '__pycache__',
                'bin', 'obj', 'Debug', 'Release', '.vs', 'build',
                'dist', 'venv', 'env', '.venv', 'vendor',
                'x64', 'x86', 'Win32', 'lib', 'dll', 'out'
            }
            dirs[:] = [d for d in dirs if d not in skip_dirs]
            
            for file in files:
                file_path = os.path.join(root, file)
                
                # 跳过隐藏文件(Unix风格)
                if file.startswith('.'):
                    continue
                
                # 跳过常见的二进制文件扩展名
                binary_ext = {
                    '.exe', '.dll', '.so', '.dylib', '.lib', '.a',
                    '.obj', '.o', '.pdb', '.pch', '.ilk', '.exp',
                    '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg',
                    '.mp3', '.mp4', '.avi', '.mkv', '.flv', '.wmv',
                    '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
                    '.pdf', '.doc', '.xls', '.ppt', 
                    '.docx', '.xlsx', '.pptx', '.vsdx'
                }
                if os.path.splitext(file)[1].lower() in binary_ext:
                    skip_count += 1
                    if verbose:
                        print(f"⏭️  跳过(二进制): {file}")
                    continue
                
                # 判断是否为文本文件
                if not is_text_file(file_path):
                    skip_count += 1
                    if verbose:
                        print(f"⏭️  跳过(非文本): {file}")
                    continue
                
                if dry_run:
                    print(f"🔍 [预览] 将转换: {file_path}")
                    success_count += 1
                    continue
                
                if convert_to_utf8_bom(file_path, verbose):
                    success_count += 1
                else:
                    fail_count += 1
        
        print("=" * 70)
        print(f"📊 统计: 成功 {success_count} | 失败 {fail_count} | 跳过 {skip_count}")
        return (success_count, fail_count, skip_count)
    
    print(f"❌ 未知路径类型: {path}")
    return (0, 0, 0)

def main():
    """主函数:处理命令行参数"""
    # 检查参数数量
    if len(sys.argv) < 2:
        print("=" * 70)
        print("文件编码转换工具 - 转UTF-8-BOM (Visual Studio兼容)")
        print("=" * 70)
        print("\n使用方法:")
        print("  python convert_to_utf8_bom.py <文件或文件夹路径> [选项]")
        print("\n选项:")
        print("  -v, --verbose    显示详细输出(默认启用)")
        print("  -q, --quiet      静默模式,不显示详细信息")
        print("  -d, --dry-run    仅预览,不实际转换")
        print("  -h, --help       显示帮助信息")
        print("\n示例:")
        print("  python convert_to_utf8_bom.py D:\\Project\\main.h")
        print("  python convert_to_utf8_bom.py D:\\Project\\src")
        print("  python convert_to_utf8_bom.py D:\\Project\\src -q")
        print("  python convert_to_utf8_bom.py D:\\Project\\src -d")
        print("=" * 70)
        sys.exit(1)
    
    # 解析参数
    path = sys.argv[1]
    verbose = True  # 默认显示详细信息
    dry_run = False
    
    # 处理选项参数
    for arg in sys.argv[2:]:
        if arg in ['-q', '--quiet']:
            verbose = False
        elif arg in ['-d', '--dry-run']:
            dry_run = True
        elif arg in ['-v', '--verbose']:
            verbose = True
        elif arg in ['-h', '--help']:
            print("帮助信息...")
            sys.exit(0)
    
    # 执行处理
    process_path(path, verbose=verbose, dry_run=dry_run)

if __name__ == "__main__":
    main()

使用很简单 例如保存为a.py  那么就这样使用

python d:/test.txt
python d:/testdir

 

posted on 2026-08-26 12:14  弘道者  阅读(11)  评论(0)    收藏  举报