浅谈如何使用代码为MP3文件写入ID3Tags

作者:郑童宇
GitHub:https://github.com/CrazyZty

1.前言

做了三年左右的Android开发,一直没写过博客,最近正好打算换工作,算是闲一些,就将以前开发所遇到的一些有趣的问题及解决方法写出来,以供他人借鉴。
  MP3作为音频领域应用最广的压缩方式之一想必大家都有所了解,本篇博文探究的不是MP3著名的压缩方式,而是如何为MP3文件写入ID3Tags。写下本博文的原因是类似的博文太少,以前做这方面的时候,google出来的大多是使用某一软件或库来进行处理,少许涉及代码的基本都是读取标签信息,或写入ID3v1标签,更糟糕的是网上大多数对ID3v2标签的读取是存在问题,以至于当我试图借鉴读取标签的博文来反向为MP3文件写入ID3v2标签时一直出现标签信息乱码的情况,直到google出一篇文章才得以解决。
  本篇博文以Java作为示例语言,以Android作为示例平台。

2.ID3标签

2.1.ID3v1标签

ID3v1是一个很方便写入和解析的标签,维基百科上对ID3v1的解释是"v1版的ID3在MP3文件的末尾128字节,以TAG三个字符开头,后面跟上歌曲信息"
  通过http://bbs.pfan.cn/post-372622.html,我们可以获取128个字节的具体信息:

名称 字节 说明
Tag 3 ID3V1标识符“TAG”的Ascii码
Title 30 歌曲名
Artist 30 歌手名
Album 30 专辑名
Year 4 日期信息
Comment 28 注释信息,有时为30字节
Reserved 1 =0说明有音轨,下一字节就是音轨;≠0表示注释是30个字节
Track 1 音轨(字节型数值),歌曲在专辑里的序号
Genre 1 歌曲风格(字节型数值)

看到这个数据结构,想必大家已经很清楚如何写入ID3v1标签了,下面我就直接贴出代码,并对代码中部分模块进行解释。

    public static void StorageMusicFileWithID3V1Tag(File sourceFile, String musicFilePath,
                                                    String songName, String artistName,
                                                    String albumName) {
        try {
            sourceFile.renameTo(new File(musicFilePath));

            RandomAccessFile musicRandomAccessFile = new RandomAccessFile(musicFilePath, "rw");
            musicRandomAccessFile.seek(musicRandomAccessFile.length() - 128); // 跳到ID3V1开始的位置

            byte[] tag = new byte[3];
            musicRandomAccessFile.read(tag);

            if (new String(tag).equals("TAG")) {
                return;
            }

            byte[] tagByteArray = new byte[128];

            musicRandomAccessFile.seek(musicRandomAccessFile.length());

            byte[] songNameByteArray = songName.getBytes("GBK");
            byte[] artistNameByteArray = artistName.getBytes("GBK");
            byte[] albumNameByteArray = albumName.getBytes("GBK");

            int songNameByteArrayLength = songNameByteArray.length;
            int artistNameByteArrayLength = artistNameByteArray.length;
            int albumNameByteArrayLength = albumNameByteArray.length;

            songNameByteArrayLength = songNameByteArrayLength > 30 ? 30 : songNameByteArrayLength;
            artistNameByteArrayLength =
                    artistNameByteArrayLength > 30 ? 30 : artistNameByteArrayLength;
            albumNameByteArrayLength =
                    albumNameByteArrayLength > 30 ? 30 : albumNameByteArrayLength;

            System.arraycopy("TAG".getBytes(), 0, tagByteArray, 0, 3);
            System.arraycopy(songNameByteArray, 0, tagByteArray, 3, songNameByteArrayLength);
            System.arraycopy(artistNameByteArray, 0, tagByteArray, 33, artistNameByteArrayLength);
            System.arraycopy(albumNameByteArray, 0, tagByteArray, 63, albumNameByteArrayLength);

            tagByteArray[127] = (byte) 0xFF; // 将流派显示为指定音乐的流派

            musicRandomAccessFile.write(tagByteArray);
        } catch (Exception e) {
            LogFunction.error("写入音乐标签异常", e);
        }
    }

