js ...运算符_「 giao-js 」用js写一个js解释器

a3cc22165a4596d700a6dbd345882c8c.png

前言

在这篇文章中,我们将通过 JS 构建我们自己的 JS 解释器,用 JS 写 JS,这听起来很奇怪,尽管如此,这样做我们将更熟悉 JS,也可以学习 JS 引擎是如何工作的!

什么是解释器 (Interpreter) ?

解释器是在运行时运行的语言求值器,它动态地执行程序的源代码。 解释器解析源代码,从源代码生成 AST(抽象语法树),遍历 AST 并逐个计算它们。

解释器 (Interpreter) 工作原理

2c3e2b353d6f5fc118c6248cdc32d0af.png
  • 词法分析 (Tokenization)
  • 语法解析 (Parsing)
  • 求值 (Evaluating)

词法分析 (Tokenization)

将源代码分解并组织成一组有意义的单词,这一过程即为词法分析(Token)。

在英语中,当我们遇到这样一个语句时:

Javascript is the best language in the world

我们会下意识地把句子分解成一个个单词:

+----------------------------------------------------------+
| Javascript | is | the | best | language | in |the |world |
+----------------------------------------------------------+

这是分析和理解句子的第一阶段。

词法分析是由词法分析器完成的,词法分析器会扫描(scanning)代码,提取词法单元。

var a = 1;[("var": "keyword"),("a": "identifier"),("=": "assignment"),("1": "literal"),(";": "separator"),
];

词法分析器将代码分解成 Token 后,会将 Token 传递给解析器进行解析,我们来看下解析阶段是如何工作的。

语法解析 (Parsing)

将词法分析阶段生成的 Token 转换为抽象语法树(Abstract Syntax Tree),这一过程称之为语法解析(Parsing)。

在英语中,Javascript is the best language 被分解为以下单词:

+------------------------------------------+
| Javascript | is | the | best | language  |
+------------------------------------------+

这样我们就可以挑选单词并形成语法结构:

"Javascript": Subject
"is the best language": Predicate
"language": Object

Javascript 在语法中是一个主语名词,其余的是一个没有什么意义的句子叫做谓语,language 是动作的接受者,也就是宾语。结构是这样的:

Subject(Noun) -> Predicate -> Object

语法解析是由语法解析器完成的,它会将上一步生成的 Token,根据语法规则,转为抽象语法树(AST)。

{type: "Program",body: [{type: "VariableDeclaration",declarations: [{type: "VariableDeclarator",id: {type: "Identifier",name: "sum"},init: {type: "Literal",value: 30,raw: "30"}}],kind: "var"}],
}

求值阶段 (Evaluating)

解释器将遍历 AST 并计算每个节点。- 求值阶段
1 + 2
||v
+---+  +---+
| 1 |  | 2 |
+---+  +---+///+---+| + |+---+
{lhs: 1,op: '+'.rhs: 2
}

解释器解析 Ast,得到 LHS 节点,接着收集到操作符(operator)节点+,+操作符表示需要进行一次加法操作,它必须有第二个节点来进行加法操作.接着他收集到 RHS 节点。它收集到了有价值的信息并执行加法得到了结果,3。

{type: "Program",body: [{type: "ExpressionStatement",expression: {type: "BinaryExpression",left: {type: "Literal",value: 1,raw: "1"},operator: "+",right: {type: "Literal",value: 2,raw: "2"}}}],
}

实践

前面我们已经介绍了解释器的工作原理,接下来我们来动动手松松筋骨吧,实现一个 Mini Js Interpreter~

实践准备

  • Acorn.js
A tiny, fast JavaScript parser, written completely in JavaScript. 一个完全使用 javascript 实现的,小型且快速的 javascript 解析器

本次实践我们将使用 acorn.js ,它会帮我们进行词法分析,语法解析并转换为抽象语法树。

Webpack/Rollup/Babel(@babel/parser) 等第三方库也是使用 acorn.js 作为自己 Parser 的基础库。(站在巨人的肩膀上啊!)

  • The Estree Spec

