2021SC@SDUSC

文章目录

  • 一、Zxing中的一维码
  • 二、OneDReader
  • 三、一维码万能解码类——MultiFormatOneDReader
  • 四、CodaBarReader
    • 一、认识CodaBar
    • 二、CodaBar解码方法——decodeRow
    • 三、一维码定位方法
      • 直线拟合
      • 膨胀腐蚀
    • 四、验证
  • 五、一维码和二维码对比

一、Zxing中的一维码

在Zxing的目录结构中,所有的一维码有关代码被放到了同一个文件夹oned中

这是由于不同的一维码的编码解码逻辑比较简单并且关联性较大。这篇博客主要以CodaBar为例分析解码过程。

二、OneDReader

所有一维码的解码器都要继承OneDReader类。OneDReader是Reader的子类。在二维码中,解码方法都是decode,但是在一维码中,解码方法都命名为decodeRow。
各方法介绍如下:

方法 作用
decode(BinaryBitmap image):Result 重写方法。
decode(BinaryBitmap image,Map<DecodeHintType,?> hints) :Result 重写方法,外部调用的解码方法
reset():void 重写方法,方法里面是空的,也就是OneDreader不具体描述这个方法
doDecode(BinaryBitmap image, Map<DecodeHintType,?> hints):Result 内部实现的解码方法
recordPattern(BitArray row, int start,int[] counters):void 记录从给定点开始的一行中连续运行的白色和黑色像素的数量
recordPatternInReverse(BitArray row, int start, int[] counters):void 上一个方法是从前向后记录,这个方法是从后向前记录,只在rss中用到
patternMatchVariance(int[] counters,int[] pattern,float maxIndividualVariance) :float 确定一组观察到的黑白值运行计数与给定目标模式的匹配程度。是所有模式中,预期模式比例的总方差与模式长度的比率。
decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints): Result 抽象方法,尝试对给定一行图像的一维条形码格式进行解码。需要每个码根据自己的特点实现。

docode

 //注:即使图片支持旋转,在没有TRY_HARDER标志的情况下也不会尝试旋转@Overridepublic Result decode(BinaryBitmap image,Map<DecodeHintType,?> hints) throws NotFoundException, FormatException {try {// 直接调用解码算法return doDecode(image, hints);} catch (NotFoundException nfe) {// 传输的条形码可能发生形变boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);//如果解码的目标是花更多的时间去寻找条形码;优化精度,而不是速度,并且此子类支持逆时针旋转。if (tryHarder && image.isRotateSupported()) {// 返回图像数据逆时针旋转90度的新对象,二值图像BinaryBitmap rotatedImage = image.rotateCounterClockwise();// 调用解码算法Result result = doDecode(rotatedImage, hints);//记录我们发现它逆时针旋转90度/270度Map<ResultMetadataType,?> metadata = result.getResultMetadata();int orientation = 270;if (metadata != null && metadata.containsKey(ResultMetadataType.ORIENTATION)) {//但如果我们在doDecode()中发现了相反的结果,在此处添加该结果//ORIENTATION表示图像中条形码的可能近似方向。该值以从正常垂直方向顺时针旋转的角度给出,其值在[0360]范围内。例如,通过自上而下读取找到的1D条形码将被称为方向为“90”。orientation = (orientation +(Integer) metadata.get(ResultMetadataType.ORIENTATION)) % 360;}result.putMetadata(ResultMetadataType.ORIENTATION, orientation);//更新结果点ResultPoint[] points = result.getResultPoints();if (points != null) {int height = rotatedImage.getHeight();for (int i = 0; i < points.length; i++) {points[i] = new ResultPoint(height - points[i].getY() - 1, points[i].getX());}}return result;} else {// 抛出NotFoundException没有找到异常throw nfe;}}}

doDecode
decode交付给doDecode进行后续解码操作,而doDecode只是负责扫描定位,具体的解码方法在每个码的decodeRow中实现。

