-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcontent.json
1 lines (1 loc) · 45.4 KB
/
content.json
1
{"meta":{"title":"shellvon的博客","subtitle":"菜鸟一枚","description":"好好学习,天天向上","author":"shellvon","url":"http://von.sh"},"pages":[{"title":"Change Log","date":"2017-12-07T04:48:48.000Z","updated":"2017-12-07T05:49:50.000Z","comments":true,"path":"changelog/index.html","permalink":"http://von.sh/changelog/index.html","excerpt":"","text":"序很开心您能阅读本文,本文包含此博客的所有变更消息,主要目的是让任何人都可以看到此博客在任何时候的变化。包括我自己。 Changelog所有本博客的一些重大变更等历史我都将手工加入此文。 2017.12.07Added 增加此 Changelog 入口以及 README 使用 Cloudflare 将全站转成 HTTPS commit by 62b1ad42e79 2017.12.06Added 增加 RSS 功能 commit by 2e7ae3369d5 2017.12.01Added 增加旧版 TerminalBlog 入口 commit by d1afe48a14ca 初次增加友链 commit by 5b7de57df9b 增加 LiveRe 评论和 algolia 搜索 commit by dd8ba0433e0 Removed 关闭分类/标签页的评论 commit by b7de57df9b 2017.11.30Added 增加腾讯统计以及 LeanCloud 阅读次数统计 commit by 78698ec89ff 撰写第一篇文章《30分钟学正则》 commit by f46835872b9 购买域名 von.sh 并使用 Hexo + Next 完成搭建 commit by f46835872b9"},{"title":"关于我","date":"2017-11-30T09:25:34.000Z","updated":"2017-11-30T09:39:17.000Z","comments":true,"path":"about/index.html","permalink":"http://von.sh/about/index.html","excerpt":"","text":"这是我第X次尝试写博客了。希望我能坚持下去。"},{"title":"所有标签","date":"2017-11-30T06:53:41.000Z","updated":"2017-12-01T04:01:30.000Z","comments":false,"path":"tags/index.html","permalink":"http://von.sh/tags/index.html","excerpt":"","text":""},{"title":"所有分类","date":"2017-11-30T06:53:58.000Z","updated":"2017-12-01T04:01:00.000Z","comments":false,"path":"categories/index.html","permalink":"http://von.sh/categories/index.html","excerpt":"","text":""}],"posts":[{"title":"谈谈爬虫与反爬虫","slug":"talk-about-spider-and-anti-spider","date":"2017-12-08T02:30:31.000Z","updated":"2017-12-08T03:02:00.000Z","comments":true,"path":"2017/12/08/talk-about-spider-and-anti-spider/","link":"","permalink":"http://von.sh/2017/12/08/talk-about-spider-and-anti-spider/","excerpt":"前言最近同事在群里发了这么一个爬虫地址 (未满18岁慎入),不得不佩服 Scrapy 的强大,但是这让我想到了另外一个问题,PornHub 可以估计随便爬,但是比如像淘宝/亚马逊的商品信息可是一个公司至关重要的数据,显然舍不得让爬虫随便来爬。这就涉及到爬虫与反爬虫的策略问题。","text":"前言最近同事在群里发了这么一个爬虫地址 (未满18岁慎入),不得不佩服 Scrapy 的强大,但是这让我想到了另外一个问题,PornHub 可以估计随便爬,但是比如像淘宝/亚马逊的商品信息可是一个公司至关重要的数据,显然舍不得让爬虫随便来爬。这就涉及到爬虫与反爬虫的策略问题。 爬虫爬虫的目的是要尽可能的模拟人的行为,所以越接近人类行为越不容易被发现,很多基础的问题是必须先考虑的。比如浏览器的 User-Agent 和 防止盗链用的 Referer。鄙人没有写多少爬虫,简单总结一下我在写爬虫的一些经验和技巧吧。 灵活的请求头 不一样非要 Chrome 或者 Firefox, 有时候伪装成知名的爬虫的请求头也许反而更好。比如使用BaiduSpider+模拟百度。 优先尝试移动端 通常移动端的反爬虫策略会比较简单,所以更加容易绕过 尝试使用代理 很多比如 IP 限制的,我们可能可以使用 X-Forward-For 就搞定了,但更多的时候我们需要动态的IP库 尝试使用 Tor 代理服务器 有时候可以看看万能的淘宝 需要节制而且友好一点 所谓节制,是需要注意自己爬虫的请求频率,我个人喜欢随机休眠以继续,不要以固定频率去做 所谓友好,就是爬虫抓取之前可以看看 robots.txt 另外,注意节制也算友好,因为不至于给目标服务器带来过大压力 注意爬取顺序 一般我们分为 DFS 和 BFS,在爬取的时候如何最快程度找到自己需要的内容的时候,这需要考虑。 URL 去重 ? 善于用库 比如 Python 的 Scrapy 和 Java 的 crawler4j 涉及到模拟浏览器的库, 比如 Headless Chrome 和 PhantomJS,为自动化测试而出来的 Selenium 比如使用 OCR 技术用于验证码识别的 teseract,复杂一点的验证码可能需要一些矫正技术和 AI 算法 这里的库还可以考虑一些在线提供的免费API,比如 apify 好好利用 Chrome DevTools 工具 和一些抓包工具比如 Charles 移植性和扩张性 当然,也包括了如何让自己的代码一次写了,到处跑(Java 口号)。最大程度复用哦。 善于使用 Google / Github ,而不是百度 比如你可能需要了解反爬虫技术(即所谓的反反爬虫) Xpath Or Regex 等等技术 知己知彼 多看看一些反爬虫策略,才能更好的反反爬虫。 可以利用多账号进行爬取 比如临时邮箱 https://10minutemail.net/ 看一下优秀的开源爬虫的设计思路架构模式 始终保持学习的心态去学 注意一些陷阱 比如 Zip炸弹攻击 蜜罐技术 当然,我不是一个专业写爬虫的,只是小打小闹过,以上的内容纯属个人经验,很多没提到的需要自己总结和网上多学习。 反爬虫大多数时候,爬虫都挺讨厌的,所以有了反爬虫,遗憾的是,目前没有任何技术可以绝对或者完美的方式反爬虫。这里我主要说一些常见的反爬虫思路,有些甚至在我第一次知道的时候觉得脑洞之大。 请求头的检查 比如 User-Agent 或者 Referer,甚至 Host,前不久和同事利用 Chrome 的 WebRequest 结合 油猴脚本 就利用请求头搞了一些事。这里需要注意的是,各种请求头,都不可信。 IP 频率限制 这种方式是基本的一个Rate Limiter,我们在设计后端 API 的时候也经常会遇到。比如: 基于 Nginx 扩展的 ngx_http_limit_req_module 基于 Redis 的 INCR 命令 漏桶算法和令牌桶算法 使用授权 比如需要登陆注册的,或者使用黑白名单。 验证码 需要注意的是,有些验证码实现实在太low了,有的却逆天的困难,比如12306的验证码。这时候还得考虑要不要接入黑产(比如这种地方)。实现一个验证码至少应该考虑这些问题: 一个验证码只能用一次,而且有有效期 不要把 0/o和l/1这种不容易区分的放一起。 字符个数/字符倾斜的位置最好别固定,不然一个简单的 OCR 轻松可以搞定 不过现在啥技术都是云,我的博客里面的搜素,统计,CDN都是第三方服务。显然验证码也可以使用各种平台的。 检查爬虫的一些特征值 前面提到的比如请求头算,但是还有一些其他的。比如淘宝首页,他会检查自己的 JS 执行环境是不是使用了前文提到的PhantomJS之类的玩意儿。因为这些库都有他自己的一些特征: 123456789101112131415// 各种模拟器的check//代码来自://https://github.com/GangZhuo/BaiduPCS/blob/cda508bf87a433a2dfc5938008f6f0447e698b36/pcs/pcs_passport_dv.c#L682-L693 var a = [ (window['phantom'] ? 1 : 0).toString(), (window['_phantom'] ? 1 : 0).toString(), (window['callPhantom'] ? 1 : 0).toString(), (window['__fxdriver_unwrapped'] ? 1 : 0).toString(), (window['fxdriver_id'] ? 1 : 0).toString(), (document.getElementsByTagName('html')[0].getAttribute('webdriver') == null ? 0 : 1).toString(), (document['$cdc_asdjflasutopfhvcZLmcfl_'] ? 1 : 0).toString(), (document['__webdriver_script_fn'] ? 1 : 0).toString(), (window['webdriver'] ? 1 : 0).toString(), (window['ClientUtils'] ? 1 : 0).toString() ]; 前文提到的 Zip 炸弹,或者使用 iframe。 阻止 copy && paste 事件 (😂,没错,没有版权意识的人类也是可怕的爬虫) 直接将文字替换为图的形式 显然,对方不得不使用 OCR ,可增加技术成本。 安全性考虑的还有 经常性的变更自己的 HTML 内容 API 接口不要返回太多东西 使用 Ajax …. 在 Github 上还有一篇文章写的不错 How to Prevent Scraping。也可以看看。 接下来我说一下我遇到的一些优秀的反爬虫思路。 利用 web font 做 Unicode 映射 文章参见: 反击“猫眼电影”网站的反爬虫策略 这个实现成本其实很简单,对于爬虫来说,如果知道映射规则也只是做一个table替换罢了。而且,通常情况下,对于中文的防采集不适合这种方式,因为中文的字体库太大了。所以通常都是数字,英文则适合使用此方法实现防采集。 具体实现方式 icomoon 上可以自己做字体和 Unicode 映射,在这之前我们只需要在网上随便下载一些常见的字体导入即可。 因为导入的时候仅支持svg和json,所以我们下载的比如ttf需要转化成svg, 我们需要在另外的网站上转化即可,比如这 Everything Fonts 制作好之后这还需要后端在输出的时候做好配合。其实就是个字典映射罢了,关键是这个映射规则尽可能复杂一些才好。 我个人觉得可以后端可以随机加载不同的字体,里面的映射关系都不一样。设置比如把1->3, 7->2 这种,都是字,但不是原来含义才容易让人迷惑。 利用视觉误差 这一点在刚说的 How to Prevent Scraping 内有提到过: Screw with the scraper: Insert fake, invisible honeypot data into your pageAdding on to the previous example, you can add invisible honeypot items to your HTML to catch scrapers. An example which could be added to the previously described search results page: 1234567<div class="search-result" style="display:none"> <h3 class="search-result-title">This search result is here to prevent scraping</h3> <p class="search-result-excerpt">If you're a human and see this, please ignore it. If you're a scraper, please click the link below :-) Note that clicking the link below will block access to this site for 24 hours.</p> <a class"search-result-link" href="/scrapertrap/scrapertrap.php">I'm a scraper !</a></div>(The actual, real, search results follow.) 即我们可以利用 CSS 的样式来完成一些有趣的事情,让爬虫看到的和用户看到的不是同样的数据(建议看完上面的英文版原文,不然不会知道为啥) 除了英文中提到的,还可以利用一样的原理来做这些事情: 每个几个字符之间随机插入一些没啥用的标签,然后把这些标签的style全部设置为display: none; 这样对用户来说看到的东西一样,但是呢,爬虫无论用正则还是用Xpath或者怎么样,都增加了其提取难度。哦,我最近看到了这样的东西对这种策略应该有点用:mozilla/readability 如果能确定是爬虫,那么后端不要报错,而是随机生成数据返回。我没记错的话,当初的亚马逊就是这样的。 JS 混淆这个大多数人都做了,但是混淆的好坏也是不一样的,举例来讲,图片的 JS 代码压缩混淆等,我们有很多好的工具可以使用: Javascript在线解压缩 - 在线工具 jsbeautifier 甚至还有基于机器学习的 JSNice 我见过的比较好的混淆的例子有 商标网上检索系统,他的代码简直丧心病狂,可以文章首图或者参见这里:http://wsjs.saic.gov.cn/tmrp/js/main.js?1.127&D9PVtGL=803ba5。可惜美中不足的是,我们只需要替换他的queryString就可以看到源代码了。比如上述链接换成:http://wsjs.saic.gov.cn/tmrp/js/main.js?1.127 即可完整查看。 尾声鄙人的爬虫经验基本算是无,所有很多内容可能理解错了或者没有考虑到的,希望有了解的朋友可以给我留一下言。最后,谢谢你耐心的看完此篇文章🙏。","categories":[{"name":"技术","slug":"技术","permalink":"http://von.sh/categories/技术/"}],"tags":[{"name":"爬虫","slug":"爬虫","permalink":"http://von.sh/tags/爬虫/"}]},{"title":"PHP Generator 笔记","slug":"php-generator-note","date":"2017-12-06T08:55:51.000Z","updated":"2017-12-06T12:39:12.000Z","comments":true,"path":"2017/12/06/php-generator-note/","link":"","permalink":"http://von.sh/2017/12/06/php-generator-note/","excerpt":"图片来源 https://wpengine.com/try/php7-hosting/ 序先说,标题虽然是《Generator 笔记》,但实际上本文会主要内容会是yield。 以鄙人的拙见,目前大多数 PHPer 对 PHP 的 yield 关键字并不怎么了解,但实际上这却是一块非常值得学习的地方,至少于我而言如此。yield 为 PHP 引入了生成器,协程 等一些复杂概念,导致入门门槛也挺高。 关于学不学yield, 目前,我遇到过以下几类人:","text":"图片来源 https://wpengine.com/try/php7-hosting/ 序先说,标题虽然是《Generator 笔记》,但实际上本文会主要内容会是yield。 以鄙人的拙见,目前大多数 PHPer 对 PHP 的 yield 关键字并不怎么了解,但实际上这却是一块非常值得学习的地方,至少于我而言如此。yield 为 PHP 引入了生成器,协程 等一些复杂概念,导致入门门槛也挺高。 关于学不学yield, 目前,我遇到过以下几类人: 学个 PHP 都是为了简单快速搞东西上线,不用这玩意也能满足老板需求(不想学) 自己写的代码也遇不到啥问题非要yield搞定的( 没有应用场景) yield 带来了生成器,协程,异步等一系列复杂性概念,文档少的可怜,(门槛高) 理解协程的又难免会和 Golang 的 goroutines 做对比。(不屑于学) 我算第三类,文档少的可怜。当然,还有就是自己的聪明程度不够。关于 PHP yield这个,我尝试看了许多博文,可按照国内的技术尿性,大家的博文内容都写的是一样的。也不知道啥时候搜索引擎可以帮我智能去重就好了。值得庆幸的是,Google 还是给了我一些不错的文章: Cooperative-multitasking-using-coroutines-in-PHP 中文: 在PHP中使用协程实现多任务调度 What-generators-can-do-for-you 当然官方文档也是不错的内容: RFC:generators 和 PHP 手册 > 语言参考 > 生成器 由于PHP的资料太少,我反其道而行之,选择了去学习 Python 的生成器。关于 Python 的生成器,我主要看了大神 Dabeaz 的几个 PPT,都可以在他博客里面找到。本人的笔记,全部借(cao)鉴(xi)自此大神的 PPT。 yield的概念迭代器协议在 PHP 中, 可以实现 Iterator 接口, 从而可以让对象自行决定如何遍历以及每次遍历时哪些值可用。这样的接口,我们称之为迭代器协议 (Iteration Protocol) 。 12345678Iterator extends Traversable {/* 方法 */ abstract public mixed current ( void ) // 返回当前元素 abstract public scalar key ( void ) // 返回当前元素的键 abstract public void next ( void ) // 向前移动到下一个元素 abstract public void rewind ( void ) // 返回到迭代器的第一个元素 abstract public boolean valid ( void ) // 检查当前位置是否有效} 生成器 Generator 就是是实现了迭代器协议的一种对象。这种对象比较特殊: 无法直接使用 new 实例化 得使用 yield 产生 可以通过send方法传值给yield所在位置 为什么需要引入关键字 yield 呢? 我们先来看一个例子,需要读取一个文件每一行出来: 1234567891011// 使用数组.function getLines($file) { $f = fopen($file, 'r'); $lines = []; if (!$f) throw new Exception(); while (false !== $line = fgets($f)) { $lines[] = $line; } fclose($f); return $lines;} 上述代码有个明显的问题:如果当文件足够大的情况下,数组$lines会特别大,内存随时会跪。 幸运的是,我们可以使用迭代器,一行一行的读,参见这里的例子: 12345678910111213141516171819202122232425262728293031// 自己实现文件遍历class FileIterator implements Iterator { protected $f; protected $data; protected $key; public function __construct($file) { $this->f = fopen($file, 'r'); if (!$this->f) throw new Exception(); } public function __destruct() { fclose($this->f); } public function current() { return $this->data; } public function key() { return $this->key; } public function next() { $this->data = fgets($this->f); $this->key++; } public function rewind() { fseek($this->f, 0); $this->data = fgets($this->f); $this->key = 0; } public function valid() { return false !== $this->data; }} 如你所见,虽然这样解决了内存问题,但是代码复杂的多了。于是乎,Generator也上线了,一个函数里面只要有yield关键字,就是Generator,它实现了迭代器协议。 123456789// 使用 yield 完成相同功能function getLines($file) { $f = fopen($file, 'r'); if (!$f) throw new Exception(); while ($line = fgets($f)) { yield $line; } fclose($f);} 注意到上述代码完成了和迭代器相同的功能,复杂性大大降低,相较于数组实现,性能开销也会明显下降。 生成器如你所见,生成器写法和普通函数写法基本上一致,只是没有用return,而是yield。 生成器和普通函数区别其实挺大的。 调用一个生成器只是会创建一个生成器对象,而不会直接运行这个函数: 123456789>>> function xrange($start, $limit, $step = 1) {... for ($i = $start; $i <= $limit; $i += $step) {... echo \"call me\"; <------... yield $i; |... } |... } |=> null |>>> $gen = xrange(1,3) # < --- 注意看没有执行echo,没有任何输出。它=> Generator {#176} # < --- 显示它是一个Generator 调用current()函数可以唤醒(Resume)生成器,执行到yield关键字的地方继续暂停(Suspends),继续往下执行需要执行next()。即yield可以产生中断点。 这个和Python不一样哦。Python是直接调用next的(虽然可以使用装饰器) 123456789>>> $gen->current(); // <--- 注意这一行也只执行了第一个echo.call me⏎ // 执行完current之后,生成器里面的代码会继续往后执行,=> 1 // 直到遇到yield这个关键字,然后继续暂停. ^----注意这个1是来自yield后面的$i所产生而返回的,即yield将$i返回给了调用者。>>> $gen->next();call me⏎ // 执行到第二次循环,然后输出.=> null >>> $gen->current(); // 现在拿到的是$i=2的时候。=> 2 如上,因为生成器是需要的时候(执行next())才会执行。不会贪婪的一次性生成所有数据放在内存中,而是特别的懒,我们称为惰性求值( Lazy Evaluation),所以内存占用很小。也许你也注意到了,我们可以一直next()调用往下执行,但是并没有prev之类的接口。所以生成器属于 One-time Operation,一次性操作。因此上面的xrange 你只能遍历一次,如果想要多次,你必须得重新调用一次生成器。 不同于return返回的是一个value, yield可以让你产生一系列的值。 yield 使用场景yield 最容易让人想到的一个点就是可以让一个普通的函数随便变成高大上的生成器,而生成器是可以迭代的。即yield作为最简单的用途来说,就是可以放在循环中,一直迭代数据。 但其实从 Python 中 yield 的作用来看,主要被分成了三种类型( 参见 PPT): 迭代器 Iterator 可以作为生产者产生数据 接受消息 可以作为消费者接受数据 陷阱 Trap 可以实现多任务调度 PHP 亦是如此。下文以 PHP 为例子,把三种方式都简单记录一下。 生产者如前文,当yield放在一个循环中,我们可以利用yield 作为生产者产生有限或者无限的数据。如前文提到的 xrange 这种基础用法。考虑这样的一种情况: 我需要打开一个日志文件,然后将所有符合某种规则的日志行全部提取出来,然后需要对这些行进行一定的处理,然后做展示,大概这样: 1234567891011foreach($lines as $line) { // 可能有逻辑其他处理 if (preg_match($regex, $line)) { // 此条件下其他处理逻辑 $line = andParsedLine($line); if ($isDuplcaite($line)) { // 其他逻辑 echo $line; } }} 这就是传说中的 Pyramid of doom(金字塔厄运),大概这样 这个流程其实很类似于我们 Linux 下的管道: grep xxx file | awk xxxx | sort | uniq | head ... 同样的道理,我们可以把函数封装成这样 => functionC(functionB(functonA())) 从而避免或者减少金字塔厄运(事实上,为了解决这样的问题,还有比如Promise,Thunk很多玩意儿): 12345678910111213141516171819202122232425262728// 一行一行的读文件(产生数据)function readByLine($file) { $f = fopen($file, 'r'); while (!feof($f)) { yield $f; } fclose($f);}// 使用正则过滤(继续生产数据)function grep($gen, $regex) { foreach ($gen as $line) { if (preg_match($regex, $line)) { yield $line; } }}// 现实文件的前n行(还是生产数据)function head($gen, $n){ $current = 0; foreach ($gen as $line) { if ($current > $n) { break; } echo $line; }}head(grep(readByLine(\"/www/xxxx.log\"), 'xxxxx'), 3); 因为 yield 是惰性求值的,在需要的时候才会做计算,我们不用担心这样的代码会出现内存过大之类的问题。(相反,如果上述代码换成 return来写,就可能会爆哦) 很多时候, PHP 中一些自带的函数 array_map, array_filter, array_column等等,我们都可以用 yield 将其替换为 生成器 (Python中这些API已经如此了) 题外话: https://3v4l.org/hQPpe PHP 5.x 的时候生成器方案比普通快。PHP 7.x 的时候生成器方案比普通慢。在 HHVM 上 普通方案的速度太快(比生成器快接近3倍) 前文提到过协程, 中断点 我们还没有接触,显然可以知道yield的作用其实还有很多。 消费者 Generator 不仅仅实现了Iterator接口, 而且还有自定义的一些方法,比如send()。 这个send可不得了,它可以实现所谓的双向通讯。具体来说,上面我们处理日志的流程是这样的: 1234 +------------+ +------+ +------+/www/xxxx.log -->| readByLine |+--->| grep |+-->| head |+--> +------------+ +------+ +------+ 可以发现,无论是grep还是head,它们都是自己主动去Pull之前的数据。我们仅仅用到了yield的返回功能,还差发送功能。 现在我们看一下 Generator::send 向生成器中传入一个值,并且当做 yield 表达式的结果,然后继续执行生成器。 如果当这个方法被调用时,生成器不在 yield 表达式,那么在传入值之前,它会先运行到第一个 yield 表达式 这基本就是 PHP 中实现 协程 的原理,然而 PHP 里面严格意义上叫做半协程(semicoroutines),因为控制权并没有真正的协程那么自由: Generators), also known as semicoroutines。while both of these can yield multiple times, suspending their execution and allowing re-entry at multiple entry points, they differ in that coroutines can control where execution continues after they yield, while generators cannot, instead transferring control back to the generator’s caller.[6] That is, since generators are primarily used to simplify the writing of iterators, the yieldstatement in a generator does not specify a coroutine to jump to, but rather passes a value back to a parent routine. 不过我一直觉得概念其实并没有那么的重要,不用太过于纠结。 看到这个函数,我们需要想到几点: 调用send时,我们会去唤醒之前协程有 yield 表达式的地方。 这时候执行权在协程那边,您可以理解为 Goto 语句。 当遇到下一次yield的时候,才会把数据返回,并且交回控制权。 我们还是来看一下 yield 作为消费者的话,以 grep 举例: 12345678910111213// 可以作为消费者接受数据的grep.function grep($regex) { while (true) { $line = yield; // <-- 可以从这里接受一个数据(通过generator->send传递给他.) if (preg_match($regex, $line)) { echo \"Matched: {$line}\\n\"; } }}# >>> $filter->send(\"python generator\");# => null# >>> $filter->send(\"shellvon\");# Matched: shellvon 如上面的代码所示,我们原来的处理流程可以变成这样: 123 as source +------+ send() +------+ send() +--------+/www/xxxx.log -->| grep |+------->| head |+------->| output | +------+ +------+ +--------+ 如上图所示,上图有几个特点: 您可以使用 send()方法把数据push给下一个操作逻辑,注意与之前迭代器的Pull相比较 您可能发现 readByLine 没有在上图出现,那是因为上图画的都是消费者,不是生产者。readByLine 其实还是存在,以生产者身份。而这样一个典型的管道操作是需要有一个数据源的,即生产者。 生产者显然不是一个协程,readByLine可以仍然保持之前的迭代器,通过send将数据发出来。 如果脑洞更加大一点, 相比于之前的 Pull,我们 Push 的时候,可以同时 send 给多个消费者。更加复杂: 123456789 +-----------+ +------>| coroutine |+- - - - - - - + | +-----------+ | +-------+--+ +-----------+ +-----------+source+--->|coroutine |--->| coroutine |+- - - ->| coroutine | +-------+--+ +-----------+ +-----------+ | +-----------+ | +------>| coroutine |+- - - - - - - + +-----------+ 当然,多个消费者最后可以再 send 给同一个 pipe , 聚合回来。 另外,其实我们的 source 本身来源也可以不唯一。比如我们可以使用 PHP SPL 中提供的一些有用组件: MultipleIterator 和 AppendIterator,之前的 head 我们可以使用 LimitIterator。 甚至,按照 Dabeaz 大神的说法,我们的来源和 send的 目标也好,不一定非要是字符串,非要是文件。 陷阱这里的陷阱是指计算机专业术语,我在 Github 找到了一个中文解释: 程序中使用请求系统服务的系统调用而引发的事件,称作陷入中断(trap interrupt),也称软中断(soft interrupt),系统调用(system call)简称trap。 之所以用到这个概念,主要原因是因为我们的 yield 可以产生中断点与操作系统的 Trap 很类似。即: 我们可以利用 yield 会获取(通过你send数据给它)与返回控制权这个特性完成一个类似操作系统的调度器。 这就是文章开始提到的 在PHP中使用协程实现多任务调度 的内容(PHP Generator的发明人 Nikita Popov 在PHP的 rfc 中也说到了Dabeaz 大神的 PPT,所以我觉得他的这篇文章应该也是受到此 PPT 的影响,具体参见 Part6 及其 Part7) 所以其实本章节更好的题目应该是 利用协程完成多任务调度。因为中文/英文都特别详细,我就不用做啥笔记了。附上地址: 中文: 在PHP中使用协程实现多任务调度 英文: Cooperative multitasking using coroutines (in PHP!) 题外话: 因为Nikita Popov 的那篇 Cooperative multitasking using coroutines in PHP, 目前其实已经有不少的项目和文章了,比如: PHP异步编程: 手把手教你实现co与Koa https://github.com/pinguo/php-msf http://zanphp.io/ 附录在学习协程和生成器的时候,我检索了各种关键字,其中发现了不少我个人觉得营养价值比较高的文章或者博客,我作为附录放这里,希望我每一次看的时候都能学到更多。 编程珠玑番外篇-Q 协程的历史,现在和未来 操作系统简单实现与基本原理 A Functional Guide to Cat Herding with PHP Generators","categories":[{"name":"技术","slug":"技术","permalink":"http://von.sh/categories/技术/"}],"tags":[{"name":"Generator","slug":"Generator","permalink":"http://von.sh/tags/Generator/"},{"name":"PHP","slug":"PHP","permalink":"http://von.sh/tags/PHP/"}]},{"title":"又一次新的开始","slug":"a-new-beginning-again","date":"2017-12-01T06:03:14.000Z","updated":"2017-12-07T04:15:06.000Z","comments":true,"path":"2017/12/01/a-new-beginning-again/","link":"","permalink":"http://von.sh/2017/12/01/a-new-beginning-again/","excerpt":"序前天无意看到了这么一篇博文 You should blog even if you have no readers","text":"序前天无意看到了这么一篇博文 You should blog even if you have no readers 书中有个观点: 写作能够使你更加聪明,因为写作的过程显露出一个人想法的缺陷。在写文章的过程中,你需要组织你的想法,你会仔细的思考,从而强制你去锤炼你的思想。加上我最近的一些遭遇,我突然就特想弄个博客。 离其实博客我之前搞过几次,OSChina / Lofter / Sina / Wordpress / TerminalBlog 再见!最后一次用 TerminalBlog 还是去年。之后觉得自己肚子里没货,语文又是在及格线上来回波动的理科生,放弃了。 今天又是新的开始,主要原因有几个: 好记性不如烂笔头,很多东西如果不以文字或者其他持久化方式存下来,就会被淡忘。 我希望我可以回看当初得想法的时候,可以感受到自己的成长。 希望自己变得聪明(笑~😊 我口头上说了几次希望女朋友能用博客记录自己学习前端的成长之路,但都没有实际行动。是时候动一下了。 之所以选择 Hexo 而放弃之前自己写的 TerminalBlog 也是考虑了一些因素: 我的静态博客生成器太简单了,满足不了一些复杂的需求。 我希望我女朋友也能用,对于 Python小 白来说,学习这个完全没有文档的生成器,复杂了。 女朋友是学前端的,所以选择 NodeJS 的 Hexo,而不是其他语言的,比如 Ruby Jeklly(当然也包括我自己的^_^) 域名 von.sh 是昨天和“TW首席咨询师” kimLeo 商量定下来的,昨天才买的,不到500元。 因为是CNAME到 Github Pages 的,所以其实我的老博客也可以在这个域名下。因此,我不打算迁移老内容过来了。 这里是老博客入口: http://von.sh/TerminalBlog/ 跋封面图是 Freebuf 偷来的,没有图的博文总觉得少了点什么。 写博文真是一个无比难的问题,这个新的开始。我希望我们都能坚持。另外,谢谢 esrever10 大佬的赞助费。","categories":[{"name":"生活","slug":"生活","permalink":"http://von.sh/categories/生活/"}],"tags":[{"name":"随想","slug":"随想","permalink":"http://von.sh/tags/随想/"}]},{"title":"30分钟学正则","slug":"learn-regex-in-30-minutes","date":"2017-11-29T05:56:18.000Z","updated":"2017-12-01T12:18:41.000Z","comments":true,"path":"2017/11/29/learn-regex-in-30-minutes/","link":"","permalink":"http://von.sh/2017/11/29/learn-regex-in-30-minutes/","excerpt":"有一些人,遇到一个问题时就想:“我知道,我会使用正则表达式。” 然后他就有两个问题了。–by Jamie Zawinski","text":"有一些人,遇到一个问题时就想:“我知道,我会使用正则表达式。” 然后他就有两个问题了。–by Jamie Zawinski 前些天优化一个 API 的时候,我在代码中发现一个正则就在群里随便乱吐槽了几句。然后leader就突然兴起让我给团队内来一场正则表达式的分享。对了,我吐槽的这个正则长这样: 1^[a-z]([a-z0-9]*[-_\\.]?[a-z0-9]+)*@([a-z0-9]*[-_]?[a-z0-9]+)+[\\.][a-z]{2,3}([\\.][a-z]{2})?$ 当时我只说写这么长且无用的正则,简直了…. 各位知道这个正则是做什么的么? 各位知道当匹配[email protected]这种会有多少次回溯么? 序文本处理工具,大家想到的绝对会有正则表达式(Regular Expressions)这个大佬,在当今的编程语言中,也找不出几个不支持正则的(有,当然有,比如 brainfuck) ,我周围的程序员也没有没接触过正则的。不过,接触了不代表就懂正则了。 “我也不知道它是怎么工作的,反正跑起来正常就行了”–by 匿名用户 其实,正则表达式并没有那么那么的晦涩难懂,高大上,它不过是一个普通的 DSL 罢了,复杂的文本处理,也不应该用正则,而应该考虑比如 flex/bison 这种 parser 了。 正则基础什么是正则表达式?Regular Expression,简称 RE/Regex。用于匹配字符串中字符组合的模式。我们可以通过认识集合中有限的字符(正则表达式中的元字符)来识别属于这种集合的任意多的其他字符。这样来讲,其实你之前已经接触过正则了,只是你不知道而已。比如: Ctrl + F 字符串子串匹配问题, 本质上也是正则。 echo “shellvon” | grep von 我们在命令行下经常使用的 grep,其实你的 von 也算正则了… 可是当我们要匹配的都是数字或者都是字母的时候。或者100个相同字母的时候,写起来就啰嗦复杂了。也谈不上模式!所以呢,我们引入了一种这样功能的字符: 自己不代表自己原本含义,而都有特殊含义的字符。比如,我希望点号 .可以匹配任意字符,具有这种功能的字符,我们称之为 元字符(metacharacters) 元字符生活中,我们也早已经接触过所谓的元字符了。比如: 互联网脏话 -> 用*给替代了 二胖Plus、鑫胖、金三月半、鑫月半、金三肥 -> 真实含义代表啥大家也知道… 正则表达式里面有许多这样的元字符,这些元字符构成一个有限子集,让我们可以利用这个有限子集去识别和匹配无限可能的文本。 元字符 含义 ^和$ 行的开始^和结束$ []和[^] 字符组[]和排除型[^] - 连字符,必须出现在字符组内,表示范围比如[0-9] . 匹配任意字符(可能会排除\\n哦) *和+ 量词,匹配该字符之前的字符,*是>=0次,+是>=1次 {n}和{n,}和{n,m} 区间量词,匹配次数{n}匹配n次,{n,m}匹配n~m次 | 或者 ? 量词,标记?前的字符为可选 () 字符集,标记完全匹配括号内的数据 \\ 转义,用于匹配一些保留字符(比如元字符) 如上表,这些字符出现在正则里面的时候,通常都不代表本来的含义。关于上表,还有几点需要注意: 脱字符^必须出现在文本开头,美元符$必须出现在行尾 排除形里面也出现了脱字符^,代表排除时也必须出现在中括号[]内的第一个字符,否则不具有此含义。 连字符,顾名思义,必须具有起止字符,否则就是自己本来的意思。比如[-]代表匹配自己,特殊一点的是[a-c-d]由于a-c已经表示了范围,后面的-就找不到开始范围,那么第二个-就代表自己,所以匹配的是abc-d这5个字符。 我们来看元字符的一些例子:123456789"[Tt]he" => The car parked in the garage ^^^ ^^^"[^c]ar" => The car parked in the garage ^^^ ^^^"c.+t" => The fat cat sat on the mat ^^^^^^^^^^^^^^^^^^ ¦----被(.+)匹配-¦"b.g" => baidugoogle big bug bag ^^^ ^^^ ^^^ 如你所见,我们正则的表达式匹配流程会是从左边往右边挨个来,就像你读本文如此。这些元字符可不简单,万丈高楼也是靠它们。 子表达式在算法领域有个很好的思想叫做 分治法 。当我们遇到一个复杂的需求,我们总是可以拆解成无数个小的问题,然后尝试去解决小问题,最后这些小问题的答案来解决大问题。正则表达式亦是如此。一个复杂的正则表达式都是由一些简单的正则表达式组合起来的。这些简单的正则我们称之为 子表达式 。它们通常是指用 | 分割的多选分支或者括号内的表达式。比如gr(e|a)y。我们可以将(e|a)看成子表达式,其中e和a也可以看成子表达式,严格意义上讲,上述的g/r/y等也算子表达式。不过这样太细了意义也不大。我们主要还是关注竖线|分割和括号()包围起来的更有意思。首先,考虑这样一个问题:如果需要让你用刚学的元字符去匹配一个0-23的范围的数字(比如时间刚好这个范围)怎么办?哦,天啦,你可能会说[0-23] 刚好啊,因为-可以表示范围,很遗憾,并不行,通常-能表示的开始和结束都只有一个字符(虽然16进制这种就不是了),但是你要知道,上述表达式的真实含义是匹配[0123]这四个数字。这个可以参见我前文提到的关于连字符-的注意事项。这种方式不可行,另外一个最容易想到的就是拆分它,我们可以利用子表达式这个概念。怎么拆呢? 1. `[0-9]` 可以匹配范围个位数 2. `1[0-9]` 可以匹配10-19的 3. `2[0-3]` 可以匹配20-23的 显然,最后用元字符|(代表或)将他们连接起来就符合要求了撒。 如果更加复杂一点呢,比如要匹配一个 IPv4 地址呢?其实原理一样,只是数字范围变成了0-255。这正如文章开头的图中所示。 更为复杂的就是,考虑匹配一个日期,需要考虑月份和天数的关系,甚至还有闰年(通常这种时候你还要用正则的话,你估计已经开始过度使用正则了,你需要避免这样) 简写懒惰是程序员的美德之一。在上面的例子中我们写[0-9]要写5个字符,有没有更简写的呢? 简写字符 含义 . 匹配任意字符 \\d和\\D \\d表示数字,\\D非数字 \\w和\\W \\w等效于[a-zA-Z0-9_],\\W相反 \\s和\\S \\s匹配所有空格字符,\\S相反 举个例子:123456"\\d{2,3}" => The number was 9.9997. ^^^"car\\sis" => The car is parked. ^^^^^^"^\\d+" => 13 is less than 18. ^^ 捕获,非捕获与反向引用目前为止,我们知道括号可以用在子表达式中,表示完整的一个整体。括号另外还有一个作用是 捕获 。所谓捕获,就是指正则引擎(正则的大脑)可以记住匹配的结果,会给这些结果取一个小名,用于之后的用途,取名规则是这样的:每个括号从左向右以左括号为标志,会自动拥有一个组号,从1开始。比如: (hello (world (regex)))分组如下: 1. hello world regex <-- 第一个组,组号为1,后面以此类推 2. world regex <-- 第二个组 3. regex <-- 第三个组,因为左括号出现的比较晚 这样有个好处,我们可以在正则里面通过组号拿之前匹配的结果,这种方式叫做 反向引用 。 举个例子:123“([ab])\\1” => abcdebbcde ^^ ¦-----这个`b`就是因为前面的([ab])匹配到了一个字符b。我们可以用\\1来引用起之前结果 显然,有时候我们并不需要捕获(不然数字一直在增大的同时,让正则脑袋记那么多内容,效率也不好),所以与之对应的还有 非捕获。写法很相似:(?:pattern)。一个更具体的对比表如下: 语法 含义 例子 (pattern) 匹配pattern并捕获结果,自动设置组号 (\\d{2})+\\d (?:pattern) 匹配pattern,但是不捕获 hell(?:o|y) 匹配hello/helly 显然,如果一个复杂的表达式里面引入过多的非捕获,虽然可以减少正则引擎去记忆,但是这也会增加我们阅读正则表达式的难度,所以,请深思熟虑是否有必要为了这么一点性能而用非捕获。另一方面,在复杂的正则表达式中如果全部使用了捕获,试想一下这种情况:有天你突然把某个括号删除了,或者需要在中间某处增加一个括号,会产生什么样的影响?聪明的你应该想到了,会导致组号的变更。比如原来是\\3,现在可能变成了\\2或者\\4,或者其他。所以捕获如果按照数字取名字实在不是很好。于是写 Python 正则的那群人说,要不给这些组取名字的事情交给用户吧,比如叫张三,李四。这种技术叫做命名捕获。大概是这么写的(?P<myname>hello) 它不仅具有原来数字的乳名\\1,还有用户你自己为它取的名字myname。这种方式很棒,.Net觉得不错也抄袭了过去,只不过语法不一样罢了。更多的细节请参见:Named Capturing Groups and Backreferences 正则进阶之前基础部分已经可以完成日常简单的需求了,甚至我们仔细想想可以知道正则最重要的两点原则: 匹配先从需要查找的字符串的起始位置尝试匹配,是从左至右的。 总是尝试匹配尽可能多的字符,我们称之为贪婪。 第一点:比如”Python”匹配 “i am Pythoner, i love Python.”; 正则匹配到 _Python_er, 而不是后面的 Python 。因为他是左边开始的,先找到谁就是谁。 第二点: 贪婪(Greedy),我们的所有量词都是尽可能多的匹配! 贪婪与非贪婪还记得这个例子么?123"c.+t" => The fat cat sat on the mat ^^^^^^^^^^^^^^^^^^ ¦----被(.+)匹配-¦ 我们来描述一下这个流程: 正则表达式的c开始不能匹配’T’,直到遇到单词’cat’中的’c’,于是宣告’c’匹配成功。开始尝试.来匹配后续 正则表达式中的.开始匹配单词’cat’中的’.’,发现还是可以匹配。 有趣的事情来了: 正则发现.后面有个+,即告诉我吃至少一个,那么我怎么抉择呢?答案是:我尽可能吃撑,吃到吃不下去为止–这就是贪婪。 于是.继续匹配,一路所向披靡,因为.啥字符都可以吃啊,然后走到了最后,单词’mat’中’t’的后面为止。发现没有匹配了,点号吃不下去了,就该让表达式c.+t中的t上场,发现t也不行,怎么办呢?难道宣告正则匹配失败??不不不,毕竟+表示的1到多嘛,我可以通知.吐一个字符出来,让t试试呢 于是.就吐出来了’t’,记住这时候.+已经吃下了’at sat on the ma’这么多字符。 这时候发现t可以匹配’mat’中的’t’.这时候文本也是结束了,正则引擎宣告匹配成功! 这就是贪婪,.+企图吃掉所有字符,可惜最后匹配失败,它很不情愿的吐出了最后一个字符’t’,然后才让正则匹配成功的。 试想,如果.+不贪婪,很谦虚,那么我可以只吃一个字符’a’,让t取匹配’cat’中’t’.结果也会成功。但是却贪婪了。。贪婪与否会严重影响正则匹配的结果 所以用户可能会问了:我怎么告诉正则引擎不贪婪呢?我怎么告诉正则引擎如果我贪婪了,我也不想吐出来呢?(比贪婪还贪婪) 这其实都很简单:在原来标准量词的后面加一个问号,即为忽略优先量词在原来标准量词的后面加一个加号,即为占有优先量词 语法 含义 忽略优先 *?,+? ,?? ,{n,m}? 占有优先 *+,++,?+,{n,m}+ 我们看一下这个例子:1234567891011".*shell" => shellxxxxxshell ^^^^^^^^^^^^^^^ ¦----- 之前的shellxxxxx都被.*吃了,这种情况参见上一个关于贪婪的描述 ".*?shell" => shellxxxxxshell ^^^^^ ¦---.*什么都不要,因为?告诉它忽略优先".*+shell" => shellxxxxxshell什么都匹配不到,因为.*已经可以贪婪的吃掉整句话,有了+表示占有优先,吃了是不会吐出来的,所以啥都吃完了,也没有字符留给正则里面的shell来匹配了。导致整个匹配都会失败 调试这个可能是最重要的一个环节。工欲善其事,必先利其器。调试正则的话,也需要一个特别好的工具,我这里推荐在线的工具:https://regex101.com/一两句无法说清,所以试图以图的形式简单介绍一下。 具体学习,还是请以 Learn by Practice 为准。而不是 Learn by manual。 环视前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(使用[^])。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?这时候我们需要查找它所在的位置!正则表达式里面匹配位置的在我们之前接触过的有^和$,前者是开始,后者是结束。 语法 匹配条件 例子 (?<=…) 子表达式能匹配左侧文本 (?<=[a-z])[A-Z] (?<!…) 子表达式不能匹配左侧文本 (?=…) 子表达式能匹配右侧文本 (?=[a-z])[A-Z] (?!…) 子表达式不能匹配右侧文本 \\d{3}(?!\\d) 具体位置的举例如下: 123456(?=\\d) (?<=\\d) 3hellvon 3 hellvon ^ ^ ¦ ¦---找到位置了,左侧能匹配\\d, ¦ 找到位置了,右侧能匹配\\d,所以找到的位置在这里 环视功能可以做很多有趣的事情:比如匹配标签: (?<=<(\\w{3})>).*(?=<\\/\\1>)比如判断密码的强度: 123456^(?=.*[0-9]) <-- 右侧必须有数字 (?=.*[a-z]) <-- 右侧必须有小写字母 (?=.*[A-Z]) <-- 右侧必须有大写字母 (?=.*[@#$%^&+=]) <-- 右侧必须要有特殊字符 (?=\\S+$).{10,} <-- 必须是非空格,且10位以上.$ 其他特性正则里面有很多重要的特性,限于30分钟和篇幅原因,我无法都写上,我提一下一些概念,有兴趣的同学可以自行检索关键字。 正则表达式引擎(流派) 比如NFA/DFA. 递归的正则表达式 正则回溯 宽松模式 条件判断 正则本地化(locale) 以及Unicode 模式修饰符(re pattern modifiers) 参考资料 杂项 https://stackoverflow.com/a/22944075 书籍 Mastering Regular Expressions PCRE https://www.pcre.org/ 手册 http://www.rexegg.com 手册 https://www.regular-expressions.info/ man pcre (Mac/Linux下) 调试 https://regex101.com/ 可视化 https://www.debuggex.com/ PHP 项目 FastRoute (应用正则很好的例子) Python 项目 Bottle (应用正则很好的例子) Markdown Parser","categories":[{"name":"技术","slug":"技术","permalink":"http://von.sh/categories/技术/"}],"tags":[{"name":"Regex","slug":"Regex","permalink":"http://von.sh/tags/Regex/"},{"name":"文本处理","slug":"文本处理","permalink":"http://von.sh/tags/文本处理/"}]}]}