从零实现最简编译模型

简介

前两日我偶然间在 GitHub 上发现了一个项目:the-super-tiny-compiler,官方介绍说这可能是一个最简的编译器。刚好之前学过「编译原理」这门课,我的兴趣一下子就上来了,简单看了一下,这个项目是将一个 Lisp 表达式转化为 C 的表达式的编译器,中间涉及词法分析、语法分析、AST 树遍历转化以及最后的代码输出环节,下面我就带大家一起来简单实现一下。

词法分析

词法分析也叫解析,每一个编译器需要做的第一步都是词法分析,具体是什么意思呢?简单来说就是把要进行转化的「源代码」拆解开,形成一个一个小部件,称为 token。比如说如下将一个 JavaScript 语句拆解开的例子:

let name = "touryung";

大致分解就可以得到一个 token 数组:[let, name, =, "touryung", ;],这样才有利于进行下一步操作。由于我们此次实现的是最简编译器,因此编译器内部只实现了对小括号、空格、数字、字符串、变量的识别。

整体框架

要实现词法分析器(解析器),首先我们需要先搭出一个框架。词法分析的整体思路就是遍历输入的字符串,然后识别不同的 token,将它保存到 token 数组

框架如下,不同部分的意思已在注释中标出:

const WHITESPACE = /\s/; // 空格
const NUMBERS = /[0-9]/; // 数字
const LETTERS = /[a-z]/i; // 变量

function tokenizer(input) {
  let current = 0; // 当前识别到的下标
  let tokens = []; // token 数组

  while (current < input.length) {
    // 遍历
    let char = input[current]; // 当前遍历到的字符

    // 不同的 token 识别操作

    throw new TypeError(`I dont know what this character is: ${char}`);
  }

  return tokens;
}

搭出框架,下一步就是识别不同的 token 了。

识别括号

识别括号很简单,当遍历到当前字符是左右括号时,将一个描述当前 token 的对象放入 token 数组即可。

// 识别左括号
if (char === "(") {
  tokens.push({ type: "paren", value: "(" }); // 压入描述当前 token 的对象
  current++;
  continue;
}
// 识别右括号
if (char === ")") {
  tokens.push({ type: "paren", value: ")" });
  current++;
  continue;
}

识别空格

这里需要注意,因为空格实际上对编程语言的语法来说是无关紧要的,这就是为什么将 Javascript 代码压缩之后仍然能够正常运行。因此当我们识别到空格的时候,不需要将其放入 token 数组进行下一步的操作。

实际上,在词法分析这一步,类似空格、注释、换行符这类不影响程序语法的 token 就不会送入下一步进行处理了。

因此,当我们识别到空格的时候,只需要继续遍历即可:

// 空格,不处理
if (WHITESPACE.test(char)) {
  current++;
  continue;
}

识别数字/变量/字符串

我为什么要把这三种 token 写在一起呢?原因是从数字开始,这三种 token 的匹配逻辑都很相似,由于匹配的 token 可能不再是单个字符,因此需要在内部继续循环直到匹配完整个 token 为止。

// 数字,循环获取数值
if (NUMBERS.test(char)) {
  let value = ""; // 匹配的数字赋初值

  while (NUMBERS.test(char)) { // 遍历,如果还能匹配就累加
    value += char;
    char = input[++current];
  }

  tokens.push({ type: "number", value }); // 压入描述当前 token 的对象
  continue;
}

// 变量,和 number 类似
if (LETTERS.test(char)) {
  let value = "";

  while (LETTERS.test(char)) {
    value += char;
    char = input[++current];
  }

  tokens.push({ type: "name", value });
  continue;
}

// 字符串,前后的 "" 需要跳过
if (char === '"') {
  let value = "";
  char = input[++current]; // 跳过前面的引号

  while (char !== '"') { // 结束条件,匹配到末尾的引号
    value += char;
    char = input[++current];
  }

  char = input[++current]; // 跳过后面的引号
  tokens.push({ type: "string", value });
  continue;
}

其中需要注意,识别字符串类似上面两种,但是也有两点不同:

  1. 在字符串识别时需要跳过前后的引号,只匹配中间具体的值;
  2. 在中间进行遍历的时候结束条件是匹配到末尾的引号。

有人可能会问,如果跳过的前后的引号以后要怎么知道它是字符串呢,这时候压入数组的 token 描述对象作用就出来了,它有一个 type 属性可以指定当前 token 的类型。

小总结

