代码之美,正则之道 您所在的位置:网站首页 用什么方法对正则表达式进行验证呢 代码之美,正则之道

代码之美,正则之道

2023-04-23 14:20| 来源: 网络整理| 查看: 265

0 分享至

用微信扫码二维码

分享至好友和朋友圈

导语 “如果罗列计算机软件领域的伟大发明,我相信绝对不会超过二十项,在这个名单当中,当然应该包括分组交换网络,Web,Lisp,哈希算法,UNIX,编译技术,关系模型,面向对象,XML这些大名鼎鼎的家伙,而正则表达式也绝对不应该被漏掉。”-- Jeffrey Friedl《精通正则表达式》序言

从1956年至今,正则表达式活跃了半个多世纪,其热度依然不减,可见技术半衰期之长,因此,学习正则,不但重要,且受益漫长。

本文涉及 js、php、java、python、bash 等语言,共计 1.2w 字,适合前后端同学系统学习正则并熟练掌握正则!

你有没有在搜索文本的时候绞尽脑汁, 试了一个又一个表达式, 还是不行.

你有没有在表单验证的时候, 只是做做样子(只要不为空就好), 然后烧香拜佛, 虔诚祈祷, 千万不要出错.

你有没有在使用sed 和 grep 命令的时候, 感觉莫名其妙, 明明应该支持的元字符, 却就是匹配不到.

甚至, 你压根没遇到过上述情况, 你只是一遍又一遍的调用 replace 而已 (把非搜索文本全部替换为空, 然后就只剩搜索文本了), 面对别人家的简洁高效的语句, 你只能在心中呐喊, replace 大法好.

为什么要学正则表达式. 有位网友这么说: 江湖传说里, 程序员的正则表达式和医生的处方, 道士的鬼符齐名, 曰: 普通人看不懂的三件神器. 这个传说至少向我们透露了两点信息: 一是正则表达式很牛, 能和医生的处方, 道士的鬼符齐名, 并被大家提起, 可见其江湖地位. 二是正则表达式很难, 这也从侧面说明了, 如果你可以熟练的掌握并应用它, 在装逼的路上, 你将如日中天 !

显然, 有关正则表达的介绍, 无须我多言. 这里就借助 Jeffrey Friedl 的《精通正则表达式》一书的序言正式抛个砖.

“如果罗列计算机软件领域的伟大发明, 我相信绝对不会超过二十项, 在这个名单当中, 当然应该包括分组交换网络, Web, Lisp, 哈希算法, UNIX, 编译技术, 关系模型, 面向对象, XML这些大名鼎鼎的家伙, 而正则表达式也绝对不应该被漏掉.

对很多实际工作而言, 正则表达式简直是灵丹妙药, 能够成百倍的提高开发效率和程序质量, 正则表达式在生物信息学和人类基因图谱的研究中所发挥的关键作用, 更是被传为佳话. CSDN的创始人蒋涛先生在早年开发专业软件产品时, 就曾经体验过这一工具的巨大威力, 并且一直印象深刻.”

因此, 我们没有理由不去了解正则表达式, 甚至是熟练掌握并运用它.

回顾历史

要论正则表达式的渊源, 最早可以追溯至对人类神经系统如何工作的早期研究. Warren McCulloch 和 Walter Pitts 这两位神经大咖 (神经生理学家) 研究出一种数学方式来描述这些神经网络.

1956 年, 一位叫 Stephen Kleene 的数学家在 McCulloch 和 Pitts 早期工作的基础上, 发表了一篇标题为”神经网事件的表示法”的论文, 引入了正则表达式的概念.

随后, 发现可以将这一工作应用于使用 Ken Thompson 的计算搜索算法的一些早期研究中. 而 Ken Thompson 又是 Unix 的主要发明人. 因此半个世纪以前的Unix 中的 qed 编辑器(1966 qed编辑器问世) 成了第一个使用正则表达式的应用程序.

至此之后, 正则表达式成为家喻户晓的文本处理工具, 几乎各大编程语言都以支持正则表达式作为卖点, 当然 JavaScript 也不例外.

正则表达式的定义

正则表达式是由普通字符和特殊字符(也叫元字符或限定符)组成的文字模板. 如下便是简单的匹配连续数字的正则表达式:

