【C#】C#中使用GDAL3(二):Windows下读写Shape文件及超详细解决中文乱码问题
转载请注明原文地址:https://www.cnblogs.com/litou/p/15035790.html
本文为《C#中使用GDAL3》的第二篇,总目录地址:https://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的文档资料显示(https://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();
}
}
}
结果如下,"测试"可以正常还原,而"测试面"最后一个字还原失败。其原因是编码转换的问题,与平台无关,具体可参考该文章(https://blog.csdn.net/yuwenruli/article/details/6911401)。

要解决字符串乱码问题,只需要将原始UTF8编码的字节数组正确的使用UTF8解码即可。
前面提到GDAL中返回乱码字符串的函数(如GetName)已经把UTF8编码的字节数组返回为错误编码的字符串,且无法还原为完整的UTF8编码的字节数组,只能从源头开始处理。
解决方法2:在GDAL的C#源码中修正返回乱码字符串的函数。
以Layer.GetName为例,修改OgrPINVOKE.cs里面SWIGStringHelper的CreateString函数说明,并增加UTF8编码处理。
//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接口(接口名称可查阅https://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;
}
运行结果如下,已经可以识别中文字符串调用参数了。

【C#】C#中使用GDAL3(二):Windows下读写Shape文件及超详细解决中文乱码问题的更多相关文章
- springboot项目中文件的下载(解决中文乱码问题)
最近使用springboot项目,一直以来文件都以英文格式存储,这次使用的是xls文件下载,文件名为中文的,特此记录下中文文件名的下载以及springboot中下载路径报错问题. 正文 在使用spri ...
- windows下命令行利器---Cmder(安装,中文乱码,配置右键菜单)
很多人都是在win下开发的,这样就会出现,经常需要命令行操作,而win cmd命令和linux命令有很大差异,导致大家很难受,今天给大家介绍一个win下命令行的利器-Cmder 一.先看一下它的容颜 ...
- Windows下C++遍历文件夹中的文件
Windows下,在VS中开发,C++遍历文件夹下文件. 在Windows下,遍历文件所用到的函数和结构体,需要在程序中包含头文件#include <io.h>,在VS中,头文件io.h实 ...
- 用脚本如何实现将Linux下的txt文件批量转化为Windows下的txt文件?
众所周知,Windows和Linux的文件换行回车格式不同,Windows下换行格式是\r\n(回车+换行),Linux下换行格式为\n(只是换行),因此,其中一个操作系统的文本文件若需要在另外一个中 ...
- Windows 下目录及文件向Linux同步
本文解决的是Windows 下目录及文件向Linux同步的问题,Windows向 Windows同步的请参考:http://www.idcfree.com/article-852-1.html 环境介 ...
- Windows下如何将一个文件夹通过Git上传到GitHub上(转)
在通过windows系统的电脑上写代码,需要将项目上传到GitHub上去.比如在Pycharm上写Django后端,整个项目是一个文件夹的形式,那么怎么才能这个文件夹通过Git命令上传到GitHub上 ...
- Linux&Windows下批量修改文件后缀
Linux下从给定文件夹中找出小于1M的文件,并批量添加.gif后缀 先看一下文件夹下的目录的格式 ll -Sh -rw-rw-r-- 1 yangkun yangkun 17M May 10 15: ...
- windows下打开.ipynb文件
windows下打开.ipynb文件1.首先要下载python,设置环境变量2.下载pip,设置环境变量3.打开命令行,进入到python的Scripts文件中,按顺序执行下面三个命令pip inst ...
- 转:浅析windows下字符集和文件编码存储/utf8/gbk
最近老猿在学习文件操作及网络爬虫相关知识,发现字符集及编码的处理非常重要,而老猿原来对此了解并不多,因此找了几篇文章看了一下,将老猿认为比较的相关文章转载一下.感谢各位原创大神! 1,字符集 这里主要 ...
随机推荐
- docker-compose 部署 Apollo 自定义环境
Apollo 配置中心是什么: Apollo是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境.不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限.流程治理等特性. ...
- .NET解密得到UnionID
由于微信没有提供.NET的解码示例代码,自己搜索写了一个,下面的代码是可用的 var decryptBytes = Convert.FromBase64String(encrypdata); var ...
- Pytest学习笔记8-参数化
前言 我们在实际自动化测试中,某些测试用例是无法通过一组测试数据来达到验证效果的,所以需要通过参数化来传递多组数据 在unittest中,我们可以使用第三方库parameterized来对数据进行参数 ...
- CS 面试题目总结(问题+答案)
开源了一个新的github仓库CS 面试题目总结(问题+答案),主要总结一些CS大厂常见的面试问题,所有的问题与答案参考了网络上的许多博客和github仓库,也希望各位读者能够对这个仓库进行补充,毕竟 ...
- js笔记14
1.作用域面试题 画图分析 2.DOM document object model 节点树状图 document>documentElement>body>tagname 3.我们常 ...
- 与KubernetesAPI服务器交互
在介绍过的Downward API提供了一种简单的方式,将pod和容器的元数据传递给在它们内部运行的进程.但这种方式其实仅仅可以暴露一个pod自身的元数据,而且只可以暴露部分元数据.某些情况下,应用需 ...
- 18、linux文件属性
文件的描述信息: [root@centos6 /]# ls -lih 总用量 118K 3538945 drwxr-xr-x 3 root root 4.0K 8月 23 17:12 app 3276 ...
- oracle :如何测试数据库安装是否成功
要测试数据安装是否成功,可按顺序执行以下两个步骤: 测试步骤 1: 请执行操作系统级的命令: tnsping orcl (如果出现[TNS-03505:无法解析名称]的提示错误: 那就改为tnspi ...
- 集合类线程安全吗?ConcurrentModification异常遇到过吗?如何解决?
集合类不安全的问题 1. ArrayList的线程不安全问题 1.1 首先回顾ArrayList底层 ArrayList的底层数据结构是数组 底层是一个Object[] elementData的数组, ...
- Centos中安装Node.Js
NodeJs安装有好几种方式: 第一种: 最简单的是用yum命令,可惜我现在用的时候 发现 镜像中没有nodejs:所以这种方式放弃: 第二种:去官网下载源码,然后自己编译:编译过程中可能会出现问题, ...