首页 文章 精选 留言 我的

精选列表

搜索[基础搭建],共10000篇文章
优秀的个人博客,低调大师

爬虫逆向基础,理解 JavaScript 模块化编程 webpack

关注微信公众号:K哥爬虫,QQ交流群:808574309,持续分享爬虫进阶、JS/安卓逆向等技术干货! 简介 在分析一些站点的 JavaScript 代码时,比较简单的代码,函数通常都是一个一个的,例如: function a() {console.log("a")} function b() {console.log("a")} function c() {console.log("a")} 但是稍微复杂一点的站点,通常会遇到类似如下的代码结构: !function(i) { function n(t) { return i[t].call(a, b, c, d) } }([ function(t, e) {}, function(t, e, n) {}, function(t, e, r) {}, function(t, e, o) {} ]); 这种写法在 JavaScript 中很常见,对于熟悉 JavaScript 的人来说可能非常简单,但是爬虫工程师大多数都是用 Python 或者 Java 来写代码的,看到这种语法就有可能懵了,由于在剥离 JS 加密代码时会经常遇到,所以理解这种语法对于爬虫工程师来说是非常重要的。 这种写法貌似没有官方的名称,相当于进行了模块化编程,因此大多数人称其为 webpack,上面的示例看起来比较费劲,简单优化一下: !function (allModule) { function useModule(whichModule) { allModule[whichModule].call(null, "hello world!"); } useModule(0) }([ function module0(param) {console.log("module0: " + param)}, function module1(param) {console.log("module1: " + param)}, function module2(param) {console.log("module2: " + param)}, ]); 运行以上代码,会输出 module0: hello world!,相信通过浅显易懂的变量名和函数名,应该就可以看懂大致含义了,调用 useModule(0),从所有函数里选择第一个,将 hello world! 传递给 module0 并输出。 仔细观察以上代码,我们会发现主要用到了 !function(){}() 和 function.call() 语法,接下来就一一介绍一下。 函数声明与函数表达式 在 ECMAScript(JavaScript 的一个标准)中,有两个最常用的创建函数对象的方法,即使用函数声明或者函数表达式,ECMAScript 规范明确了一点,即函数声明必须始终带有一个标识符,也就是我们所说的函数名,而函数表达式则可以省略。 函数声明,会给函数指定一个名字,会在代码执行以前被加载到作用域中,所以调用函数在函数声明之前或之后都是可以的: test("Hello World!") function test(arg) { console.log(arg) } 函数表达式,创建一个匿名函数,然后将这个匿名函数赋给一个变量,在代码执行到函数表达式的时候才会有定义,所以调用函数在函数表达式之后才能正确运行,否则是会报错的: var test = function (arg) { console.log(arg) } test("Hello World!") IIFE 立即调用函数表达式 IIFE 全称 Immediately-invoked Function Expressions,译为立即调用函数表达式,也称为自执行函数、立即执行函数、自执行匿名函数等,IIFE 是一种语法,这种模式本质上就是函数表达式(命名的或者匿名的)在创建后立即执行。当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。IIFE 主要用来隔离作用域,避免污染。 IIFE 基本语法 IIFE 的写法非常灵活,主要有以下几种格式: 1、匿名函数前面加上一元操作符,后面加上 (): !function () { console.log("I AM IIFE") }(); -function () { console.log("I AM IIFE") }(); +function () { console.log("I AM IIFE") }(); ~function () { console.log("I AM IIFE") }(); 2、匿名函数后面加上 (),然后再用 () 将整个括起来: (function () { console.log("I AM IIFE") }()); 3、先用 () 将匿名函数括起来,再在后面加上 (): (function () { console.log("I AM IIFE") })(); 4、使用箭头函数表达式,先用 () 将箭头函数表达式括起来,再在后面加上 (): (() => { console.log("I AM IIFE") })() 5、匿名函数前面加上 void 关键字,后面加上 (), void 指定要计算或运行一个表达式,但是不返回值: void function () { console.log("I AM IIFE") }(); 有的时候,我们还有可能见到立即执行函数前面后分号的情况,例如: ;(function () { console.log("I AM IIFE") }()) ;!function () { console.log("I AM IIFE") }() 这是因为立即执行函数通常作为一个单独模块使用一般是没有问题的,但是还是建议在立即执行函数前面或者后面加上分号,这样可以有效地与前面或者后面的代码进行隔离,否则可能出现意想不到的错误。 IIFE 参数传递 将参数放在末尾的 () 里即可实现参数传递: var text = "I AM IIFE"; (function (param) { console.log(param) })(text); // I AM IIFE var dict = {name: "Bob", age: "20"}; (function () { console.log(dict.name); })(dict); // Bob var list = [1, 2, 3, 4, 5]; (function () { var sum = 0; for (var i = 0; i < list.length; i++) { sum += list[i]; } console.log(sum); })(list); // 15 Function.prototype.call() / apply() / bind() Function.prototype.call()、Function.prototype.apply()、Function.prototype.bind() 都是比较常用的方法。它们的作用一模一样,即改变函数中的 this 指向,它们的区别如下: call() 方法会立即执行这个函数,接受一个多个参数,参数之间用逗号隔开; apply() 方法会立即执行这个函数,接受一个包含多个参数的数组; bind() 方法不会立即执行这个函数,返回的是一个修改过后的函数,便于稍后调用,接受的参数和 call() 一样。 call() call() 方法接受多个参数,第一个参数 thisArg 指定了函数体内 this 对象的指向,如果这个函数处于非严格模式下,指定为 null 或 undefined 时会自动替换为指向全局对象(浏览器中就是 window 对象),在严格模式下,函数体内的 this 还是为 null。从第二个参数开始往后,每个参数被依次传入函数,基本语法如下: function.call(thisArg, arg1, arg2, ...) 示例: function test(a, b, c) { console.log(a + b + c) } test.call(null, 1, 2, 3) // 6 function test() { console.log(this.firstName + " " + this.lastName) } var data = {firstName: "John", lastName: "Doe"} test.call(data) // John Doe apply() apply() 方法接受两个参数,第一个参数 thisArg 与 call() 方法一致,第二个参数为一个带下标的集合,从 ECMAScript 第5版开始,这个集合可以为数组,也可以为类数组,apply() 方法把这个集合中的元素作为参数传递给被调用的函数,基本语法如下: function.apply(thisArg, [arg1, arg2, ...]) 示例: function test(a, b, c) { console.log(a + b + c) } test.apply(null, [1, 2, 3]) // 6 function test() { console.log(this.firstName + " " + this.lastName) } var data = {firstName: "John", lastName: "Doe"} test.apply(data) // John Doe bind() bind() 方法和 call() 接受的参数是相同的,只不过 bind() 返回的是一个函数,基本语法如下: function.bind(thisArg, arg1, arg2, ...) 示例: function test(a, b, c) { console.log(a + b + c) } test.bind(null, 1, 2, 3)() // 6 function test() { console.log(this.firstName + " " + this.lastName) } var data = {firstName: "John", lastName: "Doe"} test.bind(data)() // John Doe 理解 webpack 有了以上知识后,我们再来理解一下模块化编程,也就是前面所说的 webpack 写法: !function (allModule) { function useModule(whichModule) { allModule[whichModule].call(null, "hello world!"); } useModule(0) }([ function module0(param) {console.log("module0: " + param)}, function module1(param) {console.log("module1: " + param)}, function module2(param) {console.log("module2: " + param)}, ]); 首先,这整个代码是一个 IIFE 立即调用函数表达式,传递的参数是一个数组,里面包含三个方法,分别是 module0、module1 和 module2,可以将其视为三个模块,那么 IIFE 接受的参数 allModule 就包含这三个模块,IIFE 里面还包含一个函数 useModule(),可以将其视为模块加载器,即要使用哪个模块,示例中 useModule(0) 即表示调用第一个模块,函数里面使用 call() 方法改变函数中的 this 指向并传递参数,调用相应的模块进行输出。 改写 webpack 对于我们爬虫逆向当中经常遇到的 webpack 模块化的写法,可以很容易对其进行改写,以下以一段加密代码为例: CryptoJS = require("crypto-js") !function (func) { function acvs() { var kk = func[1].call(null, 1e3); var data = { r: "I LOVE PYTHON", e: kk, i: "62bs819idl00oac2", k: "0123456789abcdef" } return func[0].call(data); } console.log("加密文本:" + acvs()) function odsc(account) { var cr = false; var regExp = /(^\d{7,8}$)|(^0\d{10,12}$)/; if (regExp.test(account)) { cr = true; } return cr; } function mkle(account) { var cr = false; var regExp = /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/; if (regExp.test(account)) { cr = true; } return cr; } }([ function () { for (var n = "", t = 0; t < this.r.length; t++) { var o = this.e ^ this.r.charCodeAt(t); n += String.fromCharCode(o) } return encodeURIComponent(n) }, function (x) { return Math.ceil(x * Math.random()) }, function (e) { var a = CryptoJS.MD5(this.k); var c = CryptoJS.enc.Utf8.parse(a); var d = CryptoJS.AES.encrypt(e, c, { iv: this.i }); return d + "" }, function (e) { var b = CryptoJS.MD5(this.k); var d = CryptoJS.enc.Utf8.parse(b); var a = CryptoJS.AES.decrypt(e, d, { iv: this.i }).toString(CryptoJS.enc.Utf8); return a } ]); 可以看到关键的加密入口函数是 acvs(),acvs() 里面又调用了 IIFE 参数列表里面的第一个和第二个函数,剩下的其他函数都是干扰项,而第一个函数中用到了 r 和 e 参数,将其直接传入即可,最终改写如下: function a(r, e) { for (var n = "", t = 0; t < r.length; t++) { var o = e ^ r.charCodeAt(t); n += String.fromCharCode(o) } return encodeURIComponent(n) } function b(x) { return Math.ceil(x * Math.random()) } function acvs() { var kk = b(1e3); var r = "I LOVE PYTHON"; return a(r, kk); } console.log("加密文本:" + acvs()) 总结 看完本文后,你可能会觉得 webpack 也不过如此,看起来确实比较简单,但实际上我们在分析具体站点时往往不会像上述例子这么简单,本文旨在让大家简单理解一下模块化编程 webpack 的原理,后续 K 哥将会带领大家实战分析比较复杂的 webpack!敬请关注!

