zoukankan      html  css  js  c++  java
  • 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#值类型对应关系我都写好了:

     1 string getVF(string VR, byte[] VF)
     2 {
     3     string VFStr = string.Empty;
     4     switch (VR)
     5     {
     6         case "SS":
     7             VFStr = BitConverter.ToInt16(VF, 0).ToString();
     8             break;
     9         case "US":
    10             VFStr = BitConverter.ToUInt16(VF, 0).ToString();
    11 
    12             break;
    13         case "SL":
    14             VFStr = BitConverter.ToInt32(VF, 0).ToString();
    15 
    16             break;
    17         case "UL":
    18             VFStr = BitConverter.ToUInt32(VF, 0).ToString();
    19 
    20             break;
    21         case "AT":
    22             VFStr = BitConverter.ToUInt16(VF, 0).ToString();
    23 
    24             break;
    25         case "FL":
    26             VFStr = BitConverter.ToSingle(VF, 0).ToString();
    27 
    28             break;
    29         case "FD":
    30             VFStr = BitConverter.ToDouble(VF, 0).ToString();
    31 
    32             break;
    33         case "OB":
    34             VFStr = BitConverter.ToString(VF, 0);
    35             break;
    36         case "OW":
    37             VFStr = BitConverter.ToString(VF, 0);
    38             break;
    39         case "SQ":
    40             VFStr = BitConverter.ToString(VF, 0);
    41             break;
    42         case "OF":
    43             VFStr = BitConverter.ToString(VF, 0);
    44             break;
    45         case "UT":
    46             VFStr = BitConverter.ToString(VF, 0);
    47             break;
    48         case "UN":
    49             VFStr = Encoding.Default.GetString(VF);
    50             break;
    51         default:
    52             VFStr = Encoding.Default.GetString(VF);
    53             break;
    54     }
    55     return VFStr;
    56 }
    View Code

    找个dicom文件在十六进制编辑器下瞧瞧 给你整明白:

    所有dataElement从前到后按tag又可简单分段:

    文件元dataElement 不受传输语法影响 总是以显示VR方式表示  因为它里面就定义了传输语法
    普通dataElement 受传输语法影响 显示VR表示方式还是隐式VR表示方式
    像素数据dataElement 最重要也是最大的一个数据项 其实存储的就是图像数据

    几个特殊的tag很重要 前面说过了tag就是dicom里定义的字典。文件元dataElement 和跟像素数据相关的dataElement 都很重要,其他的很多 如果全部照顾完的话估计得写上千行switch语句吧,所以没有必要一般我们一般只抓取关键的tag。并且在隐式语法下要确定VR也必须根据字典来确定
    关键的tag如下:

      1 string getVR(string tag)
      2 {
      3     switch (tag)
      4     {
      5         case "0002,0000"://文件元信息长度
      6             return "UL";
      7             break;
      8         case "0002,0010"://传输语法
      9             return "UI";
     10             break;
     11         case "0002,0013"://文件生成程序的标题
     12             return "SH";
     13             break;
     14         case "0008,0005"://文本编码
     15             return "CS";
     16             break;
     17         case "0008,0008":
     18             return "CS";
     19             break;
     20         case "0008,1032"://成像时间
     21             return "SQ";
     22             break;
     23         case "0008,1111":
     24             return "SQ";
     25             break;
     26         case "0008,0020"://检查日期
     27             return "DA";
     28             break;
     29         case "0008,0060"://成像仪器
     30             return "CS";
     31             break;
     32         case "0008,0070"://成像仪厂商
     33             return "LO";
     34             break;
     35         case "0008,0080":
     36             return "LO";
     37             break;
     38         case "0010,0010"://病人姓名
     39             return "PN";
     40             break;
     41         case "0010,0020"://病人id
     42             return "LO";
     43             break;
     44         case "0010,0030"://病人生日
     45             return "DA";
     46             break;
     47         case "0018,0060"://电压
     48             return "DS";
     49             break;
     50         case "0018,1030"://协议名
     51             return "LO";
     52             break;
     53         case "0018,1151":
     54             return "IS";
     55             break;
     56         case "0020,0010"://检查ID
     57             return "SH";
     58             break;
     59         case "0020,0011"://序列
     60             return "IS";
     61             break;
     62         case "0020,0012"://成像编号
     63             return "IS";
     64             break;
     65         case "0020,0013"://影像编号
     66             return "IS";
     67             break;
     68         case "0028,0002"://像素采样1为灰度3为彩色
     69             return "US";
     70             break;
     71         case "0028,0004"://图像模式MONOCHROME2为灰度
     72             return "CS";
     73             break;
     74         case "0028,0010"://row高
     75             return "US";
     76             break;
     77         case "0028,0011"://col宽
     78             return "US";
     79             break;
     80         case "0028,0100"://单个采样数据长度
     81             return "US";
     82             break;
     83         case "0028,0101"://实际长度
     84             return "US";
     85             break;
     86         case "0028,0102"://采样最大值
     87             return "US";
     88             break;
     89         case "0028,1050"://窗位
     90             return "DS";
     91             break;
     92         case "0028,1051"://窗宽
     93             return "DS";
     94             break;
     95         case "0028,1052":
     96             return "DS";
     97             break;
     98         case "0028,1053":
     99             return "DS";
    100             break;
    101         case "0040,0008"://文件夹标签
    102             return "SQ";
    103             break;
    104         case "0040,0260"://文件夹标签
    105             return "SQ";
    106             break;
    107         case "0040,0275"://文件夹标签
    108             return "SQ";
    109             break;
    110         case "7fe0,0010"://像素数据开始处
    111             return "OW";
    112             break;
    113         default:
    114             return "UN";
    115             break;
    116     }
    117 }
    View Code

    最关键的两个tag:
    0002,0010
    普通tag的读取方式 little字节序还是big字节序  隐式VR还是显示VR。由它的值决定

     1 switch (VFStr)
     2 {
     3     case "1.2.840.10008.1.2.1"://显示little
     4         isLitteEndian = true;
     5         isExplicitVR = true;
     6         break;
     7     case "1.2.840.10008.1.2.2"://显示big
     8         isLitteEndian = false;
     9         isExplicitVR = true;
    10         break;
    11     case "1.2.840.10008.1.2"://隐式little
    12         isLitteEndian = true;
    13         isExplicitVR = false;
    14         break;
    15     default:
    16         break;
    17 }
    View Code

    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 原理不多讲 等比中项十字相乘法 这个是初中的知识哈。初中没读过的童鞋飘过。。。

    代码走起

      1 class DicomHandler
      2     {
      3         string fileName = "";
      4         Dictionary<string, string> tags = new Dictionary<string, string>();//dicom文件中的标签
      5         BinaryReader dicomFile;//dicom文件流
      6 
      7         //文件元信息
      8         public Bitmap gdiImg;//转换后的gdi图像
      9         UInt32 fileHeadLen;//文件头长度
     10         long fileHeadOffset;//文件数据开始位置
     11         UInt32 pixDatalen;//像素数据长度
     12         long pixDataOffset = 0;//像素数据开始位置
     13         bool isLitteEndian = true;//是否小字节序(小端在前 、大端在前)
     14         bool isExplicitVR = true;//有无VR
     15 
     16         //像素信息
     17         int colors;//颜色数 RGB为3 黑白为1
     18         public int windowWith = 2048, windowCenter = 2048 / 2;//窗宽窗位
     19         int rows, cols;
     20         public void readAndShow(TextBox textBox1)
     21         {
     22             if (fileName == string.Empty)
     23                 return;
     24             dicomFile = new BinaryReader(File.OpenRead(fileName));
     25 
     26             //跳过128字节导言部分
     27             dicomFile.BaseStream.Seek(128, SeekOrigin.Begin);
     28 
     29             if (new string(dicomFile.ReadChars(4)) != "DICM")
     30             {
     31                 MessageBox.Show("没有dicom标识头,文件格式错误");
     32                 return;
     33             }
     34 
     35 
     36             tagRead();
     37 
     38             IDictionaryEnumerator enor = tags.GetEnumerator();
     39             while (enor.MoveNext())
     40             {
     41                 if (enor.Key.ToString().Length > 9)
     42                 {
     43                     textBox1.Text += enor.Key.ToString() + "
    ";
     44                     textBox1.Text += enor.Value.ToString().Replace('', ' ');
     45                 }
     46                 else
     47                     textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('', ' ') + "
    ";
     48             }
     49             dicomFile.Close();
     50         }
     51         public  DicomHandler(string _filename)
     52         {
     53             fileName = _filename;
     54         }
     55 
     56         public void saveAs(string filename)
     57         {
     58             switch (filename.Substring(filename.LastIndexOf('.')))
     59             {
     60                 case ".jpg":
     61                     gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);
     62                     break;
     63                 case ".bmp":
     64                     gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);
     65                     break;
     66                 case ".png":
     67                     gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
     68                     break;
     69                 default:
     70                     break;
     71             }
     72         }
     73         public bool getImg( )//获取图像 在图像数据偏移量已经确定的情况下
     74         {
     75             if (fileName == string.Empty)
     76                 return false;
     77             
     78             int dataLen, validLen;//数据长度 有效位
     79             int imgNum;//帧数
     80 
     81             rows = int.Parse(tags["0028,0010"].Substring(5));
     82             cols = int.Parse(tags["0028,0011"].Substring(5));
     83 
     84             colors = int.Parse(tags["0028,0002"].Substring(5));
     85             dataLen = int.Parse(tags["0028,0100"].Substring(5));
     86             validLen = int.Parse(tags["0028,0101"].Substring(5));
     87 
     88             gdiImg = new Bitmap(cols, rows);
     89 
     90             BinaryReader dicomFile = new BinaryReader(File.OpenRead(fileName));
     91 
     92             dicomFile.BaseStream.Seek(pixDataOffset, SeekOrigin.Begin);
     93 
     94             long reads = 0;
     95             for (int i = 0; i < gdiImg.Height; i++)
     96             {
     97                 for (int j = 0; j < gdiImg.Width; j++)
     98                 {
     99                     if (reads >= pixDatalen)
    100                         break;
    101                     byte[] pixData = dicomFile.ReadBytes(dataLen / 8 * colors);
    102                     reads += pixData.Length;
    103 
    104                     Color c = Color.Empty;
    105                     if (colors == 1)
    106                     {
    107                         int grayGDI;
    108 
    109                         double gray = BitConverter.ToUInt16(pixData, 0);
    110                         //调窗代码,就这么几句而已 
    111                         //1先确定窗口范围 2映射到8位灰度
    112                         int grayStart = (windowCenter - windowWith / 2);
    113                         int grayEnd = (windowCenter + windowWith / 2);
    114 
    115                         if (gray < grayStart)
    116                             grayGDI = 0;
    117                         else if (gray > grayEnd)
    118                             grayGDI = 255;
    119                         else
    120                         {
    121                             grayGDI = (int)((gray - grayStart) * 255 / windowWith);
    122                         }
    123 
    124                         if (grayGDI > 255)
    125                             grayGDI = 255;
    126                         else if (grayGDI < 0)
    127                             grayGDI = 0;
    128                         c = Color.FromArgb(grayGDI, grayGDI, grayGDI);
    129                     }
    130                     else if (colors == 3)
    131                     {
    132                         c = Color.FromArgb(pixData[0], pixData[1], pixData[2]);
    133                     }
    134 
    135                     gdiImg.SetPixel(j, i, c);
    136                 }
    137             }
    138 
    139             dicomFile.Close();
    140             return true;
    141         }
    142         void tagRead()//不断读取所有tag 及其值 直到碰到图像数据 (7fe0 0010 )
    143         {
    144             bool enDir = false;
    145             int leve = 0;
    146             StringBuilder folderData = new StringBuilder();//该死的文件夹标签
    147             string folderTag = "";
    148             while (dicomFile.BaseStream.Position + 6 < dicomFile.BaseStream.Length)
    149             {
    150                 //读取tag
    151                 string tag = dicomFile.ReadUInt16().ToString("x4") + "," +
    152                 dicomFile.ReadUInt16().ToString("x4");
    153 
    154                 string VR = string.Empty;
    155                 UInt32 Len = 0;
    156                 //读取VR跟Len
    157                 //对OB OW SQ 要做特殊处理 先置两个字节0 然后4字节值长度
    158                 //------------------------------------------------------这些都是在读取VR一步被阻断的情况
    159                 if (tag.Substring(0, 4) == "0002")//文件头 特殊情况
    160                 {
    161                     VR = new string(dicomFile.ReadChars(2));
    162 
    163                     if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
    164                     {
    165                         dicomFile.BaseStream.Seek(2, SeekOrigin.Current);
    166                         Len = dicomFile.ReadUInt32();
    167                     }
    168                     else
    169                         Len = dicomFile.ReadUInt16();
    170                 }
    171                 else if (tag == "fffe,e000" || tag == "fffe,e00d" || tag == "fffe,e0dd")//文件夹标签
    172                 {
    173                     VR = "**";
    174                     Len = dicomFile.ReadUInt32();
    175                 }
    176                 else if (isExplicitVR == true)//有无VR的情况
    177                 {
    178                     VR = new string(dicomFile.ReadChars(2));
    179 
    180                     if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN")
    181                     {
    182                         dicomFile.BaseStream.Seek(2, SeekOrigin.Current);
    183                         Len = dicomFile.ReadUInt32();
    184                     }
    185                     else
    186                         Len = dicomFile.ReadUInt16();
    187                 }
    188                 else if (isExplicitVR == false)
    189                 {
    190                     VR = getVR(tag);//无显示VR时根据tag一个一个去找 真烦啊。
    191                     Len = dicomFile.ReadUInt32();
    192                 }
    193                 //判断是否应该读取VF 以何种方式读取VF
    194                 //-------------------------------------------------------这些都是在读取VF一步被阻断的情况
    195                 byte[] VF = { 0x00 };
    196 
    197                 if (tag == "7fe0,0010")//图像数据开始了
    198                 {
    199                     pixDatalen = Len;
    200                     pixDataOffset = dicomFile.BaseStream.Position;
    201                     dicomFile.BaseStream.Seek(Len, SeekOrigin.Current);
    202                     VR = "UL";
    203                     VF = BitConverter.GetBytes(Len);
    204                 }
    205                 else if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))//靠 遇到文件夹开始标签了
    206                 {
    207                     if (enDir == false)
    208                     {
    209                         enDir = true;
    210                         folderData.Remove(0, folderData.Length);
    211                         folderTag = tag;
    212                     }
    213                     else
    214                     {
    215                         leve++;//VF不赋值
    216                     }
    217                 }
    218                 else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夹结束标签
    219                 {
    220                     if (enDir == true)
    221                     {
    222                         enDir = false;
    223                     }
    224                     else
    225                     {
    226                         leve--;
    227                     }
    228                 }
    229                 else
    230                     VF = dicomFile.ReadBytes((int)Len);
    231 
    232                 string VFStr;
    233 
    234                 VFStr = getVF(VR, VF);
    235 
    236                 //----------------------------------------------------------------针对特殊的tag的值的处理
    237                 //特别针对文件头信息处理
    238                 if (tag == "0002,0000")
    239                 {
    240                     fileHeadLen = Len;
    241                     fileHeadOffset = dicomFile.BaseStream.Position;
    242                 }
    243                 else if (tag == "0002,0010")//传输语法 关系到后面的数据读取
    244                 {
    245                     switch (VFStr)
    246                     {
    247                         case "1.2.840.10008.1.2.1"://显示little
    248                             isLitteEndian = true;
    249                             isExplicitVR = true;
    250                             break;
    251                         case "1.2.840.10008.1.2.2"://显示big
    252                             isLitteEndian = false;
    253                             isExplicitVR = true;
    254                             break;
    255                         case "1.2.840.10008.1.2"://隐式little
    256                             isLitteEndian = true;
    257                             isExplicitVR = false;
    258                             break;
    259                         default:
    260                             break;
    261                     }
    262                 }
    263                 for (int i = 1; i <= leve; i++)
    264                     tag = "--" + tag;
    265                 //------------------------------------数据搜集代码
    266                 if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > 0)//文件夹标签代码
    267                 {
    268                     folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
    269                 }
    270                 else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == 0)//文件夹结束标签
    271                 {
    272                     folderData.AppendLine(tag + "(" + VR + "):" + VFStr);
    273                     tags.Add(folderTag + "SQ", folderData.ToString());
    274                 }
    275                 else
    276                     tags.Add(tag, "(" + VR + "):" + VFStr);
    277             }
    278         }
    279 }
    View Code

    好了收工。
    测试下成果

     1 if (openFileDialog1.ShowDialog() != DialogResult.OK)
     2     return;
     3 
     4 string fileName = openFileDialog1.FileName;
     5 
     6 handler = new DicomHandler(fileName);
     7 
     8 handler.readAndShow(textBox1);
     9 
    10 this.Text = "DicomViewer-" + openFileDialog1.FileName;
    11 
    12 
    13 backgroundWorker1.RunWorkerAsync();
    View Code

    这里处理gdi位图的时候直接用的setPix 处理速度比较慢所以用了backgroundWorker,实际应用中请使用内存缓冲跟指针的方式
    否则效率低了是得不到客户的认可的哦,gdi位图操作可使用lockBits加指针的方式 ,12位的灰度像素数据可以第一次读取后缓存到内存中 以方便后面调窗的快速读取
    优化这点代码也不难哈 对指针什么的熟点就行了,前几章都有。

    这是ezDicom 经过公认测试的软件 我们来跟他对比一下,打开 
    调窗测试,我们注意到两个东西 在没有窗宽窗位时 默认窗宽是2047+1即2048  窗位是2048/2即1024
    直观的感受是调窗宽像在调图像对比度 ,调窗位像在调图像亮度。
    窗宽为255的时候图像是最瑞丽的 因为255其实就是8位图像的默认窗宽。
    注意窗位那里有小小区别,ez窗位显示的是根据1024那里为0开始偏移 而我的程序是根据窗宽中间值没有偏移
    没有偏移的情况稍微符合逻辑点吧。
    但是可以看到原理是一样的 结果是一样的。


    源码下载测试dcm文件: 猛击此处
    另外本文的调窗代码是有问题的 升级版本请看《医学影像调窗技术》一文中的改进代码。

    人生,总是有一些空城旧事,年华未央;总是有些季节,一季花凉,满地忧伤。许多事,看开了,便会峰回路转;许多梦,看淡了,便会云开日出。学会思索,学会珍藏,微笑领悟,默默坚强。
  • 相关阅读:
    Oracle执行查询报错ORA-01034: ORACLE not available
    shell echo单行和多行文字定向写入到文件中
    oracle查看EM管理器状态显示Environment variable ORACLE_UNQNAME not defined. Please set ORACLE_UNQNAME to database unique name.
    正确使用 Element $confirm 方法,联调删除接口
    Express 统一配置响应头 header 方法
    jquery.i18n.properties.js 实现多语言
    navicat导出数据结构到word
    RabbitMQ用户角色及权限控制
    chrome谷歌浏览器自动填充用户名密码错位
    JVM性能调优参数整理
  • 原文地址:https://www.cnblogs.com/yuzhou133/p/4686489.html
Copyright © 2011-2022 走看看