<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Kingsley's Blog]]></title><description><![CDATA[Stay Simple, Sometimes Excited.]]></description><link>http://kingsleyxie.cn/</link><generator>Ghost 0.11</generator><lastBuildDate>Mon, 13 Apr 2026 21:23:42 GMT</lastBuildDate><atom:link href="http://kingsleyxie.cn/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[给我也整一个——《中华人民共和国机动车驾驶证》]]></title><description><![CDATA[<p>文章目录</p>

<p>两年多没更新博客了，趁此机会除除草，记录一下我曲折的拿驾照历程。</p>

<h3 id="">写在前面</h3>

<p>当年高考之后就想着得考个驾照以备不时之需了，但是考虑到家里的驾校不规范现象还是太多，而且练车不方便，时间也是个棘手问题，就一直拖到了大学开学。低年级的时候怕没时间就继续拖，结果发现越到后面越忙，最终大四上学期在深圳实习的时候，抽时间回广州报了个名，想着能在毕业前拿到驾照就好，没想到事与愿违，世风日下，世事难料……</p>

<h3 id="">准备工作</h3>

<p>2019 年 11 月份，找好教练之后约了个时间，周末回学校的时候就顺便搞搞报名的事情，首先是去做驾照体检，周末开的医院不多，按照教练的指引到了石碁人民医院，位置还蛮偏僻的，一通操作之后拿到了《机动车驾驶人身体条件证明》，然后坐公交再换地铁回学校，在地铁口等教练过来带我去驾校，发现这驾校的位置也挺偏僻的。</p>

<p>到了驾校之后先是要照相做照片，负责照相的人拿着相机就往角落的方向走过去，我说我自己带了电子版的和印好的照片，他就拿着我的 U 盘打开图片假装在 PS 里面操作了一下，上传到应该是车管所的什么系统里，然后对着我带的印好的照片说，你这个没用，我说我这个有用，我这是之前按照驾驶证照片要求的标准尺寸印的，传统照片是讲究标准的，诶……我一说，</p>]]></description><link>http://kingsleyxie.cn/the-story-of-getting-my-drivers-license/</link><guid isPermaLink="false">91793a20-493d-4d86-b562-ecc7ce78f2b5</guid><category><![CDATA[Life]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Thu, 11 Feb 2021 07:19:29 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2021/02/drivers-license.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2021/02/drivers-license.png" alt="给我也整一个——《中华人民共和国机动车驾驶证》"><p>文章目录</p>

<p>两年多没更新博客了，趁此机会除除草，记录一下我曲折的拿驾照历程。</p>

<h3 id="">写在前面</h3>

<p>当年高考之后就想着得考个驾照以备不时之需了，但是考虑到家里的驾校不规范现象还是太多，而且练车不方便，时间也是个棘手问题，就一直拖到了大学开学。低年级的时候怕没时间就继续拖，结果发现越到后面越忙，最终大四上学期在深圳实习的时候，抽时间回广州报了个名，想着能在毕业前拿到驾照就好，没想到事与愿违，世风日下，世事难料……</p>

<h3 id="">准备工作</h3>

<p>2019 年 11 月份，找好教练之后约了个时间，周末回学校的时候就顺便搞搞报名的事情，首先是去做驾照体检，周末开的医院不多，按照教练的指引到了石碁人民医院，位置还蛮偏僻的，一通操作之后拿到了《机动车驾驶人身体条件证明》，然后坐公交再换地铁回学校，在地铁口等教练过来带我去驾校，发现这驾校的位置也挺偏僻的。</p>

<p>到了驾校之后先是要照相做照片，负责照相的人拿着相机就往角落的方向走过去，我说我自己带了电子版的和印好的照片，他就拿着我的 U 盘打开图片假装在 PS 里面操作了一下，上传到应该是车管所的什么系统里，然后对着我带的印好的照片说，你这个没用，我说我这个有用，我这是之前按照驾驶证照片要求的标准尺寸印的，传统照片是讲究标准的，诶……我一说，他啪就站起来了，很快啊！然后上来就是一个打印机输出，一个二维码亮相，一个收钱到手，我一看，哦，源赖士要薅钱，这驾校给我的第一印象就这么差，而且那个打印机的质量还不如我高中自己一百多块钱买的，打出来的照片看得我以为自己得了白化病，属实有点 emmmmmm。</p>

<p>接下来就是给我介绍了班别和价格，<del>盲猜了一下挂科概率</del>我直接选择了最便宜的不包补考的那种班，教练告诉我如果补考的话除了补考费还有额外的补训费要交，考虑清楚哦，我想了想还是决定一把梭直接开干，赌狗赌到最后应有尽有，然后就签合同交钱了，这个时候发现缴费竟然是直接微信转账转给教练就可以……让我不禁开始怀疑这驾校怎么这么多迷惑操作。交完钱之后教练送我回到了学校，周末过完就回到深圳继续实习了，然后就是准备考科目一。</p>

<p>顺便提一句，从 2019 年 6 月 1 日开始，因为公安部的 <a href="http://www.gov.cn/fuwu/2019-05/31/content_5396290.htm">新措施</a>，驾考报名不再需要居住证了，会省去一个麻烦的流程，并且同一时间开始，允许变更一次考试地进行异地分科目考试，这也是我后面刚好赶上的一个好政策，点赞！</p>

<h3 id="">科目一</h3>

<p>估计也是六一新政实行的前后，科目一的培训形式有了点变化，不再要求去驾校上课，只需要下载个车学堂的 APP 刷够学时然后约考就可以了，于是我就边上班边刷学时，这个 APP 实话说很难用，让人根本没有任何刷题和刷视频的心情，我只能放在显示器底下立着让它播放视频，然后每到他要拍照人脸识别的时候伸个手指点击确定，刷起来还挺快的，顺便就把科目四的学时也给刷满了，然后教练说可以到 12123 约考再下载驾考宝典刷题了，我约了个周一上午的，想着到时候周末回学校再多请半天假就可以搞定了。</p>

<p>约考成功之后，考前几天去下载驾考宝典刷题，刷了几道就感觉没劲了，这上千道题要我一个个做属实有点要命，于是就开启背题模式看简单的直接跳过，有点坑的就收藏一下准备第二轮再做，很快就看完了，把收藏的题目刷了两遍之后再做了几份模拟，看起来能及格而且满分的希望很大，于是我脑海里闪过了一个想法，要不争取一下看看能不能所有科目都拿个满分，给自己一种心理上的愉悦感。</p>

<p>考试前一天再做了一份模拟，感觉差不多胸有成竹了，保险起见上网搜了下其他平台的题目，挑着争议题和易错题做了做，感觉还是有些有坑的地方，不过通过肯定是没问题的，考试当天再挑着刷了刷，教练说因为就我一个人考所以就不送我去考场了，让我自己地铁+公交过去，这考场果然和体检医院、驾校一样偏僻。进到考场之后，布局和微机室差不多，坐上去点击确认就开始做题，因为是想着争取满分的，所以越到后面感觉压力越大，仔仔细细地看每一个字，虽然有几道没见过的题目，不过都没啥坑，做完满分签字就回去了，然后就回深圳继续打工。</p>

<h3 id="">转学</h3>

<p>本来我的想法是，实习到年前离职，过完年之后刚好大四下学期，学车拿驾照时间是充足的，还有毕业旅行什么的都可以按计划行事，简直完美。结果，一场突如其来的疫情，让本就不富裕的我，雪上加霜。</p>

<h5 id="">新驾校</h5>

<p>由于众所周知的情况，总之就是没法回学校没法学车，直到七月份才能回去领双证收拾东西滚蛋。看了下广州这个驾校的合同，考完科目一的这种情况要退学，违约金直接就 40%，然后额外扣除其他的各种杂项培训和考试费用，而直到这个时候我都还连车都没碰过，心在滴血。到了深圳之后，正想着这驾照可怎么办的时候，有天出去和朋友约晚饭回来，发现住的小区对面竟然就正好有一个驾校的训练场，针不戳。因为当天回去得比较晚，驾校已经下班了，于是在第二天（也刚好是去公司办入职手续的那天）上午起早了点去问了下情况，客服没上班，不过刚好有个教练在那边，过去和他大概聊了会，然后加了个微信打算就在这学了。</p>

<p>教练和客服问完情况之后，当天晚上就告诉了我转入的操作步骤，然后说过几天有新政策要执行马上涨价了，可以尽快交钱，不然涨价一触即发。这真是深圳特产，不管什么风吹草动都是涨价一触即发，实习的时候已经见识过了，尤其是房地产中介，堪称万事万物归于一触即发。保险起见我还是让教练拍个合同内容给我先看看，他直接发来了一份 PDF 文件，我看了下，补考只需要交车管所定的补考费，没有补训费用，退学违约金最多 20%，相比之下广州这个驾校的合同可以说是非常黑了。</p>

<p>确认没啥问题之后准备付钱，我以为需要去训练场的门店，结果竟然不需要，教练给打了个电话告知注意事项，然后发了个驾校的商户收款码，感觉整个操作流程给人的靠谱程度比广州那个驾校好很多，估计也有一部分原因是深圳管得比较严。</p>

<h5 id="">变更考试地</h5>

<p>因为前文提到的 2019 年六一新政，允许变更一次考试地进行异地分科目考试，简单来说就是通过这个流程办理的话前面的考试成绩可以保留，直接继续后面的科目学习和考试就行。考虑到能省下一点时间是一点，于是我就去 <a href="http://szjj.sz.gov.cn/CGZL/">深圳交警网上车管所</a> 预约了，为了稳妥一些我还特地搜了下怎么操作，最后竟然搞错了预约成“驾驶证其他业务”，因为我那时候还不知道这个操作叫做“变更考试地”，在我的认知里面它应该叫做“异地转入”，看了眼所有的选项感觉都不太对，就选择了这个看起来万能的名称，到了车管所之后才知道不行。前台的工作人员直截了当告诉我预约错了下次再来吧，虽然知道今天肯定是没机会了但还是有点不甘心，在那转了会，碰上了一个没预约的也想变更考试地的，同样被工作人员说请回吧下次一定，然后又碰上另一个和我一样约错了的。临走前我问清楚了他们的放号时间是每天下午 18:00，回去之后下午到点了我打开一看，好家伙，变更考试地的业务，每小时只排了五个可预约的号，然后一秒钟就全天的都被预约完了，这能玩？过了两天踩点提交终于搞到了一个中午的号，到时间后再去了一趟车管所，除了排队等了一个多小时之外别的都还好，实际办事的效率还是可以的，等待的时候看到车管所的公告机器上列了各个驾校的合格率，瞄了一眼没看到我要报名的这个驾校，然后第二次播放的时候定睛一看，哇擦竟然是第一名，有嗲东西。</p>

<p>从车管所办完变更考试地之后，材料自己带回来交给驾校，还需要几张照片，这个时候我把广州那个驾校说没用的照片带了过去问可不可以用，客服说可以，签合同之前同样是特地嘱咐了一下注意事项，然后提醒补考只需要交补考费，没有其他费用，违约金相关的也重点圈了下，让我看看没问题就签字。我瞄到桌子上贴的价格表，发现考完科一异地转入的费用竟然是和普通 C1 班直接报名一样的，等于我除了不用重新考科一之外什么都没有，和直接就地重新报名是一样的，血亏。而且暑期优惠活动期间，手动挡和自动挡同价，血亏。</p>

<p>另外，办理异地转入手续不再需要体检表，但需要提前准备好驾驶证照片回执，办回执是我在某天上班路上顺路去一个路口的照相馆搞的，不知道是为了套近乎还是真的，老板娘说自己以前也是做驾校的，我不禁佩服这搭讪能力。然后也想起来，广州那个驾校估计是把这个驾驶证回执的流程放到自己驾校里面搞了，真是肥水不流外人田，闭环能力挺强的。</p>

<h5 id="">旧驾校</h5>

<p>到这个时候，我的转学流程就都搞定了，只剩下找旧驾校退款了。</p>

<p>关于退款的事情，教练在微信里语焉不详，让我总感觉是在刻意隐瞒什么，还和我说直接找客服退款是没用的，有空的时候联系他就行，于是又是某个回广州的周末，我就顺便预约去办退款手续了，这次没有车接送，教练说今天没来大学城，让我自己先坐个公交车到驾校附近然后联系他。我下车之后在路口等，他<del>突然袭击</del>骑了个小电驴带我过去，然后让我坐会，他拿着收据合同什么的去办退款。</p>

<p>搞完之后回来，又到了那个签合同交钱的房间，给我一笔一笔算钱，前面的都没啥问题，到后面突然冒出几个什么辛苦费，我满脸问号，和他说这是哪里来的，他说“我这前后辛辛苦苦给你跑了那么多趟，你总得给我点吧”，于是我仔细想了想，除了收钱那天以外我就再也没见过他，这是哪门子的“辛苦那么多趟”，我就也和他明说了。这个时候教练直接开启了不要脸模式，“那我不能亏啊，总得赚点，你多少要给我一点”，我这人脸皮薄，没见过这世面，被直接当面说出这么不要脸的话，搞得我反而有点束手无策。签了合同的不按合同执行，原来还有这种操作。这个时候我也完全明白为什么不让我直接去联系客服办退款了，老中间商了，他又补了一句“你看你大老远跑来一趟，我们就给多给少也把这个事就处理掉”，他估计也是知道我不想麻烦，不然如果去投诉肯定能通过的，我暗自感慨这不愧是老油条，专做大学生生意的教练。于是把所谓的辛苦费压了一半就算了，他又微信转账把退款转回给我，只有 35% 的学费，只能说聊胜于无。</p>

<p>临走的时候教练告诉我往这里出去几分钟就有个公交车站，我立马就明白了，这 tm 肯定是个不通往大学城的公交车线路啊，不然我来的时候怎么不是坐这班车过来的，真有够社会的啊，收钱的时候开车接送，退款的时候坐个公交车到附近然后小毛驴接过去，退完款连小毛驴都没有了直接让走路回去。我打开高德地图看了下，果然这个公交车站的路线毫无用途，于是我只能导航走路回到下车的那个公交车站对面，再坐同一班公交车原路返回。</p>

<p>退款的这个事情实话说是挺恶心的，接送这种都是小事，只是我本以为广州的驾校应该不至于在合同之外还搞这么多小动作，无论驾校是否会因为退学的原因扣教练的钱，这个损失都不应该是我来承担的，讲道理不管怎么算最亏的难道不是我吗。每次想起这个事情的感觉，就和想起当年高中毕业照排好版的名单被照相馆拿去去掉了排版样式直接打出来一样，非常膈应。</p>

<h3 id="">科目二</h3>

<h5 id="">科二练车</h5>

<p>转入手续搞完之后也一直没啥时间，直到差不多九月份才开始练车，因为训练场就在小区对面，我就在能练车的日子里早起一些练习一个小时然后走路去公司，刚好还能赶上早餐，这样好.jpg</p>

<p>科目二的练车模式是提前预约然后一个人一辆车练习，以一个小时为单位，没有额外的等待时间，这一点还是挺不错的。第一个小时就是转了几圈方向盘，然后上车，练一会直行和倒车，全程靠离合控制车速，大概过了十几二十分钟就教左倒库了，倒了几次之后教练就下车去把控全局<del>顺便把控其他车的全局</del>，练了半个多小时，最后停车的时候车身歪了大概 1/3 在车位线的外面，和教练说走了的时候教练吐槽了句“你这都停到哪里去了”。</p>

<p>后来第二次练车，继续练左倒库，练了一会教练开始教右倒库，然后就是左右倒库轮流练习，一开始老是把点位记乱了。啊，当时记不住了，教练说停停，两分钟以后啊，就好了。这个时候车上还装了个辅助设备，可以判断你的位置和离线间距，亲测对练习还是挺有帮助的。第三次练车的时候继续练习左右倒库，快结束的时候教练看练得基本差不多了就让我从训练场的起点开始把完整流程走一遍，辅助设备也直接开启了考试模式，一路上交代了下点位和注意事项，后面几次练车就是直接用辅助设备的考试模式自己练车了，走了几圈基本没啥问题。</p>

<p>由于七月份车管所新政策对学时的要求增加了（真 · 一触即发），以前好像是有个卡然后教练帮你插在设备上刷就可以，现在是随车人脸识别，13min 进行一次拍照，不满学时不让考试，就很尴尬。为了凑够学时，有时候我就比正常练车更早一点起来，练个二三十分钟（因为那个时候还没人约车），然后去模拟器上面刷一个小时的学时，那个时候的模拟器一台能开机没法用，一台开机都开不了，然后就坐在那玩手机，还得提防着别被 13min 一次的拍照拍到玩手机。</p>

<p>练车大概总共五个多小时之后，教练就说给我安排约考了，这个驾校的客服也有点骚操作，因为练车以外的事情都是客服专门来负责的，他们就把学员的账号统一管理，考试时间和教练、学员确认之后由他们来操作预约，估计是为了方便统一接送，每个月固定间隔，每一批固定人数这样子。那时候刚好碰上国庆前后驾考系统升级，车管所把考试时间调整了一下，国庆假期里可以考试，我就让客服约了 10.5 的科目二考试，教练说 10.4 找个上午还是下午去考场模拟一下。</p>

<h5 id="">科二模拟</h5>

<p>考前几天驾校发了短信提醒考场模拟是自愿自费、量力而行，考前两天客服还特地打电话再次告知这是自愿自费的，考前一天教练就让我们先在训练场练了会车然后带我们去考场踩点和模拟。</p>

<p>和我同一天考试的有个大叔，还有个和我合租的舍友很像的小伙汁（下称“假舍友”），到了考场之后先看看路线，略微有点破败，教练说这里的考试车已经是算比较好的了，把注意事项说完之后就去交钱模拟，保险起见我还是模拟了一下，费用是 300 一小时，还挺贵，而且是贴在墙上的私人收款二维码，感觉怪怪的。我们三个人交了 900，教练去领了一辆车让我们轮流模拟总共三个小时。</p>

<p>我一开始坐在副驾驶上，所以就第一个去模拟了，一圈开下来没啥问题，除了倒车入库的库有点坡度，车子有点向后溜，另外就是模拟车太破了，后视镜的调节杆都断了，只能用手去直接按后视镜才能调，教练说正式考试的车子还是比较新的，都是电动调节的后视镜。接下来就继续轮流练车，大叔第一圈倒车压线，半坡起步距离不达标，后面几圈也老是半坡起步扣分，假舍友倒车也压线，不过后面几圈都没啥大问题。我感觉我好像有点亏，模拟没发现什么问题<del>，而且因为都没扣分导致花一样的钱模拟的时长反而最短</del>，还不如直接用考试来当模拟，大不了补考反正也比模拟便宜多了，何况正式考试还有两次机会，还有一个主要的原因是科目二练车都有辅助设备标识，即使没扣分也能看得出来哪里有欠缺、应该向哪个方向微调，多练几次基本就 OK 了，考试的顺序和路线都是和练车一样的。</p>

<h5 id="">科二考试</h5>

<p>考试当天先在训练场一人练了一圈，大叔看起来还是处于懵逼状态，我有点怀疑他能不能过了，然后去到考场之后取号排队等待，等了也挺久的。考试车的副驾驶没有安全员，上车做好准备就按指纹起步，考完没扣分，然后开回起点下车打印成绩单，回到车上等其他两个人。打成绩单的时候碰到考场有个大兄弟作弊被抓一脸懊悔坐在那，后来了解到好像是因为打小抄被发现……科目二打小抄……？Excuse Me？</p>

<p>等到他们俩出来，大叔第一圈挂了，第二圈在坡道起步被扣了两个 10 分，压线 80 通过。假舍友就不一样了，属实让我见识了一把什么叫骚操作，第一圈车开到右倒库停止线了发现没打指纹，然后这个时候按指纹，直接判零分，安全员过去把车开回起点，第二圈吸取教训，记得打指纹了，不过打得有点早，安全带都没系。他带出了一张比较优秀的成绩单，别人挂科的话成绩单上至少都有考试视频照片，这份成绩单就比较简约，因为项目还没开始就挂了，留白很多，很有艺术感，教练哭笑不得，吐槽了句“三年都碰不到一个你这样的人才”。</p>

<h3 id="">科目三四</h3>

<p>科目二考完之后，问了下客服大概什么时候可以科三练车，客服说和科二的模式不一样，要先等大概一个多月的时间排考，排到之后预约考试，考前再分配教练统一练车。在我后续又问了几次之后，客服还是没有告诉我啥时候可以练车，最后就只和我说先刷学时，结果一看，我不仅需要刷科三的学时，而且之前车学堂刷的科四的学时也没了，要重新刷。</p>

<h5 id="">车学堂学时</h5>

<p>其实原因很简单，就是做了异地转入之后驾校变了，但是这垃圾 APP 不会给你同步数据，我打开一看，记录的驾校都还是之前的那个驾校，咨询了一下客服，她去找车学堂客服重新备案了，驾校信息更新之后我的观看记录还在，但是他们说这是之前驾校的数据了，要我重新观看，于是我只能重新刷满学时，到 APP 上显示的学时是要求学时的两倍多了之后才终于满足要求。我一定要在这里骂一句 mmp，可惜人家是典型的政策垄断类 APP，再难用再怎么骂你还是得按照他们的要求来。</p>

<h5 id="">科三学时</h5>

<p>按照七月新政，学时刷够了才能约考，至少八个小时，其中六个小时必须是车上的学时，也就是说刷模拟器最多俩小时。科二在模拟器刷学时的时候就感觉很无聊了，我想着时间不能这么浪费掉，刚好之前某次和女票逛书店的时候发现了一本果麦出品的《<a href="https://book.douban.com/subject/35170896/">平面国</a>》<del>，但是没买</del>，然后也刚好实习的时候发现深圳图书馆有个 <a href="https://www.szlib.org.cn/libraryNetwork/view/id-5.html">城市街区自助图书馆</a> 项目，更刚好的是我上班的大厦大厅就有一台这个机器，于是我去看了下能借些啥书，发现旧版本的平面国能借，就趁机体验一把，同时也借了另外一本忘记因为哪本书里提及而在豆瓣标记想读的《<a href="https://book.douban.com/subject/6105339/">煤老板自述三十年</a>》。深图的这个项目是针不戳，预约之后两天就给你送到指定的自助图书馆机器上，然后就可以吃饭或者下班的时候顺路去一楼大厅取书，全程分文不收。不过也有个缺点是能预借的书其实不多，但也很不错了，点赞！</p>

<p>去模拟器刷学时的时候我就带上了《平面国》这本书去看，这是本在《三体》评价里面偶尔会提及的关联书籍，刚好看完这个再去看《三体》，计划通。到训练场之后发现这模拟器竟然换新了，还多了一个专门负责模拟器教学的教练，坐上去几分钟以后我就开始看书了，因为这个模拟器真的不能用啊，一套廉价无手感的汽车操纵设备，一台廉价垃圾电脑，一个廉价垃圾 Flash 游戏，这拿来练车？？？感觉稍微有点脑子的都看得出来这刷学时政策基本上就是车管所联合模拟器厂商搞的捞钱套路了，成本都是驾校和学员承担，我们车管所真是太厉害啦！</p>

<p>其实我还是体验了几分钟的，车子从停车场开出去，一个大大的“广州火车站”摆在眼前，让我感觉这个阴谋不简单，估计是要在整个广东推行这个捞钱政策，果然不久之后广州也开始有同样的学时要求了。没开一会，路边就一个醉汉拎着酒瓶扭扭捏捏在走路，搞得我有点在玩低配版 GTA 的感觉，再开一会就到了个前方道路维修的地方，全部拦起来不让过了，好嘛，建模也就做那么点地方，然后我往回开，拐了个弯开始赛车，跑着跑着就穿模了掉地下去，我又以为我在玩低配版 Minecraft，可是一直掉一直掉都没有掉出这个世界……？属实无聊，这 Flash 游戏质量感觉顶多是十年前的水平，捞钱捞得有点恶心人了。</p>

<p>看书中途抬头一看，发现这个模拟器上的摄像头和车上刷学时的那个不太一样，然后这个时候科二一起考试的大叔刚好也来刷模拟器学时，不过他启动完之后就走了，过了几分钟系统显示拍照失败，负责模拟器的教练就让大叔回来重新拍一下，过了会又出现了一样的情况，然后教练就去问模拟器那边的客服，那边反馈说按照车管所要求需要有一张过程照片，所以系统开始五分钟之后要拍一张照片，拍摄成功了才能离开。教练在和旁边的另一个训练场工作人员说要投诉他们，收了钱竟然不告诉他们这种注意事项，还说群里其他驾校的也在吐槽这个问题。好嘛，所以其实从上到下每个人都知道这是纯粹捞钱的政策，大家都是在应付。</p>

<p>模拟器只需要刷两个小时，比较好搞，而车上的六个小时就比较尴尬了，早上我也没那么多时间去，而且早上有学员要练车的，让我占用着车也不太好，客服说让我直接联系科二的教练安排刷车上学时，我以为好歹会让我开一会的，没想到啊没想到，竟然是纯粹去打坐。由于车上刷的学时有时间限制，过了零点的就无效了，我就只能早点下班然后过去刷三个小时，两天刷完六个小时，还特地预留多了几分钟以防万一。结果过两天到系统上一查，沃日，怎么还缺了五分钟，客服说中间有一些是无效的，申诉过了也没用，他们审核就是经常会漏几分钟。好吧，这车管所不仅捞钱捞得厉害，搞起这种事情都要来恶心人，点个确认就能解决的事情都能给你漏掉一部分。我只能另外找了一天去约在车上多刷半小时把漏的几分钟续上。</p>

<h5 id="">又是科三学时</h5>

<p>当我以为学时应该不再是问题的时候，客服突然又告诉我，正常情况下刷够 8h 就可以约考，但是因为我是异地转入的，需要刷满 18h 才能参加考试，不然到时候没法签到。我当场就想骂人了，但是车管所的规定显然也不是驾校和客服能左右的，只能继续刷了，10 个小时的学时想想都要吐，我问了下客服晚上基本没啥人刷，于是就打算找几个晚上直接过去刷满就行，同时又预借了一直想看的顾森大佬（<a href="http://www.matrix67.com/">Matrix67</a>）的两本书《<a href="https://book.douban.com/subject/10779597/">思考的乐趣</a>》和《<a href="https://book.douban.com/subject/25918542/">浴缸里的惊叹</a>》。</p>

<p>补刷学时的时候，碰上旁边有位大兄弟也在刷，不过他更惨，科二考完了还在刷科二的学时，刷完以后还得继续刷科三的学时。这是位骂友，我们一起骂了傻X车管所这个刷学时的政策，然后碰上他的科二教练进来，我发现竟然和我是同一个科二教练，这教练似乎每天来得最早走得最晚，他给我简单介绍了下我这个要补充的学时是市网算的，之前刷的八个小时是省网算的，两个是不同的平台，数据都会传上去，但是省网的零点之后的数据就不统计，市网的可以计入一两点的，我顿时产生了一个想法，按这样的话应该接下来再来两天就能刷满了，美滋滋。教练走后这位骂友化身夸友，和我夸这驾校合格率多高，这个科二教练多优秀啥的，然后和我说我可能不知道，深圳有些驾校很恶心的，听描述感觉就和我在广州报名的那个驾校差不多。我们聊得很愉快，屋子内外充满了快活的空气。</p>

<p>然后我继续看书他继续玩手机，突然脑子里产生了一个绝妙的念头，而且这里空白的地方挺大，写得下：既然计时五分钟之后就不再拍照，可以一直刷学时，我不如直接回去洗个澡再回来，反正就住在对面。于是我和骂友说了下让他帮忙先照看着，我一会就回来，回去的路上本来还想着如果小区门口的烤冷面在的话可以搞一个当夜宵，可惜烤冷面大爷这两天懈怠工作都不上班了。洗完澡回去一看，怎么屏幕黑了，骂友说他的也刚开机重新刷，他就出去打了个电话回来就都断电了，这波要是学时不记录那就是血亏，于是重新登录刷学时，骂友过了会就走了，我看到一点多也关机回去了。第二天查到发现学时还是在的，还行。</p>

<p>后面两次刷学时还是差不多的时间安排，刷一会回去洗了个澡继续刷到一点多，除了有一次下班晚了点没赶上，门锁了。有的时候发现外面的车上坐着和我当初一样的打坐者，肯定也是科三刷车上学时的，老远看还有点吓人。而且上次的关机问题也被我发现原因了，客服给模拟器的电脑安装了一个定时关机软件，到点了就全部自动关掉了，说是防止刷学时忘关电脑？后面的刷学时我就直接坐在日光灯下的座位了，看书方便，毕竟模拟器那个垃圾椅子不太舒服，而且前面挡着个方向盘不好伸展，光线还不好。</p>

<p>刷完之后我到市网对应的系统上查了下，终于是刷够了，我要感谢傻X车管所的这个刷学时政策，前后加起来连带我抽出的一些空余时间，我已经看完上面提到的五本书了。虽然现在也没太养成习惯，但这起码告诉了我抽时间还是有可能做得到的。《平面国》是个挺有趣的故事，篇幅上有种《小王子》的味道，趣味性又有点像余华的小说，虽然内容风格不太一样，但是一开始看就停不下来，很快就能看完。《三体》就不用多说了，要不是第二部篇幅长的话我可能就不会借后面几本书了。《煤老板自述三十年》是本有点故事会风格的书，坦白讲作者写作能力不是很强，不过有些篇章倒也挺有意思。Matrix67 的两本偏向数学科普或者说趣味数学题的书我是挑着自己感兴趣的章节阅读的，作者水平自然是很强的，造诣颇高，但是作为书籍来说的话，感觉似乎质量并没有我想象的那么高？</p>

<h5 id="">科三练车</h5>

<p>刷完学时之后客服还是迟迟没有动静，在我一再催问之下最后才终于给约了一月份的科目三考试。科三是考前三天练车，我约的是周二，所以刚好一个周末加周一总共三天练车，有了之前科二的血亏模拟经验，我和教练说不太想去考场模拟，教练劝了我一下说科三和科二不太一样，最好还是能模拟几圈，我想着三条路线一圈 200，我起码得花 600 块钱，好像不太值，仍然说不了不了。教练最后还是说那还是去看看考场，我答应了。</p>

<p>客服周五才给我推教练的微信，然后教练拉了个群，发了考场的三条线路模拟视频，两个来自优酷视频一个来自腾讯视频，我在 B 站找到了原作者投的原版视频，收藏并康了康。我们这一批四个人，一个周一考试三个周二考试，他上午带两个轮流练半小时，下午我们也是轮流练半小时，总共应该一人练了两小时，因为练车路线和实际考试的路线不一样，就只是分项目一个个针对性练了下，这个教练的车有点凄惨，后视镜和科二那个模拟车一样只能手动按压来调，辅助设备和语音设备都没有，项目提示语音是靠手机连接车载音频设备来播放的。</p>

<p>周日上午是四个人一起练车，每个人走一圈，大概四五十分钟，中午就直接去看考场了，我们的考场是在牛湖，后来听教练说因为这是深圳最简单的路线，不过地理位置有点偏，都快到东莞去了，开车大概五十分钟，到了之后先吃个午饭，价格和味道都还行，没啥特别要吐槽的，然后教练带我们去考试起点开始讲注意事项，出来之后上车开车把路线都走了一遍，一路交代要注意的地方，走完之后他们就去交钱模拟。之前我还在想车上坐着个安全员，那带四个人的话教练不就没地方坐了，后来观察了路上的模拟车发现原来教练是不上车的，科目三的模拟是安全员带着来模拟。</p>

<p>本来我还是有点犹豫要不要模拟的，但是看着路上的模拟车和行车速度我大概估算了一下，一圈哪怕按照四十五分钟计算，他们三个人每个人把三条线跑完，保守估计也得到晚上九点十点了，于是我果断跑路，自己坐地铁回去了。考场确实偏僻，自己开车的话五十分钟就到了，坐地铁就得将近两小时，这是深圳地形所限，很多公交地铁都是绕来绕去的。我走到地铁口就花了差不多二十分钟，牛湖站看起来还蛮新的，是四号线往福田口岸方向的起点。回去之后我又看了看路线图和考场模拟视频，感觉记得好像不是很清楚，而且因为练车时长不多，有点虚，于是周一下午又去练了会车，问了下教练他们昨晚模拟到几点，果然不出我所料，快十一点才回来。然后我们三个周二考试的，应该一人也练了四十分钟的样子就回去了，这个时候另外两个人练车的时候还时不时熄火，我感觉我应该稳了，然而别看我当时笑得欢，结果考试的时候自己就应验了。</p>

<p>最后客服又叫我刷科四的题，连续三次在驾校一点通上面模拟考试 96 分以上截图发给她才允许考科目四，要截图历史成绩单需要注册登录才行，我又是个很不喜欢注册新账号的人，虽然感觉好像不发也是没关系的，但还是老老实实搞了。</p>

<p>周日练车的时候教练也问了下我们科四的题刷得怎么样了，周一考试那个学员说刷了驾考宝典的，教练说那个软件题目太简单了，这个时候我意识到好像还真是，当时科一就是最后自己在网上找的别的平台的题发现还有一些有坑的，其中就有驾校一点通。我用当初科一的类似方法把科四的题目过了遍，时隔一年多发现很多记忆都没了，其实是有点浪费时间，然后在驾考宝典上做了个模拟，满分，切到驾校一点通上模拟差点不及格，然后直接进专项练习里面把易错题、争议题、多选题都刷了一遍，模拟了几次都是 98/100，感觉基本上就稳了。（此处麻烦驾校一点通结算一下广告费）</p>

<h5 id="">科三考试</h5>

<p>由于车管所只让选择日期，时间随机分配，我们三个人约到的时间分别是 11/12/13 点，教练统一带我们过去。考试前夜打开 B 站打算重温模拟视频，首页竟然给我推了谭警官的视频<del>，让我有种不祥的预感</del>。考试当天早上到考场附近是九点多，教练在附近找了个地方最后练习一下，一人跑了二十分钟，同行的有个妹子还是熄火了一次。都练完之后就往考场方向走，经过考试路段的时候遇到一辆在考超车的车，被后面一辆安全员高速开过来的车冲过去，差点撞上，如果当时考试车上的安全员踩了刹车的话那这个考生就血亏了一次考试机会。然而当时我并不知道，这竟然也会是我第一圈挂了的原因。</p>

<p>到考试为止我的科三练车时长大概是三个半小时多一点，我当时进考场的感觉就和当年走进《计算方法》期末考考场的感觉是一样的，处于一个深知自己熟练程度还不太够、有点听天由命的状态。取完号还是典型的长等待时间，甚至等到了考场午餐时间，让我们先去吃饭十五分钟之后再回来等，我走出考场教练让我们去考场对面可以先吃一顿午饭，最后就我一个人虽然不饿但还是进去点餐了。这个店看起来怪怪的，本身是个饮料店，强行加了四份快餐的菜单，这个相比模拟的时候那个饭店就差多了，是又贵又难吃<del>，怪不得教练自己不来吃</del>。</p>

<p>深圳的科三考试规则是一个考生一辆车一个安全员，和广州那种一个安全员带三个考生不太一样，告知线路想了下路线和项目顺序就上车做好准备工作然后按指纹起步，实地走的时候发现右转后到掉头的路有点长，竟然没有加四档，浪费了一个四档机会。然而当时我并不知道，这个念头竟然会是我第二圈挂了的原因。</p>

<p>掉头之后，后面跟上一辆出租车，到了左转的地方等前面的车走完了我秉承着考试的一个绿灯一个车的原则停车等待，后面的出租车不耐烦疯狂按喇叭，可惜很显然我这要是起步肯定是过不去的<del>，赶时间就不要跟在考试车后面好不好，都写着“正在考试请避让”了</del>。</p>

<p>第一圈快走完的时候，到了最后的一个红绿灯路口，跳转绿灯准备起步，对面方向有个面包车掉头冲进我要进入的车道，虽然好像并不会撞上，但我被吓得直接一脚刹车下去，听到了一个不太好的声音：转速表掉到 0，车子熄火了……我直接懵了，上一次熄火还是科目二练车的时候，一下子有点记不清该怎么操作，更关键的是我记混了一个事情，教练之前提醒过有些不能停车的地方如果你 10s 内能起步的话是不会扣分的，我当时满脑子想着 10s 内要赶快重新点火启动，手忙脚乱之中刚被我打着的车又一脚刹车踩了下去，语音提示“考试结束”。实际上熄火是一定扣 10 分的，只是当时语音没有播报出来而已，那个地方并不是不允许停车的地方，和 10s 内起步完全没有任何关系。如果我当时不去抢那十秒钟时间，正常操作点火起步的话也就扣十分，还是可以过的。</p>

<p>然后回到起点，换了个安全员开始同一线路的第二圈，前面也没什么问题，到了右转之后的路段我想着上一圈长距离没有加四档的遗憾，直接加上了四档开始跑，结果突然语音来了句“前方路口直行”。我就没想过会跑第二圈，本来就处于一个有点懵逼的状态了，四档的状态下遇到这个项目更懵逼，直到轻踩刹车通过路口语音报“考试结束”之后我都还没有意识到我提前加四档是作死，还问安全员发生甚么事了，安全员很有职业素养，一通操作准备开车的时候才告诉我“刚才超速了啊”，我才猛然反应过来。</p>

<p>按照考试规则，路口直行项目是要减速慢行，有两个判定标准，一个是有减速动作，也就是踩下刹车，另一个是有减速效果，要求车速不高于 30，你要点刹或者停车都可以。正常三档跑的话点刹然后过了路口之后上四档刚刚好，但是由于我忘记了这里还有一个路口直行的项目，作大死提前加上了四档。四档要求车速不能低于 30，否则会被判速度与挡位不匹配。</p>

<p>这两个问题我都是知道的，教练也有教过。按照事故复盘的标准，我当时有两个办法，最好的办法就是停车重新起步，只要别四档急刹就不会扣分，另一个办法是踩到 30 以下，被判速度与挡位不匹配扣十分，还是有机会过。然而当时距离路口很近了，来不及想那么多，并且我以为速度与挡位不匹配也是扣 100 分，想着有没有可能压着 30 过路口。于是第二次机会就被我作死提前加档给浪费掉了。</p>

<p>打印成绩单的时候被工作人员问了下“考科四吗”，我正懵逼呢，她看到了成绩补了一句“哦 没过”，然后就把成绩单给我了，这些工作人员挺调皮的，给人预约完科四告诉学员现在打电话给教练说科三没过，引得考场内外充满了快活的空气，可快乐是他们的，我什么也没有。</p>

<p>回到车里等另外两位学员考完，一个 90 一个 100，然后他们考前再做了几遍模拟科四就去考科四了，一人错了几道题，去宣誓室等驾照，拿到手就回去了。等待途中遇上另一个教练过来和我们教练交谈，说到昨天有个学员刚回深圳，就练了一个小时不到就带他来模拟，模了五圈，同一批一起的直接模拟到下半夜三点多才结束，好家伙，这科三教练有够好当的，只需要教一个小时不到，剩下的全部自费让安全员来教？不过仔细想想科三分项目本身不难，关键的确实就是路线和项目顺序，模拟效果肯定是最好的，也没什么毛病<del>，如果不用花钱就更好了</del>。</p>

<p>没考过确实还是有点难受的，对我接下来两天的心情有点影响，而且我本来差点就在考前把相关的 APP 直接卸载掉了，而而且且我还在科三练车的时候继续用那个图书预借功能借了陈新亚的《<a href="https://book.douban.com/subject/7054896/">图解汽车构造与原理</a>》和《<a href="https://book.douban.com/subject/26916930/">发动机图解</a>》，到考试的时候也还没看完。</p>

<h5 id="">又是科三练车</h5>

<p>本来我以为要年后才能重新考试了，但是因为之前了解过客服的效率，以及科三考试的另一个学员也吐槽了客服预约太慢，说她是自己去预约的，然后告诉我只要和教练确认时间安排没问题就可以自己约考，我就在十天后看了看考试日期发现能争取在年前搞定，然后又找教练问了下他年前还有没有要考试的学员，刚好赶上最后一批。</p>

<p>抽了半天时间去练车，一起练车的是个快退休的大妈，一样是重考的，聊起天来很热情<del>，不过我似乎并不是特别适应这种话多的交际</del>。从她和教练的口中得知小区对面这个训练场的科三教练全部被调到福田去了，以后这边的科三学车就统一带去别的地方集中训练三天，吓得我不禁感叹还好赶上了年前的最后一批。</p>

<p>大妈说她就是集中训练三天的，但是之前的教练啥都不说，很多细节都不提示，夸这个教练真好，这么看来我运气还不错，碰到的两个教练都挺好的。然后又说她当时模拟了六圈，我的大妈呀，一个比一个舍得，这比那个五圈的还狠。</p>

<p>科目三的考试费是 280，重考费是 140，这次就是我自己交钱了，想了想用不到一圈的模拟费用跑了差不多两圈，好像还挺赚？（</p>

<p>然后我翻了下合同，发现科目二的补考费只需要 65，又开始心疼我那 300 块钱的模拟费了 23333，当然，额外付出的时间和精力成本算进去肯定是不见得划算的。</p>

<p>考试前一天本来是不需要重新去看考场的，但是教练说二月一号又改了一些规则，最好还是去看看，所以我还是跟他们过去了。改动点里面有一个是没有语音提示的“通过公共汽车站”项目，换了位置，这真是有点意思，这种无语音提示的项目操作方式和注意事项全靠口口相传，光听语音提示操作根本不可能通过考试，拿到个过时的信息跟着操作也直接就挂了。</p>

<p>另外一个重要的改动点是，因为有个考生投诉安全员在开考的时候没有告诉她线路，考场一气之下决定以后都不提前告知线路，直接听语音提示操作了，虽然问题也不大。</p>

<p>这次看考场有两个不太一样的条件，第一是天气非常热，广东的冬天热到要穿短袖，但是忘记带遮阳伞导致我只能穿着外套防止被晒伤；第二是考试期间教练车不能进入部分考试路段，所以我们只能全程走路，一条路线 3km，总共三条，把重合的线路去掉也有差不多 5km，在这大热天的下午走得是又累又热。最搞笑的是这走路就一路上目睹好多开着开着突然挂掉打双闪被安全员带回去的车<del>，让人感觉有点害怕</del>。</p>

<p>考前重新刷了下科四的收藏题和易错题，基本也没啥问题，同时也把陈新亚那两本书看完了，《图解汽车构造与原理》的质量还不错，但这种机械部件感觉还是得依赖视频的形式才能达到更好的表现效果，《发动机图解》感觉有点没诚意，大量照搬《图解汽车构造与原理》里面的图和内容，直接原封不动大段复制粘贴自己的书再补充一点内容就当新书，这样不太好。</p>

<h5 id="">又是科三考试</h5>

<p>考试的 2.6 周六本来应该是个休息日，之前在等预约结果的时候直接给我显示计划考试人数 270，当前排位 271，没想到短信结果还是显示预约成功了。</p>

<p>这次去考场就是和当年进《计算方法》的补考考场一样的心情了，虽然对路线和项目顺序已经从基本记住变为完全记住，但是万一再挂了就有点不知所措了。这回没有在考场附近练车，过去了直接就取号准备考试了。</p>

<p>进入候考室，前面的电视屏幕有个实时监控，等分成四部分，每个部分显示一辆车的情况，等对着电视屏幕的考生刚好去考试的时候，我就坐了过去，盯着屏幕看，一分钟以后屏幕左下角的考生就挂了，又过了十几秒右下角的考生也挂了，很尴尬。</p>

<p>到了我上车，常规操作走到分岔路口，语音播报“前方路口右转”，好吧，还是二号线，那我可是轻车熟路了，路口直行点刹，加四档，停车换一档，掉头，又跟上来一辆货车，到左转的口发现安全员的 jio 动了起来，难道是怕我跟车太近？我就在差不多一个车位的距离的时候停住了。然后跳了绿灯，前面过去好几辆车，显然我更不可能过得去了，停车等待，后面的货车和上次的出租车一样疯狂按喇叭，那你让我一个考试车能怎么办呢。</p>

<p>到了上次熄火的红绿灯路口，发现安全员的 jio 又开始动了起来，不是吧阿 Sir，这也怕我闯红灯？停车等绿灯，起步，没熄火，稳了，然后就是超车和变更车道项目，搞完之后到了最右侧车道，终点近在咫尺，但是前面的车刚过去还没走，为了避免被逼停在路口挂掉，我就停车等待，发现从前面的车下来的竟然是和我一起过来考试的小哥。等安全员带着他开走之后，我最后一把起步，终点停车，空档手刹。这个时候终于知道他们刚刚为啥这么久了，这语音设备慢得很，手刹拉紧就判断结束了，但是会一直讲一堆废话，“正在上传xxx 正在查询xxx”之类的，终于报到“成绩合格 请将车开回起点”的时候，安全员下车让我脱掉反光马甲坐后面，终于稳了。</p>

<p>打印成绩单现场预约科目四，基本没人排队，随时可以过去考，但是保险起见我还是把收藏题看了遍再做了三份模拟，总共错了两道题，能不能全满分就看这次了，想想还有点小激动呢。</p>

<p>科四进入之后，发现这个考场设计有点奇葩，电脑放在底下，显示器倾斜，上面放一块透明塑料板，随便找个角度都有点反光，非常难受。和科目一的时候一样，仔细看每个字慢慢做，这把手气不太好，有几道有坑的没做过的题目，搞得有点心惊胆战的。最后还是满分过了，去宣誓室坐着看教育视频现场等驾照。</p>

<h3 id="">领取驾驶证</h3>

<p>最后终于还是四科满分的结果，虽然过程和预想的不太一样，走了一遍未曾设想的道路。领取驾驶证要求去宣誓室观看半小时的教育视频，不允许带电子设备，不过只有一个摄像头会在有人交谈的时候提醒保持安静，实际上并不会真的算你看了多久的视频。视频的质量挺高，而且因为是警示作用的，都是出了人命的那种案例，虽然没有血腥画面但也挺吓人的。轮播完一遍之后有个工作人员带着一叠驾驶证进来，到前面一个个念名字，没有我的，于是他们起立宣誓走人，我继续等了一轮，到第二轮还是没有我的，我已经有点不耐烦了，就出去活动筋骨，同时盯着制证室的进展。过了一会工作人员带着一小小叠驾驶证过去，我紧随其后，但是她走到宣誓室门口就站住了，然后看着驾驶证念了我的名字。</p>

<p>很显然又出问题了，屋漏偏逢连夜雨，等了两轮别人的宣誓领取之后终于我的也做好了，但是她说证件虽然做好了不过不能给我，因为系统出了问题有些档案上传的流程一直失败……和我一样情况的还有另外两个人，工作人员说每个考场都偶尔会有个别这种情况，他们也不知道啥时候能恢复，有时候半小时有时候一整天，昨天下午也出现了同样的情况一直失败。</p>

<p>我估计大概率是异地转入比普通报名多出来的流程卡住了，问了下另外两个人也确实同样是异地转入的，回到宣誓室又等了几轮，直到和我同来的大妈刚进宣誓室没几分钟证就来了，然后领完走人，我们几个有问题的仍然没法领。</p>

<p>工作人员让我们先去吃午饭，晚点再来看看行不行，我和教练说明了情况，教练说之前也没碰过这种问题，以前的异地转入也是一样正常拿证的，看来今天运气属实不太好，到了最后阶段了都能碰上这事。总共等了差不多两个小时，感觉希望也不大了，加上那天还是非工作日，最后决定还是填单邮寄吧，于是我的驾驶证又得推迟 N 天才能到手，还被薅了 EMS 市内到付 25 块钱，血亏。</p>

<p>直到今天，这个 EMS 快递单还没有任何动静，直接查不到信息。</p>

<p>考完第二天，交管12123 的 APP 和 <a href="https://gd.122.gov.cn/">交通安全综合服务平台</a> 上都已经自动关联上了驾驶证，考试和分数相关的信息也都看不到了。我去找了点资料大致了解了下自动挡的档位和操作方式，然后把驾校一点通的账号注销了，相关 APP 全部卸载掉，心情舒畅。</p>

<p>Update: 2.18，初七复工终于送达，但是 EMS 公众号还是查不到，于是问了下快递员是怎么回事，他说要在深圳 EMS 的号才能查到，我试了下仍然没有（而且看起来深圳 EMS 的公众号里，实际查询链接也是和之前一样的），回来之后又在广东 EMS 之类的能用的公众号都试了下，全部查不到，最后是在快递单上面给的网址 <a href="http://www.gdems.com">http://www.gdems.com</a> 里搜了单号才找到：</p>

<p><img src="http://kingsleyxie.cn/content/images/2021/02/ems-query.png" alt="给我也整一个——《中华人民共和国机动车驾驶证》"></p>

<p>行吧，全网就这么一个能查的地方，虽说这种省内件是内部消化，但是 EMS 这个信息共享实在是做得太差了，搞一大堆的细分地域的公众号又没有任何实际作用，各自为政<del>，百花齐放</del>。</p>

<h3 id="">终于结束</h3>

<p>一波 N 折，终于把考驾照的事情了结了，基本上能踩的坑都被我踩了个遍，最后总共花了最开始的价格的差不多两倍，额外浪费的时间和精力也是属实出乎我的意料，这要得益于傻X车管所的刷学时政策，同时再次真诚感谢深圳图书馆的城市街区自助图书馆项目，我怎么都不会想到学个车都能把这俩应该毫不相干的部门关联起来，或许这就是交叉信息学吧（bushi</p>

<p>总的来说，如果单纯考虑“学车”这件事的话，体验还是挺不错的，科目二科目三的教练都不错，练车体验也还可以。但如果是说“考驾照”这件事，那我的感受是非常糟糕，从广州的驾校、教练到深圳的车管所、考场、EMS 快递公司，整个流程都是很让人不爽的体验而且基本没地方吐槽，只能在这里发发牢骚。</p>

<h5 id="">一点教训</h5>

<p>虽然我应该是朋友圈里面比较晚拿到驾照的那一批了，但还是把自己总结的一些点列举一下，或许能帮到一些人：</p>

<ul>
<li>签合同交钱之前，甚至前往驾校之前，要一份合同模板仔细看看，如果感觉太黑的话可以多找几个驾校对比，在学车合同上就疯狂克扣的驾校要小心</li>
<li>现在大部分车都是自动挡了，如果学车价格和手动挡同价的话可以考虑直接报名自动挡的，报名约考之类的排队都会轻松很多，而且几乎没有熄火和档位这类问题</li>
<li>战线不要拉太长，一方面是不确定因素太多，另一方面是科一和科四重合的题目比较多，短期记忆能派上一些用场，节省时间</li>
<li>科目一四裸考有风险，有些题目是确实有坑，没做过很容易踩上的，当然不需要那么完美主义拿满分，要突击的话直接去把免费整理好的易错题和多选题做一遍基本上就 OK 了，及格还是问题不大的</li>
<li>科目三的路线和项目顺序一定要记清楚，尤其不要记漏了，每个操作之后都要想一下接下来是什么项目，如果平时练车是分项目练习的话，可以去考场模拟一下路线熟悉项目流程</li>
<li>驾校教练教的是完全面向考试的一套最小操作子集，和实际开车肯定是不一样的，对一些操作有疑问的话可以及时问教练，了解清楚情况，一般教练都会给你解释清楚</li>
<li>虽然记熟所有的扣分项对应的分数不太现实，但有可能的话还是可以看看，比如我的两次扣分如果都清楚知道对应的扣分标准的话也许就能做出最合适的选择了，满分除了心理上能爽一会以外并没有任何意义，和压线过是一模一样的结果</li>
<li>驾考的每个科目都确实存在一定的运气成分，万一挂了一次要及时调整好心态，把握好第二次机会</li>
</ul>

<h5 id="">一点感慨</h5>

<p>去年差不多这个时候，我觉得我今年过年前应该会整理一下整个 2020 年的经历写出来，毕竟涵盖了大四下学期和工作前半年这么重要的时光，然而过了一年发现对很多人事物的看法都发生了非常大的变化，有点写不出来了，只好用这么篇文章代替，好歹给许久没动过的博客增加一点新的东西。</p>

<p>最后，祝大家新年玉快、活家欢落、万四如意！</p>]]></content:encoded></item><item><title><![CDATA[写在二十周岁]]></title><description><![CDATA[<p>其实一直也没什么特别的过生日的习惯，不过特殊的日子还是想留下点不一样的东西。过年前的 <a href="http://kingsleyxie.cn/at-the-age-of-twenty/../memoirs-of-contests-and-life-so-far/">回忆录</a> 里，我也曾经写了很多所谓的感悟，但是还是在发布之前删掉了，因为我很清楚，这个年纪，在所谓的人生经历方面，能输出的东西太少太少了，过几个月再看看这些幼稚的文字肯定会想删掉。不过这篇文章既然是写在生日这天的，就随意点啦。</p>

<h3 id="">十年</h3>

<blockquote>
  <p>[lyric]立ち止まるほど　意味を問うほど</p>
  
  <p>きっとまだ大人ではなくて</p>
  
  <p>今見てるもの　今出会う人</p>
  
  <p>その中でただ前だけを見てる</p>
</blockquote>

<p>我很喜欢 《Letter Song》 这首歌，有段时间单曲循环了很久，不过话说回来，它又是和其他那些单曲循环过很久的歌一样，反复听意味着那段日子是我最脆弱的时候。歌里写的是另一首中的女主对十年后的自己的倾诉与询问，巧的是，今年六月也正是这首歌发布整整十年，doriko 发了条 <a href="https://twitter.com/doriko_/status/1011516365767770112">Twitter</a> 说他曾以为“十年后”的未来很远，如今却觉得这一天的到来比想象要快。</p>

<p>整十的数字总会被特殊对待。十年，有多漫长，十年，又究竟能够给一个人带来多大的改变。十年前的那个我，各个方面都和如今这么不相像，但也算没有低于一直以来对自己的期待吧，而十年后的我，是否仍然还能对现在的自己说出这样的话呢。</p>]]></description><link>http://kingsleyxie.cn/at-the-age-of-twenty/</link><guid isPermaLink="false">a4897bc3-d895-4627-8fcb-72eaf70a18f6</guid><category><![CDATA[About]]></category><category><![CDATA[Life]]></category><category><![CDATA[Thinking]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Wed, 31 Oct 2018 15:00:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2017/02/About.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2017/02/About.jpg" alt="写在二十周岁"><p>其实一直也没什么特别的过生日的习惯，不过特殊的日子还是想留下点不一样的东西。过年前的 <a href="http://kingsleyxie.cn/at-the-age-of-twenty/../memoirs-of-contests-and-life-so-far/">回忆录</a> 里，我也曾经写了很多所谓的感悟，但是还是在发布之前删掉了，因为我很清楚，这个年纪，在所谓的人生经历方面，能输出的东西太少太少了，过几个月再看看这些幼稚的文字肯定会想删掉。不过这篇文章既然是写在生日这天的，就随意点啦。</p>

<h3 id="">十年</h3>

<blockquote>
  <p>[lyric]立ち止まるほど　意味を問うほど</p>
  
  <p>きっとまだ大人ではなくて</p>
  
  <p>今見てるもの　今出会う人</p>
  
  <p>その中でただ前だけを見てる</p>
</blockquote>

<p>我很喜欢 《Letter Song》 这首歌，有段时间单曲循环了很久，不过话说回来，它又是和其他那些单曲循环过很久的歌一样，反复听意味着那段日子是我最脆弱的时候。歌里写的是另一首中的女主对十年后的自己的倾诉与询问，巧的是，今年六月也正是这首歌发布整整十年，doriko 发了条 <a href="https://twitter.com/doriko_/status/1011516365767770112">Twitter</a> 说他曾以为“十年后”的未来很远，如今却觉得这一天的到来比想象要快。</p>

<p>整十的数字总会被特殊对待。十年，有多漫长，十年，又究竟能够给一个人带来多大的改变。十年前的那个我，各个方面都和如今这么不相像，但也算没有低于一直以来对自己的期待吧，而十年后的我，是否仍然还能对现在的自己说出这样的话呢。</p>

<p>其实都不用谈十年，在忙碌的日子里，偶尔能有时间看看，就会发现一周前甚至两三天前的经历都好像已经很远很远了，觉得那时的自己好像是另一个人一样。生活节奏随着日常事务的增多而变得越来越快，快到有时候上午做完的事情到傍晚都感觉像是几天前的一样，赶完一个 ddl 几乎直接无缝对接到下一个。不过换个角度想想，每次的回顾都能发现自己进步了很多，好像也是一种快乐吧。</p>

<h3 id="">回首</h3>

<p>2018 年剩下最后两个月，回头看看这一年的开头，元旦前后将近一个月的时间在近乎绝望中度过。调整好心情之后，发现其实远还有更多更有意义的事情，能以更大的重量把我压到喘不过气。</p>

<p>那我有被压垮过吗？曾经手头几个项目和大作业同步在肝，能睡觉的时间少到可以忽略<del>，然而还是可以在保证质量的前提下抽出时间划水摸鱼</del>；也曾经写自己感兴趣的东西写到半夜三点多都不困，早上起来继续写<del>写到快傍晚才想起来中饭忘吃了</del>；还有突然发现某个大作业马上到 ddl 了还没开始做，于是连续几天三四点睡八点继续起床上课，但那段日子仍然过得非常开心<del>，甚至那个大作业还随手拿了个满绩</del>。</p>

<p>可我也真的体验过极力排斥的情绪，知道那种面对着完全不喜欢的项目是什么心情，知道路上走着像是没有灵魂的躯体一样完全不想回到座位上继续写代码是怎样的感觉。</p>

<p>将近一年的时间里，经历过身体上的彻底筋疲力尽，也从各种方面体验过心理上的极度疲惫和无力。这些经历让我发现，我最害怕的大概是，没有了自己热爱的东西能全心投入，然后胡思乱想导致突然的一股无力感和对未来的恐惧，就和去年这时候金工实习结束后经历的心路历程差不多。</p>

<h3 id="">珍惜</h3>

<p>这两个字，很早就接触到了，曾经自以为懂得什么是珍惜，甚至我记得大概四年前在 QQ 空间发过这么一段网红文字（突然自爆黑历史）：</p>

<blockquote>
  <p>我们耳边总是充斥着各种等待的声音：等我有时间了，我要如何如何；等我有钱了，我要什么什么；等我退休了，我要怎么怎么……于是，各种美好都无限地延后着，有些永远等不到了，有些即使你等到了，是不是还有当初的心情和当初的人？</p>
</blockquote>

<p><del>啊，真是沁人心脾。</del></p>

<p>那时候的我，看到这段话，以为就懂得了应该要好好珍惜一切，千万不要永远都只会等待。后来呢，不知道花了多大的代价，留下了多少遗憾和不甘，才发觉“珍惜”这个词的真正意义，不是别人几句话就能说明的。就像很多其他的道理一样，看起来简单到大家都懂，但是没有一定的经历，没有走过相应的人生阶段，真的很难理解。也许几年后再看今天的我，也还是会和站在现在看那时候一样的想法吧。</p>

<p>这一年，得到的成长和收获有很多，但失去的也不在少数，有些是自己主动放弃的，也有很多是无奈和遗憾，可能终归对待多数必须面对的事还是要学会去权衡利弊做出最适合自己的选择。我唯一希望的，就是能比从前更加懂得珍惜，别总是意识到的时候才觉得错过了好多。</p>

<blockquote>
  <p>[lyric] 奔波中心灰意淡</p>
  
  <p>路上纷扰波折再一弯</p>
  
  <p>一天想 想到归去但已晚</p>
</blockquote>

<h3 id="">前路</h3>

<p>那篇退役回忆录的最后，我曾写过一段话，作为对自己短期内的未来的期待：</p>

<blockquote>
  <p>我对自己所希望的只是，无论正处于的阶段是顺利还是曲折，都一定要保持冷静，清楚地认识和了解自己。然后还有一点就是：</p>
  
  <p>对留下的脚印 回头 挥挥手</p>
</blockquote>

<p>这其实是我在元旦前后那段日子里，对接下来一年自身成长方向的大致把握，因为当时的一些事情让我发觉我其实并不是那么了解自己，也远没有想象中的那般能接受曲折坎坷。因此，我希望可以试着把一切清零，多站在更加客观的角度审视自己，发现身上那些很明显但却一直没注意到的缺点，并学着把前行的动力来源只放在自己身上。</p>

<p>而接近尾声的如今，我能交出一份怎样的答卷呢。我想说一直以来我对自己不远的未来的预估都还是挺准确的。将近一年的日子里，心情的跌宕起伏算不上特别剧烈，但还是很真实地存在着的，而我也可以说我的确在各种状态下都一直保持着冷静，无论手头的事情是一切顺利还是持续不如愿。</p>

<p>说到了解自己，这个话题实在太大了，我并不觉得能给出一个什么样的回答。不过对比当时的我，也确实算是更明白了自己的期待和向往。但关于未来的准确方向，关于自己想要的究竟是什么，也仍然是像大多数人一样在一直尝试和探寻着。</p>

<p>接下来要面对的，可能会是更多的无奈和更大的压力。我能大致把握自己的水平是什么样，也能规划好前方一小段的路，却同样看到了很多也许很久都不可能达到的高度。对这些可望不可及的未来，可能唯一能说出的，也就只有一句“虽不能至，然心向往之”了吧。</p>

<p>大学生涯已经进入后半段了，但愿在即将到来的下一个环节，可以得到不辜负自己这么久期待与付出的结果。</p>

<h3 id="">感恩</h3>

<p>这两天收到了很多意料之中和意料之外的祝福与感动，在这里再次向所有人说声谢谢。</p>

<p>早上也收到了百步梯的 <a href="http://kingsleyxie.cn/at-the-age-of-twenty/../share-the-story-behind-birthday-email-repository/">生日邮件系统</a> 发来的贺卡（我 发 我 自 己），到今天这个系统已经稳定运行整整三个月了呢，部署的时候没考虑那么远，碰巧上线时间是三个月前的今天也算一种惊喜吧，又突然想感叹一下时间过得好快。</p>

<p>十月的最后一天马上过去了，这个月在前所未有的忙碌中伴随着各种快乐、欣喜<del>和熬夜掉发</del>，部门迎来了 18 级的小伙伴们，安排招新、入门培训和各种琐事，给 17 级的同学们布置近期的活动页面任务并陆续部署测试上线，以及公司这边实习项目的一期收尾，前几天还疯狂填上了开学前挖下的几篇博客的坑，如果说还有一点什么的话就是<del>抄</del>做了一堆堆堆堆堆堆堆布置起来都不管学生死活的课程作业。</p>

<p>这个月的结束也顺带是许多其他事情的告一段落，像是一段新的生活即将开始<del>，当然我不是想说要开始好好努力，反正我已经认定自己是条咸鱼了</del>。</p>

<p>最后的最后，还是想说声感谢，给这一年的经历以及过程中遇到的形形色色的人们，给从大一相识到现在的各位朋友，给百步梯技术部的大家，当然还有，给点进这篇文章的你。</p>

<style>  
html {  
    /*
        The following variables stands for:
            Start Color
            End Color
            Vertical Length
            Horizontal Length
            Width
        of the lyric style, respectively.

        Not decided yet about which color gradient to use?
        Try <a href="https://uigradients.com">https://uigradients.com</a>, it may be of great help!
    */

    --lrc-start: #414345;
    --lrc-end: transparent;
    --lrc-vlen: 30%;
    --lrc-hlen: 30%;
    --lrc-width: 2px;
}

blockquote.lyric {  
    /* Lyric styles as blockquote */
    text-align: center;
    background-repeat: no-repeat;
    background-image:
        linear-gradient(
            to bottom, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(
            to right, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(
            to top, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(
            to left, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(transparent, transparent);
    background-size:
        var(--lrc-width) var(--lrc-vlen),
        var(--lrc-hlen) var(--lrc-width),
        var(--lrc-width) var(--lrc-vlen),
        var(--lrc-hlen) var(--lrc-width),
        calc(100% - (var(--lrc-width) * 2))
        calc(100% - (var(--lrc-width) * 2));
    background-position:
        left top, left top,
        right bottom, right bottom,
        var(--lrc-width) var(--lrc-width);

    /* Style offsets for Ghost */
    padding: .5em 0;
    border: none;
}
</style>

<script>  
    //Lyric Blockquote Placeholder
    const lrcBPH = '[lyric]';

    document.querySelectorAll("blockquote")
    .forEach(function(ele) {
        //Check if the blockquote content starts with `lrcBPH`
        var txt = ele.firstElementChild.innerText;
        if(txt.indexOf(lrcBPH) == 0) {
            ele.firstElementChild.innerText =
            txt.substr(lrcBPH.length, txt.length);

            ele.classList.add("lyric");
        }
    });
</script>]]></content:encoded></item><item><title><![CDATA[分享一下百步梯生日邮件发送系统的设计与实现]]></title><description><![CDATA[<p>文章目录</p>

<p><del>不，这不是一个大作业的标题。</del></p>

<h3 id="background">Background</h3>

<p>百步梯有一个专属的人员管理系统，里面存着组织内所有人自己填写的个人信息，包括生日数据。大概在蛮久之前，我就有在想能不能做个生日祝福之类的功能每天自动发送给当天生日的同学，然后我就<del>写出来了（没有）</del>在暑假的时候让部门的设计小姐姐去做个贺卡的底图，同时我去想了想具体实现方案并做了些 Demo，最后在七月底的时候用了两三天把它写完，然后测试 &amp; 修修补补在 07.31 正式部署上百步梯的服务器。</p>

<p>然而尴尬的是当晚查日志发现那一天并没有人生日(눈‸눈)，我以为是查询语句写得有问题但是手动查了一遍发现确实没有……不过第二天总算还是完全正常运行的，并且因为配置的是每月 1 号给所有部长发送本部门的当月生日信息提醒，虽然已经充分测试过，但是看着两类邮件总共几十封发出去都没有出现问题还是很爽的<del>，没错我对自己的要求已经变得这么低了</del>。</p>

<p>因为写这个系统的时候还有别的事在忙，时间有限，肯定也还有蛮多写得不太好的地方，不过暂时不打算更新了，毕竟人员管理系统已经用了五年多，急需换代，如果我能在明年换届<del>退休</del>之前重写出来的话，打算把这个直接整合进去作为新系统的一部分（突然立 flag</p>

<p>下面来介绍一下这个功能的具体实现过程，部分踩过的坑以及棘手的问题就不详细讲了，它们的解决方案来源也都放在对应代码文件的开头部分，主要都来自 StackOverflow</p>]]></description><link>http://kingsleyxie.cn/share-the-story-behind-birthday-email-repository/</link><guid isPermaLink="false">86c1af26-6955-44ad-ab77-356a44c63d4b</guid><category><![CDATA[BBT]]></category><category><![CDATA[Program]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Wed, 17 Oct 2018 15:06:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/10/prototype.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/10/prototype.png" alt="分享一下百步梯生日邮件发送系统的设计与实现"><p>文章目录</p>

<p><del>不，这不是一个大作业的标题。</del></p>

<h3 id="background">Background</h3>

<p>百步梯有一个专属的人员管理系统，里面存着组织内所有人自己填写的个人信息，包括生日数据。大概在蛮久之前，我就有在想能不能做个生日祝福之类的功能每天自动发送给当天生日的同学，然后我就<del>写出来了（没有）</del>在暑假的时候让部门的设计小姐姐去做个贺卡的底图，同时我去想了想具体实现方案并做了些 Demo，最后在七月底的时候用了两三天把它写完，然后测试 &amp; 修修补补在 07.31 正式部署上百步梯的服务器。</p>

<p>然而尴尬的是当晚查日志发现那一天并没有人生日(눈‸눈)，我以为是查询语句写得有问题但是手动查了一遍发现确实没有……不过第二天总算还是完全正常运行的，并且因为配置的是每月 1 号给所有部长发送本部门的当月生日信息提醒，虽然已经充分测试过，但是看着两类邮件总共几十封发出去都没有出现问题还是很爽的<del>，没错我对自己的要求已经变得这么低了</del>。</p>

<p>因为写这个系统的时候还有别的事在忙，时间有限，肯定也还有蛮多写得不太好的地方，不过暂时不打算更新了，毕竟人员管理系统已经用了五年多，急需换代，如果我能在明年换届<del>退休</del>之前重写出来的话，打算把这个直接整合进去作为新系统的一部分（突然立 flag</p>

<p>下面来介绍一下这个功能的具体实现过程，部分踩过的坑以及棘手的问题就不详细讲了，它们的解决方案来源也都放在对应代码文件的开头部分，主要都来自 StackOverflow 和 PHP 官方文档，这篇文章侧重讲贺卡图片的生成和邮件发送方面的内容。</p>

<p>项目的 Repo 在 <a href="https://github.com/KingsleyXie/BirthdayEmail">GitHub</a>，里面有给<del>还算看得过去的</del>详细的简介和文档，以及可以在 <a href="https://github.com/KingsleyXie/BirthdayEmail/tree/master/Sample">Sample</a> 文件夹里看到使用测试数据生成的贺卡图片样本，按照艺术字体是否支持、姓名文本是否带有参考线分为四个子文件夹。这篇文章里面所有的演示用代码片段都经过完整测试放在了 <a href="https://github.com/KingsleyXie/Miscellaneous/tree/master/Snippets/BirthdayEmail%20Demo">这里</a>。</p>

<h3 id="">首先需要考虑的问题</h3>

<h5 id="">生日数据获取</h5>

<p>作为一个额外添加的功能，加上考虑到人员管理系统项目的重写快提上日程了，当然是单独配置作为一个新项目部署了。换届以来经历各种操作之后，对它的数据库结构也已经是很清楚了，于是就给配了一个只有读取权限的用户，所有需要的数据查询和排序逻辑都是写的 Raw SQL<del>，也不多，全部加起来才一百行左右的查询代码</del>。</p>

<p>这个里面比较骚的事情是数据库里存着一些测试用户，乱七八糟的用户名，为了不影响图片生成我得手动找到这些数据然后在查询语句里面过滤掉。后来还给自己加了点戏，仅给未毕业（按照 07-01 毕业来计算）的 BBTer 生成贺卡并发送邮件，于是查询代码稍微又长了一点点。</p>

<h5 id="">贺卡发送形式</h5>

<p>邮件发送是不用说的，但是具体是用哪种方式来发送还确实是考虑了一会，如果只发个大家都一样的贺卡的话太没意思了<del>而且也没什么技术含量</del>，所以肯定要让名字出现在邮件内容里面。</p>

<p>我们知道邮件可以直接发纯文本，也可以在里面使用一部分 HTML 标签。最开始的时候我是想用 HTML 的排版方式来形成一个贺卡的样子，后来感觉这样的排版风险有点大，毕竟各个邮件客户端对 HTML 的支持程度是不一样的，到时候还得在用新 API 和保证兼容性之间权衡，而且碰上一些奇奇怪怪的排版问题的话，因为整个内容是在别人的客户端上，很可能没测试到就导致一批人的显示出现问题。</p>

<p>然后我就想了下能不能直接把名字做进图片里面，整个贺卡作为一张图片，再配合一小部分通用的样式来进行布局让整体看起来美观一点，找了找发现 PHP 有直接的这类库可以用，这回稳了.jpg</p>

<h5 id="">测试用例</h5>

<p>生日贺卡里能体现唯一性的就是自己的名字了，所以它有着特殊的地位，处理的时候要仔细考虑各种可能出现的情况。最常见的就是两个字和三个字的名字了，但是毕竟我们得保证在其它情况下系统也能正常运行。查询了一下线上数据库，按长度正序倒序排列都看了一遍，发现情况也不是很多，二三四五个字的有，超过五个的也有，还有一长串的中间带个 <code>·</code> 的那种，既然涵盖了所有可能情况，测试用例都覆盖到的话应该就不会有什么问题。</p>

<p>另一个容易被忽视的问题就是，一些字体，尤其是特殊字体，支持的文字数量是有限的，碰到这种情况如果强行用不支持它的字体生成的话会导致那个位置一片空白（看具体处理机制也可能是一个带叉的方框），所以需要对这些不支持的情况做个 CSS 对字体的 fallback 系统类似的考虑。</p>

<p>下文中用“章保滑”模拟常见的三个字的姓名用于测试<del>，毕竟太膜法的名字也不敢用来当测试数据对吧</del>。</p>

<h3 id="demo">写个 Demo</h3>

<p>考虑好上面这些东西之后，确定核心的几个需求能实现就可以正式开工了，这时候设计稿也给到了我，可以写个 Demo 出来试试看：</p>

<pre><code class="language-php">header('Content-type: image/png');

$image = imagecreatefrompng('card.png');
$color = imagecolorallocate($image, 144, 139, 134);
$font = getcwd() . '/name.ttf';

$name = "章保滑";

imagettftext($image, 70, 0, 637, 165, $color, $font, $name);  
imagepng($image);  
imagedestroy($image);  
</code></pre>

<p>同文件夹内要准备好底图和字体，70 是字号，(637, 165) 是文字框的左下角坐标，(144, 139, 134) 是<del>用 QQ 截图工具得到的</del>文字颜色的 RGB 值。在浏览器访问这个 PHP 文件就能得到加上姓名的效果图了：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/card-demo.png" alt="分享一下百步梯生日邮件发送系统的设计与实现" width="50%"></p>

<div hidden>  
<img src="http://kingsleyxie.cn/content/images/2018/10/card-demo.png" alt="分享一下百步梯生日邮件发送系统的设计与实现">
</div>

<p>哦哟不错哦（主要还是设计小姐姐优秀）。</p>

<h3 id="">字体支持问题</h3>

<h5 id="">问题简述</h5>

<p>接下来解决一下刚刚谈到的艺术字体不支持显示的姓名的问题，因为字体文件这个东西，可以简单认为是给每一个编码的文字都对应设计的一个在计算机上显示的方式，而每个文字是有它自己的唯一编码的，那我们很容易想到如果出现了一个编码的文字，而这个字体并没有设计它如何显示的话，会被怎样处理。再稍微详细一点的相关知识，可以参考 <a href="http://www.ruanyifeng.com/blog/2014/07/chinese_fonts.html">中文字体网页开发指南</a>。</p>

<p>高中的时候我时不时会主动帮忙排版一些东西，班上有同学的字是不太常用的那种，然后很多时候为了效果我都会用上特殊字体，所以如果是要用上全班名单的什么文件的话，为了保证整体的统一，可选的字体就那么几种了。当然有时候会先用上艺术字体然后给那个字单独调整字体和大小，让它看起来和旁边的字没有太明显区别就行了。</p>

<p>关于这个字体支持问题，还有个题外话，就是之前有人提议一个字体如果碰到不支持的文字就直接给显示成带叉的方块，这样可以给排版的人一个提醒，因为他发现如果默认使用字体回落的方式的话，会导致带有不常见字的句子同时被多种字体应用，排出来的会是一行千奇百怪高低不平的字，这让他感到非常难受。</p>

<p>在翻数据库的时候我也意外发现了一个 “堃” 字是不被这次用的艺术字体支持的，所以可以用它来做测试样例，尝试实现一个自动字体回落的功能。不过其实它还不是特别不常见，我见过的最不被特殊字体支持的字是 “隺” 字<del>，没错就是我刚刚提到的高中同学的名字里的那个字</del>：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/chat.png" alt="分享一下百步梯生日邮件发送系统的设计与实现" width="50%"></p>

<div hidden>  
<img src="http://kingsleyxie.cn/content/images/2018/10/chat.png" alt="分享一下百步梯生日邮件发送系统的设计与实现">
</div>

<p>在这里感谢这位同学的友情出场（</p>

<h5 id="">解决方案</h5>

<p>那我当然是不可能自己想出来的，不过明显这么常见的问题搜一下就知道了。前面说过链接都在各个代码文件的开头所以这里就不给啦，主要的思路是提取出字体文件的相关信息，然后计算一个字符的码位（<a href="https://en.wikipedia.org/wiki/Code_point">Code point</a>），再从字体文件的信息里判断出这个码位是不是被它支持。</p>

<p>获取码位的代码是下面这样的，我根据评论区的一个提示稍微修改了一下让它能支持的范围广一点：</p>

<pre><code class="language-php">private function ord_utf8($c) {  
    $byte0 = ord(substr($c, 0));
    if ($byte0 &lt; 0x80) return $byte0;

    $byte1 = ord(substr($c, 1));
    if ($byte0 &lt; 0xE0) return (($byte0 &amp; 0x1F) &lt;&lt; 6) + ($byte1 &amp; 0x3F);

    $byte2 = ord(substr($c, 2));
    return (($byte0 &amp; 0x0F) &lt;&lt; 12) + (($byte1 &amp; 0x3F) &lt;&lt; 6) + ($byte2 &amp; 0x3F);
}
</code></pre>

<p>入参 <code>$c</code> 就是单个的字符了，还有因为名字里面不会出现 emoji 所以这里的 <code>ord_utf8</code> 是没有考虑四字节的 UTF-8 字符的情况的，字体一般也不会去单独给 emoji 做设计<del>，主要是那个解决方案没写而我又自己编不出来</del>。</p>

<p>提取字体文件的信息的操作比较复杂，所以直接用的一个叫 <a href="https://github.com/PhenX/php-font-lib">Fontlib</a> 的库。通过字体文件的信息判断一个字符是不是被这个字体支持的代码是这样的：</p>

<pre><code class="language-php">private function charInFont($char, $font) {  
    $subtable = null;
    foreach($font-&gt;getData('cmap', 'subtables') as $_subtable) {
        if ($_subtable['platformID'] == 3
            &amp;&amp; $_subtable['platformSpecificID'] == 1) {
            $subtable = $_subtable;
            break;
        }
    }

    $ord = $this-&gt;ord_utf8($char);
    if (isset($subtable['glyphIndexArray'][$ord])) return true;
    return false;
}
</code></pre>

<p>秉承着作为一个辣鸡的“能用就行”的信念我就没去深究他是怎么做的了<del>，毕竟看了也搞不懂啊</del>。</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/can-use-just-ok.jpg" alt="分享一下百步梯生日邮件发送系统的设计与实现" width="50%"></p>

<div hidden>  
<img src="http://kingsleyxie.cn/content/images/2018/10/can-use-just-ok.jpg" alt="分享一下百步梯生日邮件发送系统的设计与实现">
</div>

<p>入参 <code>$char</code> 是单个的字符，<code>$font</code> 由调用 <code>charInFont</code> 的数组遍历部分传入。剩下的问题就是要把一个姓名分割成其各个单字符组成的数组，这个毕竟是 UTF-8 编码的多字节字符组成的串，不能直接用切割一般字符串的方法，不过很快就找到个操作，再整合一下把字体转为所需的库的 Font 对象的操作以及遍历数组各字符进行判断的逻辑，用于判断整个姓名字符串是否被一个字体文件支持的代码就有了：</p>

<pre><code class="language-php">public function isStringValid($str, $font_path) {  
    $font = Font::load($font_path);
    if ($font instanceof Collection) {
        $font = $font-&gt;getFont(0);
    }

    $chars = preg_split('//u', $str, null, PREG_SPLIT_NO_EMPTY);
    foreach ($chars as $char) {
        if (!$this-&gt;charInFont($char, $font)) return false;
    }
    return true;
}
</code></pre>

<p>这里用到的两个第三方类分别是 Fontlib 库里面的 <code>FontLib\Font</code> 和 <code>FontLib\TrueType\Collection</code>，两个传入参数分别是姓名和字体文件的路径。</p>

<h5 id="">单元测试</h5>

<p>继续之前当然是要先测试一下这个东西写得有没有问题，只要 xjb 构造那么十几二十个样例姓名，然后遍历跑一遍就可以了，为了稳一点我们准备个比较通用的字体文件同样跑一遍然后对比一下，结果参考 <a href="https://github.com/KingsleyXie/BirthdayEmail/blob/master/Sample/CLITest_result.txt">Sample/CLITest_result.txt</a>，可以看到是完全符合预期的，爽。</p>

<h3 id="">字体大小</h3>

<p>因为给到用来填名字的就一个框，可是各种名字的长短不同，全部统一的字体大小肯定效果不好。我也没想到什么一劳永逸的解决办法，因为总的情况也不多，就一个个肉眼观察看起来最舒服的大小然后手动记录了 2333：</p>

<pre><code class="language-php">public function getSize($text) {  
    $fontSize = [
        87,   // == 2 characters                   =&gt; '李华'
        70,   // == 3 characters                   =&gt; '章保滑'
        63,   // == 4 characters                   =&gt; '章保滑华'
        52,   // == 5 characters                   =&gt; '恶魔喵喵喵'
        35,   // &gt; 5 characters                    =&gt; '恶魔喵喵喵喵'
        43    // characters with interval          =&gt; '尼古拉斯·章保滑'
    ];

    return $fontSize[$this-&gt;parseIndex($text)];
}
</code></pre>

<p>（不要在意注释里奇奇怪怪的示例</p>

<p>当然如果是艺术字体不支持的名字导致要用备用字体的话，所有的大小就得重新调了，最后的字号总量是 6 * 2 = 12 个。<code>parseIndex()</code> 是获取一个名字应该用哪个字号的方法：</p>

<pre><code class="language-php">public function parseIndex($text) {  
    if (strpos($text, '·') !== false) return 5;    // characters with interval

    $len = $this-&gt;charLen($text);
    if ($len &lt; 6) return $len - 2;                 // [2, 5] characters
    return 4;                                      // &gt; 5 characters
}
</code></pre>

<p>如果名字带点的话直接返回最后一个字号，后面处理的时候会把点号换成换行符然后让点号前后的字分两行排。其它的就是直接返回长度了，长度大于 5 的就给个比较小的字号直接往左排起。<code>charLen()</code> 用的是和上面分割姓名为数组同样的实现方法：</p>

<pre><code class="language-php">public function charLen($text) {  
    return count(preg_split('//u', $text, null, PREG_SPLIT_NO_EMPTY));
}
</code></pre>

<p>这两个函数的测试结果同样在 <a href="https://github.com/KingsleyXie/BirthdayEmail/blob/master/Sample/CLITest_result.txt">Sample/CLITest_result.txt</a>，稳的。</p>

<h3 id="">居中对齐</h3>

<p>既然姓名是放在一个框里面的，而各个姓名的字号大小又不一样，如果都是往左开始排的话难免会有一些难看，所以就要想想能不能让姓名在这个给定范围的框里面居中对齐了。</p>

<p>最开始我用的是和字号一样的一个个手动调的方式记录下一个数组然后去分析当前姓名适用哪种情况，但是写起来感觉相当吔。后来发现有个 <a href="http://php.net/manual/en/function.imagettfbbox.php"><code>imagettfbbox</code></a> 函数，能用和我们写姓名到图片上的 <a href="http://php.net/manual/en/function.imagettftext.php"><code>imagettftext</code></a> 函数一样的参数得到这部分文字的边角坐标值，于是用这个得到的文字宽高值和允许放姓名的外框的值就可以计算出文字相对框边界的位置，让它水平和竖直都居中。</p>

<p>不过这个实现是有点问题的，后面“细节还是很重要”一节会详谈。</p>

<h3 id="">绘制参考线</h3>

<p>接下来画几条参考线来看看文字是不是确实如我们所想的居中在外框里，并且内容和原有的文字在同一个高度。为了不占太多空间这里就把参数表里面的换行去掉了：</p>

<pre><code class="language-php">// Outer Rectangle
imagerectangle($image, TextBox::$RECT['left'], TextBox::$RECT['top'], TextBox::$RECT['right'], TextBox::$RECT['bottom'], $color);

// Inner Rectangle
imagerectangle($image, $bounding['upper_left_x'], $bounding['upper_left_y'], $bounding['lower_right_x'], bounding['lower_right_y'], $color);

// Text Height Lines
imageline($image, 000, $bounding['upper_left_y'], 950, $bounding['upper_left_y'], $color);  
imageline($image, 000, $bounding['lower_right_y'], 950, $bounding['lower_right_y'], $color);  
</code></pre>

<p>那个不太正确的计算文字边角坐标位置的代码就不放了，如果想要的话可以在 <a href="https://github.com/KingsleyXie/BirthdayEmail/commit/8c188f795f457a9e7e89122eed882559dea9da39#diff-2ce6a809392e99829290c566b9c9d253">Commit 历史</a> 里面拿到，不过要注意两边一个是算的左上和右下，另一个是左下和右上，记得修改一下对应的索引值。</p>

<p>加上参考线之后画出来的结果是这样的：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/ref-line.png" alt="分享一下百步梯生日邮件发送系统的设计与实现"></p>

<p>哦豁，完蛋.jpg</p>

<h3 id="">细节还是很重要</h3>

<p><del>追求卓越，幸福人生。</del>  </p>

<h5 id="">解决问题</h5>

<p>参考线一画出来就发现文字和实际的位置差了一点点，虽然肉眼看可能不太会发现，但是这个细节既然发现了还是要去解决一下的，这个问题的解决算是我在这个系统的实现过程中耗时最多的一部分了。</p>

<p>在我搜索的过程中发现有人和我碰到过一样的问题，就在 <code>imagettfbbox</code> 函数官方文档下面的 <a href="http://php.net/manual/en/function.imagettfbbox.php#75407">讨论区</a> 里，另外还有一个 Stack Overflow 的 <a href="https://stackoverflow.com/questions/36929656/imagettfbbox-calculates-wrong-rectangle-when-text-begins-with-number">提问</a> 也是类似的情况。经过了解整理和各种测试验证，最后明白问题出现的原因是这样的：文字本身的定位并不是从它的左下角开始，而是有一条略高于底部线的基线 <code>baseline</code>，这个函数用的计算方式就是基于这个 <code>baseline</code> 的，但是绘制文字用的 <code>imagetftext</code> 却是用底部线作为起点的，这就导致了文字位置的微小偏移。比如我们测试一下：</p>

<pre><code class="language-php">print_r(imagettfbbox(70, 0, $font, $name));  
// Array ( [0] =&gt; 9 [1] =&gt; 5 [2] =&gt; 218 [3] =&gt; 5 [4] =&gt; 218 [5] =&gt; -65 [6] =&gt; 9 [7] =&gt; -65 )
</code></pre>

<p>给出的边角定位点坐标是：</p>

<p>$$
\begin{matrix}
(9, 5) &amp; (218, 5) \\
(9, -65) &amp; (218, -65)
\end{matrix}
$$</p>

<p>可以看到边界并不是从零开始，而是往另一个方向延申了一段，也就是它计算得到的值并不是相对边界的距离而是相对基线的距离，实际上这个情况下 \(y = 0\) 就是刚刚说的基线了，水平方向的位移也是类似的问题。我们用个不带底图的方式再实验一下（<a href="https://github.com/KingsleyXie/Miscellaneous/blob/master/Snippets/BirthdayEmail%20Demo/baseline.php">baseline.php</a>）：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/baseline-demo-1.png" alt="分享一下百步梯生日邮件发送系统的设计与实现"></p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/baseline-demo-2.png" alt="分享一下百步梯生日邮件发送系统的设计与实现"></p>

<p>下方很明显被截掉了一小段，可以推测出现在的底部线就是它原来基线的位置，为了让问题更明显一点，我们换用等线字体尝试一下 <code>ABCfpqnyg</code> 这串字母会被绘制成什么样：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/baseline-demo-3.png" alt="分享一下百步梯生日邮件发送系统的设计与实现"></p>

<p>英文字母四线三格的最后一格在底部被截断了，这就很明显是因为它的基线是倒数第二行线，所以基线下的内容都会因为被当作超出了边框范围。</p>

<p>解决方法其实就是把这个偏差值给加回来，<code>imagettfbbox</code> 给出的相对高度值还是没问题的，只是它的坐标往两边都有延申导致了绘制时有一边会被忽略掉，毕竟一般默认的绘制起始位置还是 \((0, 0)\) 的。应用到这个项目代码里面，还需要一些修改，因为并不是所有的文字都是完全居中对齐的，竖直方向上当然是要让中线和底图原有的文字在一个高度上，这样才不会导致看起来有高度差，但是水平方向上，如果文字多的话，应该是左边顶到边框往右边排，还有如果计算出来的文字宽度超过了给定的框的范围的话也应该用左对齐的方式而不是居中对齐。所有情况考虑进去之后，获取边界点左边的代码大概 50+ 行，所以还是不贴上来了，有兴趣的话可以自己看 <a href="https://github.com/KingsleyXie/BirthdayEmail/blob/master/Text/TextBox.php">TextBox.php</a>。</p>

<p>字体设计真的是一门艺术。关于基线和排版相关的内容，本来能讲的东西还有很多，但是因为我其实也算不上特别熟，加上文章好像也挺长了，就不献丑了，告辞。</p>

<h5 id="">再次检验</h5>

<p>现在用新的边界点值重新生成一下带有参考线的贺卡看看：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/prototype-with-ref-lines.png" alt="分享一下百步梯生日邮件发送系统的设计与实现"></p>

<p>这回真稳了（不是</p>

<p>现在的整体效果是这样的了：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/10/prototype.png" alt="分享一下百步梯生日邮件发送系统的设计与实现" width="50%"></p>

<div hidden>  
<img src="http://kingsleyxie.cn/content/images/2018/10/prototype.png" alt="分享一下百步梯生日邮件发送系统的设计与实现">
</div>

<p>所有其他长度的姓名也都有测试结果，文章开头就提到过</p>

<blockquote>
  <p>可以在 <a href="https://github.com/KingsleyXie/BirthdayEmail/tree/master/Sample">Sample</a> 文件夹里看到使用测试数据生成的贺卡图片样本，按照艺术字体是否支持、姓名文本是否带有参考线分为四个子文件夹。</p>
</blockquote>

<p>到这里，生日贺卡生成的问题就算是全部解决了，整理一下代码然后只对外开放一个输入姓名的接口，就可以准备和邮件发送模块对接了。</p>

<h3 id="">邮件发送模块</h3>

<h5 id="">工具准备</h5>

<p>首先要决定用什么工具来发送<del>，自己写是不可能自己写的，这辈子都不可能自己写的，只能用用别人的东西做个 API Caller 假装是个程序员才能维持得了生活这样子</del>，为了性能和稳定性，最好还是选择使用大厂的邮件服务作为第三方发送者，然后找个广泛被使用的邮件处理库比如 <a href="https://github.com/PHPMailer/PHPMailer"><code>PHPMailer</code></a><del>，东拼西凑</del>就差不多能解决邮件发送的问题了。</p>

<p>要想不通过邮箱服务商提供的界面来发送邮件的话，我们需要从他们那里获取一些配置需要用到的信息，比如我是用 SMTP 来发送的，那么就需要对应的SMTP 服务器、SMTP 安全协议、SMTP 服务器端口号，以及发件人的用户名、密码、地址、名称，一般为了保证账号安全，密码不会直接让用原密码，而是要求单独生成一个授权码之类的用于项目中，这些具体的值服务商都会提供，这里就没必要讲了。</p>

<h5 id="">图片导入形式</h5>

<p>最后一个需要决定的事情就是贺卡图片是通过附件添加还是给个链接处理还是直接嵌入正文。在大概高二的时候，偶然看到过知乎上的这样一个问题：</p>

<blockquote>
  <p><a href="https://www.zhihu.com/question/28411992">电子邮箱为了安全，默认不下载图片，为什么图片会有潜在的危险呢？</a></p>
</blockquote>

<p>一般为了安全，以及不向发送者泄露自己的用户是否已经读取、何时读取、读取时的 IP 地址等一个网络请求正常都会附带的信息，邮箱服务商是要在用户主动要求下才会去下载外链图片的，不过现在像 Gmail 之类的邮箱都已经是直接先下载到自己的服务器上了，可以很好地解决安全和信息泄露等一系列问题。因为我不知道国内这些邮箱以及各种各样的客户端对这个事情的处理态度是什么，加上平时也见过尤其是 QQ 邮箱默认是不显示图片的，所以附件添加和链接处理的想法可以被否掉了。</p>

<p>我们以 <code>base64</code> 的编码方式嵌入一张图，它就作为整个邮件内容文本的一部分了，浏览器或者其他客户端会在显示 HTML 格式的邮件内容的时候把这段编码再解码成原图片。<code>PHPMailer</code> 对此做了很好的接口可以直接用：</p>

<pre><code class="language-php">$this-&gt;mailer-&gt;addEmbeddedImage($card, 'card');
// Then use `cid:card` as value of src in HTML code
</code></pre>

<p>这样添加好嵌入图片的编码之后，我们就可以在邮件正文的 HTML 里面用 <code>&lt;img src="cid:line"&gt;</code> 的方式放进这个编码图片了。写好整个的 HTML 代码之后，再给不支持 HTML 渲染的邮箱客户端写点简短的纯文本生日祝福（不支持显示图片的话我也没办法不是），记得设置好 <code>isHTML(true)</code>，应该就没什么别的邮件方面的问题要额外考虑了：</p>

<pre><code class="language-php">// HTML code mail body
$this-&gt;mailer-&gt;Body = $body;
// Plain text as a fallback for clients that does not support HTML render
$this-&gt;mailer-&gt;AltBody = $altbody;
// Set the default format as HTML first
$this-&gt;mailer-&gt;isHTML(true);
</code></pre>

<h5 id="">另一些细节</h5>

<p>考虑一下手机端和电脑端的布局情况，为了避免图片太宽不好看，设置个最大显示宽度让它自己缩放到合适大小，这个时候还需要让图片在水平方向上居中，直接一个 <code>margin: auto</code> 就 OK 了。再稍微调整一下几个细节的地方，加上常规的编码和兼容性设置，最后写出的 HTML 内容也就是 <code>$body</code> 的值是这样的：</p>

<pre><code class="language-html">&lt;meta charset="UTF-8"&gt;  
&lt;meta http-equiv="X-UA-Compatible" content="IE=Edge"&gt;  
&lt;meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"&gt;  
&lt;div style="margin: auto; max-width: 577px;"&gt;  
    &lt;img style="width: 100%;" src="cid:card"&gt;
    &lt;img style="width: 100%;" src="cid:line"&gt;
    &lt;p&gt;生日快乐 (づ￣ ³￣)づ&lt;/p&gt;
    &lt;p style="text-align: right;"&gt;—— 百步梯&lt;/p&gt;&lt;br&gt;
&lt;/div&gt;  
</code></pre>

<p>额外嵌入的 <code>line</code> 图片是一条分割线，因为大部分邮件客户端都是白色的底色，而贺卡的底图也是白色，这样的话文字和图片的区分就不明显，看起来略有些奇怪。这段 HTML 没有完全按照规范来写，样式都是直接内联的，也没区分 head 和 body，但是对这个贺卡邮件而言已经足够了。</p>

<p>测试过程中发现 Gmail 会把邮件中的图片文件名显示出来（本来 <code>base64</code> 编码后应该是没有文件名在里面的，估计是 PHPMailer 给嵌入的图片加了个 download 属性，用文件名作为它的值？没仔细去查），所以保存生成后的图片的时候要注意一下贺卡文件命名方式。</p>

<p>谈及测试我要顺便吐槽一下 QQ 邮箱的电脑网页端竟然连 <code>base64</code> 编码的图都默认不给显示，要加进白名单才行，emmmmm……这说安全也说不通，说是为了过滤广告就更不可信了，明明我在那里收到过的所有广告邮件的图都是不用点就显示出来的，真是吔。不过手机端还是有直接显示出来的，所以就懒得理这奇葩操作了。</p>

<p>最后补充一下在这个系统里面日志记录的重要性，邮件发送方是用第三方提供的服务，接受方也会是各种各样的服务商，图片的生成、保存、发送等过程都有出现意外的可能，很多因素都不在这个系统的控制范围内，比如对方邮箱可能设置的拦截阈值很低导致我们发出的邮件被拒收，所以留下日志方便出现问题的时候快速定位是很有必要的。</p>

<p>因为图片文件本身就会一直按日期保存下来，所以我在日志里记录的是每次从数据库取出的数据，以及邮件发送成功后往日志文件末尾直接添加一行发送记录，还有出错时先在日志文件写下出错信息再给配置中的管理组每人发送一封报错邮件。</p>

<h3 id="">部署上线</h3>

<p>改好数据库等配置信息，给执行操作的用户在当前目录下的写权限，先跑一遍 CLITest 确定文字处理和图片生成都没有问题后，用个样例 SQL 来执行一次贺卡发送的功能，一切正常的话就能准备正式使用了。</p>

<p>为了方便，整合出了两个单独的 PHP 文件，直接运行或者在浏览器访问它们都可以，一个 <code>remind.php</code> 用于给所有部长发送当月生日信息提醒，另一个 <code>index.php</code> 用于给当天生日的同学生成、保存并发送生日贺卡。不过为了防止误操作以及基于安全的考虑，不建议部署到 Web 目录下提供给浏览器访问，实在必须这样做的话务必把这两个文件名改成很长的随机字符串。我的做法是放在 <code>home</code> 文件夹内，直接通过命令行执行 <code>php filename.php</code> 来完成操作。</p>

<p>接下来用 <code>crontab</code> 设置定时任务，按照我的想法是每个月的 1 号早上 10:00 发生日信息提醒，每天 11:00 生成和发送生日贺卡，所以内容就是这样了：</p>

<pre><code class="language-shell">0 11 * * * cd path/to/directory &amp;&amp; php index.php  
0 10 1 * * cd path/to/directory &amp;&amp; php remind.php  
</code></pre>

<p>注意 <code>crontab</code> 的执行不是在当前项目文件夹里的，所以要么指定好绝对路径，要么用 <code>cd</code> 进入项目文件夹然后再运行，个人感觉后者看起来舒服一点。对 <code>crontab</code> 不太熟的话可以用一下 <a href="https://crontab.guru/">这个在线工具</a>，能根据输入的前五个参数的值来计算接下来的几个匹配时间，用来检验一下写出来的是不是和实际想的一样，这样有问题也能尽早发现。</p>

<h3 id="">最后的话</h3>

<p>这个系统正式上线到现在已经快有三个月了，还没有出现过任何问题<del>，超爽的</del>。</p>

<p>虽然部署之前已经测试过了很多次覆盖率足够高的样例，也做了一旦出错就自动给我发报错邮件的功能，但是刚开始的几天还是偶尔感觉有些担心，毕竟项目在实际生产环境上的运行是永远比想象的要复杂很多的，特别是这种每天定时用完全不同的数据执行的任务。于是会时不时登上服务器看看日志，或者进邮箱看看已发送的邮件有没有问题，几天之后也没收到报错邮件，看了看图片文件夹列表和日志也都显示每天的运行是一切正常的，于是就差不多安心了。</p>

<p>填完坑的现在，也就是 10 月底，系统总共发出去的邮件有几百封了，收到一些同学的感谢回信，好像也是做为开发者的一种别样快乐呢(ಡωಡ)</p>]]></content:encoded></item><item><title><![CDATA[结合 LeetCode 上的几道简单题再谈谈位操作（二）：异或的魅力]]></title><description><![CDATA[<p>文章目录</p>

<p>接着上一篇文章来讲些简单的位操作，这篇侧重的是异或的一些用途，不过其实前一篇文章的后面两道题也都有部分用到异或的内容，可见它的用途还算是挺广泛的。下面来看一些更加贴近异或操作思维的题目。</p>

<h3 id="problem136singlenumber">Problem 136. Single Number</h3>

<p>把一个非空数组里只出现一次的数字找出来，剩下的数字保证是出现两次的。这个直接用异或的特性就行了，一个数如果和自身进行按位异或的话，因为每一位都对应相等，所以结果就是一串 0。这样我们把整个数组遍历异或一遍，重复的数字都消掉了，结果就是那个只出现了一次的值，毕竟一个数字和 0 异或之后得到的是它本身：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    int singleNumber(vector&lt;int&gt;&amp; nums) {
        int ans = 0;
        for (int i : nums) {
            ans ^= i;
        }
        return ans;
    }
};
</code></pre>

<p>但是提交之后发现运行效率竟然才超过 33% 的代码，在我好奇还有什么更快的解法的时候，去看了下跑得最快的<del></del></p>]]></description><link>http://kingsleyxie.cn/bit-manipulation-revisited-from-some-easy-problems-on-leetcode-part-two/</link><guid isPermaLink="false">eab7d947-eac5-4fc7-a004-f40f72e72051</guid><category><![CDATA[Program]]></category><category><![CDATA[Bit Manipulation]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Fri, 05 Oct 2018 07:25:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/10/single-number-ii.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/10/single-number-ii.png" alt="结合 LeetCode 上的几道简单题再谈谈位操作（二）：异或的魅力"><p>文章目录</p>

<p>接着上一篇文章来讲些简单的位操作，这篇侧重的是异或的一些用途，不过其实前一篇文章的后面两道题也都有部分用到异或的内容，可见它的用途还算是挺广泛的。下面来看一些更加贴近异或操作思维的题目。</p>

<h3 id="problem136singlenumber">Problem 136. Single Number</h3>

<p>把一个非空数组里只出现一次的数字找出来，剩下的数字保证是出现两次的。这个直接用异或的特性就行了，一个数如果和自身进行按位异或的话，因为每一位都对应相等，所以结果就是一串 0。这样我们把整个数组遍历异或一遍，重复的数字都消掉了，结果就是那个只出现了一次的值，毕竟一个数字和 0 异或之后得到的是它本身：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    int singleNumber(vector&lt;int&gt;&amp; nums) {
        int ans = 0;
        for (int i : nums) {
            ans ^= i;
        }
        return ans;
    }
};
</code></pre>

<p>但是提交之后发现运行效率竟然才超过 33% 的代码，在我好奇还有什么更快的解法的时候，去看了下跑得最快的<del>香港记者的</del>代码，发现是完全一样的思路和实现，不过他多加了一行取消 C++ 的 IO 流与 <code>cstdio</code> 同步以及取消绑定 <code>cin</code> 和 <code>cout</code> 的设置，所以运行快了点：</p>

<pre><code class="language-cpp">static int fast_io = []() {  
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    return 0;
}();
</code></pre>

<p><del>啊真是骚</del></p>

<p>不过本来关闭与 <code>cstdio</code> 的流同步和取绑 C++ 自己的 <code>cin</code>、<code>cout</code> 不是什么新奇的操作，就是写算法代码的时候不太会单独再去考虑这个。这段代码不太走寻常路的地方是，这种让填充代码的题目不允许修改 <code>main</code> 函数的内容，所以他用的全局静态变量的方式来保证设置会在一开始被运行，又学到一招，太强大了.jpg。</p>

<p>异或的这个特性还是挺广为人知的，大多数人对按位异或的第一次接触应该都和我一样是刚开始学编程语言中的位操作符的时候，要求不用临时变量来实现两个变量值的交换：</p>

<pre><code class="language-cpp">x ^= y; // x_new  
y ^= x; // y_new (= y_final)  
x ^= y; // x_final  
</code></pre>

<p>上面各行注释对应接下来两个公式，表示的是左边的那个结果值，不带后缀的表示原始值。把第一行 x 的运算结果代入后面两行代码，展开分别可以得到</p>

<div>  
$$
y_{final} = y \oplus x_{new} = y \oplus (x \oplus y) = x  
$$

$$
x_{final} = x_{new} \oplus y_{new} = (x \oplus y) \oplus x = y  
$$
</div>

<p>感觉是对异或的“一次存储两次清除”（我口胡的简称）特性的完美利用了。</p>

<h3 id="problem389findthedifference">Problem 389. Find the Difference</h3>

<p>给定两个小写字母串 s 和 t，t 中除了比 s 多一个字母以外，剩下的内容和 s 相同，但是不保证顺序和多出来的字符的位置。这个题本质上和前面那个从数组里找出只出现一次的数字是完全没区别的，从零开始遍历异或所有的字符就行了：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    char findTheDifference(string s, string t) {
        char ans = 0;
        for (char c : s) ans ^= c;
        for (char c : t) ans ^= c;
        return ans;
    }
};
</code></pre>

<h3 id="problem461hammingdistance">Problem 461. Hamming Distance</h3>

<p>计算两个整数的汉明距离，也就是二进制表达形式各对应位上值不同的总位数。</p>

<p>因为异或操作本身是在比较两个 bit 是否相反，在两个整数按位异或之后得到的值中，每一个 1 都代表它们在这个位上的取值是不同的，于是这个结果的二进制表达中含有的 1 的个数，就是需要求的汉明距离了，1 的个数的简洁和快速求法前两篇文章都提过了所以不赘述。</p>

<pre><code class="language-cpp">class Solution {  
public:  
    int hammingDistance(int x, int y) {
        int n = x ^ y, ans = 0;
        while (n) {
            n &amp;= n - 1;
            ans++;
        }
        return ans;
    }
};
</code></pre>

<h3 id="problem693binarynumberwithalternatingbits">Problem 693. Binary Number with Alternating Bits</h3>

<p>判断一个正整数的二进制形式是不是连续的相反位，即类似 <code>10101...</code> 这样。</p>

<p>虽然最高位一定是一个 1，可以往后一个个按位对比 0 和 1，但是毕竟计算机里面存的整数是长度固定的，会有前导 0 的填充，定位到最高位比较麻烦。我们可以从末尾入手，每次判断最低的两位值是否相反，然后右移一位继续判断，直到碰到相同的两位，这时候可能是右移到了最高位碰到两个 0，也可能是数字中间本来就存在两个连续的位，所以跳出这个循环之后只要值为 0 就能保证它是满足条件的数，反之则不是：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    bool hasAlternatingBits(int n) {
        while ((n &amp; 1) ^ ((n &amp; 2) &gt;&gt; 1)) n &gt;&gt;= 1;
        if (n) return false;
        return true;
    }
};
</code></pre>

<p>末尾的值当然是 <code>n &amp; 1</code> 了，而倒数第二位通过 <code>n &amp; 2</code> 可以拿到，不过为了能和末尾的位进行异或，需要把它的位置也调到最后一位，直接右移一位就行了。</p>

<p>同样需要注意的是这里会导致入参的值被改变，实际用的时候需要确定这个值是不是允许修改。</p>

<h3 id="problem137singlenumberii">Problem 137. Single Number II</h3>

<p>上面这些都是非常简单的题，所以也没什么好讨论的 xjb 写一段话就可以给代码，接下来的两题相对来说会比较有意思一点。</p>

<p>其实这三部曲我做的顺序是先 Single Number 然后 Single Number III 最后 Single Number II，不过为了不逼死强迫症<del>也就是我自己</del>还是按顺序放吧。</p>

<p>这个相对第一题来说，唯一不同的是除了那个只出现一次的数字以外，其它数字出现的次数都是<strong>三</strong>次。用异或可以解决偶数次重复的问题，毕竟结果都是 0，可 3 是个奇数，连续异或得到的是自身，会和只出现一次的混在一起，看来并不能仅通过异或来解决。</p>

<p>这道题的解决思路是我高中时一次机缘巧合碰到的，如果之前没有了解过这种方法的话我觉得我肯定想不出来这样的实现，它是从结果值的构造上入手的：首先创建一个为 0 的整数作为初始结果，然后遍历所有数字，统计每一位上含有的 1 的总个数，对三取模作为当前位的值，最后把这个二进制数作为十进制输出：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    int singleNumber(vector&lt;int&gt;&amp; nums) {
        int pos = sizeof(int) * 8, ans = 0;
        while (pos--) {
            int cnt = 0;
            for (int num : nums) {
                cnt += (num &gt;&gt; pos) &amp; 1;
            }
            ans += (cnt % 3) &lt;&lt; pos;
        };
        return ans;
    }
};
</code></pre>

<p>我们来一点点分析代码，首先因为 int 所占位数在各个平台上有可能不同，所以不要写死，而是通过 <code>sizeof()</code> 获得它的字节数然后乘以 8 (1 byte = 8 bits) 得到当前环境下的 int 存储位数，接着去计算所有数字在每一位上含有的 1 的总数。</p>

<p>如果重复了三次的数字某个二进制位是 1 的话，3 对 3 取模就为 0 了，那么结果就是那个只出现了一次的数在这个位上的值。如果这个位上是 0 也是一样的，直接就得到仅出现一次的数在本位的值。这样循环结束之后，就相当于提取出了我们需要的结果的每一个二进制位上的值，像是把它一位位构造出来的一样，并且这个方法可以推广到任意的数字而不是仅仅适用于偶数次的重复情况。</p>

<p>这个解法理解起来很容易，不过代码略显复杂和啰嗦<del>，位运算的大部分解法应该都是相当优雅的</del>。</p>

<p>讨论区有个帖子 <a href="https://leetcode.com/problems/single-number-ii/discuss/43294/Challenge-me-thx">Challenge me , thx</a>，里面给出了很妙的解法：</p>

<pre><code class="language-cpp">public int singleNumber(int[] A) {  
    int ones = 0, twos = 0;
    for(int i = 0; i &lt; A.length; i++){
        ones = (ones ^ A[i]) &amp; ~twos;
        twos = (twos ^ A[i]) &amp; ~ones;
    }
    return ones;
}
</code></pre>

<p>作者在下面的一条评论给出了简要的解释，前面一个部分的异或和第一题是差不多的想法，就是如果一个数字不在就放进去，在的话就会导致这个数字被从中删掉。不同的操作是后面的对另一个值取非然后和刚刚得到的值按位与，这是为了保证被放进 <code>ones</code> 里面的数是不在 <code>twos</code> 里面的，反之亦然。</p>

<p>也就是说，第一次出现的数会被放进 <code>ones</code>，第二次出现的话就会被从 <code>ones</code> 中删除然后放进 <code>twos</code>，这样第三次出现的数会再和 <code>twos</code> 异或一次被删。最后 <code>ones</code> 里面存的就只有那个只出现一次的数，而 <code>twos</code> 最后的值则是 0。</p>

<p>我们可以用类似数字逻辑里对状态机状态跳转的理解来看待它，先考虑数组里面的数字仅有一位的情况，因为会出现三次所以需要两个比特来表示当前状态，以下用 <code>H</code> 代表高位，<code>L</code> 代表低位，我们的统计状态转换应该是这样的：\(00 \Rightarrow 01 \Rightarrow 10 \Rightarrow 00\)，为了详细一点说明下面给出真值表：</p>

<table>  
  <tr>
    <th>Status H</th>
    <th>Status L</th>
    <th>Data</th>
    <th>Jump H</th>
    <th>Jump L</th>
    <th>Occured</th>
  </tr>
  <tr>
    <td>0</td>
    <td>0</td>
    <td>0</td>
    <td>0</td>
    <td>0</td>
    <td></td>
  </tr>
  <tr>
    <td>0</td>
    <td>0</td>
    <td>1</td>
    <td>0</td>
    <td>1</td>
    <td>Once</td>
  </tr>
  <tr>
    <td>0</td>
    <td>1</td>
    <td>0</td>
    <td>0</td>
    <td>1</td>
    <td></td>
  </tr>
  <tr>
    <td>0</td>
    <td>1</td>
    <td>1</td>
    <td>1</td>
    <td>0</td>
    <td>Twice</td>
  </tr>
  <tr>
    <td>1</td>
    <td>0</td>
    <td>0</td>
    <td>1</td>
    <td>0</td>
    <td></td>
  </tr>
  <tr>
    <td>1</td>
    <td>0</td>
    <td>1</td>
    <td>0</td>
    <td>0</td>
    <td>Clear</td>
  </tr>
</table>

<p>然后化简整理一下 <code>Jump H</code> 和 <code>Jump L</code> 的逻辑表达式就可以了，注意下式中所有的取非上划线除了最后一个以外都是<strong>只作用在单个字母上</strong>的而不是对整体的：</p>

<p>$$
L_{jump} = \overline{H}\overline{L}D +\overline{H}L\overline{D} = \overline{H}(\overline{L}D + L\overline{D}) = \overline{H}(L \oplus D) <br>
$$</p>

<p>$$
H_{jump} = \overline{H}LD + H\overline{L}\overline{D} = (H \oplus L) \cdot \overline{(L \oplus D)} <br>
$$</p>

<p>\(H_{jump}\) 最后的那个异或不是化简得到的，只是另外一个公式而已，那么得到逻辑表达式之后我们就可以写出代码了：</p>

<pre><code class="language-cpp">int lower = 0, higher = 0;  
for (int num : nums) {  
    int temp = lower;
    lower = (lower ^ num) &amp; ~higher;
    higher = (higher ^ temp) &amp; (~(temp ^ num));
    // OR: higher = (~higher &amp; temp &amp; num) | (higher &amp; ~temp &amp; ~num);
}
return lower;  
</code></pre>

<p>但是这样很明显有个缺点是需要一个临时中间变量，而且公式也显得复杂了些，特别是后面那行被注释掉的替代方案。我们可以考虑在计算 <code>higher</code> 时使用新的 <code>lower</code> 值来替代掉旧的值，当然公式也需要重新整理，注意现在的 L 不再是 <code>Status L</code> 而是 <code>Jump L</code> 了：</p>

<p>$$
H_{jump} = \overline{H}\overline{L}D + H\overline{L}\overline{D} = \overline{L}(H \oplus D) <br>
$$</p>

<p>形式和 \(L_{jump}\) 的计算一模一样，而且不再需要中间变量，这个代码看起来就很舒服了，<del>一家人</del>一段代码就是要整整齐齐：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    int singleNumber(vector&lt;int&gt;&amp; nums) {
        int lower = 0, higher = 0;
        for (int num : nums) {
            lower = (lower ^ num) &amp; ~higher;
            higher = (higher ^ num) &amp; ~lower;
        }
        return lower;
    }
};
</code></pre>

<p>所以所谓的 <code>ones</code> 和 <code>twos</code> 分别是第一次和第二次出现的数字集合的说法，实际上就是一个状态码的高低两位而已，两者相配合存储着当前数出现的次数情况。毕竟它们本身只是个整数，用数字集合的方式来理解难免还是不太直观。</p>

<p>我们刚刚考虑的是数字仅有一个比特位的情况，但是因为所有数字都是相同格式的，拓展到 n 位也完全成立，相邻位之间并不会相互影响，毕竟我们需要的只是最后留下来的数字的二进制表达形式而已，不用关心它本身是什么，也不用关心它的各个相邻位之间的关系，因此可以把每个比特位拆开看待，独立进行各自的运算。</p>

<p>也正是因为题目需要的仅仅是一个结果，而不用了解中间过程的数字、状态是什么，我们可以忽略一个数字把另一个数字的统计状态修改掉的情况，比如某位为 1 的数 A 被加进了 同样位置的二进制位为 1 的数 B，看起来好像是 B 被统计多了一次而 A 被忽略掉了，但是实际上由于对称性，B 还是会对应修改到 A 的这一位上，最终所有的相互修改都被叠加抵消掉，只剩下 lower 里面存着我们需要的值。</p>

<p>这个解决方案也可以推广到任意的数字而不仅仅是 3，不过显然它不能和构造的那个思路一样直接通过修改一个数字来应对变更，而是要根据次数决定状态码的位数，再重新整理出逻辑表达式，拓展性可能不是太好。</p>

<p>另外，在运行时间分布图上最快的一个是这样做的：</p>

<pre><code class="language-cpp">int one = 0, two = 0;  
for (const auto num : nums) {  
    two |= one &amp; num;
    one ^= num;
    auto three = ~(one &amp; two);
    one &amp;= three;
    two &amp;= three;
}
return one;  
</code></pre>

<p>但是提交发现运行时间并没有多大差别，好像又是 IO 同步那方面的耗时严重一点，我觉得要考虑一下以后是不是每次都直接加上前面那段优化 IO 的代码上去了（</p>

<p>他这个解法也利用到了刚刚提到的那个对称性的特点，为了简化问题先只考虑一个数字重复三次的情况，再推广到数组里的所有数字，只要把一个数字的情况考虑完善就能不加额外条件直接适用题目的情景了。模拟走一下循环段里面的流程：</p>

<ul>
<li><code>two |= one &amp; num</code>：按位与把 <code>num</code> 和 <code>one</code> 中都有 1 的位置提取出来，实际上就是把重复的数提取出来，然后通过按位或传递进 <code>two</code> 里面，也就是 <code>two</code> 里面现在会存着第二次出现的数字</li>
<li><code>one ^= num</code>：这个就是前面讲了很多次的如果 <code>num</code> 是第一次出现就放进 <code>one</code> 里面，不是就把它从 <code>one</code> 里面删掉</li>
<li><code>three = ~(one &amp; two)</code>：按位与再提取出 <code>one</code> 和 <code>two</code> 中都有 1 的位置，取反存进 <code>three</code>，所以 <code>three</code> 里面现在存着的是第三次出现的数字的反码</li>
<li><code>one &amp;= three</code>：因为 <code>three</code> 里面是反码，就相当于如果 <code>one</code> 里面的数已经是第三次出现，就和自己的反码进行一次按位与，等于把这个数从 <code>one</code> 里面删掉</li>
<li><code>two &amp;= three</code>：同上，把出现第三次的数从 <code>two</code> 里面删除</li>
</ul>

<p>这样到最后 <code>one</code> 里面就是那个唯一一个仅出现一次的数字了，它和最开始给的那个对 3 取模的思路是类似的，就是消掉出现多次的数，留下出现仅一次的数。如果要推广到任意数字的话，应该得继续往下写 <code>four</code>、<code>five</code> 之类的了<del>，听起来就很可怕</del>。</p>

<h3 id="problem260singlenumberiii">Problem 260. Single Number III</h3>

<p>这个题说是 III 但是比 II 更简单一点，还是其它数字在数组中出现了两次，只是现在出现仅一次的数字有两个了，我们总不可能直接遍历异或然后从得到的结果中给拆分出两个数来。但这个异或的结果还是有价值的，根据这个位操作的特性我们可以知道，结果中含有 1 的位，对应的原来两个数的相同位置上的值一定是相反的。</p>

<p>从这个题目和第一题的相似程度来看，我们可以考虑把数组分为两个子数组，各自包含一个只出现一次的结果，这样就可以用完全一样的解法对两个子数组分别求得结果，那么问题就只在如何分组上了。</p>

<p>刚刚提到整个数组的所有数字异或之后得到的值中，含有 1 的位表示两个答案中那个位置的值是相反的，我们可以就用这个作为掩码来区分两个数，因为现在需要的只是分组而已不用考虑别的位的情况，而对于其它数，无论该位的值是 0 还是 1，两次异或到最后都会被抵消，进而两个子数组各自只剩下一个我们需要的结果。</p>

<p>所以现在，问题就只剩下如何从一个数的二进制表达形式中提取出某个 1 的位置，我们到现在已经用到了很多次将一个数的最低位 1 置零的操作：<code>n &amp;= (n - 1)</code>，可以从这里下手看看能不能得到那个最低位 1。在第一篇文章对这个的解释中，我们知道 <code>n - 1</code> 得到的是最低位 1 及其后的所有位都取反的值，并且这个“其后的所有位”当然都是由零转变成的一了。</p>

<p>这个特性我们稍微修改一下就能用上做为提取末位 1 的桥梁，很显然“末位 1 及其后的所有位”是 <code>1000...000</code> 的格式，只要前面的值也全是 0 就行了，而一个取非加上和原数进行按位与就能清除掉所有这些多余的高位，也就是 <code>n &amp; (~ (n - 1))</code> 得到的便是仅最低位 1 保持为 1，其它位都为 0 的数字。</p>

<p>从取反的角度考虑一下可以想出另一个方案，如果一个数末尾是 <code>...111</code> 这样的话，加上个 1 就是 <code>...1000</code> 了，也就是取反加一可得到和刚刚一样的结果，然后按位与一次就清除掉了多余的高位：<code>n &amp; (~n + 1)</code>，只从代码上看它就是前面那个公式把取反符号放进括号内了而已。</p>

<p>实际上这个取反加一后的值就是二进制下的补码，直接把获得掩码的代码换成 <code>n &amp; -n</code> 就行了：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    vector&lt;int&gt; singleNumber(vector&lt;int&gt;&amp; nums) {
        int mask = 0;
        for (int num : nums) mask ^= num;
        mask &amp;= -mask;

        vector&lt;int&gt; ans(2, 0);
        for (int num : nums) {
            if (num &amp; mask) ans[0] ^= num;
            else ans[1] ^= num;
        }
        return ans;
    }
};
</code></pre>]]></content:encoded></item><item><title><![CDATA[结合 LeetCode 上的几道简单题再谈谈位操作（一）：对前文的拓展]]></title><description><![CDATA[<p>文章目录</p>

<p>大概那么五六七个月之前，我看到《编程之美》中某道位操作相关的题目，觉得挺有意思，于是稍微整理了一下相关的知识瞎扯出了 <a href="http://kingsleyxie.cn/bit-manipulation-revisited-from-some-easy-problems-on-leetcode-part-one/../talking-about-bit-operations-from-a-problem-on-bop/">从《编程之美》中的一道面试题开始谈谈位操作</a> 这篇文章，今天突然心血来潮点进<del>从来没用过的</del> LeetCode 发现有一个 Bit Manipulation 的分类专题，于是就挑了其中几道简单的题目做，因此导致了这篇和下篇文章的突然诞生。</p>

<p>首先有几题是可以直接用上前篇文章里面讲到的位操作技巧的，比如通过 <code>n &amp;= (n - 1)</code> 来计算一个数的二进制表示中含有多少个 1，以及 ASCII 大小写字母转换的 trick，下面来看具体题目。</p>

<h3 id="problem338countingbits">Problem 338. Counting Bits</h3>

<p>题目很简单，输入一个非负整数 <code>num</code>，按顺序输出 \([0, num]\) 范围内每个数的二进制表达中含有 1 的个数，直接循环对每个数进行计算也能过：</p>

<pre><code class="language-cpp">class Solution</code></pre>]]></description><link>http://kingsleyxie.cn/bit-manipulation-revisited-from-some-easy-problems-on-leetcode-part-one/</link><guid isPermaLink="false">c669b522-44c4-421d-acec-e1fd6c2db59d</guid><category><![CDATA[Program]]></category><category><![CDATA[Bit Manipulation]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Sun, 30 Sep 2018 15:07:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/09/counting-bits.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/09/counting-bits.png" alt="结合 LeetCode 上的几道简单题再谈谈位操作（一）：对前文的拓展"><p>文章目录</p>

<p>大概那么五六七个月之前，我看到《编程之美》中某道位操作相关的题目，觉得挺有意思，于是稍微整理了一下相关的知识瞎扯出了 <a href="http://kingsleyxie.cn/bit-manipulation-revisited-from-some-easy-problems-on-leetcode-part-one/../talking-about-bit-operations-from-a-problem-on-bop/">从《编程之美》中的一道面试题开始谈谈位操作</a> 这篇文章，今天突然心血来潮点进<del>从来没用过的</del> LeetCode 发现有一个 Bit Manipulation 的分类专题，于是就挑了其中几道简单的题目做，因此导致了这篇和下篇文章的突然诞生。</p>

<p>首先有几题是可以直接用上前篇文章里面讲到的位操作技巧的，比如通过 <code>n &amp;= (n - 1)</code> 来计算一个数的二进制表示中含有多少个 1，以及 ASCII 大小写字母转换的 trick，下面来看具体题目。</p>

<h3 id="problem338countingbits">Problem 338. Counting Bits</h3>

<p>题目很简单，输入一个非负整数 <code>num</code>，按顺序输出 \([0, num]\) 范围内每个数的二进制表达中含有 1 的个数，直接循环对每个数进行计算也能过：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    vector&lt;int&gt; countBits(int num) {
        /* 1. Iterate Each Number */
        vector&lt;int&gt; ans;
        for (int i = 0; i &lt;= num; i++) {
            int n = i, cnt = 0;
            while (n) {
                n &amp;= n - 1;
                cnt++;
            }
            ans.push_back(cnt);
        }
        return ans;
    }
};
</code></pre>

<p>当然它要数组的结果那摆明了是想提示计算过程中应该可以用上前面计算的值，要不然直接给一个数让求就够了。题面也有暗示这一点：</p>

<blockquote>
  <p>It is very easy to come up with a solution with run time \(O(n*sizeof(integer))\). But can you do it in linear time \(O(n)\)/possibly in a single pass?</p>
</blockquote>

<p>用个输入为 100 的 Testcase 观察它的输出结果，可以很容易发现规律：</p>

<pre><code>[00, 03]: 0, 1, 1, 2
[04, 07]: 1, 2, 2, 3

[00, 07]: 0, 1, 1, 2, 1, 2, 2, 3
[08, 15]: 1, 2, 2, 3, 2, 3, 3, 4

[00, 15]: 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4
[16, 31]: 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5
</code></pre>

<p>结合各个数字的二进制表达形式（参考 <a href="https://www.convertbinary.com/numbers/">这里</a>），实际上就是数字每次上升一次幂的话，就对应高一位多个 1 然后后面的几位从头开始，如此往复，所以只要从零开始每次把当前数组的各元素加 1 复制一遍进去就可以得到答案：</p>

<pre><code class="language-cpp">/* 2. Focus On The First Bit */
vector&lt;int&gt; ans(1, 0);  
int sz = 1;  
while (sz &lt;= num) {  
    for (int i = 0; i &lt; sz; i++) {
        ans.push_back(ans[i] + 1);
        // If the size satisfies the requirement
        if (ans.size() &gt; num) break;
    }
    sz = ans.size();
};
return ans;  
</code></pre>

<p>从首位去考虑就是直接多了一个 1，那如果从末位去考虑的话，情况就是上一个幂对应的一组数字在当前幂中每个重复两遍并加上末尾的一个 1/0，比如：</p>

<pre><code>[5, 7]: 100 | 101 | 110 | 111
[8, 15]: 1000 1001 | 1010 1011 | 1100 1101 | 1110 1111
</code></pre>

<p>每两个数字的前三位关联前一个区间对应数字的完整三位，所以一个 n 位二进制数包含的 1 的总个数就由【前 n-1 位包含 1 的个数】和【它自己的末位是否为 1 】决定，而前几位包含的个数在计算当前数字的时候是已知的，这样问题就可以用动态规划来解决了，两个决定因素的值可以用位运算来提取，代码长度骤减<del>，逼格也高了很多</del>：</p>

<pre><code class="language-cpp">/* 3. Focus On The Last Bit */
vector&lt;int&gt; ans(num + 1, 0);  
for (int i = 1; i &lt; ans.size(); i++) {  
    ans[i] = ans[i &gt;&gt; 1] + (i &amp; 1);
}
return ans;  
</code></pre>

<p>其实还有一个差不多的规律可以从结果集合里面发现，就是从 \([2, 3]\) 开始，每个 \(2^n(n \ge 1)\) 长度的区间，前一半数字都来自前一段区间，后一半数字是前一半数字各自加 1 所得：</p>

<pre><code>[02, 03]: 1, 2
[04, 07]: 1, 2, 2, 3
[08, 15]: 1, 2, 2, 3, 2, 3, 3, 4
[16, 31]: 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5
</code></pre>

<p>那这个其实是从第二个二进制位考虑了，也就是前半组的第二位为 0，后半组的第二位为 1，剩下三位拼接后还是和前一个区间对应数字的完整三位相同，所以基本的原理都差不多，不过这个的话写起来稍微有点啰嗦，我就不贴代码上来了。</p>

<h3 id="problem784lettercasepermutation">Problem 784. Letter Case Permutation</h3>

<p>这个题目要求就是把给定的一个字符串中每个字母的大小写情况进行排列然后输出所有的可能结果，因为它说了输入的字符串中只包含字母和数字，所以直接 ASCII 码判断，大于等于 65 的字符一定是字母，然后用深搜回溯就可以解决问题了：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    vector&lt;string&gt; ans;

    vector&lt;string&gt; letterCasePermutation(string S) {
        dfs(S, 0);
        return ans;
    }

    void dfs(string S, int len) {
        if (S.length() == len) {
            ans.push_back(S);
            return;
        }

        dfs(S, len + 1);
        if (S[len] &lt; 65) return;

        S[len] ^= 0x20;
        dfs(S, len + 1);
    }
};
</code></pre>

<p>把这道题放进来是因为里面的字符大小写转换用到了前面一篇文章里提到的方法，也就是和 <code>0x20</code> 进行按位异或得到转换后的字符，这样就可以做到不通过判断而保证【回溯时去掉原本的字母并换上改变大小写之后的对应字母】的需求。</p>

<p>不过其实用判断的方式去写只是代码稍微长一点点而已，也没什么影响，毕竟这不是性能瓶颈 = =</p>

<p>因为字母只分大写和小写两种情况，可以用二叉树来理解这个搜索过程，每往下一层就相当于添加一个新的字符，父节点一定会和其中一个子节点值相同，最后生成的树的叶子节点就是所有的排列情况，树的高度正好是字符串的长度。</p>

<p>回溯的递归调用栈有时候可能会导致开销过大，这题其实可以直接用循环来做的，还是刚刚说的二叉树的思路，如果是数字那么直接进入只有一个子节点的下一层，也就是直接将当前字符拼接到字符串的末尾，而如果是字母的话就先复制一份到结果列表里，然后对这两个字符串分别加上大写和小写的当前字符，就相当于产生两个子节点，这样的操作一直到处理完整个输入字符串，也就是二叉树到了叶子节点，就得到了所有的排列情况：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    vector&lt;string&gt; letterCasePermutation(string S) {
        vector&lt;string&gt; ans{""};
        int sz;
        for (char c : S) {
            sz = ans.size();
            for (int i = 0; i &lt; sz; i++) {
                if (c &lt; 65) {
                    ans[i] += c;
                } else {
                    ans.push_back(ans[i]);
                    ans[i] += c;
                    ans[i + sz] += c ^ 0x20;
                }
            }
        }
        return ans;
    }
};
</code></pre>

<p>一个要注意的细节是 ans 数组需要一个空串作为初始值。以上两个解法在 LeetCode 上跑的时间都是 8ms<del>，可能因为数据规模不大区分不出来？</del>。</p>

<p>（话说越写越觉得这不是一道侧重位操作的题目 2333</p>

<h3 id="problem476numbercomplement">Problem 476. Number Complement</h3>

<p>题目要求是输出一个正整数二进制表达式从最高位 1 开始各位取反之后的数，这个在概念上是没有难度的，不过实现起来还是容易疏忽细节问题。比如说，按位取反 <code>~</code> 操作符取反的范围是整个数字的二进制表达形式，而不会去管在是不是最高位一之前，所以这样直接取反之后得到的数是不正确的，比如 <code>101</code> 如果存着的形式是 <code>0000 0101</code>，对它进行按位取反之后得到 <code>1111 1010</code>，而我们实际想要的是 <code>0000 0010</code>。</p>

<p>另外，这里因为题目是说给正整数，所以不用去考虑带符号整数相关的问题，举个简单例子：</p>

<pre><code class="language-cpp">unsigned int a = 11;  
int b = 11;  
std::cout &lt;&lt; (~a) &lt;&lt; " " &lt;&lt; (~b);  
// Outputs "4294967284 -12"
</code></pre>

<p>11 的二进制是 <code>1011</code>，在 4 字节存储下带有 28 个前导 0，取反之后得到的就是 <code>1111 1111 1111 1111 1111 1111 1111 0100</code>，作为无符号整数它的值是 4294967284，而作为带符号整数的话它就是 -12 了。</p>

<p>回到问题本身，既然不能整体一起取反，我们可以考虑分开一位位取反然后放进结果中，只要不断右移 n 然后取下最低位乘以当前次幂加进结果直到 n 为零也就是最高位 1 已经被处理过时停止：</p>

<pre><code class="language-cpp">class Solution {  
public:  
    int findComplement(int num) {
        int ans = 0, pos = 0;
        while (num) {
            ans += (!(num &amp; 1)) &lt;&lt; pos;
            pos++;
            num &gt;&gt;= 1;
        }
        return ans;
    }
};
</code></pre>

<p>对一位比特进行的取反，因为刚刚提到的原因，同样不能够用按位取非操作来完成，毕竟计算机内部对这些数的处理都是以比特为最低单位的。但是逻辑取非就可以很好达到我们的需求：对 0 进行逻辑取非得 1，对被视为真的任何非零值（当然这里只会有 1 一个非零值）进行逻辑取非得到 <code>false</code> 也就是 0。</p>

<p>不过要注意这份代码是会对传入的 num 进行修改的，如果要求不能修改入参的话得加个临时变量来存。</p>

<p>这道题还有另一个思路，就是还是用对整体的取反来进行，但是是通过和一个低 n 位全为 1 的数字直接进行异或做到的，大致想法其实和掩码差不多，那么主要问题就变成了如何构造出这样一个数字。一个 n 位都为 1 的二进制数可以由 \(2^{n} - 1\) 得到，所以还是用原来的对入参值移位的方式我们就可以得到这个 n 的值，进而得到这个我们需要的数字。但是这次就必须得用个临时变量存入参了，不然拿到 n 之后就失去了异或操作的其中一个操作数。</p>

<pre><code class="language-cpp">int numdump = num, mask = 1;  
while (numdump) {  
    mask &lt;&lt;= 1;
    numdump &gt;&gt;= 1;
}
return num ^ (mask - 1);  
</code></pre>

<p>亲测前后两种方法的 Runtime 分别是 0ms 和 4ms，看起来后者就多了一个异或而已<del>，可是前者不是多了一次按位与吗</del>，别的好像也没什么区别<del>，难道又是因为测试数据规模小测不出差别</del>。</p>]]></content:encoded></item><item><title><![CDATA[LaTeX 排版技巧续集：从数据结构作业到数据库作业]]></title><description><![CDATA[<p>文章目录</p>

<p>这篇文章算是上一篇 <a href="http://kingsleyxie.cn/some-latex-typography-skills-for-database-homework/../some-latex-typography-skills-for-data-structure-homework/">整理和分享一些数据结构作业用到的 LaTeX 排版技巧</a> 的一个补充，主要的区别在于数据库课程是用中文上的，加上要交的题目类型还是有些区别的，所以作业的内容排版上就不同了，因为数据结构作业排版的那篇文章已经把大部分的内容讲掉了所以这里就只说那里面没提过或者相对它有较大修改的部分。</p>

<h3 id="">基础内容</h3>

<h5 id="">封面</h5>

<p>封面的结构和数据结构作业用的基本是一样的，不过因为内容里有中文，需要使用中文的 TeX 环境，并且设置文档编码为 UTF-8 以避免构建出来的文档乱码，这些改变只需要把原来的 <code>\documentclass[titlepage]{article}</code> 改成 <code>\documentclass[UTF8,titlepage]{ctexart}</code> 即可：</p>

<pre><code>\documentclass[UTF8,titlepage]{ctexart}

\title{Database Homework 1}
\author{Kingsley}
\date{\today}

\begin{document}
\maketitle

\clearpage
\tableofcontents
\clearpage
\end{document}</code></pre>]]></description><link>http://kingsleyxie.cn/some-latex-typography-skills-for-database-homework/</link><guid isPermaLink="false">6e0c08a8-c91d-4059-bd03-b43f399d7aa4</guid><category><![CDATA[LaTeX]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Sat, 21 Jul 2018 10:37:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/07/toc-default.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/07/toc-default.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"><p>文章目录</p>

<p>这篇文章算是上一篇 <a href="http://kingsleyxie.cn/some-latex-typography-skills-for-database-homework/../some-latex-typography-skills-for-data-structure-homework/">整理和分享一些数据结构作业用到的 LaTeX 排版技巧</a> 的一个补充，主要的区别在于数据库课程是用中文上的，加上要交的题目类型还是有些区别的，所以作业的内容排版上就不同了，因为数据结构作业排版的那篇文章已经把大部分的内容讲掉了所以这里就只说那里面没提过或者相对它有较大修改的部分。</p>

<h3 id="">基础内容</h3>

<h5 id="">封面</h5>

<p>封面的结构和数据结构作业用的基本是一样的，不过因为内容里有中文，需要使用中文的 TeX 环境，并且设置文档编码为 UTF-8 以避免构建出来的文档乱码，这些改变只需要把原来的 <code>\documentclass[titlepage]{article}</code> 改成 <code>\documentclass[UTF8,titlepage]{ctexart}</code> 即可：</p>

<pre><code>\documentclass[UTF8,titlepage]{ctexart}

\title{Database Homework 1}
\author{Kingsley}
\date{\today}

\begin{document}
\maketitle

\clearpage
\tableofcontents
\clearpage
\end{document}
</code></pre>

<h5 id="">段落层次</h5>

<p>有时候我们可能需要调整部分内容是否在目录页进行展示，这时候可以通过 <code>\setcounter{}</code> 命令来指定，<code>tocdepth</code> 的值指定目录层将显示到哪一级为止，<code>secnumdepth</code> 的值指定层级的编号到哪一级为止，比如这样的一个文档：</p>

<pre><code>\documentclass[titlepage]{article}

\setcounter{tocdepth}{2}
\setcounter{secnumdepth}{5}

\begin{document}
\tableofcontents

\section{Section}
\subsection{Subsection}
\subsubsection{Subsubsection}
\paragraph{Paragraph}
\subparagraph{SubParagraph}
\end{document}
</code></pre>

<p><code>tocdepth</code> 是 2，对应 article 文档里的 <code>subsection</code> 层，而 <code>secnumdepth</code> 是 5，对应 <code>subparagraph</code> 层，（下面简写成 <code>(2, 5)</code>）显示出来的样子就是目录上到第二级为止，而编号到第五级为止：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/07/toc-2-5.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"></p>

<p>如果去掉这两行的话，默认的显示是 <code>(3, 3)</code> 的值的效果：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/07/toc-default.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"></p>

<p>如果是 <code>(5, 2)</code> 的话，展示出来的就是目录中显示了五级但是编号只到第二级为止：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/07/toc-5-2.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"></p>

<p>顺带一提这里是 <code>article</code> 而不是刚刚说的 <code>ctexart</code>了，两者对部分内容的居中方式有一点区别，不过不影响这里的演示就不管啦。</p>

<p>（写了三个示例其实主要是为了方便自己以后记不清的话可以来这里看图片想起来2333）</p>

<p>这个设置其实我自己用到的场景是这样的：大部分时候作业的题目是不按顺序的，那我可能得把题号写进 <code>section</code> 的文本里面，但是这样的话编排出来会导致题号的数字前面又有一个编号，看起来会有点乱，所以在这种情况下就直接用 <code>\setcounter{secnumdepth}{0}</code> 来直接去掉了所有标题级别的自动编号。</p>

<h5 id="">超链接的红色外框</h5>

<p>这个其实很简单了，应该刚开始用 LaTeX 的时候都会碰到所以上次写的时候就没提，这里因为刚刚有提及目录以及后面有用到个图片引用的功能就顺带提一下。</p>

<p>默认生成的任何文档，比如我们刚刚生成的那个目录文档，里面的链接都是不可点击的，那么要让它们能够在点击后链接到对应的地址，可以通过引入 <code>hyperref</code> 包来达到目的，但是这样的话又会发现所有可以点击的文字都被加上了一个红色的外方框，很显然这可以让链接文字和普通文字区分开来，让阅读者知道那个地方是有链接的，但是有时候我们可能并不想让这个链接有这样的方框<del>，讲道理很丑啊好不好</del>，这个时候直接对它加上一个 <code>hidelinks</code> 选项即可，所以就是在文件引入 package 的代码部分加上这个即可：</p>

<pre><code>\usepackage[hidelinks]{hyperref}
</code></pre>

<h3 id="">关系代数</h3>

<p>数据库作业那肯定是免不了要写关系代数的，但是几个连接的符号又比较难搞定，在一番查找和对比之后发现了最简单的方式，就是使用 <code>unicode-math</code> 包，这样不仅连接符号可以轻松搞定，整个关系代数公式的排版都轻松了很多，不过缺点就是它还不支持在公式里面放中文，所以我当时是用拼音代替的。</p>

<p>默认的那个字体其实很难看，可以通过 <code>\setmathfont{}</code> 来换一个看起来舒服一点的字体，我个人是用的 <code>Asana Math</code>，然后还要注意的一个地方是因为我平时使用的是 pdfLaTeX，但是这个 <code>unicode-math</code> 是得用 XeLaTeX 去 build 才能使用的，直接在文件的开头加上一行指定类型的代码即可，顺便吐槽一下这个 XeLaTeX 的构建时间真的是巨长，不知道是不是我的使用姿势不太对。</p>

<p>最后的示例文件内容看起来大概是这样：</p>

<pre><code>% !TEX program = xelatex
\documentclass[UTF8,titlepage]{ctexart}
\usepackage{unicode-math}

\setmathfont{Asana Math}

\begin{document}
$\pi_{JNO}(SPJ) - \pi_{JNO}(\sigma_{CITY='CT' \land COLOR='CL'}(S \Join SPJ \Join P))$
\end{document}
</code></pre>

<p>效果好像还是可以的：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/07/relational-algebra.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"></p>

<p>左右连接和全连接符号分别是 <code>\leftouterjoin</code>、<code>\rightouterjoin</code> 和 <code>\fullouterjoin</code>，更多其它符号可以在 <code>unicode-math</code> 包的 <a href="http://mirrors.ibiblio.org/CTAN/macros/latex/contrib/unicode-math/unimath-symbols.pdf">文档</a> 中找到。</p>

<h3 id="sql">SQL 代码</h3>

<p>代码环境的常见用法和配置之前那篇文章已经提了很多，所以这里就不赘述了，只说几个和 SQL 代码显示相关的问题。</p>

<p>首先就是如果 SQL 代码里面包含单引号，就会无法正常构建，这个问题直接通过引入 <code>textcomp</code> 包即可解决。但是其实在渲染的效果上会发现这个引号并不是竖直的那种适用于代码显示的格式，我们可以在 <code>\lstset{}</code> 里指定 <code>upquote=true</code> 来让这个引号看起来更舒服一点。</p>

<p>另一个常见的情况是我们希望把 SQL 代码中的某个词高亮，当然办法有很多，这里只说一个通过嵌入 LaTeX 命令的实现方式，同样需要在 <code>\lstset{}</code> 里设置，语法是这样的：<code>escapeinside={\%*}{*)}</code>。前后两组大括号内分别是自定义的起止分隔符，这样我们就可以在 <code>%*</code> 和 <code>*)</code> 内通过 <code>\textbf{}</code> 来让某部分显示粗体了：</p>

<pre><code>\documentclass[UTF8,titlepage]{ctexart}
\usepackage{listings}
\usepackage{textcomp}
\usepackage[T1]{fontenc}

\lstset{
    basicstyle=
        \def\fvm@Scale{0.8}
        \fontfamily{fvm}\selectfont,
    upquote=true,
    escapeinside={\%*}{*)},
}

\begin{document}
\begin{lstlisting}
SELECT * FROM users WHERE username = 'admin'  
SELECT * FROM users WHERE %*\textbf{username}*) = 'admin'  
\end{lstlisting}
\end{document}
</code></pre>

<p>注意那个起始符的百分号在设置里是要转义的，但是在代码里不需要转义，生成的效果是这样：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/07/sql-code.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"></p>

<h3 id="er">E-R 图</h3>

<p>E-R 图是数据库作业排版里面的一个相对比较棘手的问题了，不用说都知道这种复杂图形绘制的纯 LaTeX 解决方案得往万能的 <code>tikz</code> 那边去考虑，查了查发现还真有个 <code>er</code> 的库用来画 E-R 图，不过说实话个人感觉并不是很好用。</p>

<h5 id="tikz">使用 <code>tikz</code> 解决</h5>

<p>在 <a href="http://www.texample.net/">TeXample</a> 网站上找到了两个方案，一个是别人自己修改样式后的版本：<a href="http://www.texample.net/tikz/examples/entity-relationship-diagram/">Example: Entity-Relationship diagram</a>，另一个是直接使用 <code>er</code> 库来完成的稍微没有前面的那么华丽的版本：<a href="http://www.texample.net/tikz/examples/er-diagram/">Example: Entity-relationship diagram</a>。</p>

<p>我用的是后面的一个方案，然后自己加了点 <code>\tikzset{}</code> 的设置让每个节点看起来有比较统一的宽高，代码就不贴在这里了，可以在 <a href="https://github.com/KingsleyXie/Miscellaneous/blob/master/LaTeX/Database/er-diagram.tex">GitHub</a> 上查看，<code>\node[attr]</code> 里的 <code>attr</code> 为 <code>entity</code> 和 <code>relationship</code> 时分别代表这是一个实体和关系，然后对它进行一个命名方便其它节点可以用 <code>[below of=name]</code> 之类的方式来定义自己相对它的位置，并且用这种方式定义好自己相对另一个节点的位置，以及节点的文本内容，最后加上边、边上的数字和方向以及边所指向的节点，看起来也算是挺直观的，很容易理解并且魔改成自己的，但是最后排出来的效果并不怎么样。</p>

<h5 id="">更方便的解决方案</h5>

<p>用 <code>tikz</code> 来画比较复杂的图的话，要达到自己想要的效果其实是需要不少时间的。主要的问题在于，一方面学习成本比较高，还得花蛮大的精力去调整宽高这些属性的统一程度的问题，特别是遇到需要一定时间来构建的图的话<del>等待起来会让人很烦躁</del>，另一方面是如果碰上需要修改结构之类的情况，工作量确实不小，所以比较推荐使用 <a href="https://www.draw.io/">drawio</a> 之类的网站画好图然后选中那个区块导出成 PDF，再嵌入 LaTeX 文档：</p>

<pre><code>\begin{center}
\includegraphics[width=0.7\textwidth]{filename.pdf}
\end{center}
</code></pre>

<p>通过这种方式，从最终效果上看生成的文件还是一样可以选中文字和整体矢量放大，但是无论是生成图片还是增加节点、修改结构都方便了很多，并且远不止适用于 E-R 图这一项，只要网站能提供或者使用者能画出来的任何图都可以用这种方式完成，生产力 MAX。</p>

<p>可选的 <code>width</code> 选项指定插入图片的宽度，如果不设置的话碰到稍大的图片就很容易会排错乱掉，比如图片的宽度超过了文档的最大宽度的情况。<code>width=\textwidth</code> 表示图片宽占满文档的最大宽度，通过在它的前面加某个数字即可调整图片的显示规模。</p>

<h3 id="">查询树图和关系代数语法树图</h3>

<p>因为这两种树的特性，不用像每个节点<strong>最多</strong>有两颗子树的二叉树一样去考虑给空缺的一边占位，所以可以直接用数据结构作业时画普通树的 <code>forest</code> 包来排版，只是这颗树不需要给节点加外框之类的，而是直接的文本或者公式了。</p>

<h5 id="">查询树</h5>

<pre><code>\begin{forest}
[, phantom, s sep = 1cm
    [Answer
        [{project(Cname)}
            [{select(Student.Sdep = `IS')}
                [{join(SC.Cno = Course.Cno)}
                    [{join(Student.Sno = SC.Sno)}
                        [Student]
                        [SC]]
                    [{Course}]
                ]
            ]
        ]
    ]
]
\end{forest}
</code></pre>

<p>语法和格式上基本上和前篇文章讲的用 <code>forest</code> 包排版普通树的是一样的，不过它的节点毕竟不是数字了，而且包含了等号空格之类的特殊字符，所以要用一对大括号括起作为一个文本，这段代码对应效果如下：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/07/query-tree.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"></p>

<h5 id="">关系代数语法树</h5>

<p>相对查询树的主要的区别是有些操作的文本被替换成对应的关系代数了，那么就得用前文所说的关系代数的排版方式放进数学公式的环境里：</p>

<pre><code>\begin{forest}
[, phantom, s sep = 1cm
    [$\pi_{Cname}$
        [$\sigma_{SC.Cno\,=\,Course.Cno}$
            [$\times$
                [$\sigma_{Student.Sno\,=\,SC.Sno}$
                    [$\times$
                        [SC]
                        [$\sigma_{Student.Sdep\,=\,'IS'}$
                            [Student]
                        ]
                    ]
                ]
                [{Course}]
            ]
        ]
    ]
]
\end{forest}
</code></pre>

<p>有几个微小的细节，首先是为了整体的统一，应该把所有的节点都各自放进数学公式环境中（<code>$Node Text$</code>），不然关系代数节点和纯文本节点的字体不一样看起来就有点不爽（</p>

<p>其次是关系代数的等号前后应该略留空隙，这里直接使用 <code>\,</code> 的方式来作为占位空格了，注意如果是直接的空格字符的话渲染出来的等号左右两边是直接和等号相连没有间隔的。</p>

<p>最后就是对比查询树会发现两份代码中对 <code>IS</code> 字符的引号是不同的，在数学环境下用的是直接的两个字符作为引号，而文本环境中用的是 LaTeX 给普通文本环境用的的标准引号对 <code>`'</code>。</p>

<p>如果引号的使用反过来的话，前者输出的将会是 <code>’IS’</code> 而不是正确的 <code>‘IS’</code>，后者输出的将会是 <code>‘IS'</code> 而不是正确的 <code>'IS'</code>（准确说那个其实好像也不是单引号 反正就是看起来类似它的一个符号 这里就不深究了）。</p>

<p>最后给上这个关系代数语法树的效果图：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/07/relation-algebra-syntax-tree.png" alt="LaTeX 排版技巧续集：从数据结构作业到数据库作业"></p>

<h3 id="figure"><code>figure</code> 环境</h3>

<p>这个东西展开讲肯定是内容超多的，这里仅仅带过和本文相关的一个部分，就是对如何把前面的几份图片放进 <code>figure</code> 环境的一点简要整理。</p>

<p>对于 tikzpicture 我们可以这样把它居中作为图片放进去：</p>

<pre><code>\begin{figure}[!htb]
\begin{center}
\begin{tikzpicture}
blabla  
\end{tikzpicture}
\caption{E-R Diagram}\label{fig:er-diagram}
\end{center}
\end{figure}
</code></pre>

<p>当然对于刚刚提到的 <code>forest</code> 也是一样的，把里面起止的两个 <code>tikzpicture</code> 改成 <code>forest</code> 就行了，这里嵌套了一个 <code>center</code> 环境用于直接让图片水平居中。</p>

<p><code>\caption</code> 一行指定该图的名称为 “E-R Diagram”，LaTeX 会按照图片在整个文档中的顺序进行自动编号，其中 <code>\label</code> 里面定义的值让这张图片可以出现在插图列表里面，并且可以在文档的其它地方通过 <code>\ref</code> 命令引用该图片：</p>

<pre><code>\begin{document}
\listoffigures

\section{E-R 图}
如下图 \ref{fig:er-diagram} 所示。

\begin{figure}[!htb]
blabla  
\caption{E-R 图}\label{fig:er-diagram}
\end{figure}
\end{document}
</code></pre>

<p>最终的图前文字显示是 “如下图 1 所示”，即 LaTeX 会自动找到这个文本对应的图片并将其编号渲染出来，如果加上前面提到的链接的设置的话，这个 <code>ref</code> 命令的引用处就可以点击跳转到对应的图片位置了，完整的代码和 PDF 文件参考 Repo，这里也不给了。</p>

<p>最后，图片的排版可能出现一个问题，就是文字明明写在图片的前面，却被排版到了图片之后，通过引入 <code>flafter</code> 包可以解决这个问题：</p>

<pre><code>\usepackage{flafter}
</code></pre>

<h3 id="">源文件相关说明</h3>

<p>所有的代码和对应生成的 PDF 文件都放在了 <a href="https://github.com/KingsleyXie/Miscellaneous/tree/master/LaTeX/Database">GitHub</a> 上，不过 Repo 里的代码有些是用的另一个 <code>documentclass</code>：</p>

<pre><code>\documentclass[tikz, border={5pt, 15pt}]{standalone}
</code></pre>

<p>这是为了能直接让生成的 PDF 仅有必要的留白（这里设置的是上下 15pt，左右 5 pt），而不是默认的 A4 之类的大小，因为用于演示的代码生成出的内容仅占一小部分，剩下一大片空看起来就有点难受。</p>

<h3 id="">延伸参考</h3>

<p>（前三个复制粘贴自上篇文章，因为感觉这段还是挺有价值的，第四个是新补充的）</p>

<ol>
<li><p><a href="https://ctan.org">CTAN</a> <br>
这里收录了大部分的 TeX 包以及他们的 README、官方文档等内容，非常详尽。在我感叹 <code>tikz</code> 这个包怎么功能这么强大的时候，我找到了这个包的 <a href="http://mirror.lzu.edu.cn/CTAN/graphics/pgf/base/doc/pgfmanual.pdf">文档</a>，然后就……emmm</p></li>
<li><p><a href="https://www.sharelatex.com/learn">Share LaTeX</a> <br>
这个网站本身是用于在线合作编写 LaTeX 文档的，他们提供的这个 <code>learn</code> 部分感觉对于学习 LaTeX 很有帮助。我记得我高中的时候也有了解过这个网站，因为当时它给的 Demo 里有一张青蛙的图片，让我印象很深刻（</p></li>
<li><p><a href="https://en.wikibooks.org/wiki/LaTeX">WikiBook</a> <br>
搜索结果里经常也看到来自这里的内容，不过我没太仔细了解，应该和上面的那个差不多。相对官方文档而言，这种整合并且带索引的结果可能有时候更符合我们的需求。</p></li>
<li><p><a href="http://www.texample.net/">TeXample</a> <br>
好像是在搜索怎么用 <code>tikz</code> 画 E-R 图的时候发现的？这个网站对每种样例都给出了详尽的 TeX 代码和它们对应的预览图、PDF 文件，所以可以很方便地直接尝试里面的各种样例<del>去魔改</del>。</p></li>
</ol>]]></content:encoded></item><item><title><![CDATA[浅析 MySQL InnoDB 存储引擎中的事务、并发与锁]]></title><description><![CDATA[<p>文章目录</p>

<h3 id="">一点背景</h3>

<p>之前数据库课程的最后一次实验中有一项是验证数据库并发操作带来的问题以及事务的各个隔离级别对这些问题解决的程度，在测试过程中发现有些情况和课本的描述并不一样，一开始以为是隔离级别设置的步骤之类的出了问题，后来了解到其实各个数据库的实际实现都多少有自己对系统并发性能等方面的考虑，没有完全依照标准来完成。再之后在图书馆发现了一本讲 MySQL InnoDB 存储引擎的书，就仔细去了解了一下关于这个存储引擎的事务、并发和锁等方面的知识，同时看了一些 MySQL 的文档以及几篇博客，于是就自己对当时实验的用例进行修改设计了一些新的测试和验证代码，并把了解到的相关内容整理成了这篇文章。</p>

<h3 id="">准备工作</h3>

<h5 id="">创建数据库和测试数据</h5>

<pre><code class="language-sql">DROP DATABASE IF EXISTS trans_test_db;  
CREATE DATABASE trans_test_db;  
USE trans_test_db;  
CREATE TABLE demo (  
    id INTEGER NOT NULL AUTO_INCREMENT,
    value INTEGER,
    PRIMARY KEY(id)</code></pre>]]></description><link>http://kingsleyxie.cn/brief-analysis-of-mysql-innodb-storage-engine/</link><guid isPermaLink="false">c071bcfc-a546-4cce-bcf5-262842eca9bc</guid><category><![CDATA[Program]]></category><category><![CDATA[Database]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Wed, 27 Jun 2018 13:15:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/06/mysql.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/06/mysql.png" alt="浅析 MySQL InnoDB 存储引擎中的事务、并发与锁"><p>文章目录</p>

<h3 id="">一点背景</h3>

<p>之前数据库课程的最后一次实验中有一项是验证数据库并发操作带来的问题以及事务的各个隔离级别对这些问题解决的程度，在测试过程中发现有些情况和课本的描述并不一样，一开始以为是隔离级别设置的步骤之类的出了问题，后来了解到其实各个数据库的实际实现都多少有自己对系统并发性能等方面的考虑，没有完全依照标准来完成。再之后在图书馆发现了一本讲 MySQL InnoDB 存储引擎的书，就仔细去了解了一下关于这个存储引擎的事务、并发和锁等方面的知识，同时看了一些 MySQL 的文档以及几篇博客，于是就自己对当时实验的用例进行修改设计了一些新的测试和验证代码，并把了解到的相关内容整理成了这篇文章。</p>

<h3 id="">准备工作</h3>

<h5 id="">创建数据库和测试数据</h5>

<pre><code class="language-sql">DROP DATABASE IF EXISTS trans_test_db;  
CREATE DATABASE trans_test_db;  
USE trans_test_db;  
CREATE TABLE demo (  
    id INTEGER NOT NULL AUTO_INCREMENT,
    value INTEGER,
    PRIMARY KEY(id)
) ENGINE = InnoDB;
INSERT INTO demo(value) VALUES (1), (2), (3);  
</code></pre>

<h5 id="">创建测试用户</h5>

<pre><code class="language-sql">CREATE USER 'session1'@'localhost' IDENTIFIED BY 'password';  
GRANT ALL PRIVILEGES ON trans_test_db.* TO 'session1'@'localhost';

CREATE USER 'session2'@'localhost' IDENTIFIED BY 'password';  
GRANT ALL PRIVILEGES ON trans_test_db.* TO 'session2'@'localhost';  
</code></pre>

<p>前面的这两个步骤用 <code>root</code> 或者其它高权限的账号操作，接下来就可以开始测试了，下面的代码全部使用刚刚创建完成的 <code>session1</code> 和 <code>session2</code> 两个用户完成。</p>

<p>注意为了测试事务的几种隔离级别及其影响，我们需要在一个用户的事务未完成（完成指提交/回滚）时切换到另一个用户进行其它操作，所以最好是直接在命令行输入代码进行测试，并且开启两个终端分别登录这两个用户以便于切换，这里顺手安利一个比较好用的 SQL 命令行操作工具 <a href="https://github.com/dbcli/mycli">mycli</a>。</p>

<h5 id="">设置测试环境和隔离级别</h5>

<pre><code class="language-sql">USE trans_test_db;  
SET SESSION innodb_lock_wait_timeout = 3;

-- It is strongly recommended to reset(DROP &amp; RE-CREATE)
-- the database before starting a new test session
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- OR:  
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- OR:  
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- OR:  
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;  
</code></pre>

<p>上面这些设置需要在两个打开的 MySQL 客户端中都执行，保证测试的时候两个用户都是在同一个隔离级别下。修改了 timeout 值是因为操作过程中涉及到一些锁的超时验证，为了节省时间便把这个等待时长设置得短了一些。然后设置隔离级别时因为会对数据进行修改等操作，建议是一次测试完一个隔离级别的所有情况然后用最开始的建库代码重建数据库，重新开始新一轮的测试。</p>

<p>这里用的设置全部是 <code>SET SESSION</code> 而不是 <code>SET GLOBAL</code>，所以更改仅会对当前会话产生效果，不用担心影响到其它数据库、终端或者用户。</p>

<h3 id="">并发带来的问题</h3>

<p>为了方便更加直观地查看模拟两个并发事务的 SQL 代码，这里用图片的方式来展示它们。</p>

<p>我们通过刚刚打开的两个终端，分别模拟两个事务执行，左右两边分开的代码分别代表两个事务的执行内容和顺序，从上至下进行。注意不要一次把一个事务的操作全部执行完了，而是应该按照整体的上下顺序切换命令行进行操作。</p>

<h5 id="">丢失更新</h5>

<p><img src="http://kingsleyxie.cn/content/images/2018/06/Lost_Update.png" alt="浅析 MySQL InnoDB 存储引擎中的事务、并发与锁"></p>

<p>数据库意义上的丢失更新其实很多时候都不被当作并发带来的问题之一，因为更新操作在任何的事务隔离级别下都会加排他锁，在提交事务之前可以避免其它事务对当前事务所修改值的更新操作。测试的时候会发现哪怕是使用隔离级别最低的 <code>READ UNCOMMITED</code>，在事务二中执行 <code>UPDATE</code> 时都会抛出锁等待超时的错误：</p>

<pre><code>ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction  
</code></pre>

<p>但是要注意的是，这里说的是“数据库意义上”的丢失更新不会出现，即不会出现一个事务对某数据的修改还未完成时，该数据的值就被其它事务更新覆盖了的情况。实际的业务环境中，如果用以上写法还是会有可能导致丢失更新问题出现，比如想象一个情景，两个事务都根据 <code>SELECT</code> 语句取出的值去进行操作，事务二在事务一更新前拿到了这个值，然后事务一进行了更新后提交事务，此时事务二用这个旧的值执行增减操作然后更新提交，没有出现数据库层面上的丢失更新问题，并发也没有锁的冲突，但是事务一的更新确实被事务二的覆盖了。</p>

<p>这种情况要解决，一般来说代价最小的方案就是在查询时说明这份数据将会被更新，禁止其它事务进行查询操作，也就是锁定读（<code>SELECT ... FOR UPDATE</code>等）。这个时候如果其它事务进行普通的 <code>SELECT</code> 操作，仍然能得到它的值，但如果是另一个需要更新它的值的事务进行 <code>FOR UPDATE</code> 的查询操作时，会抛出和上面一样的锁等待超时错误。</p>

<p>事务一的 <code>COMMIT</code> 之后有一行查询语句，这个是不需要手动再提交一次的，因为 InnoDB 引擎默认将没有显示指定事务开始（<code>START TRANSACTION/BEGIN</code>）的单条语句都当作一个事务来执行，这个开关可以通过 <code>SET autocommit = {0 | 1}</code> 来手动修改。</p>

<h5 id="">脏读</h5>

<p><img src="http://kingsleyxie.cn/content/images/2018/06/Dirty_Read.png" alt="浅析 MySQL InnoDB 存储引擎中的事务、并发与锁"></p>

<p>如果确实不把丢失更新当作数据库并发带来的问题的话，脏读是最低级的一个问题了，它对应事务隔离级别中的 <code>READ UNCOMMITED</code> 级别，测试中可以发现除了这个级别以外，其它三个隔离级别下都不会出现在事务一中读到事务二提交之前的修改结果的情况，即三次 <code>SELECT</code> 操作读到的都是同一个值。</p>

<p>脏读的主要麻烦点在于读到的数据有可能是在数据库中从来没有存在过的，所以除了那些对数据准确性要求不高的业务以外，一般都不太会使用 <code>READ UNCOMMITED</code> 级别。</p>

<h5 id="">不可重复读</h5>

<p><img src="http://kingsleyxie.cn/content/images/2018/06/Non-Repeatable_Read.png" alt="浅析 MySQL InnoDB 存储引擎中的事务、并发与锁"></p>

<p>在事务二对事务一中已经查询过的数据进行更新操作后，如果事务一在提交之前再次进行相同的查询得到的结果和上次不同，那么就产生了不可重复读的问题。</p>

<p>不可重复读对应的是事务隔离级别中的 <code>REPEATABLE READ</code>，也就是前两个隔离级别（RU、RC）会有不可重复读的情况出现，而后两个隔离级别（RR、SZ）可以解决不可重复读的问题。</p>

<h5 id="">幻读</h5>

<p><img src="http://kingsleyxie.cn/content/images/2018/06/Phantom_Read.png" alt="浅析 MySQL InnoDB 存储引擎中的事务、并发与锁"></p>

<p>事务一使用某个条件查询一个集合，之后事务二向数据库插入了一些新的符合这个查询条件的数据，于是当事务一再次查询时会发现返回的集合比上次的数量要一些，就像查询操作产生了幻影（Phantom）一样。</p>

<p>本来按照 SQL1992 标准的话，RR 级别下是会出现幻读的问题的，这个问题仅在 <code>SERIALIZABLE</code> 级别下才会被解决，但是测试的时候会发现这个问题和前面的不可重复读一样，在 RU、RC 级别下会出现，而在 RR、SZ 级别下并没有，后面会谈及具体原因。</p>

<p>在《MySQL 技术内幕：InnoDB 存储引擎（第二版）》的 <code>6.5 锁问题</code> 一节中把不可重复读和幻读混为一谈了，个人感觉这是不太合适的，毕竟本身不可重复读和幻读就被定义为两个不同的并发执行问题，而且它们的侧重点也不同：虽然看起来都是事务一两次查询然后中间由事务二进行一次数据库的增改操作，最后导致两次查询的结果不同，但是不可重复读是由 <code>UPDATE/DELET</code> 导致的两次查询结果不一致，而幻读是由 <code>INSERT</code> 导致的，这两种情形在实现上就是不同的情况了，毕竟对已经在数据库的数据可以直接进行加锁，而阻止其他事务插入满足一个查询条件的数据，其实现代价和运行开销都是很不一样的。所以尽管两个问题在概念上都是不可复现第一次查询，但后者会被单独拿出来看待。</p>

<p>关于不可重复读和幻读两个问题的更多讨论和解决方案，会在下文中提到，因为 InnoDB 对它们的实现涉及更复杂的数据库并发控制技术。</p>

<h3 id="">简单整理</h3>

<p>RU(READ UNCOMMITTED)、RC(READ COMMITTED)、RR(REPEATABLE READ) 和 SZ(SERIALIZABLE) 四个隔离级别一级比一级严格，一般情况下隔离级别每严格一级可以依次解决脏读（Dirty Read）、不可重复读（Non-repeatable Read）和幻读（Phantom Read）现象。</p>

<p>InnoDB 实现了 <a href="http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt">SQL:1992</a> 标准中定义的上述所有四种隔离级别，按照其定义，四种隔离级别下并发执行时可能出现的情况列表应当是这样的：</p>

<table>  
  <tr>
    <th>Isolation Level</th>
    <th>P1 ("Dirty read")</th>
    <th>P2 ("Non-repeatable read")</th>
    <th>P3 ("Phantom")</th>
  </tr>
  <tr>
    <td>READ UNCOMMITTED</td>
    <td>Possible</td>
    <td>Possible</td>
    <td>Possible</td>
  </tr>
  <tr>
    <td>READ COMMITTED</td>
    <td>Not Possible</td>
    <td>Possible</td>
    <td>Possible</td>
  </tr>
  <tr>
    <td>REPEATABLE READ</td>
    <td>Not Possible</td>
    <td>Not Possible</td>
    <td>Possible</td>
  </tr>
  <tr>
    <td>SERIALIZABLE</td>
    <td>Not Possible</td>
    <td>Not Possible</td>
    <td>Not Possible</td>
  </tr>
</table>

<p>但是上面提到，实际上在测试过程中会发现 InnoDB 引擎的 RR 级别已经没有了幻读（即表中 P3）的问题，然而事务二中的数据又是确实存储进了数据库的，这是因为 MySQL 的这个存储引擎用 MVCC 来保证了在 RR 级别下的一致性读，这样的话 <code>REPEATABLE READ</code> 就和 <code>SERIALIZABLE</code> 在效果上基本相同了，除了后者会对事务中的 <code>SELECT</code> 语句自动加上锁，参考其 <a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html">官方文档</a> 中给的说明：</p>

<blockquote>
  <p><strong>SERIALIZABLE</strong></p>
  
  <p>This level is like <code>REPEATABLE READ</code>, but InnoDB implicitly converts all plain <code>SELECT</code> statements to <code>SELECT ... FOR SHARE</code> if autocommit is disabled. </p>
</blockquote>

<p>算是功能的实现者在完全遵循标准和提高系统性能之间做的一个权衡吧<del>，毕竟隔壁 Oracle 数据库甚至只实现了三个事务隔离级别呢（</del>。也正是因此，MySQL 中的默认事务隔离级别是 RR，而不是其他数据库常用的默认 <code>READ COMMITTED</code>。</p>

<h3 id="mvcc">多版本并发控制（MVCC）</h3>

<p>在谈及 MVCC 之前，先来看看针对幻读问题的一个额外测试：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/06/Phantom_Read_Extra.png" alt="浅析 MySQL InnoDB 存储引擎中的事务、并发与锁"></p>

<p>在 RR 级别下，按顺序执行这些操作，会发现第一次和第二次的 <code>SELECT</code> 结果是相同的，而在事务一中更新那个“不存在”的行后，再次执行第三次 <code>SELECT</code>，会发现这个新的行又突然出现了，这说明事务二的插入数据操作确实是影响到了数据库中存放的内容的。</p>

<p>MySQL 官方文档的 <a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html">一致性非锁定读</a> 中有对这个情况做出解释，大致意思就是一致性读会使用多版本的并发控制方式让事务读取一个快照以保证每次读取的数据一致，但是如果事务中有对这些“不存在”的数据进行更新等操作就是例外情况了，这个条件下事务就能在之后的查询中看到这些数据。</p>

<p>一致性非锁定读是指 InnoDB 通过 MVCC 的方式保证每次读取都返回某个时间点的数据，用以保证数据查询的一致性，它是 <code>READ COMMITTED</code> 和 <code>REPEATABLE READ</code> 两个隔离级别对 <code>SELECT</code> 进行操作的默认处理。不同的是，对于 RC 级别，读取的快照是每次执行查询操作时产生的，而 RR 级别读取的快照是事务开始后的第一次查询操作产生的，我们也可以在开始事务时加上 <code>WITH CONSISTENT SNAPSHOT</code> 修饰符来保证读取的是进入事务最开始时的快照。</p>

<p>“非锁定读”意味着读取操作不需要对数据进行加锁，因为它读取的是快照中的数据，而快照中的数据是不可能需要修改/删除等操作的，它只是相当于一份历史记录而已。</p>

<p>InnoDB 对每行数据增加了事务 ID 的字段，用于存储最后对其进行操作的事务，并有一个回滚指针指向操作对应的 undo record，并且在多版本并发控制的情况下，行中还有一个删除标记用于记录数据是否被删除，这样可以让数据不被实际物理删除，以保证其它事务不会突然读取不到某条数据。</p>

<p>MVCC 的实现主要依赖 undo log，而这个 log 本身又被用于数据库的事务回滚操作，所以通过这样的方式实现的多版本并发控制仅需极小的额外开销。</p>

<h3 id="">行锁的算法</h3>

<p>最后非常浅显地谈谈 InnoDB 中几个常用锁的算法。</p>

<p>针对行记录的锁，我们可以使用 Record Lock、Gap Lock 和 Next-Key Lock 三种算法：</p>

<ul>
<li>Record Lock：针对单行记录的锁</li>
<li>Gap Lock：锁定一个范围</li>
<li>Next-Key Lock：锁定记录及记录前的一个范围，即 Record Lock + Gap Lock</li>
</ul>

<p>具体举例来说，如果数据库中目前有 <code>id</code> 分别为 1，2，5，6，7 的五条数据，并且有给这个列创建索引，那么我们使用一个 <code>WHERE id = 5</code> 的锁定读，Record Lock 锁定的是 <code>id = 5</code> 的记录，Gap Lock 锁定的是 <code>(2, 5)</code> 这个区间，而 Next-Key Lock 锁定的便是 <code>(2, 5]</code> 区间了。</p>

<p>InnoDB 正是使用了 next-key locking 的方法，在 RR 级别下如果使用了锁定读，便可防止其它事务向该区间插入数据，从源头上解决了幻读的问题，也就是说这个解决方案并不是 MVCC 那种用快照和行记录中的事务 ID 先后顺序判断的方式“隐藏”了新插入的数据，而是直接用锁禁止掉了其它事务的 <code>INSERT</code> 语句的执行。</p>

<p>对这几个锁的实际测试这里没有整理出 SQL 代码，因为涉及到了一些关于索引是否为唯一索引、锁降级、辅助索引等内容，想等之后把详细内容整理好并进行测试和验证，作为另一篇单独的文章发出来。</p>

<h3 id="">主要参考资料</h3>

<ul>
<li><a href="https://book.douban.com/subject/24708143/">MySQL技术内幕：InnoDB存储引擎</a></li>
<li><a href="https://draveness.me/mysql-innodb">『浅入浅出』MySQL 和 InnoDB</a></li>
<li><a href="https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/">The basics of the InnoDB undo logging and history system</a></li>
<li><a href="https://en.wikipedia.org/wiki/Isolation_%28database_systems%29">Isolation (Database Systems)</a></li>
<li><a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control">MVCC(Multiversion concurrency control)</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/">MySQL 8.0 Reference Manual</a>
<ul><li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html">InnoDB Multi-Versioning</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html">InnoDB Locking</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html">Transaction Isolation Levels</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html">Consistent Nonlocking Reads</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html">Locking Reads</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html">Locks Set by Different SQL Statements in InnoDB</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html">Phantom Rows(next-key locking)</a></li></ul></li>
</ul>

<p>文中的所有 SQL 测试代码都整合好放在了 <a href="https://github.com/KingsleyXie/NaiveProjects/blob/master/Snippets/trans_test.sql">Github</a> 上，演示用代码的图片是用 <a href="https://carbon.now.sh/">carbon</a> 生成的。</p>]]></content:encoded></item><item><title><![CDATA[从《编程之美》中的一道面试题开始谈谈位操作]]></title><description><![CDATA[<p>文章目录</p>

<p>在计算机的内部，数据都是使用二进制表示的，而对于计算机来说，进行基于二进制的位操作，效率要比进行除法、取余等操作高很多。最常见的对代码的优化就包含将乘除 2 的幂操作改为左右移位操作，很多时候编译器也会对代码自动执行这个优化。</p>

<p>我第一次在算法题的代码里见到位操作应该是在高二的时候<del>，某个晚自习不想学习然后跑到机房去乱翻别人的博客发现这个</del>，顿时就感觉如沐春风出神入化，所以一直印象挺深刻的。这个学期开学后不久在图书馆发现了《编程之美》这本书<del>，虽然里面的好多内容都看不懂</del>，其中有一道题感觉还蛮有趣的，就想从这道题出发，简单整理一下自己了解过的一些位操作相关的知识。</p>

<p>题目是这样的：</p>

<blockquote>
  <p>2.1 求二进制数中 1 的个数</p>
  
  <p>对于一个字节（8 bits）的变量，求其二进制表示中 “1” 的个数，要求算法的执行效率尽可能地高。</p>
</blockquote>

<p><em>Note：以下和题目相关的代码中，<code>n</code> 表示输入变量，<code>ans</code> 表示运算结果。</em></p>

<h3 id="">常规思路</h3>

<p>直觉告诉我们，大概也许应该可能差不多需要先将变量值转化为二进制，然后统计这个二进制表示中有多少个 1，最直接的方式当然是每次除以二取余然后累加这个余数了：</p>

<pre><code class="language-cpp">while (n)</code></pre>]]></description><link>http://kingsleyxie.cn/talking-about-bit-operations-from-a-problem-on-bop/</link><guid isPermaLink="false">ba9acd3d-edb2-4e0b-9a4f-ae2f14d78481</guid><category><![CDATA[Program]]></category><category><![CDATA[Bit Manipulation]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Wed, 16 May 2018 12:12:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/05/bits-cover.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/05/bits-cover.png" alt="从《编程之美》中的一道面试题开始谈谈位操作"><p>文章目录</p>

<p>在计算机的内部，数据都是使用二进制表示的，而对于计算机来说，进行基于二进制的位操作，效率要比进行除法、取余等操作高很多。最常见的对代码的优化就包含将乘除 2 的幂操作改为左右移位操作，很多时候编译器也会对代码自动执行这个优化。</p>

<p>我第一次在算法题的代码里见到位操作应该是在高二的时候<del>，某个晚自习不想学习然后跑到机房去乱翻别人的博客发现这个</del>，顿时就感觉如沐春风出神入化，所以一直印象挺深刻的。这个学期开学后不久在图书馆发现了《编程之美》这本书<del>，虽然里面的好多内容都看不懂</del>，其中有一道题感觉还蛮有趣的，就想从这道题出发，简单整理一下自己了解过的一些位操作相关的知识。</p>

<p>题目是这样的：</p>

<blockquote>
  <p>2.1 求二进制数中 1 的个数</p>
  
  <p>对于一个字节（8 bits）的变量，求其二进制表示中 “1” 的个数，要求算法的执行效率尽可能地高。</p>
</blockquote>

<p><em>Note：以下和题目相关的代码中，<code>n</code> 表示输入变量，<code>ans</code> 表示运算结果。</em></p>

<h3 id="">常规思路</h3>

<p>直觉告诉我们，大概也许应该可能差不多需要先将变量值转化为二进制，然后统计这个二进制表示中有多少个 1，最直接的方式当然是每次除以二取余然后累加这个余数了：</p>

<pre><code class="language-cpp">while (n)  
{
    ans += n % 2;
    // Same as:
    // if (n % 2 == 1) ans++;
    n /= 2;
}
</code></pre>

<p>当然如果了解过位操作的话就可以直接把乘法换成右移，取余换成和 1 进行按位与操作：</p>

<pre><code class="language-cpp">while (n)  
{
    ans += n &amp; 1;
    n &gt;&gt;= 1;
}
</code></pre>

<p>看起来代码并没有太大不同，但是正如文章开头所说，使用位操作的效率要比除法和取余高很多。尽管如此，优化只是作用在了常数上，两者的时间复杂度还是一样的 \(O(\log n)\)，其中 \(\log n\) 为 <code>n</code> 的二进制表示的位数。</p>

<h3 id="">降低时间复杂度</h3>

<h5 id="">原理</h5>

<p>书上给出了一种时间复杂度更低的解法，它基于这样的一个操作：</p>

<pre><code class="language-cpp">n &amp;= (n - 1)  
</code></pre>

<p>这个操作本身的作用是将 <code>n</code> 的最低位 1 置零，我们可以来看一下下面三个例子：</p>

<pre><code class="language-cpp">1011 - 0001 = 1010 (&amp; 1011 = 1010)  
1010 - 0001 = 1001 (&amp; 1010 = 1000)  
1100 - 0001 = 1011 (&amp; 1100 = 1000)  
</code></pre>

<p>稍微分析一下，不难看出为什么上述操作可以达到将最低位 1 置零的效果：</p>

<ul>
<li>如果 <code>n</code> 的末位就是 1，那么 <code>n - 1</code> 与 <code>n</code> 只有末位不同，与操作之后就保留了前面的所有位不变而将末位变为了 0。</li>
<li>如果 <code>n</code> 的末位不是 1，那么减法也不会影响到最低位 1 前面的数，而只会将它及其后的所有位取反（因为有一个借位的逻辑）。这样相反的位再进行与操作后就自然全部都是 0 了，也就是说最低位 1 及其后的所有位都变为了 0，效果上就是最低位 1 被置零。</li>
</ul>

<p>（不知道是不是描述得有点乱23333</p>

<h5 id="">应用</h5>

<p>这个操作的特性在这道题上可以完美利用上，我们只需要每次将最低位 1 置零然后累加操作次数，最后的结果正好就是其二进制表示中含有的 1 的个数：</p>

<pre><code class="language-cpp">while (n)  
{
    n &amp;= n - 1;
    ans++;
}
</code></pre>

<p>这个算法的时间复杂度降到了 \(O(M)\)，其中 \(M\) 为 <code>ans</code> 的大小，也就是 <code>n</code> 的二进制表示中含有的 1 的个数。现在算法的复杂度只与 1 的个数有关了，相对原来的与总位数有关算是降低了一些。</p>

<h3 id="">进一步了解执行效率</h3>

<h5 id="">打表</h5>

<p>写算法题代码的时候，有时为了效率会用空间换取时间，常见的一种<del>骚</del>操作就是打表，也就是预先算好所有的结果后放到一个数组中，运行的时候不需要进行计算而只需要访问对应下标的值即可，时间复杂度是 \(O(1)\)。</p>

<p>这个方法的代码我就不贴了233333，感觉面试的时候并不太敢这么做，怕是会被打死[捂脸]。不过这个题目给定了变量是一个字节的，可能也有让面试者往这个方向思考的意思，毕竟如果访问量巨大而输入范围很小的话，提前算好值然后查表取肯定是比每次重新计算的效率高很多的，所以还是取决于具体情景。</p>

<h5 id="">分支</h5>

<p>关于执行效率，额外想谈一下书上提到的对另一种解法的分析，也就是用类似打表的方式罗列出所有的情况，但是使用分支语句进行执行而非直接通过下标访问数组，代码类似这样：</p>

<pre><code class="language-cpp">switch (n)  
{
    case 0x00: // 0
        ans = 0;
        break;
    case 0x01: // 1
    case 0x02: // 2
    case 0x04: // 4
    case 0x08: // 8
    case 0x10: // 16
    case 0x20: // 32
    case 0x40: // 64
    case 0x80: // 128
        ans = 1;
        break;
    // ...
}
</code></pre>

<p>乍一看感觉好像效率和上面的那个 \(O(1)\) 算法差不多，但是仔细思考一下就会发现问题：<code>switch</code> 语句是会将表达式的值与每个 <code>case</code> 后的值进行逐一比较的，那么如果输入的 <code>n</code> 在这段语句偏后的部分，就需要进行多次比较后才能得到结果，几乎相当于对输入范围进行了一遍枚举，所以实际的执行效率可能甚至会低于最开始提到的两个算法。</p>

<h3 id="">微小的拓展</h3>

<h5 id="0">将最低位 0 置一</h5>

<p>有了将最低位 1 置零的操作，我们可能会考虑是不是应该也有将最低位 0 置一的操作（逃</p>

<p>其实对比分析一下两者的操作，然后又因为它们是相对的运算，很容易发现把各运算符取反一下就可以达到要求了：</p>

<pre><code class="language-cpp">n |= (n + 1)  
</code></pre>

<h5 id="2">判断 2 的幂</h5>

<p>2 的幂的二进制表示显然有个特点就是有且只有一位是 1，那么在理解了 \(O(M)\) 那个解法后，其实也容易想到，如果将最低位 1 置零后与原数进行与操作，结果就能表示这个数是不是 2 的幂了：</p>

<pre><code class="language-cpp">bool isPowerOfTwo = (!(n &amp; (n - 1))) &amp;&amp; (n &gt; 0)  
</code></pre>

<p>核心的判断就在 <code>n &amp; (n - 1)</code> 的运算结果，这个值如果为零就说明 <code>n</code> 只有一位是 1，也就代表 <code>n</code> 是 2 的幂。有趣的是在翻到下一题的最后有个“相关题目”一栏，里面也给出了这个题目和提示，当时看到的时候有种莫名的惊喜感hhh 明明是我先来的.jpg（</p>

<h3 id="">微大的拓展</h3>

<p>谈及位操作，也顺带一提关于 ASCII 大小写字母转换的一个 trick。</p>

<p>我们知道大写字母 <code>A</code> 的 ASCII 码是 65，小写 <code>a</code> 是 97，两者相差 32，并且之后的所有字母大小写相差都是按顺序同步下去的。之前在看王爽老师的《汇编语言》时，7.4 节（第三版里面）就提出了关于字母大小写转换的问题。</p>

<p>很容易想到的转换实现方案就是对大写字母加上 32，对小写字母减去 32，但是这样又涉及到了一个判断的问题，有没有其它更简洁的解决办法呢？这里就可以用上位运算的一些特性了，32 正好是 \(2^5\)，也就是对应 ASCII 码二进制表示的右起第六位（从一开始计数 = =），那么把这一位设置为 1 就可以保证结果是小写字母而不用考虑它原来是什么，反之设置为 0 的结果就一定是大写字母了。</p>

<p>那如果要求就是把大写字母变成小写，小写字母变成大写的话，还可以不通过判断实现吗？如果理解了上面的操作，那么就很容易想到这个问题其实就等同于将某一个特定位置的二进制位的值进行反转的问题，而针对特定位的反转，取非操作相对来说还是麻烦了一点，直接通过和这个位对应的值进行异或操作来实现会简洁很多。</p>

<p>在给代码之前先 xjb 提一下针对右起第 n 个二进制位的设置操作，首先最简单的当然是设为 1 了，直接和 <code>1 &lt;&lt; n</code> 进行按位或就行，同样地，反转一个位只需要直接和它进行按位异或：<code>num ^ (1 &lt;&lt; n)</code>。将某一位设为 0 就需要用【第 n 位为 0 而其它位全为 1 】的数进行按位与操作，很显然我们需要的这个数和刚刚提到的数是每一位都相反的，所以中间进行一次取反操作即可获得，也就是结果为 <code>num &amp; ~(1 &lt;&lt; n)</code>。对应下来，字母大小写转换相关操作代码如下：</p>

<pre><code class="language-cpp">// 0x20 stands for 32 in decimal

char upper = code &amp; ~(0x20);  
char lower = code | 0x20;  
char reverse = code ^ 0x20;  
</code></pre>

<p>如果想让代码看起来更有趣<del>（骚）</del>一点的话，可以把 <code>0x20</code> 改成 <code>' '</code>，毕竟空格的 ASCII 码正好就是 32，有没有觉得这个 ASCII 码表的设计好美妙嘿嘿嘿。</p>

<p>文章封面图片来自 <a href="https://www.videoblocks.com/video/computer-bits-running-over-computer-screen-hqxrx3fsipl3ezm2">VideoBlocks</a>。</p>]]></content:encoded></item><item><title><![CDATA[关于同源策略与跨域问题]]></title><description><![CDATA[<p>同源策略是现代浏览器安全里一个非常重要的机制，它通过限制客户端脚本读写不同源网站资源的权限来保护用户的信息安全。但是随着应用的复杂程度越来越高以及开发者的需求逐渐增多，很多时候又需要通过数据的跨域传输实现一些功能。</p>

<p>常用的跨域实现方式有 JSONP、CORS 或者服务器代理等。JSONP 依靠的是一些特殊 HTML 标签如 <code>&lt;img&gt;</code>、<code>&lt;script&gt;</code>、<code>&lt;link&gt;</code> 等可以加载非同源资源的特性，毕竟很多时候外码需要依靠单独的 CDN 服务器来减少对这些静态资源的请求时间，而 CORS 可以用于跨域的 AJAX 请求，是解决跨域问题非常重要和常用的技术，相比 JSONP 它支持的功能要多很多，不过代价就是需要服务端配置支持，当然这个配置也不麻烦。</p>

<p>写这篇文章是因为之前对同源策略和跨域问题刚了解的时候有一点误解，搞清楚之后决定用个简单的示例整理一下。关于它们的必要性、原理和实现之类的已经有很多讲得非常清楚的文章，比如 <a href="http://www.ruanyifeng.com/blog/">阮一峰的网络日志</a> 里有两篇专门介绍它们的博客（具体地址在文末参考资料处），我这里就直接从实际测试开始<del>瞎扯</del>了。</p>

<h3 id="">准备</h3>

<p>为了方便进行测试，这里的跨域就直接用“</p>]]></description><link>http://kingsleyxie.cn/same-origin-policy-and-cross-origin-problem/</link><guid isPermaLink="false">9790c573-d289-47b2-98d5-65c0a01cf04b</guid><category><![CDATA[Web]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Mon, 26 Mar 2018 15:11:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/03/cors.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/03/cors.png" alt="关于同源策略与跨域问题"><p>同源策略是现代浏览器安全里一个非常重要的机制，它通过限制客户端脚本读写不同源网站资源的权限来保护用户的信息安全。但是随着应用的复杂程度越来越高以及开发者的需求逐渐增多，很多时候又需要通过数据的跨域传输实现一些功能。</p>

<p>常用的跨域实现方式有 JSONP、CORS 或者服务器代理等。JSONP 依靠的是一些特殊 HTML 标签如 <code>&lt;img&gt;</code>、<code>&lt;script&gt;</code>、<code>&lt;link&gt;</code> 等可以加载非同源资源的特性，毕竟很多时候外码需要依靠单独的 CDN 服务器来减少对这些静态资源的请求时间，而 CORS 可以用于跨域的 AJAX 请求，是解决跨域问题非常重要和常用的技术，相比 JSONP 它支持的功能要多很多，不过代价就是需要服务端配置支持，当然这个配置也不麻烦。</p>

<p>写这篇文章是因为之前对同源策略和跨域问题刚了解的时候有一点误解，搞清楚之后决定用个简单的示例整理一下。关于它们的必要性、原理和实现之类的已经有很多讲得非常清楚的文章，比如 <a href="http://www.ruanyifeng.com/blog/">阮一峰的网络日志</a> 里有两篇专门介绍它们的博客（具体地址在文末参考资料处），我这里就直接从实际测试开始<del>瞎扯</del>了。</p>

<h3 id="">准备</h3>

<p>为了方便进行测试，这里的跨域就直接用“协议不同”这一点来实现了，服务端由本地的 PHP 处理进程充当，地址是 <code>http://localhost</code>，客户端就直接使用 <code>file://file/location</code>。</p>

<p>首先在客户端的 HTML 文件里写入一段发送请求的 JS 代码，<code>&lt;script&gt;</code> 标签自己加吧：</p>

<pre><code class="language-javascript">var request = new XMLHttpRequest();  
request.open('GET', 'http://localhost/cors.php?param=mydata');  
request.onload = function() {  
    console.log(this.response);
};
request.send();  
</code></pre>

<p>然后在本地服务器对应的根目录下建个 <code>cors.php</code>，内容如下：</p>

<pre><code class="language-php">echo 'return data';

$file = fopen('./demo.txt', 'a');
fwrite($file, $_GET['param']);  
fclose($file);  
</code></pre>

<h3 id="">发送请求</h3>

<p>在浏览器用 <code>file://</code> 协议访问客户端的 HTML 文件（其实就是直接用浏览器打开这个文件），然后 JS 代码被执行的时候请求就会被发送出去，看看这个时候的控制台情况：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/03/fail.png" alt="关于同源策略与跨域问题"></p>

<p>发现它报了个错误，<code>console.log()</code> 也没有执行到，然后再看看请求信息：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/03/request.png" alt="关于同源策略与跨域问题"></p>

<p>比正常请求多了个 <code>Origin</code> 的 Header，响应状态码是 <code>200 OK</code>，<code>GET</code> 请求的参数也正常发出了，响应信息里也有内容：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/03/response.png" alt="关于同源策略与跨域问题"></p>

<p>实际上，响应信息并没有被客户端或者说页面脚本获取到，这里是控制台给开发者的调试信息。接下来我们打开那个和 PHP 文件同文件夹的 <code>demo.txt</code>，会发现它记录着传过来的参数值 <code>mydata</code>。</p>

<h3 id="">解决问题</h3>

<p>要想让客户端能获取到返回值，只需要在 PHP 文件开头加上一行设置 <code>Access-Control-Allow-Origin</code> 的代码即可：</p>

<pre><code class="language-php">header('Access-Control-Allow-Origin: *');  
</code></pre>

<p>这个时候客户端重新发送请求，控制台就有了对返回值的记录：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/03/success.png" alt="关于同源策略与跨域问题"></p>

<p>请求和响应以及参数值被写入文件这些还是和之前一样的。</p>

<h3 id="">误解</h3>

<p>在我一开始了解到同源策略的时候，我联系到了 XSS 攻击，大部分情况下要传送一些数据的话，目的地都是攻击者自己的服务器，而这肯定是和实际浏览的页面不同源的。那既然有了同源策略，是不是就不用考虑 Cookie 等信息被这样获取到了呢。</p>

<p>显然我还是 Naive，我以为浏览器实现同源策略就是直接拦截掉跨域请求不给发送，但是从上面的实验中我们可以看到请求是发送出去了的，并且服务端可以像处理其它正常请求一样获取到所有的参数值。</p>

<h3 id="">简单整理</h3>

<p>通过 CORS 实现的跨域需要服务端的支持，浏览器在检测到跨域请求的时候，对于简单请求会直接在头信息里加上 <code>Origin</code> 字段表示请求的来源，然后被访问的服务器通过这个值判断是否许可本次请求，并在返回的头信息中设置 <code>Access-Control-Allow-Origin</code> 等配置参数，一般它的值就是收到的 <code>Origin</code> 的值，不过一些 API 提供方也会把它设置为通配符 <code>*</code>，表示允许任何来源的请求。浏览器收到响应信息后再根据 <code>Access-Control-Allow-Origin</code> 字段的值决定是否允许页面获取返回的数据。</p>

<p>对跨域的限制是浏览器的行为，但要实现跨域需要相应服务端的配合。它限制的是向别的站点读写资源的权限，但是对发送请求并没有做这样的限制，数据还是可以直接发送到其它站点。毕竟这个机制下，站点要收到一份请求才能判断和决定是否允许访问相应的内容。</p>

<p>封面图片来自 <a href="https://cdn-images-1.medium.com/max/1600/0*heiz7awNkQ1B0O8e.png">Medium</a>。</p>

<h3 id="">参考资料</h3>

<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Cross-Origin Resource Sharing</a></li>
<li><a href="http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html">浏览器同源政策及其规避方法</a></li>
<li><a href="http://www.ruanyifeng.com/blog/2016/04/cors.html">跨域资源共享 CORS 详解</a></li>
<li><a href="https://segmentfault.com/a/1190000009624849">同源策略与JS跨域（JSONP , CORS）</a></li>
<li><a href="https://segmentfault.com/a/1190000003710973">浏览器和服务器实现跨域（CORS）判定的原理</a></li>
</ul>

<p>顺带提下与此相关的另一个安全规范，可以看这一篇：<a href="https://imququ.com/post/content-security-policy-reference.html">Content Security Policy 介绍</a>。</p>]]></content:encoded></item><item><title><![CDATA[班门弄斧之谈谈 Web 密码安全]]></title><description><![CDATA[<p>文章目录</p>

<h3 id="">起因</h3>

<p>去年暑假在整理一个辣鸡项目的代码的时候，思考到用户密码存储相关的问题，然后也去找了点资料。我当时最开始用的是 SHA256 算法生成哈希值，然后更新的时候就强化了一下关于密码储存和验证的模块。后来其实也一直想写这篇文章出来，因为我发现很多不管是用户还是开发者都不太注重密码安全这个问题，甚至还有在数据库里明文存密码的。刚好在写完了挖坑不到一半停下的 <a href="http://kingsleyxie.cn/talking-about-web-password-security/../web-backend-advanced-for-bbt-tech-2017/">百步梯技术部 2017 级 Web 后端进阶</a> 之后，也把这篇文章的坑填上。</p>

<h3 id="">传输安全</h3>

<p>我们先来看看用户密码从在输入框输入到写入数据库以及后续的验证操作会经过哪些阶段。</p>

<p>注册时的流程当然是前端把数据传送给服务端，然后服务端接收后进行一些处理写入数据库。验证时取出这个处理后的值，然后处理刚刚的验证请求发来的密码，最后进行比对。这里的“处理”具体是什么后面会详细讲。</p>

<p>这个阶段主要的安全问题在于，数据从前端发送给服务器的过程可能会被窃听。所以一般涉及密码输入的页面都强制要求使用 <code>HTTPS</code> 协议，现代的大部分浏览器也会对在非 <code>HTTPS</code> 页面上输入密码这些敏感操作给出高危提示。</p>

<p>有些时候会看到有网页把密码先在前端加密后再发送给服务器，感觉这个操作意义其实不大。想象一下如果加密后的数据在传输的时候被窃听了，那窃听者在伪装成用户的时候，把这个字符串同样地发送给后端，是不是还是可以达到欺骗的效果。换句话讲，对后端服务器而言，无论传输过来的密码实际上是否有进行加密，都只是它后续操作的原文而已。</p>

<p>更严重的问题是，比如客户端用非对称加密算法对密码进行加密，</p>]]></description><link>http://kingsleyxie.cn/talking-about-web-password-security/</link><guid isPermaLink="false">fac9d1a7-cf82-4fc4-8f11-1b55fc43ae0d</guid><category><![CDATA[Web]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Sat, 17 Mar 2018 03:32:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/03/pwd-sec.jpeg" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/03/pwd-sec.jpeg" alt="班门弄斧之谈谈 Web 密码安全"><p>文章目录</p>

<h3 id="">起因</h3>

<p>去年暑假在整理一个辣鸡项目的代码的时候，思考到用户密码存储相关的问题，然后也去找了点资料。我当时最开始用的是 SHA256 算法生成哈希值，然后更新的时候就强化了一下关于密码储存和验证的模块。后来其实也一直想写这篇文章出来，因为我发现很多不管是用户还是开发者都不太注重密码安全这个问题，甚至还有在数据库里明文存密码的。刚好在写完了挖坑不到一半停下的 <a href="http://kingsleyxie.cn/talking-about-web-password-security/../web-backend-advanced-for-bbt-tech-2017/">百步梯技术部 2017 级 Web 后端进阶</a> 之后，也把这篇文章的坑填上。</p>

<h3 id="">传输安全</h3>

<p>我们先来看看用户密码从在输入框输入到写入数据库以及后续的验证操作会经过哪些阶段。</p>

<p>注册时的流程当然是前端把数据传送给服务端，然后服务端接收后进行一些处理写入数据库。验证时取出这个处理后的值，然后处理刚刚的验证请求发来的密码，最后进行比对。这里的“处理”具体是什么后面会详细讲。</p>

<p>这个阶段主要的安全问题在于，数据从前端发送给服务器的过程可能会被窃听。所以一般涉及密码输入的页面都强制要求使用 <code>HTTPS</code> 协议，现代的大部分浏览器也会对在非 <code>HTTPS</code> 页面上输入密码这些敏感操作给出高危提示。</p>

<p>有些时候会看到有网页把密码先在前端加密后再发送给服务器，感觉这个操作意义其实不大。想象一下如果加密后的数据在传输的时候被窃听了，那窃听者在伪装成用户的时候，把这个字符串同样地发送给后端，是不是还是可以达到欺骗的效果。换句话讲，对后端服务器而言，无论传输过来的密码实际上是否有进行加密，都只是它后续操作的原文而已。</p>

<p>更严重的问题是，比如客户端用非对称加密算法对密码进行加密，却忽视了公玥被中间人替换之类的安全风险，那这个加密操作也就基本形同虚设了。就是说如果传输的渠道本身就是不安全的，光靠这些信息加密操作并没有多大意义。总而言之，<strong>Web 前端发送密码等数据给服务器这个阶段的传输安全，应该由 <code>HTTPS</code> 协议保证</strong>，而不需要提前对某些值应用加密算法。</p>

<h3 id="">存储安全</h3>

<h5 id="">加密哈希函数</h5>

<p>要了解接下来的内容，首先需要有一个有关“加密哈希函数”的概念。具体的定义我可能也描述不好，有兴趣的可以自己去找相关的资料，这里就列举一下它那些能被用来做加密和验证的特性：</p>

<ul>
<li>对于任意长度的输入值，输出值的长度都是相同且固定的</li>
<li>对相同的输入值，输出值总是相同的</li>
<li>对哪怕相差很少的两个输入值，输出值都有非常大的差异</li>
<li>由输入值计算输出值很容易，但由输出值反推出输入值非常难</li>
<li>几乎不可能构造出输出值完全相同的两个输入</li>
</ul>

<p>实际上，根据 <a href="https://zh.wikipedia.org/wiki/%E9%B4%BF%E5%B7%A2%E5%8E%9F%E7%90%86">鸽巢原理</a>，输出值的取值域有限，而输入值的取值域可以视为无穷，还是存在产生相同输出的两个输入值的。如果对于一个加密哈希函数，可以相对容易地找到这样两个值，这个时候这个加密哈希算法当然就是不安全的了。</p>

<p>而第四点的“非常难”指的是，反推的时间消耗极大，大到在现实时间内无法接受，于是我们可以认为这个反推是不可能的。实际上密码学的很多安全都是基于一些数学上的难题实现的，比如 RSA 加密就基于对大整数进行因数分解的困难<del>，数学真是美妙啊</del>。</p>

<p>由此我们可以看到，对密码进行加密哈希，并将得到的结果存入数据库，后续通过同样的散列算法进行比对，即可在保证安全的前提下验证密码是否一致。</p>

<h5 id="">逻辑实现</h5>

<p>下面就可以进行逻辑上的实现了，基本上只要提供了加密哈希函数库的语言都可以做到，代码量也很少。使用过程中不会涉及哈希函数的构造和实现<del>，而且我也不会</del>，但是有一个意识必须有：正式上线使用的密码相关的函数，不要自己去实现，而应该使用身经百战的公认的代码库里的函数，否则如果实现上有问题，加密哈希的安全性就没有保证了。</p>

<p>我们在用户数据表里增加一个字段 <code>hash</code>，用来存储密码的哈希值。在注册操作进行时，计算好用户的密码哈希，然后存到这个字段里。在验证的时候，取出这条数据对用户名和密码进行验证。</p>

<p>这里顺便提一下，一般验证的时候会把“用户名不存在”和“密码错误”视为同一个错误“用户名或密码错误”，也就是不给登录者输入的账号是否存在的提示，这样在一些防范盲目攻击的时候有比较大的作用，攻击者如果不知道有哪些账号是存在的而哪些是不存在的，就可能会耗费更多的时间在尝试用户名和密码上。当然这个还是取决具体的项目场景，如果觉得有必要给登录者账号是否存在的提示的话就应该分别处理。</p>

<h3 id="">存储安全强化</h3>

<p>这里所谈的“安全”，其实指的是在数据库泄露的情况下如何保证用户的密码不泄露。试想攻击者通过某些漏洞拿到上面的那张数据表，他真的不能由此得到用户的密码吗?</p>

<p>尽管加密哈希可以保证从哈希值直接反推出原文难以实现，但我们很容易想到一种方法得到部分弱密码的用户名：预先计算常见的弱密码的哈希值，再对整张表进行比对，如果有相同的值那么他们的原文一定是相同的，于是就可以得知这个用户名对应的密码，这个预先计算好的表叫做 <a href="https://zh.wikipedia.org/wiki/%E5%BD%A9%E8%99%B9%E8%A1%A8">彩虹表</a>（<a href="https://en.wikipedia.org/wiki/Rainbow_table">Rainbow table</a>）。</p>

<p>还有一个问题，同样基于“对相同的输入值，输出值总是相同的”这一特性。如果攻击者发现表中有两个密码哈希值相同，而他又掌握着其中一个人的密码，这个时候所有相同哈希值的密码就都被推知了。毕竟密码是人想出来的，为了便于记忆还是可能有一些使用相同密码的情况，而这个时候哪怕有一个人的密码没保护好，安全影响的范围就会扩大。</p>

<p>有没有什么办法可以保护这些密码呢？对于弱密码来说，输入时的密码强度限制当然是非常重要的一个手段，但是后端也仍然有义务做好密码安全方面的工作，所以就有了以下两种常见的强化方式。</p>

<h5 id="salt">salt</h5>

<p>既然用户输入的密码太弱，我们是不是可以强行让它变强[奸笑]。比如用户输入的密码是 <code>123456</code>，我们可以在存储的时候拼接一段随机字符串比如 <code>f21sdsd0fsa</code>，然后把拼接后的串 <code>123456f21sdsd0fsa</code> 作为加密哈希函数的输入进行计算，得到的值和拼接的随机字符串一起存入数据库。</p>

<p>实际上，这个拼接的随机字符串有它自己的名字，叫做 <code>salt</code>，中文一般叫“盐值”。密码加盐之后就会变咸，咸了就更难破解了（逃</p>

<p>具体实现的时候可以有很多方式对密码进行加盐操作，前后拼接是比较简单直接的方式，还有诸如间隔插入等打乱原密码程度更高的操作，只要保证操作用上了盐值的所有位并且能复现就可以了，当然没事也别搞得太复杂，毕竟真正的安全核心不在这块，而在于加密哈希函数的安全性和盐值的长度与随机性。</p>

<p>就数据库储存方面而言，可以增加一个 <code>salt</code> 字段用于存储盐值，然后原来存哈希值的字段用于存加盐后的密码哈希。验证的时候同样是取出用户名对应的数据然后用这个盐值拼接输入的密码，再对比计算出的哈希值。当然也可以只使用原来的字段，把盐值和哈希值拼接后作为一个值写入数据库，反正它们是一一对应的，只要能方便完成分离和拼接就不会对业务逻辑有什么影响，注意字段的长度容量要控制好。</p>

<p>特别注意加盐的操作是对每个密码单独进行的，而且密码所用的盐值不能相同，如果都用同一个盐值的话对于之前提到的两个安全问题都是没有帮助的：相同的密码还是得到了相同的哈希值，而预先计算的攻击方式，由于盐值是和加盐后的哈希一起存放的，攻击者可以用这个盐值去进行预先计算的操作，和破解不加盐的攻击耗时基本相同……嗯其实准确一点说，不加盐的哈希值网上可以直接下载到一堆，配合使用不够安全的加密哈希算法的话基本和明文存是同样的危险级别，可以自己去搜索验证一下，很多网站不仅提供这类文件下载还直接提供在线破解功能。</p>

<h5 id="pepper">pepper</h5>

<p>考虑更极端的情况，数据库泄露了之后攻击者同时拿到了盐值和加盐后的哈希值，然后他就可以用这些数据直接强行计算一些弱密码拼接对应盐值后的哈希值进行比对，因为每个哈希值都有唯一对应的盐值。当然实际上这个计算量是很大的，除非是非常重要或者有很大商业价值的密码，一般不会有人这么做<del>，留着这计算能力干点别的可能收入还更多呢</del>。</p>

<p>为了避免这一类攻击，又出现了另一种强化的方式，叫做 pepper，不过我没找到它的中文叫什么，直译是“胡椒”我们都知道，嗯……和盐一起就是椒盐了hhh。</p>

<p>pepper 的基本机制和 salt 是一样的，也是通过拼接一段随机字符串来加强密码安全性。不同的地方在于，它不和加盐后的哈希一起存储，而是单独存放在一些和应用程序无关的地方，甚至可以写到硬件里面。这样的话数据库泄露后由于攻击者不能拿到 pepper 值，就不能进行预先计算之类的攻击了。</p>

<p>而对于从相同哈希值得知相同密码的情况，一般使用 pepper 的时候不会只有一个，而是预先生成很多个然后在计算的时候随机选择其中一个，生成哈希值存放进数据库。验证的时候需要遍历所有的 pepper 值进行拼接计算哈希，然后对比取出的哈希值，如果有匹配到就验证成功，如果一个都没匹配上就验证失败。显然 pepper 的数量太少的话还是容易有风险，而随着数量的增加计算量也会增加很多，在登录注册请求较多时会给后端带来很大压力，这个阈值要做好取舍。</p>

<p>可能会有人考虑再极端一点的情况，就是攻击者直接在登录页面一个个尝试弱密码，有没有可能被撞对几个。对于这种情况，除了前面提到的不提示用户名是否存在的操作以为，还有很多防护措施，比如我们平时经常看到的验证码、二次认证、手机扫码登录、不常用登录地址判断等，此外还有请求峰值限制、IP 限制之类的操作，要想从这个渠道一个个试密码，代价是非常高的。</p>

<p>其实攻击和防护本来也不可能做到任何一方完全主导，对于防护者来说，只能不断提高攻击的成本，而不能指望有一个完美的解决方案。就拿登录验证来说，我们必需保证正常用户登录的时候可以得到正常的处理和返回结果，那总不可能为了彻底防止攻击直接关掉登录接口吧。</p>

<h5 id="">简单对比</h5>

<p>先给一下 <a href="https://en.wikipedia.org/wiki/Salt_(cryptography)">salt</a> 和 <a href="https://en.wikipedia.org/wiki/Pepper_(cryptography)">pepper</a> 的维基百科链接，对应的中文页面前者是缺了好多内容后者是直接没有，所以还是看英文的吧。</p>

<p>salt 和 pepper 的基本机制比较类似，都是通过打乱和增长原密码来强化安全性。但是 salt 是关联到每个具体密码的，而 pepper 单独存放，一般不放在和项目相关的文件夹或数据库中。在使用的时候，它们都需要保证值的随机性，并且要有足够的长度和复杂度。</p>

<p>这两种强化方式各有自己的优缺点（好像是废话），实际使用的时候，如果是比较重要或者密码非常敏感的项目一般是要两者同时用上的。不过我自己就只用加盐一项，pepper 毕竟需要单独存放值，而且好像也比较少看到有关的资料。</p>

<h3 id="">具体实现</h3>

<p>接下来简单讲讲关于密码加盐哈希具体的实现方式，pepper 的实现机制相差不算特别大就不提了，有兴趣的可以自己去尝试<del>，主要是我也没在线上项目里用过</del>。</p>

<h5 id="">算法选择</h5>

<p>具体实现首先要考虑的问题当然是选用什么算法了，前面也提到了有关密码安全的函数不要自己造轮子，所以这个也可以说是选用系统或语言提供的什么函数的问题。</p>

<p>密码学相关的内容总是需要非常严谨的，毕竟应用上去之后出现问题麻烦就大了。一般使用的算法是 SHA(Secure Hash Algorithm) 家族，它是目前很多机构和组织公认的标准，既然是家族那肯定是有很多版本的，我直接从 <a href="https://zh.wikipedia.org/wiki/SHA%E5%AE%B6%E6%97%8F">维基百科</a> 上截个图：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/03/sha-functions.png" alt="班门弄斧之谈谈 Web 密码安全"></p>

<p>其中的 MD5 是用作对比的，它并不是 SHA 家族里的成员。</p>

<p>我们可以看到右侧的“碰撞攻击”一栏给出了这些算法目前的安全性，MD5 在 1996 年后就被证实存在弱点可以被加以破解了，而 SHA-1 现在也已经出现了一些问题，它们都不应该再被视为安全的算法。</p>

<p>据此而言我们选择使用的算法应该至少是 SHA-2，它至少在目前还没有出现明显的可攻击弱点，我之前使用的是 SHA-256，现在推荐还是用高位一些的比如 SHA-512 之类的。</p>

<h5 id="">盐值生成</h5>

<p>盐值因为一般也要求定长，很容易想到直接使用哈希函数的输出值，而输入值只要是随机数就可以满足它的随机性要求了。虽然也可以直接用时间戳之类的作为盐值，但是毕竟它的值是一直增长的感觉还是不够吼，而通过限定上下限产生的随机数又是取值范围相对强哈希小一些的纯数字，不太推荐使用。</p>

<p>这个输出的哈希值只要长度满足我们的要求就行了，并不需要太过考虑被破解的问题，毕竟一个随机数拿到也没有什么用，至于具体选择哪个算法<del>，还是看心情吧</del>，我并没有找到太多相关的资料。看到一小部分说法是盐值长度要和加密哈希的最终输出长度相同，不过我个人感觉像 SHA-1 之类的输出应该就已经足够了。</p>

<h5 id="">代码示例</h5>

<p>下面以 PHP 为例给点简单的代码示例，直接的哈希函数有 <code>md5()</code> 和 <code>sha1()</code>等，但是在这些函数的官方文档页面也给了不要将它们用于对密码进行哈希操作的警告，还附带一个链接到详细说明有关密码哈希问题的 <a href="http://php.net/manual/en/faq.passwords.php">文档</a>……嗯我感觉这个页面的内容还是很有必要看看的。</p>

<p>其它的哈希算法就直接都放在 <code>hash()</code> 函数里了，通过参数选择想要使用的具体算法，为了便于演示和理解这里就直接用它配合 SHA-512 进行加盐哈希，其实实现上还是有安全漏洞，具体原因后面会提及。</p>

<p>代码本身很简单：</p>

<pre><code class="language-php">// Hash
$salt = sha1(mt_rand());
$hash = hash('sha512', $password . $salt);

// Verify
$curr_hash = hash('sha512', $input . $salt);
$passed = ($curr_hash === $hash);
</code></pre>

<p>前面两个结果存到数据库里，验证的时候取出再计算对比即可。</p>

<h3 id="">时序攻击</h3>

<h5 id="">问题</h5>

<p>在比对字符串的时候，一般的做法是从前往后逐一进行比较，遇到不同的值直接中断然后返回结果。那么在字符串长度比较大的时候，前面很多位相同和第一位开始就不同的处理时间就会有区别。抛开网络请求和其它处理流程不看，仅考虑字符串验证的部分，校验时间越长就说明匹配的位数越多，攻击者于是可以通过校验时间的长短大致判断出是哪一位开始出现了不同值，然后对应进行更改并不断重复这个流程，这种做法需要的尝试次数可比盲猜少多了。</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/03/unacceptable.jpg" alt="班门弄斧之谈谈 Web 密码安全"></p>

<p>可见有时候性能太高也不见得是好事，甚至可能语言本身实现的比较并没有这个问题，但编译或解释代码的时候为了执行性能而进行了这样的优化，用到安全相关的代码上就产生漏洞了。</p>

<p>这种攻击方式叫做时序攻击（<a href="https://en.wikipedia.org/wiki/Timing_attack">Timing attack</a>）。当然如果加上网络请求以及其它后端处理的话，这种时间差不会有太大影响，但是千万不要觉得它只是理论上存在的一种攻击方式，时序攻击的利用是有真实出现过的。</p>

<p>类似的手段还有比如通过对芯片能量消耗、功率曲线等数据的分析来破解需要的数据，这类攻击的总称是 <a href="https://en.wikipedia.org/wiki/Side-channel_attack">Side-channel attack</a>，可以翻译为“旁路攻击”，它们的出发点不在算法本身而在其它漏洞甚至物理实现上。</p>

<p>说实话如果不是查资料了解到这种攻击类型的存在，我真不见得能自己意识到一个密码系统能在这种地方出现漏洞，所以安全相关的问题还是远比想象的难得多啊。我也是在这个时候理解了为什么说“涉及密码安全的大部分算法都是为密码学单独设计的”，整个系统中任何一个环节稍一疏忽就可能导致所有的努力都白费。</p>

<h5 id="">解决方案</h5>

<p>解决方案当然就是让字符串对比的操作耗时恒定啦，我们可以在对比操作过程中根据被对比的字符串长度定下对比时间。然后在对比操作时仅记录下状态而不要急着直接中断操作，并且保证对任意长度的输入都会进行相同次数的同类运算，到最后再返回对比结果。</p>

<p>注意定下的对比时间不能依赖于输入字符串的长度，因为它也是可变的。实际的实现为了方便可以要求对比的两个字符串等长，不等长的情况下直接返回 <code>false</code>，当然依靠时序攻击就可能得知哈希值的长度，不过拿到长度也没有很大用处对吧= =</p>

<p>很多语言的相等比较操作符和 <code>strcmp()</code> 之类的字符串比较函数都是耗时非恒定的，毕竟要考虑到语言整体的性能。但是它们也一般会提供一些专用于密码哈希值比较的库、模块或函数，一些框架里也会单独提供这些功能，像 PHP 里密码专用的哈希函数和针对时序攻击强化过的验证函数就分别是 <a href="http://php.net/manual/en/function.crypt.php"><code>crypt()</code></a> 和 <a href="http://php.net/manual/en/function.hash-equals.php"><code>hash_equals()</code></a>。</p>

<h3 id="">收尾之前</h3>

<p>PHP 还提供了更方便使用的哈希和验证函数，就是 <a href="http://php.net/manual/en/function.password-hash.php"><code>password_hash()</code></a>和 <a href="http://php.net/manual/en/function.password-verify.php"><code>password_verify()</code></a>，前者的输出值直接涵盖加密算法、算法选项（如消耗代价）、盐值和实际哈希值，这样用一个值存储的话无论是维护还是验证都更加高效和便捷了：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/03/php-crypt-sample.svg" alt="班门弄斧之谈谈 Web 密码安全"></p>

<p>设计数据库的时候同样要特别留意它的长度容量，因为这个不像我们上面演示的做法一样能保证长度一直不变，它默认是根据系统环境提供的功能多少以及当前能实现的密码算法强度等因素决定的，官方推荐的长度是“最好 255 个字符”。</p>

<p>对应前面的代码示例，安全一点的做法就是这样的：</p>

<pre><code class="language-php">// Hash
$salt = sha1(mt_rand());
$hash = password_hash($password, PASSWORD_DEFAULT, ['salt' =&gt; $salt]);

// Verify
$passed = password_verify($input, $hash);
</code></pre>

<p>PHP 7.0.0 开始弃用了 <code>salt</code> 选项，也就不需要我们考虑盐值的生成问题了，它会自己用默认方式生成。所以代码就简单了更多，一行生成一行验证即可，安全性相比我们最开始的实现也有非常大的提升：</p>

<pre><code class="language-php">// Hash
$hash = password_hash($password, PASSWORD_DEFAULT);

// Verify
$passed = password_verify($input, $hash);
</code></pre>

<p>如果稍微看过上面提到的专用于密码的函数的文档的话，就会发现它们默认不会使用 SHA 家族的函数，而是用比如 <code>blowfish</code> 之类的哈希算法。这其实和前一部分一样也是因为性能太好产生了安全问题：SHA 生成哈希结果太快了，这就导致通过枚举计算可能的取值来暴力破解的代价小了很多，所以一般会选择比较慢的哈希算法来真正应用到密码安全相关的函数里。</p>

<h3 id="">小结</h3>

<p>我就简单谈谈我自己的看法吧，我觉得用户选择了我们的产品，我们就有义务保护好他们的数据安全，尤其是密码相关的内容，这就类似木桶效应，特别是现在很多人都在各种地方使用相同的密码的情况下，一个安全漏洞导致的密码泄露，可能会影响到这个用户的其它各种账号安全。</p>

<p>从另一个方面来讲，用户自己也应该尽量避免使用弱密码并保护好自己的密码安全，不能完全依靠或者说完全信任开发者的安全防护方案。</p>

<p>那么，了解完这些后应该也就明白为什么用到密码的网站都能验证和重置密码，却不能真正“找回”密码了，因为哪怕是数据库的管理员也没法知道密码原文究竟是什么，需要知道密码的只有用户自己。</p>

<h3 id="">题外话</h3>

<p>这里的密码保存及验证考虑到实际需求尤其是安全性问题，使用的是单向散列函数，定长输出值的特性方便了存储结构的设计，而不能逆推的特性保证了存储后的原密码安全。有时候我们需要用到能解密的加密函数，这也是个非常有趣的话题，尤其是有关对称加密和非对称加密的原理，让人不禁啧啧称赞数学的美妙（</p>

<p>另一方面，除了专为密码学用途设计的这些加密哈希函数以外，还有很多的其他哈希函数。类似的单向散列函数本身也有非常多的其他用途，比如我们经常用到的文件校验机制，同样利用了它们的一些特性以保证接收的文件在传输过程种没有经过他人的篡改。</p>

<p>然后再次安利结城浩的《图解密码技术》，讲得非常清晰易懂而且涵盖了关于加密、摘要、认证等方面的相关技术。当然这个是科普层面的书籍，适合我这种数学不太好的人看，如果想再深入了解的话可以找点密码学相关的专业经典来看（逃</p>

<p>文章封面图片来自 <a href="https://cdn-images-1.medium.com/max/2000/1*WI8d3fUDy3OOFu-PpVTE2Q.jpeg">Medium</a>，所有代码示例的整合版本也都放在了 <a href="https://github.com/KingsleyXie/NaiveProjects/blob/master/Snippets/hash_demo.php">GitHub</a>。</p>]]></content:encoded></item><item><title><![CDATA[百步梯技术部 2017 级 Web 后端进阶]]></title><description><![CDATA[<p>文章目录</p>

<h3 id="">写在前面</h3>

<p>这篇文章是面向百步梯技术部 2017 级后端组的同学们的，结合目前大家的学习进度以及时间安排，并参考去年同期的我们接下来所需要的知识储备，整理出下面这些内容。鉴于大家现在也没有上手使用后端框架，写这篇文章的时候就完全没有考虑框架相关的内容了，比如配置等方面的东西如果是用框架的话这里就没有必要讲了。</p>

<p>文章的大部分都是关于后端安全的，并且由于主要偏向的是安全意识这块的内容，就没有给出太多实际的代码，只是希望大家能了解在后端的开发过程中需要注意哪些问题，在接下来的锻炼中我相信大家也会更深刻地理解重视这些问题的意义和作用。</p>

<p>虽然标题是“进阶”，但实际上也只是相对“入门”而言在难度方面有一小部分提高，距离真正的进阶其实还有挺长的路，不过不要太担心啦 2333，踏踏实实走下去才是坠重要的。以及，如果有什么讲得不太好的地方也欢迎大家提出。</p>

<h3 id="">参数验证</h3>

<p>看到大家目前的后端代码都有做这一块的判断，无论是基本的非空非零判断还是基于正则表达式的格式判断都有考虑，挺不错的<del>，虽然有些时候逻辑写得有点糟糕</del>。</p>

<p>那我想额外讲讲有关参数验证的必要性之类的问题，后端一般是介于前端和数据库之间的一层服务，实际上前端和数据库也都会对它们涉及到的参数进行验证，但是尽管如此，在后端逻辑里进行参数验证还是非常必要的，具体原因我就试着分这两块来讲。</p>

<h5 id="">前端与后端</h5>

<p>这是参数验证的一个非常重要的原因，后端开发有个基本的准则：<strong>前端传来的任何数据都是不可信的</strong>。</p>

<p>如果大家用过一些诸如 <a href="https://www.getpostman.com">Postman</a> 之类的后端调试工具的话，就应该知道前端的请求发送都是可以通过其他方式模拟的，换句话说就是，在请求的数据发送给后端之前的任何操作和验证实际上都可以绕过。所以前端对某个参数进行的检查并不能保证这个数据传给后台的时候一定符合要求。</p>

<p>那既然如此，</p>]]></description><link>http://kingsleyxie.cn/web-backend-advanced-for-bbt-tech-2017/</link><guid isPermaLink="false">ea98b11d-3d2e-40b5-a395-43d183c9ade3</guid><category><![CDATA[BBT]]></category><category><![CDATA[Web]]></category><category><![CDATA[Program]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Fri, 09 Mar 2018 11:50:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/03/backend.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/03/backend.jpg" alt="百步梯技术部 2017 级 Web 后端进阶"><p>文章目录</p>

<h3 id="">写在前面</h3>

<p>这篇文章是面向百步梯技术部 2017 级后端组的同学们的，结合目前大家的学习进度以及时间安排，并参考去年同期的我们接下来所需要的知识储备，整理出下面这些内容。鉴于大家现在也没有上手使用后端框架，写这篇文章的时候就完全没有考虑框架相关的内容了，比如配置等方面的东西如果是用框架的话这里就没有必要讲了。</p>

<p>文章的大部分都是关于后端安全的，并且由于主要偏向的是安全意识这块的内容，就没有给出太多实际的代码，只是希望大家能了解在后端的开发过程中需要注意哪些问题，在接下来的锻炼中我相信大家也会更深刻地理解重视这些问题的意义和作用。</p>

<p>虽然标题是“进阶”，但实际上也只是相对“入门”而言在难度方面有一小部分提高，距离真正的进阶其实还有挺长的路，不过不要太担心啦 2333，踏踏实实走下去才是坠重要的。以及，如果有什么讲得不太好的地方也欢迎大家提出。</p>

<h3 id="">参数验证</h3>

<p>看到大家目前的后端代码都有做这一块的判断，无论是基本的非空非零判断还是基于正则表达式的格式判断都有考虑，挺不错的<del>，虽然有些时候逻辑写得有点糟糕</del>。</p>

<p>那我想额外讲讲有关参数验证的必要性之类的问题，后端一般是介于前端和数据库之间的一层服务，实际上前端和数据库也都会对它们涉及到的参数进行验证，但是尽管如此，在后端逻辑里进行参数验证还是非常必要的，具体原因我就试着分这两块来讲。</p>

<h5 id="">前端与后端</h5>

<p>这是参数验证的一个非常重要的原因，后端开发有个基本的准则：<strong>前端传来的任何数据都是不可信的</strong>。</p>

<p>如果大家用过一些诸如 <a href="https://www.getpostman.com">Postman</a> 之类的后端调试工具的话，就应该知道前端的请求发送都是可以通过其他方式模拟的，换句话说就是，在请求的数据发送给后端之前的任何操作和验证实际上都可以绕过。所以前端对某个参数进行的检查并不能保证这个数据传给后台的时候一定符合要求。</p>

<p>那既然如此，前端的数据验证意义何在呢？可以设想一下，如果每次填写一张表单都要等到提交后才能知道哪些数据是不符合要求的，用户体验是不是就非常差了。而且这样也会导致服务端的压力增大，可能因为响应过慢而进一步降低了用户体验。</p>

<p>其实我们也能看到现在很多表单的用户体验做得非常棒，输入框验证数据都是实时的，<del>毕竟有了万能的 JavaScript，绑定个 <code>onchange</code> 事件就可以了</del>：在用户离开当前输入框时进行验证，不符合条件直接显示错误信息，然后在改对后自动去掉。我个人感觉这种实时提醒修改的逻辑要比最后一起验证批量提醒更能节省用户的时间，体验也好很多（想象一下点完提交发现一片红的极端情况= =），值得学习一个。</p>

<p>所以一般来说，<strong>前端进行参数验证是为了优化用户体验以及减轻服务端压力，而后端进行参数验证是为了安全，防止一些乱搞的数据被写入数据库</strong>。</p>

<h5 id="">后端与数据库</h5>

<p>在创建数据库的时候，我们可以对表里的字段增加一些约束条件，最简单的比如 <code>NOT NULL</code> 之类的，它们也可以看作是一种参数验证，因为不满足这些约束条件的数据会被数据库系统直接拒绝写入。</p>

<p>但是很显然，我们不能把所有对参数的验证都放在数据库这个服务层上，增大数据库压力是一部分原因，出错时相对后端验证而言不方便捕捉和返回错误信息等也是部分原因。</p>

<p>由于后端的验证并不能由用户直接绕过，所以一些相对不是那么必要的格式约束就可以只放在后端进行。当然设计数据库的时候还是要做好业务层面的格式约束的，这样能避免后端考虑不周导致的后果，也方便后期的维护。</p>

<p>总之，参数验证主要要考虑的就是从“从前端拿到数据之后”到“写入数据库之前”中间这个阶段需要做什么样的判断操作，尤其是“从前端拿到的任何数据都是不可信的”这一点，务必要作为思考逻辑流程时参照的基本准则。</p>

<p>当然如果为了验证写一堆的 <code>if...else</code> 可能会很烦，不过到这个程度的时候基本是在用框架了，很多框架都做了这方面的简化工作，像通过注解来直接限制传入参数的格式等，有兴趣的可以去了解一下<del>或者尝试自己实现一个类似的东西</del>。</p>

<h3 id="">权限控制</h3>

<p>很多时候一个项目并不是所有的页面都完全开放的，有些可能要求用户登录之后才能查看，再复杂一点的可能要求登录的用户有相应级别的权限才能访问一些特定路径下的页面或者执行一些敏感操作。</p>

<p>权限控制大部分是后端的工作，当然前端的判断也是很重要的：需要权限的入口不应该直接展示给无权限的用户（为了吸引用户注册那种这里就不考虑了），这个具体的实现可以前后端协商，比如前端通过一个后端接口获取当前用户的权限级别和允许的入口列表等然后对应这些数据构造和渲染页面之类的。</p>

<p>但是就像前面提到的，这个前端的限制可以很容易被绕过，而且主要还是为了用户体验而不是安全，所以后端在接收到请求之后必须判断用户有没有对应的权限，这里就稍微提一下一些常用的权限控制方法，实际上因为权限对应的是用户，很多时候主要工作就是验证用户身份。</p>

<p><em>Note: 接下来的内容部分涉及到有关编码、摘要和加密等方面的内容，为了避免叙述混乱我就尽量直接用“验证”之类的词来表达，只要理解它可以在保证安全的情况下做到验证内容是否符合要求即可，具体的内容可以自己去搜索相关的资料学习，顺便安利一下结城浩的《图解密码技术》（最后会提到这个）</em></p>

<h5 id="session">Session</h5>

<p>在开始之前，我想先简单讲讲有关 Session 的知识。我们知道，HTTP 本身是无状态的协议，每一个请求都是独立的，服务端不能知道连续接收的两个请求是不是来自同一个会话，于是后来<del>随着时代的发展和需求的增加</del>出现了 Cookie 和 Session 这样的机制。</p>

<p>Cookie 一般由服务端生成并保存在浏览器等客户端中，在后续的请求中通过附带这个 Cookie 值就可以告知服务端自己的身份。但是这个值很容易被客户端修改，甚至有时候可以伪造成其他用户进行操作。</p>

<p>后来就出现了 <code>HttpOnly</code> 等选项用于强化它的安全性，而且用于验证的 Cookie 值一般都会用摘要算法生成，修改了就直接相当于失效了。不过目前可能应该大概也许一般不会直接用 Cookie 进行敏感信息的储存和验证，而是使用 Session 机制。</p>

<p>Session 是存储在服务端的一种数据，用于记录和后续验证客户端的身份。那客户端如何向服务端证明自己的身份呢？一般我们使用 SessionID 的方式，这个 SessionID 和服务端存着的对应的 Session 值是相关联的，服务端会发给客户端，然后客户端把这个值存在 Cookie 中，在后续的请求中带上这个值，服务端进行验证即可知道当前请求对应着哪一个用户。</p>

<p>写了这些希望大家可以大致明白 Session 机制是什么，它本身可以认为和 Cookie 没有什么关系，只是为了保存这个 SessionID 我们一般都用 Cookie 来实现，其实 LocalStorage 和 Index DB 也可以的，只要客户端支持的保存方式能用来保存这个值就行了。</p>

<p>像我们去年这个时候在写一个弹幕的任务的时候，因为一些原因需要不通过 PHP 验证用户登录状态，当时就是在用户表里加了一个字段用来存 SessionID（PHP 里是 PHPSESSID），然后判断请求传输的那个 SessionID 和存在数据库里的值是不是一致来区分用户。</p>

<p>PHP 的 Session 用起来比较方便，只要一个 <code>session_start();</code> 然后把 <code>$_SESSION</code> 当作普通数组来使用、赋值、取值就可以了，但是这个实现还是希望大家理解，作为（伪）开发者，只会用肯定是不行的。感兴趣的话可以在一些页面打开控制台看看，设置了 Session 的页面都有一个 Cookie 项是存着 SessionID 的，不想写代码验证的可以直接打开 <a href="https://www.w3schools.com/php/showphp.asp?filename=demo_session1">W3Schools 的例子</a>，在控制台的 <code>Application-&gt;Cookies</code> 项里面。</p>

<p>所以回到权限控制的内容，如果用户分级比较简单的话可以直接用 Session 机制来实现，也就是根据 Session 存储的用户信息判断他的权限，然后决定是返回错误提示还是继续执行。比较需要注意的是，权限验证一般是要在所有操作之前进行的，比如连接数据库、加载配置信息等。另外一点就是，对于这种方式实现的权限控制，每次请求都要重新验证一遍权限。</p>

<h5 id="token">Token</h5>

<p>这里的 Token 中文翻译为“令牌”，Session 虽然基本可以解决验证的问题了，但是它毕竟是存在服务端的数据，一旦用户量大了就可能对服务器产生压力，毕竟默认 Session 数据都是放在内存里的。</p>

<p>Token 机制就避免了大量数据的保存的问题，它的一般实现是服务端将客户端信息签名后发回给客户端，客户端保存这个信息，并作为后续的身份认证数据。服务端在收到这个数据后，通过自己的密钥解密、验证这个信息，并据此判断客户端身份。这种机制服务端只需要存一对密钥即可，相对 Session 来说就没有太大的数据占用的问题。</p>

<h5 id="aclrbac">ACL 与 RBAC</h5>

<p>权限和用户的关系稍微多一点，我们可能就会考虑把这样的对应关系写进数据库，避免由代码来直接进行权限定义和控制带来的维护困难。其实现在已经有很多成熟的模型了，像 ACL(Access Control List) 这种就是类似的想法<del>，我就提一下它不往下讲了，因为我并没有用过</del>。</p>

<p>如果项目再复杂一些的话，直接定义用户和权限的对应关系也会变得很乱、很难维护。于是就有了 RBAC(Role Based Access Control) 这种模型，用户和角色关联，角色和权限关联，这样抽象出来之后我们的工作就清晰了很多，只要思考好项目需要哪些用户角色，他们分别应该具有什么权限，然后再给用户分配相应的角色即可。这个东西很多框架都有实现，现阶段你们可能还用不到，我也点到为止。</p>

<h3 id="">代码注入</h3>

<p>接下来讲一些代码注入方面的内容，这一块的安全很多时候要由前后端共同负责。就像之前参数验证一节提到的“前端传来的任何数据都是不可信”的一样，安全这方面的基本准则就是<strong>用户提交的任何数据都可能产生安全风险</strong>。风险相对比较大的比如 Shell 注入和一些语言的 <code>eval()</code>  函数（用于通过字符串执行代码和命令，函数的字符串参数会被当作代码解析执行）之类的，不过对我们而言并不常用，我觉得带过就行了。</p>

<p>以下几个是比较常见的代码注入风险以及相应的防护措施。</p>

<h5 id="sql">SQL 注入</h5>

<p>这个东西我们很早就已经提过，看大家的代码现在也都做了防注入的工作，但是我相信有部分同学是和我当时一样只会用而不知道为什么的。字符串拼接的危险不言而喻，最常用的例子大概就是密码验证的拼接了：</p>

<pre><code class="language-sql">SELECT * FROM users WHERE username = $username AND password = $password  
</code></pre>

<p>那我们输入这样的密码：<code>xxx OR 1=1</code>，拼接之后就是：</p>

<pre><code class="language-sql">SELECT * FROM users WHERE username = uesrname AND password = xxx OR 1=1  
</code></pre>

<p>因为 <code>OR</code> 后面的表达式永远为真，所以通过字符串拼接 SQL 语句就导致了攻击者可以绕过密码验证而登录任意账户了。但是基本不会有人真的这么写，至少会进行一下过滤，而且密码也不可能用明文存储和验证。这个例子只是用来做个比喻，让大家知道 SQL 注入大致是什么。</p>

<p>防注入的方法也有很多，比如过滤 SQL 关键词、过滤单引号、转义特殊字符等，但是相对来说通过预处理语句（Prepared Statements）来实现，无论是安全性还是对原始数据的影响都是比较符合业务需求的。</p>

<p>目前的大部分数据库系统都资瓷预处理语句的概念，这种参数化传递数据进行增删查改的功能本身也都是由数据库系统实现的，我们用的 mysqli、 PDO 等是把它封装起来便于具体的语言使用这些功能。而且它的实现也不仅仅是用于防注入，还有个批量执行的功能也非常有实用价值，特别是涉及格式固定的批量数据增删查改的时候，我们只要写好预留参数的 SQL Prepare 语句，然后每次设置好参数值执行一次就可以了，可以在一定程度上减轻数据库压力，毕竟没有了解析重复 SQL 语句的时间。</p>

<p>以 MySQL 为例，举个简单的例子：</p>

<pre><code class="language-sql">PREPARE stmt FROM "INSERT INTO table (a, b) VALUES (?, ?)";  
SET @x = 1;  
SET @y = 2;  
SET @z = 3;  
EXECUTE stmt USING @x, @y;  
EXECUTE stmt USING @y, @z;  
DEALLOCATE PREPARE stmt;  
</code></pre>

<p>执行后 <code>table</code> 表会多两行值，分别是 <code>(1, 2)</code> 和<code>(2, 3)</code>。这样应该就很容易理解关于参数化的安全性和对批量操作的便捷性了。语言对数据库预处理模块的封装，在底层都是用类似的方式执行的，当然这只是个最基本的例子，实际上还有诸如参数类型绑定等更多的功能，这里就不多提了，主要是希望大家能对防注入这块有稍微深入一点的了解。</p>

<h5 id="">跨站脚本</h5>

<p>和 SQL 注入相同，跨站脚本（XSS）也主要是因为拼接攻击者刻意制造的字符串导致的，这个我们也在之前提到过，拼接普通 HTML 标签导致 DOM 和页面结构混乱相对来说还算小事了，如果代码里面有诸如读取甚至传送 Cookie 等相对敏感的数据操作的话就会涉及到比较严重的问题。</p>

<p>这个的防护一般需要前后端同时做过滤，后端比如 PHP 可以使用 <code>htmlspecialchars()</code> 函数对每个字段编码一次，实际上是把一些 HTML 特殊字符换成对应的编码符号，在页面上看起来和原来是一样的，但是不会被当作实际代码执行，可以用其他语言的转义符之类的知识来对比理解。</p>

<p>前端的话可能要自己写个类似的转码函数了，主要就是尖括号这块要做好处理，涉及用户自定义的数据都要检查过滤一遍。和后端拿到前端的数据不能直接处理一样，前端拿到后端返回的数据后也不能直接就放进页面里，需要仔细考虑是以文本方式补充还是以 HTML 格式补充，以及有没有什么疏忽会导致意料之外的代码被执行。</p>

<h5 id="">跨站请求伪造</h5>

<p>跨站请求伪造（CSRF）指的是攻击者欺骗用户在不知情的情况下向服务端发送一个非本愿的请求，是不是读起来有点绕 2333。最简单的例子就是，比如某个站点用 URL 直接编码的方式提交点赞请求 <code>http://example.com?like=23</code>，攻击者发现这一点后，可以更改这个 URL <code>http://example.com?like=233</code> 然后用诱导点击等方式欺骗用户点击这个链接，那用户就在不知情的情况下给一个自己并不知道的内容点了个赞。这类安全问题在于，我们通过验证可以确认用户身份，但却不能知道这个请求是不是用户自愿发出的，或者准确点说，是不是用户在不知情的情况下发出的。</p>

<p>前面提到的 Token 的验证方式，可以用于这个问题的防护，比如把它的值添加到 URL 中，攻击者不可能猜出它的值来模拟请求链接。实际上我们一般也不会通过 GET 的方式传递比较重要的操作的数据，这类请求前后端都是使用 POST 进行交互的，尽管如此要模拟这个请求还是比较容易，毕竟模拟的站点在别人那里。</p>

<p>不知道大家现在能不能区分 XSS 和 CSRF，刚开始学可能一下子不太好理解。我借用 <a href="https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0">维基百科</a> 上的一句话再给点对比：XSS 利用的是用户对网站的信任，也就是默认网站不会有盗取用户 Cookie 等行为，而 CSRF 利用的是服务端对客户端的信任，也就是默认请求都是用户自愿发出的。</p>

<h3 id="api">后端 API</h3>

<p>这个阶段应该基本都是使用后端 API 的模式对前端的请求进行处理和返回的，我讲一下除了逻辑代码以外的一些可能需要注意的问题。</p>

<h5 id="">返回格式</h5>

<p>这个格式可以自己定，和前端商量好并且便于理解就行。推荐用类似微信 API 的格式：</p>

<pre><code class="language-json">{
    "errcode": 0,
    "data": "data",
    "errmsg": ""
}
</code></pre>

<p>其中 <code>errcode</code> 用来记录操作的返回码，默认成功为 0，失败为其它值。这样做其实是方便前端的判断，如果非零就直接把 <code>errmsg</code> 里的信息提示给用户。至于用 0 和其它值区分成功和失败，就是为了便于理解，大概就是成功只有成功一种情况，而失败的原因可能有很多<del>，怎么有种鸡汤的感觉</del>。</p>

<p>出错的情况下后端要在编码返回数据时注意在 <code>errmsg</code> 段给出详细的错误信息，然后设置 <code>errcode</code> 为非零值，至于具体是什么值可以自己给每一种情况分类编码，这个东西主要的用处就是调试的时候方便定位错误来源，特别是有些错误的报错信息 <code>errmsg</code> 一样的情况下。</p>

<p><code>data</code> 段的内容就是实际的数据了，可以是单一的值，也可以是 JSON 数组，具体当然是取决于接口定义。</p>

<h5 id="">接口文档</h5>

<p>接口文档当然是接口的提供方来写的，一般写文档都是比较痛苦的事，毕竟程序员坠讨厌的两件事就是：</p>

<ol>
<li>别人的代码没写文档  </li>
<li>要给自己的代码写文档</li>
</ol>

<p>但是写好文档可以提高团队合作的效率，还能避免很多后期的撕 X，想想如果接口很多的话用口头交接的方式有多恐怖就知道了。</p>

<p>理论上只要前端能看懂，文档写成什么样都行。不过我们还是要知道规范一点的文档怎么写，在时间允许的情况下也要尽量争取锻炼，这样以后写起文档来就“身经百战，见得多啦”（先声明我不是我没有。</p>

<p>接口文档现在有很多的开放平台、第三方接口都给出了非常好的例子，可以自己去找来学习。一般为了方便和美观建议用 Markdown 编写，主要的结构如下：</p>

<pre><code>POST /host/path/to/api

请求参数：（数据示例）
请求参数说明：（表格）

返回参数：（数据示例）
返回参数说明：（表格）
</code></pre>

<p>嗯大概就是这样……首先是请求方法和请求路径，然后是请求参数和返回参数的分别示例和说明，说明里的表格内容包括几个列：参数名、参数格式、参数描述、是否必须、默认值、示例、备注等，其中前三四个是一定要有的，后面的可以看自己心情决定取舍。</p>

<h3 id="">配置问题</h3>

<p>接下来写点团队合作的时候需要注意的一些问题，以及测试、部署的时候可能能用上的部分 Tips。</p>

<h5 id="">项目配置</h5>

<p>所有会因为环境不同而不一样的值都要放到单独的配置文件里面，这样即便于调试也便于部署上线。常见的配置内容当然是数据库的地址、端口、数据库名、用户名和密码这些了，其他比较经常用到的就是一些限时项目的开放和截止时间，比如报名表这些的。这个时间检查和参数验证一样，前后端都要考虑到。</p>

<h5 id="git">关于 Git</h5>

<p>配置文件一般都是放在 <code>.gitignore</code> 里的，这个大家都知道，但是在其他人拿到 Repo 想部署的时候，他怎么知道需要哪些配置信息呢？一般我们使用一个 <code>config.example.php</code> 文件来代替真实的 <code>config.php</code>，然后在那个 <code>example</code> 文件里给出所有的配置变量名，它们的值设置为示例值，比如 <code>DATABASE_NAME</code> 这种，这样就能在保证自己配置不泄露的情况下给其他人提供很大的方便。</p>

<p>关于这个配置和备份文件还有一个容易疏忽的点，就是服务端是通过一些像 Nginx、Apache 之类的 Web 服务器程序来对请求进行处理的，比如对 PHP 文件我们会使用代理转发的方式转发到用于处理 PHP 文件的端口。如果不使用和原文件相同的拓展名，而是直接在末尾加个 <code>.example</code> 或者 <code>.bak</code> 来标识示例或备份文件，而这些文件又因为一些原因被放到了线上，那么根据默认的请求处理规则，它们会被客户端直接下载下来或者至少能在网页上查看到内容。特别是需要紧急修改配置文件所以操作前备份的情况，很可能在线上环境进行而没意识到这个安全问题，那很可能配置信息就会泄露，这是极其危险的。所以一般最简单的做法就是把这种标识信息放在中间，保证它们的拓展名和原文件相同。</p>

<p>此外，还有一个我自己之前碰到的问题，就是一个添加到 Git 仓库的文件，希望一些更改不被继续 track，当时找了蛮久的资料，因为根本不知道怎么描述这个问题[捂脸]。最后发现了这个：</p>

<pre><code class="language-shell">$ git update-index --assume-unchanged filename
</code></pre>

<p>字面意思很容易理解吧，但是要直接找到它还真花了我一点时间 2333。</p>

<p>不过这个东西在团队合作的时候慎用，而且你要保证这个文件后续不会再被更改，不然 Merge 的时候会有问题的。</p>

<p>顺便也给出对应的取消和列表的命令：</p>

<pre><code class="language-shell">$ git update-index --no-assume-unchanged filename
$ git ls-files -v | grep '^h '
</code></pre>

<h5 id="">数据库配置</h5>

<p>默认的数据库内容编码、连接操作编码等我们都统一使用 UTF-8，测试的时候也要记得尝试写入中文看看会不会导致乱码。</p>

<p>部署上线的时候一般是用命令行的，复制粘贴 SQL 语句有时候容易出问题，特别是在对中文支持不是很完善的终端上，这个时候可以把要操作的 SQL 放在一个文件里，然后用数据库系统提供的文件导入命令执行，比如 MySQL 是：</p>

<pre><code class="language-sql">mysql&gt; SOURCE file.sql;  
</code></pre>

<p>注意这是进入 MySQL 后的终端，不是命令行的终端。</p>

<p>上线测试之后，在正式上线的时候需要清空数据库，并且把一些 AUTO_INCREMENT 的值设置为初始值，这个可以通过 <code>TRUNCATE</code> 命令实现：</p>

<pre><code class="language-sql">mysql&gt; TRUNCATE TABLE table_name;  
</code></pre>

<p>然后再讲一个给数据库分配用户的事情，虽然部署上线是我们负责的，但我觉得这个东西还是要知道比较好。</p>

<p>线上环境严格来说得给每一个项目数据库创建一个单独的数据库用户，然后对应设置单独的密码，这样在数据库操作逻辑代码有漏洞的时候可以保证不对其他数据库甚至整个数据库系统产生影响：</p>

<pre><code class="language-sql">mysql&gt; CREATE USER 'db_username'@'localhost' IDENTIFIED BY 'password';  
mysql&gt; GRANT ALL PRIVILEGES ON db_name.* TO 'db_username'@'localhost';  
</code></pre>

<h3 id="">代码规范</h3>

<p>关于代码规范的问题我们也已经强调了很多次了，其实这个“规范”并不是说要完全按照他人的编写代码标准去要求自己，命名、样式这方面只要能便于团队合作就足够了。我相信大家在项目越写越大的过程中也感受到了维护一份不规范的代码时消耗的时间和精力是非常大的，所以务必重视基本的代码规范要求，不要让不同的模块有太大的耦合度，也要保证自己的代码对其他模块产生的影响尽可能小。</p>

<p>额外提一下关于功能实现方面的规范问题，我们在写代码的时候不能停留在“能跑就可以”的阶段<del>，虽然有时候确实是这样</del>， 在时间允许的情况下应该多考虑能不能用更优雅的方式实现。</p>

<h3 id="">小结</h3>

<p>写这篇文章主要是希望大家能了解一下接下来的大致学习路线，以及强化后端开发的安全意识。可能不一定每个都用得上，但知道有这些东西以后，在碰到相关的问题的时候能比较快想到解决方案，希望这篇文章能对大家有一定的帮助。</p>

<p>最后安利几本相关的经典书籍，分别是上野宣的《图解 HTTP》、钟晨鸣/徐少培的《Web前端黑客技术揭秘》还有结城浩的《图解密码技术》。</p>

<p>时间紧迫，行文仓促，可能有写得不太严谨的地方，欢迎指正。</p>]]></content:encoded></item><item><title><![CDATA[整理和分享一些数据结构作业用到的 LaTeX 排版技巧]]></title><description><![CDATA[<p>文章目录</p>

<h3 id="background">Background</h3>

<p>上个学期修了数据结构课程，作业要求是手写拍照或者电子版都行，最后上传到教学在线，然后呢我看了看感觉好像可以顺便用来锻炼 LaTeX 的样子，于是就一直用它来完成书面作业了。过程中碰到蛮多有意思的东西，特别是到后期对一些数据结构的描述和图形绘制，现在一起整理分享一下。所有的代码和对应生成的 PDF 文件都放在了 <a href="https://github.com/KingsleyXie/Miscellaneous/tree/master/LaTeX/Data%20Structure">GitHub</a> 上。</p>

<h3 id="">格式约定</h3>

<p>除了最开始的 <a href="http://kingsleyxie.cn/some-latex-typography-skills-for-data-structure-homework/#封面">封面</a> 部分，文中的区块 LaTeX 代码都是截取的片段，对于这些代码片段，会在开头以注释代码（<code>%</code>）的方式给出关联到的 package，之后的内容位置默认在文档的 <code>\begin{document}</code> 和 <code>\end{document}</code> 之间。</p>

<p>对于排版环境、选项和包名等，直接使用行内代码格式，如 <code>titlepage</code>，而对于 LaTeX 命令，会使用其命令代码本身，也就是带 <code>\</code> 的单词，如 <code>\maketitle</code>。</p>

<p>额外提一点……博客上用的代码高亮插件不支持</p>]]></description><link>http://kingsleyxie.cn/some-latex-typography-skills-for-data-structure-homework/</link><guid isPermaLink="false">e99da262-ebea-4505-b639-af2637b19eda</guid><category><![CDATA[LaTeX]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Wed, 21 Feb 2018 09:32:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/02/latex-ds.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/02/latex-ds.png" alt="整理和分享一些数据结构作业用到的 LaTeX 排版技巧"><p>文章目录</p>

<h3 id="background">Background</h3>

<p>上个学期修了数据结构课程，作业要求是手写拍照或者电子版都行，最后上传到教学在线，然后呢我看了看感觉好像可以顺便用来锻炼 LaTeX 的样子，于是就一直用它来完成书面作业了。过程中碰到蛮多有意思的东西，特别是到后期对一些数据结构的描述和图形绘制，现在一起整理分享一下。所有的代码和对应生成的 PDF 文件都放在了 <a href="https://github.com/KingsleyXie/Miscellaneous/tree/master/LaTeX/Data%20Structure">GitHub</a> 上。</p>

<h3 id="">格式约定</h3>

<p>除了最开始的 <a href="http://kingsleyxie.cn/some-latex-typography-skills-for-data-structure-homework/#封面">封面</a> 部分，文中的区块 LaTeX 代码都是截取的片段，对于这些代码片段，会在开头以注释代码（<code>%</code>）的方式给出关联到的 package，之后的内容位置默认在文档的 <code>\begin{document}</code> 和 <code>\end{document}</code> 之间。</p>

<p>对于排版环境、选项和包名等，直接使用行内代码格式，如 <code>titlepage</code>，而对于 LaTeX 命令，会使用其命令代码本身，也就是带 <code>\</code> 的单词，如 <code>\maketitle</code>。</p>

<p>额外提一点……博客上用的代码高亮插件不支持 LaTeX，所以文章中代码区块的高亮逻辑可能看起来有点吔……它检测到什么关键词就会当做对应的语言然后处理，我也没办法╮(╯▽╰)╭</p>

<h3 id="">基础内容</h3>

<h5 id="">封面</h5>

<pre><code>\documentclass[titlepage]{article}

\title{
Homework 1 (Chapter 3) - Due 17th Sep, 2017\\  
\begin{large}
Data Structures and Algorithm Analysis in C++  
\end{large}
}
\author{Kingsley}
\date{\today}

\begin{document}
\maketitle
blabla...  
\end{document}
</code></pre>

<p><code>titlepage</code> 选项 和 <code>\maketitle</code> 命令一起，作用是把标题部分垂直居中作为单独的一页，当然内容比较少的话也可以不让它作为封面。实际交上去的版本，因为是作业，建议把 <code>\author{}</code> 的值换成名字的拼音然后加个空格写上学号。</p>

<h5 id="">目录</h5>

<p>如果文档的内容比较多的话，最好添加一个目录方便预览内容的结构：</p>

<pre><code>\clearpage
\tableofcontents
\clearpage
</code></pre>

<p>前后的两个 <code>\clearpage</code> 指令作用是把目录设置为单独的页面，可以根据自己的需求取舍。</p>

<p>这个命令生成的是自动顺序编号的目录，那有时候可能我们并不想让一个链接被自动编号，就可以用：</p>

<pre><code>\subsection*{Not Numbered Subsection}
\addcontentsline{toc}{subsection}{Not Numbered Subsection}
</code></pre>

<p>在分块的命令后加个星号 <code>*</code> 就可以让它不被自动编号，并且不会对后续区块的编号有影响。第二行的 <code>\addcontentsline</code> 作用是把当前位置加入索引中，它的使用格式如下：</p>

<pre><code>\addcontentsline{file}{sec_unit}{entry}
</code></pre>

<p>第一个参数可以取值 <code>toc</code>、<code>lof</code> 和 <code>lot</code> ，分别代表添加到目录（Table Of Contents）、插图列表（List Of Figures）和表格列表（List Of Tables）。第二个参数就是层级了，比如区块单元的名称或者 <code>figure/table</code>（用于插图/表格列表）。然后第三个是当前项的描述，也就是显示在目录列表中的名称。</p>

<h5 id="">缩进</h5>

<p>LaTeX 默认对 <code>\section{}</code> 和 <code>\subsection{}</code> 下的首段不加缩进，而对于非首段的段落默认都是有缩进的。那如果因为一些原因需要让首段有缩进的话，可以使用 <code>indentfirst</code> 包，只要在文件头部引入这个包就可以了，不需要其他的设置命令：</p>

<pre><code>\usepackage{indentfirst}
</code></pre>

<p>此外，也可以在段落开头直接使用 <code>\indent</code> 或 <code>\noindent</code> 命令来覆盖本段默认的缩进设置。</p>

<h5 id="">字母编号列表</h5>

<p>LaTeX 默认的编号列表是数字的，要使用字母编号就需要通过 <code>enumitem</code> 包来指定 <code>label</code> 的格式：</p>

<pre><code>% \usepackage{enumitem}

\begin{enumerate}[label=(\alph*)]
\item $\Theta(n)$
\end{enumerate}
</code></pre>

<p>这样的编号列表就是带括号的小写字母 <code>(a)</code>、<code>(b)</code>、<code>(c)</code> 等，相对应地，<code>\Alph*</code> 和 <code>\Roman*</code>(<code>\roman*</code>) 分别代表大写字母编号和大（小）写罗马数字编号。</p>

<h5 id="">图片</h5>

<p>直接用 <code>\includegraphics{figure-path}</code> 来插入需要的图片就可以了，大部分时候可能对于图片位置以及占用尺寸等有要求，那么可以自行添加设置，比如我是用 <code>center</code> 环境直接居中图片，然后添加 <code>width</code> 命令把图片宽度设置为文档宽度：</p>

<pre><code>\begin{center}
\includegraphics[width=1\textwidth]{figures/all-probing.jpg}
\end{center}
</code></pre>

<h3 id="">代码</h3>

<p>对于（伪）程序员来说，代码相关的东西总是会被特别关注的，排版文档的时候对代码的显示效果自然也会额外看重，下面按照分类谈谈在 LaTeX 中如何排版代码。</p>

<h5 id="">行内代码</h5>

<p>行内代码有比较多的方法可以做到，这里简单列举一下：</p>

<ul>
<li><p><code>\verb|inline code|</code> <br>
这个命令的作用是直接把两条竖线中间的内容作为行内代码显示出来，如果代码里面包含了竖线的话，为了避免不必要的麻烦可以把起止符号换成感叹号 <code>!</code>。</p></li>
<li><p><code>\texttt{inline code}</code> <br>
这个命令实际上是将内部的文字设置为打印机字体，效果上也可以作为行内代码。此外，它还可以嵌套其他 LaTeX 命令，比如对内部某个词加下划线等：</p></li>
</ul>

<pre><code>\texttt{\underline{while} (true) i++;}
</code></pre>

<ul>
<li><code>\lstinline{inline code}</code> <br>
这个是 <code>listings</code> 包里的 <code>\lstinline</code> 命令，本身就是用于排版代码的。它还可以和别的包（比如 <code>xcolor</code>）配合，高亮部分保留字：</li>
</ul>

<pre><code>% \usepackage{listings}
% \usepackage{xcolor}

\lstset{language=C,keywordstyle={\bfseries \color{orange}}}
\lstinline{while (true) i++;}
</code></pre>

<h5 id="">区块代码</h5>

<p>区块代码一般使用 <code>lstlisting</code> 环境，它也来自 <code>listings</code> 包。不过这个包排版出来的代码个人感觉看起来并不是很舒服，可以用 <code>\lstset</code> 改一下：</p>

<pre><code>% \usepackage[T1]{fontenc}

\lstset{
    basicstyle=
        \def\fvm@Scale{0.8}
        \fontfamily{fvm}\selectfont,
    tabsize=4
}
</code></pre>

<p>这样就选择了一个等宽而且看起来蛮舒服的字体，然后代码就直接放在这个环境内部就可以了：</p>

<pre><code>% \usepackage{listings}

\begin{lstlisting}
#include &lt;iostream&gt;

int main()  
{
    std::cout &lt;&lt; "Hello, World!\n";
    return 0;
}
\end{lstlisting}
</code></pre>

<p>如果对 Tab 的缩进长度有要求的话可以自己改 <code>tabsize</code> 的值。</p>

<p>顺便介绍一下 <code>verbatim</code> 环境，这个词的翻译是“逐字地”，在 LaTeX 里面是用于排版 TeX 代码的，也就是用于显示这个排版系统本身的一些命令。虽然用起来好像和 <code>lstlisting</code> 差不多，不过还是不要混淆比较好，而且个人感觉它的字体、样式设置之类的都比较麻烦。</p>

<h5 id="">文件代码</h5>

<p>有时候可能想直接从文件中把代码排版进文档，这个时候可以用 <code>\lstinputlisting</code> 命令，不用说应该也知道它同样是来自之前提到的那个包了，使用方法也很简单，直接在内容部分输入文件路径即可：</p>

<pre><code>% \usepackage{listings}

\lstinputlisting{sample.cpp}
</code></pre>

<p>如果只需要文件中的一部分代码，可以通过设置指定起止行数：</p>

<pre><code>\lstinputlisting[firstline=3, lastline=7]{sample.cpp}
</code></pre>

<h5 id="">样式设置</h5>

<p>实际上 <code>\lstset</code> 能做的当然远不止 <a href="http://kingsleyxie.cn/some-latex-typography-skills-for-data-structure-homework/#区块代码">区块代码</a> 部分提到的那些，但是如果不是整个文档都要用到的样式，原则上并不建议直接在这里面进行配置，我们可以用 <code>\lstdefinestyle</code> 来创建一个自己的样式配置，然后在需要使用到的时候只要在内容之前加一个中括号引入它即可：</p>

<pre><code>% \usepackage{listings}

\lstdefinestyle{sample-style}{
    xleftmargin=2\parindent
}

\lstinline[style=sample-style]{while (true) i++;}
\lstinputlisting[style=sample-style]{sample.cpp}
\begin{lstlisting}[style=sample-style]
// code block
\end{lstlisting}
</code></pre>

<p>更多有关 <code>listings</code> 包设置的内容可以参考 <a href="https://en.wikibooks.org/wiki/LaTeX/Source_Code_Listings#Settings">Wikibook</a></p>

<h5 id="">效果图</h5>

<p>设置好样式的行内代码的效果大致如下：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/02/inline-code.png" alt="整理和分享一些数据结构作业用到的 LaTeX 排版技巧"></p>

<p>区块代码（包括文件代码）则是这样的：
<img src="http://kingsleyxie.cn/content/images/2018/02/code-block.png" alt="整理和分享一些数据结构作业用到的 LaTeX 排版技巧"></p>

<p>具体的 LaTeX 源码和生成的 PDF 文件可以到 Repo 里看。</p>

<h3 id="">树</h3>

<h5 id="">二叉树</h5>

<p>直接用 <code>tikz</code> 包然后设置一下每个节点的样式以及层级之间的距离就可以了：</p>

<pre><code>% \usepackage{tikz}

\begin{tikzpicture}[
    every node/.style={
        circle, draw,
        inner sep=0pt,
        text width=6mm,
        align=center
    },
    level distance=10mm,
    level 1/.style={sibling distance=30mm},
    level 2/.style={sibling distance=15mm}
]
\node{$15$}
child { node{$5$}  
    child[missing]
    child { node{$7$} }
}
child { node{$20$}  
    child { node{$18$}
        child { node{$16$} }
        child[missing]
    }
    child { node{$25$} }
};
\end{tikzpicture}
</code></pre>

<p>看一下代码大概就不用多解释了……还是很直观的，<code>every node</code> 设置每个节点的样式，<code>level distance</code> 设置层级之间的默认距离，然后后面额外针对一些层使用自定义的的样式覆盖默认值。内容部分就是很显然的根据树的形状和当前节点位置选择包含或并列关系，注意空的子树要用 <code>[missing]</code> 占位。</p>

<h5 id="">普通树</h5>

<h6 id="tikz">使用 tikz 包</h6>

<p>我们可以使用刚刚提到的那个方式排版普通树：去掉占位的子树，然后根据实际子树的内容重新输入就可以了，比如：</p>

<pre><code>% \usepackage{tikz}

\node{X}
child { node{P}  
    child { node{C} }
    child { node{Q} }
    child { node{R}
        child { node{V} }
        child { node{M} }
    }
};
</code></pre>

<h6 id="forest">使用 forest 包</h6>

<p>实际上普通树有更简单的排版方式，用 <code>forest</code> 包就可以做到：</p>

<pre><code>% \usepackage{forest}

\begin{forest}
[, phantom, s sep = 1cm
    [1
        [2[3]]
        [4[5]]
        [6]
    ]
]
\end{forest}
</code></pre>

<p>无论是编写还是阅读都<del>好像</del>清晰很多。</p>

<h5 id="">森林</h5>

<p>很显然 <code>forest</code> 包本身是用来排版 forest 的，效果上就是同一行的多颗普通树组成的森林：</p>

<pre><code>% \usepackage{forest}

\begin{forest}
[, phantom, s sep = 1cm
[A
    [B [C] [D[E]] [F]]
    [G]]
[H
    [I]
    [J [K [L]]
    [M [N] [O]]]]
[P
    [Q] [R [S] [T]] [U] [V [W [X]] [Y]] [Z]]
]
\end{forest}
</code></pre>

<p>而同样地我们可以进行样式设置，可以直接在 <code>\begin{forest}</code> 后写，也可以在 <code>\forestset</code> 里定义一个样式然后引用，比如这样：</p>

<pre><code>% \usepackage{forest}

\forestset{
    forest-style/.style={
        for tree={
            circle, draw,
            every node/.style={
                circle, draw,
                inner sep=0pt,
                text width=6mm,
                align=center
            }
        }
    }
}

\begin{forest} forest-style
[, phantom, s sep = 1cm
// Forest Content
\end{forest}
</code></pre>

<p>效果大致如下：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/02/forest.png" alt="整理和分享一些数据结构作业用到的 LaTeX 排版技巧"></p>

<h3 id="">链表</h3>

<p>这个的思路其实是先用 <code>matrix</code> 把内容排版成一个表，其中的内容都定义好样式（比如方框之类），然后用 <code>\draw</code> 命令绘制箭头，代码贴上来好像也没太大意义所以就只给个文件链接吧：<a href="https://github.com/KingsleyXie/NaiveProjects/blob/master/LaTeX/Data%20Structure/linked-list.tex">linked-list.tex</a></p>

<p>好像也没有特别要注意的地方……不过调整间距确实有点麻烦，建议把字体设置为正常大小然后按照最长的位数调整单元格宽度和高度。有一点就是箭头是后续绘制上去的，要注意这个长度会不会覆盖了格子或者距离边界太远。</p>

<h3 id="b">B+ 树</h3>

<p>这个好像应该放在树那一节的hhh，不过无所谓了。</p>

<p>还是用 <code>tikz</code> 包，先通过 <code>\tikzstyle</code> 定义好节点的样式，然后应用到每一个 <code>node</code>，再根据需求调整一下行间距，最后用类似 <a href="http://kingsleyxie.cn/some-latex-typography-skills-for-data-structure-homework/#二叉树">二叉树</a> 一节提到的语法编写排版就可以了：</p>

<pre><code>% \usepackage{tikz}

\usetikzlibrary{shapes}

\begin{tikzpicture}
\tikzstyle{bplus-node}=
[
    draw, rectangle split,
    rectangle split horizontal,
    rectangle split ignore empty parts
]
\tikzstyle{every node}=[bplus-node]

\tikzstyle{level 1}=[sibling distance=35mm]
\tikzstyle{level 2}=[sibling distance=15mm]

\node {I \nodepart{two} M \nodepart{three} S} [-]
    child {node {D \nodepart{two} G}
        child {node {A \nodepart{two} B \nodepart{three} C}}
        child {node {D \nodepart{two} E}}
        child {node {G \nodepart{two} H}}
    }
    child {node {K}
        child {node {I \nodepart{two} J}}
        child {node {K \nodepart{two} L}}
    }
    child {node {P}
        child {node {M \nodepart{two} N \nodepart{three} O}}
        child {node {P \nodepart{two} R}}
    }
    child {node {U}
        child {node {S \nodepart{two} T}}
        child {node {U \nodepart{two} W}}
    }
;
\end{tikzpicture}
</code></pre>

<p>对应的排版输出结果是这样的：</p>

<p><img src="http://kingsleyxie.cn/content/images/2018/02/bplus-tree.png" alt="整理和分享一些数据结构作业用到的 LaTeX 排版技巧"></p>

<p>严格来说最后一层应该用箭头从左至右连接起来的，不过有点麻烦当时就没做<del>，现在也不想做了</del>。</p>

<h3 id="">微小的总结</h3>

<p>其实有一些内容是在需要用到的时候去搜索然后在 <a href="https://tex.stackexchange.com">Tex Stack Exchange</a> 里找到的，不过因为自己也改了蛮多而且再去找链接地址也挺麻烦的，就没有给出这些内容的参考来源。</p>

<p>我在整理这些排版方式的时候也踩了好多坑= = 看看这篇文章本来是二月份开始写的[捂脸]，前段时间开始除草然后还发现了 <code>standalone</code> 这个文档类型，可以很方便地用来写预览文件。</p>

<p>整理过程中也学到一些新的东西和规范，其实每次作业里的一些配置基本都是用全局命令直接设置的，所以在整理到一个文件里的时候简直是爆炸……各种排版样式的冲突，生成的 PDF 文件非常迷23333。所以在非必需的情况下还是要尽量少用全局设置，样式之类的东西可以在对应的环境里直接指定，也可以放在一个配置里然后在用到的时候引用，大概就是……松耦合？（逃</p>

<h3 id="">延伸参考</h3>

<p>最后列举几个经常用到的文档之类的网站吧：</p>

<ol>
<li><p><a href="https://ctan.org">CTAN</a> <br>
这里收录了大部分的 TeX 包以及他们的 README、官方文档等内容，非常详尽。在我感叹 <code>tikz</code> 这个包怎么功能这么强大的时候，我找到了这个包的 <a href="http://mirror.lzu.edu.cn/CTAN/graphics/pgf/base/doc/pgfmanual.pdf">文档</a>，然后就……emmm</p></li>
<li><p><a href="https://www.sharelatex.com/learn">Share LaTeX</a> <br>
这个网站本身是用于在线合作编写 LaTeX 文档的，他们提供的这个 <code>learn</code> 部分感觉对于学习 LaTeX 很有帮助。我记得我高中的时候也有了解过这个网站，因为当时它给的 Demo 里有一张青蛙的图片，让我印象很深刻（</p></li>
<li><p><a href="https://en.wikibooks.org/wiki/LaTeX">WikiBook</a> <br>
搜索结果里经常也看到来自这里的内容，不过我没太仔细了解，应该和上面的那个差不多。相对官方文档而言，这种整合并且带索引的结果可能有时候更符合我们的需求。</p></li>
</ol>]]></content:encoded></item><item><title><![CDATA[退役回忆录：关于OI、ACM，以及这一小段征程]]></title><description><![CDATA[<p>文章目录</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>其实是一些常见简写的含义啦</p>

<ul>
<li>OI：信息学奥林匹克</li>
<li>NOIP：全国青少年信息学奥林匹克联赛（省级）</li>
<li>省选：全国青少年信息学奥林匹克省队选拔赛</li>
<li>NOI：全国青少年信息学奥林匹克</li>
<li>ACM-ICPC：ACM 国际大学生程序设计竞赛，文中简称 ACM</li>
</ul>

<p>更多 OI 相关内容的解释请参见官方提供的 <a href="http://www.noi.cn/newsview.html?id=66&amp;hash=9CB0C5&amp;type=5">系列活动简介</a></p>

<h2 id="">前言</h2>

<p>其实回忆录这东西是早就该写的，只是因为懒加上拖延症一直放着，大一在 ACM 集训队划了将近一年的水，最后还是选择了退役，我想大概是时候对竞赛有个正式的告别吧</p>

<p>正如题所言，这篇文章是一篇退役回忆录，但也不是完全的关于 OI、ACM 的回忆，会夹杂着这个人生阶段的一些值得回忆的事情，甚至有可能部分地方会写成流水账。由于我的竞赛水平实在太菜了<del>，哪怕写满似乎也凑不了多少字</del>，所以实际上这里所写的是关于到目前（大二上学期）为止的那些愿意分享的故事，真正有关竞赛的篇幅并不长，以及，文采不好还望见谅</p>

<h2 id="">故事</h2>

<h3 id="">小学</h3>

<p>第一次和 OI 的接触是在小学六年级，</p>]]></description><link>http://kingsleyxie.cn/memoirs-of-contests-and-life-so-far/</link><guid isPermaLink="false">abf1573d-fd6d-4597-959f-53c005691b36</guid><category><![CDATA[About]]></category><category><![CDATA[Life]]></category><category><![CDATA[Thinking]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Mon, 12 Feb 2018 13:06:30 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/02/memoirs-cover.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/02/memoirs-cover.jpg" alt="退役回忆录：关于OI、ACM，以及这一小段征程"><p>文章目录</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>其实是一些常见简写的含义啦</p>

<ul>
<li>OI：信息学奥林匹克</li>
<li>NOIP：全国青少年信息学奥林匹克联赛（省级）</li>
<li>省选：全国青少年信息学奥林匹克省队选拔赛</li>
<li>NOI：全国青少年信息学奥林匹克</li>
<li>ACM-ICPC：ACM 国际大学生程序设计竞赛，文中简称 ACM</li>
</ul>

<p>更多 OI 相关内容的解释请参见官方提供的 <a href="http://www.noi.cn/newsview.html?id=66&amp;hash=9CB0C5&amp;type=5">系列活动简介</a></p>

<h2 id="">前言</h2>

<p>其实回忆录这东西是早就该写的，只是因为懒加上拖延症一直放着，大一在 ACM 集训队划了将近一年的水，最后还是选择了退役，我想大概是时候对竞赛有个正式的告别吧</p>

<p>正如题所言，这篇文章是一篇退役回忆录，但也不是完全的关于 OI、ACM 的回忆，会夹杂着这个人生阶段的一些值得回忆的事情，甚至有可能部分地方会写成流水账。由于我的竞赛水平实在太菜了<del>，哪怕写满似乎也凑不了多少字</del>，所以实际上这里所写的是关于到目前（大二上学期）为止的那些愿意分享的故事，真正有关竞赛的篇幅并不长，以及，文采不好还望见谅</p>

<h2 id="">故事</h2>

<h3 id="">小学</h3>

<p>第一次和 OI 的接触是在小学六年级，不过很多细节都记不清楚了，所以这一段里关于时间方面的描述可能有些许误差</p>

<p>五年级临近期末的某天，班上有俩同学被叫去学校的语音室，然后应该是听老师简单介绍了下这个竞赛，拿了份计算机基础知识的复印件回到教室，我拿过来看了看感觉蛮有趣的，然而因为和我并没有什么关系很快就还给他们了</p>

<p>六年级开学后不久我也被叫过去，之后就加入他们开始了额外的对于计算机方面知识的学习，后来了解到是因为我五年级期末考的数学拿了满分于是几位老师觉得“哎呦不错哦”就把我也加进去了（逃</p>

<p>在这之前我还没有接触过计算机，但是一拿到那份复印件就感觉这些知识特别有意思，对这些知识的学习兴趣也特别高，那时的体育美术音乐课什么的都是属于薛定谔的，这些课的老师也是由语数英老师兼任，所以在学业不紧张的时候基本都用来划水或者讲习题，于是我们和老师申请了一下……Permission Denied 23333 还是得老老实实在教室学习</p>

<p>小学的机房，也就是那个语音室，只有三台电脑，其中两台白色的七喜电脑是属于有年代的一类机子了，显示器也是体积超大的那种，还有一台在讲台上的电脑是液晶显示屏的，开始上机写代码的时候老师要求两人共用一台，于是我们一组就抢到了……其中一台七喜电脑，我还记得当时有个同学在辅导老师讲课的时候削铅笔发出的声音很有趣让我一听到就不停傻笑，还因此被老师问了好几次在笑什么</p>

<p>那时候学的语言还是 QBASIC，也就是微软的那个 <a href="https://en.wikipedia.org/wiki/QuickBASIC">QuickBASIC</a>，依稀记得 <code>LET</code> 语句 <code>FOR</code>语句 <code>IF...THEN...</code> 语句 什么的，编程界面是一个蓝屏的东西，大概类似 Free Pascal 的那个 FPC 吧，不过在高中再接触竞赛的时候想回忆童年却发现那个解释器都已经很难下载到了，sad</p>

<p>那么然后过了一段时间我们就一起去参加初赛了，我还记得有道选择题是 Windows 下切换输入法的快捷键，然而我并不知道，不过还好还是选对了[捂脸]，成绩出来的时候是班主任在班上说的，就我一个人通过了初赛（逃</p>

<p>那天上午放学的时候，那几个曾经一起学♂习的朋友也知道了结果，对我表示了祝贺以及鼓励，然后机房从此就我一个人在中午和放学后在那看书、敲代码，老师们也给了一些额外的练习时间给我，比如说薛定谔的体育、美术之类的课我可以去机房练习，但是其实每次去都是有点担心的，毕竟他们一有兴致就会拿来上正式的课甚至考试</p>

<p>不过那段时间也是我第一次体验到那种充实和快乐的感觉，和高二的时候在机房练习的心情几乎相同</p>

<p>再不久之后就是复赛了，由于老师记错了时间，导致我在原本以为的出发日期前一天早上被老师来家里叫醒，老师顺便提醒了一下九江天气比较冷让我多带件毛衣过去，然而我第一次住酒店<del>太激动</del>，把那件毛衣忘在酒店了嘿嘿嘿。吃完早饭和竞赛的辅导老师一起去九江，然后很喜欢坐车的我享受了七个多小时的大巴时光，心满意足.jpg</p>

<p>到酒店的时候已经是下午了，第二天早上是我们初级组的考试，我只记得一道要求写一个类似日历的程序的题目，因为大部分时间都花在这上面了，大概是最简单的题吧，剩下两题做不来就只 xjb 写了下，题目内容是什么已经完全没印象，回到学校以后就再没去过机房了，等老师通知我成绩，虽然知道多半是 gg 了</p>

<p>后来我被告知拿到了省三等奖，感觉虽然不高但也算说得过去吧，也没怎么在意它，就当是学了点额外的知识了，直到高中混到个省一的时候我才知道在信息学奥赛方面江西是一个弱省，三等奖基本上是破零分就有了，何况我那时候还是初级组，然而莫名其妙地亲戚朋友就都知道了这件事，甚至还有说我是全省第三名的？？？要知道我一直都是个比较低调的小伙子啊<del>，这怎么承受得起呢</del></p>

<p>说起来去年寒假的时候回小学去看过，发现每个教室黑板都配了台一体机……还有个教室门没锁于是我就进去仔细看了下，在想网络来源，没发现有网线接入，于是用手机搜索 WiFi 信号，发现了十几个连续编号的 WiFi 名，大概是每台机子都配有一个独立的 WiFi？这配置已经比我高中的教室黑板配置还好了……过了一会碰到当年的数学老师（毕竟他就住在那），他说是政府拨款滋瓷的，没想到政府对小学教育这么重视了（逃 不管怎么说至少从设备条件来看我觉得还是值得称赞一波的</p>

<p>要说小学阶段还有印象深刻的一件事就是当年有个辣鸡电视台，可以通过电话来控制显示在电视上的一个游戏，然后那个月电话费用 200+，真是神坑啊[捂脸]，更有趣的是前段时间在 JJ 的《一千年以后》评论区发现了一个很亲切的评论：</p>

<blockquote>
  <p>当年为了听这首歌用电话打点歌台花了100多块，差点儿就死在9岁那年</p>
</blockquote>

<h3 id="">初中</h3>

<p>小学的竞赛经历结束后，由于初中学校并没有开这方面的竞赛课程，就直到高中才再次接触了，不过倒是很意外地在学校给七年级开的数学英语竞赛课上，遇到了当时和我一起参加复赛的我们镇的另一位通过初赛的同学，剩下的事情和竞赛相关的也基本没有，所以就不多提了</p>

<p>如果说还有一点什么的话，就是初二的时候碰到了对我的英语这门学科影响很大的老师。我在五年级的时候学校开始安排英语课程，用的三年级的课本，同样是因为兴趣很大成绩一直都不错，然后也一直保持着对英语的兴趣，不过到初二其实似乎开始有些不爱学习了，很幸运能做她的学生，那时候对于学习可能多少还是有些看自己是不是喜欢对应学科的任课老师的 2333</p>

<p>初二整个一年在她的影响下我对英语的热爱又上升了一个台阶，后来当了班里的英语课代表，也是从这个时候开始我对于“责任”这个词有了更深的理解，问心无愧地说那两年的课代表工作我还是非常负责的，也尽全力去帮助一些英语学得不怎么好的同学，并且把对于责任的理解一直放在心上，实践在之后的班委工作以及高中的学习和生活上</p>

<p>到初三的时候，我发现我对英语的理解似乎有了很大的提升，也渐渐有了更加系统的概念、愿意去接受更大的挑战，当然这是当年年少轻狂的想法 hhh，那时候的我还特别爱帮老师改试卷，一部分原因也是我从小就喜欢模仿老师的字迹，包括优良评价、对错符号以及试卷分数之类的，小学毕业的时候写到试卷上的数字字迹已经基本可以以假乱真了，然后就越发享受这种改卷的过程，特别是看到同学们分不出哪张试卷是老师改的哪张试卷是我改的的时候（逃</p>

<p>除此之外，我大概也一直属于那种所谓“不务正业”的人，每次拿到新书都喜欢看看它的序言、仔细读读首尾几页很少有人会看的内容，其实有时候反倒能有一点收获，比如作者可能写这本书的时候有一些自己的想法和思考，他的编排顺序、行文逻辑或者对读者的期待和建议很多时候会写在序言里，有时你也能从一篇序言中了解这本书从零到一经历了哪些过程，有多少除了编辑内容以外的困难和付出，甚至是作者有没有认真对待他的这本书。不过呢，我当然从来也没有带着这么大的期待去阅读这些东西啦，还是好奇心驱使比较多一点</p>

<p>小时候的我对于很多电器之类的也经常会有类似的习惯：拿它的说明书仔细看介绍，然后尝试每一个功能，或者更改每一个设置看看效果，特别是家里人的手机<del>，不知道是不是因为实在无聊没事做</del>，而这个对待电子产品的习惯好像还一直保持到了现在，不过当然肯定是不会再拿别人的东西试了</p>

<p>要说更“不务正业”的可能就是我经常和英语老师借磁带回去听，尤其喜欢听磁带的开头结尾的提示音 2333，不管是小学还是初中的磁带，这些无关紧要的内容我基本都铭记在心了，比如我还依稀记得小学英语当年的磁带开头：</p>

<blockquote>
  <p>义务教育课程标准实验教科书 英语 三年级 上册 人民教育电子音像出版社出版</p>
</blockquote>

<p><del>那个背景音乐也是特别魔性的</del></p>

<p>以及初三有一次考试听到磁带的结束音，提醒老师给磁带翻面播放她才反应过来（再逃</p>

<p>现在想想，觉得似乎这种好奇心对我是有不小的影响的，比如我自己在写一个什么东西的时候就很容易在一开始考虑很多的细节……那些“无关紧要”的内容，哪怕是出现概率很低的情景也会尽量去模拟一下，可能这提高了我对于细节的重视程度</p>

<p>对于中考也没什么特别的印象，初中三年说精彩谈不上但说枯燥也不至于，有蛮多兴趣驱使的东西，也结交了感情深厚的朋友，然后作为众多平凡学生中的一员进入了高中</p>

<h3 id="">高中</h3>

<p>接下来的三年时光可以说是我这一段人生中一个很大的转折点，经历的事情对于什么都不懂的当时的我来说算是蛮多的，我想我应该很庆幸没有把这段青春的路走偏，而这除了因为自己能找到兴趣所在以外，还有很多要感谢的人和事</p>

<h5 id="oi">OI</h5>

<p>高一开学后不久，在五大奥赛的报名选拔开始前，信息学奥赛那边的老师到各个班上招人报名 NOIP 的初赛，听到这个奥赛名字我当然是选择直接报名了，虽然也很清楚自己三年时间没碰，那本就几乎为零的基础也已经不存在了。试卷是 Pascal 类的，我就记得我能算点二进制，然后简单逻辑的代码也能猜出个大概<del>，其实就是只能做出选择题的前几道</del>。当时高一学生是单独在一个机房里的<del>，大概是因为基本没戏</del>，老师走到我旁边看了看问我是不是有基础，因为都做得不错，然而她不知道我后面的题基本没一个会的了 2333</p>

<p>结果出来以后我当然是被刷了，然后就在五大奥赛选拔的时候进入了这个不被学校看重的奥赛的培训班，因为高一学业不重就有比较多的晚自习可以给奥赛生们去各个培训教室学♂习。当时的我连压缩包都不知道怎么打开 = =，所以下载安装环境都搞了蛮久的，那半年多学的是 Pascal，中途很多人都离开了，到第二年初赛前剩下的人已经只有十个左右</p>

<p>这一个阶段在奥赛这块没有什么起伏的故事，倒是因为一些别的各种事情影响（当然主要问题也是在自己身上），成绩一落千丈，然后从此习惯了排名不靠前的日子</p>

<p>经过半年多的学习，终于从掌握 Pascal 基本语法开始，到能写一些稍微复杂一点的东西，再到学基础数据结构、基础算法，高二开始的时候已经……好像还是什么都不会？我记得我暑假花了蛮长时间学深度优先搜索和广度优先搜索，通过手算来模拟，然后结合对数据结构里树和队列的理解才把这一块掌握，不过倒是一直对这块记得特别深刻</p>

<p>高二开学不久当然是准备初赛和复赛了，初赛这个时候已经是基本没什么问题了，毕竟我们不像浙江那种竞赛强省，在有些学校里不考到满分都不一定过得了初赛[捂脸]，然后一直到复赛的时候我好像还是连动态规划都没学，进考场前还在看 Dijkstra 最短路径算法……然而这些算法和数据结构都没有在那时候派上用场，那年的题目两天都是一道水题一道能暴力过 30% 数据的题和一道我做不来的题，于是我靠着纯模拟的代码拿下了这 260 分</p>

<p>其实本来我考完估分是 270 左右的，因为我非常智障地把写不来的题做了个随机生成结果的操作，然后更智障地以为能骗到个十分二十分<del>，其实在民间数据是拿了 270 分的</del>，但是不管怎样我觉得这套题目至少得有 260 分，然后成绩出来我看了看往年的分数觉得实在有点悬，直到分数线公布发现我刚好压线拿到了省一等奖</p>

<p>嗯没错，江西的省一等奖线就是这么低，我看了看公布的排名表和分数线发现浙江省的一等奖线是 500，他们有 100+ 的省一等奖获得者……而我们省过 500 的只有一个人。当然我这分数没有资格去评价别人什么，只是当时实在有些感叹教育资源的差距，在这样一个信息学奥赛弱省能拿到高分也真的不容易，我很佩服他们</p>

<p>更让我感慨的是，我们学校去参加竞赛的竟然没有分数比我高的……这真的非常尴尬，看了看原因基本都是一些边界条件没考虑到导致没能拿下那些基础的分数，到后来自己再去写一些别的东西的时候也发现这种边界情况要考虑周全好像是不太容易，我想我能拿到全部的分可能主要是因为一直都比较注意细节吧</p>

<p>我是用的 Pascal 获得的省一等奖，后来奥赛老师安排新同学们学 C++ 了，我也就跟着把编程语言换成了 C++，因为基本的逻辑还是相通的所以很快就完成了语言的切换，给我的最大感觉还是编程这东西最重要的是要多练习，接下来虽然明知希望不大还是在准备省选，但是我也没有停课，这就导致学习和竞赛两边都没搞好</p>

<p>老师在暑假的时候给我的校园卡加了开机房门的权限，从此以后我就可以经常自己过去<del>玩</del>学习了，省选到来的时候我除了多学了一点点算法以及打代码手速略有提升之外好像也没什么别的长进。结果肯定是意料之中的过个场而已啦，三道题里有一道题是拓扑排序但是我在考前一晚翻书才发现这个内容，而且在考场上也并没能写出来<del>，感觉对这个算法已经有感情了</del>，当然归根结底还是自己太菜</p>

<p>NOIP 2014 拿到奖之后，我的奥赛之路就已经告一段落，省选考完其实就是正式退役 OI 了，我也不知道有过多少次告别，偶尔有退役的心情就会在 QQ 空间之类的发点感慨。到暑假 NOI 进行的时候，因为之前竞赛有加一些通过各种方式认识的好友，看着满屏的证书、奖牌、协议之类的，还有前一年保送如今拿到录取通知书的，心理还真有点羡慕呢 2333</p>

<p>我对于“信息不对称”这个词的理解，出现在退役之后，当我发现 OI 这个东西有许多各省学长的博客和一堆 OJ 平台等诸多资源可供学习参考和训练的时候，我就比较好奇我到底是怎么学了这么久还不知道这些东西的[捂脸]。不过晚了啊，错过了就错过了吧，路还得继续走下去。这件事给我最大的影响就是，关于进入一所好大学到底有多大的意义，心里已经非常清楚了</p>

<h5 id="">学习与生活</h5>

<p>在写这一段之前，我要先澄清一下，信息学奥赛学习的内容主要是算法、数据结构和编程能力，不包括诸如修投影仪、修电脑、修手机、做 PPT、做视频、做网站等等之类的内容，我在业余的时候会去尝试一些别的东西，但是这是个人爱好问题，千万别把这些等同于竞赛 = = 因为真的有太多人误解了</p>

<p>很多高中参加 OI 的同学应该也都有被拉去修电脑修投影仪的经历吧 2333，电脑连不上投影仪或者显示的尺寸有问题的时候就会被强行叫去解决，我当然一开始是什么都不会的，不过见得多了也就能解决大部分的问题了，还能加深对电脑和投影仪一些概念的理解（</p>

<p>说起来其实也比较有趣，我初中的时候一直想着以后有电脑了要把那些小游戏网站上的游戏玩个遍，但是在高中有自己的电脑以后却发现我对游戏的兴趣其实并不大，倒是越发想多写写代码或者尝试一些别的东西</p>

<p>高一下学期的时候有个同学来找我帮她做份 PPT，给音乐课展示使用的，那时候我其实完全不会做 PPT，不过还是答应了下来<del>然后就开了个大坑</del>，我从打开 PowerPoint 软件看着完全不知道是什么的界面，到做出 Demo，中间就过了几个小时，可能本身微软的用户体验考虑得就比较好，不过我觉得这个东西能入门那么快还是需要一点天分的（逃</p>

<p>我那天做完 Demo 之后就碰到一个问题，因为那个 PPT 对于时间和动画的同步要求比较高，我并不知道可以单独对每一页重新设置时间，于是每一次排练计时之后就重新放一遍，不满意就重新进行排练计时，而且排练计时所计算的时间不会加上切换的间隔，我得自己在点击的时候算好这个时间差，这个就比较吔了，我记得那天是一直到凌晨三点多觉得对效果满意了才终于肯睡觉，而高一的我之前从来没有十二点以后睡过觉</p>

<p>也是那时候开始，我觉得自己对这种作品其实是有感情的，也决定之后经手的每一份作品一定要是自己当时的能力范围之内最好的，一直到大学的现在，我想，对于这个信念，我也一定会一直坚持下去</p>

<p>很幸运的是，在放到老师电脑上播放的时候没有什么卡顿，一切都和我预想得一样顺利，那个 PPT 的 BGM 是韩红的《天亮了》，我记得最后一句“天亮了”和那三个字的动画同步播出的时候，真的感觉特别兴奋，不过现在在自己电脑上反而做不到同步了，不知道是 Office 版本的问题还是怎么回事<del>，据说老师后来还在别的班放过这个，导致我突然成名</del></p>

<p>PPT 后来也做过蛮多次了，特别是晚会什么的之类，基本所有技术方向的东西都是我负责的（自豪脸.jpg）<del>，还顺便熟悉了话筒音响以及各种设备的操作，深感满足</del>，这之后还有一次学校安排的研究性学习，我们选择了电影鉴赏这个主题，于是我开始接触音视频的编辑，尤其 Goldwave 这软件用得特别爽，不知道现在写动画或者别的东西之类的总有做个渐变过渡的想法是不是因为那时候声音渐变效果用得太频繁了 hhh</p>

<p>高二的时候帮老师写了个微小的随机点名程序，完全的黑框框那种，代码在 <a href="https://github.com/KingsleyXie/NaiveProjects/blob/master/C%2B%2B/RandRollcall.cpp">这里</a>，不过写完以后就没有再维护过了= =，现在看起来真的是代码风格混乱，那也是我第一次写竞赛以外的代码，花了差不多两个礼拜吧，每天晚自习结束后就跑去改，因为要考虑的边界条件实在太多了，而且我那时候还只会手动测试……于是就耗了蛮长的时间在测试上，最后能正常使用当然还是非常开心的</p>

<p>学习上，前两年其实心比较乱，除了在机房能比较全神贯注以外，在别的时候很少能静下心来学习，成绩自然是没法看的，直到竞赛彻底结束，我也没有对大学有特别清楚的想法，都已经高二快结束了还是觉得无力前行，所以又经常跑去机房，写写代码也好，或者找点别人的博客看看也好，我可能一直都是那种喜欢逃避的一类人吧</p>

<p>在这两年其实知乎是给了我蛮大的帮助的，2015 年左右的知乎社区质量还是很不错的，至少相比现在而言好很多 = =，我不得不说有些价值观是那时候形成的，看着别人对于一些事情的思考，也会试着想想自己的见解，于是从此也多了很多对于身边发生的事情的思考</p>

<p>后来我逐渐去了解那些关于未来的事情，也慢慢计划着这最后一年应该怎样度过，同时把重心放在了数学英语和物理上。因为英语本来就比较好，而且兴趣也蛮大，到高三后期基本上 140± 是没什么问题了。物理的提升我自己倒是没什么感觉，只是有一次老师讲试卷的课间来问了问我试卷上游标卡尺读数那道题，我才反应过来我的物理成绩好像已经从二三十分提高到九十多了，其实我感觉做得多了以后对于高考物理这类题会有一种直觉，尤其是电路的填空题，老师想坑哪一种思维，真实的答案应该是什么思路，都比较容易直接看出来，最后高考物理也有 100+，还算不错吧<del>，虽然我大物还是差点挂科</del></p>

<p>语文和数学没什么特别大的提升，只能说在普通水平，倒是知乎给我带来的思维上的影响让我写议论文越来越觉得得心应手，化学和生物这两门课我直到高考前还是觉得没兴趣实在不想学，特别是那时候基本知道自己只要达到一本线就能有大学读所以就无所谓这两门了<del>，高考的时候这两门果然都没及格</del></p>

<p>三月份开始得准备自主招生的报名和审核材料了，我仔细看了蛮久的简章，最后看中了华工，因为它明写着省一等奖免笔试，而且面试通过就直接降一本线，我对自己的面试还是很有把握的<del>，所以感觉很稳</del>，然后只剩下一个名额，为了防止到时候犯选择困难症，我直接选择了上海交通大学，纯粹是打算去玩玩（逃，后来又发现上海科技大学是不占自招名额的，就加上了它，觉得可以拼一拼试试</p>

<p>我其实是蛮看重自招的，所以也很认真在准备自己的一些材料，自述信这东西我就前前后后改了六七遍，然后重新誊抄，一遍就是三张 A4 纸啊……尤其是我的字还难看[捂脸]。到初审结果出来之后已经是四月份了，就开始安排高考后的行程，毕竟三所大学的自招是连着的三天</p>

<p>这之后我其实还是时不时会觉得有些压抑，高三的时候晚自习下课大家基本都要留在班上继续学习半个小时，我却经常直接到操场去跑步，有时候跑完顺带思考思考人生。要是晚自习期间觉得不想学习就会跑到机房去放松心情，看看别人写的退役回忆录或者甚至刷刷知乎，反正只要有点事情让我可以暂时不用学习就行，和后来做的很多事情非常相似，就像知乎上有这么一个 <a href="https://www.zhihu.com/question/29133090/answer/43423035">回答</a>：</p>

<blockquote>
  <p>人往往是这样，做一件事情的时候，遇到困难往往不是想着去解决，而是去寻找是不是有“更有价值的事情”去做。</p>
  
  <p>其实并不是在寻找有价值的事情，你会发现，归根到底，你不是热爱彼，而是逃避此。</p>
</blockquote>

<p>其实我也深知那是在逃避，也一直在尽力让自己能分清什么是热爱什么是逃避，但是有时候硬撑着反而不是件好事吧</p>

<p>四月份除了自招安排以外还有一些很重要的事情，我在高二的时候买了台打印机<del>，可以打印彩色页面和照片，特别劲</del>，折腾了一年多，后来觉得应该要让它对于这个毕业存在些许价值，于是就自己设计了一份 A4 纸做成的小相册，给六位任课老师一人一份，每一份里面装着的照片都是我经过精心挑选和准备而且觉得很有回忆价值的。而因为我把打印机改成了连续的供墨系统，墨水还有非常多，后来就把班级合照打印了 60+ 份，在班服送到的时候一起送给每一位同学。前后两次看着满桌子的照片真的是成就感爆棚啊嘿嘿嘿<del>，我都有点舍不得把相册送出去</del></p>

<p>我记得当时的纪念相册是用的 Word 设计封面文字，然后 Excel 做边界线方便折叠和粘贴 2333，这是两个软件和打印机（以及胶棒？）配合完成的产物，这种能力所限不能用一个软件完成而找替代方案的事情我后来也有时候会经历，比如有一篇博客文章的标题图片就是先在浏览器里改 CSS 然后截屏最后用 PS 拼接而成的（逃</p>

<p>至于毕业照名单的事情，我是真的一直介怀到现在……本来照片拍好之后班主任找我照着做一份名单，我跑去机房一个个名字对着打，然后调排版调字体，最后把文件给了他，结果负责摄影的那边不知道是怎么萎事，对着这个名单自己手打了一份，并且把我加粗了的字体照样加粗了……emmm，简直是字体和排版都丑到一种境界，我到现在也不知道他们是为了省点纸张还是什么原因，明明我是调好了大小的。但是那时候因为还有别的事在忙，就没去找他们，我第二天自己拆掉了照片然后去打印上了自己的那份名单重新塑封，不过那真的是花了蛮多心思排版的东西，被这样敷衍过去，哇真的是气死</p>

<p>然后呢，高考其实很快就来了，翻翻当时为自主招生准备的一些材料，发现在我的自述信里面有这么一段话：</p>

<blockquote>
  <p>如今回头再看，参加竞赛和担当技术支持者之类的经历太有价值了，它们让我获得的一些能力正是我觉得学习计科必要的品质：让我找到了自己兴趣所在并有了人生的第一个梦想，也让我更勤奋踏实、不易被诱惑；让我懂得要为自己的热爱投入全力，忘却他物，不断挑战自己，也让我有了极强的自学能力和网络资源搜集与获取能力；更让我认识了一群不学 OI 可能一生都没有机会认识的牛人并向往融入他们的世界。</p>
</blockquote>

<p>可能也正是对于我高中三年最好的概括</p>

<h5 id="">高考</h5>

<p>那两天并没有意料之中的那么……怎么说呢，没有那么不平常？而且我们一寝室好像都睡得和平时一样特别舒服（逃</p>

<p>其实就像很多别的那些后来看起来很重要的事一样，在你真正经历的那段时间里，其实没有特别大的心情起伏，印象深刻的反而是那个文具是真的烂啊……不知道是怎么通过招标的</p>

<p>8 号考完到家，紧接着就收拾行李准备接下来的行程</p>

<h3 id="">毕业</h3>

<h5 id="">自主招生</h5>

<p>那么然后我就去参加了自主招生，第一站是上科大的校园开放日，结束后第二天紧接着是交大的笔试，然后马上就是华工的面试了</p>

<p>9 号下午到上海以后，上科大安排了一下大家的吃住以及简单熟悉一下校园环境，这短短半天给我的感觉就是……这所学校太壕了，校园开放日总共十天左右，感觉每一天的支出应该都蛮高的<del>，隐约有种已经被收买了的感觉</del></p>

<p>抱歉，有钱是真的可以为所欲为的.jpg</p>

<p>接下来就是一整天的开放日活动，用各种方式对来参加的学生进行多方面的考核，我感觉他们是真的很愿意在招募人才上投资，然后下午的面试体验还不错，对自己的发挥也很满意，有些问题其实在高三阶段都有过思考，感觉和面试官们以及同组的同学们度过的那个下午还是非常愉快的，有一部分回答我觉得一直到对于现在的自己而言也还很有意义</p>

<p>那边结束之后我就赶快带着行李东西跑去交大附近订的酒店了，然而比较吔屎的是，我订的酒店在徐汇校区旁边，然而笔试地点在闵行区，而且那天下午还有去广州的机票……我只能晚上提前熟悉一遍路然后第二天早上早点起来过去，踩完点回来之后再在附近到处转了转，然后打包了一份 KFC 到酒店吃，为什么我对这件事特别有印象呢，因为那天的服务员小姐姐超级漂亮(〃'▽'〃) 而且她漏给了我一对鸡翅还问我为什么一直站在那等，真是太可爱了（</p>

<p>笔试我本来就没报什么希望，毕竟数学物理这种考验智商的东西从来是和我没关系的，中间休息的时候发现我在的那层楼有一个贴着“交大英才”的柱子，上面是长者年轻时候的照片和介绍……我说怎么时间过得这么快！！！民民之中一股蛤意啊</p>

<p>说起来，在上海的这两所大学都是和长者有关的，交大就不用我多介绍了，而上科大的校长就是长者的长子，但是我绝对不是只因为这个原因才选这两所大学的，虽然其实确实有一部分是因为这个（雾</p>

<p>考完后感觉时间有点紧张，就打的回酒店了，没想到还是要一个多小时，花了 100+ 啊……然后赶去机场，本来计划好的在广州降落后坐地铁到酒店，没想到广州暴雨导致一堆航班延误，然后我就一直等啊等啊等啊，原本下午三点五十登机的，到凌晨的时候才终于安排登机起飞了，第一次坐飞机没什么特别的准备，降落的时候感觉耳朵疼到爆炸，而且一直延续到第二天一整天，包括面试的时候都觉得耳朵一直很不舒服，第三天起床才觉得好点</p>

<p>我到广州已经是差不多两点，只能打的去酒店了，又一次一小时 100+，到我睡下的时候已经三点多，想想我的面试是七点半的场还有些小激动呢，然而可能因为实在太累而下意识关掉了闹钟……我醒来的时候已经十一点多了，我确认了一下时间之后发现药丸于是飞速洗漱跑出去，在接近一个十字路口的时候还摔了一跤，真的是那种毫无意识的也没绊到自己就摔倒了，两边膝盖和手腕都是伤……那是我第一次明白“精神恍惚”到底是什么感觉，不过摔了一跤反而清醒了一点，拦了辆出租车到考点，门口围着一堆父母和保安，我就这样带着伤在万众瞩目下走了进去</p>

<p>检查证件的时候才发现身份证忘带了……因为登机用到拿出来就没放回那个准备面试用的文件夹，我只能再回去拿身份证，那边的志愿者和我说迟到没关系，因为有很多人都因为航班延误而迟到了<del>，我就至少心安了一些，拿完身份证顺便买了份早餐 或者叫中餐吧无所谓了</del>，回到面试的教学楼准备面试，那之前的一个小时左右真的可以说是非常惊险刺激了</p>

<p>志愿者们推来一大桶华工绿豆汤分给大家喝 2333，还有小姐姐帮我去拿了点纸，我自己甚至没发现膝盖有点血，这个时候倒是开始感觉到痛了[捂脸]，因为安排全乱了我就没太多时间去准备那两分钟的自我介绍<del>，只能现场编了</del>，还好发挥还比较稳定，最后的分数也挺高的</p>

<p>面试结束后在华工校园里转了好久，然后吃完晚饭买了瓶碘伏回去擦，因为伤口有点大块就没法盖被子，第二天醒来发现好多包啊……广州的蚊子真是恐怖。这个时候已经基本没什么事了，交大的结果虽然没出来但过不了也绝对是稳的，可是我又不想那么早回去，于是订了张机票第二天再到上海，想去交大玩玩，等结果出来之后再回去，不得不说机票提前订是真的便宜啊……我临时订的票是提前一个月订的两倍价格，订的酒店好像也是新开的，收到了一份果盘和一张可爱的便签φ(>ω&lt;*) </p>

<p>我订的晚上八点多的机票，因为下午两点得退房，就早了一些到机场，本来几个小时也就无所谓了，何况我还带了书去，那几天都快看完余华的三本小说了，没想到还是暴雨……看到航班表上一堆延误和取消的，感觉又得等到半夜了，更吔的是前一天浦东机场出了点事，导致到处安检变严了好多，把我看不清参数的充电宝扣下了= =<del>，我一直闲着，看完书就坐在那擦碘伏玩</del>，那天的雨真的是暴啊，我站在登机口附近看着闪电和雨都觉得外面完全没法待，还好我在外面的时候天气还不错，不然就更惨了</p>

<p>果然又是延误到凌晨起飞，等到降落浦东机场的时候已经四点多，而且我发现机场几乎没什么人了，也是那时候我觉得很奇怪就搜了搜，然后知道前一天发生了一起小规模的爆炸事故，不知道到底是因为凌晨本来就人少还是因为这件事导致的人少，我感觉特别累，就从到达区走到出发区找了条凳子坐了会，休息了差不多一个小时，然后看了看线路觉得可以体验一番磁悬浮 hhh，在地铁上累到真的差点睡过去，到酒店的时候已经虚得不成样子了，进到房间就睡到了下午，前一天晚上是我第一次经历通宵，还是被动的 = =</p>

<p>醒来已经下午六点多了，赶快出去觅食，然后去交大转转，路上查了下成绩发现笔试结果已经出来了，也就意味着我在晚上在交大这走一遍明天就应该要回去了，啊真是 sad，气得我在那逛了两个小时才出来吃了顿学校对面的鸡公煲，其实有点惊讶我竟然累成这样而且一天没吃饭还有走路的能力</p>

<p>第二天就彻底告别自主招生阶段了，然而我没赶上高铁……坐了一段地铁发现似乎赶不上了啊，然后跑出来坐出租车到车站，这是第三次花了 100+ 的出租车了[捂脸]，其实就差了一两分钟 Orz，没办法只能改签了，还好有晚一点点的高铁刚好可以改，就等了会然后坐那趟车回去了，在大学阶段类似的刺激经历我又体验过几次，尤其是最后一分钟进检票口的那种</p>

<p>坠有趣的是今年回家又多了一次差点和火车擦肩而过的记录：我太低估春运的人流量了，队伍太长导致没赶上本来买的车次，错过了车之后就去查有没有票……发现并没有，还好那时候心态也比较好觉得晚两天回去也没什么事，然后我再一刷新发现突然多出一张半小时后车次的票，赶快订好跑去检票，完美赶上</p>

<p>所以其实这么久以来，很多事情都有一部分是因为幸运，如果稍微有一丝的不一样，未来很可能一切都很不同了。毕竟长者说得好，还是得考虑历史的进程啊，有时就真的会有这种感觉：</p>

<blockquote>
  <p>[lyric]我拥有的都是侥幸啊</p>
  
  <p>我失去的都是人生</p>
</blockquote>

<h5 id="">暑假</h5>

<p>自主招生全部参加结束回来我就回家<del>混吃等死</del>了，交大凉了就剩下两所学校，华工本来我是很稳的，虽然发挥也不错，但是出了这么多意外还是隐隐有一丝不安，至于上科大就看自己高考成绩怎么样了。出成绩前一天的时候在开放日那天认识的几位同学突然发消息恭喜我获得了 A 档加分[奸笑]，到公布成绩的网站确认了一下，哎呦不错哦，拿到了全省两个 A 档加分中的一个（逃</p>

<p>然后第二天高考成绩出来我就彻底凉了，拿到最高一档的加分还是不够啊 Orz，所以我就只有华工一个希望了，但是你工的这个办事效率啊是真的让人捉急，高考成绩都出来了面试结果还没有，我那个时候完全不敢去想如果没有通过会是什么样<del>，而且其实还是有九成以上把握的</del>，等了差不多一个小时终于看到通过，内心 OS：好 稳了 有大学读了</p>

<p>成绩还是不错的，材料审核分和面试得分都蛮高，而且满足了第一志愿，心就放下了，填报志愿也就这一个选项，非常轻松，毕竟选择大学的工作在二三月份就已经做过</p>

<p>接下来七月份和舍友们一起去青岛玩了一个多礼拜，然而</p>

<blockquote>
  <p>[lyric]命运多舛 痴迷 淡然</p>
</blockquote>

<p>[捂脸]，我们碰到了各地的强降雨，在上饶到南昌的高铁上突然被通知去青岛的火车停运了……于是我们被困在了南昌，退票改签的队伍能排两个小时我也是服，然而改了之后发现那趟车还是晚点好久，大家都蛮累了就去休息了，车票还是退了等下一趟合适的</p>

<p>我发现 12306 那时候有个特别吔屎的逻辑就是你不能在已经购买了车票的时间段内买别的车票，完全没有考虑这种晚点的情况，必须退了票才能订别的票，不过现在好像已经不会了因为我这次回家也碰到类似的事情，没有因为时间区间内有票就不让购票</p>

<p>我们被困在南昌的时间本来是没安排的，但是既然走不了就只好强行有安排了，他们各种跑去网吧开黑，我又不玩游戏于是就自己去找了一家 KFC 坐了一下午，顺便学习 Python 哈哈哈，反正我自己带了电脑又没别的事做，那时候学的还是 Python 2<del>，真是个异端啊</del>，我记得那天下午总共学了五六章的样子，因为是基础内容所以学起来蛮快的，然后在八月份的时候又断断续续把这门课程剩下的内容结束了</p>

<p>在南昌待了差不多两天之后我们终于订到了车票，不过是从某不知名车站中转，而且这分成的两趟都是通宵差不多十二个小时的硬座，虽然惨了点但好歹是能去了[捂脸]，所以我们通宵了一趟之后在那个车站附近找了家酒店休息，然后吃完晚饭继续通宵硬座，终于是到了青岛，啊太爽了，我还记得大家下车以后那个激动，不容易啊</p>

<p>因为晚了两天到，就把后面的车票全部改签了，还好这个过程没碰到什么麻烦，大家在房间里睡了会就出去准备开始浪了，果然是能有地方玩的话只需要睡一会就感觉不到通宵的累了呢，这几天过得还是非常开心的，除了我把眼镜掉在海水浴场然后去重新配了一副 = = 要不然完全没法走路啊，实在是戴习惯了下水的时候都没感觉到有眼镜戴着，要换泳镜的时候才发现然而又不想上岸就算了</p>

<p>八月份就基本是轮流蹭饭，还是比较珍惜的毕竟以后要凑齐那么多人都不容易吧，我在我的升学宴之前花了蛮长的时间去整理这三年里关于这个班级的一些 PPT 作品以及其他一些有纪念意义的文件，然后发给了大家，也不知道是当做回忆还是当做告别，总之，大学前的人生到这就宣告结束了，很多东西都变成了回忆</p>

<h5 id="">告一段落</h5>

<p>走到这里，我真的发自内心觉得感谢国家，感谢自主招生政策，感谢信息学奥赛<del>，给了我一次重新做人的机会</del>，当然也感谢高中的班主任给了我那么大程度的自由，很高兴当年的选择，让我能在他带的班里度过这三年</p>

<p>同样要感谢的是这三年时光里所有的相遇，各位老师，各位同学，各位朋友，尤其是因为竞赛认识的一些朋友，是你们让我更清楚地了解了未来的路有多少种可能。最后能拿到录取通知书，也非常感谢上饶中学，感谢华南理工大学，感谢参加自主招生那几天遇见的所有善良热情友好的人们</p>

<h3 id="">大学</h3>

<h5 id="">学习和生活</h5>

<blockquote>
  <p>[lyric]我身在 当时你 幻想的 未来里</p>
  
  <p>这个狂热和冲动 早已冷却的如今</p>
</blockquote>

<p>其实说冷却也算不上，有了更多可以自由支配的时间还是尽可能去好好利用的。军训期间加了几个自己喜欢的部门，接着非常高兴地遇见了一群志同道合的朋♂友，在学院 ACM 选拔赛开始的时候当然还是去报名参赛了，然后进入集训队划水。对于新环境自然是适应得非常快的，所以不久就已经习惯大学生活</p>

<p>令我比较意外的是，高中因为觉得有趣而做过蛮多 PPT 的经历派上了用场，小组 Presentation 的时候主动承担起了这个<del>重</del>任，也意料之中拿了几次满分，这大概是我能在大一的两个学期英语成绩都是 Rank 1 的一部分原因（装完逼跑</p>

<p>回头望的话，我觉得我自己从高中到现在对于 PPT 的观念有了蛮大的改变<del>，审美也提高了不少</del>，而且会更多考虑交互、用户体验这块的内容，不知道是不是因为自己写代码也比较在意这块。我想以自己的经历和习惯为例分享一下关于这方面的内容，如果还能对一些人有帮助的话那就深感荣幸了：</p>

<ul>
<li><p>PPT 的关键词是“Point”，也就是要点。绝对不能把一堆文字复制粘贴上去然后照着念，我的建议是自己提取出关键词或者总结成短语，然后在做 Pre 的时候用自己的语言把这些东西重新组合起来，这样才能真正起到 “PowerPoint” 和 “Presentation” 两者配合而各司其职的作用</p></li>
<li><p>排版整齐，图片清晰。这个是给人的非常重要的印象，图片和文字怎么结合，配色之类的都是蛮重要的内容，做完一页 PPT 之后应该检查一遍这个页面的内容，留白是不是太多或者内容是不是太紧密了，排版上查看一下内容的关系是不是准确，而且图片应该尽可能找高清无水印的那种，有必要的话需要加上来源链接</p></li>
<li><p>动画、效果不能太多太乱。这也是我自己在高中阶段有时候会犯的错，看到一堆琳琅满目的动画就想全都用上，然后内容有时候就显得比较乱，而且动画是作为衬托作用的东西，不能让它的重要性盖过内容本身</p></li>
<li><p>层次分明。和前面两点也有一定的关系，对于同一级的东西应该全程使用基本相同的排版和动画，让人能看出包含关系和并列关系，便于理解，这个有时候可以用“幻灯片母版”的功能来实现，给每一个部分的开头幻灯片做一个统一的模板，层次的关系就很明显了，尽量避免复制粘贴幻灯片这种操作</p></li>
<li><p>分工合作，整体协调。PPT 是 Presentation 的一个辅助工具，而且大部分时候都是一个小组合作尝试的东西，在做幻灯片之前应该花足够的时间了解要求、确定主题，然后定好分工内容，一起讨论衔接、内容的整体性等问题，之后分开找资料制作、合并所有幻灯片，务必注意最后做出来的东西一定要是一个完整的作品，而不能是完全不相关的几份</p></li>
</ul>

<p>就我自己而言，我们的流程一般是<del>最后两三天突然发现好像下周要做 Pre 然后</del>找个地方讨论一下，定好主题，以及需要的内容，分好各自的部分回去找资料做 PPT，而为了让大家能专心在内容的质量上，我都是让其他人只要在 PPT 上放上需要的文字和图片最后发给我一个人做排版和动画的，这样也比较方便统一风格</p>

<p>我自己从来没有准备过稿子<del>，因为都是上场前半小时才做好 PPT 哪有时间啊</del>，都是在做完队友们的那些部分之后先发到群里给他们准备，然后完成自己那部分的内容，我发现我可以在排版和加动画的时候基本模拟好讲的时候的流程，所以每次 Pre 尽管没稿子也非常流畅，也越发觉得 PPT 的作用应该是使用关键词、短语或者句子给演讲者和观众关于串接内容的提示，当然这种 Groupwork 能一直拿高分，和遇见的队友一直都比较给力的原因也是分不开的，也是这个时候开始更多地去了解和认识了“团队”的重要性，很多事情不是靠一个人就能完成的</p>

<p>大一下的后半段体验了第一次通宵……准确地说是第一次主动通宵[捂脸]，因为第二天早上有个 Presentation，下午有个需要展示用的视频等着我剪辑<del>，而我到晚上还什么都没开始</del>，于是一直肝到早上七点多洗了个澡，也没法再睡觉了，而 PPT 其实还有一小部分没做好，我就继续在那搞到 Pre 开始前半个多小时，到教室以后感觉浑身没力气啊 Orz，据队友说我在台上讲的时候都一股虚弱的样子，虽然我自己并没有感觉，还好结果还是满分（自豪.jpg），下午的视频也还不错，一切结束之后赶快回去补觉了</p>

<p>大一下呢就还有一次团队合作的经历，是隔壁一流大学的蛤客松，不过我实在太菜了就没法干什么，主要负责给队友端茶倒水，拿外卖拿水果拿酸奶，中大在那两天给机房送了好多箱香蕉<del>，不知道是不是哪个学院在研究香蕉种植</del>，然后在第二天送来 Leap Motion 的时候我就一个人在旁边玩这东西顺便看他们写代码，晚上出现了一件有♂趣的事导致我十点多还跑去中大给一辆摩拜单车关上锁 hhh。关于这一次蛤客松 Bokjan 有一篇 <a href="https://bokjan.com/2017/04/sysu-hackathon-2017.html">游记</a> （放出来会不会被他打</p>

<p>刚刚过去的大二上学期有段金工实习的时间，这两周体验给我的思考就是，过去一年多的大学生活过得非常急功近利，没有踏踏实实去深入了解某一个方向或者知识点之类的。一个不太恰当的例子是很多课直到期末考前最后一天甚至一晚才愿意翻书看看，这就导致成绩单里一堆的 60+，GPA 彻底爆炸<del>，不过这些课你让我再学一遍我照样不会愿意花时间下去的</del>。更恰当点的例子可能就是在写代码或者实现某个想做的功能的时候，各种原因特别是时间问题导致的没有去系统了解一些知识，只是停留在使用甚至复制粘贴的阶段上，我觉得这样其实非常被动，也会导致之后需要付出更多的代价，所以还是应该尽可能避免的</p>

<p>这一年多的时间，在社团度过的部分也都算蛮有意义的，不过毕竟会和一些别的组织部门打交道，也有偶尔看到一些不太好的现象，把压榨干事当做正常风气甚至拿来攀比这种我就懒得吐槽了。如果说要现在的我给大一学生建议的话，我想大概会是，一定要认清楚自己的那种忙碌，是真的充实，还是只是因为组织的效率低下，后者的话，大部分情况下离开会是最好的选择</p>

<h5 id="acm">ACM</h5>

<p>这个篇幅就更不知道会有多短了……大一加入集训队之后我这种咸鱼基本就是在划水，然后寒假后按照要求提前回去参加训练，我好像还是属于偏向逃避的那类人，集训的强度觉得实在有点吃不消，又没有心情花时间好好钻研，于是后面的时间就基本是在机房学 Vim 以及写迷宫大作业了（逃</p>

<p>这个时候我已经基本知道我这种辣鸡是不适合参加 ACM 的，算法这东西还是把它当做兴趣去学习吧，如果要用参赛的那种态度把它当做一项竞技我是绝对撑不住的……虽然下学期仍然会时不时去机房划划水，但各方面的忙碌也逐渐让我到处都力不从心，我觉得该舍弃一些东西了，于是在暑假前和教练谈了谈，退出了 ACM 集训队，也在暑假的时候计划着写这篇文章</p>

<h5 id="">大作业</h5>

<p>其实大一的那些东西，也算不上是“大”作业，第一份是去年寒假的迷宫，其实就是搜索算法而已啦，为了让迷宫更有意思一点，我就去尝试把围墙换成两句诗，然后做了点别的微小的工作，这个东西就算结束了，不过后来收到一个 issue 让我尝试用 <code>ncurses</code> 重写一遍，我去查了查资料发现好像蛮有意思的然后去找了本相关的书，在暑假回家的火车上看了十几页<del>后来再也没翻过</del>（讲道理我还是很希望能有时间完成这个东西的重写的</p>

<p>后来的一份还是 C++ 的，不过这个过程就比较吔了，本来老师说看进度安排两周答辩，结果突然按照组号单双数区分……我就突然少了一周<del>玩的</del>时间，坠痛苦的是开题报告吹水过度，我莫名有种答辩药丸的感觉，甚至怀疑能不能做出来雏形，<del>不用想</del>想了想肯定是做不出来了，于是我开始找曲线救国的方案，那时候我已经在百步梯技术部待了快一年，对 Web 开发也有了基础的了解，我就找了点资料看看能不能用 C++ 写后台强行做出来，试了一晚上终于发现方案可行，大作业大概是有救了</p>

<p>但是我还是 Naïve 啊，为了方便而且要在短时间做出来肯定得用 JSON 进行交互，于是后台的存储方案就成了个问题，虽然很快就找到了一个 C++ 的 JSON 库可以用，但是我还是花了一整天的时间在进行基本的 CRUD（增删查改）测试，历尽千辛万苦终于已经能完成前后端交互和数据操作了，于是剩下的工作就是拿个之前写的项目改成这个需求，以及设计好操作逻辑，虽然内容不是特别麻烦但是已经只剩下两天时间就答辩了，肝了好长时间一直到答辩时间开始我还有两个子模块的前端没写好……还好我们组号靠后，赶在上场前完成了<del>面向答辩的</del>界面，展示还是非常顺利的，也照样拿了满绩（</p>

<p>回来之后当然是先改成真实的交互，因为赶着答辩有些细节就没处理了，而且好多功能的实现都特别吔，我还是不希望把一坨屎交上去留存档的，真的后来也耗了很大精力去做优化，虽然这一部分并没有人会在意，然后就部署上线玩了玩，地址在 <a href="https://demos.kingsleyxie.cn/supermarket-management/">这里</a></p>

<p>国庆假期的时候感觉这个后台还是太不清真了，我要为了这个项目的部署单独安装一个 Apache，得不偿失啊……于是就试着换成轻量级的 FastCGI，文章在 <a href="https://kingsleyxie.cn/a-day-spent-on-fastcgi-and-spawn-fcgi/">这里</a>（还没写完 2333），折腾了差不多两天的时间就为了重新部署，好几次陷入绝望啊QAQ，但是我觉得不搞好实在是太不爽了还是硬撑着读了一堆看不懂的文档把它解决了，完成的时候那个 excited 的心情是真的是妙♂不♂可♂言</p>

<h5 id="">工程项目</h5>

<p>嗯 名称是这样的，不要理解成大工程那种 hhh，就是一些入门级的 Web 项目</p>

<p>大一下学期的团学突然要做个用来竞赛报名的网站<del>，然而部门里并没有人能写，于是我就接下了这个锅</del>，这个网站是真的经历了好多 2333，边写边上线，一个个功能补上，最后接近报名结束才写完所有功能的，而且上线第一天还因为一些莫名其妙的原因服务器的数据库崩了……我那时候对数据库所知甚少啊，用尽各种方式都没能恢复，但是也正好在读了蛮多资料以后相信一定有办法解决，我都不知道当时是带着什么心情跑去办公室开例会的，然而还是没人能帮上忙QAQ</p>

<p>不过我还是不愿意死心，回到寝室继续捣鼓，终于慢慢出现一线希望，最后在零点左右终于成功恢复了，然后想起来晚饭也忘记吃了，经历了九个小时的绝望突然看到曙光这些也就无所谓了[捂脸]，然后重新部署到服务器上，加了个暂停报名原因的通知，七七八八一堆东西弄完已经四点多，当时的情景就是这样的：</p>

<blockquote>
  <p>[lyric]没有喧嚣 只有宁静围绕</p>
  
  <p>我 慢慢睡着 天 刚刚破晓</p>
</blockquote>

<p>后来呢，在端午节前我又接到一个锅，做一个毕业季用的 <a href="https://github.com/KingsleyXie/BooksReservation">书籍预约系统</a>，最吔的是给需求的人其实自己都不知道是要做什么，这种有个想法就推锅的行为是坠让人厌烦的，从需求分析到代码实现到部署上线都得一个人去完成。当时光是设计数据库我就花了半天的时间，用掉一堆草稿纸，因为甚至没人告诉我需要存些什么东西，用什么做取书凭证，反而是我得自己去设计好流程然后写完告诉他们。这个项目耗掉了我整个端午假期的几天时间，也是我 C++ 大作业没时间写的直接原因，那三四天我经常写代码到凌晨三四点然后出去到阳台上透透气<del>，看看凌晨四点的华工</del>。最让我受♂伤的事情是我因为觉得有必要就加了个搜♂索功能，然而给他们测试的时候竟然说不能滋瓷智♂能♂搜♂索……啊我真是该写个搜♂索♂引♂擎的可惜我也没这能♂力啊</p>

<p>这两个项目，代码真的非常非常混乱，整个项目的文件结构都很吔，我自己都看不下去了，于是在暑假就开始整理代码，花了特别长时间……我记得中间断过一段时间，然后在国庆期间才全部完成，两个项目的版本迭代都有好几次，真的前后改动非常大，就接近是在重写了。但其实改完也算不上是多好的东西，好多元素都是通过 JS 动态添加的，主要压力都在客户端上，尽管这种临时项目也没有考虑 SEO 之类东西的必要，但是不管从哪方面讲它们都还有很多需要改进的地方，不知道接下来这个学期能不能有时间再做一些优化</p>

<p>这个整理代码的阶段给我的感悟是，编写代码的时候，尤其是比较复杂一点的工程项目，<strong>遵守基本的代码规范真的非常非常重要</strong>，刚好前两天在看《阿里巴巴 Java 开发手册》，对前言里这样一段话深以为然：</p>

<blockquote>
  <p>对软件来说，适当的规范和标准绝不是消灭代码内容的创造性、优雅性，而是限制过度个性化，以一种普遍认可的统一方式一起做事，提升协作效率。代码的字里行间流淌的是软件生命中的血液，质量的提升是尽可能少踩坑，杜绝踩重复的坑，切实提升质量意识。</p>
</blockquote>

<p><em>（P.S. 总感觉这段话一堆语病啊23333）</em></p>

<p>在百步梯技术部，大大小小的东西也写过一些，不过毕竟是有技术传承的部门，分工和进度安排都挺合理的，自己在部门内外学了蛮多东西，也在尽力去帮助新同学们。我是一个辣鸡后台，但是经常自己感兴趣想写点东西的时候就会去尝试一些前端的 UI 框架<del>，顺便提高审美，学习设计</del></p>

<p>工程项目的这些经历，除了加强了我对于团队合作的理解以外，也让我一次次认识到，对于那些看起来很难的东西，应该尽量去尝试，很多时候都是可以分解成一个个小部分来完成的，同时我更理解了轮子哥所说的“要多去挑战那些正好在自己能力范围之内，但是再难一点点就做不出来的东西”的意味<del>，虽然我好像也没挑战过难的东西</del></p>

<p>此外，不管是对于工程项目还是别的什么有关于计算机的东西，我仍然一直坚持着“经手的每一份作品都一定要是自己当时的能力范围之内最好的”的信念。虽然随着年级越来越高也真的有些感觉有点不容易做到，特别是一些水课强行布置大作业之类的，可能有时候还是得看这份作品究竟是自己愿意去做的还是只是为了交一份作业</p>

<h5 id="">博客</h5>

<p>这个博客是从去年的这个时候开始用的，一年时间里也没能写出什么很有价值的东西，甚至有几篇已经发布的是没写完的，很惭愧 hhh</p>

<p>不过呢，经过一段时间的折腾之后对于网络相关的知识的理解是有非常大帮助的，暑假的时候看《图解 HTTP》和一本关于 Wireshark 抓包相关内容的书就比较轻松，然后在写了几篇文章之后觉得需要改一些元素的样式，以及给博客增加一点额外的东西，比如评论模块，数学公式插件之类的，就自己动手 丰衣足食了（难不成自己的服务器还能别人动手吗 2333，这些优化和更改是在七月的后半个月完成的，毕竟比较咸鱼，是边休♂闲♂娱♂乐边折♂腾的</p>

<p>暑假结束到学校本来想着应该要至少保持一个月一篇文章吧，结果并没有做到，事情真的太多了，国庆假期也基本在整理上个学期留下的辣鸡代码，然后一直到 12 月的时候，Java 最后一次实验写了点 Web 方面的内容，我觉得还是有一些总结的必要的，就连着写了三篇文章，可以在之前的文章列表里看到</p>

<p>另外一点就是这一年里碰到一些 bug 或者想做点什么东西去搜索资料的时候，发觉中文社区的质量真的参差不齐啊……也确实有很多好的内容我不否认，但是总是发现某些大站点里，一堆全文复制粘贴别人文章还不注明出处甚至把文中链接都删掉的“小编”，也不知道该讲什么好。我不是那种能改变这种现象的人，不过还是有能力选择回避的，这半年多一直在坚持能用英文搜索的就用英文搜索<del>，也有一部分原因是输入法切来切去好麻烦</del>，现在倒是觉得这样对自己能力的提升有更大的好处，Google 英文和 Stack Overflow 真是好东西（认真脸</p>

<p>寒假开始之后，终于有点时间可以做点自己的事情了，就先后实现了个链接卡片盒用于友情链接页面，以及一个目录生成的功能，这是我一直都想加在博客上的东西，关于这两个功能我也有写介绍如何实现的文章。然后呢，一些很微小的东西我就不做介绍了，比如我在写文章过程中就觉得需要给歌词片段写个单独的样式，然后很快就实现了，这种有需求可以自己完成的感觉真的特别爽<del>，我可能也就天天沉迷在这种没有难度的小玩意上了</del>。尽管不是每一次都是那么顺利，但是当最后完成，放到博客上发现与预期一致的时候，除了成就感以外其实还有好多没法描♂述的心情</p>

<p>我断断续续改了好多次 <a href="http://kingsleyxie.cn/memoirs-of-contests-and-life-so-far/../about/">About 页面</a>，现在已经是一篇相对比较完整的介绍这个网站以及我自己的文章。除此之外，正如前文所提，博客的很多样式或者插件之类都经过了几次优化和更改，我想我可能已经对这个博客有点感情了 2333</p>

<p>写博客真的是一件特别累的事情，一篇文章至少要耗费半天到一天的时间，很多时候还要不断重复之前做过的内容，甚至是重复试错，就为了保证文章的准确性，不过价值也同样很大吧，我觉得一份总结对于事情本身而言也是非常重要的一部分，所以也尽力坚持更新博客<del>，但是不敢保证更新的周期</del></p>

<h3 id="">未完待续</h3>

<blockquote>
  <p>[lyric]这是我自传 最终章</p>
  
  <p>写这首长诗 用一生时光</p>
</blockquote>

<p>大学应该算是人生第一个阶段的自传最终章了吧，希望能写好这首长诗</p>

<p>走到这里，我想，要感谢百步梯技术部、微软技术部，以及在大学遇到的各位志同道合的小伙伴，然后，感谢陪我走过低谷的朋友，也感谢<del>虽然时不时颓废但也</del>一直在前进的自己</p>

<h2 id="">心声</h2>

<h3 id="">归属</h3>

<p>我从初中开始住校的生活，到高中以后又因为各种原因，觉得对家的归属感越来越低。也是从初中毕业开始，所有的事情，从选择高中，选择参加竞赛，到选择大学，选择专业，安排老师同学那边的升学宴，包括跑去辗转各地参加自招，以及几年内大大小小各种事情的所有行程安排，都是我自己一个人完成的，甚至有些事是在进行了一段时间或者结果出来之后父母才知道</p>

<p>可能也是因为小时候的经历让我把这变成了一种习惯，从小学我就明白不好的事情不能和家人讲，除了招来一顿骂以外一般都没什么别的，慢慢也就很少愿意去和家人交流了。而直到大学我才开始发觉，小时候的经历，尤其是作为留守儿童的那些，给我带来的骨子里的自卑和胆怯，对我的后来有非常大的影响，也多少导致了一些……遗憾吧</p>

<h3 id="">……</h3>

<blockquote>
  <p>[lyric]深色的海面布满白色的月光</p>
  
  <p>我出神望着海 心不知飞哪去</p>
</blockquote>

<p>高中到大学，一路以来幻想过一些从未有开场的不可能，有关于梦想，也有关于爱情。有时甚至很傻地希望能过得像初中时候那样什么都不懂，除了学习和看电视基本就没别的生活</p>

<p>可是毕竟很多东西迟早是要经历的呀，人生要是那样一片空白也太无趣了不是吗</p>

<p>其实很感激这些相遇和它们背后的故事，让我有不断的反省，也能更加清楚地了解自己，明白自己所向往的和应该向往的究竟是什么。只是逐渐觉得，可能对于有些东西，已经越来越没有再期待的勇气了</p>

<h3 id="">自由</h3>

<blockquote>
  <p>[lyric]有没有那么一朵玫瑰 永远不凋谢</p>
  
  <p>永远骄傲和完美 永远不妥协</p>
</blockquote>

<p>仔细想想，这么久一直在尽力追求的，并且在接下来的日子里也一直会继续追求的，应该是自由吧。那种不需要屈服于环境，可以忠于信仰、忠于兴趣的自由，那种不用把判断所谓的“有用”和“没用”的标准看得太重、太功利的自由</p>

<p>我一直很少去想做一件事情有没有用或者和自己的专业有没有关系，大部分情况下都是觉得有意思就会去尝试，有些比较意外地在后来帮了我很大的忙，当然也有很多没有派上用场的东西。随心所欲不容易真正做到，但是还是应该去尽力让自己有实力听从内心的声音</p>

<h2 id="">短期的未来</h2>

<p>到现在为止的很多经历和故事已经足够让我认识到，有些高度是永远都不可能达到的。对于短期内的未来，也明白不用去苛求多么优秀、多么完美，而要学会接受真实的自己。每个人都有值得分享的故事，也都有不愿提及的东西，有时候，那些一笔带过甚至只字不提的，反而是最刻骨铭心的</p>

<p>而我对自己所希望的只是，无论正处于的阶段是顺利还是曲折，都一定要保持冷静，清楚地认识和了解自己。然后还有一点就是：</p>

<blockquote>
  <p>[lyric]对留下的脚印 回头 挥挥手</p>
</blockquote>

<h2 id="">结语</h2>

<p>好了就到这里吧，文章已经很长了，从暑假退出 ACM 集训队后打算写，到 8 月底开始写了一小段，然后开学后计划 10 月写完发布，却一直拖到寒假再继续……回家前一天下午本来打算去基地写点代码，但是天气很不错于是跑到中心湖去写了一段，最后终于在今天写完了</p>

<p>多了一个学期的时间，也多了一些经历和感悟，虽然对于 OI 和 ACM 本身我并没有花到太多的时间，但是竞赛的这些经历却对我有着非常深的影响，即使是在早已退役的现在，看到别人写的回忆录时心里还是会有一种别样的感觉。我花了很长时间写完这一篇文章，两万多字，真的当做回忆录也好，或者当做对走到现在的一份总结也好，接下来还是得继续往前走的</p>

<p>感谢读到这里的你</p>

<style>  
html {  
    /*
        The following variables stands for:
            Start Color
            End Color
            Vertical Length
            Horizontal Length
            Width
        of the lyric style, respectively.

        Not decided yet about which color gradient to use?
        Try <a href="https://uigradients.com">https://uigradients.com</a>, it may be of great help!
    */

    --lrc-start: #414345;
    --lrc-end: transparent;
    --lrc-vlen: 30%;
    --lrc-hlen: 30%;
    --lrc-width: 2px;
}

blockquote.lyric {  
    /* Lyric styles as blockquote */
    text-align: center;
    background-repeat: no-repeat;
    background-image:
        linear-gradient(
            to bottom, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(
            to right, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(
            to top, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(
            to left, var(--lrc-start), var(--lrc-end)
        ),
        linear-gradient(transparent, transparent);
    background-size:
        var(--lrc-width) var(--lrc-vlen),
        var(--lrc-hlen) var(--lrc-width),
        var(--lrc-width) var(--lrc-vlen),
        var(--lrc-hlen) var(--lrc-width),
        calc(100% - (var(--lrc-width) * 2))
        calc(100% - (var(--lrc-width) * 2));
    background-position:
        left top, left top,
        right bottom, right bottom,
        var(--lrc-width) var(--lrc-width);

    /* Style offsets for Ghost */
    padding: .5em 0;
    border: none;
}
</style>

<script>  
    //Lyric Blockquote Placeholder
    const lrcBPH = '[lyric]';

    document.querySelectorAll("blockquote")
    .forEach(function(ele) {
        //Check if the blockquote content starts with `lrcBPH`
        var txt = ele.firstElementChild.innerText;
        if(txt.indexOf(lrcBPH) == 0) {
            ele.firstElementChild.innerText =
            txt.substr(lrcBPH.length, txt.length);

            ele.classList.add("lyric");
        }
    });
</script>]]></content:encoded></item><item><title><![CDATA[给博客增加一个微小的目录生成器]]></title><description><![CDATA[<p>文章目录</p>

<h3 id="">前言</h3>

<p>目录对于一篇文章来说其实是蛮重要的，比如全文内容预览或者检查文章结构、标题包含关系等，然而 Ghost 的 Markdown 并不直接支持 TOC 生成语法，因为不想通过加载别的文件来完成这个小功能，就自己尝试实现了一个比较简单的 TOC Generator</p>

<h3 id="">需求分析</h3>

<h5 id="">操作流程</h5>

<p>稍微思考一下，最基本的操作流程还是比较简单的：</p>

<ol>
<li>选取出需要生成目录的 heading  </li>
<li>给这些 heading 设置锚点  </li>
<li>生成目录</li>
</ol>

<p><del>当然只要开始写就会发现其实这是一个越来越大的坑</del></p>

<h5 id="">拓展需求</h5>

<ul>
<li><p>支持点击收放 <br>
有些情况下目录区块可能略长，为了不影响体验可以对目录块增加一个对点击事件的响应，进行收放操作</p></li>
<li><p>支持占位符 <code>[TOC]</code> <br>
为了方便使用我打算把代码直接加到 Ghost 的文章模板代码里，但是有些文章其实是不需要目录的，所以就通过判断是否有占位符 <code>[TOC]</code> 来确定是否执行生成目录的代码</p></li>
</ul>

<h3 id="">基本框架</h3>

<p>生成目录的操作相对其他部分来说会复杂一些，就先不管了，首先确定一下操作的基本框架代码，对 Ghost 来说，文章主体内容是在 <code>body</code></p>]]></description><link>http://kingsleyxie.cn/implement-a-toc-generator-for-my-blog/</link><guid isPermaLink="false">19816bc7-0ef9-4f4a-9f3d-511e9f148d6b</guid><category><![CDATA[Program]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Wed, 17 Jan 2018 10:55:00 GMT</pubDate><media:content url="http://kingsleyxie.cn/content/images/2018/01/toc-generator.png" medium="image"/><content:encoded><![CDATA[<img src="http://kingsleyxie.cn/content/images/2018/01/toc-generator.png" alt="给博客增加一个微小的目录生成器"><p>文章目录</p>

<h3 id="">前言</h3>

<p>目录对于一篇文章来说其实是蛮重要的，比如全文内容预览或者检查文章结构、标题包含关系等，然而 Ghost 的 Markdown 并不直接支持 TOC 生成语法，因为不想通过加载别的文件来完成这个小功能，就自己尝试实现了一个比较简单的 TOC Generator</p>

<h3 id="">需求分析</h3>

<h5 id="">操作流程</h5>

<p>稍微思考一下，最基本的操作流程还是比较简单的：</p>

<ol>
<li>选取出需要生成目录的 heading  </li>
<li>给这些 heading 设置锚点  </li>
<li>生成目录</li>
</ol>

<p><del>当然只要开始写就会发现其实这是一个越来越大的坑</del></p>

<h5 id="">拓展需求</h5>

<ul>
<li><p>支持点击收放 <br>
有些情况下目录区块可能略长，为了不影响体验可以对目录块增加一个对点击事件的响应，进行收放操作</p></li>
<li><p>支持占位符 <code>[TOC]</code> <br>
为了方便使用我打算把代码直接加到 Ghost 的文章模板代码里，但是有些文章其实是不需要目录的，所以就通过判断是否有占位符 <code>[TOC]</code> 来确定是否执行生成目录的代码</p></li>
</ul>

<h3 id="">基本框架</h3>

<p>生成目录的操作相对其他部分来说会复杂一些，就先不管了，首先确定一下操作的基本框架代码，对 Ghost 来说，文章主体内容是在 <code>body &gt; div.site-wrapper &gt; main &gt; article &gt; section.post-content</code> 里，因为 <code>post-content</code> 是一个单独的 class，就直接用它来当做文章内容的选择器了：</p>

<pre><code class="language-javascript">var TOC = '&lt;div class="toc"&gt;Table Of Contents';

var elements = $(".post-content").find(":header");  
$.each(elements, function(key, content) {
    content.id = content.innerText;
}
TOC += 'Generated HTML Code Of TOC'

TOC += '&lt;/div&gt;';

$(".post-content&gt;p:first").before(TOC);
</code></pre>

<p>其中的 <code>$.each()</code> 里进行的操作是设置锚点，也就是把一个元素的内容作为它的 <code>id</code> 值，这样就可以通过 <code>#id</code> 的方式直接定位到对应位置了</p>

<h3 id="">目录生成</h3>

<p>结构当然是使用 HTML 的 <code>&lt;ul&gt;</code> 和 <code>&lt;li&gt;</code>，这部分的具体实现其实考虑了一段时间，中间有一些方式是有些问题的，这里就不讲了，最后的决定是使用栈来判断当前状态，然后对应确定所需增加的代码段内容，详细实现方式在下面展开讲</p>

<h5 id="">目录区块结构</h5>

<p>一般来说，目录是由 6 个 HTML 的 heading 组成的，那么很自然地考虑是不是直接分六级，然后按照顺序一一填入，但是这样会带来一个问题：如果两个不相邻的 heading 直接作为父子元素的话，会导致它们中间的间隔较大，生成的目录也就会有些别扭</p>

<p>比如我平时写文章常用的就是 <code>&lt;h3&gt;</code>、<code>&lt;h5&gt;</code>，偶尔有需要时会使用 <code>&lt;h6&gt;</code>，主要原因还是字体大小产生的层级分别比较明显一些，看起来也会舒服点hhh</p>

<p>按照上面提到的方式生成目录的话，由 <code>&lt;h3&gt;</code> 作为开头的问题比较容易解决，只是把原本 <code>&lt;h1&gt;</code> 的位置改成它，但是中间间隔了没有用到的 <code>&lt;h4&gt;</code>，这个间隔要去掉的话就需要对实现的代码做一些更改了，而且要保证在不同情况下都能正常生成，如果要我拿这个方案实现的话……可能我的想法会是先遍历一遍所有 heading 然后提前设置好分级的数量再重新遍历一遍填入</p>

<p>但是很快我就想到一个看起来可能更好一些的想法，这个想法的实现有一个前提，那就是需要保证目录的结构是比较规范的，而对于这个“规范”，我的理解是这样的：</p>

<ul>
<li>第一个出现的 heading 是文中大小最大的</li>
<li>文章所有 heading 的嵌套符合先大后小的关系</li>
</ul>

<p>一种比较特殊的情况是 <code>h1 &gt; h2 &gt; h5 &gt; h1 &gt; h3</code>，也就是 <code>h3</code> 这个在第一组目录里面没有出现的大小出现在了后面，我的想法是把它仍然当做 <code>h1</code> 的子元素，也就是最后生成的时候它和前面的 <code>h2</code> 是同一层的，实际上我觉得这种结构的出现是不太应该的……整个文章的目录结构应该比较统一，比如下面这样都比较符合我所理解的规范：</p>

<pre><code>h1 &gt; h2 &gt; h3 &gt; h5 &gt; h1 &gt; h2 &gt; h3  
h1 &gt; h2 &gt; h5 &gt; h1 &gt; h2  
h1 &gt; h2 &gt; h1 &gt; h2 &gt; h5  
</code></pre>

<p>简单来说，就是如果把一个最大标题包含的所有子标题作为一“组”标题来看的话，后出现的组就不应该有((比[前面组存在的标题]更高一级或几级) 而又 (不存在于前面组))的标题（表达无力还望见谅[捂脸]）</p>

<p>上面这些情况虽然不是很规范但是还是可以生成的，不过下面这些情况就不能存在了：</p>

<pre><code>h2 &gt; h3 &gt; h5 &gt; h1 &gt; h2  
h2 &gt; h5 &gt; h3  
</code></pre>

<p>这个直接越级会导致状态判断混乱的，实际的文章里也不应该出现这种结构吧</p>

<p>你可以在 <a href="https://demos.kingsleyxie.cn/toc-generator/">Demo 页面</a> 尝试设计标题的结构，如果不满足前面提到的“规范”条件的话，会有弹窗提醒，也许能帮助理解这个所谓的 目录结构规范 到底是什么意思</p>

<h5 id="">状态分析</h5>

<p>讨论了这么久目录结构，确定好想法以后就可以考虑怎么写代码了，其实操作就是分三个状态分别进行处理：</p>

<ul>
<li>同级标题（eg. <code>h3 &gt; h3</code>）：直接增加一个元素</li>
<li>进级标题（eg. <code>h3 &gt; h5</code>）：缩进一级作为子元素</li>
<li>退级标题（eg. <code>h5 &gt; h3</code>）：闭合最近的一次缩进，并增加一个元素</li>
</ul>

<p>当然，一次完整的缩进实际上就是增加一对新 <code>&lt;ul&gt;&lt;/ul&gt;</code> 标签，增加元素就是直接增加一对 <code>&lt;li&gt;&lt;/li&gt;</code>啦</p>

<p>要判断状态的话，很明显需要一个值用来存储之前的状态，并在操作之后更新至本次的状态，而它的初始值自然就是第一个 heading 的大小了，判断状态的话可以直接获取当前 heading 的 <code>nodeName</code> 然后通过字符串的 <code>localeCompare()</code> 方法进行对比：</p>

<pre><code class="language-javascript">// Warning: This Version Contains Bug
var TOC = '&lt;div class="toc"&gt;Table Of Contents&lt;ul&gt;';

var elements = $(".post-content").children(":header");  
var currHeading = elements[0].nodeName;

$.each(elements, function(key, content) {
    var text = content.innerText;
    var link = '&lt;a href="#' + text + '"&gt;' + text  + '&lt;/a&gt;';
    content.id = text;

    switch (currHeading.localeCompare(content.nodeName)) {
        case 0:
            TOC += '&lt;/li&gt;&lt;li&gt;' + link;
            break;

        case 1:
            TOC += '&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;' + link;
            currHeading = content.nodeName;
            break;

        case -1:
            TOC += '&lt;ul&gt;&lt;li&gt;' + link;
            currHeading = content.nodeName;
            break;
    }
});

TOC += '&lt;/ul&gt;&lt;/div&gt;';

$(".post-content&gt;p:first").before(TOC);
</code></pre>

<h5 id="debug">Debug</h5>

<h6 id="">多层退级</h6>

<p>这个版本的代码有一个 bug，可以看到在标题退级的时候它是直接闭合最近一次的缩进，但是并不是退级就一定是只退一级，考虑一下这种情况：<code>h1 &gt; h3 &gt; h5 &gt; h1 &gt; h3</code>，它就在第二组直接退了两级回到 <code>h1</code> 了</p>

<p>那怎么解决这个问题呢，很容易发现缩进、闭合缩进的过程其实和栈操作的入栈、出栈非常像，也就自然会想到用栈来完成它的实现，在标题退级时重复闭合缩进，直到栈顶元素和当前元素相同，把 <code>switch</code> 的片段改成这样：</p>

<pre><code class="language-javascript">var records = new Array();  
switch (currHeading.localeCompare(content.nodeName)) {  
    case 0:
        TOC += '&lt;/li&gt;&lt;li&gt;' + link;
        break;

    case 1:
        currHeading = content.nodeName;
        while (records.includes(currHeading)) {
            TOC += '&lt;/li&gt;&lt;/ul&gt;';
            records.pop();
        }
        TOC += '&lt;li&gt;' + link;
        break;

    case -1:
        TOC += '&lt;ul&gt;&lt;li&gt;' + link;
        records.push(currHeading);
        currHeading = content.nodeName;
        break;
}
</code></pre>

<h6 id="heading">heading 过滤</h6>

<p>其实并不是页面中的所有 heading 都应该被选择到，比如引用区块 <code>&lt;blockquote&gt;</code> 里的就应该过滤掉，这个用 jQuery 解决就很方便了，它直接提供了一个 <code>filter</code> 方法：</p>

<pre><code class="language-javascript">var elements =  
    $(".post-content").find(":header")
    .filter(":not(blockquote :header)");
</code></pre>

<p>至于不用 jQuery 怎么实现会在后面提及</p>

<h3 id="">测试页面</h3>

<p>为了方便多做一些测试，直接写了一个测试页面，基本的代码就没必要贴了，只是根据选择的 heading 给区块增加一个对应大小的标题，非要说值得讲的东西，有以下两点</p>

<ol>
<li><p>结构限制：点击生成目录的时候一般都不会考虑到结构规范的问题，所以要对增加元素的操作加个限制，原理和生成目录是一样的，也是用一个栈来判断状态，然后确定当前操作是否符合规范</p></li>
<li><p>随机字符串：为了更好区分各个区块，可以用一串定长的随机字符串作为标题元素的文本，直接用 JS 代码 <code>Math.random().toString(36).substr(2, 11)</code> 生成就可以了</p></li>
</ol>

<h3 id="">实现拓展需求</h3>

<h5 id="">响应点击事件</h5>

<p>要实现对点击事件的响应并不难，这里主要的问题还是怎么样转变收和放两个状态，在写完 <code>link-boxes</code> 之后就一直觉得样式的更改应该尽可能用 CSS 而不是 JS 来完成，所以就通过两个不同的 class 的转换实现了这个需求，代码还是不放上来了，如果有兴趣的话欢迎直接到 Repo 上查看，这个实现主要关联到两个文件：<a href="https://github.com/KingsleyXie/TOC-Generator/blob/master/toc-assets/toc.css"><code>assets/toc.css</code></a> 和 <a href="https://github.com/KingsleyXie/TOC-Generator/blob/master/toc-generator.js#L93"><code>toc-generator.js</code></a> </p>

<p>觉得有必要提的一个是，为了保证用不用 jQuery 的版本效果都一样，在收放的时候就没有用 <code>show(t)</code> 和 <code>hide(t)</code> 函数，而是用 CSS 的 <code>transition</code> 配合 class 更改完成的：</p>

<pre><code class="language-css">.toc-content {
    overflow: hidden;
    transition: all .7s;
}
.toc-off .toc-content {
    margin: 0;
    height: 0;
    opacity: 0;
}
</code></pre>

<p>在 <code>toc-off</code> 这个 class 改变的时候，<code>toc-content</code> 就会在 <code>0.7s</code> 内完成下面那些值的过渡，也就基本是出现和消失效果了，虽然这样的做法和 jQuery 的那两个函数还是有差距，但是我也想不到别的更好的实现方式，能用这几行代码解决掉收放效果的问题已经很满足了 hhh</p>

<h5 id="toc"><code>[TOC]</code> 占位符</h5>

<p>考虑到不能对页面内容有太大影响，只对文章内容的第一个段落做判断，如果它的文本正好是 <code>[TOC]</code> 的话，就把它删掉然后在这个位置生成目录，这样如果没有写这个占位符的话就不会生成目录了，代码改变也比较简单：</p>

<pre><code class="language-javascript">if ((wrapper.length == 1) &amp;&amp;  
    ($(config.contentWrapper + "&gt;:first-child").text() == '[TOC]')) {
    $(config.contentWrapper + "&gt;:first-child").remove();
    // Other TOC Generator Code
}
</code></pre>

<h3 id="js">原生 JS 实现</h3>

<p>因为代码里面其实用到 jQuery 的部分并不多，感觉加载整个文件实在太浪费了，后来就尝试了一下用原生 JavaScript 重写</p>

<p>首先……2333 在做测试页面的时候并没有一开始就写这个版本的代码，所以就临时写了个加载 BootCDN 的 jQuery 文件的函数：</p>

<pre><code class="language-javascript">function loadJQuery() {  
    var ele = document.createElement("script");
    ele.type = 'text/javascript';
    ele.src = 'https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js';
    document.getElementsByTagName("head")[0].appendChild(ele);
}
</code></pre>

<p>不直接用 HTML 加载是因为当时我在网上找一堆别人的页面测试的时候发现有些网站确实是没有加载 jQuery 的，那要强行给它们加上的话，当然是直接通过控制台添加最方便了hhh</p>

<p>好了这是题外话，重写的话其实工作量也不大，代码文件在 <a href="https://github.com/KingsleyXie/TOC-Generator/blob/master/non-jquery-version.js">这里</a>，略值得提及的可能也就这种：</p>

<pre><code class="language-javascript">$.each(elements, function(key, content) {});
$(config.contentWrapper + "&gt;:first-child").before(TOC);
</code></pre>

<p>对应改成：</p>

<pre><code class="language-javascript">elements.forEach(function(content) {});  
document.querySelector(config.contentWrapper)  
.children[0].insertAdjacentHTML('beforebegin', TOC);
</code></pre>

<p>其他的更改都非常简单，只有一个 heading 的选择器有点麻烦，因为需要把 <code>blockquote</code> 里的 heading 过滤掉，前面有提到 jQuery 提供了 <code>filter</code> 方法，但是原生的 JS 是不支持的，而 <code>querySelectorAll</code> 选择器获得的 <code>NodeList</code> 又不能直接进行过滤，我查了蛮久资料，最后虽然实现了同样的功能，但是……代码有点乱，而且整个实现看起来是有点糟糕的 2333：</p>

<pre><code class="language-javascript">var wrapper = document.querySelectorAll(config.contentWrapper);

var elements = Array.prototype.filter.call(  
    wrapper[0].querySelectorAll("h1,h2,h3,h4,h5,h6"),
    function(ele) {
        var result = true;

        document.querySelectorAll("blockquote")
        .forEach(function(bq) {
            bq.querySelectorAll("h1,h2,h3,h4,h5,h6")
            .forEach(function(v) {
                if (ele == v) result = false;
            });
        });

        return result;
    }
);
</code></pre>

<p>本来如果是 <code>querySelector</code> 的话还会稍微简单一点，但是要保证不漏掉的话就得用 <code>querySelectorAll</code> 了，然后要分别对它的各个子元素进行操作，也就是需要调用 <code>forEach</code> 方法</p>

<p>还是稍微解释一下这段代码吧……先看个简单的例子：</p>

<pre><code class="language-javascript">a = [1, 2, 3];  
Array.prototype.filter.call(  
    a, function(e) {
        return e != 2;
    }
);
</code></pre>

<p>这段代码的执行结果是 <code>[1, 3]</code>，所以作用就比较容易理解了，就是根据函数返回的布尔值真假决定第一个参数对应元素的取舍，再看回之前那份代码，<code>wrapper[0].querySelectorAll("h1,h2,h3,h4,h5,h6")</code> 选出了文章内所有的 heading，然后后面的函数选出 <code>blockquote</code> 内的所有 heading，如果发现有相同的元素则进行过滤</p>

<p>说这个实现比较糟糕是因为它对每一个 heading 元素进行对比过滤时，都会重新遍历一遍<strong>所有</strong>引用区块内的<strong>所有</strong> heading，这个冗余很大了，但是我又没找到别的解决方案，所以就将就着用了<del>，反正其实一般也感觉不到什么区别 至少功能是没问题了</del></p>

<h3 id="">部署到博客</h3>

<p>意料之中的吔屎，不过还好也都是些小问题，没有耗费太多时间</p>

<h5 id="">加载问题</h5>

<p>这个不能像 <code>link-boxes</code> 一样只给需要用到的文章直接加上代码，目录生成的需求还是比较频繁的，所以就到服务器去改模板文件 <code>post.hbs</code> 了，那要加载代码的话，对于这种不是单文件的东西直接插入代码肯定是不现实的，何况还有两张小图标，不过还好我有配置子域名可以拿来放一些自己的文件，所以就[捂脸]强行解决了，改好之后重启一下 Ghost 就行，顺便把 <code>link-boxes</code> 的文件也都放到子域名下方便加载了</p>

<h5 id="jquery">jQuery 不能用</h5>

<p>我本来是把带 jQuery 的版本作为主版本的，因为 Ghost 本身也有用到它，但是改完模板文件才发现这个文件的加载在另一个模板里，而那个模板是把 <code>post.hbs</code> 放在中间部分也就是加载 jQuery 之前的，这就导致我得把 JS 文件改成不带 jQuery 的版本了</p>

<p>还好两个版本都有写23333，要不然这个时候发现没法用就真的彻底吔屎了</p>

<p>改完之后也就把原生 JS 实现的版本换成主版本了，就像前面说的，为了一点点功能加载一个不算小的文件没有必要</p>

<h3 id="">最后</h3>

<p>哇本来打算提取出来更新一个小版本的，没想到竟然也改了这么多代码hhh，最后放到博客上看到一切正常还是特别开心的，然后花了点时间把 <a href="https://demos.kingsleyxie.cn/toc-generator/">Demo 页面</a> 也放到了服务器上，jQuery 版本的 Demo 在 <a href="https://demos.kingsleyxie.cn/toc-generator/jquery-version.html">这里</a></p>

<p>和前面的那个链接卡片盒一样，代码原来是一起在一个 Repo 里的，后来为了方便维护还是单独拿出来了：<a href="https://github.com/KingsleyXie/TOC-Generator">TOC-Generator</a></p>]]></content:encoded></item><item><title><![CDATA[友情链接]]></title><description><![CDATA[<p>Updating &amp; To Be Continued</p>

<p>（排名不分先后）</p>

<div class="link-boxes">  
    <a href="https://inohn.me" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/iNohn.jpg" alt="iNohn.jpg"></div>
        <div class="nickname">iNohn's Home</div>
    </a>

    <a href="https://ruansongsong.github.io/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/SongSong.jpg" alt="SongSong.jpg"></div>
        <div class="nickname">Songsong’s Blog</div>
    </a>

    <a href="https://withcic.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/CIC.png" alt="CIC.png"></div>
        <div class="nickname">CIC</div>
    </a>

    <a href="http://www.dzglalala.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/dddzg.jpg" alt="dddzg.jpg"></div>
        <div class="nickname">dddzg</div>
    </a>

    <a href="https://gayhub.cn/blog/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/SunDoge.jpg" alt="SunDoge.jpg"></div>
        <div class="nickname">SunDoge's Blog</div>
    </a>

    <a href="https://myafei.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/frankie.ico" alt="frankie.ico"></div>
        <div class="nickname">Frankie's Blog</div>
    </a>

    <a href="https://i-meto.com" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/METO.jpeg" alt="METO.jpeg"></div>
        <div class="nickname">萨摩公园</div>
    </a>

    <a href="http://zhuyeye.cn" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/zhuyeye.jpg" alt="zhuyeye.jpg"></div>
        <div class="nickname">槿叶轩</div>
    </a>

    <a href="http://www.mayining.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/mayining.jpg" alt="mayining.jpg"></div>
        <div class="nickname">一林的私人Studio</div>
    </a>

    <a href="https://bokjan.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Bokjan.jpg" alt="Bokjan.jpg"></div>
        <div class="nickname">Bokjan's</div>
    </a>

    <a href="https://delbertbeta.cc/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/delbertbeta.jpeg" alt="delbertbeta.jpeg"></div>
        <div class="nickname">~Prime Number~</div>
    </a>

    <a href="https://sticnarf.me/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/sticnarf.jpg" alt="sticnarf.jpg"></div>
        <div class="nickname">sticnarf</div>
    </a>

    <a href="https://hexo.xinsane.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/xinsane.svg" alt="xinsane.svg"></div>
        <div class="nickname">梦的天空之城</div>
    </a>

    <a href="https://yxz.me" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/YXZ.gif" alt="YXZ.gif"></div>
        <div class="nickname">YXZ'S BLOG</div>
    </a>

    <a href="http://jefung.cn/blog/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Jefung.jpeg" alt="Jefung.jpeg"></div>
        <div class="nickname">Jefung's Blog</div>
    </a>

    <a href="https://mental2008.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Mental.jpg" alt="Mental.jpg"></div>
        <div class="nickname">Mental's Blog</div>
    </a>

    <a href="http://blog.csdn.net/hsj970319" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Sega_hsj.jpg" alt="Sega_hsj.jpg"></div>
        <div class="nickname">Sega_hsj's Blog</div>
    </a>

    <a href="http://wendyli.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/WendyLi.jpg" alt="WendyLi.jpg"></div>
        <div class="nickname">WendyLi's Space</div>
    </a>

    <a href="http://ghyer.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/GHY.jpg" alt="GHY.jpg"></div>
        <div class="nickname">高弟</div>
    </a>

    <a href="http://www.caibf.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/CBF.jpg" alt="CBF.jpg"></div>
        <div class="nickname">CBF</div>
    </a>

    <a href="http://sforest.in/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/SJoshua.jpg" alt="SJoshua.jpg"></div>
        <div class="nickname">SJoshua</div>
    </a>
</div>

<p>有关本页面的相关信息，欢迎参考前一篇博客 <a href="http://kingsleyxie.cn/friends/../implementation-of-link-boxes">友链页面中链接卡片盒（link-boxes）的实现</a></p>

<p><link rel="stylesheet" href="https://projects.kingsleyxie.cn/static/link-boxes/link-boxes.css"></p>]]></description><link>http://kingsleyxie.cn/friends/</link><guid isPermaLink="false">da663ecd-b0ce-47d3-9855-66fb3992540a</guid><category><![CDATA[About]]></category><dc:creator><![CDATA[Kingsley]]></dc:creator><pubDate>Mon, 01 Jan 2018 13:37:00 GMT</pubDate><content:encoded><![CDATA[<p>Updating &amp; To Be Continued</p>

<p>（排名不分先后）</p>

<div class="link-boxes">  
    <a href="https://inohn.me" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/iNohn.jpg" alt="iNohn.jpg"></div>
        <div class="nickname">iNohn's Home</div>
    </a>

    <a href="https://ruansongsong.github.io/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/SongSong.jpg" alt="SongSong.jpg"></div>
        <div class="nickname">Songsong’s Blog</div>
    </a>

    <a href="https://withcic.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/CIC.png" alt="CIC.png"></div>
        <div class="nickname">CIC</div>
    </a>

    <a href="http://www.dzglalala.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/dddzg.jpg" alt="dddzg.jpg"></div>
        <div class="nickname">dddzg</div>
    </a>

    <a href="https://gayhub.cn/blog/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/SunDoge.jpg" alt="SunDoge.jpg"></div>
        <div class="nickname">SunDoge's Blog</div>
    </a>

    <a href="https://myafei.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/frankie.ico" alt="frankie.ico"></div>
        <div class="nickname">Frankie's Blog</div>
    </a>

    <a href="https://i-meto.com" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/METO.jpeg" alt="METO.jpeg"></div>
        <div class="nickname">萨摩公园</div>
    </a>

    <a href="http://zhuyeye.cn" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/zhuyeye.jpg" alt="zhuyeye.jpg"></div>
        <div class="nickname">槿叶轩</div>
    </a>

    <a href="http://www.mayining.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/mayining.jpg" alt="mayining.jpg"></div>
        <div class="nickname">一林的私人Studio</div>
    </a>

    <a href="https://bokjan.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Bokjan.jpg" alt="Bokjan.jpg"></div>
        <div class="nickname">Bokjan's</div>
    </a>

    <a href="https://delbertbeta.cc/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/delbertbeta.jpeg" alt="delbertbeta.jpeg"></div>
        <div class="nickname">~Prime Number~</div>
    </a>

    <a href="https://sticnarf.me/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/sticnarf.jpg" alt="sticnarf.jpg"></div>
        <div class="nickname">sticnarf</div>
    </a>

    <a href="https://hexo.xinsane.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/xinsane.svg" alt="xinsane.svg"></div>
        <div class="nickname">梦的天空之城</div>
    </a>

    <a href="https://yxz.me" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/YXZ.gif" alt="YXZ.gif"></div>
        <div class="nickname">YXZ'S BLOG</div>
    </a>

    <a href="http://jefung.cn/blog/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Jefung.jpeg" alt="Jefung.jpeg"></div>
        <div class="nickname">Jefung's Blog</div>
    </a>

    <a href="https://mental2008.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Mental.jpg" alt="Mental.jpg"></div>
        <div class="nickname">Mental's Blog</div>
    </a>

    <a href="http://blog.csdn.net/hsj970319" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/Sega_hsj.jpg" alt="Sega_hsj.jpg"></div>
        <div class="nickname">Sega_hsj's Blog</div>
    </a>

    <a href="http://wendyli.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/WendyLi.jpg" alt="WendyLi.jpg"></div>
        <div class="nickname">WendyLi's Space</div>
    </a>

    <a href="http://ghyer.com/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/GHY.jpg" alt="GHY.jpg"></div>
        <div class="nickname">高弟</div>
    </a>

    <a href="http://www.caibf.cn/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/CBF.jpg" alt="CBF.jpg"></div>
        <div class="nickname">CBF</div>
    </a>

    <a href="http://sforest.in/" class="link-box">
        <div class="avatar"><img src="http://kingsleyxie.cn/content/images/friends/SJoshua.jpg" alt="SJoshua.jpg"></div>
        <div class="nickname">SJoshua</div>
    </a>
</div>

<p>有关本页面的相关信息，欢迎参考前一篇博客 <a href="http://kingsleyxie.cn/friends/../implementation-of-link-boxes">友链页面中链接卡片盒（link-boxes）的实现</a></p>

<p><link rel="stylesheet" href="https://projects.kingsleyxie.cn/static/link-boxes/link-boxes.css"></p>]]></content:encoded></item></channel></rss>