至此,词法分析的工作就做完了,其实相对来说还是很好懂的,那么能不能直观的观察词法分析输出的 token 数组是什么样子的呢?当然可以,只需要编写一个样例测试一下就行了,比如:

let source = "(add 2 (subtract 4 2))"; // 源代码
let tokens = tokenizer(source);
console.log(tokens);

这是一个计算 2+(4-2) 的 Lisp 语句,将它作为输入得到的 token 数组如下所示:

[
  { "type": "paren", "value": "(" },
  { "type": "name", "value": "add" },
  { "type": "number", "value": "2" },
  { "type": "paren", "value": "(" },
  { "type": "name", "value": "subtract" },
  { "type": "number", "value": "4" },
  { "type": "number", "value": "2" },
  { "type": "paren", "value": ")" },
  { "type": "paren", "value": ")" }
]

这样就完美的达到了我们开头所说的将源代码进行拆解的目的。

语法分析

接下来就是语法分析了,语法分析的作用是根据具体的编程语言语法来将上一步输出的 token 数组转化为对应的 AST(抽象语法树),既然涉及到树结构,那么这个步骤自然少不了递归操作。

整体框架

通用的,语法分析部分也需要先搭出一个框架。整体思路就是遍历 token 数组,递归地构建 AST 树,框架如下:

function parser(tokens) {
  let current = 0;

  function walk() {
    let token = tokens[current];

    // 将不同的 token 转化为 AST 节点

    throw new TypeError(token.type);
  }

  let ast = {
    // 此为 AST 树最外层结构,是固定的
    type: "Program",
    body: [],
  };

  while (current < tokens.length) {
    // 遍历 token 数组,构建树结构
    ast.body.push(walk());
  }

  return ast;
}

构建数字和字符串节点

这两种节点的构建较为简单,直接返回描述节点的对象即可:

// 构建整数节点
if (token.type === "number") {
  current++;
  return {
    type: "NumberLiteral",
    value: token.value,
  };
}
// 构建字符串节点
if (token.type === "string") {
  current++;
  return {
    type: "StringLiteral",
    value: token.value,
  };
}

构建函数调用节点

懂 Lisp 的人都知道,在 Lisp 中括号是精髓,比如函数调用类似于这种形式: (add 1 2)。因此我们需要以括号来进行识别,具体的代码如下:

if (token.type === "paren" && token.value === "(") {
  // 左括号开始
  token = tokens[++current]; // 跳过左括号
  let node = {
    // 函数调用节点
    type: "CallExpression",
    name: token.value,
    params: [],
  };
  token = tokens[++current]; // 跳过 name

  // 只要不是右括号,就递归收集参数节点
  while (!(token.type === "paren" && token.value === ")")) {
    node.params.push(walk()); // 添加参数
    token = tokens[current];
  }

  current++; // 跳过右括号
  return node;
}

这里面需要注意的点是,某一个参数也可能是函数调用的结果,因此在解析参数时需要递归调用 walk 函数。

还有另外一点值得一提,那就是我们多次用到了这种代码结构:

if (value === "(") {
  // ...
  while (!value === ")") {
    // ...
  }
}

很明显,这种结构就是适用于遍历某个区间,因此我们在分析字符串、括号这种配对元素时就需要使用这种结构。

小总结

就进行这样简单的几个步骤,前面的 token 数组就会被我们转化成 AST 树结构了,感觉还是非常的神奇,此时,我们的输出以及编程了如下这样:

{
  "type": "Program",
  "body": [
    {
      "type": "CallExpression",
      "name": "add",
      "params": [
        {
          "type": "NumberLiteral",
          "value": "2"
        },
        {
          "type": "CallExpression",
          "name": "subtract",
          "params": [
            {
              "type": "NumberLiteral",
              "value": "4"
            },
            {
              "type": "NumberLiteral",
              "value": "2"
            }
          ]
        }
      ]
    }
  ]
}

遍历并转化 AST 树

此时我们已经得到了一棵 AST 树,编译器之所以能够将源代码转化为目标代码实际上就可以视作将源 AST 树转化为目标 AST 树,要实现这种转化过程,我们就需要对树进行遍历,然后对对应的节点进行操作。

遍历树

我们从上面可以看出,AST 树中的 body 属性和函数调用的参数实际上都是数组类型的,因此我们首先需要定义对数组类型的遍历方法,很简单,只需要遍历数组中的每个元素分别进行遍历就行了:

