漫漫技术路

  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

动手DIY一个underscorejs库及underscorejs源码分析3

发表于 2016-10-26 | 更新于 2018-11-30 | 分类于 前端技术

所有代码挂在我的github上,例子是demo6.html,DIY/4/_underscore.js.欢迎fork,star。
https://github.com/zrysmt/DIY-underscorejs

这一部分来DIY两个经常被使用的函数(或者说分析其源码),分别是throttle(节流函数)和debounce(防反跳函数)。

这两个函数特别适合一些场景:事件频繁被触发,会导致频繁执行DOM的操作,如下:

  • window对象的resize、scroll事件
  • 拖拽时候的mousemove事件
  • mousedown、keydown事件
  • 文字输入、自动完成的keyup事件

1.throttle节流函数

创建并返回一个像节流阀一样的函数,当重复调用函数的时候,最多每隔 wait毫秒调用一次该函数。对于想控制一些触发频率较高的事件有帮助。

默认情况下,throttle将在你调用的第一时间尽快执行这个function,并且,如果你在wait周期内调用任意次数的函数,都将尽快的被覆盖。如果你想禁用第一次首先执行的话,传递{leading: false},还有如果你想禁用最后一次执行的话,传递{trailing: false}。

也许你还没完全看懂,我们来做个demo测试下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="div1">
创建并返回一个像节流阀一样的函数,当重复调用函数的时候,最多每隔 wait毫秒调用一次该函数。对于想控制一些触发频率较高的事件有帮助。(注:详见:javascript函数的throttle和debounce) 默认情况下,throttle将在你调用的第一时间尽快执行这个function,并且,如果你在wait周期内调用任意次数的函数,都将尽快的被覆盖。如果你想禁用第一次首先执行的话,传递{leading: false},还有如果你想禁用最后一次执行的话,传递{trailing: false}
</div>
<script>
function updatePosition() {
console.log($('#div1').height(), $('#div1').width());
}
//不带options即第三个参数的时候(默认情况下),会执行两次,一次是执行时候的状态(A) ,一次是执行后的状态(B)
// {leading: false }不会执行第一次执行时的状态(A)
// {trailing: false}不会执行最后一次执行后的状态(B)
var throttled = _.throttle(updatePosition, 1000
/*,{
leading: false,
trailing: false
}*/
);
$(window).resize(throttled);
</script>

我们先看结果,后看下部分的源码实现。

  • 1.只拉动一次窗口,会响应两次updatePosition,分别对应状态A、B,示例Demo中有详细说明解释第三个参数。

  • 2.多次拉动窗口,第一次会立即响应,拖动比较快的时候,只会隔大概1000ms(自己设置的时间,默认100ms)响应一次。


源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
_.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};

var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);

if (!timeout) context = args = null;
};

var throttled = function() {
var now = _.now();//加入_.now(),这里不在单说,相见开头处提供的github地址。
if (!previous && options.leading === false) previous = now; //禁止第一次执行(A) remaining = wait - 0 = wait > 0 的话不会执行A
//不禁止第一次执行A的时候,previous = 0,现在时间now >= wait,就是过了wait等待时间
var remaining = wait - (now - previous); //remaining 第一次为< 0
console.warn(wait, now, remaining);
context = this;
args = arguments;
//按理来说remaining <= 0已经足够证明已经到达wait的时间间隔,但这里还考虑到假如客户端修改了系统时间则马上执行func函数(remaining > wait)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args); //第一次执行A
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) { //不会禁用第二次执行(B)
console.log("============第二次===============");
timeout = setTimeout(later, remaining); //第二次执行(B)
}
return result;
};

throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};

return throttled;
};

2.debounce 防反跳函数

返回 function 函数的防反跳版本, 将延迟函数的执行(真正的执行)在函数最后一次调用时刻的 wait 毫秒之后. 对于必须在一些输入(多是一些用户操作)停止到达_之后_执行的行为有帮助。 例如: 渲染一个Markdown格式的评论预览, 当窗口停止改变大小之后重新计算布局, 等等.

传参 immediate 为 true, debounce会在 wait 时间间隔的开始调用这个函数 。
示例Demo

1
2
var debounce = _.debounce(updatePosition, 1000);
$(window).resize(debounce);

  • 只会在停止操作后1000ms(自己设置的)执行

  • 加入第三个参数,会在操作的同时执行

var debounce = _.debounce(updatePosition, 1000,true);

首先一个使用的工具函数,不在这里详细说明了。

1
2
3
4
5
6
7
 _.delay = restArgs(function(func, wait, args) {
return setTimeout(function() {
return func.apply(null, args);
}, wait);
var restArgs = function(func, startIndex) {
};
_.restArgs = restArgs;

源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//immediate默认为false
//只在最后一次关闭的时候,延迟后执行一次
_.debounce = function(func, wait, immediate) {
var timeout, result;

var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
restArgs = _.restArgs; //增加
var debounced = restArgs(function(args) {
if (timeout) clearTimeout(timeout);
//控制timeout,一直拖动的时候会清除timeout,这样中间就不会执行了
if (immediate) {//immediate为true立刻执行
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}

return result;
});

debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};

return debounced;
};

好了就简单介绍到这里

所有代码挂在我的github上,例子是demo6.html,DIY/4/_underscore.js.欢迎fork,star。
https://github.com/zrysmt/DIY-underscorejs

动手DIY一个underscorejs库及underscorejs源码分析2

发表于 2016-10-26 | 更新于 2018-11-30 | 分类于 前端技术

接着第一篇《动手DIY一个underscorejs库及underscorejs源码分析1》

所有代码挂在我的github上。

1.兼容requirejs和seajs模块化

  • requirejs
    在代码的尾部加上
1
2
3
4
5
6
if (typeof define == 'function' && define.amd) {
//定义一个模块并且起个名字
define('_underscore', [], function() {
return _;
});
}

使用测试:代码请点我
demo3.html

1
2
3
<body>
<script data-main="demo3" src="lib/require.js"></script>
</body>

demo3.js

1
2
3
4
5
define(function(require) {
require(['../DIY/2/_underscore'], function() {
console.log(_);
});
});

  • 加上支持seajs的代码
1
2
3
4
5
6
7
8
9
if (typeof define == 'function' && define.amd) {
define('_underscore', [], function() {
return _;
});
} else if (typeof define == 'function' && define.cmd) { //seajs
define(function(require, exports, module) {
module.exports = _;
});
}

使用:
demo2.html

1
2
3
4
<script src="lib/sea-debug.js"></script>
<script>
seajs.use('./demo2');
</script>

demo2.js

1
2
3
4
define(function(require, exports, module) {
var _ = require('../DIY/2/_underscore');
console.info(_);
});

2.支持nodejs

1
root._ = _;

修改为:

1
2
3
4
5
6
7
8
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}

3._.extend

使用:

1
2
console.log(_.extend({name: 'moe'}, {age: 50}));
//结果Object {name: "moe", age: 50}
1
2
3
4
5
6
7
8
9
//类似与_.keys
_.allKeys = function(obj) {
if (!_.isObject(obj)) return [];
var keys = [];
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
// if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};

主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var createAssigner = function(keysFunc, defaults) {
return function(obj) {
var length = arguments.length;
if (defaults) obj = Object(obj);
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!defaults || obj[key] === void 0) obj[key] = source[key];
//将参数(对象)放入到obj组合到一起
}
}
return obj;
};
};
_.extend = createAssigner(_.allKeys);
_.extendOwn = _.assign = createAssigner(_.keys);

