# -*- coding: utf-8 -*- """ 文本/代码文件读取工具 (file_reader) 编码探测 + 二进制探测 + 大小守卫,供附件系统(拖拽/粘贴/上传文本与代码文件)复用。 用法:read_text_file(path) -> (content, encoding, size_kb, lines),失败抛 ValueError。 说明:纯函数模块,不依赖 Qt,便于单元测试。 """ import os # 明确拒绝的二进制/文档格式(Word/Excel/PPT/PDF/压缩包/可执行/媒体等) BINARY_EXTS = frozenset({ # Office 文档(明确排除 Word/Excel) '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.rtf', # 电子书(.pdf 已交由 tools/builtin_tools/pdf_reader.py 专门解析,不在此拦截) '.epub', '.mobi', # 压缩包 / 磁盘镜像 '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.iso', '.dmg', # 可执行 / 编译产物 '.exe', '.dll', '.so', '.dylib', '.msi', '.bin', '.apk', '.jar', '.class', '.pyc', '.pyd', '.o', '.a', # 音视频 '.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg', '.mp4', '.avi', '.mov', '.mkv', '.webm', '.wmv', # 字体 '.ttf', '.otf', '.woff', '.woff2', '.eot', # 数据库 '.db', '.sqlite', '.sqlite3', '.mdb', # 设计稿 '.psd', '.ai', '.sketch', '.fig', }) # 单文件大小上限(字节):超过则拒绝,避免内存与上下文爆炸 MAX_ATTACH_FILE_BYTES = 2 * 1024 * 1024 # 2 MB # 编码降级链:UTF-8(含BOM) → GB18030(⊇GBK/GB2312) → latin-1(永不失败) _ENCODINGS = ('utf-8-sig', 'gb18030', 'latin-1') def read_text_file(path, max_bytes=MAX_ATTACH_FILE_BYTES): """读取一个文本/代码文件。 返回 (content, encoding, size_kb, lines)。 文件过大或判定为二进制时抛出 ValueError(附带可读原因)。 """ size = os.path.getsize(path) if size > max_bytes: raise ValueError( f"文件过大({size / 1024 / 1024:.1f} MB > {max_bytes / 1024 / 1024:.0f} MB)" ) with open(path, 'rb') as f: raw = f.read() # 二进制探测:前 8KB 含 NUL 字节即判定为二进制(git 同款启发式) if b'\x00' in raw[:8192]: raise ValueError("二进制文件,无法作为文本读取") text, used_enc = None, 'utf-8' for enc in _ENCODINGS: try: text = raw.decode(enc) used_enc = enc break except UnicodeDecodeError: continue if text is None: # 理论上 latin-1 兜底永不失败 raise ValueError("无法识别文件编码") lines = text.count('\n') + 1 return text, used_enc, round(size / 1024, 2), lines