// 访问(参数)数组
function traverseArray(array, parent) {
  array.forEach((child) => traverseNode(child, parent));
}

当遍历到具体的节点时,我们就需要调用此节点类型的 enter 方法来进行访问(转化 AST)操作,不同类型的节点 enter 方法是不一样的。

function traverseNode(node, parent) {
  let method = visitor[node.type]; // 去除当前类型的方法

  if (method && method.enter) {
    // 执行对应 enter 方法
    method.enter(node, parent);
  }

  switch (
    node.type // 对不同类型节点执行不同的遍历操作
  ) {
    case "Program":
      traverseArray(node.body, node);
      break;
    case "CallExpression":
      traverseArray(node.params, node);
      break;
    case "NumberLiteral":
    case "StringLiteral":
      break;
    default:
      throw new TypeError(node.type);
  }
}

可能有人又要问,为什么执行 enter 方法时第二个参数需要传入父节点呢?这其实和后面的实际转化部分的逻辑相关,我们就放到后面来进行解释。

转化 AST 树

整体框架

一样的,我们可以首先搭出大体的框架,具体的同类型的节点访问(转化)方法后面再说。这里的转化思路就比较重要了:我们要如何在遍历旧的 AST 树时能将转化后的节点加入新的 AST 树?

这里的实现思路大体分为以下几步:

  1. 在旧的 AST 树中加入一个 _context 上下文属性,指向新的 AST 树的数组节点
  2. 当遍历旧 AST 数组节点的子元素时,将转化后的子元素放入它的父元素的 _context 属性中
  3. 根据 JavaScript 引用类型的特点,此时就实现了将转化和的节点放入新 AST 树的目的。

在图中表示出来大概如下:

我相信这已经回答了上面执行 enter 方法时为什么第二个参数需要传入父节点的问题。

function transformer(ast) {
  let newAst = {
    // 新 AST 树的最外层结构
    type: "Program",
    body: [],
  };

  // _context 用于遍历旧子节点时压入新 ast
  ast._context = newAst.body;

  let visitor = {
    // 不同类型的节点访问(转化)方法
  };

  traverser(ast, visitor); // 开始遍历旧 AST 树
  return newAst;
}

数字和字符串的转化

{
  NumberLiteral: {
    enter(node, parent) {
      parent._context.push({ // 压入新 AST 树
        type: "NumberLiteral",
        value: node.value,
      });
    },
  },
  StringLiteral: {
    enter(node, parent) {
      parent._context.push({
        type: "StringLiteral",
        value: node.value,
      });
    },
  }
}

函数调用节点的转化

函数调用节点特殊一点,由于它的参数可以视作它的子节点,因此需要将当前节点的 _context 属性指向新 AST 树对应的参数数组。

还有一点特殊的是,如果当前的函数调用不是嵌套在别的函数调用中,那么就可以再加一个 ExpressionStatement 信息,表示当前节点是一整个语句,比如 (add 2 (subtract 4 2)) 内层的括号就不能称作一个完整的语句,因为它是作为另一个函数的参数形式存在的。

{
  CallExpression: {
    enter(node, parent) {
      let expression = { // 新 AST 树的函数调用节点
        type: "CallExpression",
        callee: {
          type: "Identifier",
          name: node.name,
        },
        arguments: [],
      };

      node._context = expression.arguments; // 参数数组处理

      // 如果当前的函数调用不是嵌套在别的函数调用中
      if (parent.type !== "CallExpression") {
        expression = {
          type: "ExpressionStatement",
          expression: expression,
        };
      }

      parent._context.push(expression);
    },
  },
}

小总结

截至目前,我们已经完成了 AST 树的遍历和转化工作,这部分的工作量不小,但是也是整个编译中最精华的部分,如果顺利的话,我们现在可以得到如下转化后的新 AST 树:

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "Identifier",
          "name": "add"
        },
        "arguments": [
          {
            "type": "NumberLiteral",
            "value": "2"
          },
          {
            "type": "CallExpression",
            "callee": {
              "type": "Identifier",
              "name": "subtract"
            },
            "arguments": [
              {
                "type": "NumberLiteral",
                "value": "4"
              },
              {
                "type": "NumberLiteral",
                "value": "2"
              }
            ]
          }
        ]
      }
    }
  ]
}

这就是对应 C 代码的 AST 树的结构了,将它与之前 Lisp 的 AST 树相比,还是可以看出很多不同的。

代码生成