4.重要内部函数cb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var builtinIteratee;
//如果是函数则返回上面说到的回调函数;
//如果是对象则返回一个能判断对象是否相等的函数;
//默认返回一个获取对象属性的函数
var cb = function(value, context, argCount) {
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
if (value == null) return _.identity; //默认的迭代器
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value)) return _.matcher(value);
return _.property(value);
};
_.iteratee = builtinIteratee = function(value, context) {
return cb(value, context, Infinity);
};

4.1 _.identity

很简单但是是默认的迭代器

1
2
3
_.identity = function(value) {
return value;
};

测试很简单

1
2
var obj1 = {name:'zry'};
console.log(obj1 === _.identity(obj1));//true

4.2 _.matcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_.matcher = _.matches = function(attrs) {
attrs = _.extendOwn({}, attrs);
return function(obj) {
return _.isMatch(obj, attrs);
};
};
//两个对象是不是全等于。给定的对象是否匹配attrs指定键/值属性
_.isMatch = function(object, attrs) {
var keys = _.keys(attrs),
length = keys.length;
if (object == null) return !length;
var obj = Object(object);
for (var i = 0; i < length; i++) {
var key = keys[i];
if (attrs[key] !== obj[key] || !(key in obj)) return false;
}
return true;

};

测试:

1
2
3
4
5
var obj2 = {selected: true, visible: true};
var ready = _.isMatch(obj2,{selected: true, visible: true});
//返回一个断言函数,这个函数会给你一个断言 可以用来辨别
//给定的对象是否匹配attrs指定键/值属性
console.log(ready);//true

4.3 _.property

property函数在第一篇博客中已经实现

1
_.property = property;

5._.map

1
2
3
4
5
6
7
8
9
10
11
12
_.map = _.collect = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
results = Array(length);
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
//返回的是(value,key,obj)
}
return results;
};

6._.filter

1
2
3
4
5
6
7
8
_.filter = _.select = function(obj, predicate, context) {
var results = [];
predicate = cb(predicate, context);
_.each(obj, function(value, index, list) {
if (predicate(value, index, list)) results.push(value);
});
return results;
};

测试:

1
var evens = _.filter([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });//[2,4,6]

