-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathatom.xml
121 lines (79 loc) · 88 KB
/
atom.xml
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>shellvon的博客</title>
<icon>https://www.gravatar.com/avatar/c5accc267970a87c9990fb799670ffd0</icon>
<subtitle>菜鸟一枚</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://von.sh/"/>
<updated>2017-12-08T03:02:00.000Z</updated>
<id>http://von.sh/</id>
<author>
<name>shellvon</name>
<email>iamshellvon@gmail.com</email>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>谈谈爬虫与反爬虫</title>
<link href="http://von.sh/2017/12/08/talk-about-spider-and-anti-spider/"/>
<id>http://von.sh/2017/12/08/talk-about-spider-and-anti-spider/</id>
<published>2017-12-08T02:30:31.000Z</published>
<updated>2017-12-08T03:02:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近同事在群里发了这么一个<a href="https://github.com/xiyouMc/WebHubBot" target="_blank" rel="noopener">爬虫地址</a> (未满18岁慎入),不得不佩服 Scrapy 的强大,但是这让我想到了另外一个问题,PornHub 可以估计随便爬,但是比如像淘宝/亚马逊的商品信息可是一个公司至关重要的数据,显然舍不得让爬虫随便来爬。这就涉及到<strong>爬虫与反爬虫的策略问题</strong>。<br><img src="/assets/img/js-encryption.png" alt="JS 加密混淆"></p><a id="more"></a><h1 id="爬虫"><a href="#爬虫" class="headerlink" title="爬虫"></a>爬虫</h1><p>爬虫的目的是要尽可能的模拟人的行为,所以越接近人类行为越不容易被发现,很多基础的问题是必须先考虑的。比如浏览器的 <a href="https://www.wikiwand.com/zh-hans/%E7%94%A8%E6%88%B7%E4%BB%A3%E7%90%86" target="_blank" rel="noopener">User-Agent</a> 和 防止盗链用的 <a href="https://www.wikiwand.com/zh-hans/HTTP%E5%8F%83%E7%85%A7%E4%BD%8D%E5%9D%80" target="_blank" rel="noopener">Referer</a>。鄙人没有写多少爬虫,简单总结一下我在写爬虫的一些经验和技巧吧。</p><ul><li>灵活的请求头<ul><li>不一样非要 Chrome 或者 Firefox, 有时候伪装成知名的爬虫的请求头也许反而更好。比如使用<code>BaiduSpider+</code>模拟百度。 </li></ul></li><li>优先尝试移动端<ul><li>通常移动端的反爬虫策略会比较简单,所以更加容易绕过</li></ul></li><li>尝试使用代理<ul><li>很多比如 IP 限制的,我们可能可以使用 X-Forward-For 就搞定了,但更多的时候我们需要动态的IP库</li><li>尝试使用 <a href="https://www.wikiwand.com/zh/%E6%B4%8B%E8%91%B1%E8%B7%AF%E7%94%B1%E5%99%A8" target="_blank" rel="noopener">Tor</a> 代理服务器</li><li>有时候可以看看万能的淘宝</li></ul></li><li>需要节制而且友好一点<ul><li>所谓节制,是需要注意自己爬虫的请求频率,我个人喜欢随机休眠以继续,不要以固定频率去做</li><li>所谓友好,就是爬虫抓取之前可以看看 <a href="https://www.wikiwand.com/zh-hans/Robots.txt" target="_blank" rel="noopener">robots.txt</a> 另外,注意节制也算友好,因为不至于给目标服务器带来过大压力</li></ul></li><li>注意爬取顺序<ul><li>一般我们分为 DFS 和 BFS,在爬取的时候如何最快程度找到自己需要的内容的时候,这需要考虑。</li><li>URL 去重 ?</li></ul></li><li>善于用库<ul><li>比如 Python 的 <a href="https://scrapy.org/" target="_blank" rel="noopener">Scrapy</a> 和 Java 的 <a href="https://github.com/yasserg/crawler4j" target="_blank" rel="noopener">crawler4j</a></li><li>涉及到模拟浏览器的库, 比如 <a href="https://developers.google.com/web/updates/2017/04/headless-chrome" target="_blank" rel="noopener">Headless Chrome</a> 和 <a href="http://phantomjs.org/" target="_blank" rel="noopener">PhantomJS</a>,为自动化测试而出来的 <a href="http://www.seleniumhq.org/" target="_blank" rel="noopener">Selenium</a></li><li>比如使用 OCR 技术用于验证码识别的 <a href="https://github.com/tesseract-ocr/tesseract" target="_blank" rel="noopener">teseract</a>,复杂一点的验证码可能需要一些矫正技术和 AI 算法</li><li>这里的库还可以考虑一些在线提供的免费API,比如 <a href="https://www.apify.com/" target="_blank" rel="noopener">apify</a></li></ul></li><li>好好利用 <a href="https://developers.google.com/web/tools/chrome-devtools/?hl=zh-cn" target="_blank" rel="noopener">Chrome DevTools</a> 工具 和一些抓包工具比如 <a href="https://www.charlesproxy.com/" target="_blank" rel="noopener">Charles</a></li><li>移植性和扩张性<ul><li>当然,也包括了如何让自己的代码一次写了,到处跑(Java 口号)。最大程度复用哦。</li></ul></li><li>善于使用 Google / Github ,而不是百度<ul><li>比如你可能需要了解反爬虫技术(即所谓的反反爬虫)</li><li>Xpath Or Regex 等等技术</li></ul></li><li>知己知彼<ul><li>多看看一些反爬虫策略,才能更好的反反爬虫。</li></ul></li></ul><ul><li>可以利用多账号进行爬取<ul><li>比如临时邮箱 <a href="https://10minutemail.net/" target="_blank" rel="noopener">https://10minutemail.net/</a></li></ul></li><li>看一下优秀的开源爬虫的设计思路架构模式<ul><li>始终保持学习的心态去学</li></ul></li><li>注意一些陷阱<ul><li>比如 <a href="https://github.com/abdulfatir/ZipBomb" target="_blank" rel="noopener">Zip炸弹攻击</a></li><li>蜜罐技术</li></ul></li></ul><p>当然,我不是一个专业写爬虫的,只是小打小闹过,以上的内容纯属个人经验,很多没提到的需要自己总结和网上多学习。</p><h1 id="反爬虫"><a href="#反爬虫" class="headerlink" title="反爬虫"></a>反爬虫</h1><p>大多数时候,爬虫都挺讨厌的,所以有了<strong>反爬虫</strong>,遗憾的是,目前没有任何技术可以绝对或者完美的方式反爬虫。这里我主要说一些常见的反爬虫思路,有些甚至在我第一次知道的时候觉得脑洞之大。</p><ul><li><p>请求头的检查</p><p>比如 User-Agent 或者 Referer,甚至 Host,前不久和同事利用 Chrome 的 <a href="https://developer.chrome.com/extensions/webRequest" target="_blank" rel="noopener">WebRequest</a> 结合 油猴脚本 就利用请求头搞了一些事。<strong>这里需要注意的是,各种请求头,都不可信</strong>。</p></li></ul><ul><li><p>IP 频率限制</p><p>这种方式是基本的一个Rate Limiter,我们在设计后端 API 的时候也经常会遇到。比如:</p><ul><li>基于 Nginx 扩展的 <a href="http://nginx.org/en/docs/http/ngx_http_limit_req_module.html" target="_blank" rel="noopener">ngx_http_limit_req_module</a></li><li>基于 Redis 的 <a href="http://redis.io/commands/INCR#pattern-rate-limiter" target="_blank" rel="noopener">INCR 命令</a></li><li>漏桶算法和令牌桶算法</li></ul></li><li><p>使用授权</p><ul><li>比如需要登陆注册的,或者使用黑白名单。</li></ul></li><li><p>验证码</p><p>需要注意的是,有些验证码实现实在太low了,有的却逆天的困难,比如12306的验证码。这时候还得考虑要不要接入黑产(比如<a href="http://soft.xingjk.cn/" target="_blank" rel="noopener">这种地方</a>)。实现一个验证码至少应该考虑这些问题:</p><ul><li>一个验证码只能用一次,而且有有效期</li><li>不要把 <code>0</code>/<code>o</code>和<code>l</code>/<code>1</code>这种不容易区分的放一起。</li><li>字符个数/字符倾斜的位置最好别固定,不然一个简单的 OCR 轻松可以搞定</li></ul><p>不过现在啥技术都是云,我的博客里面的搜素,统计,CDN都是第三方服务。显然验证码也可以使用各种平台的。</p></li><li><p>检查爬虫的一些特征值</p><p>前面提到的比如请求头算,但是还有一些其他的。比如淘宝首页,他会检查自己的 JS 执行环境是不是使用了前文提到的<code>PhantomJS</code>之类的玩意儿。因为这些库都有他自己的一些特征:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 各种模拟器的check</span></span><br><span class="line"><span class="comment">//代码来自:</span></span><br><span class="line"><span class="comment">//https://github.com/GangZhuo/BaiduPCS/blob/cda508bf87a433a2dfc5938008f6f0447e698b36/pcs/pcs_passport_dv.c#L682-L693 </span></span><br><span class="line"><span class="keyword">var</span> a = [</span><br><span class="line"> (<span class="built_in">window</span>[<span class="string">'phantom'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">window</span>[<span class="string">'_phantom'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">window</span>[<span class="string">'callPhantom'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">window</span>[<span class="string">'__fxdriver_unwrapped'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">window</span>[<span class="string">'fxdriver_id'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">document</span>.getElementsByTagName(<span class="string">'html'</span>)[<span class="number">0</span>].getAttribute(<span class="string">'webdriver'</span>) == <span class="literal">null</span> ? <span class="number">0</span> : <span class="number">1</span>).toString(),</span><br><span class="line"> (<span class="built_in">document</span>[<span class="string">'$cdc_asdjflasutopfhvcZLmcfl_'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">document</span>[<span class="string">'__webdriver_script_fn'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">window</span>[<span class="string">'webdriver'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString(),</span><br><span class="line"> (<span class="built_in">window</span>[<span class="string">'ClientUtils'</span>] ? <span class="number">1</span> : <span class="number">0</span>).toString()</span><br><span class="line"> ];</span><br></pre></td></tr></table></figure></li><li><p>前文提到的 Zip 炸弹,或者使用 iframe。</p></li><li><p>阻止 copy && paste 事件 (😂,没错,没有版权意识的人类也是可怕的爬虫)</p></li><li><p>直接将文字替换为图的形式</p><ul><li>显然,对方不得不使用 OCR ,可增加技术成本。</li></ul></li><li><p>安全性考虑的还有</p><ul><li>经常性的变更自己的 HTML 内容</li><li>API 接口不要返回太多东西</li><li>使用 Ajax </li><li>…. </li></ul></li></ul><p>在 Github 上还有一篇文章写的不错 <a href="https://github.com/JonasCz/How-To-Prevent-Scraping" target="_blank" rel="noopener">How to Prevent Scraping</a>。也可以看看。</p><p>接下来我说一下我遇到的一些优秀的反爬虫思路。</p><ul><li><p>利用 web font 做 Unicode 映射</p><ul><li>文章参见: <a href="http://www.freebuf.com/news/140965.html" target="_blank" rel="noopener">反击“猫眼电影”网站的反爬虫策略</a></li></ul><p>这个实现成本其实很简单,对于爬虫来说,如果知道映射规则也只是做一个table替换罢了。而且,通常情况下,对于中文的防采集不适合这种方式,因为中文的字体库太大了。所以通常都是数字,英文则适合使用此方法实现防采集。</p><ul><li>具体实现方式</li></ul><p><a href="https://icomoon.io/app/#/select" target="_blank" rel="noopener">icomoon</a> 上可以自己做字体和 Unicode 映射,在这之前我们只需要在网上随便下载一些常见的字体导入即可。</p><p>因为导入的时候仅支持<code>svg</code>和<code>json</code>,所以我们下载的比如<code>ttf</code>需要转化成<code>svg</code>, 我们需要在另外的网站上转化即可,比如这 <a href="https://everythingfonts.com/" target="_blank" rel="noopener">Everything Fonts</a></p><p>制作好之后这还需要后端在输出的时候做好配合。其实就是个字典映射罢了,关键是这个映射规则尽可能复杂一些才好。</p><p>我个人觉得可以后端可以随机加载不同的字体,里面的映射关系都不一样。设置比如把1->3, 7->2 这种,都是字,但不是原来含义才容易让人迷惑。</p></li><li><p>利用视觉误差</p><p>这一点在刚说的 <a href="https://github.com/JonasCz/How-To-Prevent-Scraping" target="_blank" rel="noopener">How to Prevent Scraping</a> 内有提到过:</p><blockquote><h3 id="Screw-with-the-scraper-Insert-fake-invisible-honeypot-data-into-your-page"><a href="#Screw-with-the-scraper-Insert-fake-invisible-honeypot-data-into-your-page" class="headerlink" title="Screw with the scraper: Insert fake, invisible honeypot data into your page"></a>Screw with the scraper: Insert fake, invisible honeypot data into your page</h3><p>Adding 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:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><div class="search-result" style="display:none"></span><br><span class="line"> <h3 class="search-result-title">This search result is here to prevent scraping</h3></span><br><span class="line"> <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 :-)</span><br><span class="line"> Note that clicking the link below will block access to this site for 24 hours.</p></span><br><span class="line"> <a class"search-result-link" href="/scrapertrap/scrapertrap.php">I'm a scraper !</a></span><br><span class="line"></div></span><br><span class="line">(The actual, real, search results follow.)</span><br></pre></td></tr></table></figure></blockquote><p>即我们可以利用 CSS 的样式来完成一些有趣的事情,让爬虫看到的和用户看到的不是同样的数据(建议看完上面的英文版原文,不然不会知道为啥) 除了英文中提到的,还可以利用一样的原理来做这些事情:</p><ul><li>每个几个字符之间随机插入一些没啥用的标签,然后把这些标签的style全部设置为display: none; 这样对用户来说看到的东西一样,但是呢,爬虫无论用正则还是用Xpath或者怎么样,都增加了其提取难度。哦,我最近看到了这样的东西对这种策略应该有点用:<a href="https://github.com/mozilla/readability" target="_blank" rel="noopener">mozilla/readability</a></li><li>如果能确定是爬虫,那么后端不要报错,而是随机生成数据返回。我没记错的话,当初的亚马逊就是这样的。</li></ul></li><li><p>JS 混淆<br>这个大多数人都做了,但是混淆的好坏也是不一样的,举例来讲,图片的 JS 代码压缩混淆等,我们有很多好的工具可以使用:</p><ul><li><a href="https://tool.lu/js" target="_blank" rel="noopener">Javascript在线解压缩 - 在线工具</a></li><li><a href="http://jsbeautifier.org/" target="_blank" rel="noopener">jsbeautifier</a></li><li>甚至还有基于机器学习的 <a href="http://jsnice.org/" target="_blank" rel="noopener">JSNice</a></li></ul><p>我见过的比较好的混淆的例子有 <a href="http://wsjs.saic.gov.cn/" target="_blank" rel="noopener">商标网上检索系统</a>,他的代码简直丧心病狂,可以文章首图或者参见这里:<a href="http://wsjs.saic.gov.cn/tmrp/js/main.js?1.127&D9PVtGL=803ba5。" target="_blank" rel="noopener">http://wsjs.saic.gov.cn/tmrp/js/main.js?1.127&D9PVtGL=803ba5。</a><br>可惜美中不足的是,我们只需要替换他的queryString就可以看到源代码了。比如上述链接换成:<br><a href="http://wsjs.saic.gov.cn/tmrp/js/main.js?1.127" target="_blank" rel="noopener">http://wsjs.saic.gov.cn/tmrp/js/main.js?1.127</a> 即可完整查看。</p></li></ul><h1 id="尾声"><a href="#尾声" class="headerlink" title="尾声"></a>尾声</h1><p>鄙人的爬虫经验基本算是无,所有很多内容可能理解错了或者没有考虑到的,希望有了解的朋友可以给我留一下言。最后,谢谢你耐心的看完此篇文章🙏。</p>]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近同事在群里发了这么一个<a href="https://github.com/xiyouMc/WebHubBot" target="_blank" rel="noopener">爬虫地址</a> (未满18岁慎入),不得不佩服 Scrapy 的强大,但是这让我想到了另外一个问题,PornHub 可以估计随便爬,但是比如像淘宝/亚马逊的商品信息可是一个公司至关重要的数据,显然舍不得让爬虫随便来爬。这就涉及到<strong>爬虫与反爬虫的策略问题</strong>。<br><img src="/assets/img/js-encryption.png" alt="JS 加密混淆"></p>
</summary>
<category term="技术" scheme="http://von.sh/categories/%E6%8A%80%E6%9C%AF/"/>
<category term="爬虫" scheme="http://von.sh/tags/%E7%88%AC%E8%99%AB/"/>
</entry>
<entry>
<title>PHP Generator 笔记</title>
<link href="http://von.sh/2017/12/06/php-generator-note/"/>
<id>http://von.sh/2017/12/06/php-generator-note/</id>
<published>2017-12-06T08:55:51.000Z</published>
<updated>2017-12-06T12:39:12.000Z</updated>
<content type="html"><![CDATA[<p><img src="/assets/img/generator-note.svg" alt="PHP Generator 笔记"></p><blockquote><p>图片来源 <a href="https://wpengine.com/try/php7-hosting/" target="_blank" rel="noopener">https://wpengine.com/try/php7-hosting/</a></p></blockquote><h1 id="序"><a href="#序" class="headerlink" title="序"></a>序</h1><p>先说,标题虽然是《Generator 笔记》,但实际上本文会主要内容会是<code>yield</code>。</p><p>以鄙人的拙见,目前大多数 PHPer 对 PHP 的 <code>yield</code> 关键字并不怎么了解,但实际上这却是一块非常值得学习的地方,至少于我而言如此。<br><code>yield</code> 为 PHP 引入了<strong>生成器</strong>,<strong>协程</strong> 等一些复杂概念,导致入门门槛也挺高。</p><p>关于学不学<code>yield</code>, 目前,我遇到过以下几类人:<br><a id="more"></a></p><ul><li>学个 PHP 都是为了简单快速搞东西上线,不用这玩意也能满足老板需求(<strong>不想学</strong>)</li><li>自己写的代码也遇不到啥问题非要<code>yield</code>搞定的( <strong>没有应用场景</strong>)</li><li><code>yield</code> 带来了<strong>生成器</strong>,<strong>协程</strong>,<strong>异步</strong>等一系列<code>复杂性</code>概念,文档少的可怜,(<strong>门槛高</strong>)</li><li>理解协程的又难免会和 Golang 的 <a href="https://tour.golang.org/concurrency/1" target="_blank" rel="noopener">goroutines</a> 做对比。(<strong>不屑于学</strong>)</li></ul><p>我算第三类,<strong>文档少的可怜</strong>。当然,还有就是自己的聪明程度不够。<br>关于 PHP <code>yield</code>这个,我尝试看了许多博文,可按照国内的技术尿性,大家的博文内容都写的是一样的。也不知道啥时候搜索引擎可以帮我智能去重就好了。值得庆幸的是,Google 还是给了我一些不错的文章:</p><ul><li><a href="https://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html" target="_blank" rel="noopener">Cooperative-multitasking-using-coroutines-in-PHP</a><ul><li>中文: <a href="http://www.laruence.com/2015/05/28/3038.html" target="_blank" rel="noopener">在PHP中使用协程实现多任务调度</a></li></ul></li><li><a href="https://blog.ircmaxell.com/2012/07/what-generators-can-do-for-you.html" target="_blank" rel="noopener">What-generators-can-do-for-you</a></li></ul><p>当然官方文档也是不错的内容: <a href="https://wiki.php.net/rfc/generators" target="_blank" rel="noopener">RFC:generators</a> 和 <a href="http://php.net/manual/zh/language.generators.overview.php" target="_blank" rel="noopener">PHP 手册 > 语言参考 > 生成器</a></p><p>由于PHP的资料太少,我反其道而行之,选择了去学习 <code>Python</code> 的生成器。关于 Python 的生成器,我主要看了大神 <a href="http://www.dabeaz.com/tutorials.html" target="_blank" rel="noopener">Dabeaz</a> 的几个 PPT,都可以在他博客里面找到。本人的笔记,全部借(cao)鉴(xi)自此大神的 PPT。</p><h1 id="yield的概念"><a href="#yield的概念" class="headerlink" title="yield的概念"></a>yield的概念</h1><h2 id="迭代器协议"><a href="#迭代器协议" class="headerlink" title="迭代器协议"></a>迭代器协议</h2><p>在 PHP 中, 可以实现 <a href="http://php.net/manual/zh/language.oop5.interfaces.php" target="_blank" rel="noopener">Iterator 接口</a>, 从而可以让对象自行决定如何遍历以及每次遍历时哪些值可用。这样的接口,我们称之为迭代器协议 (Iteration Protocol) 。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Iterator extends Traversable {</span><br><span class="line"><span class="comment">/* 方法 */</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">public</span> mixed current ( void ) <span class="comment">// 返回当前元素</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">public</span> scalar key ( void ) <span class="comment">// 返回当前元素的键</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">public</span> void next ( void ) <span class="comment">// 向前移动到下一个元素</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">public</span> void rewind ( void ) <span class="comment">// 返回到迭代器的第一个元素</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">public</span> boolean valid ( void ) <span class="comment">// 检查当前位置是否有效</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><a href="http://php.net/manual/zh/class.generator.php" target="_blank" rel="noopener">生成器 Generator</a> 就是是实现了迭代器协议的一种对象。这种对象比较特殊:</p><ol><li>无法直接使用 new 实例化</li><li>得使用 <strong>yield</strong> 产生</li><li>可以通过<code>send</code>方法传值给yield所在位置</li></ol><p>为什么需要引入关键字 <code>yield</code> 呢?</p><p>我们先来看一个例子,需要读取一个文件每一行出来:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用数组.</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getLines</span><span class="params">($file)</span> </span>{</span><br><span class="line"> $f = fopen($file, <span class="string">'r'</span>);</span><br><span class="line"> $lines = [];</span><br><span class="line"> <span class="keyword">if</span> (!$f) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="keyword">Exception</span>();</span><br><span class="line"> <span class="keyword">while</span> (<span class="keyword">false</span> !== $line = fgets($f)) {</span><br><span class="line"> $lines[] = $line;</span><br><span class="line"> }</span><br><span class="line"> fclose($f);</span><br><span class="line"> <span class="keyword">return</span> $lines;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述代码有个明显的问题:如果当文件足够大的情况下,数组<code>$lines</code>会特别大,内存随时会跪。</p><p>幸运的是,我们可以使用迭代器,一行一行的读,参见<a href="https://blog.ircmaxell.com/2012/07/what-generators-can-do-for-you.html" target="_blank" rel="noopener">这里</a>的例子: </p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 自己实现文件遍历</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FileIterator</span> <span class="keyword">implements</span> <span class="title">Iterator</span> </span>{</span><br><span class="line"> <span class="keyword">protected</span> $f;</span><br><span class="line"> <span class="keyword">protected</span> $data;</span><br><span class="line"> <span class="keyword">protected</span> $key;</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">($file)</span> </span>{</span><br><span class="line"> <span class="keyword">$this</span>->f = fopen($file, <span class="string">'r'</span>);</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">$this</span>->f) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="keyword">Exception</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__destruct</span><span class="params">()</span> </span>{</span><br><span class="line"> fclose(<span class="keyword">$this</span>->f);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">current</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">$this</span>->data;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">key</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">$this</span>->key;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">next</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">$this</span>->data = fgets(<span class="keyword">$this</span>->f);</span><br><span class="line"> <span class="keyword">$this</span>->key++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">rewind</span><span class="params">()</span> </span>{</span><br><span class="line"> fseek(<span class="keyword">$this</span>->f, <span class="number">0</span>);</span><br><span class="line"> <span class="keyword">$this</span>->data = fgets(<span class="keyword">$this</span>->f);</span><br><span class="line"> <span class="keyword">$this</span>->key = <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">valid</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span> !== <span class="keyword">$this</span>->data;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如你所见,虽然这样解决了内存问题,但是代码复杂的多了。于是乎,<code>Generator</code>也上线了,一个函数里面只要有<code>yield</code>关键字,就是<code>Generator</code>,它实现了迭代器协议。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用 yield 完成相同功能</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getLines</span><span class="params">($file)</span> </span>{</span><br><span class="line"> $f = fopen($file, <span class="string">'r'</span>);</span><br><span class="line"> <span class="keyword">if</span> (!$f) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="keyword">Exception</span>();</span><br><span class="line"> <span class="keyword">while</span> ($line = fgets($f)) {</span><br><span class="line"> <span class="keyword">yield</span> $line;</span><br><span class="line"> }</span><br><span class="line"> fclose($f);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意到上述代码完成了和迭代器相同的功能,<strong>复杂性大大降低</strong>,相较于数组实现,<strong>性能开销也会明显下降</strong>。 </p><h2 id="生成器"><a href="#生成器" class="headerlink" title="生成器"></a>生成器</h2><p>如你所见,生成器写法和普通函数写法基本上一致,只是没有用<code>return</code>,而是<code>yield</code>。</p><p>生成器和普通函数区别其实挺大的。</p><ul><li>调用一个生成器只是会创建一个生成器对象,而<strong>不会</strong>直接运行这个函数:</li></ul><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">>>> <span class="function"><span class="keyword">function</span> <span class="title">xrange</span><span class="params">($start, $limit, $step = <span class="number">1</span>)</span> </span>{</span><br><span class="line">... <span class="keyword">for</span> ($i = $start; $i <= $limit; $i += $step) {</span><br><span class="line">... <span class="keyword">echo</span> <span class="string">"call me"</span>; <------</span><br><span class="line">... <span class="keyword">yield</span> $i; |</span><br><span class="line">... } |</span><br><span class="line">... } |</span><br><span class="line">=> <span class="keyword">null</span> |</span><br><span class="line">>>> $gen = xrange(<span class="number">1</span>,<span class="number">3</span>) <span class="comment"># < --- 注意看没有执行echo,没有任何输出。它</span></span><br><span class="line">=> Generator {<span class="comment">#176} # < --- 显示它是一个Generator</span></span><br></pre></td></tr></table></figure><ul><li><p>调用<code>current()</code>函数可以<strong>唤醒</strong>(Resume)生成器,执行到<code>yield</code>关键字的地方继续<strong>暂停</strong>(Suspends),继续往下执行需要执行<code>next()</code>。即<code>yield</code>可以产生<strong>中断点</strong>。</p><blockquote><p> 这个和Python不一样哦。Python是直接调用next的(虽然可以使用装饰器)</p></blockquote></li></ul><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">>>> $gen->current(); <span class="comment">// <--- 注意这一行也只执行了第一个echo.</span></span><br><span class="line">call me⏎ <span class="comment">// 执行完current之后,生成器里面的代码会继续往后执行,</span></span><br><span class="line">=> <span class="number">1</span> <span class="comment">// 直到遇到yield这个关键字,然后继续暂停.</span></span><br><span class="line"> ^----注意这个<span class="number">1</span>是来自<span class="keyword">yield</span>后面的$i所产生而返回的,即<span class="keyword">yield</span>将$i返回给了调用者。</span><br><span class="line">>>> $gen->next();</span><br><span class="line">call me⏎ <span class="comment">// 执行到第二次循环,然后输出.</span></span><br><span class="line">=> <span class="keyword">null</span> </span><br><span class="line">>>> $gen->current(); <span class="comment">// 现在拿到的是$i=2的时候。</span></span><br><span class="line">=> <span class="number">2</span></span><br></pre></td></tr></table></figure><p>如上,因为生成器是需要的时候(执行<code>next()</code>)才会执行。不会贪婪的一次性生成所有数据放在内存中,而是特别的懒,我们称为<strong>惰性求值( Lazy Evaluation)</strong>,所以内存占用很小。也许你也注意到了,我们可以一直<code>next()</code>调用往下执行,但是并没有<code>prev</code>之类的接口。所以生成器属于 <strong>One-time Operation</strong>,一次性操作。因此上面的<code>xrange</code> 你只能遍历一次,<strong>如果想要多次,你必须得重新调用一次生成器</strong>。</p><ul><li>不同于<code>return</code>返回的是一个value, <code>yield</code>可以让你产生一系列的值。</li></ul><h1 id="yield-使用场景"><a href="#yield-使用场景" class="headerlink" title="yield 使用场景"></a>yield 使用场景</h1><p><code>yield</code> 最容易让人想到的一个点就是可以让一个普通的函数随便变成高大上的生成器,而生成器是可以迭代的。即<code>yield</code>作为最简单的用途来说,就是可以放在循环中,一直迭代数据。</p><p>但其实从 Python 中 <code>yield</code> 的作用来看,主要被分成了三种类型( 参见 <a href="http://www.dabeaz.com/coroutines/Coroutines.pdf" target="_blank" rel="noopener">PPT</a>):</p><ul><li><a href="http://php.net/manual/zh/class.iterator.php" target="_blank" rel="noopener">迭代器 Iterator</a> 可以作为生产者产生数据</li><li>接受消息 可以作为消费者接受数据</li><li><a href="https://www.wikiwand.com/en/Trap_(computing)" target="_blank" rel="noopener">陷阱 Trap</a> 可以实现多任务调度</li></ul><p>PHP 亦是如此。下文以 PHP 为例子,把三种方式都简单记录一下。</p><h2 id="生产者"><a href="#生产者" class="headerlink" title="生产者"></a>生产者</h2><p>如前文,当<code>yield</code>放在一个循环中,我们可以利用<code>yield</code> 作为生产者产生有限或者无限的数据。如前文提到的 <code>xrange</code> 这种基础用法。考虑这样的一种情况:</p><p>我需要打开一个日志文件,然后将所有符合某种规则的日志行全部提取出来,然后需要对这些行进行一定的处理,然后做展示,大概这样:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">foreach</span>($lines <span class="keyword">as</span> $line) {</span><br><span class="line"> <span class="comment">// 可能有逻辑其他处理</span></span><br><span class="line"> <span class="keyword">if</span> (preg_match($regex, $line)) {</span><br><span class="line"> <span class="comment">// 此条件下其他处理逻辑</span></span><br><span class="line"> $line = andParsedLine($line);</span><br><span class="line"> <span class="keyword">if</span> ($isDuplcaite($line)) {</span><br><span class="line"> <span class="comment">// 其他逻辑</span></span><br><span class="line"> <span class="keyword">echo</span> $line;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这就是传说中的 <a href="https://www.wikiwand.com/en/Pyramid_of_doom_(programming)" target="_blank" rel="noopener">Pyramid of doom(金字塔厄运)</a>,大概这样<img src="/assets/img/pyramid-of-doom.jpg" alt="Pyramid of doom"></p><p>这个流程其实很类似于我们 Linux 下的管道:</p><p><code>grep xxx file | awk xxxx | sort | uniq | head ...</code></p><p>同样的道理,我们可以把函数封装成这样 => <code>functionC(functionB(functonA()))</code> 从而避免或者减少金字塔厄运(事实上,为了解决这样的问题,还有比如<code>Promise</code>,<code>Thunk</code>很多玩意儿):</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一行一行的读文件(产生数据)</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">readByLine</span><span class="params">($file)</span> </span>{</span><br><span class="line">$f = fopen($file, <span class="string">'r'</span>);</span><br><span class="line"><span class="keyword">while</span> (!feof($f)) {</span><br><span class="line"><span class="keyword">yield</span> $f;</span><br><span class="line">}</span><br><span class="line">fclose($f);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 使用正则过滤(继续生产数据)</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">grep</span><span class="params">($gen, $regex)</span> </span>{</span><br><span class="line"><span class="keyword">foreach</span> ($gen <span class="keyword">as</span> $line) {</span><br><span class="line"><span class="keyword">if</span> (preg_match($regex, $line)) {</span><br><span class="line"><span class="keyword">yield</span> $line;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 现实文件的前n行(还是生产数据)</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">head</span><span class="params">($gen, $n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">$current = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">foreach</span> ($gen <span class="keyword">as</span> $line) {</span><br><span class="line"><span class="keyword">if</span> ($current > $n) {</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">echo</span> $line;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">head(grep(readByLine(<span class="string">"/www/xxxx.log"</span>), <span class="string">'xxxxx'</span>), <span class="number">3</span>);</span><br></pre></td></tr></table></figure><p>因为 <code>yield</code> 是惰性求值的,在需要的时候才会做计算,我们不用担心这样的代码会出现内存过大之类的问题。(相反,如果上述代码换成 <code>return</code>来写,就可能会爆哦)</p><p>很多时候, PHP 中一些自带的函数 <code>array_map</code>, <code>array_filter</code>, <code>array_column</code>等等,我们都可以用 <code>yield</code> 将其替换为 <strong>生成器</strong> (Python中这些API已经如此了) </p><p><strong>题外话</strong>: <a href="https://3v4l.org/hQPpe#output" target="_blank" rel="noopener">https://3v4l.org/hQPpe</a></p><blockquote><p>PHP 5.x 的时候生成器方案比普通快。<br>PHP 7.x 的时候生成器方案比普通慢。<br>在 HHVM 上 普通方案的速度太快(比生成器快接近3倍)</p></blockquote><p>前文提到过<strong>协程</strong>, <strong>中断点</strong> 我们还没有接触,显然可以知道<code>yield</code>的作用其实还有很多。</p><h2 id="消费者"><a href="#消费者" class="headerlink" title="消费者"></a>消费者</h2><p> <code>Generator</code> 不仅仅实现了<code>Iterator</code>接口, 而且还有自定义的一些方法,比如<code>send()</code>。</p><p>这个<code>send</code>可不得了,它可以实现所谓的<strong>双向通讯</strong>。具体来说,上面我们处理日志的流程是这样的:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"> +------------+ +------+ +------+</span><br><span class="line">/www/xxxx.log -->| readByLine |+--->| grep |+-->| head |+--></span><br><span class="line"> +------------+ +------+ +------+</span><br></pre></td></tr></table></figure><p>可以发现,<strong>无论是<code>grep</code>还是<code>head</code>,它们都是自己主动去<code>Pull</code>之前的数据</strong>。我们仅仅用到了<code>yield</code>的返回功能,还差发送功能。</p><p>现在我们看一下 <a href="http://php.net/manual/zh/generator.send.php" target="_blank" rel="noopener">Generator::send</a></p><blockquote><p>向生成器中传入一个值,并且当做 <a href="http://php.net/manual/zh/language.generators.syntax.php#control-structures.yield" target="_blank" rel="noopener">yield</a> 表达式的结果,然后继续执行生成器。</p><p>如果当这个方法被调用时,生成器不在 <a href="http://php.net/manual/zh/language.generators.syntax.php#control-structures.yield" target="_blank" rel="noopener">yield</a> 表达式,那么在传入值之前,它会先运行到第一个 <a href="http://php.net/manual/zh/language.generators.syntax.php#control-structures.yield" target="_blank" rel="noopener">yield</a> 表达式</p></blockquote><p>这基本就是 PHP 中实现 <a href="https://www.wikiwand.com/zh-hans/%E5%8D%8F%E7%A8%8B" target="_blank" rel="noopener"><strong>协程</strong></a> 的原理,然而 PHP 里面严格意义上叫做半协程(semicoroutines),因为控制权并没有真正的协程那么自由:</p><blockquote><p><a href="https://www.wikiwand.com/en/Generator_(computer_science" target="_blank" rel="noopener">Generators</a>), 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.<a href="https://www.wikiwand.com/en/Coroutine#citenote6" target="_blank" rel="noopener">[6]</a> That is, since generators are primarily used to simplify the writing of <a href="https://www.wikiwand.com/en/Iterator" target="_blank" rel="noopener">iterators</a>, the <code>yield</code>statement in a generator does not specify a coroutine to jump to, but rather passes a value back to a parent routine.</p></blockquote><p>不过我一直觉得概念其实并没有那么的重要,不用太过于纠结。</p><p>看到这个函数,我们需要想到几点:</p><ul><li>调用<code>send</code>时,我们会去唤醒之前协程有 <code>yield</code> 表达式的地方。</li><li>这时候执行权在协程那边,您可以理解为 Goto 语句。</li><li>当遇到下一次<code>yield</code>的时候,才会把数据返回,并且交回控制权。</li></ul><p>我们还是来看一下 <code>yield</code> 作为消费者的话,以 <code>grep</code> 举例:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 可以作为消费者接受数据的grep.</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">grep</span><span class="params">($regex)</span> </span>{</span><br><span class="line"><span class="keyword">while</span> (<span class="keyword">true</span>) {</span><br><span class="line">$line = <span class="keyword">yield</span>; <span class="comment">// <-- 可以从这里接受一个数据(通过generator->send传递给他.)</span></span><br><span class="line"><span class="keyword">if</span> (preg_match($regex, $line)) {</span><br><span class="line"><span class="keyword">echo</span> <span class="string">"Matched: {$line}\n"</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="comment"># >>> $filter->send("python generator");</span></span><br><span class="line"><span class="comment"># => null</span></span><br><span class="line"><span class="comment"># >>> $filter->send("shellvon");</span></span><br><span class="line"><span class="comment"># Matched: shellvon</span></span><br></pre></td></tr></table></figure><p>如上面的代码所示,我们原来的处理流程可以变成这样:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">as</span> source +------+ send() +------+ send() +--------+</span><br><span class="line">/www/xxxx.log -->| grep |+------->| head |+------->| output |</span><br><span class="line"> +------+ +------+ +--------+</span><br></pre></td></tr></table></figure><p>如上图所示,上图有几个特点:</p><ul><li>您可以使用 <code>send()</code>方法<strong>把数据push给下一个操作逻辑</strong>,<span style="color:red">注意与之前迭代器的Pull相比较</span></li><li>您可能发现 <code>readByLine</code> 没有在上图出现,那是因为上图画的都是<strong>消费者</strong>,不是生产者。<code>readByLine</code> 其实还是存在,以生产者身份。<strong>而这样一个典型的管道操作是需要有一个数据源的,即生产者</strong>。</li><li>生产者显然不是一个协程,<code>readByLine</code>可以仍然保持之前的迭代器,通过<code>send</code>将数据发出来。</li></ul><p>如果脑洞更加大一点, 相比于之前的 Pull,我们 Push 的时候,可以同时 send 给多个消费者。更加复杂:</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"> +-----------+</span><br><span class="line"> +------>| coroutine |+- - - - - - - +</span><br><span class="line"> | +-----------+ |</span><br><span class="line"> +-------+--+ +-----------+ +-----------+</span><br><span class="line">source+--->|coroutine |--->| coroutine |+- - - ->| coroutine | </span><br><span class="line"> +-------+--+ +-----------+ +-----------+</span><br><span class="line"> | +-----------+ |</span><br><span class="line"> +------>| coroutine |+- - - - - - - +</span><br><span class="line"> +-----------+</span><br></pre></td></tr></table></figure><p>当然,多个消费者最后可以再 <code>send</code> 给同一个 pipe , 聚合回来。</p><p>另外,其实我们的 <code>source</code> 本身来源也可以不唯一。比如我们可以使用 <a href="http://php.net/manual/zh/book.spl.php" target="_blank" rel="noopener">PHP SPL</a> 中提供的一些有用组件: <a href="http://php.net/manual/zh/class.multipleiterator.php" target="_blank" rel="noopener">MultipleIterator</a> 和 <a href="http://php.net/manual/zh/class.appenditerator.php" target="_blank" rel="noopener">AppendIterator</a>,之前的 <code>head</code> 我们可以使用 <a href="http://php.net/manual/zh/class.limititerator.php" target="_blank" rel="noopener">LimitIterator</a>。</p><p>甚至,按照 <a href="http://www.dabeaz.com/" target="_blank" rel="noopener">Dabeaz</a> 大神的说法,我们的来源和 <code>send</code>的 目标也好,不一定非要是字符串,非要是文件。</p><h2 id="陷阱"><a href="#陷阱" class="headerlink" title="陷阱"></a>陷阱</h2><p>这里的<a href="https://www.wikiwand.com/en/Trap_(computing)" target="_blank" rel="noopener">陷阱</a>是指计算机专业术语,我在 <a href="https://github.com/chyyuu/ucore_os_docs/blob/master/lab1/lab1_3_3_2_interrupt_exception.md" target="_blank" rel="noopener">Github</a> 找到了一个中文解释: </p><blockquote><p>程序中使用请求系统服务的系统调用而引发的事件,称作陷入中断(trap interrupt),也称软中断(soft interrupt),系统调用(system call)简称trap。</p></blockquote><p>之所以用到这个概念,主要原因是因为我们的 <code>yield</code> 可以产生<strong>中断点</strong>与操作系统的 Trap 很类似。即:</p><p>我们可以利用 <code>yield</code> 会获取(通过你send数据给它)与返回控制权这个特性完成一个类似操作系统的<strong>调度器</strong>。</p><p>这就是文章开始提到的 <a href="http://www.laruence.com/2015/05/28/3038.html" target="_blank" rel="noopener">在PHP中使用协程实现多任务调度</a> 的内容(PHP Generator的发明人 <a href="https://nikic.github.io/aboutMe.html" target="_blank" rel="noopener">Nikita Popov</a> 在PHP的 rfc 中也说到了<a href="http://www.dabeaz.com/" target="_blank" rel="noopener">Dabeaz</a> 大神的 <a href="http://www.dabeaz.com/coroutines/Coroutines.pdf" target="_blank" rel="noopener">PPT</a>,所以我觉得他的这篇文章应该也是受到此 PPT 的影响,具体参见 Part6 及其 Part7)</p><p>所以其实本章节更好的题目应该是 <strong>利用协程完成多任务调度</strong>。因为中文/英文都特别详细,我就不用做啥笔记了。附上地址:</p><ul><li>中文: <a href="http://www.laruence.com/2015/05/28/3038.html" target="_blank" rel="noopener">在PHP中使用协程实现多任务调度</a></li><li><p>英文: <a href="https://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html" target="_blank" rel="noopener">Cooperative multitasking using coroutines (in PHP!)</a></p><p><strong>题外话</strong>:</p></li></ul><blockquote><p>因为<a href="https://nikic.github.io/aboutMe.html" target="_blank" rel="noopener">Nikita Popov</a> 的那篇 Cooperative multitasking using coroutines in PHP, 目前其实已经有不少的项目和文章了,比如:</p><ul><li><a href="https://www.gitbook.com/book/goghcrow/php-co-koa" target="_blank" rel="noopener">PHP异步编程: 手把手教你实现co与Koa</a></li><li><a href="https://github.com/pinguo/php-msf" target="_blank" rel="noopener">https://github.com/pinguo/php-msf</a></li><li><a href="http://zanphp.io/" target="_blank" rel="noopener">http://zanphp.io/</a></li></ul></blockquote><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><p>在学习协程和生成器的时候,我检索了各种关键字,其中发现了不少我个人觉得营养价值比较高的文章或者博客,我作为附录放这里,希望我每一次看的时候都能学到更多。</p><ul><li><p><a href="https://blog.youxu.info/2014/12/04/coroutine/" target="_blank" rel="noopener">编程珠玑番外篇-Q 协程的历史,现在和未来</a></p></li><li><p><a href="https://github.com/chyyuu/simple_os_book/blob/master/zh/chapter-4/process_schedule_principal.md" target="_blank" rel="noopener">操作系统简单实现与基本原理</a></p></li><li><p><a href="https://markbakeruk.net/2016/01/19/a-functional-guide-to-cat-herding-with-php-generators/" target="_blank" rel="noopener">A Functional Guide to Cat Herding with PHP Generators</a></p></li></ul>]]></content>
<summary type="html">
<p><img src="/assets/img/generator-note.svg" alt="PHP Generator 笔记"></p>
<blockquote>
<p>图片来源 <a href="https://wpengine.com/try/php7-hosting/" target="_blank" rel="noopener">https://wpengine.com/try/php7-hosting/</a></p>
</blockquote>
<h1 id="序"><a href="#序" class="headerlink" title="序"></a>序</h1><p>先说,标题虽然是《Generator 笔记》,但实际上本文会主要内容会是<code>yield</code>。</p>
<p>以鄙人的拙见,目前大多数 PHPer 对 PHP 的 <code>yield</code> 关键字并不怎么了解,但实际上这却是一块非常值得学习的地方,至少于我而言如此。<br><code>yield</code> 为 PHP 引入了<strong>生成器</strong>,<strong>协程</strong> 等一些复杂概念,导致入门门槛也挺高。</p>
<p>关于学不学<code>yield</code>, 目前,我遇到过以下几类人:<br>
</summary>
<category term="技术" scheme="http://von.sh/categories/%E6%8A%80%E6%9C%AF/"/>
<category term="Generator" scheme="http://von.sh/tags/Generator/"/>
<category term="PHP" scheme="http://von.sh/tags/PHP/"/>
</entry>
<entry>
<title>又一次新的开始</title>
<link href="http://von.sh/2017/12/01/a-new-beginning-again/"/>
<id>http://von.sh/2017/12/01/a-new-beginning-again/</id>
<published>2017-12-01T06:03:14.000Z</published>
<updated>2017-12-07T04:15:06.000Z</updated>
<content type="html"><![CDATA[<p><img src="//image.3001.net/images/20171117/15109302542639.jpg" alt="You should blog even if you have no readers"></p><h1 id="序"><a href="#序" class="headerlink" title="序"></a>序</h1><p>前天无意看到了这么一篇博文 <a href="http://nathanmarz.com/blog/you-should-blog-even-if-you-have-no-readers.html" target="_blank" rel="noopener">You should blog even if you have no readers</a></p><a id="more"></a><p>书中有个观点: <strong>写作能够使你更加聪明</strong>,因为写作的过程显露出一个人想法的缺陷。在写文章的过程中,你需要组织你的想法,你会仔细的思考,从而强制你去锤炼你的思想。<br>加上我最近的一些遭遇,我突然就特想弄个博客。</p><h1 id="离"><a href="#离" class="headerlink" title="离"></a>离</h1><p>其实博客我之前搞过几次,<del>OSChina / Lofter / Sina / Wordpress / TerminalBlog</del> 再见!<br>最后一次用 <a href="https://github.com/shellvon/TerminalBlog/tree/gh-pages" target="_blank" rel="noopener">TerminalBlog</a> 还是去年。之后觉得自己肚子里没货,语文又是在及格线上来回波动的理科生,放弃了。 </p><p>今天又是新的开始,主要原因有几个:</p><ol><li>好记性不如烂笔头,很多东西如果不以文字或者其他持久化方式存下来,就会被淡忘。</li><li>我希望我可以回看当初得想法的时候,可以感受到自己的成长。</li><li>希望自己变得聪明(笑~😊</li><li>我口头上说了几次希望女朋友能用博客记录自己学习前端的成长之路,但都没有实际行动。是时候动一下了。</li></ol><p>之所以选择 Hexo 而放弃之前自己写的 <a href="https://github.com/shellvon/TerminalBlog/tree/gh-pages" target="_blank" rel="noopener">TerminalBlog</a> 也是考虑了一些因素:</p><ol><li>我的静态博客生成器太简单了,满足不了一些复杂的需求。</li><li>我希望我女朋友也能用,对于 Python小 白来说,学习这个完全没有文档的生成器,复杂了。</li><li>女朋友是学前端的,所以选择 NodeJS 的 Hexo,而不是其他语言的,比如 Ruby <code>Jeklly</code>(当然也包括我自己的^_^)</li></ol><p>域名 von.sh 是昨天和“TW首席咨询师” <a href="http://blog.kimleo.net" target="_blank" rel="noopener">kimLeo</a> 商量定下来的,昨天才买的,不到500元。</p><p>因为是<code>CNAME</code>到 Github Pages 的,所以其实我的老博客也可以在这个域名下。因此,我不打算迁移老内容过来了。</p><p>这里是老博客入口: <a href="http://von.sh/TerminalBlog/">http://von.sh/TerminalBlog/</a></p><h1 id="跋"><a href="#跋" class="headerlink" title="跋"></a>跋</h1><p>封面图是 <a href="http://www.freebuf.com/news/154404.html" target="_blank" rel="noopener">Freebuf 偷来的</a>,没有图的博文总觉得少了点什么。</p><p>写博文真是一个无比难的问题,这个新的开始。我希望我们都能坚持。<br>另外,谢谢 <a href="http://esrever10.cc/" target="_blank" rel="noopener">esrever10</a> 大佬的赞助费。</p>]]></content>
<summary type="html">
<p><img src="//image.3001.net/images/20171117/15109302542639.jpg" alt="You should blog even if you have no readers"></p>
<h1 id="序"><a href="#序" class="headerlink" title="序"></a>序</h1><p>前天无意看到了这么一篇博文 <a href="http://nathanmarz.com/blog/you-should-blog-even-if-you-have-no-readers.html" target="_blank" rel="noopener">You should blog even if you have no readers</a></p>
</summary>
<category term="生活" scheme="http://von.sh/categories/%E7%94%9F%E6%B4%BB/"/>
<category term="随想" scheme="http://von.sh/tags/%E9%9A%8F%E6%83%B3/"/>
</entry>
<entry>
<title>30分钟学正则</title>
<link href="http://von.sh/2017/11/29/learn-regex-in-30-minutes/"/>
<id>http://von.sh/2017/11/29/learn-regex-in-30-minutes/</id>
<published>2017-11-29T05:56:18.000Z</published>
<updated>2017-12-01T12:18:41.000Z</updated>
<content type="html"><*@([a-z0-9]*[-_]?[a-z0-9]+)+[\.][a-z]{2,3}([\.][a-z]{2})?$</span><br></pre></td></tr></table></figure><p>当时我只说写这么长且无用的正则,简直了….</p><ol><li>各位知道这个正则是做什么的么?</li><li>各位知道当匹配<code>iamshellvon@gmail.c</code>这种会有多少次回溯么?</li></ol><h1 id="序"><a href="#序" class="headerlink" title="序"></a>序</h1><p>文本处理工具,大家想到的绝对会有正则表达式(<a href="https://www.wikiwand.com/zh-hans/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F" target="_blank" rel="noopener">Regular Expressions</a>)这个大佬,在当今的编程语言中,也找不出几个不支持正则的(有,当然有,比如 <a href="https://www.wikiwand.com/zh-hans/Brainfuck" target="_blank" rel="noopener">brainfuck</a>) ,我周围的程序员也没有没接触过正则的。不过,接触了不代表就懂正则了。</p><blockquote><p>“我也不知道它是怎么工作的,反正跑起来正常就行了”<br>–by <strong>匿名用户</strong></p></blockquote><p>其实,正则表达式并没有那么那么的晦涩难懂,高大上,它不过是一个普通的 DSL 罢了,复杂的文本处理,也不应该用正则,而应该考虑比如 <code>flex/bison</code> 这种 parser 了。</p><h1 id="正则基础"><a href="#正则基础" class="headerlink" title="正则基础"></a>正则基础</h1><h2 id="什么是正则表达式"><a href="#什么是正则表达式" class="headerlink" title="什么是正则表达式?"></a>什么是正则表达式?</h2><p><a href="https://www.wikiwand.com/zh-hans/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F" target="_blank" rel="noopener">Regular Expression</a>,简称 RE/Regex。用于匹配字符串中字符组合的模式。我们可以通过认识集合中有限的字符(正则表达式中的元字符)来识别属于这种集合的任意多的其他字符。<br>这样来讲,其实你之前已经接触过正则了,只是你不知道而已。比如:</p><ul><li>Ctrl + F<br> 字符串子串匹配问题, 本质上也是正则。</li><li>echo “shellvon” | grep von<br> 我们在命令行下经常使用的 grep,其实你的 <code>von</code> 也算正则了<br>…</li></ul><p>可是当我们要匹配的都是数字或者都是字母的时候。或者100个相同字母的时候,写起来就啰嗦复杂了。也谈不上模式!所以呢,我们引入了一种这样功能的字符: <strong>自己不代表自己原本含义,而都有特殊含义的字符</strong>。比如,我希望点号 <code>.</code>可以匹配任意字符,具有这种功能的字符,我们称之为 <strong>元字符(metacharacters)</strong> </p><h2 id="元字符"><a href="#元字符" class="headerlink" title="元字符"></a>元字符</h2><p>生活中,我们也早已经接触过所谓的元字符了。比如:</p><ul><li>互联网脏话 -> 用<code>*</code>给替代了</li><li>二胖Plus、鑫胖、金三月半、鑫月半、金三肥 -> 真实含义代表啥大家也知道<br>…</li></ul><p>正则表达式里面有许多这样的<code>元字符</code>,这些<code>元字符</code>构成一个有限子集,让我们可以利用这个有限子集去识别和匹配无限可能的文本。</p><table><thead><tr><th>元字符</th><th>含义</th></tr></thead><tbody><tr><td>^和$</td><td>行的开始^和结束$</td></tr><tr><td>[]和[^]</td><td>字符组[]和排除型[^]</td></tr><tr><td>-</td><td>连字符,必须出现在字符组内,表示范围比如[0-9]</td></tr><tr><td>.</td><td>匹配任意字符(可能会排除\n哦)</td></tr><tr><td>*和+</td><td>量词,匹配该字符之前的字符,*是>=0次,+是>=1次</td></tr><tr><td>{n}和{n,}和{n,m}</td><td>区间量词,匹配次数{n}匹配n次,{n,m}匹配n~m次</td></tr><tr><td>|</td><td>或者</td></tr><tr><td>?</td><td>量词,标记?前的字符为可选</td></tr><tr><td>()</td><td>字符集,标记完全匹配括号内的数据</td></tr><tr><td>\</td><td>转义,用于匹配一些保留字符(比如元字符)</td></tr></tbody></table><p>如上表,这些字符出现在正则里面的时候,通常都不代表本来的含义。关于上表,还有几点需要注意:</p><ul><li>脱字符<code>^</code>必须出现在文本开头,美元符<code>$</code>必须出现在行尾</li><li>排除形里面也出现了脱字符<code>^</code>,代表排除时也必须出现在中括号[]内的第一个字符,否则不具有此含义。</li><li>连字符,顾名思义,必须具有起止字符,否则就是自己本来的意思。比如[-]代表匹配自己,特殊一点的是[a-c-d]由于a-c已经表示了范围,后面的<code>-</code>就找不到开始范围,那么第二个<code>-</code>就代表自己,所以匹配的是<code>abc-d</code>这5个字符。</li></ul><p>我们来看元字符的一些例子:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">"[Tt]he" => The car parked in the garage</span><br><span class="line"> ^^^ ^^^</span><br><span class="line">"[^c]ar" => The car parked in the garage</span><br><span class="line"> ^^^ ^^^</span><br><span class="line">"c.+t" => The fat cat sat on the mat</span><br><span class="line"> ^^^^^^^^^^^^^^^^^^</span><br><span class="line"> ¦----被(.+)匹配-¦</span><br><span class="line">"b.g" => baidugoogle big bug bag</span><br><span class="line"> ^^^ ^^^ ^^^</span><br></pre></td></tr></table></figure></p><p>如你所见,我们正则的表达式匹配流程会是从左边往右边挨个来,就像你读本文如此。<br>这些元字符可不简单,万丈高楼也是靠它们。</p><h2 id="子表达式"><a href="#子表达式" class="headerlink" title="子表达式"></a>子表达式</h2><p>在算法领域有个很好的思想叫做 <strong>分治法</strong> 。当我们遇到一个复杂的需求,我们总是可以拆解成无数个小的问题,然后尝试去解决小问题,最后这些小问题的答案来解决大问题。正则表达式亦是如此。<br>一个复杂的正则表达式都是由一些简单的正则表达式组合起来的。这些简单的正则我们称之为 <strong>子表达式</strong> 。它们通常是指用 <code>|</code> 分割的多选分支或者括号内的表达式。<br>比如<code>gr(e|a)y</code>。我们可以将(e|a)看成子表达式,其中<code>e</code>和<code>a</code>也可以看成子表达式,严格意义上讲,上述的<code>g</code>/<code>r</code>/<code>y</code>等也算子表达式。不过这样太细了意义也不大。我们主要还是关注竖线|分割和括号()包围起来的更有意思。<br>首先,考虑这样一个问题:<br>如果需要让你用刚学的<strong>元字符</strong>去匹配一个<code>0-23</code>的范围的数字(比如时间刚好这个范围)怎么办?哦,天啦,你可能会说<code>[0-23]</code> 刚好啊,因为<code>-</code>可以表示范围,很遗憾,并不行,通常<code>-</code>能表示的开始和结束都只有一个字符(虽然16进制这种就不是了),但是你要知道,上述表达式的真实含义是匹配<code>[0123]</code>这四个数字。<br>这个可以参见我前文提到的关于连字符<code>-</code>的注意事项。这种方式不可行,另外一个最容易想到的就是拆分它,我们可以利用<strong>子表达式</strong>这个概念。<br>怎么拆呢?</p><pre><code>1. `[0-9]` 可以匹配范围个位数2. `1[0-9]` 可以匹配10-19的3. `2[0-3]` 可以匹配20-23的</code></pre><p>显然,最后用元字符<code>|</code>(代表或)将他们连接起来就符合要求了撒。</p><p>如果更加复杂一点呢,比如要匹配一个 IPv4 地址呢?其实原理一样,只是数字范围变成了0-255。这正如文章开头的图中所示。</p><p>更为复杂的就是,考虑匹配一个日期,需要考虑月份和天数的关系,甚至还有闰年(通常这种时候你还要用正则的话,你估计已经开始过度使用正则了,你需要避免这样)</p><h2 id="简写"><a href="#简写" class="headerlink" title="简写"></a>简写</h2><p>懒惰是程序员的美德之一。在上面的例子中我们写[0-9]要写5个字符,有没有更简写的呢?</p><table><thead><tr><th>简写字符</th><th>含义</th></tr></thead><tbody><tr><td>.</td><td>匹配任意字符</td></tr><tr><td>\d和\D</td><td>\d表示数字,\D非数字</td></tr><tr><td>\w和\W</td><td>\w等效于[a-zA-Z0-9_],\W相反</td></tr><tr><td>\s和\S</td><td>\s匹配所有空格字符,\S相反</td></tr></tbody></table><p>举个例子:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">"\d{2,3}" => The number was 9.9997.</span><br><span class="line"> ^^^</span><br><span class="line">"car\sis" => The car is parked.</span><br><span class="line"> ^^^^^^</span><br><span class="line">"^\d+" => 13 is less than 18.</span><br><span class="line"> ^^</span><br></pre></td></tr></table></figure></p><h2 id="捕获-非捕获与反向引用"><a href="#捕获-非捕获与反向引用" class="headerlink" title="捕获,非捕获与反向引用"></a>捕获,非捕获与反向引用</h2><p>目前为止,我们知道括号可以用在<a href="#子表达式">子表达式</a>中,表示完整的一个整体。括号另外还有一个作用是 <strong>捕获</strong> 。所谓捕获,就是指正则引擎(正则的大脑)可以<code>记住</code>匹配的结果,会给这些结果取一个小名,用于之后的用途,取名规则是这样的:<br>每个括号从左向右以左括号为标志,会自动拥有一个组号,从1开始。<br>比如: <code>(hello (world (regex)))</code><br>分组如下:</p><pre><code>1. hello world regex <-- 第一个组,组号为1,后面以此类推2. world regex <-- 第二个组3. regex <-- 第三个组,因为左括号出现的比较晚</code></pre><p>这样有个好处,我们可以在正则里面通过组号拿之前匹配的结果,这种方式叫做 <strong>反向引用</strong> 。</p><p>举个例子:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">“([ab])\1” => abcdebbcde</span><br><span class="line"> ^^</span><br><span class="line"> ¦-----这个`b`就是因为前面的([ab])匹配到了一个字符b。我们可以用\1来引用起之前结果</span><br></pre></td></tr></table></figure></p><p>显然,有时候我们并不需要捕获(不然数字一直在增大的同时,让正则脑袋记那么多内容,效率也不好),所以与之对应的还有 <strong>非捕获</strong>。写法很相似:<code>(?:pattern)</code>。一个更具体的对比表如下:</p><table><thead><tr><th>语法</th><th>含义</th><th>例子</th></tr></thead><tbody><tr><td>(pattern)</td><td>匹配pattern并捕获结果,自动设置组号</td><td>(\d{2})+\d</td></tr><tr><td>(?:pattern)</td><td>匹配pattern,但是不捕获 hell(?:o|y)</td><td>匹配hello/helly</td></tr></tbody></table><p>显然,如果一个复杂的表达式里面引入过多的<code>非捕获</code>,虽然可以减少正则引擎去记忆,但是这也会增加我们阅读正则表达式的难度,所以,请深思熟虑是否有必要为了这么一点性能而用非捕获。<br>另一方面,在复杂的正则表达式中如果全部使用了捕获,试想一下这种情况:<br>有天你突然把某个括号删除了,或者需要在中间某处增加一个括号,会产生什么样的影响?<br>聪明的你应该想到了,会导致组号的变更。比如原来是\3,现在可能变成了\2或者\4,或者其他。<br>所以捕获如果按照数字取名字实在不是很好。于是写 Python 正则的那群人说,要不给这些组取名字的事情交给用户吧,比如叫<code>张三</code>,<code>李四</code>。这种技术叫做<br><strong>命名捕获</strong>。大概是这么写的<code>(?P<myname>hello)</code> 它不仅具有原来数字的乳名\1,还有用户你自己为它取的名字<code>myname</code>。这种方式很棒,.Net觉得不错也抄袭了过去,只不过语法不一样罢了。更多的细节请参见:<br><a href="https://www.regular-expressions.info/named.html" target="_blank" rel="noopener">Named Capturing Groups and Backreferences</a></p><h1 id="正则进阶"><a href="#正则进阶" class="headerlink" title="正则进阶"></a>正则进阶</h1><p>之前基础部分已经可以完成日常简单的需求了,甚至我们仔细想想可以知道正则最重要的两点原则:</p><ul><li>匹配先从需要查找的字符串的起始位置尝试匹配,是从左至右的。</li><li>总是尝试匹配尽可能多的字符,我们称之为贪婪。</li></ul><p>第一点:<br>比如”Python”匹配 “i am Pythoner, i love Python.”; 正则匹配到 _Python_er, 而不是后面的 Python 。因为他是左边开始的,先找到谁就是谁。</p><p>第二点: <a href="https://stackoverflow.com/questions/2301285/what-do-lazy-and-greedy-mean-in-the-context-of-regular-expressions" target="_blank" rel="noopener">贪婪(Greedy)</a>,我们的所有量词都是尽可能多的匹配!</p><h2 id="贪婪与非贪婪"><a href="#贪婪与非贪婪" class="headerlink" title="贪婪与非贪婪"></a>贪婪与非贪婪</h2><p>还记得这个例子么?<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">"c.+t" => The fat cat sat on the mat</span><br><span class="line"> ^^^^^^^^^^^^^^^^^^</span><br><span class="line"> ¦----被(.+)匹配-¦</span><br></pre></td></tr></table></figure></p><p>我们来描述一下这个流程:</p><ol><li>正则表达式的<code>c</code>开始不能匹配’T’,直到遇到单词’cat’中的’c’,于是宣告’c’匹配成功。开始尝试<code>.</code>来匹配后续</li><li>正则表达式中的<code>.</code>开始匹配单词’cat’中的’.’,发现还是可以匹配。</li><li>有趣的事情来了: 正则发现.后面有个+,即告诉我吃至少一个,那么我怎么抉择呢?答案是:<strong>我尽可能吃撑,吃到吃不下去为止–这就是贪婪。</strong></li><li>于是.继续匹配,一路所向披靡,因为.啥字符都可以吃啊,然后走到了最后,单词’mat’中’t’的后面为止。发现没有匹配了,点号吃不下去了,就该让表达式<code>c.+t</code>中的<code>t</code>上场,发现t也不行,怎么办呢?难道宣告正则匹配失败??不不不,毕竟<code>+</code>表示的1到多嘛,我可以通知.吐一个字符出来,让<code>t</code>试试呢</li><li>于是.就吐出来了’t’,记住这时候<code>.+</code>已经吃下了’at sat on the ma’这么多字符。</li><li>这时候发现<code>t</code>可以匹配’mat’中的’t’.这时候文本也是结束了,正则引擎宣告匹配成功!</li></ol><p>这就是贪婪,<code>.+</code>企图吃掉所有字符,可惜最后匹配失败,它很不情愿的吐出了最后一个字符’t’,然后才让正则匹配成功的。</p><p>试想,如果<code>.+</code>不贪婪,很谦虚,那么我可以只吃一个字符’a’,让<code>t</code>取匹配’cat’中’t’.结果也会成功。但是却贪婪了。。<strong>贪婪与否会严重影响正则匹配的结果</strong></p><p>所以用户可能会问了:<br>我怎么告诉正则引擎不贪婪呢?<br>我怎么告诉正则引擎如果我贪婪了,我也不想吐出来呢?(比贪婪还贪婪)</p><p>这其实都很简单:<br><strong>在原来标准量词的后面加一个问号,即为<span style="color:red">忽略优先量词</span></strong><br><strong>在原来标准量词的后面加一个加号,即为<span style="color:red">占有优先量词</span></strong></p><table><thead><tr><th>语法</th><th>含义</th></tr></thead><tbody><tr><td>忽略优先</td><td>*?,+? ,?? ,{n,m}?</td></tr><tr><td>占有优先</td><td>*+,++,?+,{n,m}+</td></tr></tbody></table><p>我们看一下这个例子:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">".*shell" => shellxxxxxshell</span><br><span class="line"> ^^^^^^^^^^^^^^^</span><br><span class="line"> ¦----- 之前的shellxxxxx都被.*吃了,这种情况参见上一个关于贪婪的描述 </span><br><span class="line"></span><br><span class="line">".*?shell" => shellxxxxxshell</span><br><span class="line"> ^^^^^</span><br><span class="line"> ¦---.*什么都不要,因为?告诉它忽略优先</span><br><span class="line"></span><br><span class="line">".*+shell" => shellxxxxxshell</span><br><span class="line">什么都匹配不到,因为.*已经可以贪婪的吃掉整句话,有了+表示占有优先,吃了是不会吐出来的,</span><br><span class="line">所以啥都吃完了,也没有字符留给正则里面的shell来匹配了。导致整个匹配都会失败</span><br></pre></td></tr></table></figure></p><h2 id="调试"><a href="#调试" class="headerlink" title="调试"></a>调试</h2><p>这个可能是最重要的一个环节。<br>工欲善其事,必先利其器。调试正则的话,也需要一个特别好的工具,我这里推荐在线的工具:<br><a href="https://regex101.com/" target="_blank" rel="noopener">https://regex101.com/</a><br>一两句无法说清,所以试图以图的形式简单介绍一下。<br><img src="/assets/img/regex101.png" alt="regex101.com"></p><p>具体学习,还是请以 <strong>Learn by Practice</strong> 为准。而不是 <strong>Learn by manual</strong>。</p><h2 id="环视"><a href="#环视" class="headerlink" title="环视"></a>环视</h2><p>前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(使用[^])。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?<br>这时候我们需要查找它所在的位置!正则表达式里面匹配位置的在我们之前接触过的有<br><code>^</code>和<code>$</code>,前者是开始,后者是结束。</p><table><thead><tr><th>语法</th><th>匹配条件</th><th>例子</th></tr></thead><tbody><tr><td>(?<=…)</td><td>子表达式<strong>能</strong>匹配<span style="color:red">左侧</span>文本</td><td>(?<=[a-z])[A-Z]</td></tr><tr><td>(?<!…)</td><td>子表达式<strong>不能</strong>匹配<span style="color:red">左侧</span>文本</td><td></td></tr><tr><td>(?=…)</td><td>子表达式<strong>能</strong>匹配<span style="color:red">右侧</span>文本</td><td>(?=[a-z])[A-Z]</td></tr><tr><td>(?!…)</td><td>子表达式<strong>不能</strong>匹配<span style="color:red">右侧</span>文本</td><td>\d{3}(?!\d)</td></tr></tbody></table><p>具体位置的举例如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">(?=\d) (?<=\d)</span><br><span class="line"> 3hellvon 3 hellvon</span><br><span class="line"> ^ ^ </span><br><span class="line"> ¦ ¦---找到位置了,左侧能匹配\d,</span><br><span class="line"> ¦ </span><br><span class="line"> 找到位置了,右侧能匹配\d,所以找到的位置在这里</span><br></pre></td></tr></table></figure><p>环视功能可以做很多有趣的事情:<br>比如匹配标签: <code>(?<=<(\w{3})>).*(?=<\/\1>)</code><br>比如判断密码的强度:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">^(?=.*[0-9]) <-- 右侧必须有数字</span><br><span class="line"> (?=.*[a-z]) <-- 右侧必须有小写字母</span><br><span class="line"> (?=.*[A-Z]) <-- 右侧必须有大写字母</span><br><span class="line"> (?=.*[@#$%^&+=]) <-- 右侧必须要有特殊字符</span><br><span class="line"> (?=\S+$).{10,} <-- 必须是非空格,且10位以上.</span><br><span class="line">$</span><br></pre></td></tr></table></figure><h2 id="其他特性"><a href="#其他特性" class="headerlink" title="其他特性"></a>其他特性</h2><p>正则里面有很多重要的特性,限于30分钟和篇幅原因,我无法都写上,我提一下一些概念,有兴趣的同学可以自行检索关键字。</p><ul><li>正则表达式引擎(流派) 比如NFA/DFA.</li><li>递归的正则表达式</li><li>正则回溯</li><li>宽松模式</li><li>条件判断</li><li>正则本地化(locale) 以及Unicode</li><li>模式修饰符(re pattern modifiers)</li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li>杂项 <a href="https://stackoverflow.com/a/22944075" target="_blank" rel="noopener">https://stackoverflow.com/a/22944075</a></li><li>书籍 Mastering Regular Expressions</li><li>PCRE <a href="https://www.pcre.org/" target="_blank" rel="noopener">https://www.pcre.org/</a></li><li>手册 <a href="http://www.rexegg.com" target="_blank" rel="noopener">http://www.rexegg.com</a></li><li>手册 <a href="https://www.regular-expressions.info/" target="_blank" rel="noopener">https://www.regular-expressions.info/</a></li><li>man pcre (Mac/Linux下)</li><li>调试 <a href="https://regex101.com/" target="_blank" rel="noopener">https://regex101.com/</a></li><li>可视化 <a href="https://www.debuggex.com/" target="_blank" rel="noopener">https://www.debuggex.com/</a></li><li>PHP 项目 <a href="https://github.com/nikic/FastRoute/" target="_blank" rel="noopener">FastRoute</a> (应用正则很好的例子)</li><li>Python 项目 <a href="https://github.com/bottlepy/bottle" target="_blank" rel="noopener">Bottle</a> (应用正则很好的例子)</li><li><a href="https://github.com/search?utf8=%E2%9C%93&q=Markdown+parser&type=" target="_blank" rel="noopener">Markdown Parser</a></li></ul>]]></content>
<summary type="html">
<p><img src="/assets/img/ipv4-regex.svg" alt="IPv4"></p>
<blockquote>
<p>有一些人,遇到一个问题时就想:“我知道,我会使用正则表达式。” 然后他就有两个问题了。<br>–by <strong>Jamie Zawinski</strong></p>
</blockquote>
</summary>
<category term="技术" scheme="http://von.sh/categories/%E6%8A%80%E6%9C%AF/"/>
<category term="Regex" scheme="http://von.sh/tags/Regex/"/>
<category term="文本处理" scheme="http://von.sh/tags/%E6%96%87%E6%9C%AC%E5%A4%84%E7%90%86/"/>
</entry>
</feed>