环境要求mac OS/linux（其他操作系统类似）酷Qdocker+CoolQ HTTP API 插件NoneBotdocker如果是windows服务器或者电脑也可以安装酷Q，mac linux/linux服务器只能装酷Qdocker（推荐docker安装）!> 也可以直接使用「CoolQ HTTP API 插件」作者集成的docker镜像包，这样就无需配置「CoolQ HTTP API 插件」了。简单介绍简单介绍一下原理是什么。在酷Q中登录你的QQ账号，「酷Q」接管你的消息，并将QQ的所有消息请求和发送进行封装成一些接口，供“插件”进行进一步接管和调用。酷Q的插件后缀都是.cpk，需要安装在酷Q的内部。「CoolQ HTTP API 插件」作用就是专门针对“http请求以及信息处理”方面的插件，使用的语言当然是与酷Q一致的（易语言好像是）会易语言的开发者还是蛮少的吧...emmm「NoneBot」就像一个翻译官，将「CoolQ HTTP API 插件」的插件全部转变成“Python的方法”。「CoolQ HTTP API 插件」有很多个不同语言的翻译官，方便使用不同语言的开发者直接处理酷Q的信息。这样，我们直接写Python文件，就能直接接收或者发送QQ消息啦！开始开始安装！#1. 安装docker brew install docker #2. 安装酷Q+http插件集成镜像 docker pull richardchien/cqhttp:latest #3. 安装nonebot pip3 install nonebot 我们在docker中先启用酷Qdocker run -ti --rm --name cqhttp-test -v $(pwd)/coolq:/home/user/coolq -p 9001:9000 -p 5701:5700 -e COOLQ_ACCOUNT=123456 -e CQHTTP_POST_URL=http://example.com:8080 -e CQHTTP_SERVE_DATA_FILES=yes richardchien/cqhttp:latest 上面的命令如果需要改，就改9001和5701就可以了，其它的不用修改。其中：9001端口号就是登录酷Q的端口号5001端口号是HTTP API 插件开放的端口，这个不用管。如果需要启用多个酷Q，保证这个端口号和容器名称cqhttp-test不重复即可启动之后，打开127.0.0.1:9001，如果部署到Linux服务器上面就是ip地址:9001打开页面之后，会要求输入密码，密码默认是MAX8char。进入之后就是一个类似QQ登录界面，输入账号密码即可。（第一次登录可能要输入验证码）此时，酷Q其实就是以一个安卓手机的身份登录了你的QQ，并接管了你的所有消息处理。「CoolQ HTTP API 插件」同时也在开放接口供其他插件进行调用。接下来，我们需要使用nonebot 模块，使用Python代码接管QQ消息的接收和发送。在coolq/data/app/io.github.richardchien.coolqhttpapi/config/里面有<你的登录的QQ号>.json 文件，打开，并修改（如果没有则增加）三个变量为对应的值{ "ws_reverse_api_url": "ws://127.0.0.1:9999/ws/api/", "ws_reverse_event_url": "ws://127.0.0.1:9999/ws/event/", "use_ws_reverse": true }!> 如果docker部署电脑上的，把 127.0.0.1 改成host.docker.internal!> 如果docker是部署在Linux服务器上的，把127.0.0.1改成172.17.0.1这个操作就是让「CoolQ HTTP API 插件」把接收信息和发送的信息借给noneboot 模块来接管。所以，开始通过noneboot模块，使用Python开发吧。可以用pycharm ide，当然也可以直接新建一个文件夹，使用命令行启用。我们直接用命令行来启动。cd ~/Document mkdir qqboot_test touch boot.py cd qqboot_test & mkdir plugin 第一条指令cd切换到指定目录，后面的指令，你可以完全一样。打开boot.py，我们复制粘贴下面的代码：import nonebot from os import path if __name__ == '__main__': nonebot.init() nonebot.load_plugins( path.join(path.dirname(__file__),'plugins'), 'plugins' ) nonebot.run(host='127.0.0.1', port=9999)我们之前用ws://127.0.0.1:9999/ws/api/接管「CoolQ HTTP API 插件」插件的消息处理。上面的代码的作用启动noneboot模块，并且绑定到host的指定端口处。!> 如果是本机电脑运行的boot.py 程序，host填写127.0.0.1即可!> 如果是Linux服务器部署的boot.py 程序，host 改成0.0.0.0上面我们在qqboot_test项目里面新建了一个plugins文件夹，并且在boot.py指明插件文件夹的位置了。现在在这个插件文件夹创建一个文件吧。touch user.py在具体开发之前简单介绍一下noneboot的处理逻辑。有两种方式：noneboot 接收到用户发来的消息（自然语言），通过匹配消息的关键字，执行指定的命令函数。noneboot 直接接收命令，然后直接指定命令函数代码用户发送/文字的格式表示发送文字为名称的命令我们通过第一种方式处理用户自然语言的信息。比如，当用户发下面的句子今天天气怎么样？ 天气 天气好吗 北京天气在user.py文件里面编辑：@on_natural_language(keywords={'天气'}) async def _(session: NLPSession): # 返回意图命令，前两个参数必填，分别表示置信度和意图命令名 return IntentCommand(60.0, 'weather') @on_command('weather', aliases=('天气')) async def weather(session: CommandSession): # 从会话状态（session.state）中获取城市名称（city），如果当前不存在，则询问用户 city = session.get('city', prompt='你的城市是') weather_report = await get_weather(city) # 向用户发送天气预报 await session.send(weather_report) async def get_weather(city: str) -> str: # 这里简单返回一个字符串 return city+"的天气不错"代码分为三个部分：第一部分是解析自然语言，只要包含keywords里面的关键字就会进行对应的命令处理weather第二部分是命令处理函数，aliases是别名，意思是通过/[aliases]这种方式就可以直接执行命令函数第三部分是异步请求返回天气预报，这个正是noneBoot的一个特色。最后就是启动这个项目即可：python3 boot.py这个命令启动之后，你会看到console输出中有酷Q接管的所有信息输出。以上就是一个简单的QQ聊天机器人的开发以及部署。把机器人部署到服务器的docker上面，如果愿意可以有很多的创意，比如夜晚时间接收到任何信息都会自动回复“夜晚休息，上线后处理”但唯一可能的问题是docker中启动酷Q后，手机上不能再次登录相同的QQ号了。祝使用愉快~
系列笔记[基本数据结构]()[类的使用]()[类的继承和派生]()[面向对象特征之一——多态]()[操作符重载]()[const关键字的使用]()1. 类的定义与使用1.1 类的定义最简单的类的申明：class Student{ };在类的定义最后需要加上分号，这是和java不同的地方，否则会有编译错误。1.2 类的使用即创建类的实例——对象。在java中你会使用到new关键字，而在c++中，创建对象与使用普通的数据类型一样简单：Student studentObject;2. 类的成员变量和成员函数2.1 成员变量class Student { char name[20]; //姓名 int id_num; //学号 int age; //年龄 char sex; //性别和java不同，类的成员变量不能在创建的时候初始化值。2.2 成员函数和java不同,c++的成员函数有两种定义方式:1.类内部申明，类外定义（实现）：class student { void set_age(int a); int get_age(); }; //在类外部定义set_age函数 void student::set_age(int a) { age = a; } //在类外部定义get_age函数 int student::get_age() { return age; }在类内部申明函数，在类的外部定义的时候通过作用域操作符 :: 进行实现。申明的时候，只需写出返回类型、函数名称、参数列表、末尾分号即可，不能加上空的大括号。 返回类型 类名::成员函数名(参数列表) { //函数体 }你也可以在类的成员函数申明的前面加上inline关键字使其变成内联函数。内联函数可以优化程序的执行效率。class student { inline void set_age(int a); inline int get_age(); }; //在类外部定义set_age函数 void student::set_age(int a) { age = a; } //在类外部定义get_age函数 int student::get_age() { return age; }2. 类内部边申明边定义（实现）class Student{ void set_age(int a){age = a;} int get_age(){return age;} }这种方式其实和java的成员函数调用方式是相同的。2.3 访问限制和java的语法类似，c++也提供了三种关键字：public：本类和其他类都可以访问到private： 只有本类可以访问到protected：只有本类和派生的类可以访问。但是使用的语法格式有点不同。class book { public: void setprice(double a); double getprice(); private: double price; };通过这种格式，可以将属性为public的变量或函数申明在一起。避免了重复的写权限限制符号。当然，权限相同的成员也可以分开声明，申明的顺序也是无所谓的。2.4 访问成员变量/函数创建的对象就是类的实例，就直接使用.点选择符，这里是和java一样的。创建的如果是对象的指针，需要使用->箭头选择符访问成员。java中没有指针就没有这条规则。#include <iostream> using namespace std; class book { public: void setprice(double a); double getprice(); private: double price; }; void book::setprice(double a) { price = a; } double book::getprice() { return price; } int main() { book Alice; Alice.setprice(29.9); cout<<"The price of Alice is $"<<Alice.getprice()<<endl; book *Harry = new book; Harry->setprice(49.9); cout<<"The price of Harry is $"<<Harry->getprice()<<endl; return 0; }注意：C++类class和结构体struct区别class和struct都可以定义一个类的结构。采用struct关键字，结构体中定义的成员变量或成员函数默认都是public属性的。使用class关键字，类中定义的成员变量或成员函数默认都是private属性的，除此之外，没有其他任何区别。3. 构造函数3.1 构造函数的定义构造函数的声明与定义的方式与普通函数是相同的，也是有两种方式，但是有以下几点特殊地方：构造函数的函数名必须与类名相同；构造函数无返回值；当我们创建类对象的时候构造函数会被自动调用，而无需我们主动调用。这三点和java中语法也是一样的。通常如果在定义类的时候，没有定义任何一个构造函数的时候，系统会自动生成一个默认构造函数。默认构造函数就是不带任何参数的构造函数。其它带参数的构造函数统称为带参构造函数。如果在类中声明了任何一个构造函数，则系统不会自动生成默认构造函数。如果在创建对象时候表示使用哪个构造函数呢？只要在对象名称后加一对小括号，小括号类参数与对应的构造函数参数对应即可： class book { public: book(){} book(char* a, double p); private: double price; char[] title; }; book::book(char[] a, double p) { title = a; price = p; } int main() { book Harry("Harry Potter", 49.9); return 0; } 3.2 初始化参数表除了通过在构造函数的函数体内进行成员变量的初始化，还可以通过在构造函数的后面以下面的格式初始化参数：构造函数(参数列表):成员变量1(初始值),成员变量2(初始值)……{}举个例子：class book { public: book(){} book(char *a, double p):title(a),price(p){} };对，你没有看错，还有这种类似函数的方式给成员变量赋值。这种写法本质就是为了简写成员变量的赋值操作，你同样可以用普通的方式定义构造函数： book(char *a, double p){ title = a; price = p; }在java语法中，你可以在初始化块中给成员变量中赋值，很容易理解。但是在c++中提供这样一种以成员变量名作为函数名，参数列表是变量的值的形式初始化成员变量。值得注意的是：参数初始化顺序与初始化表列出表量的顺序无关，参数初始化顺序只与成员变量在类中声明的顺序有关。3.3 构造函数的参数默认值PHP中的函数参数可以在调用之前就可以赋值一个默认值，因为PHP不支持函数的重载。通过这种方式，当你调用的函数时候没有给其他参数赋值，其他函数也会有一个默认值:function exampleFunction($parm1,$parm2 = "world"){ echo $parm1 ." ". $parm2; } exampleFunction("hello") //输出： hello world;上面的代码可以很清楚的看到，第二个参数已经被赋值了默认值world，当你没有给定第二个参数的时候，第二个参数仍然是有值的。c++的构造函数也支持参数默认赋值，比如：class book { public: book(){} book(char* a, double p = 5.0); private: double price; char * title; }; book::book(char* a, double p) //在定义函数的时候可以不指定默认参数 { title = a; price = p; }此时，当我们这样创建对象的时候：Book Time("Time book")调用的就是第二个构造函数，并且price自动赋值为默认值5.0但默认参数的构造函数会与某些构造函数重载冲突：book(char* a);如果我们再申明这样一个构造函数，之前创建对象就会发生编译错误，因为编译器不知道调用哪一个构造函数了。换句话说，C++在支持构造函数的重载的条件下，基本上没必要使用带默认参数的构造函数。* 3.4 转型、拷贝构造函数3.4.1 转型构造函数构造函数关于是否有参数可以分为：不带参数构造函数带参数构造函数（含带默认参数的构造函数）其中带参数构造函数中有两种特殊的构造函数：转型构造函数拷贝构造函数转型构造函数和普通的构造函数没啥区别，就是参数列表只有一个参数，创建对象的时候如果调用该构造函数，就是意味着将参数转换为了一个类的对象，这没什么特别的。特别之处在于这里有一个隐式的类型转换：比如你创建一个字符串变量，但是你将该变量传入了一个参数为类对象的函数，这个时候就会自动的发生字符串到类对象的隐式对象类型转换：class student { public: student(char * n){name = n;}//转型构造函数 private : char * name; } void fun(Student studentObj){}; char * name = “Harry Potter”; fun(name); 这个时候字符串name自动转换为student类的对象，转换的过程中自动调用转型构造函数。但隐式转换会带来调试的困难，可以在构造函数前加上explict关键字，避免这种自动转换。换句话说，转型构造函数就是参数只有一个的一类构造函数，能实现将参数转换为类对象的功能。3.4.3 拷贝构造函数4. 析构函数在Java中无需自行设计析构函数，java有一套系统级的垃圾回收管理程序。在创建对象的时候系统会自动调用构造函数，在对象需要被销毁的时候，系统也会自动调用析构函数。与构造函数类似，析构函数也是一个成员函数，但是有如下特殊之处：无返回值没有参数，不能被重载，因此一个类也只能含有一个析构函数函数名必须为“~类名”的形式，符号“~”与类名之间可以有空格5. 类的高级用法5.1 常量指针this与java类似，在类的每个成员函数中包含一个常量指针，该指针指向调用该函数的对象。与java类似，该指针只要是用来区分同名的成员变量与参数名称的：void setprice(double price) { price = price; }5.2 new和delete操作符c++创建对象有两种方式，一种就是普通的直接通过类名 对象名(构造函数参数列表) 创建，另一种就是通过new关键字创建。两种方式创建对象的空间分配方式有分别。第一种空间分配在栈中，由系统可以直接管理，比如在代码块外，块内的对象就会被清理。第二种的空间分配在堆上，大小可以很大， 但是销毁得由程序本身处理。使用 new 关键字会自动调用构造函数使用 delete 关键字会自动调用析构函数5.3 const关键字const表示一些不希望被人们修改的数据，与java语法中的final static 有类似之处，但是也有很多区别。const关键字可以定义在成员变量、成员函、对象、对象的常引用上。5.3.1 修饰（成员）变量这个很好理解，在变量最前面加上关键字表示初始化之后禁止二次修改。const成员变量的初始化只能通过参数初始化表进行初始化 const int a=5;5.3.2 修饰成员函数const成员函数可以使用类中的所有成员变量，但是不能修改任何成员变量，也不能调用类中任何非const成员函数。fun(形参) const{}5.3.3 修饰类对象用const修饰的类对象，该对象内的任何成员变量都不能被修改。因此不能调用该对象的任何非const成员函数，因为对非const成员函数的调用会有修改成员变量的企图。class A { public: void funcA() {} void funcB() const {} }; int main { const A a; a.funcB(); // 可以 a.funcA(); // 错误 const A* b = new A(); b->funcB(); // 可以 b->funcA(); // 错误 }5.3.4 修饰类对象的引用我们在有些函数参数中会使用到对象，这个时候我们不是直接将对象做为参数，而是使用对象的引用。此时我们在对象的引用前加上关键字，避免函数对对象本身做出任何修改：void display(const book &b) { b.setprice(59.9); //compile error cout<<"The price of "<<b.gettitle()<<" is $"<<b.getprice()<<endl; //ok }5.3.5 重载成员函数参考文章： C++ const关键字总结5.4 static关键字好了，这个关键字可以说是和java一致的了。5.4.1 静态成员变量该变量是同步于类的所有对象，也就是类的变量5.4.2 静态成员函数5.5 友元函数和友元类5.5.1 友元函数我们知道对象是无法直接调用私有变量或者私有函数的，只有在类中的函数才可以使用类的私有变量或者私有函数。友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数，它不属于任何类，但需要在类的定义中加以声明，声明时只需在友元的名称前加上关键字friend，其格式如下：friend 返回类型 函数名(形式参数);友元函数的声明可以放在类的私有部分，也可以放在公有部分，它们是没有区别的，都说明是该类的一个友元函数。一个函数可以是多个类的友元函数，只需要在各个类中分别声明。友元函数的调用与一般函数的调用方式和原理一致。5.5.2 友元类友元类的所有成员函数都是另一个类的友元函数，都可以访问另一个类中的隐藏信息（包括私有成员和保护成员）。 当希望一个类可以存取另一个类的私有成员时，可以将该类声明为另一类的友元类。定义友元类的语句格式如下：friend class 类名;当你在一个类中申明另一个类为该类的友元的时候，表明该友元类的所有成员函数都可以使用该类的所有信息。这种顺序是单向的。其中：friend和class是关键字，类名必须是程序中的一个已定义过的类。例如，以下语句说明类B是类A的友元类： class A { … public: friend class B; … };经过以上说明后，类B的所有成员函数都是类A的友元函数，能存取类A的私有成员和保护成员。使用友元类时注意：友元关系不能被继承。友元关系是单向的，不具有交换性。若类B是类A的友元，类A不一定是类B的友元，要看在类中是否有相应的声明。友元关系不具有传递性。若类B是类A的友元，类C是B的友元，类C不一定是类A的友元，同样要看类中是否有相应的申明参考文章： C++友元函数和友元类
前言之前学校开的选修课《数据挖掘》，布置的两道算法题，时间有限完成其中一道：用Apriori算法求特定支持度的频繁项集。算法本身不难，java萌新我却花费了一天的时间，特此记录。算法描述我们目的是求出项数为K的频繁项集即L(K)。Apriori算法的核心步骤是：L(K-1)通过自连接求出项数为K的候选项集合C(K)通过对C(K)进行一系列处理（剪枝 + 支持度判断） 得到L(K)集合在说明下面更多内容之前，先对一部分概念进行说明：事务：如第一张图中，每一行就是一个事务，如 T100 I1,I2,I5。其中T100是事务编号，这个随便命名，只要保证唯一即可。后面的I1 I2 I5是该事务中的项数据集：如图一，整个表就是数据集，就是所有的事务的集合。即D = {T100,T200…}项集：包含若干个项（如I1 、I2这样的项）的集合。可以看出项集和事务似乎有点项。但是项集更随意一点，任意几个项都能组成一个项集。但是事务是人为规定好的。可信度：项集在数据集中出现的重复次数。项集不一定需要在事务中连续出现。如{I1,I2,I5}项集在数据集中出现的次数为2次，分别是第一个和倒数第二个事务。接下来，详细分析算法中的两个步骤：1. 自连接：怎么个连接法呢？以图二中的L2频繁集的集合为例：项集支持度{I1,I2}4{I1,I3}4{I1,I5}2{I2,I3}4{I2,I4}2{I2,I5}2判断两个项集是否可以自连接要看两个项集的K-1项是否完全相同。如果满足条件，连接后的项集 = 第一个项集 + 第二个项集的最后一个元素。比如{I1 I2}与{I1 I3}满足自连接条件，连接后的项集为{I1 I2 I3}。2. 剪枝：剪枝是这个算法的核心，如果不进行这个步骤的话，也能得出正确结果，但是时间就会大大增加了。剪枝的核心是若某个集合存在一个非空子集不是频繁项集，则该集合不是频繁项集。我们通过自连接组成新的K项的候选项集后，需要通过剪枝判断是否满足条件。即找出该候选项集的含有(K-1)项的子集，并分别判断每个子集是否存在于K-1项频繁项集里。只要有一个不存在，那么该K项候选项集也不可能是频繁项集。这里我们只需要找含有K-1项的子集，而不用找所有的子集，因为这个算法是从下不断递归上来的，含有更少项的子集肯定是在频繁子集里的。问题关键是怎么寻找某个K项集的K-1项子集呢？很简单，依次把K项集中的每个元素去掉一个就可以了。完成了这两步骤，就完成了这个算法了核心步骤。3. 对精简后候选集统计每一个项集的支持度4. 根据最小支持度将候选项集转换为K项频繁项集合可以看出，如果不剪枝的话，第三步的工作量是非常大的。剪枝过后，候选集的体积大大减小了。但是获取含有一项的频繁项集因为没有候选项集，就需要特殊处理。这个很简单，就是统计数据集中的每一项的支持度，并和最小支持度进行比较，得到含有一项的频繁项集。代码分析github项目地址：https://github.com/ihewro/Aprioriimport java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.util.*; /** * @author hw * 算法名称：数据挖掘：Apriori算法求特性支持度下的所有的频繁集 * 算法原理： * * 0. 获取C1的时候，因为没有L(0)频繁集，所以单独处理一个函数 * 1. 已经频繁集L(k-1)，{}自连接+剪枝}求出精简后的候选集C(k) * 2. 根据精简后的候选集C(k)根据求出频繁集L(k) * * 数据结构： * 1. 使用List<String> 存储整个数据库的数据，string代表每一行的数据 * 2. 使用HashMap<String,Integer> 存储项集以及项集的重复次数，便于查找。是{项集}的集合。 * 3. 每个项集用String类型存储。项集中的每一项使用String.split(" ")方法变成数据，以便取用项集中的每一项 * * 给定的数据格式： * 1. 每个数据以空格隔开 * 2. 每一行代表一个事务T,事务编号就是行号 * * 示例数据： * 1. {11，12，13}是一个频繁项集，是属于HashMap<String,Integer>中的一个元素，数据类型为String. * 2. Set<String> = HashMap.keySet(); 用来存储HashMap的所有key的值，也就是存储所有的项集（不包括项集的重复次数） * * 相关概念： * 1. 支持度百分比 * 2. 支持度 * 3. 事务 * 4. 候选项集 * 5. 频繁项集 */ public class Main { private static double SUPPORT_PERCENT = 0.01; private static List<String> data2DList = new ArrayList<>(); public static void main(String[] args) { System.out.println("===================Apriori算法主程序界面===================="); //0. 输入数据 /*Scanner in=new Scanner(System.in); SUPPORT_PERCENT = in.nextDouble();*/ //1. 导入数据 importData(); //2. 进行算法处理 apriori(); } /** * Apriori算法主程序，需要递归处理 * * @return */ public static void apriori(){ //扫描整个数据库D，对每一项进行计数，获得一项的{候选项集合} Map<String, Integer> stepFrequentSetMap = new HashMap<>(); System.out.println("\n=====================第" + 1 + "次扫描的频繁项集列表======================" + "\n"); stepFrequentSetMap.putAll(getFrequentSets(findCandidateOneSets())); Set<String> stringSet = stepFrequentSetMap.keySet(); for (String string: stringSet){ System.out.println("频繁集：" + string + "支持度:" + stepFrequentSetMap.get(string)); } System.out.println("\n频繁项集的个数：" + stringSet.size()); int i = 1; //当生成的频繁项集为空的时候，退出循环 while(stepFrequentSetMap != null && stepFrequentSetMap.size()>0){ i++; //打印当前的频繁项集的信息 System.out.println("\n=====================第" + i + "次扫描的频繁项集列表======================" + "\n"); stepFrequentSetMap = getFrequentSets(getMinCandidate(stepFrequentSetMap)); if (stepFrequentSetMap != null){ stringSet = stepFrequentSetMap.keySet(); for (String string: stringSet){ System.out.println("频繁集：" + string + "支持度:" + stepFrequentSetMap.get(string)); } System.out.println("\n频繁项集的个数：" + stringSet.size()); } } } /** * 导入数据 * * @return 集合和集合，即二维集合 */ private static void importData(){ File file = new File("retail.dat"); try { //文件存在且为文件类型执行接下来的操作 if (file.isFile() && file.exists()){ InputStreamReader reader = new InputStreamReader(new FileInputStream(file), "UTF-8"); BufferedReader bufferedReader = new BufferedReader(reader); String lineTxt; while ((lineTxt = bufferedReader.readLine()) != null){//读取文件中的一行 data2DList.add(lineTxt); } reader.close(); }else { System.err.println("找不到指定文件！"); } }catch (Exception e){ System.err.println("读取文件内容出错！"); e.printStackTrace(); } } /** * 寻找1项的候选集C1（起始化特殊的处理） * * @return 返回map集合，这个集合是每一项以及对应的重复次数：key->value * * key：每一个候选项/频繁项 * value：相应候选项/频繁项的重复次数 */ private static HashMap<String, Integer> findCandidateOneSets() { HashMap<String, Integer> resultSetMap = new HashMap<>(); for(String dataList :data2DList) { String[] dataString = dataList.split(" "); //查询map集合中是否有该元素，如果没有，加入该元素，否则给该元素对于的value(重复次数）+1 for (String string :dataString){ string += " "; if (resultSetMap.get(string) == null){ resultSetMap.put(string,1); }else { resultSetMap.put(string,resultSetMap.get(string) + 1); } } } return resultSetMap; } /** * 从L(k-1)频繁集合中得到精简的C(k)候选集 * * 1. {连接 + 剪枝} = {精简后的候选集C(k)} * 是对L(k-1)频繁集的每一个集合，进行[边连接成候选集，边剪枝]而不是先全部生成了{候选集}，然后再进行一次大循环，进行剪枝，这样会增加时间复杂度 * * 先验原则：若某个集合存在一个非空子集不是频繁项集，则该集合不是频繁项集 * * 1. 根据先验原则压缩候选集大小后得到精简的候选集集合 * 2. 对精简过的候选项集进行累加计数 * * @param frequentMapSet * @return 返回C(k)候选集合 */ private static Map<String, Integer> getMinCandidate(Map<String, Integer> frequentMapSet){ //需要返回的精简过后的候选项集 Map<String,Integer> minCandidateMapSet = new HashMap<>(); //导入频繁项集的KEY Set<String> frequentSet = frequentMapSet.keySet(); /** * 1. {自连接过程:产生候选项集} * * 对于每一个项集，拆开成项的数组。将该项集的每一项与其他项集的每一项比较， * 如果有不重复的项，就将该项与原项集链接起来，组成K项的候选项项集。 */ for (String frequentItemList1: frequentSet){ for (String frequentItemList2: frequentSet){ String[] itemArray1 = frequentItemList1.split(" "); String[] itemArray2 = frequentItemList2.split(" "); //自连接生成的候选项集 String linkString = ""; boolean flag = true;//是否可以连接 /** * 判断是否可以自连接的条件： * 1. 前K-1项必须相同 * 2. itemArray1的最后一项必须小于第二个项集的最后一项 * 自连接过程： * 将第一个项集与第二个项集的最后一项连接起来 */ for (int i =0;i<itemArray1.length -1 ;i++){ if (!itemArray1[i].equals(itemArray2[i])){ flag = false; break; } } if (flag && itemArray1[itemArray1.length - 1].compareTo(itemArray2[itemArray1.length -1]) < 0){ linkString = frequentItemList1 + itemArray2[itemArray2.length - 1] + " "; } /** * 2. {剪枝过程，也就是先验规则的使用} * * 找出该候选集的所有子集，并判断每个子集是否属于频繁子集 */ boolean hasInfrequentSubSet = false;// 是否有非频繁子项集，默认无 if (linkString != ""){//自连接成功 //System.out.println(linkString); //候选项集的所有项的数组 String[] itemArray = linkString.split(" "); //*重点：找出该候选集的所有子集，实际操作的时候只需要找出比候选集少一项的子集集合 //*重点：因为这都是不断的递归上来的，项数更小的项集肯定是频繁项集 for (int i = 0; i <itemArray.length; i++){ String subString = ""; for (int j = 0;j<itemArray.length; j++){ if (j!=i){ subString += itemArray[j] + " "; } } if (frequentMapSet.get(subString) == null){ hasInfrequentSubSet = true; break; } } }else{ hasInfrequentSubSet = true;//这里并不是代表存在非频繁子集，只是表示没有自连接成功，没有找到候选集 } //自连接成功，加入到候选集集合中 if (!hasInfrequentSubSet){ minCandidateMapSet.put(linkString,0); } } } /** * 3. {对生成的候选集进行统计支持度} */ Set<String> minCandidateSet = minCandidateMapSet.keySet(); // 将每一行的候选项集，由String数据变成字符串数组。 // 将每一行的事务，由String转换成List<String> for (String itemList: minCandidateSet){ String[] strings = itemList.split(" "); int num = 0; for (String data:data2DList){ List<String>dataList = Arrays.asList(data.split(" ")); Boolean flag = true; //如果候选项集中有一项在当前事务中找不到，支持度则不会增加 for (int i =0;i < strings.length;i++){ if (!dataList.contains(strings[i])){ flag = false; break; } } if (flag){ minCandidateMapSet.put(itemList,minCandidateMapSet.get(itemList) + 1); } } } return minCandidateMapSet; } /** * 从精简后的C(k)候选集中得到L(k)频繁集合 * * {统计精简后的候选集C(k)的重复次数} = {最后得到L(k)频繁集} * * 1. 对精简过的候选集进行判断（之前已经做好了计数工作），不满足支持度的进行排除 * * @param minCandidateMapSet * @return */ private static Map<String, Integer> getFrequentSets(Map<String, Integer> minCandidateMapSet){ if (minCandidateMapSet == null){ //精简后的候选集为空，表示当前项数的候选集不存在，此时需要结束该算法了 System.err.println("候选项集为空"); return null; }else{ Map<String,Integer> frequentMapSet = new HashMap<>();//需要返回的频繁项集 Set<String> minCandidateSet = minCandidateMapSet.keySet();//获取候选项集的KEY，也就是所有的项集的具体数据 Double SUPPORT = (data2DList.size() * SUPPORT_PERCENT);//最小支持度 //Double SUPPORT = 5.0; System.out.println("最小支持度为：" + SUPPORT + " 候选项集的大小为：" + minCandidateMapSet.size() + "\n"); for (String itemListString: minCandidateSet){ //如果该项集的重复次数大于或者等于最小支持度，就把该项加入到频繁项即中 if (minCandidateMapSet.get(itemListString) >= SUPPORT){ frequentMapSet.put(itemListString,minCandidateMapSet.get(itemListString)); } } if (frequentMapSet.size() == 0){ //计算得到的频繁项集为空，表示此时循环应该结束了 return null; }else{ return frequentMapSet; } } } }
问题描述handsome主题评论的时候，如果是子评论，评论内容前面会有一个@某人的锚点链接。由于主题顶部的导航占用了50px的高度，所以点击锚点链接的时候必须向下偏移原来的位置50px，才能正常显示。为此，之前写了一个解决方法：锚点链接跳转后位置上下偏移一定位置方法但是，对于ajax生成的评论，如果不刷新页面的话，点击子评论左边的锚点链接却不会向下偏移。即：以前写的js对动态生成的页面元素不管用！举个很简单的demo，见下面的代码： 这是一个段落。 请点击任意一个 p 元素，它会消失。包括本段落。 在本按钮后面插入一个新的 p 元素 这段代码很简单。有两个功能：点击按钮会插入一个 标签及其内部内容点击p标签会 隐藏 或 显示 当前 p 元素但是这段代码的问题是：点击按钮生成 p 标签，如果点击生成的这个 p 标签，则无法 隐藏 或 显示 当前 p 元素。问题出在哪儿呢？解决方法原因在于，浏览器解析js代码的时候，会寻找当前页面的所有 p 标签，并给每个 p 标签绑定上相关事件（slideToggle()）。但是未来动态生成的 p 标签却无法再次重新绑定相关的事件了！因为js已经被浏览器解析完了 这里可以使用jQuery中的delegate()方法。我们通过bind绑定的点击（click）事件，只适用于当前页面的元素使用delegate绑定的点击事件，却可以适用于当前或未来的元素（比如由脚本创建的新元素）。所以核心代码改成$(document).ready(function(){ $("div").delegate("p","click",function(){ $(this).slideToggle(); }); });delegate的语法是：$(selector).delegate(childSelector,event,data,function)简单来说，即为指定元素（selector）的子元素（childSelector） ，监听某些事件（event），绑定相关函数（function）。该bug会在handsome v3.3.0 版本中得到解决参考文章js动态加载HTML元素时出现的无效的点击事件 - 何甘霖 - 博客园
前言系列文章：typecho博客实现pjaxinstantclick 中文文档相关文章： Typecho · JS简单实现评论ajaxTypecho全站pjax方案阅读前须知：以下内容对于实现了pjax效果的站点，可能需要自行增加回调函数。以下内容完全适用于实现了instantclick站点标有（*）内容为可选内容，可略去不读如有疑问可以发送邮件或者直接评论，千万不要加我QQ提问代码分析ajax的基础知识这里使用的是jQuery ajax的底层的一个方法：$.ajax()，执行这个方法的时候，会返回一个创建的XMLHttpRequest对象。基本语法：$.ajax({ url: ,//发送请求的地址 type: ,//请求方式，是post还是get请求 data: ,//要发送到服务器的数据 error: function() { //请求失败执行的回调函数 }, success: function(data) { //请求成功执行的回调函数，参数data是请求成功后的页面的纯文本 HTML 代码 } }) 实现步骤监听comment_form评论框的提交事件：$(comment_form).submit(function() { //提交事件 }）(*) 检查评论内容以及作者邮箱内容是否符合规则（预检）：这段代码比较简单易懂，不再赘述，直接看代码就行了。发起ajax请求：就是最基础的$.ajax() 请求：$.ajax({ url: $(this).attr('action'),//提交表form的action属性 type: $(this).attr('method'),//提交表form的method属性 data: $(this).serializeArray(),//要发送到服务器的数据 error: function() { //这里写一下提示信息代码即可 }, success: function(data) { //这部分内容在第四步中会解释 } }) 获取到ajax请求的数据，从这些数据中找到最新评论的内容，并把该内容插入到当前页面中：如何从ajax成功返回的数据中找到最新评论，我有两个思路：从ajax请求成功返回的数据中寻找到最新的评论插入到当前页面中。直接在提交的时候，拿一个变量把用户提交的内容保存起来，然后，直接按照你的评论结构组装成一条评论插入到当前页面中我是第一种方法，所以以下代码分析均只适用与第一种方法。第一个问题：理解ajax请求成功后返回的数据是什么东西？返回的数据是执行提交操作后页面的html代码，换句话说，返回的数据（data）就相当于手动刷新当前页面后的html代码所以，在返回的数据里面，肯定是可以找到最新评论的！第二个问题：如何找到最新评论的内容和应该插入到什么位置？首先你得保证你的评论将较新的的评论显示在前面。其次，我把用户评论简单分成两种：母评论（parent）：直接评论文章的子评论（children）：评论文章中的某条评论我们先来看如何找到最新评论的内容：其实很简单：typecho生成commentID的时候总是越来越大的，所以只要找到数据中最大的那个commentID就是用户刚才评论的最新评论了！代码就是：new_id = $(comment_list, data).html().match(/id=\"?comment-\d+/g).join().match(/\d+/g).sort(function(a, b) { return a - b }).pop(); 但是其实还是存在一个问题的：如果你启用了评论分页功能，由于typecho显示最新评论总是在第一页，所以当用户不在第一页发出了母评论，那么从数据（data）中找到的ID最大的评论并不是最新评论，这个时候，发送ajax请求成功后，就没必要向当前页面插入评论，因为最新评论再第一页。if ($('.page-navigator .prev').length && parent_id == ""){ new_id = ''; }//做一个判断 ... if (new_id)//new_id不为空的时候才会插入 $('#' + parent_id + " .comment-children .comment-list").prepend(data);接下来就是插入的位置问题:其实很简单，如果是母评论，直接插入到comment_list的最前面即可：$(comment_list).prepend(data);如果是子评论，要在监听页面中的回复链接，当鼠标点击回复按钮的时候，通过父元素的父元素（根据个人的评论结构而定），找到回复当前的评论ID(parentID)：$(comment_reply + ' a').click(function() { // 回复 parent_id = $(this).parent().parent().parent().parent().attr("id"); $(textarea).focus(); }); $('#cancel-comment-reply-link').click(function() { // 取消 parent_id = ''; });插入的位置就是回复当前评论的最前面就行了：if (parent_id) { data = $('#comment-' + new_id, data).hide(); // 取新评论 if ($('#' + parent_id).find(".comment-children").length <= 0) { $('#' + parent_id).append(" "); } if (new_id) //new_id不为空的时候才会插入 $('#' + parent_id + " .comment-children .comment-list").prepend(data); //console.log('该评论为子评论,parent_id:' + parent_id); parent_id = ''; //console.log(data); } 代码分析到这里就结束了！handsome主题已经内置该部分代码，并且做了优化，在外观设置——高级设置中启用即可。代码下载我把相关代码写到了一个文件里ajax_comment.min.js需要的小伙伴直接下载代码，因为每个主题的评论结构有所不同，需要自行修改后才能使用。
建议直接脱到最后食用更佳。今天班群里面通知：申请实验班的同学需要把GPA报给团支书。实验班是小班教学，一个班只有30多人，老师也是很厉害的，不仅如此，氛围肯定是不一样的想当初大一下学期刚开始的时候，就有机会报名实验班，据说面试的是我们授课老师，很好过，一些GPA3.0左右的都进了，当时我GPA3.63，可是当时害怕报不上就没报！以至于，大二上学期《数据结构》蹭了大半个学期的实验班的课（我们老师讲的实在太快），造成了很大的负担。这次有机会了，可是上一学期考的并不理想，不知道这次能否成功，不过不管了！先申请再说。接下来是如何使用excel表格快速计算平均GPA。GPA公式是这样的：每一科的学分*绩点之和/所有科目的学分之和这里需要这个函数：sumproduce，sum指的是求和，product是求积。组合在一起就是乘积之和。先把教务网的成绩直接复制到excel表格中。如上图所示，学分栏G3-G28，绩点栏H3-H28每一科的学分绩点之和 公式就是：SUMPRODUCT(G3:G28,H3:H28)所有科目学分之和公式就是：SUM(G3:G28)最后公式就是：=SUMPRODUCT(G3:G28,H3:H28)/SUM(G3:G28)最后结果是：大一第一学期 3.63大一第二学期 3.64 大一第三学期 2.78大二第一学期 3.24 大二第二学期 3.38大二第三学期 3.55大三第一学期 3.36综合(所有)3.44不要问我为啥大一第三学期GPA这么低，因为那是小学期啊！小学期，是夏天最热的时候，只有两门课，一共10多天，大家基本都无心上课，导致多核程序设计学的一塌糊涂！！！惨痛的教训这么简单一个公式，我居然能写这么多文字！学期学年学分绩点课程名称成绩课程性质22015-201634数据结构与算法（I）91专业必修22015-201643.33大学英语B82公共基础必修22015-201663.67高等数学(Ⅱ)85公共基础必修22015-20162.54离散数学(Ⅰ)92专业必修22015-201613.33金工实习B+实践环节必修22015-201633.67思想道德修养与法律基础86公共基础必修22015-20161.54普通物理实验(Ⅰ)A公共基础必修22015-201643.33普通物理(Ⅰ)84公共基础必修32015-201622.33多核程序设计C+专业选修32015-201613.67程序设计实训A-实践环节必修12015-201644程序设计基础A专业必修12015-201622.67计算机科学导论B-专业必修12015-201643.33大学英语C83公共基础必修12015-20163.54线性代数93公共基础必修12015-201664.33高等数学(Ⅰ)96公共基础必修12015-201622.67中国近现代史纲要76公共基础必修12015-201612.33军事理论72公共基础必修12015-201613军事训练B实践环节必修22016-20173.54.33操作系统原理95专业必修22016-20173.52.67数字逻辑76专业必修22016-20172.53.33数论与组合数学B+专业选修22016-20173.53.67概率论与数理统计88公共基础必修22016-201724计算方法A专业选修22016-201763.67毛泽东思想和中国特色社会主义理论体系概论85公共基础必修22016-201721形势与政策合格公共基础必修32016-201723.33认识实习B+实践环节必修32016-201714操作系统原理课程设计A实践环节必修12016-201714数据结构课程设计A实践环节必修12016-20172.54数据结构与算法（II）94专业必修12016-201743电路与电子技术81专业必修12016-201743.33大学英语A84公共基础必修12016-20172.53.33离散数学(Ⅱ)83专业必修12016-201733马克思主义基本原理78公共基础必修12016-20171.53.67普通物理实验(Ⅱ)A-公共基础必修12016-201742.67普通物理(Ⅱ)75公共基础必修12017-20182.53.67Linux和Unix编程基础87专业选修12017-20182.53.67面向对象程序设计A-专业选修12017-20182.54Java程序设计A专业选修12017-20183.53编译原理80专业必修12017-20184.53计算机组成原理80专业必修12017-20183.53.67数据库原理86专业必修12017-201813.67数据库原理课程设计A-实践环节必修12017-201813计算机组成原理课程设计B实践环节必修12017-201833.67移动互联网技术A-专业选修12017-201822.33计算机图形学C+专业选修12017-201823.33数据仓库与数据挖掘84专业选修
官方文档对这方面写的比较简洁，不利于主题修改者快速入门typecho，所以总结了一些经验，方便自己也方便主题修改者创建评论模块。先写一个评论区的总的容器，id 为 comments。 ... 评论区域分为两个部分：评论输出列表模块 和 评论输入表单模块。评论输出列表模块有两种方法：使用官方自带的函数输出，方便简单，只需要加点CSS控制就行自定义评论列表模块,更灵活满足我们需求官方自带的函数输出评论代码如下：<?php $this->comments()->to($comments); ?> <?php if ($comments->have()): ?> <?php $this->commentsNum(_t('暂无评论'), _t('仅有一条评论'), _t('已有 %d 条评论')); ?> <?php $comments->listComments(); ?> <?php $comments->pageNav('« 前一页', '后一页 »'); ?> <?php endif; ?>代码很好理解。不再赘述。说明一下 ：官方文档—快速入门—调用评论页 给出的代码示例是错误 的！代码示例只能实现简单输出评论，不能对每条评论回复，是个半残品 。但是里面的函数是可以在自定义评论模块中来使用的。我们这里输出评论模块使用的使用官方自带的函数 $comments->listComments(); 。输出的HTML结构也是写死的。所以，接下来是如何自定义评论输出列表模块。自定义评论输出列表模块官方文档—自定义评论列表区域 这里面给出了很混乱的代码讲解，实质上是写了一个 threadedComments 函数来替代系统自带的 threadedCommentsCallback函数（在/var/widget/comments/archive.php内置）。这个函数是php与html互写的形式。可以看出这个函数分为了两个部分：php动态控制输出结构html语句直接固定输出结构<?php function threadedComments($comments, $options) { ... //动态控制结构 ?> ... //html 固定结构 } <?php } ?> 动态控制结构 $commentClass = ''; if ($comments->authorId) { if ($comments->authorId == $comments->ownerId) { $commentClass .= ' comment-by-author'; //如果是文章作者的评论添加 .comment-by-author 样式 } else { $commentClass .= ' comment-by-user'; //如果是评论作者的添加 .comment-by-user 样式 } } $commentLevelClass = $comments->_levels > 0 ? ' comment-child' : ' comment-parent'; //评论层数大于0为子级，否则是父级如果是文章作者的评论添加 .comment-by-author 样式如果是评论作者的添加 .comment-by-user 样式通过判断 $comments->_levels 来确定当前评论的是子级评论还是父级评论这部分一般不需要更改的，即使修改也是增加语句。HTML固定输出结构<li id="li-<?php $comments->theId(); ?>" class="comment-body<?php if ($comments->levels > 0) { echo ' comment-child'; $comments->levelsAlt(' comment-level-odd', ' comment-level-even'); } else { echo ' comment-parent'; } $comments->alt(' comment-odd', ' comment-even'); echo $commentClass; ?>"> <div id="<?php $comments->theId(); ?>"> <?php $comments->gravatar('40', ''); ?> <?php $comments->author(); ?> <a href="<?php $comments->permalink(); ?>"><?php $comments->date('Y-m-d H:i'); ?> <?php $comments->reply(); ?> <?php $comments->content(); ?> <?php if ($comments->children) { ?> <?php $comments->threadedComments($options); ?> <?php } ?> 大家马上会发现之前官方文档的错误示例代码提供的函数 ，在这里派上了用场！代码很好理解，每条评论是一个 列表项目，包含了 评论作者的头像 评论作者的信息 和 子级评论 。子级评论和父级评论的HTML结构是相同的，只是CSS样式不同而已。我们把两个部分组合在一起，就构成了我们的自定义评论输出模块。<?php function threadedComments($comments, $options) { $commentClass = ''; if ($comments->authorId) { if ($comments->authorId == $comments->ownerId) { $commentClass .= ' comment-by-author'; } else { $commentClass .= ' comment-by-user'; } } $commentLevelClass = $comments->levels > 0 ? ' comment-child' : ' comment-parent'; ?> <li id="li-<?php $comments->theId(); ?>" class="comment-body<?php if ($comments->levels > 0) { echo ' comment-child'; $comments->levelsAlt(' comment-level-odd', ' comment-level-even'); } else { echo ' comment-parent'; } $comments->alt(' comment-odd', ' comment-even'); echo $commentClass; ?>"> <div id="<?php $comments->theId(); ?>"> <?php $comments->gravatar('40', ''); ?> <?php $comments->author(); ?> <a href="<?php $comments->permalink(); ?>"><?php $comments->date('Y-m-d H:i'); ?> <?php $comments->reply(); ?> <?php $comments->content(); ?> <?php if ($comments->children) { ?> <?php $comments->threadedComments($options); ?> <?php } ?> <?php } ?>评论输入表单模块这部分很简单，直接用HTML静态结构来固定输出内容。先贴出官方文档给出的代码： <?php if($this->allow('comment')): ?> Leave a Reply <form method="post" action="<?php $this->commentUrl() ?>" id="comment_form"> <?php if($this->user->hasLogin()): ?> Logged in as <a href="<?php $this->options->adminUrl(); ?>"><?php $this->user->screenName(); ?>. <a href="<?php $this->options->index('Logout.do'); ?>" title="Logout">Logout » <?php else: ?> <input type="text" name="author" class="text" size="35" value="<?php $this->remember('author'); ?>" /> Name (Required) <input type="text" name="mail" class="text" size="35" value="<?php $this->remember('mail'); ?>" /> E-mail (Required *will not be published) <input type="text" name="url" class="text" size="35" value="<?php $this->remember('url'); ?>" /> Website <?php endif; ?> <?php $this->remember('text'); ?> <?php endif; ?>看得懂注释，应该也能看的懂代码。注意以下两点：<?php if($this->allow('comment')): ?> 这个函数详见分离文章的评论和引用通告typecho 提供了 <?php $this->remember(''); ?> 函数可以用来记住之前已经输入过的名称、邮箱地址和网址和评论内容。
lazyload.js 减轻图片加载负担图片延迟加载大家可能有听说过，其实qqdie的博客早就使用了这个jquery插件，这款插件只有4K！但是有一个缺点是文章中的图片并没有使用延迟加载，问题在于这款jquery 插件 对于图片 的结构的要求必须是这样的： 解释一下这个结构：src里面存放占位图，data-orignal 存放的才是真实的图片地址。当滚动到图片位置的时候，这个插件才会把真实的地址赋给src ，这样图片就会加载出来。是一种延迟加载的策略。这个结构对于已经固定死结构的图片（比如评论区的头像或者其他的一些结构固定的图片）比较容易，自己改一下就很简单，但是对于文章中的图片 如何自动生成这种结构，却让人头痛，总不能让用户手动在后台编辑器添加图片吧，这是不人性化的~ 一开始是想通过js获取到所有图片，然后把图片的data-orignal 属性赋值为src的值，再把 src 属性全部赋值为img/grey.gif。代码其实很简单，是这样的：//对文中图片惰性加载 var imgs = $(".wrapper-lg img"); for (var i = 0; i < imgs.length; i ++) { var srcvalue = $(imgs[i]).attr("src"); $(imgs[i]).attr("data-original",srcvalue); $(imgs[i]).attr("src","<?php $this->options->themeUrl("img/grey.gif") ?>"); }效果非常尴尬，本来图片先出来，然后界面闪一下，图片变成占位图。？？？这样就本末倒置，起不到任何作用！如果不能对文章图片延迟加载，这个插件其实没有多大作用的！就在我要放弃的时候，发现了这篇文章[LazyLoad+ob_start()，完美解决图片延迟加载 [jQuery 插件]](http://www.rainleaves.com/html/1248.html) ,作者提供了新的方法：通过php的ob_start()函数在页面加载区的时候就更换图片属性 。关于ob_start();函数原理原博文有介绍，不再赘述，这里介绍使用方法：在footer.php 中<?php //图片延缓加载相关处理，替换src为data-original，并添加占位符 $echo = ob_get_contents(); //获取缓冲区内容 ob_clean(); //清楚缓冲区内容，不输出到页面 $placeholder = "img/grey.gif"; //占位符图片 $preg = "/ /i"; //匹配图片正则 $replaced = ' '; print preg_replace($preg, $replaced, $echo); //重新写入的缓冲区 ob_end_flush(); //将缓冲区输入到页面，并关闭缓存区 ?>代码也非常容易理解，就是设置页面中所有的图片(事实上并不是所有的)的src 和data-original 的值。最后使用$("img").lazyload();就可以实现延迟加载的效果了。那么问题来了！我不想所有的图片都延迟加载，怎么办？其实上面的正则匹配中隐藏了一个黑名单机制！图片的html结构为 即图片标签的结束标签/前面如果没有空格 ，则不会被正则表达式匹配成功，也就不会延迟加载。好了，这样一来，解决了所有我遇到的问题了！最后需要了解的是延迟加载的常见设置项：加载特效：图片会以淡入的方式显示（更多特效参阅官方文档）$("img").lazyload({ effect : "fadeIn" });触发加载方式：可以选择点击图片才显示内容$("img").lazyload({ event : "click" });当然，还有一些更有趣的设置项可以参阅官方文档。插件地址：tuupola/jquery_lazyload对了，如果你对图片的黑名单机制有什么更好的想法，欢迎一起讨论。
怎么学JavaScript？JavaScript 我也学了一段时间，可是每次写起代码来总是入门级别，很是苦恼。这篇文章中说把整本书给敲下来未尝不是一个好办法，我很有体会，有时候看不进代码，敲下来的过程中就在慢慢的吸收代码的精髓，觉得很受益处，遂转载之。转载原文：统一回复《怎么学JavaScript？》鉴于时不时，有同学私信问我(老姚，下同)怎么学前端的问题。这里统一回复一下，如下次再遇到问我此问题同学，就直接把本文链接地址发给你了。首先说句题外话。关于有人管我叫大神的事情。个人感觉这跟你买东西时，人家管你叫帅哥一样，你答应与否都无妨。正题开始，前端怎么学，应该因人而异，别人的方法未必适合自己。就说说我的学习方法吧。我把大部分时间放在学习js上了。因为这个js的学习曲线，先平后陡。项目实践和练习啥的，我不说了，主要说下工作之外的时间利用问题。我是怎么学的呢，看书，分析源码。个人这几天统计了一下，前端书籍目前看了50多本吧（点击我）。大部分都是js的。市面上的书基本，差不多都看过。第一个问题是，看书有啥好处？好处应该是不言而明的，书看多了，基础会逐渐夯实起来。看多了，自己的判断力，自然就上来了。看别人的文章，就能很快判断出，对方每块儿讲得对不对，哪块儿又是自己不清楚的，模棱两可的。当然也为看源码，分析源码提供了基础。10本书读2遍的好处，应该大于一本书读20遍。10本书的交集，那就是基础知识的核心，而并集那就是所有的知识。好书当然要多读，反复读。但是只读一本是不行的。因为每本书的侧重点都不一样。从不同的侧面，去理解一个知识点，是很有意义的。所以特别佩服印度人，他跟你讲英文，你一个词语没听懂，他会蹦出n个同一意思的单词，你听懂一个，就ok了。看书也是这样的，某一块讲得不透彻，不用担心，其他书籍可以帮助你来了解。第二个问题是，书籍推荐。个人觉得不错的，没事可以翻翻的。书籍如下：《javascript面向对象编程指南》，风格轻松易懂，比较适合初学者，原型那块儿讲得透彻，12种继承方式呢。《js权威指南》、《js高级程序设计》，这两本书经典是经典，但是太厚，适合把其中任意一章都当成一本书来读。洋洋洒洒，很难一口气看完。比较适合当做参考书。《你不知道的javascript》狙击js核心细节，闭包、原型、this讲得都还清楚。目前《中册》也出了，还在看。《js设计模式与开发实践》js设计模式也是要学的，此书把js的设计模式讲得非常清晰，一点不晦涩，看起来没多少难度。《正则指引》，分析源码时，如果正则表达式不懂，没法进行下去的。此书相对来说讲得比较清晰。《基于MVC的JavaScript Web富应用开发》，看完后，基本能写出自己的mvc框架了。是本好书。《javascript函数式编程》，js是一门函数式语言，此书是函数式编程一个入门，函数是一等公民那是非常重要的。《js忍者秘籍》，jq作者写的，没有传说中的那么难读，话说就算你看完并理解所有知识点，也不会达到世界高手级别的。因为你还没有做到随心所欲。《javascript框架设计》，如果初看此书，会觉得此书有罗列代码之嫌。在我看来，此书讲究的是框架的全局观。以上书籍是我认为是成就高手之路上必须看的，也需要反复看。css相关的书籍，说实话我看得比较少，总共有六七本吧。有两本必须推荐一下：《css权威指南》，css基础知识点那是讲得非常清楚的。什么层叠优先级、line-height啥的。不是随便一本书都敢叫“权威指南”的。《css揭秘》此书我也是不断的看，此书才不屑于全面讲css3各属性呢。css规范文档能讲的，它只会讲你最不在意的。此书解决的47问题，解决思路和解决方案同等重要，很有启发性。以上各书你都可以不买，至少买本此书吧。第三个问题，怎么看。想必很多同学，都想看书，但是很难看下去。文字部分相对来说还能看看，一遇到代码，头皮就发麻了。此问题一开始时我也遇到的。说一个学习理论。比如说学英语，有个开水理论。词汇量必须达到6000才行，如果没达到，英文水平不会上去的，这跟烧开水一样，没事烧烧，放着凉凉，从来没烧到100度，那么此水是永远不能喝的。一旦煮沸过，就可以随时喝了。20本书你看不下去，说明什么呢？任何一本书，你都没看完过。熟悉的，永远只是前三章。别笑，我原先也是这样的。那么现在的问题是，怎么把一本书看完呢？很简单，敲。《基于MVC的JavaScript Web富应用开发》这本书我看时，就是这样，终于有一天，我下定决心要把此书从头到尾敲一遍。文字加代码都敲，然后就一章一章得看完了。代码敲一遍后，你会发现，没之前看起来的那么难。如果你属于一看书就犯困那种同学。强烈建议你把《javascript面向对象编程指南》此书从头到尾敲一遍。坚持看完一本书后，信心就上来了。先保证看完一本再说。看完3本后，基本应该能做到几天就能看一本了。万事开头难，加油吧。第四个问题，看书的层次问题。书看完后，要自己总结，要与其他书籍对比看。有同学同时对比着看《权威指南》和《高设》来的。随便拿出个知识点，你都能闭着眼睛说得头头是道，说明水平够了。下一块就是源码的学习了。看框架源码之前，想说一件事情：dom的api不懂，没问题，你可以百度。但是正则一定要先研究研究。不然大多数人去尝试分析源码时，遇到的挫折都在于此。怎么去阅读源码呢？敲，照着敲。有哪些代码值得去敲呢？优秀框架或者库的源码都值得你去敲。但是拿jq来敲，来入门，那不行的。原因：太他么长了。八九千行呢！！个人觉得underscore.js库是不错的第一个选择。原因都是工具方法，敲完以后自己的水平应该略有小成吧。其实有一些api的实现，你要把它当成getElementById一样，深深的印在脑海里。比如extend方法，必须张口就来。敲完underscore库后，可以考虑去看看《javascript函数式编程》这本书了。jq的源码不好敲。那么zepto的源码比较少1800多行，敲一天应该敲完了。敲几遍后，把所有不懂的地方，都百度清楚，然后就可以写自己的类jq的库了。然后就可以作为一项技能写进自己的简历里。比如“创建过自己的jquery库”。当然敲的过程，还能帮助自己对jq的api认识。然后是backbone.js，因为此框架是以类jq和underscore为基础的mvc框架。代码也没多少行。敲吧。spine.js与backbone类似。可以在敲其之前，先看看那本《基于MVC的JavaScript Web富应用开发》。希望你的简历可以添加这么一笔，“创建过自己的mvc框架”。其他的，我也敲过一些。包括jq.validate.js，包括一些插件。如果你愿意的话，bootstrap你可以去敲敲啊。源码挺多的，可以按插件逐个来敲。分析明白了，轮播、分页、下拉框等等的插件那还不是分分钟随手就写一个了。最起码看看人家api接口是怎么设计的也是极好的。话说个人在阅读其css代码中，也学到了不少东西。说到插件，有两个必须提提，一个是表格插件，一个是树。都敲完，简历里可以这么写上，“创建过自己的UI框架”。当然了，你也可以敲你喜欢的框架代码，重要的是明白其实现原理，最好理解其为啥那么设计，如果对设计模式比较熟悉的话，会经常发现原来是这么回事。。。照着敲只是分析源码的入门，用途也是为了学习，最后能用在自己的项目中，那是才是正道。就算没啥用，也是打发时间的好方式，比看电视剧强多了。我闲着无聊时，就背着敲underscore源码。。。最后说句，如果你简历上能如期写上那几句话后，必须是大神。加油吧。后记写本文的最初目的，正如文章开头说的那样，方便自己回复大家的提问。本站的任何一篇分享学习经验的文章，基本都会引起共鸣，这确实是一个值得讨论的话题。这里再说说几个事情。1.有人问我前端工作经验事情。没几年。三年多。2.初学者或新手（beginner）怎么办？看书和分析源码是重要的提高方式，但不适合新手。新手需要的是能快速的入门和入行，能快速的上手工作。一种快捷的学习方式就是看视频。正如有的同学说得那样，知道有哪些东西，怎么用就可以了。看视频是有好处的，首先它是一种被动学习方式。我最开始的入门也是看视频来的，只需要看就行了。一遍没懂，再放一遍，我基本上是1.5倍数去看的。而读书是一种主动方式，需要自己一页一页翻。需要自己主动的去理解。而很多东西，也许只是视频老师一句话，就能突出的重点，需要我们自己去解读。还有另一件事情是，比如发现自己某个知识点不太清楚，可以单独去百度。比如this，文章很多的。这种学习方式也是快速掌握知识点的好办法。书籍需要技术评审，那么看文章一定要看看评论。不过视频就不好说了，视频一般都不会讲得太深入，偶尔也有讲错的。当年我也曾被一些视频误导过，建议找不错的视频看看。各大网站培训机构的免费视频挺多的。3.看书和分析源码的时机但已经工作一年半载时，正是提高的好时候，此时可以去看书了。全面系统的梳理知识点，扫清自己的盲区。如果只是靠项目经验是不够的，通过项目来学习，那肯定是必须的，工作本身就是一个学习的过程。但是工作三年不看书的话，学又能学到多少呢？更何况每个项目都很类似，一直处在舒适区，那真就是5年经验重复第一年的了。所以我不认同这句话：面试时强调自己的学习能力是工作能力不强的表现。3年经验的水平，完全有可能超过5年的。4.没有时间去学习？如果你还没毕业，就已经天天在本站混了。其实你领先了一大步。都是混过大学的。天天充斥着lol和电视剧的陪伴，我只想说进入社会是要还的。最可怕的是什么呢？该还、还不还（这几个字别念错了）。时间是有的，就看你愿意付出不。下班后学习，周末学习，节假日别人玩的时候，在家敲代码。这样才能领先别人。5.兴趣问题？兴趣和擅长是一个良性迭代循环。你擅长某件事情，就会越喜欢它，越喜欢，就越愿意花时间，进而越擅长。此道理都懂，只是缺乏一个trigger。如果你喜欢玩游戏的话，其实你可能非常适合做前端。玩游戏就是一个反馈机制，前端工作的反馈，相对其他工作来说也是非常及时的。代码一改，网页一刷，就看到效果了。擅长、优越感、成就感通常都是连在一起的。每看完一本书，我都觉得很有成就感。每敲完一个库，也有成就感。以上纯属一家之言，每个人的学习习惯、方式、态度都不一样。先端正态度、找到自己的学习方法，进而养成坚持下去的习惯。最后说一句，你我共勉：只要你走在正确的道路上，不管、走得多慢，都是前进！本文完。
同步更新官网文档：v3.1 2017/8/17instantclick 中文文档简介instantclick.js 是一种ajax无刷新和预加载页面的技术，对于普通博客程序，有着明显的加速作用。(instantclick.js ≈ pjax + 预加载页面)而且，使用方法也十分的简单。在github截止目前，已经由4447个star了，非常可观。但是官方文档纯英文，可能阅读上有点障碍，所以，在此，翻译一下大部分的官方文档内容，方便新手快速使用。大部分采用直译方式，需要注意的地方 用小括号注释起来。（尽管如此，instantclick.js 仍然有一些bug亟待解决，如果你可以忽略缺点吧，我们一起开始吧！）目录初级阶段开始使用instantclick 工作原理预加载页面黑名单规则事件和脚本的重新加载进阶阶段跟踪页面内容的变化自定义加载条初级阶段开始使用下载instantclick.js最新的发布版本是2014/12/25发布的V3.1.0 （虽然github一直有更新，但是并没有发布新的版本~）压缩版本2.7kb开发版本21kb其他的下载方式github上面的最新开发版本通过bower（一种前端开发工具） : bower install instantclick初始化instantclick把 instantclick.js 和 初始化代码 放在你的页面的结束之前( 标签结束之前)... 现在instantclick 已经成功在你的网页上面生效。阅读下面的文章以便更熟练的使用instantclick。instantclick 工作原理：将会让你建立正确的模型以便更好的理解instantclick的机制预加载页面：将会向你展示不同的预加载页面的方法黑名单规则：因为有些链接你不想预加载事件和脚本的重新加载：将会帮助你使instantclick更好的配合你的网页中的JavaScript。instantclick不总是“即插即用”（不是通过两行代码就可以在你的网页上运行），你可能需要自定义一些设置来适应你的网站，这也是为什么阅读上述文章是强制性的。instantclick 工作原理InstantClick与传统的web应用区别不大，但是很有必要去了解这些不同的地方！instantclick使浏览器不再刷新整个页面（即无刷新页面效果）首先：你需要理解的核心内容是：instantclick在技术上使你的网站成为单页应用程序；浏览器不再刷新整个页面，而是通过instantclick技术来更改页面内容，这意味着：你不能依赖DOMContentLoaded和jQuery.ready()这两个函数来触发相关事件（这两个事件在刷新整个页面的时候才会触发，但是你可以使用[InstantClick’s events]()来替代)依赖上面两个函数的第三方脚本（比如js代码）需要调整（参阅[事件和脚本的重新加载]()）在加载页面的时候，浏览器不会在显示原本加载进度条了，instantclick有它自己的加载进度条。其次：你需要明白instantclick只会改变 和title，所以你的脚本只会被加载一次（带来的好处是速度的明显提升）。下面还有一些内容你需要了解：通过instantclick加载的每个页面的 标签里面的样式和脚本应该是相同的（因为instantclick只会加载一次 标签里的内容）如果 标签里面的某些内容依赖于网页的内容（比如在页面加载时运行的脚本或css动画），它需要调整以便正常运行。InstantClick是渐进式增强功能：如果访问者的浏览器不支持InstantClick，您的网站链接将照常工作，只是不会提升速度了。预加载页面InstantClick 关于预加载方式有多种选择。你可以根据你的服务器配置来选择合适的方式。但是不管怎样，当每个页面改变时，不会重新加载脚本和样式表，这样会使你的页面加载速度提升一倍！（与Turbolinks、pjax等等技术是类似的）默认: 在鼠标悬停预加载(on mouseover)当用户鼠标悬停在链接上开始预加载页面。如果用户网络链接不错的话，你的页面会很快的打开。初始化方法就是[开始使用]()设置的方式。不会给服务器带来额外负担：在鼠标点击的瞬间预加载(mousedown)当用户按下你的链接按钮的瞬间，页面开始预加载。这为你的服务器带来了接近零的开销，但仍然能够带来一个很“神奇”的速度提升！使用方法：将'mousedown'作为参数传递给InstantClick.initInstantClick.init('mousedown');折中的方式：鼠标悬停延迟一定时间才会预加载如果用户在您选择的延迟过后仍悬停在链接上，则该网页将开始预加载。推荐的延迟时间为：100ms和50ms。超过100ms效果比mousedown效果更差。低于50ms，则会与mouseover效果相似。要使InstantClick延迟一定时间后预加载，请将延迟（毫秒）作为参数传递给InstantClick.init。InstantClick.init(50);手机端效果上面的任何预加载方式，在手机端的时候，当用户手指接触链接时候都会开始预加载。如果您的网站针对移动设备（在安卓和iOS界面上使用了[FashClick]()技术）进行了优化，则当访问者从链接中释放手指时，会发生“点击”，导致预加载大约100 ms的延迟。如果您的网站没有针对手机进行优化，延迟时间则取决于操作系统。 Android给出300 ms，iOS给出450 ms。在同一站点上的3G请求的延迟时间通常需要大约200ms。如何选择？如果您的网站可以处理额外的负载，选择 在鼠标悬停时预加载方式。如果你的网站不能，选择在鼠标点击的瞬间预加载方式。您的网站的速度仍然会超过99％的网站。如果你想确定你的服务器是否可以，先选择在鼠标点击的瞬间预加载方式，你的服务器几乎不会有额外的压力。然后使用鼠标悬停延迟100毫秒预加载。然后50毫秒延迟（或减少更小的减少，如果你有耐心）。然后直接用在鼠标悬停时预加载，分别看你的服务器是否能够承受额外的负担。如果服务器端分析很重要，你只能使用在鼠标点击的瞬间预加载，使用任何其他方式都会带来误差。黑名单规则有一些链接是不需要通过instantclick来预加载。黑名单规则可以实现这个效果。哪些需要进入黑名单哪些需要进入黑名单，而不能进入白名单的：链接指向操作，例如注销和切换语言。链接指向需要一段时间加载的非HTML内容链接指向的页面与当前页面 标签内的css样式和脚本不同链接触发JavaScript的操作部分链接已在内部列入黑名单，且无法列入白名单：链接有target或者download属性链接与当前的域名或者协议不同链接指向当前页面的锚点链接(#anchor)把一个链接列入黑名单把一个链接加入黑名单，只需要在链接中加入data-no-instant属性Blog把一组链接列入黑名单有时候要把一组链接一起列入黑名单，这时候比给所有链接分别添加data-no-intant要方便的多。把父元素内部的所有链接列入黑名单，只需要向该父元素添加data-no-instant属性。把一个链接或者一组链接列入白名单如果您已将某个父元素列入黑名单，并且希望将其中的某个链接（或者子元素内部的所有链接）列入白名单，只需要向该链接或子元素添加data-instant属性。InstantClick在后台中从当前链接的位置到 的位置，遍历所有的父元素，如果找到data-no-instant属性，它就会认为该链接已经被列入黑名单，并停止循环遍历父元素。如果它找到data-instant属性，它会认为该链接已经被列入白名单。（此处翻译可能需要修改）如果您希望默认将所有链接列入黑名单，然后逐个将链接列入白名单或仅想把某个容器的链接列入白名单，请在 添加一个data-no-instant属性，然后向该链接或者容器中添加data-instant属性。白名单模式下面的方法已经弃用，可能会在4.0版本中删除。只有当你的网站正确激活参数（真正的参数传递给InstantClick.init），这种方法才会有用。（此处翻译可能需要修改）正确的方式™：如果要实现与白名单模式相同的效果，只需添加data-no-instant到你的 标签中，参见上一条目“把一个链接或者一组链接列入白名单”。通过向InstantClick.init传递true参数来启用白名单模式。InstantClick.init(true); /* or */ InstantClick.init(50, true); /* or */ InstantClick.init('mousedown', true);事件和脚本的重新加载InstantClick技术上使你的网站成为单页应用程序，因此当页面切换的时候，不会触发DOMContentLoaded函数。因此，一些脚本可能需要调整才能与InstantClick正常工作。InstantClick会触发4个事件以便于挂钩到页面的整个生命周期：change:当前的页面一旦改变会触发该事件，即使浏览器不支持instantclick，页面初始加载的时候也会触发该事件，这个事件可以用来替换DOMContentLoaded.它的回调可以接受一个可选的isInitialLoad参数，它是一个布尔值，当它是初始页面更改或当InstantClick不被支持时为true，而当InstantClick更改页面时为false。（此处翻译需要修改）fetch:页面开始预加载的时候receive:页面已经预加载完成，你可以使用该事件[修改页面的内容]().wait:用户已经点击了链接，但是页面还没有开始预加载。仅在页面不是立即显示（可能由于网络原因）的时候触发。监听一个事件，比如change，你可以使用InstantClick.on:InstantClick.on('change', yourCallback);您需要在InstantClick.init之前调用InstantClick.on，因为change事件在网页初始加载时就会被触发，包括浏览器不支持pushState的时候。（此处翻译可能需要修改）如果在 标签内部有一个脚本，当instantclick切换到另一个页面的时候，你并不希望重新加载它 ，你可以添加一个data-no-instant属性。 如果有一些脚本与instantclick发生冲突，建议向所有脚本添加一个data-no-instant属性，然后逐个删除每个属性，直到找到罪魁祸首。您可以通过查看Turbolinks兼容性站点上的示例（在CoffeeScript中）了解如何解决兼容性问题。例如，以下是如何使Google Analytics（网站统计与分析）（2013年末的代码）正常工作： 在receive事件里改变页面内容有时，即时改变页面内容比为instantclick重构你的后端代码要简单。你可以使用receive事件。这个事件有三个参数：url， ，body 和title。url 接收的页面的地址，它包括哈希值。它是只读的。body是body对象，title是标题文本。如果你想在页面显示之前改变页面内容，你可以修改这两个参数并返回一个对象（或者只修改其中的一个参数）。下面是一个例子：InstantClick.on('receive', function(url, body, title) { var dont_display = body.querySelector('#dont_display_me_when_loaded_with_instantclick') if (dont_display) { dont_display.setAttribute('hidden', ''); } title += ' (loaded with InstantClick)'; return { body: body, title: title }; });要记住 body 对象是body 而不是document.body!当您有多个回调函数监听receive函数时，每个后续回调将获得最后更改的内容。如果你不想修改页面内容，则不用返回任何内容或返回false。进阶阶段跟踪页面内容的变化当前跟踪页面内容的变化的方式目前有点笨拙。它可能稍后在InstantClick 4.0中更改。要检查样式表或脚本（外部或内联）何时更新，请添加一个data-instant-track属性： InstantClick将检查href或src属性（如果存在）中的更改。要指示文件已更新，请修改其属性： 如果它是内联脚本或样式，InstantClick将检查元素内容中的更改。 当检测到任何更改时，InstantClick将重新加载页面，从而使浏览器重新评估所有脚本和样式。自定义加载条进度条是一个假的进度条，只是在那里给你的用户的感觉页面的加载进度。在未来，应该可以使进度条显示真正的进度（通过查看服务器的Content-Length头）。即使页面已经立即加载，也会显示进度条，在将来这将更改。你可以在github讨论。当访问者缩放页面或旋转其设备时，该栏的大小和位置会自动调整，因此即使您的网站未针对移动设备进行优化，也会正常工作。默认情况下，进度条的颜色为#29d, 你可以改变CSS:#instantclick-bar { background: white; }你也可以让进度条消失：#instantclick { display: none; }使进度条消失的方式不是最佳的。在稍后的InstantClick版本中，可能可以通过向InstantClick.init传递参数来实现这个效果。