最后,就是最激动人心的时刻了,生成目标代码!这一步相对轻松,根据上一步生成的 AST 树,对它进行递归遍历并生成最终的代码:

function codeGenerator(node) {
  switch (node.type) {
    case "Program":
      return node.body.map(codeGenerator).join("\n");
    case "ExpressionStatement":
      return `${codeGenerator(node.expression)};`;
    case "CallExpression": // 生成函数调用式
      return `${codeGenerator(node.callee)}(${node.arguments
        .map(codeGenerator)
        .join(", ")})`;
    case "Identifier": // 生成变量名
      return node.name;
    case "NumberLiteral":
      return node.value; // 生成数字
    case "StringLiteral":
      return `"${node.value}"`; // 生成字符串(别忘了两边的引号)
    default:
      throw new TypeError(node.type);
  }
}

最终,我们实现了从 Lisp 的示例代码 (add 2 (subtract 4 2)) 到 C 语言代码 add(2, subtract(4, 2)) 的转化。

大总结

本篇文章带大家从零实现了一个编译器最基本的功能,涉及了词法分析、语法分析、AST 树遍历转化等内容。编译原理听似高深(确实高深),但是基础的部分就是那些内容,啥词法分析语法分析的,最终都会回归到对字符串的处理。

我研究的方向是前端,那别人可能认为平时可能都不会涉及到编译原理的内容,但是实际上一旦深入研究的话,类似 Babel 将 ES6+ 的代码转化为 ES5 之类的工作实际上都是编译器做的工作,还有最近很火的 esbuild,只要涉及到代码的转化,肯定都会涉及编译,甚至 Vue 内部也有一个编译器用于模板编译。

说了这么多,本意还是希望大家在平时的学习中要多多涉猎新领域的知识,扩展自己的技能面,这样才能提高自己的技术视野和上限。

最后,推荐一个我最近在学习的最简 Vue 模型项目,也可以在这里面学习到 Vue 中模板编译的原理。

http://github.com/cuixiaorui/mini-vue

(全文终)

本文转载于网络 如有侵权请联系删除