优秀的个人博客,低调大师

数据结构与算法必知基础知识

原创公众号:bigsai 文章已收录在 全网都在关注的数据结构与算法学习仓库 欢迎star 前言 数据结构与算法是程序员内功体现的重要标准之一,且数据结构也应用在各个方面,业界更有程序=数据结构+算法这个等式存在。各个中间件开发者,架构师他们都在努力的优化中间件、项目结构以及算法提高运行效率和降低内存占用,在这里数据结构起到相当重要的作用。此外数据结构也蕴含一些面向对象的思想,故学好掌握数据结构对逻辑思维处理抽象能力有很大提升。 为什么学习数据结构与算法?如果你还是学生,那么这门课程是必修的,考研基本也是必考科目。工作在内卷严重的大厂中找工作数据结构与算法也是面试、笔试必备的非常重要的考察点。如果工作了数据结构和算法也是内功提升一个非常重要的体现,对于程序员来说,想要得到满意的结果,数据结构与算法是必备功力! 数据结构 概念 数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。 简言之,数据结构是一系列的存储结构按照一定执行规则、配合一定执行算法所形成的高效的存储结构。在我们所熟知的关系数据库、非关系数据库、搜索引擎存储、消息队列等都是比较牛的大型数据结构良好的运用。当然这些应用中间件不单单要考虑单纯的结构问题。还考虑实际os、网络等其他因素。 而对于数据结构与算法这个专栏。我们程序员更改掌握的首先是在内存中运行的抽象的数据结构。是一个相对比较单一的数据结构类型,比如线性结构、树、图等等. 相关术语 在数据结构与算法中,数据、数据对象、数据元素、数据项很多人搞不清其中的关系。通过画一张图来捋一捋,然后下面举个例子给大家分享一下。 用户信息表users id name sex 001 bigsai man 002 smallsai man 003 菜虚鲲 woman users的pojo对象 class users { //略 int id; String name; String sex; } //list和woman是数据 List<users>list;//数据对象list List<users>woman;//数据对象woman list.add(new users(001,"bigsai","man"));//添加数据元素 一个users由(001,bigsai,man)三个数据项组成 list.add(new users(002,"smallsai","man"));//数据元素 list.add(new users(003,"菜虚鲲","woman"));//数据元素 woman.add(list.get(2));//003,"菜虚鲲","woman"三个数据项构成的一个数据元素 数据:对客观事物的符号表示,指所有能输入到计算机中并被计算机程序处理的符号的集合总称。上述表中的三条用户信息的记录就是数据(也可能多表多集合这里只有一个)。这些数据一般都是用户输入或者是自定义构造完成。当然,还有一些图像、声音也是数据。 数据元素:数据元素是数据的基本单位。一个数据元素由若干数据项构成!可认为是一个pojo对象、或者是数据库的一条记录。比如菜虚鲲那条记录就是一个数据元素。 数据项: 而构成用户字段/属性的有id、name、sex等,这些就是数据项.数据项是构成数据元素的最小不可分割字段。可以看作一个pojo对象或者一张表(people)的一个属性/字段的值。 数据对象:是相同性质数据元素的集合。是数据的一个子集。比如上面的users表、list集合、woman集合都是数据对象。单独一张表,一个集合都可以是一个数据对象。 总的捋一捋,数据范围最广,所有数据即数据,而数据对象仅仅是有相同性质的一个集合,这个集合是数据的子集,但并不是数据的基本单位,而数据元素才是数据的基本单位。举个例子表cat和表dog都是数据,然后表cat是个数据对象(因为都描述cat这种对象),但是数据的基本单位并不是猫和狗,而是他们的具体的每一条,比如小猫咪1号,大猫咪二号,哈士奇1号,藏獒2号这些每一条才是数据的基本单位。 还有数据类型,抽象数据类型也在下面讲一讲。 数据类型 原子类型:其值不可再分的类型。比如int,char,double,float等。 结构类型:其值可以再分为若干成分的数据类型。比如结构体构造的各种结构等。 抽象数据类型(ADT):抽象数据类型(ADT)是一个实现包括储存数据元素的存储结构以及实现基本操作的算法。使得只研究和使用它的结构而不用考虑它的实现细节成为可能。比如我们使用List、Map、Set等等只需要了解它的api和性质功能即可。而具体的实现可能是不同的方案,比如List的实现有数组和链表不同选择。 三要素 逻辑结构:数据元素之间的逻辑关系。逻辑结构分为线性结构和非线性结构。线性结构就是顺序表、链表之类。而非线性就是集合、树、图这些结构。 存储结构:数据结构在计算机中的表示(又称映像,也称物理结构),存储结构主要分为顺序存储、链式存储、索引存储和散列(哈希)存储,这几种存储通过下面这张图简单了解一下(仅仅为理解不考虑更多): 数据的运算:施加在数据上的运算包括运算的定义和实现,运算的定义基于逻辑结构,运算的实现基于存储结构。 在这里容易混淆的是逻辑结构与存储结构的概念。对于逻辑结构,不难看得出逻辑二字,逻辑关系也就是两者存在数据上的关系而不考虑物理地址的关系,比如线性结构和非线性结构,它描述的是一组数据中联系的方式和形式,他针对的是数据。看中的是数据结构的功能,比如线性表就是前后有序的,我需要一个有序的集合就可以使用线性表。 而存储结构就是跟物理地址挂钩的。因为同样逻辑结构采用不同存储结构实现适用场景和性能可能不同。比如同样是线性表,可能有多种存储结构的实现方式。比如顺序表和链表(Arraylist,Linkedlist)它们的存储结构就不同,一个是顺序存储(数组)实现,一个是链式存储(链表)实现。它关注的是计算机运行物理地址的关系。但通常同一类存储结构实现的一些数据结构有一些类似的共同点和缺点(线性易查难插、链式易插难查等等)。 算法分析 上面讲了数据结构相关概念,下面对算法分析的一些概念进行描述。 算法的五个重要特征:有穷性、确定性、可行性、输入、输出。这些从字面意思即可理解,其中有穷性强调算法要有结束的时候不能无限循环;而确定性是每条指令有它意义,相同的输入得到相同的输出;可行性是指算法每个步骤经过若干次执行可以实现;输入是0个或多个输入(可0);输出是1个或多个输出(一定要有输出)。 而一个好的算法,通常更要着重考虑的是效率和空间资源占用(时间复杂度和空间复杂度),通常复杂度更多描述的是一个量级程度而很少用具体数字描述。 空间复杂度 概念:是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n)) 空间复杂度其实在算法的衡量占比是比较低的(我们经常使用牺牲空间换时间的数据结构和算法),但是不能忽视空间复杂度中重要性。无论在刷题还是实际项目生产内存都是一个极大额指标。对于Java而言更是如此。本身内存就大,如果采用的存储逻辑不太好会占用更多的系统资源,对服务造成压力。 而算法很多情况都是牺牲空间换取时间(效率)。就比如我们熟知的字符串匹配String.contains()方法,我们都知道他是暴力破解,时间复杂度为O(n^2),不需要借助额外内存。而KMP算法在效率和速度上都原生暴力方法,但是KMP要借助其他数组(next[])进行标记储存运算。就用到了空间开销。再比如归并排序也会借助新数组在递归分冶的适合进行逐级计算,提高效率,但增加点影响不大的内存开销。 当然,算法的空间花销最大不能超过jvm设置的最大值,一般为2G.(2147483645)如果开二维数组多种多维数据不要开的太大,可能会导致heap OutOfMemoryError。 时间复杂度 概念:计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。 时间复杂度的排序:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) <O(n!) < O(n^n) 常见时间复杂度:对于时间复杂度,很多人的概念是比较模糊的。下面举例子说明一些时间复杂度。 O(1): 常数函数 a=15 O(logn): 对数函数 for(int i=1;i&lt;n;i*=2)分析:假设执行t次使得i=n;有2^t=n; t=log2~n,为log级别时间复杂度为O(logn)。 还有典型的二分查找,拓展欧几里得,快速幂等算法均为O(logn)。属于高效率算法。 O(n): 线性函数 for (int i=0;i&lt;n;i++) 比较常见,能够良好解决大部分问题。 O(nlogn): for (int i=1;i&lt;n;i++)for (int j=1;j&lt;i;j*=2) 常见的排序算法很多正常情况都是nlogn,比如快排、归并排序。这种算法效率大部分也还不错。 O(n^2) for(int i=0;i&lt;n;i++)for(int j=0;j&lt;i;j++) 其实O(n^2)的效率就不敢恭维了。对于大的数据O(n^2)甚至更高次方的执行效果会很差。 当然如果同样是n=10000.那么不同时间复杂度额算法执行次数、时间也不同。 具体 n 执行次数 O(1) 10000 1 O(log2n) 10000 14 O( n^1/2) 10000 100 O(n) 10000 10000 O(nlog2 n) 10000 140000 O(n^2) 10000 100000000 O(n^3) 10000 1000000000000 降低算法复杂度有些会靠数据结构的特性和优势,比如二叉排序树的查找,线段树的动态排序等等,这些数据结构解决某些问题有些非常良好的性能。还有的是靠算法策略解决,比如同样是排序,冒泡排序这种笨而简单的方法就是O(n2),但快排、归并等聪明方法就能O(nlogn)。要想变得更快,那就得掌握更高级的数据结构和更精巧的算法。 时间复杂度计算时间复杂度计算一般步骤:1、找到执行次数最多的语句; 2、计算语句执行的数量级 ; 3、用O表示结果。并且有两个规则: 加法规则: 同一程序下如果多个并列关系的执行语句那么取最大的那个,eg: T(n)=O(m)+O(n)=max(O(m),O(n)); T(n)=O(n)+O(nlogn)=max(O(n),O(nlogn))=O(nlogn); 乘法规则:循环结构,时间复杂度按乘法进行计算,eg: T(n)=O(m)*O(n)=O(mn) T(n)=O(m)*O(m)=O(m^2)(两层for循环) 当然很多算法的时间复杂度还跟输入的数据有关,分为还会有最优时间复杂度(可能执行次数最少时),最坏时间复杂度(执行次数最少时),平均时间复杂度,这在排序算法中已经具体分析,但我们通常使用平均时间复杂度来衡量一个算法的好坏。 数据结构与算法学习 捋过数据结构与算法基本概念的介绍,在学习数据结构与算法方面,个人把经典的数据结构与算法学习过程步骤写在下面,希望能给大家一个参考: 数据结构 单链表(带头结点、不带头结点)设计与实现(增删改查),双链表设计与实现 栈设计与实现(数组和链表),队列设计与实现(数组和链表) 二叉树概念学习,二叉树前序、中序、后序遍历递归、非递归实现 ,层序遍历 二叉排序树设计与实现(插入删除) 堆(优先队列、堆排序) AVL(平衡)树设计与实现(四种自旋方式理解实现) 伸展树、红黑树原理概念理解 B、B+原理概念理解 哈夫曼树原理概念理解(贪心策略) 哈希(散列表)原理概念理解(几种解决哈希冲突方式) 并查集/不相交集合(优化和路径压缩) 图论拓扑排序 图论dfs深度优先遍历、bfs广度优先遍历 最短路径Dijkstra算法、Floyd算法、spfa算法 最小生成树prim算法、kruskal算法 其他数据结构线段树、后缀数组等等 经典算法 递归算法(求阶乘、斐波那契、汉诺塔问题) 二分查找 分治算法(快排、归并排序、求最近点对等问题) 贪心算法(使用较多,区间选点问题,区间覆盖问题) 常见动态规划(LCS(最长公共子序列) LIS(最长上升子序列)背包问题等等) 回溯算法(经典八皇后问题、全排列问题) 位运算常见问题(参考剑指offer和LeetCode问题) 快速幂算法(快速求幂乘、矩阵快速幂) kmp等字符串匹配算法 一切其他数论算法(欧几里得、拓展欧几里得、中国剩余定理等等) 相信看完这篇文章,你应该对数据结构与算法有个不错的认知。数据结构与算法有着非常密切的关联,数据结构是为了实现某种算法,算法是核心目的。学习数据结构与算法之前,可以先参考书本或者博客先了解其功能,再研究其运行原理,再动手实战(编写数据结构或者相关题目)这样层次渐进,想要深入的学习数据结构与算法光理解是不行的,需要有大量代码实战才可。并且这条路是没有止境的,活到老,学到老,刷到老。 原创不易,bigsai请51CTO的朋友们帮两件事帮忙一下: 点赞、在看、分享支持一下, 您的肯定是我创作的源源动力。 微信搜索「bigsai」,2021我需要你的支持! 咱们下次再见!

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册