/[0-9]+/ /\\d+/

(左滑查看完整代码,下同)

“\\d” 就是元字符, 而 “+” 则是限定符.

元字符

元字符

描述

.

匹配除换行符以外的任意字符

\\d

匹配数字, 等价于字符组[0-9]

\\w

匹配字母, 数字, 下划线

\\s

匹配任意的空白符(包括制表符,空格,换行等)

\\b

匹配单词开始或结束的位置

^

匹配行首

\$

匹配行尾

反义元字符

元字符

描述

\\D

匹配非数字的任意字符, 等价于[^0-9]

\\W

匹配除字母,数字,下划线之外的任意字符

\\S

匹配非空白的任意字符

\\B

匹配非单词开始或结束的位置

[^x]

匹配除x以外的任意字符

可以看出正则表达式严格区分大小写.

重复限定符

限定符共有6个, 假设重复次数为x次, 那么将有如下规则:

限定符

描述

*

x>=0

+

x>=1

?

x=0 or x=1

{n}

x=n

{n,}

x>=n

{n,m}

n[1-9]?))\\d+/","\\\\1",\$str); //固化分组

不仅如此, php还提供了占有量词优先的语法. 如下:

\$str = "123.456"; echo preg_replace("/(\\.\\d\\d[1-9]?+)\\d+/","\\\\1",\$str); //占有量词优先

虽然java不支持固化分组的语法, 但java也提供了占有量词优先的语法, 同样能够避免正则回溯. 如下:

String str = "123.456"; System.out.println(str.replaceAll("(\\\\.\\\\d\\\\d[1-9]?+)\\\\d+", "\$1"));// 123.456

值得注意的是: java中 replaceAll 方法需要转义反斜杠.

正则表达式高阶技能-零宽断言

如果说正则分组是写轮眼, 那么零宽断言就是万花筒写轮眼终极奥义-须佐能乎(这里借火影忍术打个比方). 合理地使用零宽断言, 能够能分组之不能, 极大地增强正则匹配能力, 它甚至可以帮助你在匹配条件非常模糊的情况下快速地定位文本.

零宽断言, 又叫环视. 环视只进行子表达式的匹配, 匹配到的内容不保存到最终的匹配结果, 由于匹配是零宽度的, 故最终匹配到的只是一个位置.

环视按照方向划分, 有顺序和逆序两种(也叫前瞻和后瞻), 按照是否匹配有肯定和否定两种, 组合之, 便有4种环视. 4种环视并不复杂, 如下便是它们的描述.

字符

描述

示例

(?:pattern)

非捕获性分组, 匹配pattern的位置, 但不捕获匹配结果.也就是说不创建反向引用, 就好像没有括号一样.

‘abcd(?:e)匹配’abcde

(?=pattern)

顺序肯定环视 , 匹配后面是pattern 的位置, 不捕获匹配结果.

‘Windows (?=2000)’匹配 “Windows2000” 中的 “Windows”; 不匹配 “Windows3.1” 中的 “Windows”

(?!pattern)

顺序否定环视 , 匹配后面不是 pattern 的位置, 不捕获匹配结果.

‘Windows (?!2000)’匹配 “Windows3.1” 中的 “Windows”; 不匹配 “Windows2000” 中的 “Windows”

逆序肯定环视 , 匹配前面是 pattern 的位置, 不捕获匹配结果.