7.两个常用的工具函数_.escape,_.unescape`

7.1 _.escape

要过滤的字符串

1
2
3
4
5
6
7
8
var escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;'
};

主函数

1
2
3
4
5
6
7
8
9
10
11
12
var createEscaper = function(map) {
var escaper = function(match) {//match 匹配的子串
return map[match];
};
var source = '(?:' + _.keys(map).join('|') + ')';
var testRegexp = RegExp(source);
var replaceRegexp = RegExp(source, 'g');
return function(string) {
string = string == null ? '' : '' + string;
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
};
};

注意值了的string.replace函数第二个参数是个函数,那么返回的数据第一个是match(匹配的子串)

变量名 代表的值
match 匹配的子串。(对应于上述的$&。)
p1,p2, … 假如replace()方法的第一个参数是一个RegExp对象,则代表第n个括号匹配的字符串。(对应于上述的$1,$2等。)
offset 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是“abcd”,匹配到的子字符串时“bc”,那么这个参数将是1)
string 被匹配的原字符串。
1
_.escape = createEscaper(escapeMap);

测试:

1
console.log(_.escape('Curly, Larry & Moe')//Curly, Larry &amp; Moe

7.2 _.unescape

反转要过滤的字符串

1
2
3
4
5
6
7
8
9
10
_.invert = function(obj) {
var result = {};
var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
result[obj[keys[i]]] = keys[i];
}
return result;
};
var unescapeMap = _.invert(escapeMap);
_.unescape = createEscaper(unescapeMap);

测试:

1
console.log(_.unescape('Curly, Larry &amp; Moe'));//Curly, Larry & Moe

参考阅读:

  • http://underscorejs.org/
  • underscorejs中文:http://www.bootcss.com/p/underscore/
  • UnderscoreJS精巧而强大工具包
  • JS高手进阶之路:underscore源码经典

动手DIY一个underscorejs库及underscorejs源码分析1

发表于 2016-10-26 | 更新于 2018-11-30 | 分类于 前端技术

Underscore 是一个 JavaScript 工具库,它提供一整套函数编程的实用功能。他弥补了 jQuery 没有实现的功能,同时又是Backbone 必不可少的部分。

underscore.js源码加上注释也就1千多行,用underscore.js作为阅读源码的开始是一个不错的开始,当然阅读源码的同时,手也不能停下来。下面我会写几篇博客,一边分析源码,一边根据源码重新DIY一份(_underscore.js),基于版本:1.8.3。

underscore.js分为集合(Collections)、数组(Arrays)、函数(Functions)、对象(Objects)、工具函数(Utility)五大部分。

所有代码挂在我的github上。

1.简单的应用Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script src="underscore.js"></script>
<!-- <script src="../DIY/1/_underscore.js"></script> -->
<script>
console.info(_);
console.info(_.prototype);
/**
* 数组处理
*/
_.each([1, 2, 3], function(ele,idx) {
console.log(idx + " : " +ele);
});
_.each([1, 2, 3], console.log);
_.each({one: 1, two: 2, three: 3}, console.log);
</script>

打印结果:

展开console.info(_.prototype);打印的结果

1
2
3
4
5
6
7
8
+ Object
//... ...
- ()each:
- ()escape:
- ()every:
- ()extend:
//... ...
__proto__: Object

2.从_.each()开始

2.1 整体结构:IIFE

1
2
(function(){
}())

2.2 初始化_

1
2
3
4
5
6
7
8
9
10
11
12
13
//在浏览器上是window(self),服务器上是global
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this;

//形式:_([1, 2, 3]).each(function(ele) {});会执行下面的
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj; //存放数据
};
//形式:_.each([1, 2, 3], function(ele, idx) { });不会执行上面的函数,而是直接通过全局的_,寻找定义在_或者其原型上的方法
root._ = _;

2.3 两个类型isObject,isArrayLike判断

为了压缩我们把常用的方法/属性独立写成变量

1
2
3
4
5
6
7
var ArrayProto = Array.prototype,
ObjProto = Object.prototype;
var push = ArrayProto.push,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
var nativeIsArray = Array.isArray,
nativeKeys = Object.keys;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//判断是不是对象/函数
_.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
//属性中是否有key
var property = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length');
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

2.4 上下文绑定

1
2
3
4
5
6
7
var optimizeCb = function(func, context, argCount) {
//void 0 === undefined 返回ture
if (context === void 0) return func;
return function() {
return func.apply(context, arguments);
};
};

2.5 each方法

一个简单的版本属性

1
_.VERSION = '0.0.1';

_.each需要_.keys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 _.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {//类数组
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj); //(element, index, list)
}
} else {
var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj); //(value, key, list)
}
}
return obj; //返回obj方便链式调用
};

_.keys

1
2
3
4
5
6
7
8
9
10
11
12
_.keys = function(obj) {
//不是对象/函数,返回空数组
if (!_.isObject(obj)) return [];
//使用ES5中的方法,返回属性(数组)
if (nativeKeys) return nativeKeys(obj);
var keys = [];
for (var key in obj)
if (_.has(obj, key)) keys.push(key);
//兼容IE< 9 暂时省略
// if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};

_.has

1
2
3
_.has = function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
};

3. 将方法放入原型中

_.prototype的打印结果是:

console.logo(_.prototype);不在其原型中(其实我们定义_.** 也并没有放到原型中)如果不放到原型中,第5部分不能成功调用。
需要使用的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});
// IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236).
var nodelist = root.document && root.document.childNodes;
if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};
}

_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

混入,并且执行混入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 _.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped]; //_ 保存的数据obj
push.apply(args, arguments);
//原型方法中的数据和args合并到一个数组中
return chainResult(this, func.apply(_, args));
//见第`4`部分
//将_.prototype[name]的this指向 _ 【func.apply(_,args)已经
//将func的this指向了 _ ,并且传了参数】,返回带链式的obj,即是 _
};
});
return _;
};
_.mixin(_);

将_.*形式的方法放入到原型中
对于_(obj).*的方法,已经将数据([this._wrapped])传入到函数中(var func = _[name] = obj[name])。当然包括原型中的方法。

_mixin是支持用户自己扩展方法的。如:

1
2
3
4
5
6
7
_.mixin({
capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
}
});
_("fabio").capitalize();
=> "Fabio"

4.链式

1
2
3
4
5
_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};

使用:

1
2
3
4
_.chain(arr)
.each(function(ele) {
console.log(ele);
})//可以继续链式

1
2
3
var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};

5. 支持形如_(obj).each方式逻辑

使用形式如下:

1
2
3
 _([1, 2, 3]).each(function(n) {
console.log(n * 2);
});

我们看到第2.2部分初始化的代码

1
2
3
4
5
6
var _ = function(obj) {
console.log(this);
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj; //存放数据
};

执行的逻辑会是

  • 1.if (!(this instanceof _)) return new _(obj);再次调用构造函数,这个时候的this从window已经指向了_;
  • 2.this._wrapped = obj; //存放数据

初始化函数中console.log(this)的结果是:

6.避免冲突

1
2
3
4
5
var previousUnderscore = root._;
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};

使用:

1
2
3
4
var $$ = _.noConflict();//previousUnderscore
$$.each([1, 2, 3], function(ele, idx) {
console.info(idx + " : " + ele);
});

全部代码贴在这里,可以在我的github查看具体的所有代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
* DIY 一个underscore库1
*/
(function() {
//在浏览器上是window(self),服务器上是global
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this;

var previousUnderscore = root._;

var _ = function(obj) {
console.log(this);
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj; //存放数据
};

root._ = _;


var ArrayProto = Array.prototype,
ObjProto = Object.prototype;
var push = ArrayProto.push,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;

var nativeIsArray = Array.isArray,
nativeKeys = Object.keys;
//判断是不是对象/函数
_.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
var optimizeCb = function(func, context, argCount) {
//void 0 === undefined 返回ture
if (context === void 0) return func;
return function() {
return func.apply(context, arguments);
};
};

_.VERSION = '0.0.1';

var property = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length');
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};


_.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj); //(element, index, list)
}
} else {
var keys = _.keys(obj);
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj); //(value, key, list)
}
}
return obj; //返回obj方便链式调用
};
_.keys = function(obj) {
//不是对象/函数,返回空数组
if (!_.isObject(obj)) return [];
//使用ES5中的方法,返回属性(数组)
if (nativeKeys) return nativeKeys(obj);
var keys = [];
for (var key in obj)
if (_.has(obj, key)) keys.push(key);
//兼容IE< 9
};
_.has = function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
};
/*链式*/
_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
/*
_.chain(arr)
.each(function(ele) {
console.log(ele);
})
*/
var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};
/**
* 方法放入原型中
*/
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});
// IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236).
var nodelist = root.document && root.document.childNodes;
if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};
}

_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped]; //_ 保存的数据obj
push.apply(args, arguments);
//原型方法中的数据和args合并到一个数组中
return chainResult(this, func.apply(_, args));
//将_.prototype[name]的this指向 _ (func.apply(_,args)已经
//将func的this指向了 _ ,并且传了参数),返回带链式的obj,即是 _

};
});
return _;
};
_.mixin(_);

/**
* 避免冲突
*/
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
}());

参考阅读:

  • http://underscorejs.org/
  • underscorejs中文:http://www.bootcss.com/p/underscore/
  • UnderscoreJS精巧而强大工具包
  • JS高手进阶之路:underscore源码经典

PHP爬虫最全总结2-phpQuery,PHPcrawer,snoopy框架中文介绍

发表于 2016-10-13 | 更新于 2018-11-30 | 分类于 PHP

第一篇文章介绍了使用原生的PHP和PHP的扩展库实现了爬虫技术。本文尝试使用PHP爬虫框架来写,首先对三种爬虫技术phpQuery,PHPcrawer, snoopy进行对比,然后分析模拟浏览器行为的方式,重点介绍下snoopy

所有代码挂在我的github上

1.几种常用的PHP爬虫框架对比

1.1 phpQuery

优势:类似jquery的强大搜索DOM的能力。
pq()是一个功能强大的搜索DOM的方法,跟jQuery的$()如出一辙,jQuery的选择器基本上都能使用在phpQuery上,只要把“.”变成“->”,Demo如下(对应我的github的Demo5)

1
2
3
4
5
6
7
8
9
10
11
<?php 
require('phpQuery/phpQuery.php');
phpQuery::newDocumentFile('http://www.baidu.com/');
$menu_a = pq("a");
foreach($menu_a as $a){
echo pq($a)->html()."<br>";
}
foreach($menu_a as $a){
echo pq($a)->attr("href")."<br>";
}
?>

1.2 PHPcrawer

优势:过滤能力比较强。
官方给的Demo如下(我的github中对应demo4):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
include("PHPCrawl/libs/PHPCrawler.class.php");
class MyCrawler extends PHPCrawler
{
function handleDocumentInfo(PHPCrawlerDocumentInfo $PageInfo)
{ // As example we just print out the URL of the document
echo $PageInfo->url."<br>";
}
}
$crawler = new MyCrawler();
$crawler->setURL("www.baidu.com");
$crawler->addURLFilterRule("#\.(jpg|gif)$# i");
//过滤到含有这些图片格式的URL
$crawler->go();
?>

1.3 snoopy

优势:提交表单,设置代理等
Snoopy是一个php类,用来模拟浏览器的功能,可以获取网页内容,发送表单,
demo如下(对应github中的demo3):

1
2
3
4
5
6
7
8
9
include 'Snoopy/Snoopy.class.php';
$snoopy = new Snoopy();
$url = "http://www.baidu.com";
// $snoopy->fetch($url);
// $snoopy->fetchtext($url);//去除HTML标签和其他的无关数据
$snoopy->fetchform($url);//只获取表单
//只返回网页中链接 默认情况下,相对链接将自动补全,转换成完整的URL。
// $snoopy->fetchlinks($url);
var_dump($snoopy->results);

1.4 phpspider

优势:安装配置到数据库
提供了安装配置,能够直接连接mysql数据库,使用也是比较广泛,这里我们暂时不单独介绍。

2.模拟用户行为

2.1 file_get_contents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$opts = array(
'http'=>array(
'method'=>"GET",
'header'=>"Accept-language: en\r\n" .
"Cookie: foo=bar\r\n"
)
);

$context = stream_context_create($opts);

/* Sends an http request to www.example.com
with additional headers shown above */
$fp = fopen('http://www.example.com', 'r', false, $context);
fpassthru($fp);
fclose($fp);
?>

2.2 curl

1
2
3
4
5
6
7
8
9
10
11
12
13
$ch=curl_init();  //初始化一个cURL会话
curl_setopt($ch,CURLOPT_URL,$url);//设置需要获取的 URL 地址
// 设置浏览器的特定header
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Host: www.baidu.com",
"Connection: keep-alive",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Upgrade-Insecure-Requests: 1",
"DNT:1",
"Accept-Language: zh-CN,zh;q=0.8,en-GB;q=0.6,en;q=0.4,en-US;q=0.2",
"Cookie:_za=4540d427-eee1-435a-a533-66ecd8676d7d;"
));
$result=curl_exec($ch);//执行一个cURL会话

2.3 snoopy

  • 表单提交

我们的一个例子
form-demo.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form-demo</title>
</head>
<body>
<form action="./form-demo.php" method="post">
用户名:<input type="text" name="userName"><br>
密 码:<input type="password" name="password"><br>
<input type="submit">
</form>
</body>
</html>

form-demo.php

1
2
3
4
5
6
7
8
9
<?php 
$userName = $_POST['userName'];
$password = $_POST['password'];
if($userName==="admin"&&$password==="admin"){
echo "hello admin";
}else{
echo "login error";
}
?>

提交表单

1
2
3
4
5
6
7
8
9
10
<?php
include 'Snoopy/Snoopy.class.php';
$snoopy = new Snoopy();
$formvars["userName"] = "admin";
//userName 与服务器端/表单的name属性一致
$formvars["password"] = "admin";
$action = "http://localhost:8000/spider/demo3/form-demo.php";//表单提交地址
$snoopy->submit($action,$formvars);
echo $snoopy->results;
?>

问题1:openssl extension required for HTTPS 增加对https的支持

1
php.in ==> ;extension=php_openssl.dll 去除注释

问题2:405 Not Allowed增加

1
2
3
4
$snoopy->agent = "(compatible; MSIE 4.01; MSN 2.5; AOL 4.0; Windows 98)"; //伪装浏览器
$snoopy->referer = "http://www.icultivator.com"; //伪装来源页地址 http_referer
$snoopy->rawheaders["Pragma"] = "no-cache"; //cache 的http头信息
$snoopy->rawheaders["X_FORWARDED_FOR"] = "122.0.74.166"; //伪装ip

问题3 : snoopy使用代理

1
2
3
4
5
6
$snoopy->proxy_host = "http://www.icultivator.com";
// HTTPS connections over proxy are currently not supported
$snoopy->proxy_port = "8080"; //使用代理
$snoopy->maxredirs = 2; //重定向次数
$snoopy->expandlinks = true; //是否补全链接 在采集的时候经常用到
$snoopy->maxframes = 5; //允许的最大框架数

问题:
其实尝试了网站进行提交表单是有问题的。这样简单的处理是不能提交表单的,使用代理也是有问题
的。snoopy框架确实会有很多问题,后面有解决思路了再说。

参考阅读:

  • cURL、file_get_contents、snoopy.class.php 优缺点
  • 开源中国-PHP爬虫框架列表
  • phpQuery
  • Snoopy下载地址
  • Snoopy —— 强大的PHP采集类使用详解及示例:采集、模拟登录及伪装浏览器
  • 开源中国-snoopy博客列表

前端自动化测试工具--使用karma进行javascript单元测试

发表于 2016-10-11 | 更新于 2018-11-30 | 分类于 前端技术

前面我写了一篇博客是《前端自动化测试工具PhantomJS+CasperJS结合使用教程》其中使用CasperJS不仅可以进行单元测试,还可以进行浏览器测试,是个很不错的工具,今天介绍的工具是Karma+Jasmine+PhantomJS组合的前端javascript单元测试工具。

1.介绍

Karma是由Google团队开发的一套前端测试运行框架,karma会启动一个web服务器,将js源代码和测试脚本放到PhantomJS或者Chrome上执行。

2.安装

  • 包管理package.json
1
npm init

一路回车下去即可

  • 在项目中安装karma包
1
npm i karma --save-dev
  • karma初始化
1
karma init

按照下面的选择好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
E:\javascript\auto-test\karma-demo>karma init

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> PhantomJS
>

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> src/**/*.js
> test/**/*.js
14 10 2016 10:49:43.958:WARN [init]: There is no file matching this pattern.

>

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
>

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes


Config file generated at "E:\javascript\auto-test\karma-demo\karma.conf.js".

上图是选项的示例,这里使用jasmine测试框架,PhantomJS作为代码运行的环境(也可以选择其他浏览器作为运行环境,比如Chrome,IE等)。最后在项目中生成karma.conf.js文件

  • 安装jasmine-core
1
npm i jasmine-core --save-dev

3.demo1–ES5

目录结构

1
2
3
4
5
karma-example
├── src
├── index.js
├── test
├── package.json

源码:src–index.js

1
2
3
4
5
6
7
function isNum(num) {
if (typeof num === 'number') {
return true;
} else {
return false;
}
}

测试:test–index.js

1
2
3
4
5
6
describe('index.js: ', function() {
it('isNum() should work fine.', function() {
expect(isNum(1)).toBe(true)
expect(isNum('1')).toBe(false)
})
})

运行,执行命令

1
karma start

命令行结果

1
2
3
4
5
6
14 10 2016 10:56:13.038:WARN [karma]: No captured browser, open http://localhost:9876/
14 10 2016 10:56:13.067:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
14 10 2016 10:56:13.101:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
14 10 2016 10:56:13.119:INFO [launcher]: Starting browser PhantomJS
14 10 2016 10:56:16.207:INFO [PhantomJS 2.1.1 (Windows 8 0.0.0)]: Connected on socket /#JoOdYxAeCS4xvhHHAAAA with id 87859111
PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 SUCCESS (0.009 secs / 0.004 secs)

4.demo2-ES6

安装使用Webpack+Babel

1
2
npm i  karma-webpack --save-dev
npm i babel-loader babel-core babel-preset-es2015 --save-dev

源码src–index2.js

1
2
3
4
5
6
7
8
9
10
function isNum(num) {
if (typeof num === 'number') {
return true;
} else {
return false;
}
}

export {isNum};
// export default isNum;

测试test–index2.js

1
2
3
4
5
6
7
8
9
import {isNum} from '../src/index2';
// import isNum from '../src/index2';

describe('index2.js:', () => {
it('isNum() should work fine.', () => {
expect(isNum(1)).toBe(true);
expect(isNum('1')).toBe(false);
});
});

修改配置文件karma.conf.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
config.set({
basePath: '',
frameworks: ['jasmine'],
//修改
files: [
// 'src/**/*.js',
'test/**/*.js'
],
exclude: [],
preprocessors: {
'test/**/*.js': ['webpack', 'coverage'] //新增
//coverage为覆盖率测试,这里不再介绍
},
reporters: ['progress', 'coverage'],
// 新增--覆盖率测试
coverageReporter: {
type: 'html',
dir: 'coverage/'
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['PhantomJS'],
singleRun: false,
concurrency: Infinity,
//新增
webpack: {
module: {
loaders: [{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/,
query: {
presets: ['es2015']
}
}]
}
}
})

参考阅读:

  • http://karma-runner.github.io/
  • https://github.com/karma-runner/karma
  • 前端单元测试之Karma环境搭建

前端自动化测试工具PhantomJS+CasperJS结合使用教程

发表于 2016-10-11 | 更新于 2018-11-30 | 分类于 前端技术

下面的安装测试基于window系统(win10)

1.PhantomJS

PhantomJS 是一个基于 WebKit 的服务器端JavaScript API,它全面支持web而不需浏览器支持,其快速,原生支持各种Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG。 PhantomJS 可以用于 页面自动化, 网络监测 , 网页截屏 ,以及 无界面测试 等

1.1 安装

下载地址为:http://phantomjs.org/download.html 解压之后,可以加到环境变量中

1.2 使用

  • 示例demo1.js–截图:
1
2
3
4
5
6
7
8
9
10
11
var page = require('webpage').create();
page.viewportSize = {
width: 1366,
height: 800
};
var urls = ["https://www.baidu.com/", "https://zrysmt.github.io/"];
page.open(urls[0], function() {
console.log('welcome!');
page.render('screen.png');
phantom.exit();
});

命令行输入:

1
phantomjs demo1.js

  • 示例demo2.js–DOM操作
1
2
3
4
5
6
7
8
9
10
11
var page = require('webpage').create();
phantom.outputEncoding = "gbk"; //解决中文乱码
page.open("https://www.baidu.com/", function(status) {
console.log(status);
page.render('screen.png');
var title = page.evaluate(function() {
return document.title;
});
console.log('Page title: ' + title);
phantom.exit();
});

执行命令同上,得到结果是:

1
2
success
Page title: 百度一下,你就知道

所执行的DOM操作要在page.evaluate中,

  • 示例demo3.js–读取文件
1
2
3
4
5
6
7
var system = require('system');
var fs = require('fs');

phantom.outputEncoding = "gbk"; //解决中文乱码
var filePath = "url-02.txt";
var content = fs.read(filePath);
console.log(content);

url-02.txt中如果是很多url,一个一个访问url的话,可能会这样实现

1
2
3
4
5
6
7
page.open(urlArr[0], function(status) {
page.open(urlArr[1], function(status) {
page.open(urlArr[2], function(status) {
//... ...
});
});
});

这样写法就有很大的不方便,于是我们就引入了CasperJS

2.CasperJS

2.1 安装

1
npm i casperjs --save-dev

为方便使用也可以加入到环境变量中

2.2 使用

  • 示例demo:casper-test.js–打开网页截图
1
2
3
4
5
6
var casper = require('casper').create();
casper.start();
casper.thenOpen('http://www.baidu.com/', function () {
casper.captureSelector('baidu.png', 'html');
});
casper.run();

执行命令如下

1
casperjs casper-test.js

  • 示例demo:casper-test2.js–操作DOM,访问网页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var casper = require('casper').create();
var links;

function getLinks() {
// Scrape the links from top-right nav of the website
var links = document.querySelectorAll('ul.navigation li a');
return Array.prototype.map.call(links, function (e) {
return e.getAttribute('href')
});
}
// Opens casperjs homepage
casper.start('http://casperjs.org/');

casper.then(function () {
links = this.evaluate(getLinks);
});

casper.run(function () {
for(var i in links) {
console.log(links[i]);
}
casper.done();
});

执行命令类比同上

  • 示例demo:casper-test3.js–单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Cow() {
this.mowed = false;
this.moo = function moo() {
this.mowed = true; // mootable state: don't do that at home
return 'moo!';
};
}

casper.test.begin('Cow can moo', 2, function suite(test) {
var cow = new Cow();
test.assertEquals(cow.moo(), 'moo!');
test.assert(cow.mowed);
test.done();
});

执行命令

1
casperjs test casper-test3.js

结果是:

1
2
3
4
5
6
Test file: casper-test3.js
# Cow can moo
PASS Subject equals the expected value
PASS Subject is strictly true
PASS Cow can moo (2 tests)
PASS 2 tests executed in 0.031s, 2 passed, 0 failed, 0 dubious, 0 skipped.

  • 示例demo:casper-test4.js–浏览器测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
casper.test.begin('Google search retrieves 10 or more results', 5, function suite(test) {
casper.start("http://www.google.fr/", function() {
test.assertTitle("Google", "google homepage title is the one expected");
test.assertExists('form[action="/search"]', "main form is found");
this.fill('form[action="/search"]', {
q: "casperjs"
}, true);
});

casper.then(function() {
test.assertTitle("casperjs - Recherche Google", "google title is ok");
test.assertUrlMatch(/q=casperjs/, "search term has been submitted");
test.assertEval(function() {
return __utils__.findAll("h3.r").length >= 10;
}, "google search for \"casperjs\" retrieves 10 or more results");
});

casper.run(function() {
test.done();
});
});

执行命令如demo3类比

3.PhantomJs+CasperJs

实现异步操作

1
2
3
4
5
6
7
8
var casper = require('casper').create(); //新建一个页面

casper.start(url1); //添加第一个URL
casper.thenOpen(url2); //添加第二个URL,依次类推
casper.thenOpen(url3);
casper.thenOpen(url4);

casper.run(); //开始导航

demo(casper-phantomjs.js)如下–一次访问三十几个url:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fs = require('fs');
var casper = require('casper').create();
phantom.outputEncoding = "gbk"; //解决中文乱码

var filePath = "url-02.txt";
var content = fs.read(filePath);
var urlArr = content.split('\n');
casper.start();
for (var i = 0; i < urlArr.length; i++) {
casper.thenOpen(urlArr[i], function() {
this.echo('Page title: ' + this.getTitle());
});
}
casper.run();
// phantom.exit();

执行命令

1
casperjs casper-phantomjs.js

参考阅读:

  • http://phantomjs.org/
  • PhantomJS快速入门教程
  • http://casperjs.org/
  • 浏览器自动化测试初探 - 使用phantomjs与casperjs
  • CasperJS,基于PhantomJS的工具包

PHP爬虫最全总结1

发表于 2016-10-10 | 更新于 2018-11-30 | 分类于 PHP

 爬虫是我一直以来跃跃欲试的技术,现在的爬虫框架很多,比较流行的是基于python,nodejs,java,C#,PHP的的框架,其中又以基于python的爬虫流行最为广泛,还有的已经是一套傻瓜式的软件操作,如八爪鱼,火车头等软件。

 今天我们首先尝试的是使用PHP实现一个爬虫程序,首先在不使用爬虫框架的基础上实践也是为了理解爬虫的原理,然后再利用PHP的lib,框架和扩展进行实践。

所有代码挂在我的github上。

1.PHP简单的爬虫–原型

爬虫的原理:

  • 给定原始的url;
  • 分析链接,根据设置的正则表达式获取链接中的内容;
  • 有的会更新原始的url再进行分析链接,获取特定内容,周而复始。
  • 将获取的内容保存在数据库中(mysql)或者本地文件中

下面是网上一个例子,我们列下来然后分析
从main函数开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?php
/**
* 爬虫程序 -- 原型
*
* 从给定的url获取html内容
*
* @param string $url
* @return string
*/
function _getUrlContent($url) {
$handle = fopen($url, "r");
if ($handle) {
$content = stream_get_contents($handle, -1);
//读取资源流到一个字符串,第二个参数需要读取的最大的字节数。默认是-1(读取全部的缓冲数据)
// $content = file_get_contents($url, 1024 * 1024);
return $content;
} else {
return false;
}
}
/**
* 从html内容中筛选链接
*
* @param string $web_content
* @return array
*/
function _filterUrl($web_content) {
$reg_tag_a = '/<[a|A].*?href=[\'\"]{0,1}([^>\'\"\ ]*).*?>/';
$result = preg_match_all($reg_tag_a, $web_content, $match_result);
if ($result) {
return $match_result[1];
}
}
/**
* 修正相对路径
*
* @param string $base_url
* @param array $url_list
* @return array
*/
function _reviseUrl($base_url, $url_list) {
$url_info = parse_url($base_url);//解析url
$base_url = $url_info["scheme"] . '://';
if ($url_info["user"] && $url_info["pass"]) {
$base_url .= $url_info["user"] . ":" . $url_info["pass"] . "@";
}
$base_url .= $url_info["host"];
if ($url_info["port"]) {
$base_url .= ":" . $url_info["port"];
}
$base_url .= $url_info["path"];
print_r($base_url);
if (is_array($url_list)) {
foreach ($url_list as $url_item) {
if (preg_match('/^http/', $url_item)) {
// 已经是完整的url
$result[] = $url_item;
} else {
// 不完整的url
$real_url = $base_url . '/' . $url_item;
$result[] = $real_url;
}
}
return $result;
} else {
return;
}
}
/**
* 爬虫
*
* @param string $url
* @return array
*/
function crawler($url) {
$content = _getUrlContent($url);
if ($content) {
$url_list = _reviseUrl($url, _filterUrl($content));
if ($url_list) {
return $url_list;
} else {
return ;
}
} else {
return ;
}
}
/**
* 测试用主程序
*/
function main() {
$file_path = "url-01.txt";
$current_url = "http://www.baidu.com/"; //初始url
if(file_exists($file_path)){
unlink($file_path);
}
$fp_puts = fopen($file_path, "ab"); //记录url列表
$fp_gets = fopen($file_path, "r"); //保存url列表
do {
$result_url_arr = crawler($current_url);
if ($result_url_arr) {
foreach ($result_url_arr as $url) {
fputs($fp_puts, $url . "\r\n");
}
}
} while ($current_url = fgets($fp_gets, 1024)); //不断获得url
}
main();
?>

2.使用crul lib

Curl是比较成熟的一个lib,异常处理、http header、POST之类都做得很好,重要的是PHP下操作MySQL进行入库操作比较省心。关于curl的说明具体可以查看PHP官方文档说明http://php.net/manual/zh/book.curl.php
不过在多线程Curl(Curl_multi)方面比较麻烦。

开启crul
针对winow系统:

  • php.in中修改(注释;去掉即可)

    extension=php_curl.dll

  • php文件夹下的libeay32.dll, ssleay32.dll, libssh2.dll 还有 php/ext下的php_curl4个文件移入windows/system32

使用crul爬虫的步骤:

  • 使用cURL函数的基本思想是先使用curl_init()初始化一个cURL会话;
  • 接着你可以通过curl_setopt()设置你需要的全部选项;
  • 然后使用curl_exec()来执行会话;
  • 当执行完会话后使用curl_close()关闭会话。

例子

1
2
3
4
5
6
7
8
9
10
11
<?php
$ch = curl_init("http://www.example.com/");
$fp = fopen("example_homepage.txt", "w");

curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);

curl_exec($ch);
curl_close($ch);
fclose($fp);
?>

一个完整点的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php
/**
* 将demo1-01换成curl爬虫
* 爬虫程序 -- 原型
* 从给定的url获取html内容
* @param string $url
* @return string
*/
function _getUrlContent($url) {
$ch=curl_init(); //初始化一个cURL会话
/*curl_setopt 设置一个cURL传输选项*/
//设置需要获取的 URL 地址
curl_setopt($ch,CURLOPT_URL,$url);
//TRUE 将curl_exec()获取的信息以字符串返回,而不是直接输出
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//启用时会将头文件的信息作为数据流输出
curl_setopt($ch,CURLOPT_HEADER,1);
// 设置浏览器的特定header
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Host: www.baidu.com",
"Connection: keep-alive",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Upgrade-Insecure-Requests: 1",
"DNT:1",
"Accept-Language: zh-CN,zh;q=0.8,en-GB;q=0.6,en;q=0.4,en-US;q=0.2",
/*'Cookie:_za=4540d427-eee1-435a-a533-66ecd8676d7d; */
));
$result=curl_exec($ch);//执行一个cURL会话
$code=curl_getinfo($ch,CURLINFO_HTTP_CODE);// 最后一个收到的HTTP代码
if($code!='404' && $result){
return $result;
}
curl_close($ch);//关闭cURL
}
/**
* 从html内容中筛选链接
* @param string $web_content
* @return array
*/
function _filterUrl($web_content) {
$reg_tag_a = '/<[a|A].*?href=[\'\"]{0,1}([^>\'\"\ ]*).*?>/';
$result = preg_match_all($reg_tag_a, $web_content, $match_result);
if ($result) {
return $match_result[1];
}
}
/**
* 修正相对路径
* @param string $base_url
* @param array $url_list
* @return array
*/
function _reviseUrl($base_url, $url_list) {
$url_info = parse_url($base_url);//解析url
$base_url = $url_info["scheme"] . '://';
if ($url_info["user"] && $url_info["pass"]) {
$base_url .= $url_info["user"] . ":" . $url_info["pass"] . "@";
}
$base_url .= $url_info["host"];
if ($url_info["port"]) {
$base_url .= ":" . $url_info["port"];
}
$base_url .= $url_info["path"];
print_r($base_url);
if (is_array($url_list)) {
foreach ($url_list as $url_item) {
if (preg_match('/^http/', $url_item)) {
// 已经是完整的url
$result[] = $url_item;
} else {
// 不完整的url
$real_url = $base_url . '/' . $url_item;
$result[] = $real_url;
}
}
return $result;
} else {
return;
}
}
/**
* 爬虫
* @param string $url
* @return array
*/
function crawler($url) {
$content = _getUrlContent($url);
if ($content) {
$url_list = _reviseUrl($url, _filterUrl($content));
if ($url_list) {
return $url_list;
} else {
return ;
}
} else {
return ;
}
}
/**
* 测试用主程序
*/
function main() {
$file_path = "./url-03.txt";
if(file_exists($file_path)){
unlink($file_path);
}
$current_url = "http://www.baidu.com"; //初始url
//记录url列表  ab- 追加打开一个二进制文件,并在文件末尾写数据
$fp_puts = fopen($file_path, "ab");
//保存url列表 r-只读方式打开,将文件指针指向文件头
$fp_gets = fopen($file_path, "r");
do {
$result_url_arr = crawler($current_url);
echo "<p>$current_url</p>";
if ($result_url_arr) {
foreach ($result_url_arr as $url) {
fputs($fp_puts, $url . "\r\n");
}
}
} while ($current_url = fgets($fp_gets, 1024)); //不断获得url
}
main();
?>

要对https支持,需要在_getUrlContent函数中加入下面的设置:

1
2
3
4
5
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC ) ; 
curl_setopt($ch, CURLOPT_USERPWD, "username:password");
curl_setopt($ch, CURLOPT_SSLVERSION,3);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

结果疑惑:
我们通过1和2部分得到的结果差异很大,第1部分能得到四千多条url数据,而第2部分却一直是45条数据。

还有我们获得url数据可能会有重复的,这部分处理在我的github上,对应demo2-01.php,或者demo2-02.php

3.file_get_contents/stream_get_contents与curl对比

3.1 file_get_contents/stream_get_contents对比

  • stream_get_contents — 读取资源流到一个字符串
    与 [file_get_contents()]一样,但是 stream_get_contents() 是对一个已经打开的资源流进行操作,并将其内容写入一个字符串返回
1
2
$handle = fopen($url, "r");
$content = stream_get_contents($handle, -1);//读取资源流到一个字符串,第二个参数需要读取的最大的字节数。默认是-1(读取全部的缓冲数据)
  • file_get_contents — 将整个文件读入一个字符串
1
$content = file_get_contents($url, 1024 * 1024);

【注】 如果要打开有特殊字符的 URL (比如说有空格),就需要使用进行 URL 编码。

3.2 file_get_contents/stream_get_contents与curl对比

php中file_get_contents与curl性能比较分析一文中有详细的对比分析,主要的对比现在列下来:

  • fopen /file_get_contents 每次请求都会重新做DNS查询,并不对 DNS信息进行缓存。但是CURL会自动对DNS信息进行缓存。对同一域名下的网页或者图片的请求只需要一次DNS查询。这大大减少了DNS查询的次数。所以CURL的性能比fopen /file_get_contents 好很多。

  • fopen /file_get_contents 在请求HTTP时,使用的是http_fopen_wrapper,不会keeplive。而curl却可以。这样在多次请求多个链接时,curl效率会好一些。

  • fopen / file_get_contents 函数会受到php.ini文件中allow_url_open选项配置的影响。如果该配置关闭了,则该函数也就失效了。而curl不受该配置的影响。

  • curl 可以模拟多种请求,例如:POST数据,表单提交等,用户可以按照自己的需求来定制请求。而fopen / file_get_contents只能使用get方式获取数据。

4.使用框架

使用框架这一块打算以后单独研究,并拿出来单写一篇博客

所有代码挂在我的github上。

参考阅读:

  • 我用爬虫一天时间“偷了”知乎一百万用户,只为证明PHP是世界上最好的语言
  • 知乎 – PHP, Python, Node.js 哪个比较适合写爬虫?
  • 最近关于对网络爬虫技术总结
  • PHP实现简单爬虫 (http://www.oschina.net/code/snippet_258733_12343)]
  • 一个PHP实现的轻量级简单爬虫
  • php中file_get_contents与curl性能比较分析
  • PHP curl之爬虫初步
  • 开源中国-PHP爬虫框架列表
  • 网页抓取:PHP实现网页爬虫方式小结,抓取爬虫
  • PHP爬虫框架–PHPCrawl
  • php安装pcntl扩展实现多进程

javascript数据结构6-字典 散列 集合

发表于 2016-10-09 | 更新于 2018-11-30 | 分类于 数据结构

6.1 字典

字典是一种以键- 值对形式存储数据的数据结构,就像电话号码簿里的名字和电话号码一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>字典sample</title>
</head>
<body>
<script>
function Dictionary(){
this.add = add;
this.datastore = new Array();
this.find = find;
this.remove = remove;
this.showAll = showAll;
this.count = count;
this.clear = clear;
}

function add(key, value) {
this.datastore[key] = value;
}

function find(key) {
return this.datastore[key];
}

function remove(key) {
delete this.datastore[key];
}

function showAll() {
if(this.datastore!=null){
var datakeys=Array.prototype.slice.call(Object.keys(this.datastore));
for (var key in datakeys) {
document.write(datakeys[key] + " -> " + this.datastore[datakeys[key]]+" ");
// console.log(Object.keys(this.datastore));
console.log(key);
}
}else{
document.write("字典为空");
}
}

function count() {
var n = 0;
for (var key in Object.keys(this.datastore)) {
++n;
}
return n;
}

function clear() {
// for (var key in Object.keys(this.datastore)) {
// delete this.datastore[key];
// }
delete this.datastore;
}

//测试
var dic=new Dictionary();
dic.add("123","R");
dic.add("456","Python");
dic.add("789","JavaScipt");
document.write("</br>**************字典数目**************</br>");
var n=dic.count();
document.write(n);
document.write("</br>**************全部显示**************</br>");
dic.showAll();
document.write("</br>**************删除123--->R*************</br>");
dic.remove("123");
dic.showAll();
document.write("</br>**************清除**************</br>");
dic.clear();
dic.showAll();
</script>
</body>
</html>

6.2 散列(HashTable)

它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度
使用:MD5 和 SHA-1 可以说是目前应用最广泛的Hash算法
java中已经实现
这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HashTable散列表</title>
</head>
<body>
<script>
function HashTable() {
this.table = new Array(137); //为了避免碰撞,首先要确保散列表中用来存储数据的数组其大小是个质数。这一点很关
键,这和计算散列值时使用的取余运算有关。
this.simpleHash = simpleHash; //简单的散列表
this.betterHash = betterHash; //更好的HashTable,避免碰撞
this.showDistro = showDistro;
this.put = put;
//this.get = get;
}
function put(data) {
var pos = this.simpleHash(data);
this.table[pos] = data;
}

function simpleHash(data) {
var total = 0;
for (var i = 0; i < data.length; ++i) {
total += data.charCodeAt(i);
}
document.write("Hash value: " + data + " -> " + total+"<br/>");
return total % this.table.length;
}
function showDistro() {
var n = 0;
for (var i = 0; i < this.table.length; ++i) {
if (this.table[i] != undefined) {
document.write(i + ": " + this.table[i]+"<br/>");
}
}
}
function betterHash(string) {
const H = 31; //较小的质数 书上37不行
var total = 0;
for (var i = 0; i < string.length; ++i) {
total += H * total + string.charCodeAt(i);
}
total = total % this.table.length;
if (total < 0) {
total += this.table.length-1;
}
return parseInt(total);
}
var someNames = ["David", "Jennifer", "Donnie", "Raymond",
"Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];
var hTable = new HashTable();

for (var i = 0; i < someNames.length; ++i) {
hTable.put(someNames[i]);
}
hTable.showDistro();
</script>
</body>
</html>

这里写图片描述
这就是碰撞,为避免碰撞,使用betterHash
修改:

1
2
3
4
5
function put(data) {
// var pos = this.simpleHash(data);
var pos = this.betterHash(data);
this.table[pos] = data;
}

javascript数据结构8-图(Graph)

发表于 2016-10-09 | 更新于 2018-11-30 | 分类于 数据结构

图(graph)
图由边的集合及顶点的集合组成
有向图:
有向图
无向图:
这里写图片描述
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Graph</title>
</head>
<body>
<script>
function Graph(v){
this.vertices=v;
this.edges=0;
this.adj=[];
for(var i=0;i<this.vertices;++i){
this.adj[i]=[];
// this.adj[i].push("");
}
this.addEdge=addEdge;
this.showGraph=showGraph;

//深度优先搜索
this.dfs=dfs;
this.marked=[];
for(var i=0;i<this.vertices;++i){
this.marked[i]=false;
}

// 广度搜索
this.bfs=bfs;

}

// 增加顶点
function addEdge(v,w){
this.adj[v].push(w);
this.adj[w].push(v);
this.edges++;
}

//遍历
function showGraph(){
for(var i=0;i<this.vertices;++i){
document.write('<br/>');
document.write(i+"-->");
for(var j=0;j<this.vertices;++j){
if(this.adj[i][j]!=undefined){
document.write(this.adj[i][j]+' ');
}
}
}
}

//深度优先搜索
function dfs(v){
//var w;
this.marked[v]=true;
if(this.adj[v]!=undefined){
document.write("<br/>访问的节点:"+v);
}
// for(var w in this.adj[v]){
//console.log(this.adj[0].length);
var w=this.adj[v].shift();
while(w!=undefined){
if(!this.marked[w]){
this.dfs(w);
}
w=this.adj[v].shift();
}

//console.log(w);
//console.log(this.adj[v]);
}

// 广度搜索
function bfs(s){
var queue=[];
this.marked[s]=true;
queue.push(s);//添加到队尾
var w; //存放邻接表
while(queue.length>0){

var v=queue.shift();//从队首删除
if(v!=undefined){
document.write("<br/>访问的节点:"+v);
}

w=this.adj[v].shift();
while(w!=undefined){
if(!this.marked[w]){
this.marked[w]=true;
queue.push(w);
}
w=this.adj[v].shift();
}
}
}
//测试
var graph=new Graph(5);
graph.addEdge(0,1);
graph.addEdge(0,2);
graph.addEdge(1,3);
graph.addEdge(2,4);
//console.log(graph);
//console.log(graph.adj);
graph.showGraph();
document.write("<br/>");
document.write("======深度度优先搜索=====");
graph.dfs(0);
document.write("<br/>");
document.write("======广度优先搜索=====");
var graph1=new Graph(5);
graph1.addEdge(0,1);
graph1.addEdge(0,2);
graph1.addEdge(1,3);
graph1.addEdge(2,4);
graph1.bfs(0);
</script>
</body>
</html>

运行结果:

0–>1 2
1–>0 3
2–>0 4
3–>1
4–>2
======深度度优先搜索=====
访问的节点:0
访问的节点:1
访问的节点:3
访问的节点:2
访问的节点:4
======广度优先搜索=====
访问的节点:0
访问的节点:1
访问的节点:2
访问的节点:3
访问的节点:4

深度搜索的含义:
深度搜索
广度搜索的含义:
广度搜索

javascript数据结构7-二叉搜索树(BST)

发表于 2016-10-09 | 更新于 2018-11-30 | 分类于 数据结构

二叉树 :

这里写图片描述

闲话少说,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BST</title>
</head>
<body>
<script>
//结点
function Node(data,left,right){
this.data=data;
this.left=left;
this.right=right;
this.floor=floor; //层数
this.show=show;
}
function floor(){
return this.floor;
}
function show(){
return this.data;
}

function BST(){
this.root=null;
this.insert=insert; //插入数据
this.inOrder=inOrder; //中序排列,详细见后面解释
this.preOrder=preOrder; //先序排序
this.postOrder=postOrder; //后续排序
this.getMax=getMax; //得到最大值
this.getMin=getMin; //得到最小值
this.find=find; //查找
this.remove=remove; //删除节点
}

function insert(data){
var n=new Node(data,null,null);
if(this.root==null){
this.root=n;
this.root.floor=1;
}else{
var current=this.root;
var parent;
var c=1;
while(true){
parent=current;
if(data<current.data){
current=current.left;
c++; //计算层数的计数器加1
if(current==null){
parent.left=n;
parent.left.floor=c;
break;
}
}else{
current=current.right;
c++; //加1
if(current==null){
parent.right=n;
//rHeight++;
// console.log("**"+rHeight+"**");
parent.right.floor=c;
break;
}
}
}
}
}
//中序遍历
function inOrder(node){
if(!(node==null)){
inOrder(node.left);
document.write(node.show()+" ");
document.write("层数"+node.floor+"<br/>");
inOrder(node.right);
}
//console.count("inOrder被执行的次数");
}
//先序遍历
function preOrder(node){
if(!(node==null)){
document.write(node.show()+" ");
preOrder(node.left);
preOrder(node.right);
}
}

//后序遍历
function postOrder(node){
if(!(node==null)){
postOrder(node.left);
postOrder(node.right);
document.write(node.show()+" ");
}
}

//查找最大值
function getMax(){
var current=this.root;
while(!(current.right==null)){
current=current.right;
}
return current.data;
}
//查找最小值
function getMin(){
var current=this.root;
while(!(current.left==null)){
current=current.left;
}
return current.data;
}
//带参数---查找最小值
function getSmallest(node){
while(!(node.left==null)){
node=node.left;
}
return node;
}
//查找
function find(data){
var current=this.root;
while(current!=null){
if(current.data==data){
document.write("<br/>找到【"+data+"】节点<br/>");
return current;
}else if(data<current.data){
current=current.left;
}else{
current=current.right;
}

}
document.write("<br/>没有找到【"+data+"】 节点<br/>");
// return current;
}

//删除节点-详细解释见后后面
function remove(data) {
root = removeNode(this.root, data);
//其实root=没有用处,只是保留了函数执行的地址
}
function removeNode(node, data) {
if (node == null) {
return null;
}
if (data == node.data) {
// 没有子节点的节点
if (node.left == null && node.right == null) {
return null;
}
// 没有左子节点的节点
if (node.left == null) {
return node.right;
}
// 没有右子节点的节点
if (node.right == null) {
return node.left;
}
// 有两个子节点的节点
var tempNode = getSmallest(node.right);
node.data = tempNode.data;
node.right = removeNode(node.right, tempNode.data);
return node;
}
else if (data < node.data) {
node.left = removeNode(node.left, data);

return node;
}
else {
node.right = removeNode(node.right, data);
// console.log(node);
return node;
}
}

//测试
var nums=new BST();
nums.insert(56);
nums.insert(22);
nums.insert(81);
nums.insert(10);
nums.insert(30);
nums.insert(77);
nums.insert(92);
nums.insert(100);
document.write("*****************中序遍历***************</br>");
inOrder(nums.root);
document.write("</br>***************先序遍历***************</br>");
preOrder(nums.root);
document.write("</br>***************后序遍历***************</br>");
postOrder(nums.root);
//nums.show();
//console.log(nums);
document.write("</br>***************最大值/最小值************</br>");
document.write(nums.getMax()+"/"+nums.getMin());
document.write("</br>***************查找************</br>");
nums.find(100);
document.write("</br>***************删除节点81后************</br>");
nums.remove(81);
console.log(nums);
preOrder(nums.root);
</script>
</body>
</html>

结果:
这里写图片描述

遍历

中序遍历:
这里写图片描述
理解双层递归

1
2
3
4
5
6
7
8
9
10
function inOrder(node) {
if (!(node == null)) {
inOrder(node.left); //@1
document.document(node.show() + " "); //@2
inOrder(node.right); //@3
}
}


inOrder(nums.root); //开始执行

从根节点开始:
这里写图片描述

这里写图片描述
这里写图片描述

删除节点:

1
2
3
function remove(data) {
root = removeNode(this.root, data);
}

简单接受待删除的数据,具体执行是removeNode函数;

1
2
function removeNode(node, data) {
}

待删除的节点是:
1.叶子结点,只需要将从父节点只想它的链接指向null;

1
2
3
if (node.left == null && node.right == null) {
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
if (node.left == null && node.right == null) {
return null; //递归,找到节点置为空即可
}
//其他情况
}
else if (data < node.data) {
node.left = removeNode(node.left, data); //#1
return node; //#2
}
else {
node.right = removeNode(node.right, data);//#3
return node; //#4
}

通过if else逻辑找到node节点


//#1  node.left=null(后面的函数递归后返回的值)
//#3  node.right=null(后面的函数递归后返回的值)

2.只包含一个子节点,原本指向它的节点指向它的子节点。
3.左右子树都有的时候。两种做法:找到左子树的最大值或者右子树的最小值。这里我们用第二种。

  • 查找右子树的最小值,创建一个临时的节点tempNode。
  • 将临时节点的值赋值给待删除节点
  • 删除临时节点

注意:


//#2 //#4必须有,如果没有,则删除节点下面的所有子树都将被删除。
真个过程举个形象的说明,遍历的时候把节点之间的链条解开进行查询,return node;递归查询到最后一级后,由下向上对链条进行缝合。


1…345…7

Ruyi Zhao

70 日志
5 分类
52 标签
© 2018 Ruyi Zhao
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.5.0