最开始 Mozilla JS Parser API 是 Mozilla 工程师在 Firefox 中创建的 SpiderMonkey 引擎输出 JavaScript AST 的规范文档,文档所描述的格式被用作操作 JAvaScript 源代码的通用语言。

随着 JavaScript 的发展,更多新的语法被加入,为了帮助发展这种格式以跟上 JavaScript 语言的发展。The ESTree Spec 就诞生了,作为参与构建和使用这些工具的人员的社区标准。

acorn.js parse 返回值符合 ESTree spec 描述的 AST 对象,这里我们使用@types/estree 做类型定义。

  • Jest

号称令人愉快的 JavaScript 测试...我们使用它来进行单元测试.

  • Rollup

Rollup 是一个 JavaScript 模块打包器,我们使用它来打包,以 UMD 规范对外暴露模块。

项目初始化

// visitor.ts 创建一个Visitor类,并提供一个方法操作ES节点。
import * as ESTree from "estree";
class Visitor {visitNode(node: ESTree.Node) {// ...}
}
export default Visitor;
// interpreter.ts 创建一个Interpreter类,用于运行ES节点树。
// 创建一个Visitor实例,并使用该实例来运行ESTree节点
import Visitor from "./visitor";
import * as ESTree from "estree";
class Interpreter {private visitor: Visitor;constructor(visitor: Visitor) {this.visitor = visitor;}interpret(node: ESTree.Node) {this.visitor.visitNode(node);}
}
export default Interpreter;
// vm.ts 对外暴露run方法,并使用acorn code->ast后,交给Interpreter实例进行解释。
const acorn = require("acorn");
import Visitor from "./visitor";
import Interpreter from "./interpreter";const jsInterpreter = new Interpreter(new Visitor());export function run(code: string) {const root = acorn.parse(code, {ecmaVersion: 8,sourceType: "script",});return jsInterpreter.interpret(root);
}

实践第 1 弹: 1+1= ?

我们这节来实现 1+1 加法的解释。首先我们通过AST explorer,看看 1+1 这段代码转换后的 AST 结构。

9a9dabe82b0ead85ac9adaee360a0d5f.png

我们可以看到这段代码中存在 4 种节点类型,下面我们简单的介绍一下它们:

Program

根节点,即代表一整颗抽象语法树,body 属性是一个数组,包含了多个 Statement 节点。

interface Program {type: "Program";sourceType: "script" | "module";body: Array<Directive | Statement | ModuleDeclaration>;comments?: Array<Comment>;
}

ExpressionStatement

表达式语句节点,expression 属性指向一个表达式节点对象

interface ExpressionStatement {type: "ExpressionStatement";expression: Expression;
}

BinaryExpression

二元运算表达式节点,left 和 right 表示运算符左右的两个表达式,operator 表示一个二元运算符。 本节实现的重点,简单理解,我们只要拿到 operator 操作符的类型并实现,然后对 left,right 值进行求值即可。

interface BinaryExpression {type: "BinaryExpression";operator: BinaryOperator;left: Expression;right: Expression;
}

Literal

字面量,这里不是指 [] 或者 {} 这些,而是本身语义就代表了一个值的字面量,如 1,“hello”, true 这些,还有正则表达式,如 /d?/。

type Literal = SimpleLiteral | RegExpLiteral;interface SimpleLiteral {type: "Literal";value: string | boolean | number | null;raw?: string;
}interface RegExpLiteral {type: "Literal";value?: RegExp | null;regex: {pattern: string;flags: string;};raw?: string;
}

废话少说,开撸!!!

// standard/es5.ts 实现以上节点方法import Scope from "../scope";
import * as ESTree from "estree";
import { AstPath } from "../types/index";const es5 = {// 根节点的处理很简单,我们只要对它的body属性进行遍历,然后访问该节点即可。Program(node: ESTree.Program) {node.body.forEach((bodyNode) => this.visitNode(bodyNode));},// 表达式语句节点的处理,同样访问expression 属性即可。ExpressionStatement(node: ESTree.ExpressionStatement>) {return this.visitNode(node.expression);},// 字面量节点处理直接求值,这里对正则表达式类型进行了特殊处理,其他类型直接返回value值即可。Literal(node: ESTree.Literal>) {if ((<ESTree.RegExpLiteral>node).regex) {const { pattern, flags } = (<ESTree.RegExpLiteral>node).regex;return new RegExp(pattern, flags);} else return node.value;},// 二元运算表达式节点处理// 对left/node两个节点(Literal)进行求值,然后实现operator类型运算,返回结果。BinaryExpression(node: ESTree.BinaryExpression>) {const leftNode = this.visitNode(node.left);const operator = node.operator;const rightNode = this.visitNode(node.right);return {"+": (l, r) => l + r,"-": (l, r) => l - r,"*": (l, r) => l * r,"/": (l, r) => l / r,"%": (l, r) => l % r,"<": (l, r) => l < r,">": (l, r) => l > r,"<=": (l, r) => l <= r,">=": (l, r) => l >= r,"==": (l, r) => l == r,"===": (l, r) => l === r,"!=": (l, r) => l != r,"!==": (l, r) => l !== r,}[operator](leftNode, rightNode);},
};
export default es5;
// visitor.ts
import Scope from "./scope";
import * as ESTree from "estree";
import es5 from "./standard/es5";const VISITOR = {...es5,
};
class Visitor {// 实现访问节点方法,通过节点类型访问对应的节点方法visitNode(node: ESTree.Node) {return {visitNode: this.visitNode,...VISITOR,}[node.type](node);}
}
export default Visitor;

就这样,普通的二元运算就搞定啦!!!

实践第 2 弹: 怎么找到变量?

Javascript 的作用域与作用域链的概念想必大家都很熟悉了,这里就不再啰嗦了~

是的,我们需要通过实现作用域来访问变量,实现作用域链来搜寻标识符。

在这之前,我们先实现 Variable 类,实现变量的存取方法。

// variable.ts
export enum Kind {var = "var",let = "let",const = "const",
}
export type KindType = "var" | "let" | "const";
export class Variable {private _value: any;constructor(public kind: Kind, val: any) {this._value = val;}get value() {return this._value;}set value(val: any) {this._value = val;}
}
import { Variable, Kind, KindType } from "./variable";class Scope {// 父作用域private parent: Scope | null;// 当前作用域private targetScope: { [key: string]: any };constructor(public readonly type, parent?: Scope) {this.parent = parent || null;this.targetScope = new Map();}// 是否已定义private hasDefinition(rawName: string): boolean {return Boolean(this.search(rawName));}// var类型变量定义public defineVar(rawName: string, value: any) {let scope: Scope = this;// 如果不是全局作用域且不是函数作用域,找到全局作用域,存储变量// 这里就是我们常说的Hoisting (变量提升)while (scope.parent && scope.type !== "function") {scope = scope.parent;}// 存储变量scope.targetScope.set(rawName, new Variable(Kind.var, value));}// let类型变量定义public defineLet(rawName: string, value: any) {this.targetScope.set(rawName, new Variable(Kind.let, value));}// const类型变量定义public defineConst(rawName: string, value: any) {this.targetScope.set(rawName, new Variable(Kind.const, value));}// 作用域链实现,向上查找标识符public search(rawName: string): Variable | null {if (this.targetScope.get(rawName)) {return this.targetScope.get(rawName);} else if (this.parent) {return this.parent.search(rawName);} else {return null;}}// 变量声明方法,变量已定义则抛出语法错误异常public declare(kind: Kind | KindType, rawName: string, value: any) {if (this.hasDefinition(rawName)) {console.error(`Uncaught SyntaxError: Identifier '${rawName}' has already been declared`);return true;}return {[Kind.var]: () => this.defineVar(rawName, value),[Kind.let]: () => this.defineLet(rawName, value),[Kind.const]: () => this.defineConst(rawName, value),}[kind]();}
}export default Scope;

以上就是变量对象,作用域及作用域链的基础实现了,接下来我们就可以定义及访问变量了。

实践第 3 弹: var age = 18

b69a94223e041005ac0d7e589e74f110.png

从语法树中我们可以看到三个陌生的节点类型,来看看它们分别代表什么意思:

VariableDeclaration

变量声明,kind 属性表示是什么类型的声明,因为 ES6 引入了 const/let。 declarations 表示声明的多个描述,因为我们可以这样:let a = 1, b = 2;。

interface VariableDeclaration {type: "VariableDeclaration";declarations: Array<VariableDeclarator>;kind: "var" | "let" | "const";
}

VariableDeclarator

变量声明的描述,id 表示变量名称节点,init 表示初始值的表达式,可以为 null。

interface VariableDeclarator {type: "VariableDeclarator";id: Pattern;init?: Expression | null;
}

Identifier

顾名思义,标识符节点,我们写 JS 时定义的变量名,函数名,属性名,都归为标识符。

interface Identifier {type: "Identifier";name: string;
}

了解了对应节点的含义后,我们来进行实现:

// standard/es5.ts 实现以上节点方法import Scope from "../scope";
import * as ESTree from "estree";type AstPath<T> = {node: T;scope: Scope;
};const es5 = {// ...// 这里我们定义了astPath,新增了scope作用域参数VariableDeclaration(astPath: AstPath<ESTree.VariableDeclaration>) {const { node, scope } = astPath;const { declarations, kind } = node;// 上面提到,生声明可能存在多个描述(let a = 1, b = 2;),所以我们这里对它进行遍历:// 这里遍历出来的每个item是VariableDeclarator节点declarations.forEach((declar) => {const { id, init } = <ESTree.VariableDeclarator>declar;// 变量名称节点,这里拿到的是ageconst key = (<ESTree.Identifier>id).name;// 判断变量是否进行了初始化 ? 查找init节点值(Literal类型直接返回值:18) : 置为undefined;const value = init ? this.visitNode(init, scope) : undefined;// 根据不同的kind(var/const/let)声明进行定义,即var age = 18scope.declare(kind, key, value);});},// 标识符节点,我们只要通过访问作用域,访问该值即可。Identifier(astPath: AstPath<ESTree.Identifier>) {const { node, scope } = astPath;const name = node.name;// walk identifier// 这个例子中查找的是age变量const variable = scope.search(name);// 返回的是定义的变量对象(age)的值,即18if (variable) return variable.value;},
};
export default es5;

实践第 4 弹: module.exports = 6

我们先来看看 module.exports = 6 对应的 AST。

21e81161c36c64131654add64e62557e.png

从语法树中我们又看到两个陌生的节点类型,来看看它们分别代表什么意思:

AssignmentExpression

赋值表达式节点,operator 属性表示一个赋值运算符,left 和 right 是赋值运算符左右的表达式。

interface AssignmentExpression {type: "AssignmentExpression";operator: AssignmentOperator;left: Pattern | MemberExpression;right: Expression;
}

MemberExpression

成员表达式节点,即表示引用对象成员的语句,object 是引用对象的表达式节点,property 是表示属性名称,computed 如果为 false,是表示 . 来引用成员,property 应该为一个 Identifier 节点,如果 computed 属性为 true,则是 [] 来进行引用,即 property 是一个 Expression 节点,名称是表达式的结果值。

interface MemberExpression {type: "MemberExpression";object: Expression | Super;property: Expression;computed: boolean;optional: boolean;
}

我们先来定义 module.exports 变量。

import Scope from "./scope";
import Visitor from "./visitor";
import * as ESTree from "estree";
class Interpreter {private scope: Scope;private visitor: Visitor;constructor(visitor: Visitor) {this.visitor = visitor;}interpret(node: ESTree.Node) {this.createScope();this.visitor.visitNode(node, this.scope);return this.exportResult();}createScope() {// 创建全局作用域this.scope = new Scope("root");// 定义module.exportsconst $exports = {};const $module = { exports: $exports };this.scope.defineConst("module", $module);this.scope.defineVar("exports", $exports);}// 模拟commonjs,对外暴露结果exportResult() {// 查找module变量const moduleExport = this.scope.search("module");// 返回module.exports值return moduleExport ? moduleExport.value.exports : null;}
}
export default Interpreter;

ok,下面我们来实现以上节点函数~

// standard/es5.ts 实现以上节点方法import Scope from "../scope";
import * as ESTree from "estree";type AstPath<T> = {node: T;scope: Scope;
};const es5 = {// ...// 这里我们定义了astPath,新增了scope作用域参数MemberExpression(astPath: AstPath<ESTree.MemberExpression>) {const { node, scope } = astPath;const { object, property, computed } = node;// property 是表示属性名称,computed 如果为 false,property 应该为一个 Identifier 节点,如果 computed 属性为 true,即 property 是一个 Expression 节点// 这里我们拿到的是exports这个key值,即属性名称const prop = computed? this.visitNode(property, scope): (<ESTree.Identifier>property).name;// object 表示对象,这里为module,对module进行节点访问const obj = this.visitNode(object, scope);// 访问module.exports值return obj[prop];},// 赋值表达式节点(astPath: AstPath<ESTree.>) {const { node, scope } = astPath;const { left, operator, right } = node;let assignVar;// LHS 处理if (left.type === "Identifier") {// 标识符类型 直接查找const value = scope.search(left.name);assignVar = value;} else if (left.type === "MemberExpression") {// 成员表达式类型,处理方式跟上面差不多,不同的是这边需要自定义一个变量对象的实现const { object, property, computed } = left;const obj = this.visitNode(object, scope);const key = computed? this.visitNode(property, scope): (<ESTree.Identifier>property).name;assignVar = {get value() {return obj[key];},set value(v) {obj[key] = v;},};}// RHS// 不同操作符处理,查询到right节点值,对left节点进行赋值。return {"=": (v) => {assignVar.value = v;return v;},"+=": (v) => {const value = assignVar.value;assignVar.value = v + value;return assignVar.value;},"-=": (v) => {const value = assignVar.value;assignVar.value = value - v;return assignVar.value;},"*=": (v) => {const value = assignVar.value;assignVar.value = v * value;return assignVar.value;},"/=": (v) => {const value = assignVar.value;assignVar.value = value / v;return assignVar.value;},"%=": (v) => {const value = assignVar.value;assignVar.value = value % v;return assignVar.value;},}[operator](this.visitNode(right, scope));},
};
export default es5;

ok,实现完毕,是时候验证一波了,上 jest 大法。

// __test__/es5.test.tsimport { run } from "../src/vm";
describe("giao-js es5", () => {test("assign", () => {expect(run(`module.exports = 6;`)).toBe(6);});
}

ea3ead227c6895eaa5308ec4a4385318.png

实践第 5 弹: for 循环

var result = 0;
for (var i = 0; i < 5; i++) {result += 2;
}
module.exports = result;

00b4ba1a372f633a3bc6d4f6c494813e.png

到这一弹大家都发现了,不同的语法其实对应的就是不同的树节点,我们只要实现对应的节点函数即可.我们先来看看这几个陌生节点的含义.

ForStatement

for 循环语句节点,属性 init/test/update 分别表示了 for 语句括号中的三个表达式,初始化值,循环判断条件,每次循环执行的变量更新语句(init 可以是变量声明或者表达式)。 这三个属性都可以为 null,即 for(;;){}。
body 属性用以表示要循环执行的语句。

interface ForStatement {type: "ForStatement";init?: VariableDeclaration | Expression | null;test?: Expression | null;update?: Expression | null;body: Statement;
}

UpdateExpression

update 运算表达式节点,即 ++/--,和一元运算符类似,只是 operator 指向的节点对象类型不同,这里是 update 运算符。

interface UpdateExpression {type: "UpdateExpression";operator: UpdateOperator;argument: Expression;prefix: boolean;
}

BlockStatement

块语句节点,举个例子:if (...) { // 这里是块语句的内容 },块里边可以包含多个其他的语句,所以有一个 body 属性,是一个数组,表示了块里边的多个语句。

interface BlockStatement {0;type: "BlockStatement";body: Array<Statement>;innerComments?: Array<Comment>;
}

废话少说,盘它!!!

// standard/es5.ts 实现以上节点方法import Scope from "../scope";
import * as ESTree from "estree";type AstPath<T> = {node: T;scope: Scope;
};const es5 = {// ...// for 循环语句节点ForStatement(astPath: AstPath<ESTree.ForStatement>) {const { node, scope } = astPath;const { init, test, update, body } = node;// 这里需要注意的是需要模拟创建一个块级作用域// 前面Scope类实现,var声明在块作用域中会被提升,const/let不会const forScope = new Scope("block", scope);for (// 初始化值// VariableDeclarationinit ? this.visitNode(init, forScope) : null;// 循环判断条件(BinaryExpression)// 二元运算表达式,之前已实现,这里不再细说test ? this.visitNode(test, forScope) : true;// 变量更新语句(UpdateExpression)update ? this.visitNode(update, forScope) : null) {// BlockStatementthis.visitNode(body, forScope);}},// update 运算表达式节点// update 运算表达式节点,即 ++/--,和一元运算符类似,只是 operator 指向的节点对象类型不同,这里是 update 运算符。UpdateExpression(astPath: AstPath<ESTree.UpdateExpression>) {const { node, scope } = astPath;// update 运算符,值为 ++ 或 --,配合 update 表达式节点的 prefix 属性来表示前后。const { prefix, argument, operator } = node;let updateVar;// 这里需要考虑参数类型还有一种情况是成员表达式节点// 例: for (var query={count:0}; query.count < 8; query.count++)// LHS查找if (argument.type === "Identifier") {// 标识符类型 直接查找const value = scope.search(argument.name);updateVar = value;} else if (argument.type === "MemberExpression") {// 成员表达式的实现在前面实现过,这里不再细说,一样的套路~const { object, property, computed } = argument;const obj = this.visitNode(object, scope);const key = computed? this.visitNode(property, scope): (<ESTree.Identifier>property).name;updateVar = {get value() {return obj[key];},set value(v) {obj[key] = v;},};}return {"++": (v) => {const result = v.value;v.value = result + 1;// preifx? ++i: i++;return prefix ? v.value : result;},"--": (v) => {const result = v.value;v.value = result - 1;// preifx? --i: i--;return prefix ? v.value : result;},}[operator](updateVar);},// 块语句节点// 块语句的实现很简单,模拟创建一个块作用域,然后遍历body属性进行访问即可。BlockStatement(astPath: AstPath<ESTree.BlockStatement>) {const { node, scope } = astPath;const blockScope = new Scope("block", scope);const { body } = node;body.forEach((bodyNode) => {this.visitNode(bodyNode, blockScope);});},
};
export default es5;

上 jest 大法验证一哈~

test("test for loop", () => {expect(run(`var result = 0;for (var i = 0; i < 5; i++) {result += 2;}module.exports = result;`)).toBe(10);
});

e23f53900454b5d30d0892962d53451d.png

你以为这样就结束了吗? 有没有想到还有什么情况没处理? for 循环的中断语句呢?

var result = 0;
for (var i = 0; i < 5; i++) {result += 2;break; // break,continue,return
}
module.exports = result;

感兴趣的小伙伴可以自己动手试试,或者戳源码地址

结语

giao-js目前只实现了几个语法,本文只是提供一个思路。

有兴趣的同学可以查看完整代码。

觉得有帮助到你的话,点个 star 支持下作者 ❤️ ~

参考

bramblex/jsjs

使用 Acorn 来解析 JavaScript

Build a JS Interpreter in JavaScript Using Acorn as a Parser

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/575053.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

地区json文件_【小例子】使用jQuery实现省市区三级联动显示,附源码json文件

开发工具-intellij IDEA需求1.实现对json文件的读取。2.可以在省级选择所有省名和直辖市名3.选择一级省名后自动刷新市名4.选择二级市名后自动刷新区名逻辑分析第一步&#xff1a;读取json文件第二步&#xff1a;遍历出所有一级的省名第三步&#xff1a;把遍历出来的省名动态追…

qt 状态栏

有段时间没有写过博客了。假期去上海旅游&#xff0c;所以一直没有能够上网。现在又来到这里&#xff0c;开始新的篇章吧&#xff01;今天的内容主要还是继续完善前面的那个程序。我们要为我们的程序加上一个状态栏。状态栏位于主窗口的最下方&#xff0c;提供一个显示工具提示…

动态获取textarea后面的p标签_HTML简单标签连起实现的小玩意:

《今天不发知识点&#xff0c;刚被误封了&#xff0c;所有没有太多时间去给整理哈&#xff0c;请谅解》&#xff08;谢谢欣赏&#xff09;前面发了那么多HTML标签&#xff0c;今天来玩个小视频&#xff0c;小白也能几分钟就会的&#xff0c;代码没有写好&#xff0c;时间急了点…

elipse+pydev+python开发arcgis脚本程序

环境配置参考&#xff1a;http://www.cnblogs.com/halfacre/archive/2012/07/22/2603848.html 添加arcpy类库、arctoolbox、arcgis-bin如下所示。 windos——preference——pydev 完成环境配置。 二、获取文件路径Python方法 os.getcwd()返回的是当前文件的目录。假如我的代码…

BZOJ3427 Poi2013 Bytecomputer

可以YY一下嘛 最后一定是-1, -1, ..., -1, 0, 0, ... 0, 1, 1, ..., 1的一个数列 于是f[i][j]表示到了第i个数&#xff0c;新数列的第j项为-1 or 0 or 1的的最小代价 然后就没有然后了 1 /**************************************************************2 Problem: 3427…

mysql timdir_MYSQL学习笔记——数据类型

mysql的数据类型可以分为三大类&#xff0c;分别是数值数据类型、字符串数据类型以及日期时间数据类型。数值数据类型数值类型又可以分为整型、浮点类型、Decimal。整型mysql的整型可以分为TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT&#xff0c;下表给出了每个类型的存储空间…

python编写网页游戏脚本_[大数据]用Python脚本做一些网页游戏中力所能及的自动化任务 - 码姐姐找文...

下面是一段自动登录360传奇霸业游戏的脚本&#xff1a; from pymouse importPyMouseimporttimeimportwebbrowserfrom pykeyboard importPyKeyboard url "http://cqby.wan.360.cn/game_login.php?server_idS577&&srcloginhistory"webbrowser.open_new_tab(ur…

flutter怎么添加ios网络权限_使用Flutter控制蓝牙通讯

背景知识视频教程Dart和Flutter&#xff1a;完整的开发人员指南 - 国外课栈​viadean.comFlutter使用Firestore构建复杂的Android和ios应用 - 国外课栈​viadean.comFlutter的实际项目 - 国外课栈​viadean.com您想使用蓝牙玩物联网设备&#xff0c;但不能在其中放置任何软件吗…

python网页结构分析_Python爬虫基础之网页组成解析

当我们用浏览器访问网站时&#xff0c;每个网页的大不相同&#xff0c;你是否想过它为什么会呈现多种不同的样式呢&#xff1f;就让我们一起了解一下网页的基本组成、结构和节点等内容吧&#xff01;网页的组成 网页可以分为三大部分——HTML、CSS和JavaScript。如果把网页比作…

完全开源im框架_【行业资讯】移动端开源 IM 框架 MobileIMSDK v5.0 发布!

一、更新内容简介本次更新为主要版本更新&#xff0c;强势升级&#xff0c;可同时支持TCP、UDP两种协议&#xff0c;精心封装之下&#xff0c;实现一套API、两种协议同时并存。可能是市面上唯一同时支持UDPTCP两种协议的同类IM框架。二、MobileIMSDK简介MobileIMSDK是一套专为移…

python使用函数目的_Python函数的概念和使用

Python Python开发 Python语言 Python函数的概念和使用函数 为了便于程序的维护和更好的实现模块化&#xff0c;好的程序都会分解为很多函数。 可以这么说&#xff0c;对于任何的编程语言&#xff0c;函数都是一个非常重要的概念。 python 不仅简化了函数的定义过程&#xff0c…

feign直接走熔断_SpringCloud微服务面试必问:Hystrix 服务降级、熔断

本文作者&#xff1a;JLSong 本文链接&#xff1a;https://www.cnblogs.com/songjilong/p/12770999.html1、Hystrix是什么&#xff1f;Hystrix 是一个用于处理分布式系统的延迟和容错的开源库&#xff0c;在分布式系统里&#xff0c;许多依赖不可避免的会调用失败&#xff0c;比…

python里面print是什么意思_python里print是什么意思

python里print是什么意思,多个,是一个,对象,默认值,一个函数 python里print是什么意思 易采站长站&#xff0c;站长之家为您整理了python里print是什么意思的相关内容。 Python是一种解释型、面向对象、动态数据类型的高级程序设计语言。Python由Guido van Rossum于1989年底发明…

mysql 用户管理系统_mysql 用户管理

MySQL账户管理在我们之前登录MySQL的时候我们都是直接使用的root用户&#xff0c;root用户属于数据库系统中的超级管理员&#xff0c;有权限对mysql进行任何想要做的操作。如果在生产环境下操作数据库时也是全部直接使用root账户连接&#xff0c;这就和悬崖边跳舞差不多。所以 …

执行一次怎么会写入两次数据_浅谈 Redis 数据持久化之 AOF 模式

我们知道 Redis 之所以读写快、性能高&#xff0c;得益于它是一种基于内存的数据库&#xff0c;毫无疑问它的操作都几乎都是基于内存。但是内存型数据库也有一个很大的弊端&#xff1a;如果进程崩溃或者服务重启的时候内存数据得不到保存&#xff0c;就会造成数据丢失。为了解决…

数字化转型方法论_双中台:企业数字化转型的核心战略与方法论

当下&#xff0c;绝大部分企业的组织形态、经营模式基本都是垂直闭环型&#xff0c;即围绕一种业务或者管理&#xff0c;其计划、执行、检查、优化的闭环都是独立于企业生态之中。在过去的“局部竞争”、“渠道为王”、“终端为王”的时代&#xff0c;这种组织与经营形式很好地…

java获取cpu使用率_再一次生产 CPU 高负载排查实践

前言前几日早上打开邮箱收到一封监控报警邮件&#xff1a;某某 ip 服务器 CPU 负载较高&#xff0c;请研发尽快排查解决&#xff0c;发送时间正好是凌晨。其实早在去年我也处理过类似的问题&#xff0c;并记录下来&#xff1a;《一次生产 CPU 100% 排查优化实践》不过本次问题产…

《BI那点儿事》三国数据分析系列——蜀汉五虎上将与魏五子良将武力分析,绝对的经典分析...

《BI那点儿事》三国数据分析系列——蜀汉五虎上将与魏五子良将武力分析&#xff0c;绝对的经典分析 原文:《BI那点儿事》三国数据分析系列——蜀汉五虎上将与魏五子良将武力分析&#xff0c;绝对的经典分析献给广大的三国爱好者们&#xff0c;希望喜欢三国的朋友一起讨论&#…

java map取值_Java Set接口 Map 与枚举

Set接口概述一个不包含重复元素的 collection。更确切地讲&#xff0c;set 不包含满足 e1.equals(e2) 的元素对 e1 和 e2&#xff0c;并且最多包含一个 null 元素特点Set接口是无序的 Set 是继承于Collection的接口。它是一个不允许有重复元素的集合。Set可以存储null值,但是nu…

linux调用ocx插件_Wordpress file manager插件任意文件上传

白斩鸡卫兵实验室近日&#xff0c;Wordpress 插件file manager被爆出严重0day漏洞&#xff0c;本着技术研究角度&#xff0c;和大家分享、探讨其漏洞原因。该修复程序已在同一天发布&#xff0c;并且WP File Manager插件6.9版通过删除允许未经授权的文件上传访问的端点来解决当…