
簡介面向Linux平臺上的Qt5開發(fā)者這份資源提供針對ExcelFormat第三方庫修改后的完整工程核心解決2003格式xls文件在中文路徑下無法讀寫的問題。資源適用于統(tǒng)信UOS等國產(chǎn)化系統(tǒng)已在aarch64、mips64架構(gòu)下通過gcc編譯驗(yàn)證兼顧開發(fā)與運(yùn)維場景。壓縮包共21個文件以cpp、h源碼和編譯中間產(chǎn)物o為主同時包含pro工程文件、Makefile、UI界面以及demo示例整體僅2.61MB結(jié)構(gòu)精簡便于移植。作者定位到原庫在wcstombs與mbstowcs轉(zhuǎn)換時對非英文字符處理出錯通過在create、open兩個函數(shù)中增加NON_ENGLISH_PATH_FILENAME_SUPPORT宏分支并修改文件名長度計算從而完整支持中文、日文和韓文路徑。除修復(fù)源碼外資源還附帶可直接運(yùn)行的demo和表格樣式設(shè)置示例開發(fā)者可快速參考并集成到自己的Qt工程中節(jié)省重新排查編碼問題的時間。1. 用 Qt5 在 Linux 下讀寫 2003 版 xls這次卡住我的不是 Excel 格式是路徑前段時間接到一個 Linux 桌面小工具的需求要求用 QT5 支持中文路徑讀寫 2003 的 excel 文件也就是 .xls。一開始我以為難點(diǎn)在 2003 格式的解析上結(jié)果第三方庫一接進(jìn)來文件讀得出來中文 sheet 名全亂碼路徑帶個“臺賬”兩個字直接打不開。查了兩天才明白真正卡人的是編碼鏈路不是 Excel 格式本身。做這套方案的核心是拿一個不處理中文路徑的第三方 Excelformat 類庫源碼自己改一版。適合誰讀一類是要在 Linux 上處理歷史遺留 .xls 文件的嵌入式或桌面應(yīng)用開發(fā)者另一類是正在被“Windows 上好好的搬到 Linux 就崩”折磨的跨平臺維護(hù)者。下面我把這套修改思路、關(guān)鍵代碼、驗(yàn)證步驟和踩過的坑一次講清楚。2. 中文路徑讀寫 xls 為什么翻車BIFF8 編碼與第三方庫的假設(shè)2.1 三條編碼鏈Q(jìng)String、窄字符路徑、BIFF8 字符串一個 xls 文件的完整讀取鏈路其實(shí)是三段。第一段是應(yīng)用的 QStringQt 內(nèi)部用 UTF-16 存字符串第二段是 Linux 文件系統(tǒng)文件名在系統(tǒng)調(diào)用層是 UTF-8 字節(jié)第三段是 xls 文件內(nèi)部的 BIFF8 記錄2003 格式的字符串在記錄里可能是壓縮單字節(jié)也可能是 UTF-16LE 雙字節(jié)。三段編碼規(guī)則完全不是一套只要中間某一層的庫做了“想當(dāng)然”的轉(zhuǎn)換就立刻出問題。# Qt5 中 QString 到 Linux 文件系統(tǒng)調(diào)用的默認(rèn)鏈路 QString → toUtf8() → char* → open()/fopen() # 第三方 xls 庫常見的錯誤假設(shè)鏈路 QString → toLocal8Bit() → 當(dāng)前 locale 編碼 → 文件系統(tǒng)第二行是大多數(shù)第三方庫的默認(rèn)做法它在 Windows 上問題不大因?yàn)?Windows 的本地代碼頁和 ANSI 之間轉(zhuǎn)換相對直接。到了 Linux如果進(jìn)程 locale 是常見的 UTF-8toLocal8Bit()恰好也是 UTF-8路徑還能碰巧對上。一旦進(jìn)程跑在LANGC或者POSIXlocale 下toLocal8Bit()出來的就是 US-ASCII中文字符直接變成?文件路徑在系統(tǒng)調(diào)用層就斷了。這里最大的變量不是 Qt而是運(yùn)行環(huán)境的 locale 設(shè)置。2.2 Excelformat 這類庫對 2003 格式的經(jīng)典誤判2003 的 xls 用的是 BIFF8 二進(jìn)制流整份文件由一串記錄組成每條記錄頭是 2 字節(jié) ID 加 2 字節(jié)長度。字符串在記錄里的存儲有兩種壓縮格式用 1 字節(jié)表示一個字符非壓縮格式用 2 字節(jié) UTF-16LE 表示一個碼元。文件名路徑是 char*記錄內(nèi)字符串又有自己的壓縮標(biāo)志一個只支持 ASCII 的庫拿到這兩種混合編碼基本必出亂碼。我見過某個第三方 Excel 格式庫的解析代碼它對字符串的讀取有個簡化邏輯先讀一個字符數(shù)cch再讀一個標(biāo)志字節(jié)grbit然后直接用cch作為字節(jié)數(shù)去截取數(shù)據(jù)。這個做法在純英文 xls 里一輩子不出錯但一旦碰到含中文的 sheet 名或單元格cch表示的是字符數(shù)非壓縮字符串的實(shí)際字節(jié)數(shù)是cch * 2一旦按字節(jié)數(shù)截取后續(xù)所有記錄偏移全部錯位讀出來的內(nèi)容全是錯的甚至直接段錯誤。這也是標(biāo)題里“源碼修改版”存在的根本原因。2.3 為什么不直接換庫或用 xlsx 一勞永逸常見的思路是既然 xls 這么麻煩為什么不把文件轉(zhuǎn)成 xlsx或者直接換一個現(xiàn)代庫這個問題的答案通常不在技術(shù)上而在業(yè)務(wù)約束里。大量舊系統(tǒng)導(dǎo)出的報表、財務(wù)臺賬、設(shè)備清單都是 2003 格式遷移成本高用戶要求的就是原樣讀寫而 xlsx 是 zip 包加 XML中文路徑問題確實(shí)繞開了但打開老系統(tǒng)的歷史文件需要先做格式轉(zhuǎn)換轉(zhuǎn)換過程本身又會丟失格式、宏、批注業(yè)務(wù)上根本不能接受。換新庫我也試過。有的庫接口設(shè)計老舊返回值全是char*和 QString 接駁要自己寫轉(zhuǎn)換有的庫不提供源碼出了邊界問題只能繞。更關(guān)鍵的是自己手里這份源碼已經(jīng)在特定數(shù)據(jù)上做過容錯修正比如遇到非法記錄跳過而不是直接崩潰這個行為遷到新庫要重新驗(yàn)證一遍。綜合下來在原有第三方庫源碼上做修改是所有路線里改動面最小、驗(yàn)證成本最低的一條。3. 源碼修改版怎么做把文件打開與解析解耦的 3 個動作3.1 第一步先把讀寫函數(shù)收口到唯一入口拿到一份第三方 xls 庫源碼第一件事不是去翻它的字符串解碼而是找到所有和文件路徑打交道的地方。常見的入口包括打開文件、保存文件、另存為這三個函數(shù)里面通常都是const char* path參數(shù)然后內(nèi)部調(diào)fopen或std::ifstream。我的做法是把它們?nèi)扛某梢粋€統(tǒng)一入口外部把文件字節(jié)流傳進(jìn)來庫只負(fù)責(zé)解析內(nèi)存不再自己碰文件名。// 修改前的常見寫法某第三方庫偽代碼 bool XlsWorkbook::open(const char* path) { FILE* fp fopen(path, rb); // 中文路徑在部分 Linux locale 下失敗 if (!fp) return false; // 后續(xù)解析邏輯全部基于 FILE* } // 修改后路徑層與解析層分離 bool XlsWorkbook::openFromMemory(const unsigned char* data, size_t len) { // 只做 BIFF8 記錄解析完全不關(guān)心數(shù)據(jù)來自哪里 } bool XlsWorkbook::openQt(const QString path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return false; QByteArray bytes file.readAll(); // 注意庫內(nèi)部如果持有這個指針需要拷貝一份不能直接指向臨時對象 return openFromMemory( reinterpret_castconst unsigned char*(bytes.constData()), static_castsize_t(bytes.size())); }這個收口動作的價值在于把兩個問題徹底隔開路徑編碼問題歸路徑層格式解析問題歸解析層。后面不管環(huán)境 locale 怎么變解析層都不會受影響。openFromMemory的內(nèi)存生命周期是個關(guān)鍵參數(shù)如果庫解析完就釋放指針bytes在函數(shù)結(jié)束后失效沒關(guān)系但有些庫會持有指針以備后續(xù)寫入這種情況下必須在openFromMemory內(nèi)部做一次拷貝否則就是野指針崩潰。3.2 第二步用 QFile 接管文件字節(jié)流繞開 localeLinux 下中文路徑最穩(wěn)的打開方式就是 QFile不需要自己做任何編碼轉(zhuǎn)換。Qt 的文件層會按當(dāng)前系統(tǒng)規(guī)則處理路徑而系統(tǒng)文件名本質(zhì)是字節(jié)序列Linux 默認(rèn)接受 UTF-8。真正的問題不是系統(tǒng)不支持中文文件名而是fopen在非 UTF-8 locale 下拿到的是錯誤編碼的路徑參數(shù)。用 QFile 直接從 QString 出發(fā)不經(jīng)過窄字符串轉(zhuǎn)換這個風(fēng)險從根上消失。QFile file(QStringLiteral(/tmp/中文目錄/臺賬.xls)); if (!file.open(QIODevice::ReadOnly)) { qWarning() 打開失敗: file.errorString(); return false; } QByteArray bytes file.readAll();對于大多數(shù)業(yè)務(wù)里的 xls 文件幾百 KB 到幾 MB 是常態(tài)readAll()一次讀入內(nèi)存沒有壓力。如果遇到超大的 xls可以改用file.read(recordSize)按記錄讀取但解析層要配合改造工作量大不少。我一般先把readAll()跑通再根據(jù)實(shí)際文件大小決定要不要走流式。還有一個容易忽略的點(diǎn)QFile打開成功后要判斷file.errorString()很多中文路徑失敗到這里根本沒走進(jìn)去報錯信息會直接給出原因。3.3 第三步按 fHighByte 處理 BIFF8 字符串解碼路徑搞定了下一步是改字符串解析。BIFF8 記錄的字符串結(jié)構(gòu)是頭部 1 或 2 字節(jié)字符數(shù)cch1 字節(jié)選項(xiàng)標(biāo)志grbit后面跟著字符串?dāng)?shù)據(jù)。grbit最低位fHighByte決定存儲格式為 0 時是壓縮單字節(jié)字符串為 1 時是 UTF-16LE 字符串。第三方庫最常見的坑就是只實(shí)現(xiàn)了壓縮格式遇到中文的 UTF-16LE 數(shù)據(jù)直接按單字節(jié)讀結(jié)果自然全亂。bool readBiff8String(const QByteArray data, int pos, QString out) { if (pos 2 data.size()) return false; quint16 cch 0; uchar grbit 0; // 先讀字符數(shù)1 或 2 字節(jié)取決于長的記錄類型 cch static_castquint8(data.at(pos)); grbit static_castuchar(data.at(pos)); if (grbit 0x01) { // fHighByte 1UTF-16LE int byteLen cch * 2; if (pos byteLen data.size()) return false; QByteArray raw data.mid(pos, byteLen); out QString::fromUtf16( reinterpret_castconst ushort*(raw.constData()), cch); pos byteLen; } else { // 壓縮單字節(jié)格式按當(dāng)前代碼頁解中文環(huán)境常見 GBK/CP936 int byteLen cch; if (pos byteLen data.size()) return false; QByteArray raw data.mid(pos, byteLen); QTextCodec* codec QTextCodec::codecForName(GBK); out codec-toUnicode(raw); pos byteLen; } return true; }這段代碼有兩個參數(shù)最容易寫錯。一是cch的單位是字符不是字節(jié)非壓縮格式下跳過長度一定要乘以 2二是QString::fromUtf16的第二個參數(shù)是字符數(shù)不是字節(jié)數(shù)如果傳成byteLen會越界。壓縮格式下用 GBK 解碼是因?yàn)?2003 版 xls 在中文系統(tǒng)上生成的壓縮字符串基本就是 GBK 字節(jié)序列。如果文件來自英文系統(tǒng)代碼頁會是 CP1252這個可以根據(jù)實(shí)際數(shù)據(jù)來源做成可配置項(xiàng)。4. 在 Linux 上跑通中文路徑讀寫 xls 的最小工程4.1 最小 qmake 工程與可復(fù)現(xiàn)的調(diào)用代碼最佳實(shí)踐回歸到一個能跑的工程。這里我拿 qmake 舉例Qt5 環(huán)境基本都帶。工程文件不需要額外鏈接第三方庫因?yàn)槲覀兪窃创a修改版直接把自己改好的源文件加進(jìn)工程即可。QT core CONFIG console c11 TARGET xls_demo SOURCES main.cpp \ xls_workbook.cpp \ xls_records.cpp HEADERS xls_workbook.h \ xls_records.h主程序里做的事情很簡單構(gòu)造一個中文路徑打開 xls遍歷 sheet 名并打印出來。為了驗(yàn)證寫回功能再把第一個單元格寫入一個中文值保存為一個新文件。#include QCoreApplication #include QDebug #include xls_workbook.h int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QString path QStringLiteral(/tmp/中文目錄/臺賬.xls); XlsWorkbook wb; if (!wb.openQt(path)) { qWarning() 打開失敗請檢查路徑與權(quán)限; return 1; } qInfo() sheet 數(shù)量: wb.sheetCount(); for (int i 0; i wb.sheetCount(); i) { qInfo() sheet 名: wb.sheetName(i); } wb.setCellValue(0, 0, 0, QStringLiteral(中文寫入測試)); QString writePath QStringLiteral(/tmp/中文目錄/臺賬_輸出.xls); if (wb.saveQt(writePath)) { qInfo() 寫入成功: writePath; } return 0; }QStringLiteral是我在源碼中處理中文字面量的習(xí)慣寫法前提是源碼文件本身以 UTF-8 編碼保存。如果是從命令行參數(shù)接收路徑需要用QString::fromLocal8Bit配合環(huán)境 locale 處理不要直接用argc[1]去構(gòu)造 QString。wb.setCellValue的前兩個參數(shù)是行列索引第三個參數(shù)是 sheet 索引這部分接口名字不同庫不一樣替換成自己手里那版源碼對應(yīng)的名字即可。4.2 三種改法取舍內(nèi)存接管、標(biāo)準(zhǔn)庫 u8path、顯式 locale圍繞“如何讓庫吃到正確編碼的路徑”業(yè)界常見做法有三種。我分別列出來對比。改法實(shí)現(xiàn)難度風(fēng)險點(diǎn)適用場景QFile 讀內(nèi)存后交給庫解析低只改入口大文件需考慮流式讀取通用個人最推薦C17std::filesystem::u8path構(gòu)造路徑中依賴編譯器與標(biāo)準(zhǔn)庫版本庫內(nèi)部如果又轉(zhuǎn)一次 locale仍然亂碼本身就用 C17 的工程設(shè)置 locale 為 UTF-8 后再調(diào)庫低一條setlocale()即可影響整個進(jìn)程多線程環(huán)境容易出幺蛾子單線程小工具應(yīng)急第三種做法看起來省事隱患最大。setlocale(LC_ALL, )改的是進(jìn)程級狀態(tài)如果這個庫在解析過程中調(diào)用了任何依賴 locale 的 C 函數(shù)都會受到牽連萬一你的程序還有別的編碼敏感模塊在同進(jìn)程運(yùn)行幾個模塊之間的行為就會相互干擾。我見過一個項(xiàng)目用了這個“捷徑”結(jié)果程序里同時用 Qt 的QTextCodec和 C 的mbstowcs兩邊對同一段字節(jié)的解釋不一致數(shù)據(jù)直接錯亂成為排查了一整天的老難題。4.3 驗(yàn)證清單從打開到寫回要過的 4 個檢查點(diǎn)改完代碼不是能編譯就算完驗(yàn)證要按這個順序來。第一確認(rèn)中文路徑文件能被打開并讀完整第二打印 sheet 名肉眼確認(rèn)沒有亂碼第三寫入中文單元格然后立即重新打開新文件讀回單元格確認(rèn)與寫入值一致第四把寫出的文件另存一份在裝有 2003 兼容組件的環(huán)境或 LibreOffice 里打開檢查是否提示文件損壞。cd /tmp/中文目錄 ./xls_demo # 預(yù)期輸出 # sheet 數(shù)量: 3 # sheet 名: 匯總 明細(xì) 備注 # 寫入成功: /tmp/中文目錄/臺賬_輸出.xls這四條驗(yàn)證里最容易漏的是第三條。很多庫讀取時中文沒問題但寫回時在saveQt階段又做了一次編碼轉(zhuǎn)換把 UTF-16 轉(zhuǎn)成了錯誤代碼頁結(jié)果讀出來的是亂碼。寫回后的文件除了要能讀還要在外部辦公軟件里打開一次因?yàn)橛行鞂懗鰜淼奈募?nèi)部記錄長度是對的但字符串的代碼頁標(biāo)志寫錯了Office 兼容組件會直接判定為格式異常。5. 中文路徑讀寫 xls 避坑手冊5 個高頻問題與排查順序5.1 現(xiàn)象 1中文路徑 fopen 失敗或讀到空文件現(xiàn)象是文件明明存在fopen返回空指針或者打開后讀取字節(jié)數(shù)為 0。原因基本是進(jìn)程 locale 為 POSIX/C導(dǎo)致 UTF-8 路徑被截斷。解決方法是拋棄路徑層直接調(diào) C 庫函數(shù)改用 QFile 讀入內(nèi)存或者至少保證進(jìn)程執(zhí)行前用setlocale(LC_ALL, )把 locale 從 C 切到環(huán)境配置。我一般直接上 QFile路徑問題從此一勞永逸。5.2 現(xiàn)象 2文件名正常但 sheet 名亂碼路徑能讀文件名也對但 sheet 名打出來是“綆旱”之類的天書。原因是庫內(nèi)部解析 BIFF8 字符串時把非壓縮的 UTF-16LE 數(shù)據(jù)按單字節(jié)讀或者把壓縮字符串按 Latin-1 解碼了。解決方法是按grbit標(biāo)志分支處理參照 3.3 節(jié)代碼壓縮字符串按實(shí)際代碼頁解非壓縮字符串按 UTF-16LE 解。5.3 現(xiàn)象 3寫出的 xls 打開報格式損壞寫回能成功看起來文件也生成了但用辦公軟件打開就彈“文件格式與擴(kuò)展名不匹配”。通常是記錄長度字段寫錯字符串的字符數(shù)被當(dāng)成字節(jié)數(shù)寫入記錄頭導(dǎo)致整條記錄長度超過實(shí)際數(shù)據(jù)。排查時把生成文件和原始文件用十六進(jìn)制對比重點(diǎn)看字符串記錄附近的 4 字節(jié)長度字段確認(rèn)是len * 2還是len。5.4 現(xiàn)象 4純英文路徑正常路徑帶中文就崩潰這類崩潰多數(shù)不是段錯誤而是斷言失敗或者無限循環(huán)。原因在于某條解析邏輯讀到了錯誤的長度值偏移直接跳到非法內(nèi)存然后再在后續(xù)解析中觸發(fā)越界。解決方式是先按記錄頭把文件結(jié)構(gòu)走一遍確保每條記錄的邊界都合法再去解析內(nèi)容。我在改庫源碼時會在解析循環(huán)里加一層邊界檢查發(fā)現(xiàn)記錄長度超過文件剩余長度就停止解析而不是繼續(xù)讀。5.5 現(xiàn)象 5調(diào)試一整天只剩最后一個字節(jié)不對這是最折磨人的一種所有內(nèi)容都對了只有某個單元格末尾多了一個不可見字符。原因是 BIFF8 字符串后面可能跟一個可選的擴(kuò)展標(biāo)志某些記錄會在字符串末尾附加一個fExtSt標(biāo)志第三方庫把它當(dāng)成了字符串的一部分。解決方法是先打印出該字符串附近的原始字節(jié)看一下多出來的是什么字符再決定在解碼時把它跳過。這類問題沒有通用解只能靠對比字節(jié)流來定位。排查順序也有講究先驗(yàn)證路徑層再驗(yàn)證記錄偏移最后驗(yàn)證字符串解碼。路徑層用 QFile 能覆蓋九成問題記錄偏移通過遍歷記錄頭比對長度能在五分鐘內(nèi)發(fā)現(xiàn)解析層的結(jié)構(gòu)性錯誤字符串解碼放在最后因?yàn)橹挥衅普_了解碼才有意義。6. 用記錄遍歷器驗(yàn)證你的 xls 修改版沒有改壞文件源碼改完功能驗(yàn)證也過了還剩一件事我每次必做用記錄遍歷器把整個 xls 文件的記錄結(jié)構(gòu)走一遍。這一步能發(fā)現(xiàn)隱藏最深的偏移錯誤比單純打開文件做功能測試可靠得多。2003 的 xls 本質(zhì)是記錄流只要每條記錄的邊界正確文件結(jié)構(gòu)就是健康的一旦某一處長度算錯后面所有記錄全部錯位功能測試可能碰巧沒觸發(fā)。// 最小記錄遍歷器驗(yàn)證文件結(jié)構(gòu)完整性 bool validateXlsStructure(const QByteArray data) { int pos 0; while (pos 4 data.size()) { quint16 id 0; quint16 len 0; memcpy(id, data.constData() pos, 2); memcpy(len, data.constData() pos 2, 2); // 記錄頭 4 字節(jié)后面 len 字節(jié)是記錄體 if (pos 4 len data.size()) { qWarning() 記錄越界偏移: pos 記錄ID: QString::number(id, 16); return false; } pos 4 len; } return pos data.size(); }把這個函數(shù)掛到修改版庫的自測流程里每次改完字符串解碼邏輯都先拿一批真實(shí) xls 文件跑一遍。我踩過最大的一個坑就是某個數(shù)據(jù)類型在功能測試時看起來全對但記錄遍歷發(fā)現(xiàn)它的長度多算了兩字節(jié)導(dǎo)致其后一條格式完全錯亂。遍歷器抓到這類問題只需要幾毫秒。這套方案做完后我的習(xí)慣是保留一份“修改前 vs 修改后”的字節(jié)流對比腳本每次發(fā)布前跑一遍確認(rèn)新代碼沒有引入新的偏移變化。中文路徑和 2003 格式校驗(yàn)是兩件事分開驗(yàn)證彼此不干擾問題才能定位得準(zhǔn)。這套折騰下來以后再遇到類似需求我會先問一句話你手里的第三方庫對路徑做了什么假設(shè)希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取