Dicom格式文件解析器
转自:http://www.cnblogs.com/assassinx/archive/2013/01/09/dicomViewer.html
Dicom全称是医学数字图像与通讯,这里讲的暂不涉及通讯那方面的问题 只讲*.dcm 也就是diocm格式文件的读取,读取本身是没啥难度的 无非就是字节码数据流处理。只不过确实比较繁琐。
分析:
整体结构先是128字节所谓的导言部分,说俗点就是没啥意义的破数据 跳过就是了,然后是dataElement依次排列的方式 就是一个dataElement接一个dataElement的方式排到文件结尾 通俗的讲dataElement就是指tag。tag就是破Dicom标准里定义的数据字典。tag是4个字节表示的 前两字节是组号,后两字节是偏移号。比如0008,0018。所有dataElement在文件中都是按tag排序的 比如0002,0001 0002,0002 0003,0011
文件整体结构如下:
单个dataElement的结构如下:
显示VR:VR为OB OW OF UT SQ UN的元素结构
组号 |
元素号 |
VR |
预留 |
值长度 |
数据元素值 |
2 |
2 |
2 |
2(0x00,0x00) |
4 |
由数据长度决定 |
显示VR:VR为普通类型时元素结构(少了预留那一行)
组号 |
元素号 |
VR |
值长度 |
数据元素值 |
2 |
2 |
2 |
4 |
由数据长度决定 |
隐式VR 时元素结构
组号 |
元素号 |
值长度 |
数据元素值 |
2 |
2 |
4 |
由数据长度决定 |
要问VR是啥东东 ,值表示法 啥叫值表示法啊 俺不懂 int string short ushort 懂不 就是这个意思,Dicom标准真坑爹 非要整个怪怪的概念。
VR总共27个 跟c#值类型对应关系我都写好了:
string getVF(string VR, byte[] VF)
{
string VFStr = string.Empty;
switch (VR)
{
case "SS":
VFStr = BitConverter.ToInt16(VF, ).ToString();
break;
case "US":
VFStr = BitConverter.ToUInt16(VF, ).ToString(); break;
case "SL":
VFStr = BitConverter.ToInt32(VF, ).ToString(); break;
case "UL":
VFStr = BitConverter.ToUInt32(VF, ).ToString(); break;
case "AT":
VFStr = BitConverter.ToUInt16(VF, ).ToString(); break;
case "FL":
VFStr = BitConverter.ToSingle(VF, ).ToString(); break;
case "FD":
VFStr = BitConverter.ToDouble(VF, ).ToString(); break;
case "OB":
VFStr = BitConverter.ToString(VF, );
break;
case "OW":
VFStr = BitConverter.ToString(VF, );
break;
case "SQ":
VFStr = BitConverter.ToString(VF, );
break;
case "OF":
VFStr = BitConverter.ToString(VF, );
break;
case "UT":
VFStr = BitConverter.ToString(VF, );
break;
case "UN":
VFStr = Encoding.Default.GetString(VF);
break;
default:
VFStr = Encoding.Default.GetString(VF);
break;
}
return VFStr;
}
找个dicom文件在十六进制编辑器下瞧瞧 给你整明白:
所有dataElement从前到后按tag又可简单分段:
文件元dataElement | 不受传输语法影响 总是以显示VR方式表示 因为它里面就定义了传输语法 |
普通dataElement | 受传输语法影响 显示VR表示方式还是隐式VR表示方式 |
像素数据dataElement | 最重要也是最大的一个数据项 其实存储的就是图像数据 |
几个特殊的tag很重要 前面说过了tag就是dicom里定义的字典。文件元dataElement 和跟像素数据相关的dataElement 都很重要,其他的很多 如果全部照顾完的话估计得写上千行switch语句吧,所以没有必要一般我们一般只抓取关键的tag。并且在隐式语法下要确定VR也必须根据字典来确定
关键的tag如下:
string getVR(string tag)
{
switch (tag)
{
case "0002,0000"://文件元信息长度
return "UL";
break;
case "0002,0010"://传输语法
return "UI";
break;
case "0002,0013"://文件生成程序的标题
return "SH";
break;
case "0008,0005"://文本编码
return "CS";
break;
case "0008,0008":
return "CS";
break;
case "0008,1032"://成像时间
return "SQ";
break;
case "0008,1111":
return "SQ";
break;
case "0008,0020"://检查日期
return "DA";
break;
case "0008,0060"://成像仪器
return "CS";
break;
case "0008,0070"://成像仪厂商
return "LO";
break;
case "0008,0080":
return "LO";
break;
case "0010,0010"://病人姓名
return "PN";
break;
case "0010,0020"://病人id
return "LO";
break;
case "0010,0030"://病人生日
return "DA";
break;
case "0018,0060"://电压
return "DS";
break;
case "0018,1030"://协议名
return "LO";
break;
case "0018,1151":
return "IS";
break;
case "0020,0010"://检查ID
return "SH";
break;
case "0020,0011"://序列
return "IS";
break;
case "0020,0012"://成像编号
return "IS";
break;
case "0020,0013"://影像编号
return "IS";
break;
case "0028,0002"://像素采样1为灰度3为彩色
return "US";
break;
case "0028,0004"://图像模式MONOCHROME2为灰度
return "CS";
break;
case "0028,0010"://row高
return "US";
break;
case "0028,0011"://col宽
return "US";
break;
case "0028,0100"://单个采样数据长度
return "US";
break;
case "0028,0101"://实际长度
return "US";
break;
case "0028,0102"://采样最大值
return "US";
break;
case "0028,1050"://窗位
return "DS";
break;
case "0028,1051"://窗宽
return "DS";
break;
case "0028,1052":
return "DS";
break;
case "0028,1053":
return "DS";
break;
case "0040,0008"://文件夹标签
return "SQ";
break;
case "0040,0260"://文件夹标签
return "SQ";
break;
case "0040,0275"://文件夹标签
return "SQ";
break;
case "7fe0,0010"://像素数据开始处
return "OW";
break;
default:
return "UN";
break;
}
}
最关键的两个tag:
0002,0010
普通tag的读取方式 little字节序还是big字节序 隐式VR还是显示VR。由它的值决定
switch (VFStr)
{
case "1.2.840.10008.1.2.1\0"://显示little
isLitteEndian = true;
isExplicitVR = true;
break;
case "1.2.840.10008.1.2.2\0"://显示big
isLitteEndian = false;
isExplicitVR = true;
break;
case "1.2.840.10008.1.2\0"://隐式little
isLitteEndian = true;
isExplicitVR = false;
break;
default:
break;
}
7fe0,0010
像素数据开始处
整理
根据以上的分析相信解析一个dicom格式文件的过程已经很清晰了吧
第一步:跳过128字节导言部分,并读取"DICM"4个字符 以确认是dicom格式文件
第二步:读取第一部分 也就是非常重要的文件元dataElement 。读取所有0002开头的tag 并根据0002,0010的值确定传输语法。文件元tag部分的数据元素都是以显示VR的方式表示的 读取它的值 也就是字节码处理 别告诉我说你不会字节码处理哈。传输语法 说得那么官方,你就忽悠吧 其实就确定两个东西而已
1字节序 这个基本上都是little字节序。举个例子吧十进制数 35280 用十六进制表示是0xff00 但是存储到文件中你用十六进制编辑器打开你看到的是这个样子00ff 这就是little字节序。平常我们用的x86PC在windows下都是little字节序 包括AMD的CPU。别太较真 较真的话这个问题又可以写篇博客了。
2确定从0002以后的dataElement的VR是显示还是隐式。说来说去0002,0010的值就 那么固定几个 并且只能是那么几个 这些都在那个北美放射学会定义的dicom标准的第六章 有说明 :
1.2.840.10008.1.2 | Implicit VR Little Endian: Default Transfer Syntax for DICOM | Transfer Syntax |
1.2.840.10008.1.2.1 | Explicit VR Little Endian | Transfer Syntax |
1.2.840.10008.1.2.2 | Explicit VR Big Endian | Transfer Syntax |
上面的那段代码其实就是这个表格的实现,讲到这里你会觉得多么的坑爹啊 是的dicom面向对象的破概念非常烦的。
第三步:读取普通tag 直到搜寻到7fe0,0010 这个最巨体的存储图像数据的 dataElement 它一个顶别人几十个 上百个。我们在前一步已经把VR是显示还是隐式确定 通过前面的图 ,也就是字节码处理而已无任何压力。显示情况下根据VR 和Len 确定数据类型 跟数据长度直接读取就可以了。隐式情况下这破玩艺儿有点烦,只能根据tag 字典确定它是什么VR再才能读取。关于这个字典也在dicom标准的第六章。上面倒数第二段代码已经把重要的字典都列了出来。
第四步:读取灰度像素数据并调窗 以GDI的方式显示出来。 说实话开始我还以为dicom这种号称医学什么影像的专家制定出来的标准 读取像素数据应该有难度吧 结果没想到这么的傻瓜。直接按像素从左到右从上到下 一行行依次扫描。两个字节表示1个像素普通Dicom格式存储的是16位的灰度图像,其实有效数据只有12位,除去0 所以最高值是2047。比如CT值 从-1000到+1000,空气的密度为-1000 水的密度为0 金属的密度为+1000 总共的值为2000
调窗技术:
即把12级灰度的数据 通过调节窗宽窗位并让他在RGB模式下显示出来。还技术呢 说实话这个也是没什么技术含量的所谓的技术,两句代码给你整明白。
调节窗宽窗位到底什么意思,12位的数据那么它总共有2047个等级的灰度 没有显示设备可以体现两千多级的明暗度 就算有我们肉眼也无法分辨更无法诊断。我们要诊断是要提取关键密度值的数据 在医院放射科呆久了你一定经常听医生讲什么骨窗 肺窗 之类的词儿,这就是指的这个“窗”。比如有病人骨折了打了钢板我们想看金属部分来诊断 那么我们应该抓取CT值从800到1000 密度的像素 也就是灰度值 然后把它放到RGB模式下显示,低于800的不论值大小都显示黑色 高于1000的不论值大小都显示白色。
通过以上例子那么这个范围1000-800=200 这个200表示窗宽,800+(200/2)这个表示窗位
一句话,从2047个等级的灰度里选取一个范围放到0~255的灰度环境里显示。
怎样把12位灰度影射到8位灰度显示出来呢,还怎么显示 上面方法都给说明了基本上算半成品了。联想到角度制弧度制,设要求的8位灰度值为x 已知的12位灰度值为y那么:x/255=y/2047 那么x=255y/2047 原理不多讲 等比中项十字相乘法 这个是初中的知识哈。初中没读过的童鞋飘过。。。
代码走起
class DicomHandler
{
string fileName = "";
Dictionary<string, string> tags = new Dictionary<string, string>();//dicom文件中的标签
BinaryReader dicomFile;//dicom文件流 //文件元信息
public Bitmap gdiImg;//转换后的gdi图像
UInt32 fileHeadLen;//文件头长度
long fileHeadOffset;//文件数据开始位置
UInt32 pixDatalen;//像素数据长度
long pixDataOffset = ;//像素数据开始位置
bool isLitteEndian = true;//是否小字节序(小端在前 、大端在前)
bool isExplicitVR = true;//有无VR //像素信息
int colors;//颜色数 RGB为3 黑白为1
public int windowWith = , windowCenter = / ;//窗宽窗位
int rows, cols;
public void readAndShow(TextBox textBox1)
{
if (fileName == string.Empty)
return;
dicomFile = new BinaryReader(File.OpenRead(fileName)); //跳过128字节导言部分
dicomFile.BaseStream.Seek(, SeekOrigin.Begin); if (new string(dicomFile.ReadChars()) != "DICM")
{
MessageBox.Show("没有dicom标识头,文件格式错误");
return;
} tagRead(); IDictionaryEnumerator enor = tags.GetEnumerator();
while (enor.MoveNext())
{
if (enor.Key.ToString().Length > )
{
textBox1.Text += enor.Key.ToString() + "\r\n";
textBox1.Text += enor.Value.ToString().Replace('\0', ' ');
}
else
textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('\0', ' ') + "\r\n";
}
dicomFile.Close();
}
public DicomHandler(string _filename)
{
fileName = _filename;
} public void saveAs(string filename)
{
switch (filename.Substring(filename.LastIndexOf('.')))
{
case ".jpg":
gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
break;
case ".bmp":
gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);
break;
case ".png":
gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
break;
default:
break;
}
}
public bool getImg( )//获取图像 在图像数据偏移量已经确定的情况下
{
if (fileName == string.Empty)
return false; int dataLen, validLen;//数据长度 有效位
int imgNum;//帧数 rows = int.Parse(tags["0028,0010"].Substring());
cols = int.Parse(tags["0028,0011"].Substring()); colors = int.Parse(tags["0028,0002"].Substring());
dataLen = int.Parse(tags["0028,0100"].Substring());
validLen = int.Parse(tags["0028,0101"].Substring()); gdiImg = new Bitmap(cols, rows); BinaryReader dicomFile = new BinaryReader(File.OpenRead(fileName)); dicomFile.BaseStream.Seek(pixDataOffset, SeekOrigin.Begin); long reads = ;
for (int i = ; i < gdiImg.Height; i++)
{
for (int j = ; j < gdiImg.Width; j++)
{
if (reads >= pixDatalen)
break;
byte[] pixData = dicomFile.ReadBytes(dataLen / * colors);
reads += pixData.Length; Color c = Color.Empty;
if (colors == )
{
int grayGDI; double gray = BitConverter.ToUInt16(pixData, );
//调窗代码,就这么几句而已
//1先确定窗口范围 2映射到8位灰度
int grayStart = (windowCenter - windowWith / );
int grayEnd = (windowCenter + windowWith / ); if (gray < grayStart)
grayGDI = ;
else if (gray > grayEnd)
grayGDI = ;
else
{
grayGDI = (int)((gray - grayStart) * / windowWith);
} if (grayGDI > )
grayGDI = ;
else if (grayGDI < )
grayGDI = ;
c = Color.FromArgb(grayGDI, grayGDI, grayGDI);
}
else if (colors == )
{
c = Color.FromArgb(pixData[], pixData[], pixData[]);
} gdiImg.SetPixel(j, i, c);
}
} dicomFile.Close();
return true;
}
void tagRead()//不断读取所有tag 及其值 直到碰到图像数据 (7fe0 0010 )
{
bool enDir = false;
int leve = ;
StringBuilder folderData = new StringBuilder();//该死的文件夹标签
string folderTag = "";
while (dicomFile.BaseStream.Position + < dicomFile.BaseStream.Length)
{
//读取tag
string tag = dicomFile.ReadUInt16().ToString("x4") + "," +
dicomFile.ReadUInt16().ToString("x4"); string VR = string.Empty;
UInt32 Len = ;
//读取VR跟Len
//对OB OW SQ 要做特殊处理 先置两个字节0 然后4字节值长度
//------------------------------------------------------这些都是在读取VR一步被阻断的情况
if (tag.Substring(, ) == "")//文件头 特殊情况
{
VR = new string(dicomFile.ReadChars()); if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
{
dicomFile.BaseStream.Seek(, SeekOrigin.Current);
Len = dicomFile.ReadUInt32();
}
else
Len = dicomFile.ReadUInt16();
}
else if (tag == "fffe,e000" || tag == "fffe,e00d" || tag == "fffe,e0dd")//文件夹标签
{
VR = "**";
Len = dicomFile.ReadUInt32();
}
else if (isExplicitVR == true)//有无VR的情况
{
VR = new string(dicomFile.ReadChars()); if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
{
dicomFile.BaseStream.Seek(, SeekOrigin.Current);
Len = dicomFile.ReadUInt32();
}
else
Len = dicomFile.ReadUInt16();
}
else if (isExplicitVR == false)
{
VR = getVR(tag);//无显示VR时根据tag一个一个去找 真烦啊。
Len = dicomFile.ReadUInt32();
}
//判断是否应该读取VF 以何种方式读取VF
//-------------------------------------------------------这些都是在读取VF一步被阻断的情况
byte[] VF = { 0x00 }; if (tag == "7fe0,0010")//图像数据开始了
{
pixDatalen = Len;
pixDataOffset = dicomFile.BaseStream.Position;
dicomFile.BaseStream.Seek(Len, SeekOrigin.Current);
VR = "UL";
VF = BitConverter.GetBytes(Len);
}
else if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))//靠 遇到文件夹开始标签了
{
if (enDir == false)
{
enDir = true;
folderData.Remove(, folderData.Length);
folderTag = tag;
}
else
{
leve++;//VF不赋值
}
}
else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夹结束标签
{
if (enDir == true)
{
enDir = false;
}
else
{
leve--;
}
}
else
VF = dicomFile.ReadBytes((int)Len); string VFStr; VFStr = getVF(VR, VF); //----------------------------------------------------------------针对特殊的tag的值的处理
//特别针对文件头信息处理
if (tag == "0002,0000")
{
fileHeadLen = Len;
fileHeadOffset = dicomFile.BaseStream.Position;
}
else if (tag == "0002,0010")//传输语法 关系到后面的数据读取
{
switch (VFStr)
{
case "1.2.840.10008.1.2.1\0"://显示little
isLitteEndian = true;
isExplicitVR = true;
break;
case "1.2.840.10008.1.2.2\0"://显示big
isLitteEndian = false;
isExplicitVR = true;
break;
case "1.2.840.10008.1.2\0"://隐式little
isLitteEndian = true;
isExplicitVR = false;
break;
default:
break;
}
}
for (int i = ; i <= leve; i++)
tag = "--" + tag;
//------------------------------------数据搜集代码
if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > )//文件夹标签代码
{
folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
}
else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == )//文件夹结束标签
{
folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
tags.Add(folderTag + "SQ", folderData.ToString());
}
else
tags.Add(tag, "(" + VR + "):" + VFStr);
}
}
}
好了收工。
测试下成果
if (openFileDialog1.ShowDialog() != DialogResult.OK)
return; string fileName = openFileDialog1.FileName; handler = new DicomHandler(fileName); handler.readAndShow(textBox1); this.Text = "DicomViewer-" + openFileDialog1.FileName; backgroundWorker1.RunWorkerAsync();
这里处理gdi位图的时候直接用的setPix 处理速度比较慢所以用了backgroundWorker,实际应用中请使用内存缓冲跟指针的方式
否则效率低了是得不到客户的认可的哦,gdi位图操作可使用lockBits加指针的方式 ,12位的灰度像素数据可以第一次读取后缓存到内存中 以方便后面调窗的快速读取
优化这点代码也不难哈 对指针什么的熟点就行了,前几章都有。
这是ezDicom 经过公认测试的软件 我们来跟他对比一下,打开
调窗测试,我们注意到两个东西 在没有窗宽窗位时 默认窗宽是2047+1即2048 窗位是2048/2即1024
直观的感受是调窗宽像在调图像对比度 ,调窗位像在调图像亮度。
窗宽为255的时候图像是最瑞丽的 因为255其实就是8位图像的默认窗宽。
注意窗位那里有小小区别,ez窗位显示的是根据1024那里为0开始偏移 而我的程序是根据窗宽中间值没有偏移
没有偏移的情况稍微符合逻辑点吧。
但是可以看到原理是一样的 结果是一样的。
源码下载测试dcm文件: 猛击此处
另外本文的调窗代码是有问题的 升级版本请看《医学影像调窗技术》一文中的改进代码。
Dicom格式文件解析器的更多相关文章
- Dicom格式文件解析器[转]
Dicom格式文件解析器 Dicom全称是医学数字图像与通讯,这里讲的暂不涉及通讯那方面的问题 只讲*.dcm 也就是diocm格式文件的读取,读取本身是没啥难度的 无非就是字节码数据流处理.只不 ...
- dicom格式文件 界定标识符的处理
转自:http://www.cnblogs.com/assassinx/archive/2013/05/18/3084854.html 说到底无非几个事情 :1传输语法确定 2数据元素读取 3 7fe ...
- javap -- Java 类文件解析器
参考文档 http://blog.chinaunix.net/uid-692788-id-2681132.html http://docs.oracle.com/javase/7/docs/techn ...
- Atitit。Tree文件解析器的原理流程与设计实现 java c# php js
Atitit.Tree文件解析器的原理流程与设计实现 java c# php js 1. 解析原理与流程1 1.1. 判断目录 ,表示服 dirFlagChar = "└├─&quo ...
- MMIO----Wav格式文件解析
DirectSound只支持Wav格式的音频文件,在创建次缓冲区之前需要先确定播放的Wav音频数据的格式.如果是从本地Wav文件播放,则需要先读出它的数据格式. 1. Wav音频格式布局 Wav是WA ...
- C++PE文件格式解析类(轻松制作自己的PE文件解析器)
PE是Portable Executable File Format(可移植的运行体)简写,它是眼下Windows平台上的主流可运行文件格式. PE文件里包括的内容非常多,详细我就不在这解释了,有兴趣 ...
- springMVC:为MultipartFilte配置了上传文件解析器,报错或不能使用
一.问题描述为支持restful风格请求,并且应对可能上传文件的情况,需要在配置hiddenHttpMethodFilter过滤器之前配置MultipartFilter.目的是让MultipartFi ...
- ELF文件解析器支持x86x64ELF文件
此文为静态分析ELF文件结构,遍历其中Elf_Ehdr文件头信息,遍历Elf_Shdr节表头信息,并将所有节放置在左侧树控件上,遍历Elf_Phdr程序头也放置在左侧树控件上,并着重分析字符串表,重定 ...
- PE文件解析器的编写(二)——PE文件头的解析
之前在学习PE文件格式的时候,是通过自己查看各个结构,自己一步步计算各个成员在结构中的偏移,然后在计算出其在文件中的偏移,从而找到各个结构的值,但是在使用C语言编写这个工具的时候,就比这个方便的多,只 ...
随机推荐
- HTML之一字符集
ASCII字符集 ISO字符集 GBK等等. 1>首先,说一说为什么要设置html文件的字符集 如果不指定的话,浏览器会使用本地操作系统的字符集,那么,如果你的应用需要支持多国语言的话,就会有问 ...
- oracle数据迁移
通过这个文章演示一下Oracle的表空间迁移流程以及需要注意的诸多事项. 实验目标:将ora10g数据库实例上的表空间TBS_SEC_D迁移到secooler数据库实例上操作系统:Redhat 5.3 ...
- Android开发目录
1.ADT下载地址整理 2.之前的Android项目报错,新建Android项目报错,代码中找不到错误解决方案 3.错误“Unexpected namespace prefix "xmlns ...
- Linux下校验下载文件的完整性(MD5,SHA1,PGP)
查看: Linux下校验下载文件的完整性(MD5,SHA1,PGP) http://blog.useasp.net/archive/2014/03/29/use-md5-sha1-or-pgp-to- ...
- SQL Server数据库自增字段正确的插入值的描述
我们今天主要向大家讲述的是SQL Server数据库之向SQL Server自增字段正确的插入值的实际操作步骤,在一般的情况下,我们不能向 SQL Server 数据库自增字段中插入值,如果非要这么干 ...
- star
Astronomers often examine star maps where stars are represented by points on a plane and each star h ...
- ScriptTransformer
ScriptTransformer可能由java支持的JavaScript语言或者其他脚本语言写成的转换器.只支持Java 6以上版本. 例1: <dataConfig> <scri ...
- MySQL(25):事务的隔离级别出现问题之 不可重复读
1. 不可重复读 所谓的不可重复读(Non-Repeatable Read)是指事务中两次查询的结果不一致,原因是在查询的过程中其他事务做了更新的操作. 例如,银行在做统计报表的时候,第一次查询a账户 ...
- css中内容生成器
一,内容生成器:content 补充before和after伪类选择器: 1):将内容添加到某个选择器定义的单个或者多个元素的每一个实例之前或者之后 2)与before选择器配合使用(同理大家想下会不 ...
- oracle数据库例外处理与视图
pl/sql例外处理 例 当输入编号没有时的例外处理 declare --定义 v_ename emp.ename%type; begin -- select ename into v_ename f ...