private Result doDecode(BinaryBitmap image,Map<DecodeHintType,?> hints) throws NotFoundException {int width = image.getWidth();//宽int height = image.getHeight();//高BitArray row = new BitArray(width);//定义一个大小为宽度的BitArray来存放结果boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);// 如果不知道扫码类型并且希望更加重视扫码的精度,扫描行的步长为(height>>8和1)中最大的一个,反之为(height>>5和1)中最大的一个int rowStep = Math.max(1, height >> (tryHarder ? 8 : 5));int maxLines;// 如果不知道扫码类型并且希望更加重视扫码的精度if (tryHarder) {maxLines = height; //看看整个图像,而不仅仅是中心} else {// 如果不知道扫码类型并且希望更加重视扫码的速度maxLines = 15; // 间隔1/32的15行大致是图像的中间部分}int middle = height / 2;for (int x = 0; x < maxLines; x++) {// 从中间向外扫描。确定我们接下来要查看的行int rowStepsAboveOrBelow = (x + 1) / 2;boolean isAbove = (x & 0x01) == 0; // 判断x是否为奇数int rowNumber = middle + rowStep * (isAbove ? rowStepsAboveOrBelow : -rowStepsAboveOrBelow);if (rowNumber < 0 || rowNumber >= height) {// 如果超出顶部或底部得范围,stopbreak;}//估计此行的黑点并加载它try {//getBlackRow将一行亮度数据转换为1位数据,用于解码一维条形码,可以选择应用锐化。row = image.getBlackRow(rowNumber, row);} catch (NotFoundException ignored) {continue;}// 虽然我们将图像数据放在一个位数组中,但将其反转以处理颠倒条形码的解码是相当方便的。for (int attempt = 0; attempt < 2; attempt++) {if (attempt == 1) {row.reverse(); // 将行反转并继续//这意味着在该方法的生命周期中,我们将只绘制一次结果点,因为我们希望避免在翻转行后绘制错误的点,并且不希望在每一行扫描中都出现噪音,只绘制从中心线开始的扫描。if (hints != null && hints.containsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) {Map<DecodeHintType,Object> newHints = new EnumMap<>(DecodeHintType.class);newHints.putAll(hints);newHints.remove(DecodeHintType.NEED_RESULT_POINT_CALLBACK);hints = newHints;}}try {// 寻找条形码,decodeRow是解码的关键方法Result result = decodeRow(rowNumber, row, hints);// 找到了条形码if (attempt == 1) {// 但它是颠倒的,所以请注意result.putMetadata(ResultMetadataType.ORIENTATION, 180);// 并记住水平翻转结果点ResultPoint[] points = result.getResultPoints();if (points != null) {points[0] = new ResultPoint(width - points[0].getX() - 1, points[0].getY());points[1] = new ResultPoint(width - points[1].getX() - 1, points[1].getY());}}return result;} catch (ReaderException re) {// 继续,直到无法解码这一行}}}

三、一维码万能解码类——MultiFormatOneDReader

还记得在Reader中我们介绍的MultiFormatReader吗?我们称它是万能解码类,MultiFormatOneDReader和Reader、OneDReader、MultiFormatReader有着密切的关系,如下:所有一/二维码解码器都要实现Reader,所有一维码解码器都要继承OneDReader。为了调用方便,Zxing写了两个工厂类:MultiFormatOneDReade、MultiFormatReader;MultiFormatReader是面向一/二维码的,MultiFormatOneDReade是面向一维码的,在MultiFormatReader中对一维码的解析调用的就是MultiFormatOneDReader类。

  if (addOneDReader && !tryHarder) {readers.add(new MultiFormatOneDReader(hints));}

四者关系如图:

作为工厂类,MultiFormatOneDReader的逻辑和MultiFormatReader基本一致:

  • 判断hints是否为空?
    是->尝试所有一维码解码器
    否->尝试所有可能类型匹配的一维码解码器。
  public MultiFormatOneDReader(Map<DecodeHintType,?> hints) {@SuppressWarnings("unchecked")Collection<BarcodeFormat> possibleFormats = hints == null ? null :(Collection<BarcodeFormat>) hints.get(DecodeHintType.POSSIBLE_FORMATS);//POSSIBLE_FORMATS表示图像是几种可能的格式之一。//ASSUME_CODE_39_CHECK_DIGIT表示不管它实际是什么,都假设使用Code39的校验位。boolean useCode39CheckDigit = hints != null &&hints.get(DecodeHintType.ASSUME_CODE_39_CHECK_DIGIT) != null;Collection<OneDReader> readers = new ArrayList<>();if (possibleFormats != null) {if (possibleFormats.contains(BarcodeFormat.EAN_13) ||possibleFormats.contains(BarcodeFormat.UPC_A) ||possibleFormats.contains(BarcodeFormat.EAN_8) ||possibleFormats.contains(BarcodeFormat.UPC_E)) {// 可以推测MultiFormatUPCEANReader也是一个工厂类,针对的是EAN_13、UPC_A、EAN_8、UPC_E四种条形码readers.add(new MultiFormatUPCEANReader(hints));}if (possibleFormats.contains(BarcodeFormat.CODE_39)) {readers.add(new Code39Reader(useCode39CheckDigit));}if (possibleFormats.contains(BarcodeFormat.CODE_93)) {readers.add(new Code93Reader());}if (possibleFormats.contains(BarcodeFormat.CODE_128)) {readers.add(new Code128Reader());}if (possibleFormats.contains(BarcodeFormat.ITF)) {readers.add(new ITFReader());}if (possibleFormats.contains(BarcodeFormat.CODABAR)) {readers.add(new CodaBarReader());}if (possibleFormats.contains(BarcodeFormat.RSS_14)) {readers.add(new RSS14Reader());}if (possibleFormats.contains(BarcodeFormat.RSS_EXPANDED)) {readers.add(new RSSExpandedReader());}}if (readers.isEmpty()) {// 可以推测MultiFormatUPCEANReader也是一个工厂类,针对的是EAN_13、UPC_A、EAN_8、UPC_E四种条形码readers.add(new MultiFormatUPCEANReader(hints));readers.add(new Code39Reader());readers.add(new CodaBarReader());readers.add(new Code93Reader());readers.add(new Code128Reader());readers.add(new ITFReader());readers.add(new RSS14Reader());readers.add(new RSSExpandedReader());}this.readers = readers.toArray(EMPTY_ONED_ARRAY);}

四、CodaBarReader

一维码和二维码不同,Reader不需要将定位和解码的任务交给其他类别完成,所有的算法都在各自的Reader中(rss除外)。

一、认识CodaBar

  • Codabar构成:Codabar具有4个条和3个空(共7个单元),每个窄或宽的宽度代表一个字符(字母)。
    Codabar的基本构成如下:

    Zxing对起始终止字符的定义如下:
  private static final char[] STARTEND_ENCODING = {'A', 'B', 'C', 'D'};
  • Codabar字符的构成:Codabar可以用数字(0至9)、字母(A、B、C、D)以及符号(-、$、/、. 、+)来表示字符。

    在Zxing中,定义了字符字母串,并用一个数组存储他们
  private static final String ALPHABET_STRING = "0123456789-$:/.+ABCD";static final char[] ALPHABET = ALPHABET_STRING.toCharArray();

规定这些表示字符的编码如下,表示宽条和窄条的模式。每个int的7个最低有效位对应于宽和窄的模式,1表示“宽”,0表示“窄”。

  static final int[] CHARACTER_ENCODINGS = {0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, // 0-90x00c, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E, // -$:/.+ABCD};

以“0”为例,0的编码为0x003,低7位为0000011,即,窄窄窄窄窄宽宽,与上图中0的条式图案一致。

  • Codabar的特征:Codabar的遗漏读取比ITF的要少。同CODE 39相比,条码尺寸也较小。但这并不总意味着Codabar就不存在遗漏读取。如果条码的打印质量不好,往往在以下情形中会出现遗漏读取。
    为了避免遗漏读取,推荐采用和ITF一样的办法,把条码读取仪设置在"数位指定"功能上,只读取规定位数的数字。例如,A––––A用于罗列价格,A––––C用于特别折扣价格而C––––C为大减价。
  • Codabar的应用:应用于验血(标本)的试管上,以确定各个身份。

二、CodaBar解码方法——decodeRow

总的来说,所有一维码的解码思路都基本一致:
第一步:定位。条形码Reader扫描图像的整个宽度,试图识别是否有条形码候选对象——黑/白图。
第二步:解码。定位出条形码后开始解码。在这一步中,Reader它对图像像素进行计数和比较,以匹配开始和结束标识符。然后,它根据该代码类型的规范来解析开始标识符和结束标识符之间的模式,以解开编码的数据。

  @Overridepublic Result decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints) throws NotFoundException {Arrays.fill(counters, 0);// 记录所有白色和黑色像素的大小,从白色开始。这与recordPattern类似,只是它记录所有计数器,并使用内置的“counters”成员进行存储。在这个方法中初始化counterLength大小。setCounters(row);// 设置起始偏移量。在开始模式之前查找空白(>=开始模式宽度的50%,如上面的图所示,ABCD的起始位置都是窄框)。int startOffset = findStartPattern();int nextStart = startOffset;decodeRowResult.setLength(0);do {int charOffset = toNarrowWidePattern(nextStart);if (charOffset == -1) {throw NotFoundException.getNotFoundInstance();}//我们将字母表中的位置存储到StringBuilder中,以便在validatePattern中访问解码的模式。我们稍后将转换为实际字符。decodeRowResult.append((char) charOffset);nextStart += 8;// 我们一看到结束字符就停止。arrayContains方法判断元素是否在数组中if (decodeRowResult.length() > 1 &&arrayContains(STARTEND_ENCODING, ALPHABET[charOffset])) {break;}} while (nextStart < counterLength); // 直到超出长度范围停止// 查找空格int trailingWhitespace = counters[nextStart - 1];int lastPatternSize = 0;for (int i = -8; i < -1; i++) {lastPatternSize += counters[nextStart + i];}// 我们需要看到空白大于等于最后一个图案大小的50%,否则这可能是误报。if (nextStart < counterLength && trailingWhitespace < lastPatternSize / 2) {throw NotFoundException.getNotFoundInstance();}// 验证startOffsetvalidatePattern(startOffset);// 将字符表偏移量转换为当前字符。for (int i = 0; i < decodeRowResult.length(); i++) {decodeRowResult.setCharAt(i, ALPHABET[decodeRowResult.charAt(i)]);}// Ensure a valid start and end character// 确保起始字符和结束字符有效char startchar = decodeRowResult.charAt(0);if (!arrayContains(STARTEND_ENCODING, startchar)) {throw NotFoundException.getNotFoundInstance();}char endchar = decodeRowResult.charAt(decodeRowResult.length() - 1);if (!arrayContains(STARTEND_ENCODING, endchar)) {throw NotFoundException.getNotFoundInstance();}// 删除停止/开始字符并检查是否包含足够长的字符串if (decodeRowResult.length() <= MIN_CHARACTER_LENGTH) {// Almost surely a false positive ( start + stop + at least 1 character)// 几乎肯定是误报(开始+停止+至少1个字符)throw NotFoundException.getNotFoundInstance();}
//RETURN_CODABAR_START_END表示如果为true,则返回Codabar条形码中的起始和结束数字,而不是将其剥离。它们是字母,而其余的是数字。if (hints == null || !hints.containsKey(DecodeHintType.RETURN_CODABAR_START_END)) {decodeRowResult.deleteCharAt(decodeRowResult.length() - 1);decodeRowResult.deleteCharAt(0);}int runningCount = 0;for (int i = 0; i < startOffset; i++) {runningCount += counters[i];}float left = runningCount;for (int i = startOffset; i < nextStart - 1; i++) {runningCount += counters[i];}float right = runningCount;Result result = new Result(decodeRowResult.toString(),null,new ResultPoint[]{new ResultPoint(left, rowNumber),new ResultPoint(right, rowNumber)},BarcodeFormat.CODABAR);//SYMBOLOGY_IDENTIFIER条形码符号标识符。注:根据GS1规范,在条形码内容前加前缀时,标识符可能必须替换前导FNC1/GS字符。result.putMetadata(ResultMetadataType.SYMBOLOGY_IDENTIFIER, "]F0");return result;}

三、一维码定位方法

直线拟合

在CodaBar中使用的就是这种定位方式。在OneDReader的decode方法中,有一步是将图像旋转了90度,这样对于一张二值化后的一维码,我们可以得到如下图片:

由于条码区域的线条都是直线,如果噪声有弯曲则拟合不了,因此可以使用这种方式定位。而由于条形码只有一个维度存储数据(row),因此我们通过定位起始位置,再纵向扫描就可以解析出对应的内容。
代码

//在图片的每一行中进行查找private void setCounters(BitArray row) throws NotFoundException {counterLength = 0;// 从第一个白色的bit开始查找int i = row.getNextUnset(0);int end = row.getSize();//定位的bit位比row的长度要长,说明越界了if (i >= end) {throw NotFoundException.getNotFoundInstance();}//设置标记boolean isWhite = true;int count = 0;while (i < end) {//如果当前扫描的位置是黑色if (row.get(i) != isWhite) {count++;} else {//如果是白色,要把当前位置记录到数组counters[]中。counterAppend(count);count = 1;isWhite = !isWhite;}i++;}counterAppend(count);}

如上对行进行处理,以定位条形码的位置。

膨胀腐蚀

处理流程
对一张一维码的图片,在经过灰度处理二值化后,图像如下图(在Zxing中我们用“X”表示黑,用“ ”表示白)

通过开操作去除一些小的噪声点:

再进行闭操作,对小孔进行填充:

最后对寻找到的轮廓做最小矩形,宽高比大于1小于2,并且,宽高分别大于图片的宽高的三分之一,用以筛选掉较小的对象和其他形状的对象。筛选后,将识别到的条形码位置在原图框出,定位到条形码的位置。
原理分析
膨胀:

腐蚀:

开操作:先腐蚀再膨胀,从下图中可以看出,一些小的噪声点都被去除了。
闭操作:先膨胀再腐蚀

四、验证

由于条形码的定位标志没有QRcode那么明显,为了确保提取到的区域是条码,对于我们定位到的startOffset,我们还需要做一些验证,避免出错。
①基于形状,判断区域宽高比例,如果区域太扁或者近似正方形,则该区域不是条码区域。
②基于颜色,条码区域,黑白相间,黑色区域的面积和白色区域的面积差不多大小。
③基于纹理,中间画测量线求正负跳变次数,少于阈值说明不是条码区域。

  // 验证模式private void validatePattern(int start) throws NotFoundException {// 首先,总结出四类条纹尺寸的总尺寸;int[] sizes = {0, 0, 0, 0};int[] counts = {0, 0, 0, 0};int end = decodeRowResult.length() - 1;// 我们在中间中断这个循环,以便正确处理字符间空格。int pos = start;for (int i = 0; i <= end; i++) {int pattern = CHARACTER_ENCODINGS[decodeRowResult.charAt(i)];for (int j = 6; j >= 0; j--) {//偶数j=条形,而奇数j=空间。类别2和3用于长条纹,而0和1用于短条纹。int category = (j & 1) + (pattern & 1) * 2;sizes[category] += counters[pos + j];counts[category]++;pattern >>= 1;}// 忽略字符间的空间,它可以是任意大小的pos += 8;}// 使用定点数学计算允许的大小阈值。float[] maxes = new float[4];float[] mins = new float[4];// 将可接受性阈值定义为平均小条纹和平均大条纹之间的中点。// 条纹长度不应位于该线的“错误”一侧。for (int i = 0; i < 2; i++) {mins[i] = 0.0f;  // 接受任意小的“短”条纹。mins[i + 2] = ((float) sizes[i] / counts[i] + (float) sizes[i + 2] / counts[i + 2]) / 2.0f;maxes[i] = mins[i + 2];maxes[i + 2] = (sizes[i + 2] * MAX_ACCEPTABLE + PADDING) / counts[i + 2];}// 现在验证所有条纹是否都在阈值内。pos = start;for (int i = 0; i <= end; i++) {int pattern = CHARACTER_ENCODINGS[decodeRowResult.charAt(i)];for (int j = 6; j >= 0; j--) {// 偶数j=条形,而奇数j=空间。类别2和3用于长条纹,而0和1用于短条纹。int category = (j & 1) + (pattern & 1) * 2;int size = counters[pos + j];if (size < mins[category] || size > maxes[category]) {throw NotFoundException.getNotFoundInstance();}pattern >>= 1;}pos += 8;}}

五、一维码和二维码对比

  • 可以看到一维码没有纠错机制,如果一维码有破损,就不能被读取;对于二维码来说,即使有破损,也有可能可以正常读取。
  • 一维码只能在水平方向单向的表达商品信息,而在垂直方向则不表达任何信息,它的一定高度通常是为了便于条码设备的对准,读取。而二维码在水平和垂直方向都可表达信息,也就是说它在二维空间内存储信息。
  • 由于只有水平方向有信息,一维码扫码逻辑很简单。

欢迎提出宝贵意见,感谢观看!
参考:
ZxingAPI
Codabar介绍

2021SC@SDUSC-Zxing(十三):一维码解码分析相关推荐

  1. 2021SC@SDUSC Zxing开源代码(十七)Zxing代码解析——一维码

    2021SC@SDUSC 目录 Code 39 Code 93 Code 128 Codabar ITF 参考资料 前言:本篇博客主要介绍一维码. Code 39 Code39是条形码的一种,也被称为 ...

  2. 2021SC@SDUSC基于人工智能的多肽药物分析问题(十三)

    基于人工智能的多肽药物分析问题(十三) 2021SC@SDUSC 1. 前言 代码分析已临近尾声了,目前还剩下e2e模式的预测代码,由于两种模式的代码存在部分重叠,所以接下来的代码可能会略过一些重复代 ...

  3. 2021SC@SDUSC TencentOS Tiny源码分析(十) 邮箱队列模块一

    2021SC@SDUSC 文章目录 一. 优先级消息队列 二. 邮箱队列 三. 邮箱队列的其他操作 (一)动态创建邮箱队列 (二)销毁邮箱队列 (三)冲洗邮箱队列 本周我们首先继续上周的消息队列工作分 ...

  4. 2021SC@SDUSC HBase(十三)项目代码分析——WAL写入

    2021SC@SDUSC 目录 一.简述 二.机制 三.线程模型 四.具体实现 五.总结 一.简述 Hbase 的 WAL 机制是保证 hbase 使用 lsm 树存储模型把随机写转化成顺序写,并从内 ...

  5. 2021SC@SDUSC基于人工智能的多肽药物分析问题(六)

    基于人工智能的多肽药物分析问题(六) 2021SC@SDUSC 1. 卷积神经网络 1.1 卷积神经网络优势 在学习卷积神经网络之前,使用的是全连接神经网络,但是: 如果用全连接神经网络处理大尺寸图像 ...

  6. 2021SC@SDUSC Zxing开源代码(二)CaptureActivity代码分析-生命周期函数

    文章目录 前言 一.Android基础知识 1. Android 基本组件 2. SurfaceView .Surface与SurfaceHolder 3. Activity 生命周期 4. Andr ...

  7. Code128一维码(解码)

    由于最近在弄一个用Winform实现基于"博斯得"打印机实现打印预览的玩意遇到了几个问题. 在打印条码的时候实际打印的条码长度和预览差距很大于是就查网上查了一下资料熟悉了一下条形码 ...

  8. 2021SC@SDUSC山东大学软件学院软件工程应用与实践--Ebiten代码分析 源码分析(三)

    2021SC@SDUSC 目录 一.概述 二.代码分析 1.Update() 2.Draw() 3.Layout() 一.概述 本文将介绍ebiten在RunGame函数中逐帧执行的Update()方 ...

  9. 2021SC@SDUSC山东大学软件学院软件工程应用与实践--Ebiten代码分析 源码分析(四)

    2021SC@SDUSC 目录 一.概述 二.代码分析 1.graphics.QuadVertices() 2.mipmap.Mipmap() 3.i.mipmap.DrawTriangles() 一 ...

最新文章

  1. css display属性理解
  2. postfix导致maillog填满磁盘空间的巨坑!
  3. python变量定义问题_python 定义n个变量方法 (变量声明自动化)
  4. 台式机dp接口_精品导购:你想要的商务台式机 都在这里了!
  5. mysql 优化 修复原理_mysql下表的修复与优化
  6. MySQL千万级大表优化解决方案
  7. 强化学习研究什么?用白话讲就是……
  8. 对文档的编辑过多_Wizard 开源文档管理系统1.0发布啦
  9. 解决关灯游戏(Lights Off)
  10. 纪念非线性光学诞生:Peter Franken和非线性光学
  11. html下拉菜单hover,css用hover制作下拉菜单
  12. python怎么撤销_python撤销操作
  13. 正则十八式-第三式:龙跃于渊
  14. CSS3旋转跳跃的立方体
  15. 财务自由,整层楼沸腾!万亿蚂蚁IPO来了,诞生几千个亿万富翁?杭州、上海房价又要涨了…...
  16. 大数据概论、大数据概念、大数据特点(4V)、Volume(大量)、Velocity(高速)、Variety(多样)、Value(低价值密度)、大数据应用场景、大数据发展前景、大数据部门间业务流程分析
  17. MySQL修改表的字段
  18. cml sml区别_cml和sml的区别
  19. cs无限僵尸服务器,CS1.6僵尸版
  20. VBS 调用web接口

热门文章

  1. shell脚本实现对网卡流量监控
  2. 数学建模债券投资组合_金融建模与投资管理中的数学
  3. u盘插入计算机显示被写保护,使用U盘时提示被写保护怎么办
  4. 删除服务器电脑文件夹同步,酷盘PC版为何上传完成后删除电脑里的文件时网站上同时没了...
  5. 小型主机台式计算机,可能是最便宜的入门nas小主机
  6. 求伯君:金山电脑公司总裁
  7. 修改注册表恢复IE设置(转)
  8. IPAD模拟器HOME键消失问题
  9. 推荐一款可以保障人身财产安全的免费手机app软件 - 三眼
  10. Location Prediction综述