相关文章

  • 如何用手机快速制作好看的二维码

    由于二维码的出现,给我们的生活带来了很大的方便。由于工作的原因,小编经常会看到一些制作精美的二维码,很是好奇他们是怎么制作出如此好看的二维码的。为了给大家推荐真正好用的二维码制作软件,小编对比了市面上好多类似的软件,发现了一款还不错的App--二维码和条形码生成器,支持鸿蒙、安卓、苹果等各种手机。下面就让小编给大家详细介绍下如何用“二维码和条形码生成器”来制作精美的二维码吧。下载直接在手机的应用市场里搜索:二维码和条形码生成器(⚠️小米、魅族、三星请在应用市场里搜索:qrbar)。下载时请认准它的图标:视频教程视频内容图文教程打开app首页,点击“创建二维码”按钮。在二维码创建界面,你可以选择创建的类型,支持普通文本类型二维码、电话类型二维码、短信类型二维码、日历类型二维码、联系人类型二维码、网址类型二维码、邮箱类型二维码等等类型。输入对应的内容后,直接点击右下角的创建按钮即可。在二维码编辑界面,可以给二维码添加logo,支持自定义logo的设置,你可以选取手机相册里的任何一张图片当logo;还支持设置二维码外观,支持从相册里选择任何一张图片作为二维码的外观;还支持给二维码添加文字、设

  • Apache Web 服务器配置多个站点

    正如我之前的文章中提到的,Apache的所有配置文件都位于/etc/httpd/conf和/etc/httpd/conf.d。默认情况下,站点的数据位于/var/www中。对于多个站点,你需要提供多个位置,每个位置对应托管的站点。基于名称的虚拟主机使用基于名称的虚拟主机,你可以为多个站点使用一个IP地址。现代Web服务器,包括Apache,使用指定URL的hostname部分来确定哪个虚拟Web主机响应页面请求。这仅仅需要比一个站点更多的配置。即使你只从单个站点开始,我也建议你将其设置为虚拟主机,这样可以在以后更轻松地添加更多站点。在本文中,我将从上一篇文章中我们停止的地方开始,因此你需要设置原来的站点,即基于名称的虚拟站点。准备原来的站点在设置第二个站点之前,你需要为现有网站提供基于名称的虚拟主机。如果你现在没有站点,请返回并立即创建一个。一旦你有了站点,将以下内容添加到/etc/httpd/conf/httpd.conf配置文件的底部(添加此内容是你需要对httpd.conf文件进行的唯一更改):<VirtualHost127.0.0.1:80>  DocumentRo

  • 现阶段的语音视频通话SDK需要解决哪些问题?

    疫情让语音视频通话在越来越多的行业被广泛使用,而以后,语音视频通话在企业内的运用也会越来越广泛,比如出差、外派等工作情况的出现,语音视频通话也能够提供很大的帮助。有的企业会选择自己进行开发,但是这将耗费大量的时间成本和人力成本,所以更多的企业倾向于寻求专业公司的帮助。目前,市面上的语音视频通话都存在着杂音、卡顿甚至不兼容等通病:1.杂音在语音视频中,杂音是很常见,在复杂的语音环境下通话时,往往会出现噪音或者回声等,而这类声音会对原本的声源产生影响,导致在另一端接收到这段语音时产生杂音,当杂音较多时,原本的语音就很难被听清。2.卡顿无论在语音还是视频中,卡顿大概是最致命的,优秀的音视频通话首先就应该保证音视频通话的流畅性,音视频通话时产生卡顿大多都是网络环境的不稳定引起的,所以如何在复杂的网络环境下都能保证音视频通话的流畅性,是一个优秀的音视频语音SDK必备的。3.不兼容目前,市面上的移动端基本是以iOS系统和Android系统两分天下,iOS系统的兼容性相对好实现,但是Android系统的兼容却是很复杂的,主要是由于Android系统的多版本和多终端,导致Android手机适配的复杂性

  • 大型项目技术栈第四讲 SQL语句构建器

    SQL语句构建器1.问题Java程序员面对的最痛苦的事情之一就是在Java代码中嵌入SQL语句。这通常是因为需要动态生成SQL语句,不然我们可以将它们放到外部文件或者存储过程中。Stringsql="SELECTP.ID,P.USERNAME,P.PASSWORD,P.FULL_NAME," "P.LAST_NAME,P.CREATED_ON,P.UPDATED_ON"+ "FROMPERSONP,ACCOUNTA"+ "INNERJOINDEPARTMENTDonD.ID=P.DEPARTMENT_ID"+ "INNERJOINCOMPANYConD.COMPANY_ID=C.ID"+ "WHERE(P.ID=A.IDANDP.FIRST_NAMElike?)"+ "OR(P.LAST_NAMElike?)"+ "GROUPBYP.ID"+ "HAVING(P.LAST_NAMElike?)"+ "

  • 《腾讯政务协同平台安全白皮书》发布,助力“智慧政务”夯实安全底座

    当前“数字政务”建设正广泛开展,政务协同平台因承载了大量的政务敏感数据和关键业务应用,其安全性至关重要。但由于政务服务关键业务环节比较依赖线下场景,而政务服务涉及面广、实际应用场景复杂的特点也导致各部门之间因使用的平台软件不统一而容易出现“数据孤岛”、“安全缺口”等问题,严重影响了政务协同平台的系统安全和正常使用。今日,腾讯云立足腾讯政务协同平台,同步正式发布《腾讯政务协同平台安全白皮书》(以下简称《白皮书》),将在终端、网络、主机、应用、数据等方面的安全技术能力和对安全合规性的理解,聚合应用到政务协同平台场景中,打造安全可信的一体化政务协同安全解决方案,提升数字政务风险防范能力,为数字政府建设提供基础安全支撑。早在7月份,腾讯政务便召开战略升级发布会,并升级发布了重磅产品“一网统管”和“政务协同”平台,进一步助力政府的数字化管理和协同。关注腾讯安全(公众号TXAQ2019)回复政务协同获取白皮书政务协同数字化提速,安全已成为底座支撑腾讯根据过往大量的数字政务经验总结,利用数字化工具加强工作协同能力提升行政效能,已是各地数字政府打造的重点环节。腾讯云政务协同平台,基于政务微信统一建设的

  • Google搜索引擎小技巧

    本文参考:https://www.williamlong.info/archives/728.html搜索特定的词组当你搜索一个特定词组时,如果你只是简单地输入词组中所有的词你是无法得到最好的结果的。Google也许能够反馈出包含这个词组的结果,但它也会列出包含你所输入所有词的结果,却未必让这些词按照正确的顺序。如果你要搜索一个特定的词组,你应该将整个词组放在一个引号内。这样就能让Google搜索规定顺序的精确的关键词。例如,如果你要搜索“MontyPython”,你可以输入montypython作为你的搜索要求,接着你也许会获得可接受的结果;这些结果中会包含有着“monty”和“python”两个词的页面。但这些结果并不仅是包含了关于英国喜剧团体的页面,还包括了名叫Monty的蛇以及名叫Monty的家伙,他养了蛇来当宠物,还有其它一些包括了“monty”和“python”的词的页面,即使它们之间看起来似乎毫无关联。为了将搜索结果限定在只关于MontyPython喜剧团之内,也就是你想要搜索的页面是按规定的顺序,将这两个词作为一个词组包含在内的,你就应该在输入搜索要求时输入"

  • 2018年微服务的5个发展趋势

    原文作者:AstasiaMyers原文地址:https://medium.com/memory-leak/5-microservices-trends-to-watch-in-2018-aed135f70e51?source=search_post对于DevOps来说,2017年是重要的一年,因为生态系统参与者数量大幅增加,CNCF项目增加了两倍。展望未来,我们预计创新和市场变化将进一步加速。下面我们将讨论2018年微服务趋势:服务网格、事件驱动架构、容器本地安全性、GraphQL和混沌工程(chaosengineering)。在未来一年(2018)中,我们将关注这些趋势以及围绕这些趋势建立业务的公司。你看到什么趋势?如果你同意/不同意我们在这里概述的那些,请在下面留言,告诉我们错过了什么。1.服务网格(Servicemeshes)很火!服务网格是一个专门用于改进服务到服务通信的基础设施层,目前是云计算领域最热门的一个类别。随着容器变得越来越流行,服务拓扑变得越来越动态,需要改进网络功能。服务网格可以通过服务发现、路由、负载平衡、健康检查和可观察性来帮助管理流量。服务网格试图减少不规范

  • Hyperledger Fabric 积分代币上链方案

    本文节选自电子书《NetkillerBlockchain手札》NetkillerBlockchain手札本文作者提供有偿顾问服务,有意向致电13113668890Mr. NeoChan, 陈景峯(BG7NYT)中国广东省深圳市龙华新区民治街道溪山美地518131+86 13113668890<netkiller@msn.com>文档始创于2018-02-10版权©2018Netkiller(NeoChan).Allrightsreserved.版权声明转载请与作者联系,转载时请务必标明文章原始出处和作者信息及本声明。微信订阅号netkiller-ebook(微信扫描二维码)QQ:13721218请注明“读者”QQ群:128659835请注明“读者”网站:http://www.netkiller.cn内容摘要这一部关于区块链开发及运维的电子书。为什么会写区块链电子书?因为2018年是区块链年。这本电子书是否会出版(纸质图书)?不会,因为互联网技术更迭太快,纸质书籍的内容无法实时更新,一本书动辄百元,很快就成为垃圾,你会发现目前市面的上区块链书籍至少是一年前写的,内容已经过时,

  • Matlab基本语法5

    二维数据可视化1.基本绘图函数plot(y):如果是复数向量,则以实部为横坐标,以虚部为纵坐标plot(x,y)plot(x,y,s):s表示字符串标记plot(x1,y1,s1,...)2.子图的绘制subplot(mnp)或者subplot(m,n,p):共m行,每行n个图3.设置坐标轴axis(xminxmaxyminymax):定义x轴和y轴的范围axis(xminxmaxyminymaxzminzmax):定义x轴和y轴和z轴的范围axis(xminxmaxyminymaxzminzmaxcmincmax):定义x轴和y轴和z轴的范围,以及图形的颜色信息axisoff取消坐标轴显示4.网格线和边框gridon/off:添加/取消网格线gridminor:设置网格间的间距boxon/off:添加或者取消坐标轴的边框5.坐标轴的缩放zoom(factor):作为缩放因子进行坐标轴的缩放zoomon/off:允许/禁止对坐标轴缩放6.图形的拖拽panon/offpanxon/yon:在x轴/y轴方向拖拽7.数据光标datacursormodeon/off:该函数打开或者关闭数据光比

  • 搭建Java环境JDK,和运行环境JRE

    1:想要学习Java第一步就是搭建Java环境,就是安装JDK,又因为JDK里面包含JRE,所以在安装JDK的过程中就安装了JRE,所以以下只是给出了JDK的安装包,自行下载安装即可链接:http://pan.baidu.com/s/1hrREdUk密码:r6v0(安装过程中比较简单,在此省过)2:重点说一下环境变量配置首先打开控制面板->系统安全->系统->高级系统设置然后点击环境变量然后点击环境之后如下图,你可以选择编辑或者新建(存在的就编辑,不存在就新建)再然后在" 系统变量 "中设置3项属性,JAVA_HOME,PATH,CLASSPATH(大小写无所谓),若已存在则点击"编辑",不存在则点击"新建"。变量设置参数如下:变量名:JAVA_HOME变量值:D:\ProgramFiles(x86)\Java\jdk1.8.0_91    //这个路径就是自己安装JDK安装包的路径,你的JDK安装在什么路径就填什么就可以了变量名:CLASSPATH变量值:.;%JAVA_HOME%\lib\dt.jar;%

  • windows无法连接到打印机错误为0x000000011b_无法连接到打印机错误0000011b

    大家好,又见面了,我是你们的朋友全栈君。 最近打印机连不上,查了下网上的资料,发现是Windows10的一个更新bug导致,但是按照网上的方法视乎重启后windows会强制更新,还是无法彻底解决问题。于是在继续查找到相关资料,现在将解决方法记录下来。注意:以下操作只需要在打印机连接的那台电脑上修改即可,其他电脑无需操作一、打开注册表按住win和R键,输入regedit打开注册表二、修改依次打开[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print]结构如下HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlPrint然后鼠标右键—新建—DWORD(32位)值(D)输入RpcAuthnLevelPrivacyEnabled然后右键修改值为0,然后重启你的电脑就可以了版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至举报,一经查实,本站将立刻删除。发布者:全栈程

  • SpringBoot自定义starters

    1自定义starter的原理 1️⃣这个场景需要使用的依赖是什么? 2️⃣如何编写自动配置。 @Configuration//指定这个类是配置 @ConditionalOnXxx//在指定条件成立的情况下自动配置类生成 @AutoConfigureAfter//指定自动配置类的顺序 @Bean//给容器中添加组件 @ConfigurationProperties结合相关的XxxxProperties类来绑定相关的配置 @EnableConfigurationProperties//让XxxProperties类生效,并加入到容器中 自动配置类要能加载,需要将其配置到META-INF/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoc

  • python 自动认证登录

    importurllib importbase64 importurllib2 defauto_login(urllink,username,password): authstr='Basic%s'%base64.encodestring('%s:%s'%(username,password))[:-1] req=urllib2.Request(urllink) req.add_header('Authorization',authstr) returnurllib2.urlopen(req) if__name__=='__main__': handle=auto_login('http://192.168.1.1','admin','password') printhandle.read() 复制  注意:要使用URLlib2中的urlopen,urllib中的urlopen不支持request对象

  • 知识库总结mysql常用cmd命令

    打开命令目录  打开D盘mysql目录               d:             cd D:\Ampps\mysql\bin 常用操作 将mysql目录下bin目录中的mysql.exe放到C:\WINDOWS下,可以执行以下命令 连接:mysql-h主机地址-u用户名-p用户密码(注:u与root可以不用加空格,其它也一样) 断开:exit(回车) 创建授权:grantselecton数据库.*to用户名@登录主机identifiedby\"密码\" 修改密码:mysqladmin-u用户名-p旧密码password新密码 删除授权:revokeselect,insert,update,deleteom*.*fromtest2@localhost; 显示数据库:showdataba

  • 查询SQL Server执行过的SQL语句(执行次数)

    执行语句: SELECTTOP2000 ST.textAS'执行的SQL语句', QS.execution_countAS'执行次数', QS.total_elapsed_timeAS'耗时', QS.total_logical_readsAS'逻辑读取次数', QS.total_logical_writesAS'逻辑写入次数', QS.total_physical_readsAS'物理读取次数', QS.creation_timeAS'执行时间', QS.* FROMsys.dm_exec_query_statsQS CROSSAPPLY sys.dm_exec_sql_text(QS.sql_handle)ST WHEREQS.creation_timeBETWEEN'2020-06-1100:00:00'AND'2020-06-1200:00:00' ORDERBY QS.total_elapsed_timeDESC复制 效果图:   PS:使用以上语句的意义在于找到那些语句被执行的次数比较频繁,进而优化接口,然后找出那些sql语句执行时间长,进而做性能分析,加入

  • c++ 适配器

    #include <bits/stdc++.h>using namespace std;const int MAX = 1e5+10;vector<int> filter(const vector<int> &vec,int val,less<int> &lt) {    vector<int> nvec;    vector<int> :: const_iterator iter=vec.begin();    while((iter=find_if(iter,vec.end(),bind2nd(less<int>(),val)))!=vec.end()) {  &nbs

  • 3.MongoDB下Windows下的安装

    由于博主目前使用的是Windows的系统,没有使用Linux等其它的系统,因此此安装配置和开发使用,均是在Windows下进行的,以后在使用其它的系统的时候,再将其它系统的配置的使用补充上来。 1.下载,直接从http://www.mongodb.org/downloads 下载需要的版本即可 2.解压,下载好MongoDB数据后,将此解压至C:\MongoDB下面,直接将mongodb-win32-x86_64-2008plus-2.4.8目录中文件的解压至了MongoDB,这样的目录看起来还简洁不少。 3.创建数据库文件的存放位置,比如c:/mongodb/dbData。启动mongodb服务之前需要必须创建数据库文件的存放文件夹,否则命令不会自动创建,而且不能启动成功。默认文件夹路径为c:/data/db.使用系统默认文件夹路径时,启动服务无需加--dbpath参数说明,但文件夹还要手工创建 4.运行,打开cmd命令行,进入C:/MongoDB/bin目录,输入如下的命令启动mongodb服务:   此时MongoDB数据库已经成功运行,最下面显示的一条1c

  • 医院信息集成平台项目建设方案与实践 第4章 项目建设设计(一)

    4.1 项目总体建设原则 医院信息平台体系架构遵循以下原则: 复制 ◼ 基于医院信息化现状,实现信息共享与业务协同。即平台的建设不是一个推翻现有应用重 建的过程,而是基于现有信息系统和现有的系统数据,通过医院信息平台来整合信息,并 实现系统之间的业务协同。 复制 ◼ 基于企业信息架构分层设计思路。按照企业信息架构理论和方法,以分层的方式设计医院 信息平台,不同的层次解决不同的问题。 复制 ◼ 全面支持电子病历相关业务规范与标准体系。从数据层面遵循《电子病历基本架构与数据 标准》,即医院信息平台上保存的电子病历数据符合该标准;在电子病历生成和使用上符 合电子病历相关业务规范。 复制 ◼ 具备良好的稳定性、可靠性、完整性、可扩展性。 ◼ 具备异常应变和容错能力,确保系统安全可靠,提供运行情况实施监控。 ◼ 统筹规划、分布实施原则,先整体设计医院信息集成平台建设框架,进行基础性模块建设, 然后进行基于集成平台进行应用性建设如 CDR 等。 4.2 平台总

  • ubuntu 16.04下搭建web服务器(MySQL+PHP+Apache) 教程

      1.开始说明 下面很多可能参照网上其中以为前辈的,但有所改进吧。这些设置可能会有所不同,你需要根据不同情况进行修改。 安装apache2 2.切换管理员身份 在ubuntu中需要用root身份进行操作,所以用下面的命令确保以root身份登录: sudosu 3.开始安装mysql5 apt-getinstallmysql-servermysql-client 你将被要求提供一个mysql的root用户的密码,我们需要在红色区域设置密码。 newpasswordforthemysqlrootuser:repeatpasswordforthemysqlrootuser: 4.安装apache2 apache2的是作为一个ubuntu的软件包,因此我们可以直接用下面命令安装它: apt-getinstallapache2 现在,您的浏览器到http://localhost,你应该看到apache2的测试页: 如果顺利的话会出现: Itworke! 然后下面后有点。 apache的默认文档根目录是在ubuntu上的/var/www目录,配置文件是/etc/apache2/ap

  • 随机梯度下降法

    当不同特征中数据范围不一致时,最好对特征进行归一化处理 在进行求解最优解时,通过调节参数alpha,可以加快求解的过程 特征的选择也是非常重要

  • vue-表单与v-model

    使用v-model后,表羊控件显示的值只依赖所绑定的数据,不再关心初始化时的value 属性,对于textarea></textarea>之间插入的值,也不会生效. 使用v-model时,如果是用中文输入法输入中文,一般在没有选定饲组前,也就是在 拼音阶段,Vue是不会更新数据的,当敲下汉字时才会触发史新如果希望总是实时 更新,可以用@input来替代v-model事实上,v-model也是一个特殊的语法糖,只 不过它会在不同的表单上智能处理。例如下面的示例: 新鲜刺激的东西永远都有,玩之前掂量掂量自己几斤几两

相关推荐

推荐阅读