【C#】C#中使用GDAL3(二):Windows下讀寫Shape文件及超詳細解決中文亂碼問題
轉載請註明原文地址://www.cnblogs.com/litou/p/15035790.html
本文為《C#中使用GDAL3》的第二篇,總目錄地址://www.cnblogs.com/litou/p/15004877.html
本目錄 |
一、介紹 |
二、讀寫數據內容 |
三、中文亂碼問題 |
3.1、數據路徑或數據文件名含中文時打開失敗 |
3.2、讀取中文字元串顯示亂碼 |
3.3、函數傳入中文字元串參數報錯 |
一、介紹
Shape文件是ESRI公司開發的一種空間數據開放格式,全稱是ESRI Shapefile,該文件格式是由多個文件組成的,表示同一數據的一組文件的文件名必須相同。
要組成一份Shapefile,有三個文件是必不可少的,它們分別是shp、shx和dbf文件。組成如下:
必須文件 | .shp | 主文件,記錄要素幾何實體 |
.shx | 索引文件,記錄每一個幾何體在shp文件之中的位置 | |
.dbf | 數據文件,以dBase IV的數據表格式存儲每個幾何形狀的屬性數據 | |
可選文件 | .prj | 投影文件,保存地理坐標系統與投影資訊 |
.sbx .sbn | 其他文件 |
二、讀寫數據內容
GDAL庫內置支援讀寫ESRI Shapefile文件,無需其他插件支援。
示例Shapefile文件如下,存放在”C:\shp數據”下,圖層名稱為”測試面”,類型為面,自定義欄位有”Id”、”名稱”和”大小”,有兩條記錄。
以VS2015為例,修改自上一篇《C#中使用GDAL3(一):Windows下超詳細編譯C#版GDAL3.3.0(VS2015+.NET 4+32位/64位)》中第九部分”C#調用測試”的Demo程式。
由於Shapefile文件屬於矢量數據,所以只需註冊OGR驅動。
1、打開數據
調用Ogr.Open打開數據獲取DataSource。這裡有兩種打開方法:
1)打開shp文件,即Ogr.Open的第一個參數是shp文件的路徑,打開後得到的DataSource裡面只含shp文件本身的一份數據。
2)打開shp文件所在目錄,即Ogr.Open的第一個參數是shp文件所在目錄的路徑,打開後得到的DataSource裡面包含該目錄下所有shp文件數據。
另外,Open的第二個參數為打開方式,值0表示以只讀方式打開,值1表示以讀寫方式打開。
2、獲取圖層對象和圖層名稱
調用DataSource.GetLayerByXXXXX獲取圖層對象,這裡調用的是GetLayerByIndex,再調用Layer.GetName獲取圖層名稱。
3、獲取要素定義、欄位定義和欄位名稱
調用Layer.GetLayerDefn獲取要素定義,然後調用FeatureDefn.GetFieldDefn獲取欄位定義,再調用FieldDefn.GetName獲取欄位名稱。
4、遍歷要素記錄
循環調用Layer.GetNextFeature獲取每一條要素記錄,直到獲取的要素記錄為null則循環結束。如需要重頭開始遍歷,需要調用Layer.ResetReading重置為開頭位置。
5、讀取要素欄位值
調用Feature.GetFieldAsXXXXX獲取要素欄位值,這裡調用的是GetFieldAsInteger、GetFieldAsString和GetFieldAsDouble的傳入欄位索引值的方法。
6、設置要素欄位值
調用Feature.SetField寫入要素欄位值。
7、更新要素
調用Layer.SetFeature使要素修改生效。
using OSGeo.OGR; using System; namespace GdalDemo { class Program { static void Main(string[] args) { Ogr.RegisterAll(); ReadShapeFile(); Console.ReadKey(); } static void ReadShapeFile() { //打開數據 string path = @"C:\shp數據"; DataSource ds = Ogr.Open(path, 1); //以可寫方式打開 int lCount = ds.GetLayerCount(); for (int i = 0; i < lCount; i++) { //讀取圖層資訊 Layer layer = ds.GetLayerByIndex(i); string layerName = layer.GetName(); Console.WriteLine(String.Format("圖層名:{0}", layerName)); //讀取欄位資訊 FeatureDefn featureDefn = layer.GetLayerDefn(); int fCount = featureDefn.GetFieldCount(); for (int j = 0; j < fCount; j++) { FieldDefn fieldDefn = featureDefn.GetFieldDefn(j); string fieldName = fieldDefn.GetName(); Console.WriteLine(String.Format("欄位名:{0}", fieldName)); } //遍歷要素 Feature feature; while ((feature = layer.GetNextFeature()) != null) { //讀取要素資訊 int id = feature.GetFieldAsInteger(0); Console.WriteLine(String.Format("欄位值-id:{0}", id)); string name = feature.GetFieldAsString(1); Console.WriteLine(String.Format("欄位值-名稱:{0}", name)); double size = feature.GetFieldAsDouble(2); Console.WriteLine(String.Format("欄位值-大小:{0}", size)); //設置要素資訊 feature.SetField(0, id + 1); feature.SetField(1, name + "加"); feature.SetField(2, size + 10.12); //更新要素 layer.SetFeature(feature); //讀取修改後要素資訊 Console.WriteLine(String.Format("欄位值-修改後-id:{0}", feature.GetFieldAsInteger(0))); Console.WriteLine(String.Format("欄位值-修改後-名稱:{0}", feature.GetFieldAsString(1))); Console.WriteLine(String.Format("欄位值-修改後-大小:{0}", feature.GetFieldAsDouble(2))); //用欄位名讀取欄位值 Console.WriteLine(String.Format("欄位值-欄位名值-id:{0}", feature.GetFieldAsInteger("id"))); try { Console.WriteLine(String.Format("欄位值-欄位名值-名稱:{0}", feature.GetFieldAsString("名稱"))); } catch { } } } } } }
運行結果如下:
1)數據讀取正常
2)中文圖層名稱和欄位名稱均顯示為亂碼
3)讀取欄位值並顯示中文內容正常
4)寫入中文內容到欄位正常
5)使用中文欄位名獲取欄位值報錯
三、中文亂碼問題
要解決亂碼問題,首先要理解為什麼會出現亂碼。根據GDAL的文檔資料顯示(//gdal.org/development/rfc/rfc5_unicode.html),GDAL內部字元串使用UTF8編碼,也就是說輸入和輸出的字元串均為UTF8編碼,而我們使用的作業系統大部分都是簡體中文版的Windows,其默認的字元串編碼是GB2312(可通過C#下的System.Text.Encoding.Default.EncodingName得到),如果不做編碼轉換直接顯示的話就會出現亂碼問題。
3.1、數據路徑或數據文件名含中文時打開失敗
該情況在GDAL 3.3.0的C#介面中是不存在的。以Ogr庫為例,在Ogr.cs中可以找到Open方法,其方法內通過Ogr.StringToUtf8Bytes函數處理,把傳入的路徑字元串轉化為UTF8編碼的位元組數組,再傳入內部的Open方法,所以在調用Ogr.Open方法時,無需對傳入的路徑字元串進行編碼處理,也能正常使用。
另外在GDAL內部,參數GDAL_FILENAME_IS_UTF8的默認值是YES,所以無需顯式重複設置為YES也能正常讀取,設置為NO反而導致讀取失敗。
//Ogr.cs public static DataSource Open(string utf8_path, int update) { IntPtr cPtr = OgrPINVOKE.Open(Ogr.StringToUtf8Bytes(utf8_path), update); DataSource ret = (cPtr == IntPtr.Zero) ? null : new DataSource(cPtr, true, ThisOwn_true()); if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve(); return ret; } internal static byte[] StringToUtf8Bytes(string str) { if (str == null) return null; int bytecount = System.Text.Encoding.UTF8.GetMaxByteCount(str.Length); byte[] bytes = new byte[bytecount + 1]; System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, bytes, 0); return bytes; }
3.2、讀取中文字元串顯示亂碼
同樣是讀取字元串,讀取中文圖層名稱和欄位名稱顯示亂碼,而讀取中文欄位值則正常。
//Layer.cs public string GetName() { string ret = OgrPINVOKE.Layer_GetName(swigCPtr); if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve(); return ret; } //FieldDefn.cs public string GetName() { string ret = OgrPINVOKE.FieldDefn_GetName(swigCPtr); if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve(); return ret; } //Feature.cs public string GetFieldAsString(int id) { IntPtr cPtr = OgrPINVOKE.Feature_GetFieldAsString__SWIG_0(swigCPtr, id); string ret = Ogr.Utf8BytesToString(cPtr); if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve(); return ret; } //Ogr.cs internal unsafe static string Utf8BytesToString(IntPtr pNativeData) { if (pNativeData == IntPtr.Zero) return null; byte* pStringUtf8 = (byte*)pNativeData; int len = 0; while (pStringUtf8[len] != 0) len++; return System.Text.Encoding.UTF8.GetString(pStringUtf8, len); }
對比GetName和GetFieldAsString兩個函數可以很明顯看出來,GetFieldAsString通過調用Ogr.Utf8BytesToString將返回的UTF8編碼的位元組數組以UTF8方式解碼為字元串,所以能夠正常顯示;而GetName則直接返回字元串(實際上編譯器隱性調用了System.Text.Encoding.Default.GetString解碼為字元串),由於沒有使用UTF8解碼導致顯示為亂碼。
不完美處理方法1:在C#中將亂碼字元串還原為位元組數組並重新以UTF8方式解碼字元串。
具體方法為,將亂碼的字元串先通過System.Text.Encoding.Default.GetBytes轉換回亂碼狀態前的位元組數組,再調用System.Text.Encoding.UTF8.GetString以UTF8的方式解碼為系統識別的字元串。
該方法處理偶數個中文字元時可以正常還原,但處理奇數個中文字元時最後一個中文字元還原失敗。測試程式碼如下:
using System; using System.Text; namespace Demo { class Program { static void Main(string[] args) { string sOdd = "測試"; Console.WriteLine("原字元串:" + sOdd); string sOddUtf8 = Encoding.Default.GetString(Encoding.UTF8.GetBytes(sOdd)); Console.WriteLine("UTF8字元串:" + sOddUtf8); string sOddURestore = Encoding.UTF8.GetString(Encoding.Default.GetBytes(sOddUtf8)); Console.WriteLine("還原字元串:" + sOddURestore); Console.WriteLine(); string sEven = "測試面"; Console.WriteLine("原字元串:" + sEven); string sEvenUtf8 = Encoding.Default.GetString(Encoding.UTF8.GetBytes(sEven)); Console.WriteLine("UTF8字元串:" + sEvenUtf8); string sEvenURestore = Encoding.UTF8.GetString(Encoding.Default.GetBytes(sEvenUtf8)); Console.WriteLine("還原字元串:" + sEvenURestore); Console.ReadKey(); } } }
結果如下,”測試”可以正常還原,而”測試面”最後一個字還原失敗。其原因是編碼轉換的問題,與平台無關,具體可參考該文章(//blog.csdn.net/yuwenruli/article/details/6911401)。
要解決字元串亂碼問題,只需要將原始UTF8編碼的位元組數組正確的使用UTF8解碼即可。
前面提到GDAL中返回亂碼字元串的函數(如GetName)已經把UTF8編碼的位元組數組返回為錯誤編碼的字元串,且無法還原為完整的UTF8編碼的位元組數組,只能從源頭開始處理。
解決方法2:在GDAL的C#源碼中修正返回亂碼字元串的函數。
以Layer.GetName為例,修改OgrPINVOKE.cs裡面SWIGStringHelper的CreateString函數說明,並增加UTF8編碼處理。(如沒有找到.cs源碼文件,執行一次nmake -f makefile.vc interface即可生成)
//OgrPINVOKE.cs //修改前 protected class SWIGStringHelper { public delegate string SWIGStringDelegate(string message); static SWIGStringDelegate stringDelegate = new SWIGStringDelegate(CreateString); [global::System.Runtime.InteropServices.DllImport("ogr_wrap", EntryPoint = "SWIGRegisterStringCallback_Ogr")] public extern static void SWIGRegisterStringCallback_Ogr(SWIGStringDelegate stringDelegate); static string CreateString(string cString) { return cString; } static SWIGStringHelper() { SWIGRegisterStringCallback_Ogr(stringDelegate); } } //修改後 protected class SWIGStringHelper { public delegate string SWIGStringDelegate(IntPtr ptr); //委託類型改為IntPtr static SWIGStringDelegate stringDelegate = new SWIGStringDelegate(CreateString); [global::System.Runtime.InteropServices.DllImport("ogr_wrap", EntryPoint = "SWIGRegisterStringCallback_Ogr")] public extern static void SWIGRegisterStringCallback_Ogr(SWIGStringDelegate stringDelegate); static string CreateString(IntPtr ptr) { return Ogr.Utf8BytesToString(ptr); //返回UTF8解碼的字元串 } static SWIGStringHelper() { SWIGRegisterStringCallback_Ogr(stringDelegate); } }
修改完畢後,重新執行nmake -f makefile.vc和nmake -f makefile.vc install,將新生成的ogr_csharp.dll替換原來引入到C#項目中的文件並重新運行,發現圖層名已經能夠正常顯示外,且欄位名也同樣正常顯示了。
註:其他類庫也需要同樣修改,修改內容匯總如下:
OgrPINVOKE.cs -> Ogr.Utf8BytesToString GdalPINVOKE.cs -> Gdal.Utf8BytesToString OsrPINVOKE.cs -> Osr.Utf8BytesToString GdalConst.cs 補充Utf8BytesToString函數 GdalConstPINVOKE.cs -> GdalConst.Utf8BytesToString
修改原理可參考下圖:
1)在Feature.GetFieldAsString方法的調用鏈中,用IntPtr表示C++返回的字元指針(橙色部分),然後將其用UTF8解碼為字元串。
2)在Layer.GetName方法的調用鏈中,C++將得到的字元指針回調至C#端處理(橙色部分),處理後的字元串回到C++中繼續流轉,最後返回到C#中。而回調的C#部分直接把字元指針返回為字元串,編譯器隱性調用了System.Text.Encoding.Default.GetString解碼為字元串,故後面得到的字元串都是解碼錯誤的。
所以Layer.GetName解決亂碼的思路有兩種:
1)在SWIGStringHelper.CreateString處用UTF8解碼字元串,也就是本解決方法。且除Layer.GetName之外,其他返回字元串的函數均調用了相同的回調函數,故其他返回亂碼字元串的問題也一併解決了(如FieldDefn.GetName等)。
2)跳過ogr_wrap的所有包裝函數(包括C#回調),直接調用gdal的函數獲取,因此引申出下面的解決方法。
解決方法3:在C#中調用GDAL介面獲取內容。
以Layer.GetName為例,在C#中增加調用gdal303.dll的OGR_L_GetName介面,並使用UTF8編碼處理。FieldDefn.GetName需要調用OGR_Fld_GetNameRef介面(介面名稱可查閱//gdal.org/python)。
static string Utf8BytesToString(IntPtr ptr) { if (ptr == IntPtr.Zero) return null; MemoryStream ms = new MemoryStream(); byte b; int ofs = 0; while ((b = Marshal.ReadByte(ptr, ofs++)) != 0) { ms.WriteByte(b); } return Encoding.UTF8.GetString(ms.ToArray()); } //Layer.GetName [DllImport("gdal303.dll", EntryPoint = "OGR_L_GetName", CallingConvention = CallingConvention.Cdecl)] static extern IntPtr OGR_L_GetName(HandleRef handle); static string GetLayerName(Layer layer) { HandleRef handle = Layer.getCPtr(layer); IntPtr ptr = OGR_L_GetName(handle); return Utf8BytesToString(ptr); } //FieldDefn.GetName [DllImport("gdal303.dll", EntryPoint = "OGR_Fld_GetNameRef", CallingConvention = CallingConvention.Cdecl)] static extern IntPtr OGR_Fld_GetNameRef(HandleRef handle); static string GetFieldDfnName(FieldDefn fieldDefn) { HandleRef handle = FieldDefn.getCPtr(fieldDefn); IntPtr ptr = OGR_Fld_GetNameRef(handle); return Utf8BytesToString(ptr); }
運行結果如下,圖層名和欄位名已經正常顯示。
3.3、函數傳入中文字元串參數報錯
以Feature.GetFieldAsString(string field_name)為例,前面已通過枚舉的方式列出所有欄位名稱且包含欄位名”名稱”,但調用Feature.GetFieldAsString方法並傳入”名稱”作為參數時,卻報錯Invalid field name。
參考其方法的調用鏈,C#中傳入的字元串參數直接傳遞為C++的字元指針,編譯器隱性調用了System.Text.Encoding.Default.GetBytes將傳入的字元串編碼為GB2312位元組數組,故GDAL無法識別導致報錯。
解決方法:把傳入的字元串做編碼處理。
根據上面的分析結果逆向處理,先把字元串用UTF8編碼為位元組數據,再用Default編碼為字元串,把結果傳入函數即可。
static string Utf8String(string s) { if (!String.IsNullOrEmpty(s)) return Encoding.Default.GetString(Encoding.UTF8.GetBytes(s)); return s; }
運行結果如下,已經可以識別中文字元串調用參數了。