‘(?非捕获性分组由于结构与环视相似, 故列在表中, 以做对比. 以上4种环视中, 目前 javaScript 中只支持前两种, 也就是只支持 顺序肯定环视 和 顺序否定环视 . 下面我们通过实例来帮助理解下:

var str = "123abc789",s; //没有使用环视,abc直接被替换 s = str.replace(/abc/,456); //123456789 //使用了顺序肯定环视,捕获到了a前面的位置,所以abc没有被替换,只是将3替换成了3456 s = str.replace(/3(?=abc)/,3456); //123456abc789 //使用了顺序否定环视,由于3后面跟着abc,不满意条件,故捕获失败,所以原字符串没有被替换 s = str.replace(/3(?!abc)/,3456); //123abc789

下面通过python来演示下 逆序肯定环视 和 逆序否定环视 的用法.

import re data = "123abc789" # 使用了逆序肯定环视,替换左边为123的连续的小写英文字母,匹配成功,故abc被替换为456 regExp = r"(?";

现我们需要替换img标签的src 属性中的 “dev”字符串 为 “test” 字符串.

① 由于上述 responseText 字符串中包含至少两个子字符串 “dev”, 显然不能直接 replace 字符串 “dev”为 “test”.

② 同时由于 js 中不支持逆序环视, 我们也不能在正则中判断前缀为 “src=’”, 然后再替换”dev”.

③ 我们注意到 img 标签的 src 属性以 “.png” 结尾, 基于此, 就可以使用顺序肯定环视. 如下:

var reg = /dev(?=[^']*png)/; //为了防止匹配到第一个dev, 通配符前面需要排除单引号或者是尖括号 var str = responseText.replace(reg,"test"); console.log(str);//

当然, 以上不止顺序肯定环视一种解法, 捕获性分组同样可以做到. 那么环视高级在哪里呢? 环视高级的地方就在于它通过一次捕获就可以定位到一个位置, 对于复杂的文本替换场景, 常有奇效, 而分组则需要更多的操作. 请往下看.

千位分割符

千位分隔符, 顾名思义, 就是数字中的逗号. 参考西方的习惯, 数字之中加入一个符号, 避免因数字太长难以直观的看出它的值. 故而数字之中, 每隔三位添加一个逗号, 即千位分隔符.

那么怎么将一串数字转化为千位分隔符形式呢?

var str = "1234567890"; (+str).toLocaleString();//"1,234,567,890"

如上, toLocaleString() 返回当前对象的”本地化”字符串形式.

如果该对象是Number类型, 那么将返回该数值的按照特定符号分割的字符串形式.

如果该对象是Array类型, 那么先将数组中的每项转化为字符串, 然后将这些字符串以指定分隔符连接起来并返回.

toLocaleString 方法特殊, 有本地化特性, 对于天朝, 默认的分隔符是英文逗号. 因此使用它恰好可以将数值转化为千位分隔符形式的字符串. 如果考虑到国际化, 以上方法就有可能会失效了.

我们尝试使用环视来处理下.

function thousand(str){ return str.replace(/(?!^)(?=([0-9]{3})+\$)/g,','); } console.log(thousand(str));//"1,234,567,890" console.log(thousand("123456"));//"123,456" console.log(thousand("1234567879876543210"));//"1,234,567,879,876,543,210"

上述使用到的正则分为两块. (?!^) 和 (?=([0-9]{3})+\$). 我们先来看后面的部分, 然后逐步分析之.

“[0-9]{3}” 表示连续3位数字.

“([0-9]{3})+” 表示连续3位数字至少出现一次或更多次.

“([0-9]{3})+\$” 表示连续3的正整数倍的数字, 直到字符串末尾.

那么 (?=([0-9]{3})+\$) 就表示匹配一个零宽度的位置, 并且从这个位置到字符串末尾, 中间拥有3的正整数倍的数字.

正则表达式使用全局匹配g, 表示匹配到一个位置后, 它会继续匹配, 直至匹配不到.

将这个位置替换为逗号, 实际上就是每3位数字添加一个逗号.

当然对于字符串”123456”这种刚好拥有3的正整数倍的数字的, 当然不能在1前面添加逗号. 那么使用 (?!^) 就指定了这个替换的位置不能为起始位置.

千位分隔符实例, 展示了环视的强大, 一步到位.

正则表达式在JS中的应用ES6对正则的扩展

ES6对正则扩展了又两种修饰符(其他语言可能不支持):

y (粘连sticky修饰符), 与g类似, 也是全局匹配, 并且下一次匹配都是从上一次匹配成功的下一个位置开始, 不同之处在于, g修饰符只要剩余位置中存在匹配即可, 而y修饰符确保匹配必须从剩余的第一个位置开始.

var s = "abc_ab_a"; var r1 = /[a-z]+/g; var r2 = /[a-z]+/y; console.log(r1.exec(s),r1.lastIndex); // ["abc", index: 0, input: "abc_ab_a"] 3 console.log(r2.exec(s),r2.lastIndex); // ["abc", index: 0, input: "abc_ab_a"] 3 console.log(r1.exec(s),r1.lastIndex); // ["ab", index: 4, input: "abc_ab_a"] 6 console.log(r2.exec(s),r2.lastIndex); // null 0

如上, 由于第二次匹配的开始位置是下标3, 对应的字符串是 “_”, 而使用y修饰符的正则对象r2, 需要从剩余的第一个位置开始, 所以匹配失败, 返回null.

正则对象的 sticky 属性, 表示是否设置了y修饰符. 这点将会在后面讲到.

u 修饰符, 提供了对正则表达式添加4字节码点的支持. 比如 “[emoji]” 字符是一个4字节字符, 直接使用正则匹配将会失败, 而使用u修饰符后, 将会等到正确的结果.

var s = "[emoji]"; console.log(/^.\$/.test(s));//false console.log(/^.\$/u.test(s));//true

UCS-2字节码

有关字节码点, 稍微提下. javaScript 只能处理UCS-2编码(js于1995年5月被Brendan Eich花费10天设计出来, 比1996年7月发布的编码规范UTF-16早了一年多, 当时只有UCS-2可选). 由于UCS-2先天不足, 造成了所有字符在js中都是2个字节. 如果是4个字节的字符, 将会默认被当作两个双字节字符处理. 因此 js 的字符处理函数都会受到限制, 无法返回正确结果. 如下:

var s = "[emoji]"; console.log(s == "\\uD834\\uDF06");//true [emoji]相当于UTF-16中的0xD834DF06 console.log(s.length);//2 长度为2, 表示这是4字节字符

幸运的是, ES6可以自动识别4字节的字符.因此遍历字符串可以直接使用for of循环. 同时, js中如果直接使用码点表示Unicode字符, 对于4字节字符, ES5里是没办法识别的. 为此ES6修复了这个问题, 只需将码点放在大括号内即可.

console.log(s === "\\u1D306");//false ES5无法识别[emoji] console.log(s === "\\u{1D306}");//true ES6可以借助大括号识别[emoji]

附: ES6新增的处理4字节码的函数

String.fromCodePoint()

String.prototype.codePointAt()

String.prototype.at() :返回字符串给定位置的字符

有关js中的unicode字符集, 请参考阮一峰老师的 Unicode与JavaScript详解.

以上是ES6对正则的扩展. 另一个方面, 从方法上看, javaScript 中与正则表达式有关的方法有:

由上, 一共有7个与js相关的方法, 这些方法分别来自于 RegExp 与 String 对象. 首先我们先来看看js中的正则类 RegExp.

RegExp

RegExp 对象表示正则表达式, 主要用于对字符串执行模式匹配.

语法: new RegExp(pattern[, flags])

参数 pattern 是一个字符串, 指定了正则表达式字符串或其他的正则表达式对象.

参数 flags 是一个可选的字符串, 包含属性 “g”、”i” 和 “m”, 分别用于指定全局匹配、区分大小写的匹配和多行匹配. 如果pattern 是正则表达式, 而不是字符串, 则必须省略该参数.

var pattern = "[0-9]"; var reg = new RegExp(pattern,"g"); // 上述创建正则表达式对象,可以用对象字面量形式代替,也推荐下面这种 var reg = /[0-9]/g;

以上, 通过对象字面量和构造函数创建正则表达式, 有个小插曲.

“对于正则表达式的直接量, ECMAscript 3规定在每次它时都会返回同一个RegExp对象, 因此用直接量创建的正则表达式的会共享一个实例. 直到ECMAScript 5才规定每次返回不同的实例.”

所以, 现在我们基本不用担心这个问题, 只需要注意在低版本的非IE浏览器中尽量使用构造函数创建正则(这点上, IE一直遵守ES5规定, 其他浏览器的低级版本遵循ES3规定).

RegExp 实例对象包含如下属性:

实例属性

描述

global

是否包含全局标志(true/false)

ignoreCase

是否包含区分大小写标志(true/false)

multiline

是否包含多行标志(true/false)

source

返回创建RegExp对象实例时指定的表达式文本字符串形式

lastIndex

表示原字符串中匹配的字符串末尾的后一个位置, 默认为0

flags(ES6)

返回正则表达式的修饰符

sticky(ES6)

是否设置了y(粘连)修饰符(true/false)

compile

compile 方法用于在执行过程中改变和重新编译正则表达式.

语法: compile(pattern[, flags])

参数介绍请参考上述 RegExp 构造器. 用法如下:

var reg = new RegExp("abc", "gi"); var reg2 = reg.compile("new abc", "g"); console.log(reg);// /new abc/g console.log(reg2);// undefined

可见 compile 方法会改变原正则表达式对象, 并重新编译, 而且它的返回值为空.

test

test 方法用于检测一个字符串是否匹配某个正则规则, 只要是字符串中含有与正则规则匹配的文本, 该方法就返回true, 否则返回 false.

语法: test(string), 用法如下:

console.log(/[0-9]+/.test("abc123"));//true console.log(/[0-9]+/.test("abc"));//false

以上, 字符串”abc123” 包含数字, 故 test 方法返回 true; 而 字符串”abc” 不包含数字, 故返回 false.

如果需要使用 test 方法测试字符串是否完成匹配某个正则规则, 那么可以在正则表达式里增加开始(^)和结束(\$)元字符. 如下:

console.log(/^[0-9]+\$/.test("abc123"));//false

以上, 由于字符串”abc123” 并非以数字开始, 也并非以数字结束, 故 test 方法返回false.

实际上, 如果正则表达式带有全局标志(带有参数g)时, test 方法还受正则对象的lastIndex属性影响,如下:

var reg = /[a-z]+/;//正则不带全局标志 console.log(reg.test("abc"));//true console.log(reg.test("de"));//true var reg = /[a-z]+/g;//正则带有全局标志g console.log(reg.test("abc"));//true console.log(reg.lastIndex);//3, 下次运行test时,将从索引为3的位置开始查找 console.log(reg.test("de"));//false

该影响将在exec 方法讲解中予以分析.

exec

exec 方法用于检测字符串对正则表达式的匹配, 如果找到了匹配的文本, 则返回一个结果数组, 否则返回null.

语法: exec(string)

exec 方法返回的数组中包含两个额外的属性, index 和 input. 并且该数组具有如下特点:

第 0 个项表示正则表达式捕获的文本

第 1~n 项表示第 1~n 个反向引用, 依次指向第 1~n 个分组捕获的文本, 可以使用RegExp.\$ + “编号1~n” 依次获取分组中的文本

index 表示匹配字符串的初始位置

input 表示正在检索的字符串

无论正则表达式有无全局标示”g”, exec 的表现都相同. 但正则表达式对象的表现却有些不同. 下面我们来详细说明下正则表达式对象的表现都有哪些不同.

假设正则表达式对象为 reg , 检测的字符为 string , reg.exec(string) 返回值为 array.

若 reg 包含全局标示”g”, 那么 reg.lastIndex 属性表示原字符串中匹配的字符串末尾的后一个位置, 即下次匹配开始的位置, 此时 reg.lastIndex == array.index(匹配开始的位置) + array[0].length(匹配字符串的长度). 如下:

var reg = /([a-z]+)/gi, string = "World Internet Conference"; var array = reg.exec(string); console.log(array);//["World", "World", index: 0, input: "World Internet Conference"] console.log(RegExp.\$1);//World console.log(reg.lastIndex);//5, 刚好等于 array.index + array[0].length

随着检索继续, array.index 的值将往后递增, 也就是说, reg.lastIndex 的值也会同步往后递增. 因此, 我们也可以通过反复调用 exec 方法来遍历字符串中所有的匹配文本. 直到 exec 方法再也匹配不到文本时, 它将返回 null, 并把 reg.lastIndex 属性重置为 0.

接着上述例子, 我们继续执行代码, 看看上面说的对不对, 如下所示:

array = reg.exec(string); //["Internet", "Internet", index: 6, input: "World Internet Conference"] console.log(reg.lastIndex);//14 array = reg.exec(string); //["Conference", "Conference", index: 15, input: "World Internet Conference"] console.log(reg.lastIndex);//25 array = reg.exec(string); //null console.log(reg.lastIndex);//0

以上代码中, 随着反复调用 exec 方法, reg.lastIndex 属性最终被重置为 0.

问题回顾

在 test 方法的讲解中, 我们留下了一个问题. 如果正则表达式带有全局标志g, 以上 test 方法的执行结果将受 reg.lastIndex影响, 不仅如此, exec 方法也一样. 由于 reg.lastIndex 的值并不总是为零, 并且它决定了下次匹配开始的位置, 如果在一个字符串中完成了一次匹配之后要开始检索新的字符串, 那就必须要手动地把 lastIndex 属性重置为 0. 避免出现下面这种错误:

var reg = /[0-9]+/g, str1 = "123abc", str2 = "123456"; reg.exec(str1); console.log(reg.lastIndex);//3 var array = reg.exec(str2); //["456", index: 3, input: "123456"]

以上代码, 正确执行结果应该是 “123456”, 因此建议在第二次执行 exec 方法前, 增加一句 “reg.lastIndex = 0;”.

若 reg 不包含全局标示”g”, 那么 exec 方法的执行结果(array)将与 string.match(reg) 方法执行结果完全相同.

String

match, search, replace, split 方法请参考字符串API.

如下展示了使用捕获性分组处理文本模板, 最终生成完整字符串的过程:

var tmp = "An \${a} a \${b} keeps the \${c} away"; var obj = { a:"apple", b:"day", c:"doctor" }; function tmpl(t,o){ return t.replace(/\\\${(.)}/g,function(m,p){ console.log('m:'+m+' p:'+p); return o[p]; }); } tmpl(tmp,obj);

上述功能使用ES6可这么实现:

var obj = { a:"apple", b:"day", c:"doctor" }; with(obj){ console.log(`An \${a} a \${b} keeps the \${c} away`); }

正则表达式在H5中的应用

H5中新增了 pattern 属性, 规定了用于验证输入字段的模式, pattern的模式匹配支持正则表达式的书写方式. 默认 pattern 属性是全部匹配, 即无论正则表达式中有无 “^”, “\$” 元字符, 它都是匹配所有文本.

注: pattern 适用于以下 input 类型:text, search, url, telephone, email 以及 password. 如果需要取消表单验证, 在form标签上增加 novalidate 属性即可.

正则引擎

目前正则引擎有两种, DFA 和 NFA, NFA又可以分为传统型NFA和POSIX NFA.

DFA Deterministic finite automaton 确定型有穷自动机

NFA Non-deterministic finite automaton 非确定型有穷自动机

Traditional NFA

POSIX NFA

DFA引擎不支持回溯, 匹配快速, 并且不支持捕获组, 因此也就不支持反向引用. 上述awk, egrep命令均支持 DFA引擎.

POSIX NFA主要指符合POSIX标准的NFA引擎, 像 javaScript, java, php, python, c#等语言均实现了NFA引擎.

有关正则表达式详细的匹配原理, 暂时没在网上看到适合的文章, 建议选读 Jeffrey Friedl 的 [第三版] 中第4章-表达式的匹配原理(p143-p183), Jeffrey Friedl 对正则表达式有着深刻的理解, 相信他能够帮助您更好的学习正则.

有关NFA引擎的简单实现, 可以参考文章 基于ε-NFA的正则表达式引擎 - twoon.

总结

在学习正则的初级阶段, 重在理解

①贪婪与非贪婪模式

②分组

③捕获性与非捕获性分组

④命名分组

⑤固化分组, 体会设计的精妙之处.

而高级阶段, 主要在于熟练运用

⑥零宽断言(或环视)解决问题, 并且熟悉正则匹配的原理.

实际上, 正则在 javaScript 中的功能不算强大, js 仅仅支持了①贪婪与非贪婪模式, ②分组, ③捕获性与非捕获性分组 以及 ⑥零宽断言中的顺序环视. 如果再稍微熟悉些 js 中7种与正则有关的方法(compile, test, exec, match, search, replace, split), 那么处理文本或字符串将游刃有余。

正则表达式, 在文本处理方面天赋异禀, 它的功能十分强大, 很多时候甚至是唯一解决方案. 正则不局限于js, 当下热门的编辑器(比如VS Code,Sublime, Atom) 以及 IDE(比如WebStorm, IntelliJ IDEA) 都支持它. 您甚至可以在任何时候任何语言中, 尝试使用正则解决问题, 也许之前不能解决的问题, 现在可以轻松的解决.

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

/阅读下一篇/ 返回网易首页 下载网易新闻客户端


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有