代码很简单,另外对于中文电脑而言,解析ID3v1中的标签是按照GBK的格式来的,所以在这里要注意一下转换字节的格式,当然如果要标签写入的是除了非亚洲语言的话那就按照对应的字符编码来修改代码即可,除此以外没有复杂的地方。
  不过从上面代码我们可以很明显看出ID3v1标签的缺点,一是可以保存的信息比较少,二是如果歌曲的信息过长,比如歌名过长那就无法完整存储的歌名,如何解决这一问题呢,那就是使用ID3v2标签。

2.2.ID3v2标签

再次借鉴于http://bbs.pfan.cn/post-372622.html,我们可以得到ID3v2标签的基本信息。
  ID3V2 与 ID3V1 的作用差不多,也是记录 MP3 的有关信息,但 ID3V2 的结构比 ID3V1 要复杂得多,而且可以伸缩和扩展。ID3V2 到现在一共有 4 个版本,但流行的播放软件一般只支持第 3 版,既ID3V2.3。由于ID3V1记录在 MP3 文件的末尾,ID3V2 就只好记录在 MP3 文件的首部了。
  每个 ID3V2.3 的标签都一个标签头和若干个标签帧或一个扩展标签头组成。歌曲的信息如标题、作者等都存放在不同的标签帧中,扩展标签头和标签帧并不是必要的,但每个标签至少要有一个标签帧。对于 VB 爱好者来说,你可以把 ID3V2 看作是一个对象,而把标签帧看作是 ID3V2 的一个属性,那么,标签帧的标识符就可以看作是属性名了。
  一首MP3如果有ID3V2.3的话,那么ID3V2.3的标签头占用文件最前面的10个字节,其数据结构如下:
  1.ID3V2.3标签头结构:

名称 字节 说明
Header 3 ID3V2.3标识符"ID3"的Ascii码,否则认为没有ID3V2.3
Ver 1 版本号,=03
Revision 1 副版本号,=00
flag 1 标志字节,一般没意义,=00
Size 4 标签内容长度,高位在前,不包括标签头的10个字节

2.ID3V2标签帧
  标签内容由若干个标签帧组成。每个标签帧都由一个10个字节的帧头和至少 1个字节的不固定长度的帧内容组成,它们顺序存放在文件中。
  每个帧都由帧头和帧内容组成,数据结构如下:

名称 字节 说明
FrameID 4 帧标识符的Ascii码
Size 4 帧内容及编码方式的合计长度,高位在前
Flags 2 标志,只使用了6位,详见表6,一般均=0
encode 4 帧内容所用的编码方式。许多帧没有此项(注意,这一部分作者写错了,文档上事实并没有此属性,因为其他的文章关于标签帧的描述都没有提及此属性,故而我专门去了http://id3.org/id3v2.3.0进行考证,最后在文章3.3.ID3v2 frame overview中可以看到ID3V2标签帧并没有此内容)
帧内容 至少1个字节

基本的标签格式内容展示这些就足够了,更多的扩展内容我就不放到这里了,现在上代码。

    public static void StorageMusicFileWithID3V2Tag(File sourceFile, String musicFilePath,
                                                    String songName, String artistName,
                                                    String albumName) {
        try {
            RandomAccessFile musicRandomAccessFile =
                    new RandomAccessFile(sourceFile.getAbsolutePath(), "rw");
            musicRandomAccessFile.seek(0);

            byte[] tag = new byte[3];
            musicRandomAccessFile.read(tag);

            if (new String(tag).equals("ID3")) {
                sourceFile.renameTo(new File(musicFilePath));
                return;
            }
        } catch (Exception e) {
            LogFunction.error("存储音乐文件异常", e);
        }

        try {
            byte[] encodeByte = {3}; // 03 表示的UTF8编码
            byte[] tagByteArray;
            byte[] tagHeadByteArray;
            byte[] tagFrameHeadByteArray;

            byte[] songNameByteArray = songName.getBytes("UTF-8");
            byte[] artistNameByteArray = artistName.getBytes("UTF-8");
            byte[] albumNameByteArray = albumName.getBytes("UTF-8");

            final int tagHeadLength = 10;
            final int tagFrameHeadLength = 10;
            final int tagFrameEncodeLength = 1;
            final int tagFillByteLength = 20; // 这个填充字节是我看到其他MP3文件ID3标签都会在尾端添加的数据,为了保险起见我也加上了

            int byteArrayOffset = 0;
            int songNameByteArrayLength = songNameByteArray.length;
            int artistNameByteArrayLength = artistNameByteArray.length;
            int albumNameByteArrayLength = albumNameByteArray.length;
            int songNameFrameTotalLength = songNameByteArrayLength + tagFrameEncodeLength;
            int artistNameFrameTotalLength = artistNameByteArrayLength + tagFrameEncodeLength;
            int albumNameFrameTotalLength = albumNameByteArrayLength + tagFrameEncodeLength;

            int totalTagLength = tagHeadLength + tagFrameHeadLength + songNameByteArrayLength +
                    tagFrameHeadLength + artistNameByteArrayLength +
                    tagFrameHeadLength + albumNameByteArrayLength +
                    tagFillByteLength;
            int tagContentLength = totalTagLength - tagHeadLength;

            tagByteArray = new byte[totalTagLength];

            tagHeadByteArray = new byte[tagHeadLength];
            System.arraycopy("ID3".getBytes(), 0, tagHeadByteArray, 0, 3);
            tagHeadByteArray[3] = 3;
            tagHeadByteArray[4] = 0;
            tagHeadByteArray[5] = 0;
            tagHeadByteArray[6] = (byte) ((tagContentLength >> 7 >> 7 >> 7) % 128);
            tagHeadByteArray[7] = (byte) ((tagContentLength >> 7 >> 7) % 128);
            tagHeadByteArray[8] = (byte) ((tagContentLength >> 7) % 128);
            tagHeadByteArray[9] = (byte) (tagContentLength % 128);
            System.arraycopy(tagHeadByteArray, 0, tagByteArray, byteArrayOffset,
                    tagHeadLength);
            byteArrayOffset += tagHeadLength;

            tagFrameHeadByteArray = new byte[tagFrameHeadLength];
            System.arraycopy("TIT2".getBytes(), 0, tagFrameHeadByteArray, 0, 4);
            tagFrameHeadByteArray[4] = (byte) ((songNameFrameTotalLength >> 8 >> 8 >> 8) % 256);
            tagFrameHeadByteArray[5] = (byte) ((songNameFrameTotalLength >> 8 >> 8) % 256);
            tagFrameHeadByteArray[6] = (byte) ((songNameFrameTotalLength >> 8) % 256);
            tagFrameHeadByteArray[7] = (byte) (songNameFrameTotalLength % 256);
            tagFrameHeadByteArray[8] = 0;
            tagFrameHeadByteArray[9] = 0;
            System.arraycopy(tagFrameHeadByteArray, 0, tagByteArray, byteArrayOffset, tagFrameHeadLength);
            byteArrayOffset += tagFrameHeadLength;
            System.arraycopy(encodeByte, 0, tagByteArray, byteArrayOffset, tagFrameEncodeLength);
            byteArrayOffset += tagFrameEncodeLength;
            System.arraycopy(songNameByteArray, 0, tagByteArray, byteArrayOffset,
                    songNameByteArrayLength);
            byteArrayOffset += songNameByteArrayLength;

            tagFrameHeadByteArray = new byte[tagFrameHeadLength];
            System.arraycopy("TPE1".getBytes(), 0, tagFrameHeadByteArray, 0, 4);
            tagFrameHeadByteArray[4] = (byte) ((artistNameFrameTotalLength >> 8 >> 8 >> 8) % 256);
            tagFrameHeadByteArray[5] = (byte) ((artistNameFrameTotalLength >> 8 >> 8) % 256);
            tagFrameHeadByteArray[6] = (byte) ((artistNameFrameTotalLength >> 8) % 256);
            tagFrameHeadByteArray[7] = (byte) (artistNameFrameTotalLength % 256);
            tagFrameHeadByteArray[8] = 0;
            tagFrameHeadByteArray[9] = 0;
            System.arraycopy(tagFrameHeadByteArray, 0, tagByteArray, byteArrayOffset, tagFrameHeadLength);
            byteArrayOffset += tagFrameHeadLength;
            System.arraycopy(encodeByte, 0, tagByteArray, byteArrayOffset, tagFrameEncodeLength);
            byteArrayOffset += tagFrameEncodeLength;
            System.arraycopy(artistNameByteArray, 0, tagByteArray, byteArrayOffset,
                    artistNameByteArrayLength);
            byteArrayOffset += artistNameByteArrayLength;

            tagFrameHeadByteArray = new byte[tagFrameHeadLength];
            System.arraycopy("TALB".getBytes(), 0, tagFrameHeadByteArray, 0, 4);
            tagFrameHeadByteArray[4] = (byte) ((albumNameFrameTotalLength >> 8 >> 8 >> 8) % 256);
            tagFrameHeadByteArray[5] = (byte) ((albumNameFrameTotalLength >> 8 >> 8) % 256);
            tagFrameHeadByteArray[6] = (byte) ((albumNameFrameTotalLength >> 8) % 256);
            tagFrameHeadByteArray[7] = (byte) (albumNameFrameTotalLength % 256);
            tagFrameHeadByteArray[8] = 0;
            tagFrameHeadByteArray[9] = 0;
            System.arraycopy(tagFrameHeadByteArray, 0, tagByteArray, byteArrayOffset, tagFrameHeadLength);
            byteArrayOffset += tagFrameHeadLength;
            System.arraycopy(encodeByte, 0, tagByteArray, byteArrayOffset, tagFrameEncodeLength);
            byteArrayOffset += tagFrameEncodeLength;
            System.arraycopy(albumNameByteArray, 0, tagByteArray, byteArrayOffset,
                    albumNameByteArrayLength);

            byte[] dataByteBuffer = new byte[1024];

            FileInputStream fileInputStream = new FileInputStream(sourceFile);
            FileOutputStream fileOutputStream =
                    FileFunction.GetFileOutputStreamFromFile(musicFilePath);

            fileOutputStream.write(tagByteArray);

            while (fileInputStream.read(dataByteBuffer) > 0) {
                fileOutputStream.write(dataByteBuffer);
            }

            fileOutputStream.close();
            fileInputStream.close();

            FileFunction.DeleteFile(sourceFile.getAbsolutePath());
        } catch (Exception e) {
            LogFunction.error("写入音乐标签异常", e);
        }
    }

关于"TIT2","TPE1","TALB"分别代表什么意思,还有帧头的计算,我就不过多描述了,这些通过阅读资料都很容易理解。
  然而有一个地方需要注意,同时这也是我认为的ID3v2标签中的一个大坑,可以跳到代码的74行看到这么一段代码: System.arraycopy(encodeByte, 0, tagByteArray, byteArrayOffset, tagEncodeLength);,同时这段代码也在代码的90行和106行出现,这段代码十分重要,原因可以参考http://blog.csdn.net/chenmeimei_8899/article/details/3901948。核心内容如下:
  "关于MP3 tag的资料很多,大家可以自己去找,大概讲的都是大同小异,但是我觉得他们忘了很重要的一点,也是我忽略的,后来解析文件出错了,才恍然大悟。那就是tag信息的编码。
  大家都知道每个frame的头是由10个字节组成的,具体内容我就不仔细写了,在着10个字节后有一个字节,大家可以仔细观察一下,只有四种情况 00 ,01,02,03,这个代表什么呢?这个就是表示tag的编码方式的。00代表的就是ISO-8859-1,01代表的就是UTF-16编码,02代表 UTF16BE,03表示的UTF8编码,在tag信息是不采用这种表明,但是这种编码在tag里不是错的。"
  还记得我上面关于ID3v2标签信息标红的那一段吗,对就是encode那个属性,咦,你这会肯定好奇了,我明明说了文档没有,但现在在代码中却加了上去,为什么呢?
  事实是http://id3.org/id3v2.3.0的3.3.ID3v2 frame overview中有这么一段:
  "A tag must contain at least one frame. A frame must be at least 1 byte big, excluding the header.
  If nothing else is said a string is represented as ISO-8859-1 characters in the range $20 - $FF. Such strings are represented as <text string>, or <full text string> if newlines are allowed, in the frame descriptions. All Unicode strings use 16-bit unicode 2.0 (ISO/IEC 10646-1:1993, UCS-2). Unicode strings must begin with the Unicode BOM ($FF FE or $FE FF) to identify the byte order.
  All numeric strings and URLs are always encoded as ISO-8859-1. Terminated strings are terminated with $00 if encoded with ISO-8859-1 and $00 00 if encoded as unicode. If nothing else is said newline character is forbidden. In ISO-8859-1 a new line is represented, when allowed, with $0A only. Frames that allow different types of text encoding have a text encoding description byte directly after the frame size. If ISO-8859-1 is used this byte should be $00, if Unicode is used it should be $01. Strings dependent on encoding is represented as <text string according to encoding>, or <full text string according to encoding> if newlines are allowed. Any empty Unicode strings which are NULL-terminated may have the Unicode BOM followed by a Unicode NULL ($FF FE 00 00 or $FE FF 00 00).“
  我的理解是这样的,一个标签至少有一帧,一帧在除去帧头后至少有一字节,就是前面描述的数据帧中的帧内容,而这个帧内容就实现了encode的功能,不过我翻了数次3.3.ID3v2 frame overview我依旧没找到"01代表的就是UTF-16编码,02代表 UTF16BE,03表示的UTF8编码"这样的细分,不知道是不是后面默认的规则,还是别的原因,如果有朋友知道请务必告诉我。另外还有个很奇怪的地方,我用HexEditor2读取的网上下载的MP3文件中,它们的ID3v2头中除了标签头和标签帧以外后面都是会有很长一段空白数据,我没有输出这段空白数据也没什么影响,不会影响标签解析,但为什么会有这么一段数据,我是没有找到原因,如果有知道的朋友请务必告诉我。
  好了,回到代码中,我在给标签赋的数据是UTF8编码,所以将帧头后的第一字节赋值为03,然后我们就可以顺利的随意写入指定的信息了。
  当然肯定会有朋友问我,如果我们不写入这个字节会如何,那我就微笑着回答你吧“:会乱码,会有让你反复观察代码,反复参考网上的文档,抱着我明明没错但为什么解决不了的绝望心情却也解决不了的乱码“,诶,好像若有若无的透露出一些不该透露的信息呢,呵呵。
  ps:如果要使用我的代码,请注意一下目前的功能只会为没有对应tag信息的MP3文件写入标签信息,不会覆盖已有的tag,这是当时的需求决定的,后期我会考虑在GitHub上完善,当然现在有这样需求的同学就请自己写啦。另外我的代码只写入了歌名,歌手名,专辑名这三个标签,如果有比如说需要写入图片之类的朋友完全可以参照我的写入代码进行扩展,毕竟原理是相同的。

3.总结

编写ID3标签写入这一模块真是段很有趣的经历,最开始的ID3v1标签实现起来十分简单,但考虑到ID3v1标签局限性太强了,就直接转ID3v2标签,本来信心满满,但一上手才发现两个的难度差别极大,前面倒好,数据的写入与帧头的计算还算简单,但写出文件后,文件信息永远是乱码,反复在网上翻找文档,一直找不到解决方法。
  因为写入标签只是我临时起意,不属于公司硬性需求,所以数小时没解决后,我都打算放弃,但心里实在是不服,我干脆在网上下了新的音乐文件,用HexEditor2将下载的文件和我写入标签的音乐文件在字节层次上进行对比,同时手动通过HexEditor2来修改音乐文件信息来观察标签信息改变情况,最后发现正常的音乐文件和我自己写出的文件之间差了一个字节,其实算我运气好,因为在到这个文件前以及比对了别的文件,但当时前面几组正常音乐文件的编码不是UTF8编码,所以tag后面的信息是完全不同的,而这个文件是UTF8编码的tag,我进而猜测了问题所在,但网上的文档对于这个编码字节基本没有描述,最后花了很长时间才找到描述编码那篇文章,接着修正代码,得到正确的程序。虽然花了比较长的时间,不过修正代码后,输出正常的音乐文件时内心的满足感让我感觉值了。身为一名工程师自然应该披荆斩棘,直面难题。
  本文所有代码已经托管到https://github.com/CrazyZty/WriteTag,大家可以自由下载。
  嘛,这篇博文就到这里结束了,后期我有时间的话会做音频数据合成,蓝牙SPP数据通信,手机敲击感应等一系列博客,我尽量选我做过的但网上开源比较少的模块做分享,当然如果与公司以前项目有关的话,我会隐藏核心的算法与数据,以我做的基础版本来讲解(这里顺便一提,我所要分享的都是我自己做的,我是不会拿别人的东西做分享的),其他的比如蓝牙BLE通信这些网上有比较成熟方案的我会斟酌情况,如果其中有存在一些比较棘手的模块,我会考虑进行分享。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,503评论 6 13
  • 可以看我的博客 lmwen.top 或者订阅我的公众号 简介有稍微接触python的人就会知道,python中...
    ayuLiao阅读 3,024评论 1 5
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 《记事本圆梦计划》是一本关于人生管理的书,作者熊谷正寿,日本最具影响力的网络公司GMO集团创办人。这本书是他的成功...
    做自己的CEO阅读 2,451评论 3 38