Redis支持数据的持久化,可以将内存中的数据保存的在磁盘中,重启时可以再次加载进行使用
Redis不仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的储存。
Redis支持数据的备份,即master-slave模式的数据备份
性能极高-Redis能读的速度是110000次/s,写的速度是81000次/s。
丰富的数据类型-Redis支持二进制的案例String,Lists,Hashes,Sets以及Oracle Sets等数据类型操作。
院子-Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
丰富的特性-Redis还支持publish/subscribe,通知,key过期等等特性。
Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以及追加的方式产生的,因为他们并不需要进行随机访问。
Redis的set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis的有序集合和set一样也是string类型的元素集合,且不允许重复的成员。不同的是每个元素都 会关联一个double类型的分数,redis正式通过分数来为集合中的成员进行从小到大的排序。有序集合 的成员是唯一的,但分数却可以重复。
标记事务的开始:MULTI
执行所有事务:EXEC
取消事务:DISCARD
见识一个或多个key,如果在事务执行前这个(或这些)key被其它命令所改动,那么事务将会被打断WATCH key [key……]
注意:单个redis命令的执行是原子性的,但Redis没有在事务上增加任何维持原子性的机制,所以Redis事务的执行并不是原子性的。事务可以理解为一个打包的批量执行的脚本,但批量命令并非原子性的操作,中间某条指令的失败不会导致前面已做的指令的回滚,也不会造成后续的指令不做。
RDB(redis database),可以理解为快照/内存快照,RDB持久化过程是将当前进程中的数据生成快照存储 到硬盘中。
AOF(append only file),以日志的方式记录每次写命令,服务重启的时候重新执行AOF文件中的命令 来恢复内存数据。因为解决了数据持久化实时性的问题,所以目前AOF是Redis持久化的主流方式。
RDB和AOF两种持久化方式的触发机制都分为两种:手动触发和自动触发。
手动触发:
执行save和bgsave两个命令可以手动触发RDB持久化。
save命令会阻塞当前服务器,直到RDB完成为止,如果数据量大的话会造成长时间的阻塞,线上环境一般禁止使用。
bgsave很好理解,就是background save,执行bgsave命令时Redis进程会fork一个子进程来完成RDB的过程,完成后自动结束,所以Redis主进程阻塞时间只有fork阶段的那一下。相对于save,阻塞时间很短。
自动触发:
在redis.config配置文件里可以配置自动触发:save <seconds> <changes>,这个配置的规则指的是在seconds秒内发生changes次写操作,就会自动进行一次bgsave,例如:save 900 1,指的是如果900秒内有1条Key信息发生变化就会触发一次bgsave。
还有在执行shutdown命令的时候,如果没有开启AOF持久化功能,那么会自动执行一次bgsave。
执行流程:
执行bgsave命令的时候,Redis主进程会检查是否有子进程在执行RDB/AOF持久化任务,如果有的话,直接返回。
Redis主进程会fork一个子进程来执行执行RDB操作,fork操作会对主进程造成阻塞(影响Redis的读写),fork操作完成后会发消息给主进程,从而不再阻塞主进程。
RDB子进程会根据Redis主进程的内存生成临时的快照文件,RDB完成后会使用临时快照文件替换掉原来的RDB文件。
RDB子进程完成RDB持久化后会发消息给主进程,通知RDB持久化完成。
RDB优点:
RDB文件小,非常适用于定时备份,用于灾难恢复。
Redis加载RDB文件的速度比AOF快很多,因为RDB文件中直接存储的内存数据,而AOF文件中存储的是一条条命令。
RDB缺点:
RDB无法做到实时持久化,因为fork子进程属于重量级操作,会阻塞Redis主进程。
存在老版本的Redis不兼容新版本RDB格式文件的问题。
主要因为RDB持久化不支持实时持久化,只要Redis服务宕机了,那么从上一次bgsave到宕机之间的所有数据都会丢失,所以在数据实时性要求高的情况下不适合使用RDB,所以Redis又提供了AOF持久化
其他细节性说明:
在有子进程执行RDB过程的时候,Redis主进程的读写不受影响,但是对于Redis的写操作不会同步到主进程的主内存中,而是会写到一个临时的内存区域作为一个副本,等到主进程接收到子进程完成RDB过程的消息后再将内存副本中的数据同步到主内存。
Redis默认采用LZF算法对RDB文件进行压缩,所以生成的内存文件会比内存小很多。
说明:AOF默认是关闭的,可以在redis.conf配置文件中添加下面配置开启
AOF:appendonly yes
手动触发:
执行bgrewriteaof命令直接触发AOF重写。
自动触发:
在redis.config配置文件中有两个配置项:
auto-aof-rewrite-min-size 64MB auto-aof-rewrite-min-percenrage 100
上面两个配置表示:
- 当AOF文件小于64MB的时候不进行AOF重写
- 当当前AOF文件比上次AOF重写后的文件大100%的时候进行AOF重写
可以在redis.conf配置文件中添加这两个参数来自动触发AOF重写,执行bgrewriteaof命令
执行流程:
所有的写命令都会追加到aof_buf(缓冲区)中。
可以使用不同的策略将AOF缓冲区中的命令写到AOF文件中。
随着AOF文件的越来越大,会对AOF文件进行重写。
当服务器重启的时候,会加载AOF文件并执行AOF文件中的命令用于恢复数据。
简单分析一下AOF执行流程中的一些问题:
因为Redis为了效率,使用单线程来响应命令,如果每次写命令都追加写硬盘的操作,那么Redis的响应速度还要取决于硬盘的IO效率,显然不现实,所以Redis将写命令先写到AOF缓冲区。
写道缓冲区还有一个好处是可以采用不同的策略来实现缓冲区到硬盘的同步,可以让用户自行在安全性和性能方面做出权衡。
同步策略:
在了解同步策略之前,需要先来了解两个三方法flushAppendOnlyFile、write和save:
redis的服务器进程是一个事件循环,文件事件负责处理客户端的命令请求,而时间事件负责执行serverCron函数这样的定时运行的函数。在处理文件事件执行写命令,使得命令被追加到aof_buf中,然后在处理时间事件执行serverCron函数会调用flushAppendOnlyFile函数进行文件的写入和同步
write:根据条件,将aof_buf中的缓存写入到AOF文件
save:根据条件,调用fsync或fdatasync函数将AOF文件保存到磁盘
Redis支持的三种同步策略:
AOF_FSYNC_NO:不保存(write和read命令都由主进程执行)
AOF_FSYNC_EVERYSEC:每一秒钟保存一次(write由主进程完成,save由子进程完成)
AOF_FSYNC_ALWAYS:每执行一个命令保存一次(write和read命令都由主进程执行)
AOF_FSYNC_NO:
在这种策略下,每次flushAppendOnlyFile函数被调用的时候都会执行一次write方法,但是不会执行 save方法。
只有下面三种情况下才会执行save方法:
Redis被关闭
AOF功能被关闭
系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的save操作都会引起Redis主进程阻塞,并且由于长时间没有执行save命令,所以save 命令执行的时候,阻塞时间会很长。
AOF_FSYNC_EVERYSEC:
在这种策略下,save操作原则上每隔一秒钟就会执行一次, 因为save操作是由后台子线程调用的, 所 以它不会引起服务器主进程阻塞。
其实根据Redis的状态,每当 flushAppendOnlyFile函数被调用时,write命令和save命令的执行又分 为四种不同情况:
根据以上图知道,在AOF_FSYNC_EVERYSEC策略下, 如果在情况1时发生故障停机, 那么用户最多 损失小于2秒内所产生的数据;而如果在情况2时发生故障停机,堆积了很多save命令,那么用户损 失的数据是可以超过 2 秒的。
AOF_FSYNC_ALWAYS:
在这种模式下,每次执行完一个命令后,write和save命令都会执行。
另外,因为save命令是由Redis主程序执行的,所以在save命令执行期间,主程序会被阻塞。
三种策略优缺点:
AOF_FSYNC_NO策略虽然表面上看起来提升了性能,但是会存在每次save命令执行的时候相对长时间阻塞主进程的问题。并且数据的安全性的不到保证,如果Redis服务器突然宕机,那么没有从AOF缓存中保存到硬盘中的数据都会丢失。
AOF_FSYNC_ALWAYS策略的安全性的到了最大的保障,理论上最多丢失最后一次写操作,但是由于每个写操作都会阻塞主进程,所以Redis主进程的响应速度受到了很大的影响。
AOF_FSYNC_EVERYSEC策略是比较建议的配置,也是Redis的默认配置,相对来说兼顾安全性和性能。
重写机制:
随着命令不断从AOF缓存中写入到AOF文件中,AOF文件会越来越大,为了解决这个问题,Redis引入了AOF重写机制来压缩AOF文件。
AOF文件的压缩和RDB文件的压缩原理不一样,RDB文件的压缩是使用压缩算法将二进制的RDB文件压缩,而AOF文件的压缩主要是去除AOF文件中的无效命令,比如说:
同一个key的多次写入只保留最后一个命令
已删除、已过期的key的写命令不再保留
重写流程:
执行bgrewriteaof命令的时候,如果当前有进程正在执行AOF重写,那么直接返回;如果有进程正在执行bgsave,那么等待bgsave执行完毕再执行AOF重写。
Redis主进程会fork一个子进程执行AOF重写,开销和RDB重写一样。
AOF重写过程中,不影响Redis原有的AOF过程,包括写消息到AOF缓存以及同步AOF缓存中的数据到硬盘。
AOF重写过程中,主进程收到的写操作还会将命令写到AOF重写缓冲区,注意和AOF缓冲区区分开。
由于AOF重写过程中原AOF文件还在陆续写入数据,所以AOF重写子进程只会拿到fork子进程时的AOF文件进行重写。
子进程拿到原AOF文件中的数据写道一个临时的AOF文件中。
子进程完成AOF重写后会发消息给主进程,主进程会把AOF重写缓冲区中的数据写道AOF缓冲区,并且用新的AOF文件替换旧的AOF文件。
其他细节性说明:
Redis对AOF的重要性看得比RDB重,因为RDB的时候如果有进程正在执行AOF,那么直接返回;而AOF的时候如果有进程正在执行RDB,那么等RDb结束再执行AOF。
Redis再AOF重写的时候新建一个AOF重写缓冲区的目的是为了保证重写过程中的写命令数据不会丢失。
子进程在重写AOF文件的时候,每次写硬盘的数据量由配置决定,不能太大,否则会导致硬盘阻塞(默认32MB)。
AOF重写的整个过程有三个部分会阻塞进程:
主进程fork子进程的时候
主进程把AOF重写缓冲区中的数据写到AOF缓冲区的时候
使用新的AOF文件替换掉旧的AOF文件的时候
RDB持久化基于内存快照存储二进制文件,AOF持久化基于写命令存储文本文件。
RDB文件采用了压缩算法,比较小;AOF文件随着命令的叠加会越来越大,Redis提供了AOF重写来压缩AOF文件。
恢复RDB文件的速度比AOF文件快很多。
RDB持久化方式实时性不好,所以AOF持久化更主流。
合理的使用AOF的同步策略,理论上不会丢失大量的数据。
Redis重启的时候优先加载AOF文件,如果AOF文件不存在再去加载RDB文件。
如果AOF文件和RDB文件都不存在,那么直接启动。
不论加载AOF文件还是RDB文件,只要发生错误都会打印错误信息,并且启动失败。
使用phpinfo();查看PHP的版本:
去下面的两个网站下载对应版本的压缩包并解压(注意:必须下载 nts 版本)
https://windows.php.net/downloads/pecl/releases/igbinary/
https://windows.php.net/downloads/pecl/releases/redis/
复制两个文件中的如下四个文件到php环境中的
ext文件夹中(F:\phpstudy\PHPTutorial\php\php-7.0.12-nts\ext)
打开Apache的配置文件 php.ini,复制下面的两行代码到php.ini 文件中,并重启环境
extension=php_igbinary.dll extension=php_redis.dll
重新使用 phpinfo() 函数 查看php相关信息,出现下图才是安装成功,如果失败请查看下载的对应压缩包的版本是否正确
去下面的网站下载对应的压缩包并解压:https://github.com/MicrosoftArchive/redis/releases/
直接解压,并且cmd到解压目录下,运行文件夹中的redis-server.exe,出现下图即为安装成功:
要想在PHP中使用redis这个窗口是不能关的,否则redis将无法使用。当然如果一直开着会很麻烦,所以我们设置一下开机自启,让他在系统中一直启动着。用cmd打开解压目录,运行以下代码:
redis-server --service-install redis.windows-service.conf --loglevel verbose
设置开机自动启动,打开cmd窗口并输入:services.msc,找到redis 服务点击启动即可
如果命令失败是找不到redis服务的
1067错误:
原因1:可能是因为他需要在logs目录下生成日志文件,而执行命令时权限不够没有生成,所以只需要手动创建一个logs目录即可。
原因2:肯能是因为redis的启动窗口未关闭造成的
json_encode 所有字符串数据的编码必须是 UTF-8,不然返回false
ini_set('session.cookie_lifetime',0);
把安装目录下的composer.phar移动到项目根目录下:
1、找到composer位置的命令:which composer
2、把安装目录下的composer.phar移动到项目根目录下
3、在项目下正常运行安装命令即可
javascript的sessionStorage,在微信关闭时会自动失效,默认设置。这里的关闭不是指彻底关闭微信,而是只要离开页面就会失效!
设置:
sessionStorage.setItem('inviteuid', inviteuid);
获取:
sessionStorage.getItem('inviteuid')
判断是否设置了:
sessionStorage.getItem("inviteuid") != null
if ($a) { } else switch($a) { }
if ($a) { } else do { } while (!$a);
//例如判断数组是否为空后做操作 if (count($array)) { for ($i = 0; $i < count($array); $i++) { } } else { //数组为空的逻辑 } //可以写成 if (count($array) == 0) { //数组为空的逻辑 } else for ($i = 0; $i < count($array); $i++) { }
$a = true; if ($a) { echo “true”; } else label: { echo “false”; } //转化后写法 $a = true; if ($a) { echo "true"; } else { label: ; //单独的一条语句 } echo "false"; //所以运行结果是:truefalse //label不是一个常量可以是任意字符串
unserialize(): Error at offset 0 of 13465 bytes
类似于上面这种报错大多数是因为缓存引起的,建议先彻底清掉缓存试一下,再找其他原因!
var_dump(1...9);//输出10.9
原因:
首先明确一点,var_dump()把1...9识别为了浮点数。
1...9会被依次识别为:1(浮点数1),然后是.(字符串连接符号),然后是.9(浮点数0.9)
function sum(...$numbers) { $acc = 0; foreach ($numbers as $n) { $acc += $n; } return $acc; } echo sum(1, 2, 3, 4);//输出10
function add($a, $b) { return $a + $b; } echo add(...[1, 2]);//输出3 $a = [1, 2]; echo add(...$a);//输出3
PHP 从5.6+开始在用户自定义函数中支持可变数量的参数列表
if (print("1\n") && print("2\n") && print("3\n") && print("4\n")) { ; }//输出:4 111
实际上等同于:
if (print ("1\n" && print ("2\n" && print ("3\n" && print "4\n")))) { ; }
解析:
print并不是一个函数,所以并不要求一定要有小括号(所以即使你写了小括号,括号也会在语法分析阶段被忽略),即:第一个代码块在PHP看来就是第二个代码块的样子。
所以就是:
最先输出4, 然后输出 "3\n" && print的结果1 , 然后输出 "2\n" && 1的结果1, 最后是 "1\n" && 1的结果1。
多数情况直接删除与入口文件同级的user.ini
在Linux系统时,路径中的斜杠必须用(/)而不能用反斜杠(\)否则将识别不到文件。
首先说一下self的限制,使用 self:: 或者 __CLASS__ 对当前类的静态引用,取决于定义当前方法所在的类。
<?php class A { public static function who() { echo __CLASS__; } public static function test() { self::who(); } } class B extends A { public static function who() { echo __CLASS__; } } B::test();//A
以上案例本意是想输出:B,却因为self限制而失败。
后期静态绑定本想通过引入一个新的关键字表示运行时最初调用的类来绕过限制。简单地说,这个关键字能够让你在上述例子中调用 test() 时引用的类是 B 而不是 A。最终决定不引入新的关键字,而是使用已经预留的 static 关键字。
<?php class A { public static function who() { echo __CLASS__; } public static function test() { static::who(); // 后期静态绑定从这里开始 } } class B extends A { public static function who() { echo __CLASS__; } } B::test();//B
在非静态环境下,所调用的类即为该对象实例所属的类。由于 $this-> 会在同一作用范围内尝试调用私有方法,而 static:: 则可能给出不同结果。另一个区别是 static:: 只能用于静态属性。
foo(); static::foo(); } } class B extends A { /* foo() will be copied to B, hence its scope will still be A and * the call be successful */ } class C extends A { private function foo() { /* original method is replaced; the scope of the new one is C */ } } $b = new B(); $b->test(); $c = new C(); $c->test(); //fails
输出结果:
success! success! success! Fatal error: Call to private method C::foo() from context 'A' in /tmp/test.php on line 9
后期静态绑定的解析会一直到取得一个完全解析了的静态调用为止。另一方面,如果静态调用使用 parent:: 或者 self:: 将转发调用信息。
<?php class A { public static function foo() { static::who(); } public static function who() { echo __CLASS__."\n"; } } class B extends A { public static function test() { A::foo(); parent::foo(); self::foo(); } public static function who() { echo __CLASS__."\n"; } } class C extends B { public static function who() { echo __CLASS__."\n"; } } C::test(); //结果: A C C
//快捷查询 Db::table('think_user') ->where('name|title','like','thinkphp%') ->where('create_time&update_time','>',0) ->find(); //sql: SELECT * FROM `think_user` WHERE ( `name` LIKE 'thinkphp%' OR `title` LIKE 'thinkphp%' ) AND ( `create_time` > 0 AND `update_time` > 0 ) LIMIT 1
->whereOr([//第一维代表多个条件 [//第二维之间用or链接 //第三维之间用and连接 ['create_time', '<=', time()], ['status', '=', '1'] ],[ ['id', '>=', 10], ['update_time', '>=', time()], ] ]) //拼凑出来的结果: //where (create_time <= time() and status = 1) //or (update_time >= time() and id >= 10)
适用于多对多,并且有中间表的,例如,不同用户关联不同的兴趣标签,关联数据单独存在一个表中:
建表:
article表(字段:id,……)
label表(字段:id,name,……)
article_label表(字段:id,article_id,label_id,createtime,……)
article模型中:
//方法名随意,调用时使用此方法名 public function Labels(){ //参数1:关联模型 //参数2:中间表表名,不带表前缀 //参数3:中间表 关联 关联模型的外键 //参数4:中间表 关联 当前模型的外键 return $this->belongsToMany('LabelModel', 'article_label','label_id','article_id'); }
控制器中使用:
//查询 $data=ArticleModel::with('Labels')->select(); //使用,因为查询结果为对象,所以调用方式如下: //打印第一条文章数据下第一个标签的名字 dump($data[0]->Labels[0]->name); //接口调用:直接用json_encode()处理后自动变成三维数组 echo json_encode($data); //删除 $data = ArticleModel::find(1);//查询文章信息 $data->Labels()->detach();//删除article_label表中当前文章的信息 //添加 $label_ids=[1,2,3,4,5,6,7,8,9];//需要绑定的label表主键id //参数1是需要绑定的多个label_id,参数2是其他需要补充的字段 $data->Labels()->attach($label_ids,['createtime'=> time()]);//保存
一般用于一条记录对应多条记录,例如某个文章的评论:
建表:
article表(字段:id,……)
comment表(字段:id,article_id,content,……)
article模型中:
//方法名字随意,调用时方法名作为参数使用 public function comments(){ //参数1:关联模型名 //参数2:关联模型外键 //参数3:当前模型表主键 return $this->hasMany('CommentModel','article_id','id'); }
控制器中使用:
$data=ArticleModel::with('comments')->select(); //使用,因为查询结果为对象,所以调用方式如下: //打印第一条文章数据下第一条评论数据的内容 dump($data[0]->comments[0]->content); //接口调用:直接用json_encode()处理后自动变成三维数组 echo json_encode($data);
一般用于多级分类存在同一个表中:
建表:
classify表(字段:id,name,level,pid,……)
classify模型中:
//方法名字随意,调用时方法名作为参数使用 public function secondClassify(){ //参数1:当前模型名 //参数2:上级id(原关联模型外键) //参数3:当前模型表主键 return $this->hasMany('ClassifyModel','pid','id'); }
控制器中使用:
//查询一级分类,自动补全二级 $data=ClassifyModel::with('secondClassify')->where('level',1)->select(); //使用,因为查询结果为对象,所以调用方式如下: //打印第一条一级分类数据下第一个二级分类的名称 dump($data[0]->secondClassify[0]->name); //接口调用:直接用json_encode()处理后自动变成三维数组 echo json_encode($data);
一般用于主记录查询子记录(子表有关联记录在主表),例如一个用户信息只对应一个用户:
建表:
user表(字段:id,message_id,……)
message表(字段:id,user_id,……)
user模型中:
//方法名字随意,调用时方法名作为参数使用 public function user(){ //参数1:关联模型名 //参数2:关联模型外键 //参数3:当前模型主键 return $this->hasOne('UserModel','message_id','id'); }
控制器中使用:
$data=UserModel::with('user')->select(); //使用,因为查询结果为对象,所以调用方式如下: //打印当前用户信息对应的用户生日 dump($data[0]->user->birthday); //接口调用:直接用json_encode()处理后自动变成三维数组 echo json_encode($data);
一般用于主记录查询子记录(子表有一条记录属于主表),例如一个用户只有一条用户信息:
建表:
user表(字段:id,message_id,……)
message表(字段:id,user_id,……)
classify模型中:
//方法名字随意,调用时方法名作为参数使用 public function messages(){ //参数1:关联模型名 //参数2:关联模型外键 //参数3:当前模型主键 return $this->belongsTo('MessageModel','message_id','id'); }
控制器中使用:
$data=UserModel::with('messages')->select(); //使用,因为查询结果为对象,所以调用方式如下: //打印第一条用户数据下子信息的金额 dump($data[0]->messages->money); //接口调用:直接用json_encode()处理后自动变成三维数组 echo json_encode($data);
//取单列:获取所有标签名,输出结构:[id=>name] LabelModel::column('name','id'); //取多列:获取所有标签名和创建时间,输出结构:[id=>[id,name,createtime]] LabelModel::where('status',1)->column('name,createtime','id');
cache()可以用于select、find、value和column方法,以及其衍生方法,使用cache方法后,在缓存有效期之内不会再次进行数据库查询操作,而是直接获取缓存中的数据,关于数据缓存的类型和设置可以参考缓存部分。
1. 简单的存储
//查询news表中id=10的新闻存储于cache中 //写true默认读取配置的中缓存时间 db('news')->cache(true)->find(10); //你也可以自定义时间,60秒表示: db('news')->cache(true,60)->find(10);
2. 指定缓存标识
//等同于使用缓存时的键,默认读取配置的中缓存时间: db('news')->cache('key')->find(15); //全局读取这条数据: $data = \think\Cahce::get('key');
3.支持设置缓存标签:
//缓存键:key,缓存时间:60秒,标签为:tagName db('news')->cache('key',60,'tagName')->find(15); //全局读取带标签的缓存: Db::name('news')->cache('news_list',60,'shx')->select(); $data = \think\Cache::tag('shx')->get('news_list');
封装性:
也称为信息隐藏,就是将一个类的使用和实现分开,只保留部分接口和方法与外部联系,或者说只公开了一些供开发人员使用的方法。于是开发人员只 需要关注这个类如何使用,而不用去关心其具体的实现过程,这样就能实现MVC分工合作,也能有效避免程序间相互依赖,实现代码模块间松藕合。
继承性:
就是子类自动继承其父级类中的属性和方法,并可以添加新的属性和方法或者对部分属性和方法进行重写。继承增加了代码的可重用性。PHP只支持单继承,也就是说一个子类只能有一个父类。
多态性:
子类继承了来自父级类中的属性和方法,并对其中部分方法进行重写。于是多个子类中虽然都具有同一个方法,但是这些子类实例化的对象调用这些相同的方法后却可以获得完全不同的结果,这种技术就是多态性。多态性增强了软件的灵活性。
易维护
采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
质量高
在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。
效率高
在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。
易扩展
由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
正则表达式是一种文本模式,包括普通字符(例如a到z之间的字母)和特殊符号(称为“元字符”)。
正则表达式使用单个字符串来描述、匹配一系列匹配某个语法规则的字符串。
正则表达式是繁琐的,但它是强大的,学会之后的应用会让你提高效率。
*:匹配的字符可以不出现,也可以出现一次或多次(大于等于0)
+:匹配的字符至少出现一次(大于等于1)
?:匹配的字符最多只可以出现一次(0或1)
{n}:代表匹配n次
{n,}:代表至少匹配n次,最多无限
{n,m}:代表至少匹配n次,最多匹配m次
^:匹配输入字符串的开始位置
$:匹配输入字符串的结束位置
\b:匹配一个单词边界,即单词与空格之间的位置(光标所在的位置)
\B:非单词边界匹配
跟在定界符//的后面:
g:匹配出所有符合要求的结果,例:/\d+/g 匹配字符串中所有数字组合
s:使正则表达式中的(.)可以匹配到换行符
i:忽略大小写配匹配,例:/[a-z]/gi 匹配所有字母忽略大小写
m:进行多行匹配,必须配合定位符^和$使用,例:
str="1233asfasfasf\nasdas13232\n54346waqadsafaf\n9999asdasda" str.match(/^\d+/gm)//匹配每行以数字开头的内容 结果: ["1233", "54346", "9999"]
字符 | 描述 |
(pattern) | 匹配 pattern 并获取这一匹配。这是一个获取匹配。 |
(?:pattern) | 匹配 pattern 但不获取匹配结果,这是一个非获取匹配,不进行存储供以后使用,但是消耗字符匹配。 |
(?=pattern) | 正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,不需要获取供以后使用。 |
(?!pattern) | 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,不需要获取供以后使用。 |
(?<=pattern) | 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。 |
(?<!pattern) | 反向否定预查,与正向否定预查类似,只是方向相反。 |
x|y | 匹配 x 或 y。 |
[xyz] | 字符集合。匹配所包含的任意一个字符。 |
[^xyz] | 负值字符集合。匹配未包含的任意字符。 |
[a-z] | 字符范围。匹配指定范围内的任意字符。 |
[^a-z] | 负值字符范围。匹配任何不在指定范围内的任意字符。 |
\d | 匹配一个数字字符。等价于 [0-9]。 |
\D | 匹配一个非数字字符。等价于 [^0-9]。 |
\s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。 |
\S | 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。 |
\f | 匹配一个换页符。等价于 \x0c 和 \cL。 |
\n | 匹配一个换行符。等价于 \x0a 和 \cJ。 |
\r | 匹配一个回车符。等价于 \x0d 和 \cM。 |
\t | 匹配一个制表符。等价于 \x09 和 \cI。 |
\v | 匹配一个垂直制表符。等价于 \x0b 和 \cK。 |
\w | 匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'。 |
\W | 匹配非字母、数字、下划线。等价于 '[^A-Za-z0-9_]'。 |
\cx | 匹配由 x 指明的控制字符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。 |
在使用正则表达式的时候,我们经常会使用()把某个部分括起来,称为一个子模式。
获取匹配:是指系统会在幕后将所有的子模式匹配结果保存起来,供我们查找或者替换。
非获取匹配:是指系统不会保存子模式的匹配结果,子模式的匹配更多的只是作为一种限制条件使用,如正向预查(?=)、反向预查(?<=)、负正向预查(?!)、负反向预查(?<!)
一般放在整个正则表达式的后面。
正向预查的意思是,子模式仅仅作为允许出现的条件限制,并不作为匹配结果输出。
//内容: Windows 95 and Windows 98 are the successor. Then Windows 2000 and Windows Xp appeared. Windows Vista is the Latest version of the family. //正则表达式 /Windows(?= [\d]+\b)/g //匹配结果: Windows Windows Windows //以上三个Windows分别指的是Windows 95、Windows 98、Windows 2000中的Windows。
一般放在整个正则表达式的后面。
负正向预查意思是,子模式仅仅作为不允许出现的条件限制,并不作为匹配结果输出。
//内容: Windows 95 and Windows 98 are the successor. Then Windows 2000 and Windows Xp appeared. Windows Vista is the Latest version of the family. //正则表达式 /Windows(?! [\d]+\b)/g //匹配结果: Windows Windows //以上两个Windows分别指的是Windows Xp、Windows Vista中的Windows。
一般放在整个正则表达式的前面。
反向预查与正向预查很相似,子模式仅仅作为允许出现的条件限制,不作为结果输出。
不同的是,正向预查匹配子模式前面的结果作为匹配结果,而反向预查匹配子模式后面的结果作为匹配结果。
//内容 CNY:100.2 USD:222.1 USD:301.3 HKD:122.1 CNY:114.4 //正则表达式 /(?<=CNY:)\d+\.\d/g //匹配结果: 100.2 114.4
一般放在整个正则表达式的前面。
负反向预查与反向预查很相似,子模式仅仅作为不允许出现的条件限制,不作为结果输出。
不同的是,负正向预查匹配子模式前面的结果作为匹配结果,而负反向预查匹配子模式后面的结果作为匹配结果。
//内容 CNY:100.2 USD:222.1 USD:301.3 HKD:122.1 CNY:114.4 //正则表达式 /(?<!CNY:)\b\d+\.\d/g //匹配结果: 222.1 301.3 122.1
.匹配任意除换行符“\n”外的字符;
*表示匹配前一个字符0次或无限次;
?表示前边字符的0次或1次重复
+或*后跟?表示非贪婪匹配,即尽可能少的匹配,如*?重复任意次,但尽可能少重复;
.*? 表示匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。
如:a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab和ab。不加问号的话匹配的是aabab。
有两种情况:
1. num范围在1-99,表示匹配到第几个括号的重复字符。
具体用法只有:/(.)\1/ 用来匹配两个连续的字符。其他具体用法不明确
2. num是0或者是三位数,表示8进制所对应的ASCII码字符
str="1aa2" str.match(/(.)\141/g) 结果: ["1a"]
\c匹配control+控制字符,就是说匹配由x指明的控制字符。
比如:\cj匹配control+j,代表\n。
\ca \cb \cc ... 又分别匹配 control+a, control+b,control+c....,具体他们等价于什么,看运行的程序了
百度搜索控制字符可以查看所有a-z的匹配
[0-9]+匹配多个数字,[0-9]匹配单个数字,+匹配一个或多个。
{3,15}匹配长度介于3-15字符长度,{2}匹配确定的两次,{1,}至少匹配一次
例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
()标记一个表达式的开始和结束位置。
.匹配换行符\n之外的任何单字符。
\将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义字符。
{}标记限定符表达式的开始和结束
|指明两项之间的一个选择,一般配合()使用。
[\u4e00-\u9fa5] //匹配常用中文汉字(繁体字可能匹配不到) [a-z] //匹配所有的小写字母 [A-Z] //匹配所有的大写字母 [a-zA-Z] //匹配所有的字母 [A-z] //匹配所有的字母 [0-9] //匹配所有的数字 [0-9\.\-] //匹配所有的数字,英文句号和减号 [ \f\r\t\n\v] //匹配所有的白字符(空格、换页符、回车符、水平制表符、垂直制表符) [^a-z] //除了小写字母以外的所有字符 [^\\\/\^] //除了(\)(/)(^)之外的所有字符 [^\"\'] //除了双引号(")和单引号(')之外的所有字符 ^[a-zA-Z0-9_]{1,}$ // 所有包含一个以上的字母、数字或下划线组成的字符串 ^[1-9][0-9]{0,}$ // 所有的正整数 ^\-{0,1}[0-9]{1,}$ // 所有的整数 /^[-]?(0{1}|[1-9][0-9]*)(\.[0-9]+)?$/ // 所有的浮点数 /^\w+@\w+\.\w+$/ //邮箱验证
由于章节编号在大的输入文档中会很可能超过九,所以您需要一种方式来处理两位或三位章节编号。下面的正则表达式匹配编号为任何位数的章节标题
/Chapter [1-9][0-9]*/
含义:匹配Chapter 加上一个大于零的数字,[1-9]匹配1到9任何一个数字,[0-9]匹配0-9任何一个数字,*表示前面的表达式[0-9]匹配次数大于等于0。之所以不用+,是因为编号不一定需要两位数或者更多位数,之所以不用?,是因为它会让编号限制在两位数。
下面的正则表达式匹配一个章节标题,该标题只包含两个尾随数字,并且出现在行首:
/^Chapter [1-9][0-9]{0,1}/
真正的章节标题不仅出现行的开始处,而且它还是该行中仅有的文本。它即出现在行首又出现在同一行的结尾。下面的表达式能确保指定的匹配只匹配章节而不匹配交叉引用。通过创建只匹配一行文本的开始和结尾的正则表达式,就可做到这一点。
/^Chapter [1-9][0-9]{0,1}$/
/a/匹配字母a
/7/匹配数字7
/a.c/匹配以a开头,c结尾,中间除了\n以外的换行符。(.可以匹配字符串中打印或非打印字符,除了\n)
/index\.html/匹配index.html(.前面加反斜杠代表就是.)
请注意,没有串联运算符。只须在一个字符后面键入另一个字符。(通俗讲,两个子表达式或者匹配符之间不需要连接符)
括在中括号表达式中的字符只匹配处于正则表达式中该位置的单个字符
/text [12345]/
可以匹配到text 1、text 2、text 3、text 4、text 5。
也可以写成
/text [1-5]/
但是开始值必须小于结束值
如果在中括号中使用连字符-,请用以下三种方法:
(1) [/-]反斜杠转义
(2) [-a-z]或[a-z-]将连字符放在中括号的开始或者结尾
(3) [!--]或[!-~]创建一个范围开始值小于连字符,结束值大于等于连字符
/text [^1-5]/中括号内以^开头表示匹配除了中括号内以外的任何数字和字符
替换使用 | 字符来允许在两个或多个替换选项之间进行选择。
/^Chapter|Section [1-9][0-9]{0,1}$/
上面的正则表达式要么匹配行首的单词 Chapter,要么匹配行尾的单词 Section 及跟在其后的任何数字。
/^(Chapter|Section) [1-9][0-9]{0,1}$/
在上面的正则表达式的适当位置添加括号,就可以使该正则表达式匹配 Chapter 1 或 Section 3
Js中的match()可以查找一个或多个正则表达式可以匹配的字符串,当正则为全局匹配时,返回所有匹配成功的值组成的数组。当正则为非全局匹配时,返回第一个匹配成功的相关内容。如果没有匹配到则返回null。
g可以控制是否查找多个
var str ="1;9uisjflk;jamsg56a654ag654ga"; var reg=/\d/g; console.log (str.match (reg)); 结果: ["1", "9", "5", "6", "6", "5", "4", "6", "5", "4"]
匹配连续的
var str ="1;9uisjflk; jamsg56a654ag654ga"; var reg=/\d+/g; console.log (str.match (reg)); 结果: ["1", "9", "56", "654", "654"]
Js中的test()可以验证字符串中有有没有符合要求的字符,如果字符串中有匹配的值返回 true ,否则返回 false。
var str="d2545ad5a4d54"; var patt=/\d/g; var result=patt.test(str); console.log(result); 结果: true exec() Js中的 exec()可以提取正则中符合要求的值,并且以数组的形式返回,与match()方法有相似之处(主要是全局匹配时不同,match()会一次性返回所有符合的字符,exec()是一个个返回 )。 不全局匹配的话(不加g):返回一个数组:匹配到的字符、匹配到的字符位置、匹配的整个字符串本身: var str="sad5a4d54add225sd22a"; var re=/\d+/; console.log(re.exec(str),'第一次'); console.log(re.exec(str),'第二次'); console.log(re.exec(str),'第三次'); console.log(re.exec(str),'第四次'); 结果: ["5", index: 3, input: "sad5a4d54add225sd22a", groups: undefined] "第一次" ["5", index: 3, input: "sad5a4d54add225sd22a", groups: undefined] "第二次" ["5", index: 3, input: "sad5a4d54add225sd22a", groups: undefined] "第三次" ["5", index: 3, input: "sad5a4d54add225sd22a", groups: undefined] "第四次"
如果全局匹配(加g):返回一个数组:匹配到的字符(每次调用指针会向后移一个,匹配到最后一个符合的字符,如果再次调用会返回null,如果还继续调用,就会从头开始匹配)、匹配到的字符位置、匹配的整个字符串本身
var str="sad5a4d54add225sd22a"; var re=/\d+/g; console.log(re.exec(str),'第一次') console.log(re.exec(str),'第二次') console.log(re.exec(str),'第三次') console.log(re.exec(str),'第四次') console.log(re.exec(str),'第五次') console.log(re.exec(str),'第六次') console.log(re.exec(str),'第七次') 结果: ["5", index: 3, input: "sad5a4d54add225sd22a", groups: undefined] "第一次" ["4", index: 5, input: "sad5a4d54add225sd22a", groups: undefined] "第二次" ["54", index: 7, input: "sad5a4d54add225sd22a", groups: undefined] "第三次" ["225", index: 12, input: "sad5a4d54add225sd22a", groups: undefined] "第四次" ["22", index: 17, input: "sad5a4d54add225sd22a", groups: undefined] "第五次" null "第六次" ["5", index: 3, input: "sad5a4d54add225sd22a", groups: undefined] "第七次"
search()与indexOf()方法类似,用来查找指定字符串在字符串中出现的位置。
返回的是第一个符合要求的结果在整个字符串中的位置
如果没有符合正则要求的结果,返回-1
var str="sad5a4d54add225sd22a"; var re=/\d+/g; console.log (str.search (re)); 结果: 3
replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
var str="sad5a4d54add225sd22a"; var re=/\d+/g; console.log (str.replace(re, "*")) 结果: sad*a*d*add*sd*a
函数执行一个正则表达式的搜索和替换
语法:mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
$pattern:要搜索的模式,可以是字符串或一个字符串数组。(正则表达式)
$replacement:用于替换的字符串或字符串数组。
$subject:要搜索替换的目标字符或字符串数组。
$limit:可选,对于每个模式用于每个subject字符串的最大可替换次数。默认是-1(无限制)。
$count:可选,替换执行的次数
返回值:
如果subject是一个数组,preg_replace()返回一个数组,其他情况返回一个字符串。
如果匹配被查找到,替换后的 subject 被返回,其他情况下 返回没有改变的 subject。如果发生错误,返回 NULL。
$str = 'runo o b'; $str = preg_replace('/\s+/', '', $str); // 结果将会改变为'runoob'
使用基于数组索引的搜索替换
$string = 'The quick brown fox jumped over the lazy dog.'; $patterns = array(); $patterns[0] = '/quick/'; $patterns[1] = '/brown/'; $patterns[2] = '/fox/'; $replacements = array(); $replacements[2] = 'bear'; $replacements[1] = 'black'; $replacements[0] = 'slow'; //虽然下标是倒叙,但数组的实际顺序还是210,所以quick对应bear,brown对应black,fox对 应slow echo preg_replace($patterns, $replacements, $string);
结果:
The bear black slow jumped over the lazy dog.
使用其他参数:
$count = 0; echo preg_replace(array('/\d/', '/\s/'), '*', 'xp 4 to', -1 , $count); echo $count; //3
结果:
xp***to 3
QueryList是一套用于内容采集的PHP工具,它使用更加现代化的开发思想,语法简洁、优雅,可扩展性强。相比传统的使用晦涩的正则表达式来做采集,QueryList使用了更加强大而优雅的CSS选择器来做采集,大大降低了PHP做采集的门槛,同时也让采集代码易读易维护。
QueryList是直接获取的页面上的元素内容,而不是直接获取api中的内容。
网址:https://querylist.cc/,新版环境要求php>=7.1
composer安装命令:
composer require jaeger/querylis
如果上面的composer命令太慢,请使用国内镜像:
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
说明:当时使用时是在tp5中测试使用的,所以下面的内容都是基于tp5进行举例的。
$ql=QueryList::get($src);
$data=$ql->find('div')->attr("id");//获取页面上一个div的id值
QueryList有个find()方法,用于采集单个元素,它通过jQuery选择器选择DOM元素,用法同jQuery的find()方法。
$ql=QueryList::get('https://www.iqshw.com/'); //获取第一张图片的链接地址,下面四种方法完全等价 $data[]=$ql->find('div')->attr('src'); $data[]=$ql->find('div')->src; $data[]=$ql->find('div:eq(0)')->src;//等推获取第n张:img:eq(n-1) $data[]=$ql->find('div')->eq(0)->src;//等推获取第n张:eq(n-1) //获取其他属性值 $data[]=$ql->find('div')->alt; //获取其它自定义属性值,只能获取一种 //获取所有属性值 $data[]=$ql->find('div')->attr("*"); //获取元素下的HTML内容 $data[]=$ql->find('#id')->html(); $data[]=$ql->find('#id .class')->html(); //获取其它自定义属性值,只能获取一种 //获取元素下的纯文本内容 $data[]=$ql->find('.class')->text();
获取多个元素的单个属性值(QueryList中凡是涉及到集合的地方返回的都是Collection集合对象,这个对象有个all()方法,用于把当前对象转成数组,所以你会发现下面很多写法都是$data->all() )
$ql=QueryList::get('https://www.iqshw.com/'); //获取所有图片的src属性值 $data[]=$ql->find('img')->map(function ($item){ return $item->src; })->all(); //等价与下面这句话 $data[]=$ql->find('img')->attrs('src')->all(); //获取元素中所有的html内容和text内容 $data[]=$ql->find('#id')->htmls()->all(); $data[]=$ql->find('.class')->texts()->all();
列表采集才是QueryList的核心功能,这里主要涉及到两个函数的用法:rules()和range()
$rules = [ '规则名1' => ['选择器1','元素属性'], '规则名2' => ['选择器2','元素属性'], // ... ]; //代码示例: $ql=QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules=[ //采集标题 'title'=>['.postTitle2','text'], //采集内容 'text'=>['.c_b_p_desc','text'], //采集时间 'time'=>['.dayTitle','text'] ]; $data=$ql->rules($rules)->query()->getData()->all(); //等同于上面 $data=$ql->rules($rules)->queryData();
用range()函数来配合rules()进行循环采集列表内容,range()函数的作用是选择一个元素作为多个数据之间的“切片”。
$ql=QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules=[ //采集标题 'title'=>['.postTitle2','text'], //采集内容 'text'=>['.c_b_p_desc','text'], //采集时间 'time'=>['.dayTitle','text'] ]; $range='.day';//切片选择器 $data=$ql->rules($rules)->range($range)->query()->getData()->all();
利用remove()来过滤不需要的内容:
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //移除选中html中的a标签 $data = $ql->find('.c_b_p_desc:eq(0)')->remove('a')->html(); //只保留a标签中的html内容 $data = $ql->find('.day:eq(0)')->find('a')->remove()->html();
列表采集时过滤不需要的元素,使用规则:
$rules = [ '规则名1' => ['选择器1','元素属性','内容过滤选择器'], '规则名2' => ['选择器2','元素属性','内容过滤选择器'], // ... ];
内容过滤选择器参数不光可以定义要移除的内容还可以定义要保留的内容,多个值之间用空格隔开, 有如下2条规则:
内容移除规则:选择器名前面添加减号(-),表示移除该标签以及标签内容。
内容保留规则:选择器名前面没有减号(-)(此时选择器只能为HTML标签名,不支持其他选择器), 当要采集的[元素属性] 值为text时表示需要保留的HTML标签以及内容,为html时表示要过滤掉的 HTML标签但保留内容。
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules = [ //采集标题 'title' => ['.postTitle2', 'text'], //采集内容 'text' => ['.c_b_p_desc', 'html','-a'], //采集时间 'time' => ['.dayTitle', 'text'] ]; $range = '.day';//切片选择器 $data = $ql->rules($rules)->range($range)->query()->getData()->all();
获取到数据后进行二次处理:
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules = [ //采集标题 'title' => ['.postTitle2', 'text'], //采集内容 'text' => ['.c_b_p_desc', 'html'], //采集时间 'time' => ['.dayTitle', 'text'] ]; $range = '.day';//切片选择器 $data = $ql->rules($rules)->range($range)->query()->getData(function($item){ $qls = QueryList::html($item['text']); $qls->find('a')->remove(); $item['content'] = $qls->find('')->html(); return $item; })->all();
$ql = QueryList::get('https://www.iqshw.com/'); //定义采集规则 $rules = [ //采集标题 'title' => ['a', 'text'], ]; $range = 'li';//切片选择器 //乱码处理,设置输出编码 $data = $ql->rules($rules)->range($range) ->encoding('UTF-8','GB2312') ->query()->getData()->all(); //如果设置输出参数无法解决乱码,那就使用removeHead()方法移除html头部 $data = $ql->rules($rules)->range($range) ->removeHead()->query()->getData()->all(); //或者 $data = $ql->rules($rules)->range($range) ->encoding('UTF-8','GB2312') ->removeHead()->query()->getData()->all(); //手动转码 $html=$text=iconv("GB2312","UTF-8",file_get_contents('https://www.iqshw.com/')); $data = QueryList::html($html)->rules([ 'title' => ['.news-comm-wrap a ', 'text'], ])->range($range)->query()->getData()->all();
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules = [ //采集标题 'title' => ['.postTitle2', 'text'], ]; $range = '.day';//切片选择器 //使用flatten()方法将多维集合转为一维的 $data = $ql->rules($rules)->range($range)->query()->getData()->flatten()->all();
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules = [ //采集标题 'title' => ['.postTitle2', 'text'], ]; $range = '.day';//切片选择器 //take()方法返回给定数量项目的新集合,对最初的采集结果data进行处理: $data = $ql->rules($rules)->range($range)->query()->getData()->take(2)->all();
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules = [ //采集标题 'title' => ['.postTitle2', 'text'], ]; $range = '.day';//切片选择器 //使用reverse()来倒转集合中的项目 $data = $ql->rules($rules)->range($range)->query()->getData()->reverse()->all();
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules = [ //采集标题 'title' => ['.postTitle2', 'text'], ]; $range = '.day';//切片选择器 //filter()方法用于按条件过滤数据,只保留满足条件的数据。 //只能过滤,不能修改添加原数据,如果需要修改添加则用getData $data = $ql->rules($rules)->range($range)->query()->getData() ->filter(function ($item){ return $item['title']!='PHP使用引用实现无限极分类'; })->all();
$ql = QueryList::get('https://www.cnblogs.com/yulongcode/'); //定义采集规则 $rules = [ //采集标题 'title' => ['.postTitle2', 'text'], ]; $range = '.day';//切片选择器 //map() 方法遍历集合并将每一个值传入给定的回调。 //该回调可以任意修改项目并返回,从而形成新的被修改过项目的集合。 $data = $ql->rules($rules)->range($range)->query()->getData() ->map(function ($item){ $item['time']=time(); return $item; })->all();
attr()方法除了获取DOM元素的值外,还有第二个参数,用于设置元素属性值
text()方法是获取元素的纯文本内容,加参数表示设置元素内容
html()方法是获取元素的HTML内容,加参数表示设置元素HTML内容
以上三个用法基本和jQuery相同,添加完后再获取,才会出现在获取的数据中。
append()追加元素
replaceWith()替换元素
removeAttr()移除元素属性
parent()用户获取当前元素的父级元素
next()和prev()用于获取当前元素临近的下一个元素和上一个元素
更多使用说明请查看使用文档。
直接在网站运行目录的.htaccess文件中输入以下内容:
Apache:
<IfModule mod_rewrite.c> RewriteEngine on RewriteBase / RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php?s=/$1 [QSA,PT,L] </IfModule>
Nginx:
location / { if (!-e $request_filename){ rewrite ^(.*)$ /index.php?s=$1 last; break; } }
/** * 判断两个时间之间相差的月份 * @param $time1 * @param $time2 * @return float|int */ function get_month_interval($time1, $time2) { if($time1>$time2){ $t=$time1; $time1=$time2; $time2=$t; } $y1=date('Y',$time1); $y2=date('Y',$time2); $m1=date('m',$time1); $m2=date('m',$time2); $months=($y2-$y1)*12+($m2-$m1); return $months; }
在vendor\topthink下执行composer命令,默认安装最新发送电子邮件的插件包:
composer require phpmailer/phpmailer
安装图例:
引入方式:
use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception;
使用方法:
$mail = new PHPMailer(true);// Passing `true` enables exceptions try { //服务器配置 $mail->CharSet = "UTF-8";//设定邮件编码 $mail->SMTPDebug = 0;// 调试模式输出 $mail->isSMTP();// 使用SMTP $mail->Host = 'smtp.qq.com';// SMTP服务器 $mail->SMTPAuth = true;// 允许 SMTP 认证 $mail->Username = '*******@qq.com';// SMTP 用户名 即邮箱的用户名 $mail->Password = '**********';// SMTP 密码 部分邮箱是授权码(例如163邮箱) $mail->SMTPSecure = 'ssl';// 允许 TLS 或者ssl协议 $mail->Port = 465;// 服务器端口 25 或者465 具体要看邮箱服务器支持 $mail->setFrom('******@qq.com', 'shx');//发件人 $mail->addAddress($email, 'Joe');// 收件人 //$mail->addAddress('ellen@example.com');// 可添加多个收件人 //$mail->addReplyTo('xxxx@163.com', 'info');//回复的时候回复给哪个邮箱 建议和发件人一致 //$mail->addCC('cc@example.com');//抄送 //$mail->addBCC('bcc@example.com');//密送 //发送附件 // $mail->addAttachment('../xy.zip');// 添加附件 // $mail->addAttachment('../thumb-1.jpg', 'new.jpg');// 发送附件并且重命名 //Content $mail->isHTML(true);// 是否以HTML文档格式发送 发送后客户端可直接显示对应HTML内容 $mail->Subject = '我是邮件标题'; $mail->Body = '我是邮件内容'; $mail->AltBody = '如果邮件客户端不支持HTML则显示此内容'; $mail->send(); return '邮件发送成功'; } catch (Exception $e) { return '邮件发送失败: ' . $mail->ErrorInfo; }
本人用的是QQ邮箱的相关配置,所以这里申请方式就以QQ邮箱为例。
登入QQ邮箱。
进入设置,点击账户,向下翻,开启POP3/SMTP服务。
记录一下授权码,就是代码中的SMTP密码,无法复看,一旦忘记就只能重新申请!
SMTP Error: Could not authenticate.错误怎么解决?
网上大多数说的解决方案是:替换某个函数或者开启php.ini的某些功能。我个人曾多次出现此情况,前面的两个修改方案从未成功解决过错误,也可能我的问题和他们的报错原因不一样。本人多次入坑,强烈建议直接重新申请一次授权码就可以了,简单快捷!
这个错误我发现只要我配置好以后隔段时间如果不去使用它,可能就会产生这个错误,我的代码和配置从没动过,但是隔一段时间仍旧失效,所以我怀疑QQ邮箱这边可能有一定验证机制,长时间不使用就会出现类似于注销的情况。