-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 452 KB
/
index.json
1
[{"location":"https://codenow.me/","text":"","title":"ATTS Group"},{"location":"https://codenow.me/articles/","text":"","title":"文章"},{"location":"https://codenow.me/algorithm/","text":"","title":"算法"},{"location":"https://codenow.me/translation/","text":"","title":"翻译"},{"location":"https://codenow.me/tips/","text":"","title":"Tips"},{"location":"https://codenow.me/rules/","text":"","title":"玩法"},{"location":"https://codenow.me/joinus/","text":"","title":"加入我们"},{"location":"https://codenow.me/articles/learning_the_shell_03/","text":" 变量 变量包括\n 变量名 变量值 本地变量\n 在当前系统某个环境才生效的变量,作用范围小。 包括普通变量和命令变量。 普通变量定义方式\n 变量名=变量值(变量值必须是一个整体,中间无特殊字符,=两边无空格) 变量名=\u0026lsquo;变量值\u0026rsquo;(看到什么输出什么) 变量名=\u0026ldquo;变量值\u0026rdquo;(变量值范围内有可以解析的变量 A,将 A 的结果和其余内容组合成整体重新赋值给变量 B) 注意:数字一般不加引号,其它默认加双引号。\n 命令变量定义方式\n 变量名=命令 变量名=$(命令) 执行流程:\n 执行反引号或者 $() 里的命令 将命令执行后的结果赋值给新的变量 全局变量\n 当前 Shell 以及其派生出来的子 Shell 中都有效的变量。 全局变量定义方式\n 变量=值, export 变量 export 变量=值 查看全局变量\nenv 查看变量\n $变量名 \u0026rdquo;$变量名\u0026rdquo; ${变量名} \u0026rdquo;${变量名}\u0026rdquo; 取消变量\n unset 变量名 注意: 想让全局变量在任何的命令行中有效,需要修改 /etc/profile 文件, 比如 export name=\u0026quot;wang\u0026quot;追加到文件末尾, 然后在某个命令行执行 source /etc/profile, 则 name 全局变量在当前窗口有效,其他窗口还是没效果。 重启系统,则所有的窗口都有 name 全局变量,对所有用户有效。 用户目录下的 .bashrc,里面的变量只对某个用户有效,是当前用户的全局变量。 而 /etc/profile,里面的变量对所有用户有效。 ","title":"Shell 入门 03"},{"location":"https://codenow.me/translation/howtocontributetoopensource02/","text":" 四、寻找你想要贡献的项目 如果你没有过开源项目的经验,不妨参考一下美国前总统肯尼迪的一些建议,他曾经说过,“不要问你的国家可以为你做什么,问一下你可以为你的国家做什么”。\n开源贡献可以在项目的任何阶段,贯穿整个项目始终。 你不必担心太多你的第一个贡献是怎么样的,看起来是怎么样的。 相反,你应该回顾一下你用过的项目和你想要用的项目。你将会发现你热衷于贡献的项目会是你经常使用的项目。\n在那些项目里面,当你觉得某个地方某个东西可以更好或者更不一样时,按照你的直觉做。\n开源项目并不是一个私人俱乐部,它是由像你这样的人组成的。“开源项目”只是一个特别的术语,把世界上的问题当成可修复的来看待的。\n你可能在浏览 README 文件的时候发现了一个打不开的链接或一个错字。或者你是一个新用户,然后你发现有些地方走不通,或者有个问题需要加进文档里。 与其忽略它不理不顾,不如叫人来修复它,看看你能不能帮上什么忙。这就是开源项目的意义所在。\n有 28% 的开源贡献都是文档类型,比如修改错字,调整格式或翻译文档。\n下面的资源也许能够帮助你发现你新的开源项目:\n GitHub Explore Open Source Friday First Timers Only CodeTriage 24 Pull Requests Up For Grabs Contributor-ninja First Contributions 评估清单 当你发现了一个你想要贡献的项目,看看这个项目是不是接受贡献者,对此做一个快速的判断。否则,你的辛勤工作可能就得不到任何回复了。 这里有一个简单的清单帮助评估项目是否适合新的贡献者。\n是否满足开源项目的定义\n是否有许可证?一般在仓库的根目录里有个叫 LICENSE 的文件 项目是否活跃地接受贡献\n看一下主分支里的提交活动。在 GitHub 里,你可以在仓库的主页面里看到相关的信息。\n上一个提交是什么时候? 这个项目有多少个贡献者? 多久有人提交一次?(在 github 里,你可以点击顶部的 Commits 查看) 下一步,看看这个项目的 issues。\n有多少个公开 issues? 维护者对这些公开 issues 回复得快吗? 在 issues 里是否有积极地讨论? 这些 issues 是近期的吗? 有已关闭的 issues 吗?(在 Github 里,在 issues 页面点击 closed 可以看到关闭的 issues) 现在看看项目的 pull requests\n有多少公开的 pull requests? 维护者对这些公开 pull requests 回复得快吗? 在 pull requests 里是否有积极地讨论? 这些 pull requests 是近期的吗? 最近 pull requests 被合并的时候是几时?(在 Github 里,在 pull requests 页面点击 closed 可以看到关闭的 pull requests) 项目是否热情\n一个友好且热情的项目意味着他们乐意接受新的贡献者\n维护者是否对 issues 里的问题有有效的回复 在 issues、讨论论坛或者聊天软件里(像 IRC 或 Slack)的人是否友好? pull requests 有没有得到审核? 维护者有没有感谢人们的贡献? 五、怎样提交一个贡献 你发现了你喜欢的项目,你也做好了贡献的准备。 终于到这一步了,我们将教你正确的进行贡献。\n高效的沟通 无论你是一个一次性贡献者还是打算加入社区,与他人合作将会是你在开源项目里得到锻炼的重要能力之一。 在你打开一个 issue,pull request 或在聊天里问问题之前,记住这些要点,帮助你的想法被有效的被理解。\n给出信息\n帮助别人加快速度。如果你遇到了一个错误,解释一下你在尝试做什么,怎样重现错误。如果你在给出建议,解释一下你的这个想法会对这个项目有什么帮助。\n Good: 我做了 Y 的时候 X 没有出现\nBad: X 坏了。请修复它!\n 提前做功课\n你可以不懂,但是要表明你至少努力过。在寻求帮助之前,先看看项目的 README、文档、issues、邮件列表,以及在网络上搜索答案。当你证明你已经努力过之后,人们会对你更欣赏。\n Good: 我不知道怎么实现 X。我找了帮助文档但是没有得到任何信息。\nBad: 我要怎么实现 X?\n 保持请求简短明了\n像发一封邮件,每一个贡献,无论多简单或多有帮助,都需要有人审核。很多项目收到的请求比人们能够提供帮助的更多。把请求写的简短明了一点,你得到帮助的几率更大。\n Good: 我想要写 API 教程\nBad: 有一天我在高速路上开车然后停下来加油的时候,我有一个关于我们项目很棒的想法,在我讲它之前我先给你看看这个…\n 保持所有沟通公开\n尽管很有诱惑性,不要尝试私下去接触维护者,除非是要分享一些敏感信息(比如安全问题或者严重违反什么行为)。当你把对话公开的时候,人们或许会从对话交流中学习到受益到什么东西。交谈本身就可以是贡献。\n Good: (评论)@xx维护者,你好,我们在这个 pull request 上要怎么进展?\nBad: (邮件)你好,通过邮件联系你真是抱歉,但是我希望你可以审核一下我的 pull request\n 你可以问问题,但是要有耐心\n某个程度上大家都是新人,甚至有经验的贡献者看新的项目的时候也需要花点时间。同样就算是长期维护者也不是对项目的每一个部分都熟悉。展示你希望他们也如此对待你的耐心。\n Good: 谢谢你帮我看这个错误,我按你说的做了,这个是输出结果。\nBad: 为什么你没有解决我的问题,这个不是你的项目吗?\n 尊重社区的决定\n你的想法或许和社区的优先级或愿景有所不同。他们也许会反馈给你或者也选择不理你的建议。尽管你应该讨论并寻求妥协,但维护者通常会需要更多处理你的建议的时间\n Good: 虽然我很失望你不能支持我的用例,但是我理解你所解释的只有一小部分用户会受到影响。谢谢你的聆听。\nBad: 为什么不支持我的用例!我无法接受!\n 最重要的是保持优雅\n开源项目是由世界各地的贡献者组成。在沟通的过程中因为语言、文化、地理和时区的不同有可能会产生信息语境的丢失。另外,书面文字无法传达语调和情绪。在沟通中都要假设对方的意图是好的。礼貌的拒绝一个建议、寻求更多的信息或者更进一步阐述你的立场这些都不错。试着让互联网变得更好吧!\n收集信息 在开始之前,快速检查一下确保你的想法之前是否已经被提出并讨论过了的。大概浏览一下 README、issues、邮件列表和 StackOverflow。 你不需要花几个小时把所有东西都过一遍,可以用关键术语做一个快速搜索看看。 如果你在其它地方没有发现你的想法,你可以进行下一步了。如果项目是在 Github 的,你可以通过开一个 issue 或 pull request 沟通:\n Issues 就像开始一段对话或讨论 Pull requests 用于开始解决问题 轻量级沟通,像阐述或如何提问,试一下在 StackOverflow,IRC,Slack 或其它聊天工具上进行。看看项目拥有哪个渠道。 在你打开一个 issue 或 pull request 之前,检查一下项目的贡献文档(通常是在一个叫 Contributing 的文件或在 README 里),看下你是否需要指定什么具体内容。比如,你可能需要遵循一个模版的格式或者你需要用测试。 如果你准备做一个比较大的贡献,在开始之前先打开一个 issue 去问一下。在你做可能不被通过的工作之前,观察一个项目一段时间通常会更好(在 Github 里,点击 watch 将会收到所有对话的提醒),了解一下社区成员。\n打开一个 issue 在以下情况你应该开一个 issue:\n 报告你不能自己解决的错误 讨论一个高层级的话题或想法(比如,社区、愿景或政策) 提出一个新功能或其它项目想法 在 issues 上沟通的 tips:\n 如果你在一个公开 issue 上看到你想解决的 issue,留言让别人知道你已经在解决这个 issue,这样可以减少多人重复工作的概率 如果一个 issue 是比较早之前打开的,很有可能 issue 已经有人在解决或者已经被解决了。在开始工作之前留言确认一下。 如果你开了一个 issue,后来自己有找到答案了,留言让别人知道,并关闭 issue。即使是记录结果也是对项目的贡献。 打开一个 pull request 在以下情况你应该开一个 pull request:\n 提交小修复(比如,改正错字、修复打不开的链接或一个明显的错误) 开始处理已经在 issue 里被要求或被讨论过的问题 一个 pull request 不代表着必须完成工作。通常越早打开 pull request 越好,这样别人可以观看并对你的进度给出反馈。你可以在主题行中标记它为 “WIP”(Work in Progress)。你可以在之后增加更多的 commits。 如果项目在 Github 上,是这样提交一个 pull request 的:\n Fork 一个仓库并把它克隆到本地。通过把原始仓库添加为远程把本地连接到原始的上游仓库。这样当你提交你的 pull request 的时候,上游的更改也会和你保持同步,这样合并冲突的情况会少一点。(更多细节操作可以看这里更多细节操作可以看这里) 为你的修改创建一个新的分支 在你的 pull request 里加上任何有关的 issue 参考或者支持文档(比如,“Closes #37”) 如果你的修改包括 html/css 的差异,请把修改前后的截屏图片附上。把图片拉到你 pull request 的正文里 测试你的修改。把你的修改在已有的测试上都跑一遍,必要的话创建一个新的测试。无论有没有测试,确保你的修改不会破坏现有项目的运行。 尽最大努力把项目的格式维护好。这可能意味着你需要用自己不常用的缩进、分号或注释,但是这样做会让维护者更好的合并项目,别人更容易懂,将来也更容易维护。 如果这是你的第一个 pull request,看一下 Make a Pull Request,这是 @kentcdodds 创建的视频教程。你也可以在 @Roshanjossey 创建的 First Contributions 里多练习一下建一个 pull request。\n六、你提交贡献后会发生什么 当你提交一个贡献之后,下面的情况也许会发生:\n你得不到任何回应 希望你在开始贡献之前检查一下项目的活动信号。不过就算是一个活跃的项目,也有可能你的贡献会得不到反馈。 如果你在一个星期内都得不到回应,在同一个帖子里礼貌的请求别人帮忙审核是合理的。如果你知道谁是审核你贡献的那个人,你可以在帖子里 @ 他。 不要私下接触那个人,记住公开的沟通对开源项目是很重要的。 如果你礼貌请求了还是没有人回应,那也许以后也不会有人回应。这也许不会好受,但是不要让它在阻挡你,每个人都会遇到这样的情况。有很多你没有得到回应的理由,包括超出你控制范围的个人情况。试一下别的开源项目。记得在没有获得社区成员回应的时候,不要做太多的贡献。\n有人请求修改你的贡献 被要求对你的贡献进行修改这个情况很常见。无论是对你的想法的反馈还是修改你的代码。 当有人请求修改,要有响应。毕竟他们花了时间审核你的贡献。开了一个 PR 然后不管不顾这样是一个不好的行为。如果你不知道如何修改,搜索答案,或者寻求帮助。 如果你没有时间处理这个 issue 时(比如,这个对话进行了几个月,你现在的处境有所改变)让维护者知道你的情况,这样他们知道情况了,也许也有新的人可以接任。\n你的贡献没有通过 你的贡献有可能最终不会被通过。希望你没有投放太多的精力在里面。如果你不确定为什么没有被通过,可以向维护者要反馈或解释。无论如何最终你需要尊重这个决定。不要争辩也不要变得敌视。如果你不认同你完全可以自己做一个自己的版本。\n你的贡献被通过了 万岁!你成功做了一个开源贡献!\n七、成功啦 无论你是刚进行了你第一个开源贡献还是在寻找新的贡献方式,希望你得到了我们的鼓舞并行动起来。就算你的贡献最终没有被通过,也不要忘记和维护者为他帮助过你而道谢。开源项目是由像你这样的人组成的,通过一个 issue,pull request,留言和击掌。\n","title":"如何做开源贡献者 02"},{"location":"https://codenow.me/tips/jquery_noconflict/","text":" 描述:\n 前段时间在调用 jQuery 表单验证的一个方法时发现怎么样都调不到。搜索了一下感觉是因为引进的其它第三方库里也用了其它版本的 jQuery。因为存在多个 jQuery 版本,无法识别。\n 解决:\n jQuery.noConflict()\n 作用:\n 让渡 jQuery 控制权 也可以为 jQuery 变量规定新的名字 例子:\n var j = jQuery.noConflict(); // 再用它来调用 jQuery 方法 j.validator.addMethod(); ","title":"jQuery NoConflict()"},{"location":"https://codenow.me/algorithm/leetcode_917_reverse_only_letters/","text":" 题号:917 难度:easy 链接:https://leetcode.com/problems/reverse-only-letters/ 描述:只反转字符串中的字母 class Solution: def reverseOnlyLetters(self, S: str) -\u0026gt; str: # solution 1 # new_list = [] # for s in S[::-1]: # if s.isalpha(): # new_list.append(s) # for i,s in enumerate(S): # if not s.isalpha(): # new_list.insert(i, s) # return \u0026#39;\u0026#39;.join(new_list) # solution 2 S = list(S) left, right = 0, len(S)-1 while left \u0026lt;= right: if S[left].isalpha() and S[right].isalpha(): S[left], S[right] = S[right], S[left] left += 1 right -= 1 elif S[left].isalpha() and (not S[right].isalpha()): right -= 1 else: left += 1 return \u0026#34;\u0026#34;.join(S)","title":"Leetcode_917: Reverse Only Letters"},{"location":"https://codenow.me/translation/an_introduction_to_functional_programming_with_python/","text":" 原文地址:https://julien.danjou.info/python-and-functional-programming/\n许多Python开发人员不知道您可以在多大程度上使用Python中的函数式编程,这是一个遗憾:除了少数例外,函数式编程允许您编写更简洁和高效的代码。此外,Python对函数式编程的支持非常广泛。\n在这里,我想谈一点关于如何真正使用我们最喜欢的语言进行编程的功能性方法。\n纯函数 当您使用函数样式编写代码时,您的函数被设计为没有副作用:相反,它们接受输入并生成输出,而不保留状态或修改返回值中未反映的任何内容。遵循这个理想的函数被称为纯函数函数。\n让我们从一个常规的、非纯函数的示例开始,该函数删除列表中的最后一项:\ndef remove_last_item(mylist): \u0026#34;\u0026#34;\u0026#34;Removes the last item from a list.\u0026#34;\u0026#34;\u0026#34; mylist.pop(-1) # This modifies mylist 此函数不是纯函数:它在修改给定参数时有副作用。让我们将其重写为纯粹的函数:\ndef butlast(mylist): \u0026#34;\u0026#34;\u0026#34;Like butlast in Lisp; returns the list without the last element.\u0026#34;\u0026#34;\u0026#34; return mylist[:-1] # This returns a copy of mylist 我们定义了一个butlast()函数(如lisp中的butlast),该函数不修改原始列表而返回最后一个元素的列表。相反,它会返回一份已进行修改的列表副本,允许我们保留原始列表。使用函数式编程的实际优势包括:\n 模块性。以功能性的风格写作,在一定程度上分离在解决你的个别问题和使代码部分在其他环境中更容易重用。因为函数不依赖于任何外部变量或状态,从另一段代码调用它是直截了当。\n 简洁。函数式编程通常比其他范式更不冗长。\n 并发性。纯函数函数是线程安全的,可以运行同时地。有些函数语言会自动执行此操作,可以如果您需要扩展应用程序,这是一个很大的帮助,尽管这不是在python中是这样的。\n 可测试性。测试一个功能性程序非常简单:你所需要的一切是一组输入和一组预期输出。它们是等幂的,意思是用相同的参数反复调用相同的函数将始终返回相同的结果。\n 注意,Python中的列表理解等概念在其方法中已经具有了功能,因为它们的设计是为了避免副作用。下面我们将看到,python提供的一些功能函数实际上可以表示为列表理解!\npython函数式函数 在使用函数式编程操作数据时,您可能会反复遇到相同的一组问题。为了帮助您有效地处理这种情况,python包含了许多用于函数编程的函数。在这里,我们将快速概述一些内置函数,这些函数允许您构建完全功能的程序。一旦您了解了什么是可用的,我鼓励您进一步研究并尝试在您自己的代码中可能应用的函数。\n将函数应用于带有映射的项 map()函数采用形式map(function,iterable),并对iterable中的每个项应用函数,以返回iterable映射对象:\n\u0026gt;\u0026gt;\u0026gt; map(lambda x: x + \u0026#34;bzz!\u0026#34;, [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;]) \u0026lt;map object at 0x7fe7101abdd0\u0026gt; \u0026gt;\u0026gt;\u0026gt; list(map(lambda x: x + \u0026#34;bzz!\u0026#34;, [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;])) [\u0026#39;I thinkbzz!\u0026#39;, \u0026#34;I\u0026#39;m goodbzz!\u0026#34;] 您还可以使用列表理解编写一个相当于map()的函数,其中\n如下所示:\n\u0026gt;\u0026gt;\u0026gt; (x + \u0026#34;bzz!\u0026#34; for x in [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;]) \u0026lt;generator object \u0026lt;genexpr\u0026gt; at 0x7f9a0d697dc0\u0026gt; \u0026gt;\u0026gt;\u0026gt; [x + \u0026#34;bzz!\u0026#34; for x in [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;]] [\u0026#39;I thinkbzz!\u0026#39;, \u0026#34;I\u0026#39;m goodbzz!\u0026#34;] 用筛选器筛选列表 filter()函数接受表单筛选器(function or none,iterable),并根据函数返回的结果筛选iterable中的项。这将返回ITerable筛选器对象:\n\u0026gt;\u0026gt;\u0026gt; filter(lambda x: x.startswith(\u0026#34;I \u0026#34;), [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;]) \u0026lt;filter object at 0x7f9a0d636dd0\u0026gt; \u0026gt;\u0026gt;\u0026gt; list(filter(lambda x: x.startswith(\u0026#34;I \u0026#34;), [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;])) [\u0026#39;I think\u0026#39;] 您还可以使用列表理解编写一个相当于filter()的函数,比如:\n\u0026gt;\u0026gt;\u0026gt; (x for x in [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;] if x.startswith(\u0026#34;I \u0026#34;)) \u0026lt;generator object \u0026lt;genexpr\u0026gt; at 0x7f9a0d697dc0\u0026gt; \u0026gt;\u0026gt;\u0026gt; [x for x in [\u0026#34;I think\u0026#34;, \u0026#34;I\u0026#39;m good\u0026#34;] if x.startswith(\u0026#34;I \u0026#34;)] [\u0026#39;I think\u0026#39;] 使用枚举获取索引 enumerate()函数的形式为enumerate(iterable[,start]),并返回一个iterable对象,该对象提供一系列元组,每个元组由一个整数索引(从start开始,如果提供)和iterable中的相应项组成。当需要编写引用数组索引的代码时,此函数非常有用。例如,不要写:\ni = 0 while i \u0026lt; len(mylist): print(\u0026#34;Item %d: %s\u0026#34; % (i, mylist[i])) i += 1 使用enumerate()可以更有效地完成相同的任务,如:\nfor i, item in enumerate(mylist): print(\u0026#34;Item %d: %s\u0026#34; % (i, item)) 对列表进行排序 sorted()函数采用排序的形式(iterable,key=none,reverse=false),并返回iterable的排序版本。key参数允许您提供返回要排序的值的函数:\n\u0026gt;\u0026gt;\u0026gt; sorted([(\u0026#34;a\u0026#34;, 2), (\u0026#34;c\u0026#34;, 1), (\u0026#34;d\u0026#34;, 4)]) [(\u0026#39;a\u0026#39;, 2), (\u0026#39;c\u0026#39;, 1), (\u0026#39;d\u0026#39;, 4)] \u0026gt;\u0026gt;\u0026gt; sorted([(\u0026#34;a\u0026#34;, 2), (\u0026#34;c\u0026#34;, 1), (\u0026#34;d\u0026#34;, 4)], key=lambda x: x[1]) [(\u0026#39;c\u0026#39;, 1), (\u0026#39;a\u0026#39;, 2), (\u0026#39;d\u0026#39;, 4)] 查找满足任何和所有条件的项 any(iterable)和all(iterable)函数都根据iterable返回的值返回布尔值。这些简单函数相当于以下完整的python代码:\ndef all(iterable): for x in iterable: if not x: return False return True def any(iterable): for x in iterable: if x: return True return False 这些函数用于检查iterable中的任何或所有值是否满足给定条件。例如,下面检查了两个条件的列表:\nmylist = [0, 1, 3, -1] if all(map(lambda x: x \u0026gt; 0, mylist)): print(\u0026#34;All items are greater than 0\u0026#34;) if any(map(lambda x: x \u0026gt; 0, mylist)): print(\u0026#34;At least one item is greater than 0\u0026#34;) 这里的关键区别在于,您可以看到,当至少一个元素满足条件时,any()返回true,而all()仅当每个元素满足条件时返回true。对于空的iterable,all()函数也将返回true,因为所有元素都不是false。\n将列表与zip组合 函数的形式是zip(iter1[,iter2[…]),它接受多个序列并将它们组合成元组。当需要将键列表和值列表组合到dict中时,这很有用。与这里描述的其他函数一样,zip()返回iterable。这里有一个键列表,我们映射到值列表以创建字典:\n\u0026gt;\u0026gt;\u0026gt; keys = [\u0026#34;foobar\u0026#34;, \u0026#34;barzz\u0026#34;, \u0026#34;ba!\u0026#34;] \u0026gt;\u0026gt;\u0026gt; map(len, keys) \u0026lt;map object at 0x7fc1686100d0\u0026gt; \u0026gt;\u0026gt;\u0026gt; zip(keys, map(len, keys)) \u0026lt;zip object at 0x7fc16860d440\u0026gt; \u0026gt;\u0026gt;\u0026gt; list(zip(keys, map(len, keys))) [(\u0026#39;foobar\u0026#39;, 6), (\u0026#39;barzz\u0026#39;, 5), (\u0026#39;ba!\u0026#39;, 3)] \u0026gt;\u0026gt;\u0026gt; dict(zip(keys, map(len, keys))) {\u0026#39;foobar\u0026#39;: 6, \u0026#39;barzz\u0026#39;: 5, \u0026#39;ba!\u0026#39;: 3} 下一步是什么? 虽然Python经常被宣传为面向对象的,但它可以以非常实用的方式使用。它的许多内置概念,如生成器和列表理解,都是面向功能的,不会与面向对象的方法冲突。python提供了一组内置函数,可以帮助您保持代码不受任何副作用的影响。这也限制了对一个项目的全球状态的依赖,为了你自己的利益。\n","title":"Python的函数式编程"},{"location":"https://codenow.me/algorithm/binary_tree_using_python/","text":"用Python方式实现二叉树\n测试数据1:\n 7 8 9 测试数据2:\n 7 8 9 23 36 57 58 #! /usr/bin/python class BiTNode: def __init__(self, left=0, right=0, data=0): self.left = left self.data = data self.right = right class BinaryTree: def __init__(self, base): self.base = base def is_empty(self): if self.base == 0: return True else: return False def pre_order_traversal(self, jd): if jd == 0: return print(jd.data) self.pre_order_traversal(jd.left) self.pre_order_traversal(jd.right) def in_order_traversal(self, jd): if jd == 0: return self.in_order_traversal(jd.left) print(jd.data) self.in_order_traversal(jd.right) def post_order_traversal(self, jd): if jd == 0: return self.post_order_traversal(jd.left) self.post_order_traversal(jd.right) print(jd.data) if __name__ == \u0026#39;__main__\u0026#39;: jd1 = BiTNode(data=8) jd2 = BiTNode(data=9) base = BiTNode(jd1, jd2, 7) x = BinaryTree(base) print(\u0026#34;pre_order\u0026#34;) x.pre_order_traversal(x.base) print(\u0026#34;in_order\u0026#34;) x.in_order_traversal(x.base) print(\u0026#34;post_order\u0026#34;) x.post_order_traversal(x.base) jd1 = BiTNode(data=58) jd2 = BiTNode(data=57) jd3 = BiTNode(data=36) jd4 = BiTNode(jd2, jd1, 23) jd5 = BiTNode(right=jd3, data=9) jd6 = BiTNode(right=jd4, data=8) base = BiTNode(jd6, jd5, 7) x = BinaryTree(base) print(\u0026#34;pre_order traversal\u0026#34;.center(20, \u0026#34;#\u0026#34;)) x.pre_order_traversal(x.base) print(\u0026#34;in_order traversal\u0026#34;.center(20, \u0026#34;#\u0026#34;)) x.in_order_traversal(x.base) print(\u0026#34;post_order traversal\u0026#34;.center(20, \u0026#34;#\u0026#34;)) x.post_order_traversal(x.base) C:\\Python37\\python.exe C:/python_workspace/tree/binary_tree.py pre_order 7 8 9 in_order 8 7 9 post_order 8 9 7 pre_order traversal# 7 8 23 57 58 9 36 #in_order traversal# 8 57 23 58 7 9 36 post_order traversal 57 58 23 8 36 9 7 Process finished with exit code 0","title":"用Python实现二叉树"},{"location":"https://codenow.me/articles/django_support_https/","text":" 一、django中的HTTPS HTTPS在web应用中与web服务器有关,比如搭建nginx+django应用,通过反向代理https和http请求重定向到django的http请求上,https证书在web服务器上配置,与django应用无关。当反向代理也是走https请求时,django则需要通过插件使django可支持https。\n二、 django中的SECURE_SSL_REDIRECT配置 在settings.py中添加SECURE_SSL_REDIRECT = True,默认下配置为SECURE_SSL_REDIRECT = False\n1. 设置SECURE_SSL_REDIRECT = True 此时在浏览器发出http请求时django会重定向到https上。\n以 $ python manage.py runserver启动应用,发出http请求后django后台日志如下: \u0026ldquo;GET / HTTP/1.1\u0026rdquo; 301 0 Self-signed SSL certificates are being blocked:Fix this by turning off \u0026lsquo;SSL certificate verification\u0026rsquo; in Settings \u0026gt; General\u0026hellip;\n但此时web应用是不支持https的,报错如下 You\u0026rsquo;re accessing the development server over HTTPS, but it only supports HTTP\n2. 设置SECURE_SSL_REDIRECT = False 此时http请求不会跳转到https,http此时django能正确访问。如果直接请求HTTPS时会报错如下: You\u0026rsquo;re accessing the development server over HTTPS, but it only supports HTTP.\n三、django的https支持:sslserver插件 1.如果django需要HTTPS支持,可安装有sslserver插件: $ pip install django-sslserver 2. 在settings.py中添加配置 SECURE_SSL_REDIRECT = False INSTALLED_APPS = ( ... \u0026#34;sslserver\u0026#34;, ... ) 3. 自带证书启动django应用 $ python manage.py runsslserver 4. 指定证书启动django应用 $ python manage.py runsslserver --certificate /path/to/certificate.crt --key /path/to/key.key 0.0.0.0:8000 当SECURE_SSL_REDIRECT = False时,http请求无响应,https请求能正确访问。 当SECURE_SSL_REDIRECT = True时,http请求会重定向https,此时django支持https,可正确访问。\n","title":"Django同时支持http/https"},{"location":"https://codenow.me/tips/django_mysql/","text":" 1. 数据库设置 settings.py DATABASES = { \u0026#39;default\u0026#39;: { \u0026#39;ENGINE\u0026#39;: \u0026#39;django.db.backends.mysql\u0026#39;, \u0026#39;NAME\u0026#39;: \u0026#39;autotest\u0026#39;, \u0026#39;USER\u0026#39;: \u0026#39;root\u0026#39;, \u0026#39;PASSWORD\u0026#39;: \u0026#39;123456\u0026#39;, \u0026#39;HOST\u0026#39;: \u0026#39;127.0.0.1\u0026#39;, \u0026#39;PORT\u0026#39;: \u0026#39;3306\u0026#39;, } } 2. models.py from django.db import models from django import forms from datetime import datetime # Create your models here. class indexUsers(models.Model): username = models.CharField(max_length=30,verbose_name=\u0026#34;姓名\u0026#34; ) age = models.IntegerField(default=3, verbose_name=\u0026#34;年龄\u0026#34;) phone = models.IntegerField(default=11, verbose_name=\u0026#34;电话\u0026#34;) addtime = models.DateField(default=datetime.now, blank=True, null=True, verbose_name=\u0026#34;添加时间\u0026#34;) class Meta: verbose_name = u\u0026#39;用户管理\u0026#39; verbose_name_plural = u\u0026#39;用户管理\u0026#39; def __str__(self): return self.username 3. 创建一个能够建立数据库表的文件 D:\\python_workspace\\autotest\u0026gt;python manage.py makemigrations Migrations for \u0026#39;testtemplate\u0026#39;: testtemplate\\migrations\\0001_initial.py - Create model indexUsers 0001_initial.py文件的本质,其实就是一个创建数据库表的文本\nD:\\python_workspace\\autotest\u0026gt;python manage.py sqlmigrate testtemplate 0001 BEGIN; -- -- Create model indexUsers -- CREATE TABLE `testtemplate_indexusers` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(30) NOT NULL, `age` integer NOT NULL, `phone` integer NOT NULL, `addtime` date NULL); COMMIT; D:\\python_workspace\\autotest\u0026gt; 4. 创建数据库表 D:\\python_workspace\\autotest\u0026gt;python manage.py migrate","title":"Django中Mysql的使用"},{"location":"https://codenow.me/tips/goland-jvm-optimize/","text":"Help-\u0026gt;Edit Custom VM Options\n-Xms3g -Xmx3g -XX:ReservedCodeCacheSize=512m -XX:+UseCompressedOops -Dfile.encoding=UTF-8 -XX:SoftRefLRUPolicyMSPerMB=50 -ea -Dsun.io.useCanonCaches=false -Djava.net.preferIPv4Stack=true -Djdk.http.auth.tunneling.disabledSchemes=\u0026quot;\u0026quot; -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Xverify:none -server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log -XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof ","title":"Goland Jvm Optimize"},{"location":"https://codenow.me/algorithm/leetcode_70_climbing_stairs/","text":" 题号:70 难度:Easy 链接:https://leetcode-cn.com/problems/climbing-stairs/\n Python代码\nclass Solution: def __init__(self): self.cache = {1: 1, 2: 2} def climbStairs(self, n: int) -\u0026gt; int: if n in self.cache: return self.cache[n] result = self.climbStairs(n - 1) + self.climbStairs(n - 2) self.cache[n] = result return result","title":"Leetcode 70: Climbing Stairs"},{"location":"https://codenow.me/translation/goroutine-leaks-abandoned-receivers/","text":" 原文地址\n简介 Goroutine 泄露在 Go 编程中是很常见的问题。在我的前一篇文章中,我介绍了 Goroutine 泄露的问题,并提供一个许多开发者都会犯的错误。这篇文章继续前文,讨论另一个关于 Goroutine 泄露的场景。\n被遗弃接收者的泄露 在这个例子中,你将可以看到多个 Goroutine 被阻塞,等待永远不会被发送的值\n文章中的这个程序启动了多个 Goroutine 来处理文件中一批数据。每个 Goroutine 从输入 channel 接受值,然后通过输出 channel 发送新值。\nListing 1\nhttps://play.golang.org/p/Jtpla_UvrmN\n35 // processRecords is given a slice of values such as lines 36 // from a file. The order of these values is not important 37 // so the function can start multiple workers to perform some 38 // processing on each record then feed the results back. 39 func processRecords(records []string) { 40 41 // Load all of the records into the input channel. It is 42 // buffered with just enough capacity to hold all of the 43 // records so it will not block. 44 45 total := len(records) 46 input := make(chan string, total) 47 for _, record := range records { 48 input \u0026lt;- record 49 } 50 // close(input) // What if we forget to close the channel? 51 52 // Start a pool of workers to process input and send 53 // results to output. Base the size of the worker pool on 54 // the number of logical CPUs available. 55 56 output := make(chan string, total) 57 workers := runtime.NumCPU() 58 for i := 0; i \u0026lt; workers; i++ { 59 go worker(i, input, output) 60 } 61 62 // Receive from output the expected number of times. If 10 63 // records went in then 10 will come out. 64 65 for i := 0; i \u0026lt; total; i++ { 66 result := \u0026lt;-output 67 fmt.Printf(\u0026#34;[result ]: output %s\\n\u0026#34;, result) 68 } 69 } 70 71 // worker is the work the program wants to do concurrently. 72 // This is a blog post so all the workers do is capitalize a 73 // string but imagine they are doing something important. 74 // 75 // Each goroutine can\u0026#39;t know how many records it will get so 76 // it must use the range keyword to receive in a loop. 77 func worker(id int, input \u0026lt;-chan string, output chan\u0026lt;- string) { 78 for v := range input { 79 fmt.Printf(\u0026#34;[worker %d]: input %s\\n\u0026#34;, id, v) 80 output \u0026lt;- strings.ToUpper(v) 81 } 82 fmt.Printf(\u0026#34;[worker %d]: shutting down\\n\u0026#34;, id) 83 } 在第 39 行定义了一个名为 processRecords 的函数。该函数接收一个 string 的 slice 值。在第 46 行创建了一个名为 input 缓存的 channel。在 47 和 48 行循环把 slice 中的 string 值发送到 channel 中。创建的输入通道具有足够的容量来保存 slice 中的每个值,因此在 48 行中的发送都不会阻塞。这个 channel 作为一个 pipeline 分布在多个 Groutine 中。\n接下来的 56 到 60 行,程序创建了一个 Goroutine 池来从 pipeline 中接收值。56 行创建了一个名为 output 的缓存 channel,每个 Goroutine 都会把值发送到这个 channel。57 行到 59 行用 worker 函数创建等同于逻辑 CPU 数量的 Goroutine,传入循环变量 i、input channel、output channel。\nwoker 函数被定义在第 77 行,函数的签名定义 input 为 \u0026lt;- chan string,意味着这是一个只读 channel;另一个参数 output 是 chan\u0026lt;- string,意味着是只写 channel。\n在这个函数的 78 行中,Goroutine 使用 range 循环从 input 中接收值,使用 range 从 channel 中接收值直到 channel 被关闭同时再也读不出值为止。每次循环中接收到的值赋给迭代变量 v,同时在 79 行打印出来。然后第 80 行,worker 函数将 v 传给 strings.ToUpper 函数返回一个新的 string。这个 worker 立即把这个 string 发送到 output channel 中。\n回到 processRecords 函数中,现在已经执行到第 65 行。正在运行着一个循环,直到从 output channel 接收并处理了所有值。在 66 行 processRecords 函数等待接收来自另一个 Goroutine 的值,接收到的值在 67 行打印出来。当该程序接收到每个 input 值,他会退出循环并终止改功能。\n运行这个程序打印转换后的数据,看起来视乎很正常的工作,其实正在泄露多个 Goroutine,实际上程序永远不会到达 82 行,这行打印这个 worker 已经。及时 processRecords 函数已经返回,每个 worker 的 Goroutine 仍然在 78 行等待 input 中的值。实际上 channel 的接收者一直等待到 channel 关闭并且 channel 为空。这个问题就在于程序从来没有去关闭 channel。\nFIX:通知完成 修复这个泄露只需要一行代码 close(input)。关闭 channel 是表示 “不在发送数据” 的一种方式。关闭 channel 最合适的地方应该是在第 50 行发送完最后一个值之后,如 Listing 2 所示:\nListing 2\nhttps://play.golang.org/p/QNsxbT0eIay\n45 total := len(records) 46 input := make(chan string, total) 47 for _, record := range records { 48 input \u0026lt;- record 49 } 50 close(input) 关闭一个缓冲 channel 后,channal 中的值仍然是有效的,channel 被仅仅是关闭发送而不是接收。worker Goroutine 运行 range input 将会得到缓冲 channel 已经被关闭的信号,这可以让 worker 在程序退出前终止循环退出。\n结论 正如前一篇文章中所提到的,Go 中使用 Goroutine 变得简单,但是你有责任好好的使用它们。在这篇文章中我展示了另一个使用 Goroutine 很容易犯的错误。还是有更多使用 Goroutine 并发泄露的陷阱。未来的文章我们会继续这些陷阱。和以前一样,我将继续重复这一建议 “永远不要启动一个你不知道如何停止的 Goroutine”。\n并发是一种有用的工具,但必须谨慎使用。\n","title":"Goroutine 泄露——被遗弃的接受者"},{"location":"https://codenow.me/articles/python-namedtuple/","text":"namedtuple 是一个简化 tuple 操作的工厂函数,对于普通元组我们在访问上只能通过游标的访问,在表现力上有时候比不上对象。\n命名的元组实例没有每个实例的字典,因此它们是轻量级的,并且不需要比常规元组更多的内存。\n假如想计算两个点之间的距离根据定义:\n需要两个点的 x、y 坐标,我们可以直接使用元组表示 p1 和 p2 点\n\u0026gt;\u0026gt;\u0026gt; import math \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; p1, p2 = (1, 2), (2, 3) \u0026gt;\u0026gt;\u0026gt; s = math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; print(s) 1.4142135623730951 \u0026gt;\u0026gt;\u0026gt; 对于 p1 点的 x 坐标使用 p1[0] 表示,对阅读上有一定的困扰,如果可以使用 p1.x 就语义清晰了。\n这个场景就是 namedtuple 的典型应用,让字段具有名字,使用 namedtuple 重写上面例子\n\u0026gt;\u0026gt;\u0026gt; import collections \u0026gt;\u0026gt;\u0026gt; import math \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; Point = collections.namedtuple(\u0026#39;Point\u0026#39;, [\u0026#39;x\u0026#39;, \u0026#39;y\u0026#39;]) \u0026gt;\u0026gt;\u0026gt; p1, p2 = Point(1, 2), Point(2, 3) \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; s = math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2) \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; print(s) 1.4142135623730951 \u0026gt;\u0026gt;\u0026gt; 好奇宝宝肯定就会想知道 namedtuple 是如何让字段具有名字的,先看看函数的签名\nnamedtuple(typename, field_names, *, rename=False, defaults=None,module=None) 第一个和第二参数前面已经使用过了,typename 就是新命名元组的名字,我们最经常的就是模仿的类,所以会使用类的定义风格。field_names 参数用于定义字段的名字,除了上面使用 ['x', 'y'] 还可以使用 \u0026quot;x y\u0026quot; 或者 \u0026quot;x, y\u0026quot;,定义方法选择自己喜欢的就好。\nrename 参数默认是 False,顾名思义就是重命名字段名字,假如我们使用了非法的变量名(比如关键字等)会被重命名成别的名字。\n [!DANGER]\n这种改变定义的行为是最好不要做,除非你能保证任何人知道这个行为。\n defaults 参数可以是 None 或者一个可迭代的值,根据具有默认值的字段必须在没有初始值的后面,所以defaults 提供的默认值都是最右匹配。\n\u0026gt;\u0026gt;\u0026gt; from collections import namedtuple \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; Point = namedtuple(\u0026#39;Point\u0026#39;, \u0026#34;x y z\u0026#34;, defaults=[2, 3]) \u0026gt;\u0026gt;\u0026gt; p1 = Point(1) \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; print(p1) Point(x=1, y=2, z=3) \u0026gt;\u0026gt;\u0026gt; 如果定义了 module,则将命名元组的 __module__属性设置为该值。\n... if isinstance(field_names, str): field_names = field_names.replace(\u0026#39;,\u0026#39;, \u0026#39; \u0026#39;).split() field_names = list(map(str, field_names)) typename = _sys.intern(str(typename)) ... 进入函数的第一步先对两个基本的参数 typename 和 field_names 进行处理。\n如果 field_names 是一个字符串就 replace 把 , 转化成空格,再 split 成标准的 list。 list(map(str, field_names)) 保证了 field_names 的每个值都是 str 类型。 _sys.intern 把 typename 注册到全局中,可以加快对字符串的寻找。\n... if rename: seen = set() for index, name in enumerate(field_names): if (not name.isidentifier() or _iskeyword(name) or name.startswith(\u0026#39;_\u0026#39;) or name in seen): field_names[index] = f\u0026#39;_{index}\u0026#39; seen.add(name) ... 对于设置了 rename=True 会对不合法的 field_name 重新命名,从代码中可以看出重新命名的规则是:如果不合法,判断是不是 关键字、是不是以 下划线 开头,是不是 已经存在,如果符合其中一项就会对用 _{当前的 index}变量重新命名。\n... for name in [typename] + field_names: if type(name) is not str: raise TypeError(\u0026#39;Type names and field names must be strings\u0026#39;) if not name.isidentifier(): raise ValueError(\u0026#39;Type names and field names must be valid \u0026#39; f\u0026#39;identifiers: {name!r}\u0026#39;) if _iskeyword(name): raise ValueError(\u0026#39;Type names and field names cannot be a \u0026#39; f\u0026#39;keyword: {name!r}\u0026#39;) seen = set() for name in field_names: if name.startswith(\u0026#39;_\u0026#39;) and not rename: raise ValueError(\u0026#39;Field names cannot start with an underscore: \u0026#39; f\u0026#39;{name!r}\u0026#39;) if name in seen: raise ValueError(f\u0026#39;Encountered duplicate field name: {name!r}\u0026#39;) seen.add(name) ... 接下来对输入的 typename 和 field_names 经检查了一下参数,仍是使用上面的三个规则,确保 typename 和 field_names 中的元素是合法的字符串。\n... field_defaults = {} if defaults is not None: defaults = tuple(defaults) if len(defaults) \u0026gt; len(field_names): raise TypeError(\u0026#39;Got more default values than field names\u0026#39;) field_defaults = dict(reversed(list(zip(reversed(field_names), reversed(defaults))))) ... 如果设置了 defaults 参数,要最右匹配到 field_names。先使用了 zip 函数,把 reversed 后的 field_names 和 defaults 组合成元组的 list\n\u0026gt;\u0026gt;\u0026gt; field_names = [\u0026#39;x\u0026#39;, \u0026#39;y\u0026#39;, \u0026#39;z\u0026#39;] \u0026gt;\u0026gt;\u0026gt; defaults = [2, 3] \u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; print(list(zip(reversed(field_names), reversed(defaults)))) [(\u0026#39;z\u0026#39;, 3), (\u0026#39;y\u0026#39;, 2)] \u0026gt;\u0026gt;\u0026gt; 最后在使用 dict(reversed(...)) 转化成 dict 类型。\n... # Variables used in the methods and docstrings field_names = tuple(map(_sys.intern, field_names)) num_fields = len(field_names) arg_list = repr(field_names).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;)[1:-1] repr_fmt = \u0026#39;(\u0026#39; + \u0026#39;, \u0026#39;.join(f\u0026#39;{name}=%r\u0026#39; for name in field_names) + \u0026#39;)\u0026#39; tuple_new = tuple.__new__ _dict, _tuple, _len, _map, _zip = dict, tuple, len, map, zip # Create all the named tuple methods to be added to the class namespace s = f\u0026#39;def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list}))\u0026#39; namespace = {\u0026#39;_tuple_new\u0026#39;: tuple_new, \u0026#39;__name__\u0026#39;: f\u0026#39;namedtuple_{typename}\u0026#39;} # Note: exec() has the side-effect of interning the field names exec(s, namespace) __new__ = namespace[\u0026#39;__new__\u0026#39;] __new__.__doc__ = f\u0026#39;Create new instance of {typename}({arg_list})\u0026#39; if defaults is not None: __new__.__defaults__ = defaults ... 这部分动态设置参数的过程,重点关注 exec(s, namespace) ,s 是 __new__ 方法的定义,其中的 arg_list 是我们设置的属性名字会转换成 x, y, x 这种形式,填充的 s 中。namespace 则是 exec 过程中可使用的变量,这里传入了 tuple_new = tuple.__new__ 用于创建一个新的 tuple。\n... @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f\u0026#39;Expected {num_fields} arguments, got {len(result)}\u0026#39;) return result _make.__func__.__doc__ = (f\u0026#39;Make a new {typename} object from a sequence \u0026#39; \u0026#39;or iterable\u0026#39;) def _replace(_self, **kwds): result = _self._make(_map(kwds.pop, field_names, _self)) if kwds: raise ValueError(f\u0026#39;Got unexpected field names: {list(kwds)!r}\u0026#39;) return result _replace.__doc__ = (f\u0026#39;Return a new {typename} object replacing specified \u0026#39; \u0026#39;fields with new values\u0026#39;) def __repr__(self): \u0026#39;Return a nicely formatted representation string\u0026#39; return self.__class__.__name__ + repr_fmt % self def _asdict(self): \u0026#39;Return a new dict which maps field names to their values.\u0026#39; return _dict(_zip(self._fields, self)) def __getnewargs__(self): \u0026#39;Return self as a plain tuple. Used by copy and pickle.\u0026#39; return _tuple(self) # Modify function metadata to help with introspection and debugging for method in (__new__, _make.__func__, _replace, __repr__, _asdict, __getnewargs__): method.__qualname__ = f\u0026#39;{typename}.{method.__name__}\u0026#39; ... 接着定义了一些列的方法,这些方法最后都是用于生成 namedtuple 后所拥有的方法,根据简单的注释可以很容易知道他们的用途\n... # Build-up the class namespace dictionary # and use type() to build the result class class_namespace = { \u0026#39;__doc__\u0026#39;: f\u0026#39;{typename}({arg_list})\u0026#39;, \u0026#39;__slots__\u0026#39;: (), \u0026#39;_fields\u0026#39;: field_names, \u0026#39;_field_defaults\u0026#39;: field_defaults, # alternate spelling for backward compatiblity \u0026#39;_fields_defaults\u0026#39;: field_defaults, \u0026#39;__new__\u0026#39;: __new__, \u0026#39;_make\u0026#39;: _make, \u0026#39;_replace\u0026#39;: _replace, \u0026#39;__repr__\u0026#39;: __repr__, \u0026#39;_asdict\u0026#39;: _asdict, \u0026#39;__getnewargs__\u0026#39;: __getnewargs__, } # _tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc) for index, name in enumerate(field_names): doc = _sys.intern(f\u0026#39;Alias for field number {index}\u0026#39;) class_namespace[name] = _tuplegetter(index, doc) result = type(typename, (tuple,), class_namespace) ... 定义 class_namespace 传入上面定义好一系列方法,最后使用 type 创建出一个新的 class。\n [!NOTE]\nPython 所有的东西都是 type 这个函数创建出来的,包括 type 本身,更多 type 相关信息参考 https://docs.python.org/3/library/functions.html#type\n ... # For pickling to work, the __module__ variable needs to be set to the frame # where the named tuple is created. Bypass this step in environments where # sys._getframe is not defined (Jython for example) or sys._getframe is not # defined for arguments greater than 0 (IronPython), or where the user has # specified a particular module. if module is None: try: module = _sys._getframe(1).f_globals.get(\u0026#39;__name__\u0026#39;, \u0026#39;__main__\u0026#39;) except (AttributeError, ValueError): pass if module is not None: result.__module__ = module return result ... 最后需要把 module 属性设置回 result 的 __module__ 中,这些信息会在 pickle 会被用到。\n总结一下,namedtuple 创建过程大体分成三个部分:\n 提取参数、定义 tuple 所需的方法 根据参数名字动态构建 __new__ 函数,和最后生成的 tuple 的属性可以对应上 填充 class_namespace,用 type 函数创建一个 tuple 对象 其实在不久之前,namedtuple 还是直接使用字符串模板生成,现在这种实现方法更优雅了。\n","title":"Python Namedtuple 源码分析"},{"location":"https://codenow.me/translation/howtocontributetoopensource01/","text":" 原链接: How to Contribute to Open Source\n一、为什么要为开源做贡献 给开源项目做贡献是一个具有投资价值回报的事情,在学习、传授和积累任何你可以想象到的技能经验 等等各个方面。 为什么人们要为开源做贡献?原因很多!\n提高现有技能 无论是敲代码、UI 设计、平面设计、写作或整理,如果你想要实践,开源项目里可以总有一个任务适合你。\n认识拥有同样爱好的人 热情的开源项目团队总是可以让人流连忘返。 许多人的终身友谊就是在参加开源项目时认识的,无论是在会议里遇到还是在深夜里讨论墨西哥卷饼,都保持着良好的联系。\n找到导师或者传授他人 在一个共享项目里和别人合作意味着你不得不告诉别人你的做事方式以及向他人寻求帮助。 学习和教学对于每一个参加的人来说,都会变的日常和丰富。\n建立公共事务帮助你提高声誉以及事业 根据定义,你所有的开源项目都是公共的,意味着你在任何地方都可以以它们为例子来证明你的能力。\n学习通用技能 开源项目提供很多锻炼领导力和管理能力的机会,像解决冲突、管理团队和确定工作的优先级。\n能够让你作出改变,就算是小改变 参加开源项目不意味着你是一个终身贡献者。你有没有在什么网站上看到过什么错字,然后希望有人可以纠正它,其实你可以去做。 开源项目通过它们的活动帮助人们感受到组织的氛围以及它们如何体验这个世界的。这本身就很有意义。\n二、贡献什么 如果你是一个新手贡献者,这个过程看上去有点吓人。 怎么找到合适的项目?如果不会敲代码怎么办?如果出了错怎么办? 别担心,有各种参与开源项目的方式! 为了能让你充分利用自身经验,下面有一些提示。\n你不必贡献代码 对于贡献开源的一个常见误解就是你一定要贡献代码。事实上,这是一个项目里最被忽视的一个部分。\n你喜欢活动策划吗? 组织项目的研讨会或者聚会,像@fzamperin 为 NodeSchool 做的那样 组织项目会议 帮助社区成员找到合适的会议室和写会议提案 你喜欢设计吗? 重组布局,帮助项目可读性 进行用户研究,改进项目的导航条和目录,像 Drupal 建议的那样 整理设计风格指南,帮助项目拥有一个一致的视觉效果 设计 T 恤或者新 logo,像 hapi.js 的贡献者做的那样 你喜欢写作吗? 写项目文档或者改进项目文档 建立一个示例文件夹,展示项目的使用方式 为项目启动简报,或从邮件列表里挑出一些精彩的集锦 写项目教程,像 PyPA 的贡献者做的那样 翻译项目文档 你喜欢整理东西吗? 对于重复的 issues 的链接,建议增加一个新的 issue 标签,保持事情的调整性 浏览公开 issues,建议关闭旧的 issues 在最近的公开 issues 里问具体的问题,使讨论向前推进 你喜欢敲代码吗? 找到一个待解决的公开 issue,像 @dianjin 为 Leaflet 做的那样 询问你是否可以帮助写一些新功能 使项目启动自动化 改进工具和测试 你喜欢帮助别人吗? 在像 Stack Overflow 或 Reddit 的网站上解答有关项目的问题,像这个 Postgres 例子一样 在公开 issues 为人解答疑问 帮助调节项目与外界的沟通渠道 你喜欢帮助别人有关代码的事吗? 审核别人提交的代码 写项目使用教程 为贡献者提供指导,像 @ereichert 为 @bronzdoc 在 Rust 上做的那样 你不一定要做软件项目! 虽然开源项目通常指软件,你也可以尝试别的东西。还有很多其它像书籍、食谱、列表或者类等形式的开源项目。 例子:\n @sindresorhus 创建了一个 awesome 列表 @h5bp 给前端工程师们维护着一个面试常见问题列表 @stuartlynn 和 @nicole-a-tesla 建立了一个系列介绍关于海雀的一些有趣的事实。 就算你是一个软件工程师,从事文档项目可以让你快速开始开源项目。 从与代码无关的工作开始,看起来就没那么可怕了,而且可以借助这个过程建立你的自信心和积累你的经验。\n三、在新项目里定位你自己 对于任何超过纠正错字的建议,贡献开源就像是在一个派对上走向一群陌生人。 当他们在聊金鱼聊得起劲的时候,你过去说关于羊驼的事,他们大概会觉得你奇怪。 所以,与其毫无来由地提出你的建议,不如先学会开始观察整个房间,这样做可以增加你的观点被注意和被聆听的机会。\n剖析开源项目 每个开源社区都是不一样的。 在一个开源项目花了很多年时间意味着你足够了解这个项目。 当你去了另外一个项目,你可能会发现项目用的词汇、规范和沟通风格都不一样。 也就是说,许多开源项目其实都遵循着一个类似的组织结构。了解不同的社区角色和整个流程会帮助你快速的定位到任何新项目。\n一个典型的开源项目有以下类型的人:\n 作者: 创建项目的人或机构 所有者:对组织或仓库拥有管理所有权的人,不一定是作者 维护人员:负责推动项目愿景和管理项目组织方面的贡献者,可能同时是作者或所有者 贡献者:为项目做出贡献的所有人 社区成员:使用项目的人,他们可能会积极参与对话或者表达对于项目方向的意见 大一点的项目可能还有小组委员会或针对不同任务的工作组,像工具、分类、社区调节和活动组织。 在项目网站上找关于团队介绍的页面,或者在仓库里的管理文档里找到相关的信息。\n一个项目也有文档。这些文件通常被列在仓库的最高层级。\n LICENSE:从定义上理解,每个开源项目都必须拥有开源许可证。如果没有的,那就不是开源 README:帮助新的社区成员加入项目的一个指导手册。通常描述了项目的目的和如何开始使用 CONTRIBUTING:README 是教人们如何使用项目,而 CONTRIBUTING 文档则是教人们如何为这个项目做贡献。 通常描述了所需要的贡献类型和整个流程的运作模式。不是所有项目都有这个文件,有这个文档表示这个项目欢迎更多的贡献者 CODE_OF_CONDUCT:代码准则规范为参与者的行为设定了基本的规则,有助于营造一个友好的热情的环境。 不是所有项目都有这个文件,有这个文档表示这个项目欢迎更多的贡献者 Other documentation:可能是其它的比如教程、演示或管理方针之类的文档。大项目里比较常见 最后,开源项目会有下面的工具来组织讨论。 阅读档案会帮助你加深对社区的想法和运作方式的理解。\n Issue tracker:人们讨论关于项目的问题的地方 Pull requests:人们讨论和审核正在进行修改的地方 Discussion forums or mailing lists:有些项目会使用讨论论坛或邮箱列表这些渠道进行特定的对话主题,比如 \u0026ldquo;我应如何\u0026hellip;\u0026rdquo; 或 \u0026ldquo;你对\u0026hellip;的看法是\u0026rdquo; 等,而不是报告错误或请求新功能。某些项目也会选择用 Issue tracker 进行所有类型的对话 Synchronous chat channel:一些项目会使用像 Slack 或 IRC 作为日常对话、协作和交换想法的沟通渠道 ","title":"如何做开源贡献者 01"},{"location":"https://codenow.me/articles/learning_the_shell_02/","text":" Shell 脚本基本知识 脚本常见编辑器:\n vi vim\n 脚本命名:\n 见名知义\n 脚本执行方式\n # 推荐 - bin/bash /path/to/script-name 或 bash /path/to/script-name # 文件有可执行权限,在当前路径下执行脚本 - /path/to/script-name 或 ./script-name # 注意 `.` 符号 - source script-name 或 . script-name 说明:\n ☆☆☆ 当脚本文件本身没有`可执行权限`或脚本首行没有命令解释器时使用的方法时,推荐用 bash 执行。 会开启新进程,不能共享环境,不能共享变量 ☆☆☆ 当脚本文件具有可执行权限时使用。 会开启新进程,不能共享环境,不能共享变量 ☆☆ 使用 source 或者 `.` 加载脚本文件,使脚本环境与当前用户环境一致, 不会新开进程,能与 shell 窗口共享进程,共享环境,共享变量 脚本开发规范\n 文件后缀是 .sh 脚本文件首行是且必须是脚本解释器,一般是 #! /bin/bash 脚本解释器后要有脚本基本信息,尽量用英文注释。 常见注释信息包括脚本名字、功能描述、脚本版本、作者和联系方式等 脚本内容执行:由上至下,依次执行 代码书写优秀习惯\n 成对符号一次性写,如 ()、{}、[]、'' 等\n [ ] 中括号两端要有空格\n 流程控制语句一次性写完,再写内容\n 通过缩进增加可读性\n ","title":"Shell 入门 02"},{"location":"https://codenow.me/tips/mysql_replace/","text":" 描述:\n 数据库存储的数据与前端传过来的数据格式不一定统一\n 解决:\n 用 replace 方法解决这个问题\n 例子:\n 数据库表里的电话号码: PHONE_NUMBER: xxx-xxxx-xxxx\n 前端传过来的电话号码: phoneNumber: xxxxxxxxxxx\n 另要注意,当语句里用到 OR 时要用括号给括起来\nSELECT * FROM table WHERE 1 = 1 AND (REPLACE(PHONE_NUMBER, \u0026#34;-\u0026#34;, \u0026#34;\u0026#34;) = phoneNumber OR PHONE_NUMBER = phoneNumber);","title":"用 replace 方法修改 mysql 存储的数据"},{"location":"https://codenow.me/algorithm/leetcode_806_number_of_lines_to_write_string/","text":" 题号:806 难度:easy 链接:https://leetcode.com/problems/number-of-lines-to-write-string/ 描述:给出 26 个字母每个占位的宽度和一串字母字符串,每行宽度为 100, 当每行加上要输入下一个的字母宽度超过 100 的话这个字母就跳到下一行。求这串字母占多少行且最后一行占位宽度 class Solution: def numberOfLines(self, widths: List[int], S: str) -\u0026gt; List[int]: letter_list = [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;f\u0026#34;,\u0026#34;g\u0026#34;,\u0026#34;h\u0026#34;,\u0026#34;i\u0026#34;,\u0026#34;j\u0026#34;,\u0026#34;k\u0026#34;,\u0026#34;l\u0026#34;,\u0026#34;m\u0026#34;,\u0026#34;n\u0026#34;,\u0026#34;o\u0026#34;,\u0026#34;p\u0026#34;,\u0026#34;q\u0026#34;,\u0026#34;r\u0026#34;,\u0026#34;s\u0026#34;,\u0026#34;t\u0026#34;,\u0026#34;u\u0026#34;,\u0026#34;v\u0026#34;,\u0026#34;w\u0026#34;,\u0026#34;x\u0026#34;,\u0026#34;y\u0026#34;,\u0026#34;z\u0026#34;] letter_d = dict(zip(letter_list, widths)) str_widths = 0 lines = 1 for letter in S: letter_width = letter_d[letter] if str_widths + letter_width \u0026gt; 100 * lines: str_widths = 100 * lines + letter_width lines += 1 else: str_widths += letter_width last_line_width = 100 - (lines * 100 - str_widths) res = [lines,last_line_width] return res ","title":"Leetcode_806: Number of Lines To Write String"},{"location":"https://codenow.me/translation/choosing_a_faster_json_library_for_python/","text":" 原文链接:https://pythonspeed.com/articles/faster-json-library/\n使用JSON越多,就越可能遇到JSON编码或解码作为瓶颈的情况。Python的内置库也不错,但是有多个更快的JSON库可用:您如何选择使用哪一个? 事实上,没有一个正确的答案,也没有一个最快的JSON库来管理它们:\n “快速JSON库”对于不同的人来说意味着不同的东西,因为他们的使用模式不同。 速度不是万能的,还有其他你可能关心的事情,比如安全和定制。 为了帮助您根据需要选择最快的JSON库,我想分享一下我为Python选择快速JSON库所经历的过程。您可以使用此过程选择最适合您特定需求的库:\n 确保确实存在问题。 定义基准。 根据附加要求进行过滤。 对其余的候选人进行基准测试。 步骤1:您真的需要一个新的JSON库吗? 仅仅因为使用了JSON并不意味着它是一个相关的瓶颈。在花时间考虑哪个JSON库之前,您需要一些证据表明,Python的内置JSON库在您的特定应用程序中确实是一个问题。 在我的例子中,我从我的因果日志库Eliot的基准中了解到这一点,它表明JSON编码占用了生成消息所用CPU时间的25%。我能得到的最快的加速是以33%的速度运行(如果JSON编码时间变为零),但是这是一个足够大的时间块,它迟早会排在列表的最前面。\n步骤2:定义基准 如果您查看各种JSON库的基准页,他们将讨论如何处理各种不同的消息。然而,这些消息并不一定符合您的用法。他们经常测量非常大的消息,在我的例子中,至少我关心小消息。\n因此,您需要想出一些与您的特定使用模式相匹配的度量标准:\n 你在乎编码,解码,还是两者兼而有之? 您使用的是小消息还是大消息? 典型的消息是什么样子的? 在我的例子中,我主要关心对小消息进行编码,这是由Eliot生成的日志消息的特殊结构。基于一些真实的日志,我提出了以下示例消息:\n{ \u0026#34;timestamp\u0026#34;: 1556283673.1523004, \u0026#34;task_uuid\u0026#34;: \u0026#34;0ed1a1c3-050c-4fb9-9426-a7e72d0acfc7\u0026#34;, \u0026#34;task_level\u0026#34;: [1, 2, 1], \u0026#34;action_status\u0026#34;: \u0026#34;started\u0026#34;, \u0026#34;action_type\u0026#34;: \u0026#34;main\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;value\u0026#34;, \u0026#34;another_key\u0026#34;: 123, \u0026#34;and_another\u0026#34;: [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;], } 步骤3:基于附加要求的过滤器 性能并不是所有的事情,还有其他你可能关心的事情。在我看来:\n 安全性/抗崩溃性:日志消息可以包含来自不受信任源的数据。如果JSON编码器在坏数据上崩溃,那么这对可靠性或安全性都不好。 自定义编码:Eliot支持自定义JSON编码,因此您可以序列化其他类型的Python对象。一些JSON库支持这一点,其他的则不支持。 跨平台:在Linux、MacOS、Windows上运行。 维护:我不想依赖一个没有得到积极支持的库。 我考虑的库有orjson、rapidjson、ujson和hyperjson。\n我根据上述标准筛选出了其中一些:\n ujson有许多关于崩溃的bug文件,甚至那些已经修复的崩溃也不总是可用的,因为自2016年以来还没有发布过。 hyperjson只有MacOS的软件包,而且总体上看起来还很不成熟。 步骤4:基准测试 最后的两个竞争者是rapidjson和orjson。我进行了以下基准测试:\nimport time import json import orjson import rapidjson m = { \u0026#34;timestamp\u0026#34;: 1556283673.1523004, \u0026#34;task_uuid\u0026#34;: \u0026#34;0ed1a1c3-050c-4fb9-9426-a7e72d0acfc7\u0026#34;, \u0026#34;task_level\u0026#34;: [1, 2, 1], \u0026#34;action_status\u0026#34;: \u0026#34;started\u0026#34;, \u0026#34;action_type\u0026#34;: \u0026#34;main\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;value\u0026#34;, \u0026#34;another_key\u0026#34;: 123, \u0026#34;and_another\u0026#34;: [\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;], } def benchmark(name, dumps): start = time.time() for i in range(1000000): dumps(m) print(name, time.time() - start) benchmark(\u0026#34;Python\u0026#34;, json.dumps) # orjson only outputs bytes, but often we need unicode: benchmark(\u0026#34;orjson\u0026#34;, lambda s: str(orjson.dumps(s), \u0026#34;utf-8\u0026#34;)) benchmark(\u0026#34;rapidjson\u0026#34;, rapidjson.dumps) 结果是:\n$ python jsonperf.py Python 4.829106330871582 orjson 1.0466396808624268 rapidjson 2.1441543102264404 即使需要额外的Unicode解码,orjson也是最快的(对于这个特定的基准!).\n像往常一样,存在着权衡。orjson的用户比rapidjson少(比较orjson PyPi stats和rapidjson PyPi stats),而且没有conda包,所以我必须自己打包它以用于conda-forge。但它肯定要快得多。\n你的用例,你的选择 你应该用orjosn吗?不一定。您可能有不同的需求,并且您的基准可能不同,例如,您可能需要解码大型文件。\n关键在于过程:找出您的特定需求、性能等,并选择最能满足您需求的库。\n","title":"为Python选择一个快速的JSON库"},{"location":"https://codenow.me/algorithm/leetcode_238_product_of_array_except_self/","text":"题号:238 难度:medium 链接:https://leetcode.com/problems/product-of-array-except-self/\n#!/usr/bin/python # -*- coding:utf-8 -*- class Solution: def productExceptSelf(self, nums): len_nums = len(nums) result = [None] * len_nums left = 1 right = 1 result[0] = left for i in range(1, len_nums): left = left * nums[i-1] result[i] = left for i in range(len_nums-2, -1, -1): right = right * nums[i+1] result[i] *= right return result if __name__ == \u0026#39;__main__\u0026#39;: test_list = [1, 2, 3, 4] test_result = Solution().productExceptSelf(test_list) print(test_result)","title":"Leetcode 238 Product of Array Except Self"},{"location":"https://codenow.me/articles/how_to_use_python_filter_emoji/","text":" 参考博客:http://my.oschina.net/jiemachina/blog/189460\n1. 将emoji表情替换为指定字符串 import re def filter_emoji(desstr, restr=\u0026#39;\u0026#39;): \u0026#34;\u0026#34;\u0026#34;过滤表情\u0026#34;\u0026#34;\u0026#34; try: # python UCS-4 build的处理方式 highpoints = re.compile(u\u0026#39;[\\U00010000-\\U0010ffff]\u0026#39;) except re.error: # python UCS-2 build的处理方式 highpoints = re.compile(u\u0026#39;[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]\u0026#39;) return highpoints.sub(restr, desstr) 2. 把字符串变成表情 import HTMLParser def str_2_emoji(emoji_str): \u0026#34;\u0026#34;\u0026#34;把字符串转换为表情\u0026#34;\u0026#34;\u0026#34; if not emoji_str: return emoji_str h = HTMLParser.HTMLParser() emoji_str = h.unescape(h.unescape(emoji_str)) # 匹配u\u0026#34;\\U0001f61c\u0026#34;和u\u0026#34;\\u274c\u0026#34;这种表情的字符串 co = re.compile(r\u0026#34;u[\\\u0026#39;\\\u0026#34;]\\\\[Uu]([\\w\\\u0026#34;]{9}|[\\w\\\u0026#34;]{5})\u0026#34;) pos_list=[] result=emoji_str # 先找位置 for m in co.finditer(emoji_str): pos_list.append((m.start(),m.end())) # 根据位置拼接替换 for pos in range(len(pos_list)): if pos == 0: result=emoji_str[0:pos_list[0][0]] else: result=result+emoji_str[pos_list[pos-1][1]:pos_list[pos][0]] result = result +eval(emoji_str[pos_list[pos][0]:pos_list[pos][1]]) if pos==len(pos_list)-1: result=result+emoji_str[pos_list[pos][1]:len(emoji_str)] return result","title":"如何使用Python过滤emoji"},{"location":"https://codenow.me/tips/create_verification_code_using_python/","text":"#!/usr/bin/env python # -*- coding:utf-8 -*- import random import string def gen_random_string(length): num_of_numeric = random.randint(1,length-1) num_of_letter = length - num_of_numeric numerics = [random.choice(string.digits) for _ in range(num_of_numeric)] letters = [random.choice(string.ascii_letters) for _ in range(num_of_letter)] all_chars = numerics + letters random.shuffle(all_chars) result = \u0026#39;\u0026#39;.join(all_chars) return result if __name__ == \u0026#39;__main__\u0026#39;: print(gen_random_string(10)) C:\\Python37\\python.exe C:/python_workspace/test2.py 1863N575T5","title":"如何用Python产生验证码"},{"location":"https://codenow.me/tips/goland-live-template/","text":"配置 Goland 的 live template 提升开发效率\n","title":"Goland Live Template"},{"location":"https://codenow.me/algorithm/leetcode_58_length-of-last-word/","text":" 题号:58 难度:Easy 链接:https://leetcode-cn.com/problems/length-of-last-word/\n class Solution: def lengthOfLastWord(self, s: str) -\u0026gt; int: words = s.strip().split(\u0026#34; \u0026#34;) last_world = words[-1] return len(last_world)","title":"Leetcode_58_length of Last Word"},{"location":"https://codenow.me/translation/goroutine-leaks-the-forgotten-sender/","text":" 原文地址\n 简介 并发编程允许开发者使用多个执行者去解决问题,这么做通常可以提高性能。并发并不意味着多个执行者同时运行,意味着执行的顺序从有序变成无序。在过去这种编程方法(并发编程)一般是由标准库或者第三方开发者为主导。\n在 Go 中类似 Gotoutines 和 channels 的并发特性都是集成语言中同时减少乃至移除了对库的依赖,这就造成了在 Go 中写并发编程很容易的错觉。在决定使用并发的时候还是需要谨慎,如果没有正确的使用还是会带来一些特别的副作用和陷阱。如果你不小心,这些陷阱会产生复杂和令人厌恶的错误。\n我将在这篇文章中讨论 Goroutine 泄露带来的陷阱。\nGoroutine 泄露 在内存管理方面 Go 屏蔽了许多细节。Go 编译器使用 逃逸分析 确定变量在内存中的位置,在运行时使用 GC 来跟踪和管理堆的分配。虽然这些机制可以不能完全避免 内存泄露,但是极大的降低了发生的概率。\n一种常见的内存泄露类型就是 Goroutine 泄露。如果你启动了一个你希望它终止但是它不会终止的 Goroutine,这时候它已经泄露了。它会一直存在程序的生命周期中,并且无法释放为 Goroutine 分配的内存,这也是 “Never start a goroutine without knowing how it will stop” 建议的主要原因之一。\n要说明基本的 Goroutine 泄露,看下面代码:\nListing 1 https://play.golang.org/p/dsu3PARM24K\n31 // leak is a buggy function. It launches a goroutine that 32 // blocks receiving from a channel. Nothing will ever be 33 // sent on that channel and the channel is never closed so 34 // that goroutine will be blocked forever. 35 func leak() { 36 ch := make(chan int) 37 38 go func() { 39 val := \u0026lt;-ch 40 fmt.Println(\u0026#34;We received a value:\u0026#34;, val) 41 }() 42 } Listing 1 定义了一个函数命名为 leak。这个函数在第 36 行创建了一个通道,允许 Goroutines 传递整型数据。然后在 38 行创建了一个被阻塞的 Gotoutine,这是因为在 39 行一直在等待从 channel 中获取值。这个 Goroutine 一直在等待,但是 leak 函数返回了。程序的其他部分无法通过 channel 发送数据,Goroutine 在 39 行无限的等待,第 40 行的 fmt.Println 永远不会被调用。\n在这个例子中,Goroutine 泄露很容易在 code review 中被发现。但是我无法列出 Goroutine 泄露的所有可能,但是这篇文章可以详细说可能遇到的一种 Goroutine 泄露:\n被遗忘发送者的泄露 这个泄露的例子,将会看到被无限阻塞的 Goroutine,等待发送值到 channel 中\n程序根据一些搜索词找到一条记录然后打印出来,该程序围绕着一个名为 search 函数构建:\nListing 2 https://play.golang.org/p/o6_eMjxMVFv\n29 // search simulates a function that finds a record based 30 // on a search term. It takes 200ms to perform this work. 31 func search(term string) (string, error) { 32 time.Sleep(200 * time.Millisecond) 33 return \u0026#34;some value\u0026#34;, nil 34 } search 函数在 Listing 2 的第 31 行,mock 了一个模拟在数据库中查询或者 web 调用的长耗时操作,这里硬编码成 200 ms。\n该程序调用 search 函数在 Listing 3 中显示如下:\nListing 3 https://play.golang.org/p/o6_eMjxMVFv\n17 // process is the work for the program. It finds a record 18 // then prints it. 19 func process(term string) error { 20 record, err := search(term) 21 if err != nil { 22 return err 23 } 24 25 fmt.Println(\u0026#34;Received:\u0026#34;, record) 26 return nil 27 } 在 Listing 3 的 19 行,定义了一个函数 process,这个函数接受一个 string 类型的参数作为搜索词。在 20 行,这个参数传入 search 函数返回一个结果或者错误。如果发生了错误,这个错误会在 22 行被返回,如果没有错误结果会在 25 行被打印出来。\n对于某些应用程序,顺序调用搜索的延时是不能接受的。假设无法使搜索运行的更快,可以将 process 函数改成不由 search 函数影响的延迟。\n为此可以使用 Goroutine,如下 Listing 4 所示。不幸的是,这是一次错误的尝试,造成了潜在的 Goroutine 泄露。\nListing 4 https://play.golang.org/p/m0DHuchgX0A\n38 // result wraps the return values from search. It allows us 39 // to pass both values across a single channel. 40 type result struct { 41 record string 42 err error 43 } 44 45 // process is the work for the program. It finds a record 46 // then prints it. It fails if it takes more than 100ms. 47 func process(term string) error { 48 49 // Create a context that will be canceled in 100ms. 50 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 51 defer cancel() 52 53 // Make a channel for the goroutine to report its result. 54 ch := make(chan result) 55 56 // Launch a goroutine to find the record. Create a result 57 // from the returned values to send through the channel. 58 go func() { 59 record, err := search(term) 60 ch \u0026lt;- result{record, err} 61 }() 62 63 // Block waiting to either receive from the goroutine\u0026#39;s 64 // channel or for the context to be canceled. 65 select { 66 case \u0026lt;-ctx.Done(): 67 return errors.New(\u0026#34;search canceled\u0026#34;) 68 case result := \u0026lt;-ch: 69 if result.err != nil { 70 return result.err 71 } 72 fmt.Println(\u0026#34;Received:\u0026#34;, result.record) 73 return nil 74 } 75 } 在 listing 4 的第 50 行,重写了 process 函数,创建了一个 Context 用于 100ms 后可以被 canceled。更多有关如何使用 Context 的内容可以参考 golang.org blog post。\n在 54 行,改程序创建了一个无缓冲的 channel,允许 Goroutine 通过这个 channel 传送 result 类型的数据。第 58 行到 61 行是一个匿名的 Goroutine 函数。这个 Goroutine 调用 search 函数,尝试通过 channel 发送它的返回值在第 60 行。\n在第 66 行的 case 中接受来自 ctx.Done() 的 channel。这个部分会在 Context 被 cancled(超过 100 ms 的时间)时被执行,如果该部分被执行 process 函数返回一个错误说明放弃在 67 行等待的 search 函数。\n另外在 68 行的 case 接收来自 ch channel 的值,把值赋给变量 result。实现的和之前一样,在 69 行和 70 行检查错误,如果没有错误在 72 行打印结果,然后返回 nil 表示成功。\n这次重构设置了 process 函数等待 search 函数的最长时间,可是在这个实现中埋下了 Goroutine 泄露的隐患。考虑一下 Goroutine 在代码中的运行情况,在第 60 行往 channel 中发送,此 channel 会阻塞发送直到另一个 Goroutine 做\n好接收的准备。在超时的情况下,接受者将会停止从 Goroutine 接受的等待,继续运行。这将导致 Goroutine 永远被阻塞直到一个新的接受者出现,当然这个永远不会发送,这就发生了 Goroutine 泄露。\nFix:多一点空间 解决这个泄露最简单的办法就是将 channel 从无缓存改成容量为 1 的缓存通道。\nListing 5 https://play.golang.org/p/u3xtQ48G3qK\n53 // Make a channel for the goroutine to report its result. 54 // Give it capacity so sending doesn\u0026#39;t block. 55 ch := make(chan result, 1) 现在 timeout 后,程序继续运行,搜索的 Goroutine 将结果发送到 channel 后返回。Goroutine 和 channel 的所占用的内存会很自然地被回收。\n在 The Behavior of Channels 中 William Kennedy 提供几个有关 channel 行为的几个例子,同时说明了它们其中的原理。文章中最后一个例子 Listing 10 也提到类似的超时例子。阅读这篇文章获得更多有关使用缓冲 channel 和合适的大小的建议。\n总结 虽然 Go 可以很容易使用 Goroutine,但是我们有责任更恰当的使用它们。在这篇文章中我举了一个错误使用 Goroutine 的例子。还有很多 Gotoutine 泄露的例子,以及在并发编程中还有可能碰到其他的陷阱。在以后的文章中,我将提供更多有关 Goroutine 泄漏和其他并发陷阱的例子。现在我会给你这个建议,任何时候你开始 Goroutine 你必须问自己:\n 它什么时候会终止? 什么会阻止它终止? 并发是一种有用的工具,但必须谨慎使用。\n","title":"Goroutine 泄露——被遗忘的发送者"},{"location":"https://codenow.me/algorithm/leetcode_9_palindrome_number/","text":" 题号:9 难度:Easy 链接:https://leetcode.com/problems/palindrome-number/\n Python代码\nclass Solution: def isPalindrome(self, x: int) -\u0026gt; bool: str_x = str(x) for i in range(0,int(len(str_x)/2)): if str_x[i] != str_x[-i-1]: return False return True","title":"Leetcode: 9 Palindrome Number"},{"location":"https://codenow.me/articles/java_initialization/","text":" 近期在学习Java语言,整理了部分初始化内容的知识点\n区分重载方法 当有多个方法拥有相同名字时,可以为每个方法设置不同的参数类型列表,从而实现方法的区分。\npublic class Test{ static f(String s, int i){ System.out.println(\u0026#34;String:\u0026#34; + s + \u0026#34;, int:\u0026#34; + i); } static f(String s, int i, int j){ System.out.println(\u0026#34;String:\u0026#34; + s + \u0026#34;, int:\u0026#34; + i + \u0026#34;, int:\u0026#34;+ j); } } 同样可以通过改变参数顺序重载方法:\npublic class Test{ static f(String s, int i){ System.out.println(\u0026#34;String:\u0026#34; + s + \u0026#34;, int:\u0026#34; + i); } static f(int i, String s){ System.out.println(\u0026#34;String:\u0026#34; + s + \u0026#34;, int:\u0026#34; + i); } } this关键字 this:调用对象的引用\n// Simple use of the \u0026#34;this\u0026#34; keyword public class Leaf { private int i = 0; private Leaf increment(){ i++; return this; // 返回调用对象的引用 } private void print(){ System.out.println(\u0026#34;i=\u0026#34; +i); } public static void main(String[] args) { Leaf x = new Leaf(); x.increment().increment().increment().print(); //increment返回this,所以可以直接但语句多次调用方法 } } 在构造器中调用构造器\n// Calling constructors with \u0026#34;this\u0026#34; public class Flower { private int petalCount = 8; private String s = \u0026#34;initial value\u0026#34;; Flower(int petals){ petalCount = petals; System.out.println(\u0026#34;petalCount= \u0026#34;+petalCount); } Flower(String s, int petals){ this(petals); // 使用this调用构造器 this.s = s; // 使用this调用对象引用 System.out.println(\u0026#34;s: \u0026#34;+this.s+\u0026#34;petalCount: \u0026#34;+this.petalCount); } } 可变参数列表 class A{} public class VarArgs { private static void printArray(Object... args){ // object... args 设置可变参数列表 for(Object obj: args){ System.out.println(obj+\u0026#34; \u0026#34;); } System.out.println(); } public static void main(String[] args) { printArray(47, 3.14, 11.11); // 直接传入参数 printArray(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;); printArray(new A(), new A(), new A()); } }","title":"JAVA初始化零碎知识点整理"},{"location":"https://codenow.me/articles/learning_the_shell_01/","text":" Shell 的定义 一个命令解释器 位于操作系统和应用程序之间 Shell 的作用 shell 负责把应用程序的输入命令信息解释给操作系统,将操作系统指令处理后的结果解释给应用程序。\nShell 的分类 图形界面式 桌面 命令行式\n windows 系统 (cmd.exe)\n Linux 系统 (sh, bash, zsh\u0026hellip;)\n 查看系统 shell 信息 echo $SHELL 查看系统支持的 shell cat /etc/shells Shell 的使用 手工方式 逐行输入命令,逐行进行确认执行\n 脚本方式 把执行命令写进脚本文件中,通过执行脚本达到执行效果\n shell 脚本定义 当可执行的 Linux 命令或语句不在命令行状态下执行,而是通过一个文件执行时,我们称文件为shell 脚本。\nshell 脚本示范 创建一个脚本 vim temp.sh 脚本内容\n# !/bin/bash 告知系统解释器执行路径 # it's a temp script echo 'hi' echo 'atts group' 执行脚本\nbash temp.sh 执行效果\nhi atts group ","title":"Shell 入门 01"},{"location":"https://codenow.me/tips/git_commit_multiple_lines_messages/","text":" 配置 git 调用 vim 编辑器\ngit config --global core.editor vim 使用 直接使用 git commit 调用\n","title":"调用 vim 来提交 Git Commit 多行注释"},{"location":"https://codenow.me/tips/mysql_backslash/","text":"MySQL 中,使用 like 匹配 / 时,前面要加 四个 反斜杠\n","title":"Mysql_backslash"},{"location":"https://codenow.me/algorithm/leetcode_17_lettercombinationsofaphonenumber/","text":" 题号:17\n难度:medium\n链接:https://leetcode.com/problems/letter-combinations-of-a-phone-number/ 描述:手机9键盘上,每个数字代表几个字母,给定几个数字,返回他们能组成的所有组合\n from functools import reduce from typing import List class Solution: def letterCombinations1(self, digits: str) -\u0026gt; List[str]: \u0026quot;\u0026quot;\u0026quot;一个一个数字地来就可以了,直接 reduce 解决\u0026quot;\u0026quot;\u0026quot; data = { '0': '', '1': '', '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' } if not digits: return [] if len(digits) == 1: return list(data[digits]) return reduce(lambda x, y: [i + j for i in x for j in y], [data[x] for x in digits]) def letterCombinations(self, digits: str) -\u0026gt; List[str]: \u0026quot;\u0026quot;\u0026quot;看了 discuss ,确实还可以用递归 就结果而言,递归比reduce多消耗了一点点空间,时间上则是相同的 \u0026quot;\u0026quot;\u0026quot; data = { '0': '', '1': '', '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' } if not digits: return [] if len(digits) == 1: return list(data[digits]) others = self.letterCombinations(digits[1:]) res = [a + b for a in data[digits[0]] for b in others] return res if __name__ == '__main__': digits = '237' print(Solution().letterCombinations(digits)) ","title":"Leetcode_17_LetterCombinationsOfAPhoneNumber"},{"location":"https://codenow.me/translation/explain_python_entry_point/","text":"原文链接:https://stackoverflow.com/a/9615473/7537990\nEntryPoints 提供了一个基于文件系统的,持久化的对象名称注册机制,以及一个基于名称的直接对象导入机制(由 setuptools 包实现)\n它们将一个 Python 对象的名称与自由格式标识符相关联。因此只要是使用了当前 Python 安装环境并且知道此标识符,任何代码都可以通过这个关联了的名称访问对应的对象,无论这个对象在什么地方被定义。这个被关联的名称可以是存在于一个 Python 模块的任意一个名称;例如一个类名,函数名,或者变量名。Entry point 机制并不在乎这个名称指向什么,只要它是可以被导入的。\n举个例子吧,我们写一个函数,以及一个有完全合格的名称的虚拟 python 模块“myns.mypkg.mymodule”:\ndef the_function(): \u0026quot;function whose name is 'the_function', in 'mymodule' module\u0026quot; print \u0026quot;hello from the_function\u0026quot; Entry points 通过 setup.py 里的 entry_points 声明来注册。要把 the_function 注册到 my_ep_func 这个 entrypoint 上,我们可以这样:\n\tentry_points = { 'my_ep_group_id': [ 'my_ep_func = myns.mypkg.mymodule:the_function' ] }, 在上面的例子中,entry points 是分组写的;有一个对应的 API 可以找到一个分组下的所有 entry points(如下示例)。\n在 package 被安装时(例如运行 python setup.py install),上面的声明会被传递给 setuptools。然后它会把传递过来的信息写到一个特殊的文件里。在那之后,我们就可以使用 pkg_resources_API(setuptools 的一部分)通过被关联的名称来查找 entry point 并且访问相应的对象了。\nimport pkg_resources named_objects = {} for ep in pkg_resources.iter_entry_points(group='my_ep_group_id'): named_objects.update({ep.name: ep.load()}) 在这里,setuptools 读取了写到特殊文件里的 entry point 信息,找到了 entry point,导入了对应的模块(myns.mypkg.mymodule),并且在调用 pkg_resources.load() 时检索到了在那里被定义的 the_function。\n假设这个分组内没有其他 entry point 被注册,那么调用 the_function 就会非常简单:\n\u0026gt;\u0026gt;\u0026gt; named_objects['my_ep_func']() hello from the_function 因此,即使在刚开始可能有点难以掌握,但这个 entry point 机制确实简单易用。它为可插拔 Python 软件开发提供了一个有用的工具。\n","title":"Explain_python_entry_point"},{"location":"https://codenow.me/articles/sentry-pushbear/","text":" 上周写了一下 sentry 可以通过 webhook 发送错误日志给微信,但是需要自己额外搭一个服务,虽然可定制化确实强,但还是有点麻烦\n于是五一期间给 sentry 写了个插件,简单安装之后,只需要填上 pushbear 的 SendKey,就可以直接在微信上接收错误提醒啦~\n效果如下:\n(这里缺上图,后面补)\n目前我配了这四个参数:project, culprit, message 和 tags。一般已经可以快速定位错误项目和位置了,至于具体的 trace,可以点击 message 直接进入 sentry 查看\n安装也超简单:\n如果 sentry 是 docker 安装的 在 onpremise/requirements.txt 里加上一行 sentry-pushbear docker-compose build: 拉取插件代码 docker-compose run --rm web upgrade: 更新 web 服务,如果插件有问题这里会报错 docker-compose up -d: 重启 sentry,插件生效 如果 sentry 是 python 安装的 pip install sentry-pushbear 重启 sentry 使用 pushbear 服务 在项目配置页,也就是 project-\u0026gt;{project}-\u0026gt;settings 页面里,点击 All integration(or plugins or Legacy Integrations) 页面,可以找到 PushBear Notifications 插件\n点击右侧开关启用插件后,再点击 configure plugin 就可以进入到当前 project 的插件配置页,如图\n在 SendKey 这一栏里填入从 pushbear 里申请的通道 SendKey,之后,就可以点 Test Plugin 进行测试了,微信上会立刻收到类似这样的消息,这就表示 pushbear 安装配置成功了\n(这里缺张图,后面补)\n再根据个人情况,配置一下 alert rules,比如只有 fatal 级别才发微信推送,点下保存,这就算完工了~\n顺便,如果有人没有用过 server酱,我也要强行安利一下:\npushbear 是在 server酱 右上角点击“一对多推送”即可,同样简洁,同样可以快速上手。\nserver酱:http://sc.ftqq.com/3.version\npushbear: https://pushbear.ftqq.com\n","title":"Sentry Pushbear"},{"location":"https://codenow.me/translation/making_first_contribution/","text":" 原链接: A Step by Step Guide to Making Your First GitHub Contribution \n前言 如果你还没有 Github 账户,或者不知道什么是 Git,请阅读:编程菜鸟?你昨天就应该把 Git 给学了\n拜见老师 希望你来这之前已经注册好 Github 账户并做好开始第一个开源项目贡献的准备了。 作为一个新手,贡献一个项目可能会觉得可怕。我明白,我也曾经试过。我花了很长时间才完成我的第一个 Pull Request。这就是为什么我希望你认识 Roshan Jossey 的原因。Roshan 建了一个 Github 仓库 First Contributions ,手把手带新人过一遍 Github 的贡献流程,还提供了一个仓库给大家做自己的第一个贡献。\n开始你的第一个开源项目贡献 1. Fork 仓库 打开 First Contributions 这个仓库,点击页面上的 Fork 按钮。这样会创建一份仓库备份到你自己的账户。 2. 克隆仓库 现在把这个仓库克隆到你自己的电脑上。点击 clone 按钮 再点击 copy to clipboard 图标。 打开终端并运行以下的 git 指令:\ngit clone \u0026quot;你刚刚拷贝到的地址\u0026quot; 你刚刚拷贝到的地址(不包括双引号)就是 First Contributions 这个仓库的地址。查看你之前的步骤去获得这个地址。 比如:\ngit clone https://github.com/this-is-you/first-contributions.git this-is-you 的地方就是你的 Github 用户名。 这样你就把你 Github 上这个仓库的内容拷贝到你的电脑了。\n3. 创建分支 进入到你电脑上的仓库目录\ncd first-contributions 使用 git checkout 指令创建一个分支\ngit checkout -b \u0026lt;add-your-name\u0026gt; 例子:\ngit checkout -b add-alonzo-church (分支的名字不一定要有 add 在里面,但是因为这个分支的目的是为了把你的名字加入名单中,所以有 add 是合理的。)\n4. 增加一些必要的修改并对修改进行 commit 现在用文本编辑器打开 Contributors.md 这个文件,把你的名字加在里面,然后保存。回到你的终端上的项目目录,执行 git status 指令,你就会看到这些修改。执行 git add 指令把这些修改加到你刚刚创立的分支上。\ngit add Contributors.md 现在用 git commit 指令提交修改\ngit commit -m \u0026quot;Add \u0026lt;your-name\u0026gt; to Contributors list\u0026quot; 用你自己的名字替代 \u0026lt;your-name\u0026gt;\n5. Push 修改到 Github 用 git push 指令 push 修改\ngit push origin \u0026lt;add-your-name\u0026gt; 用你早前创建的分支名字替代 \u0026lt;add-your-name\u0026gt;\n6. 确认你的修改 这时你去到你 Github 仓库页面上看,会发现一个 Compare \u0026amp; pull request 按钮。 点击按钮。 现在确认这个 pull request 然后我就会把你的修改合并到这个项目的主分支上。一旦修改被合并你就会收到一封提醒邮件。 你 fork 的主分支不会有改变。为了让你的 fork 与我的同步,请跟着下一步走。\n7. 让你的 fork 与这个仓库同步 首先,切换到主分支上\ngit checkout master 然后增加我仓库的地址为 upstream remote url\ngit remote add upstream https://github.com/Roshanjossey/first-contributions 这是在告诉 git 这个项目的另一个版本在这个地址上,我们称之为 upstream。一旦修改被合并,拉取这个仓库的最新版本。\ngit fetch upstream 这就是我们怎么拉取 fork(远程上游) 里所有修改的了。 现在你需要把我仓库的最新版本合并到你的主分支。\ngit rebase upstream/master 这里你在把你拉取的所有修改应用到主分支上。 如果你现在 push 主分支,你的 fork 也会有这些变化:\ngit push origin master 注意你这里 push 的远程对象叫 origin 这个时候我已经把你的分支 \u0026lt;add-your-name\u0026gt; 合并到我的主分支上了,你也把我的主分支合并到你自己的主分支上了。 你的分支不再用到了,你可以删掉它。\ngit branch -d \u0026lt;add-your-name\u0026gt; 你也可以删掉在远程仓库的分支的版本\ngit push origin --delete \u0026lt;add-your-name\u0026gt; 这不是必须的,但是这个分支的名字展示了它的主要目的。因此它的寿命相应的会很短。\n你做到啦! 现在你已经具备了在网上做开源项目贡献的能力。尽管放胆去试试吧!\n","title":"手把手带你创建你第一个 GitHub 贡献"},{"location":"https://codenow.me/algorithm/leetcode_938_range_sum_of_bst/","text":" 题号:938 难度:easy 链接:https://leetcode.com/problems/range-sum-of-bst/ 描述:返回一棵二叉搜索树两个节点之间的值的和,包括两个节点 class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None class Solution: def rangeSumBST(self, root: TreeNode, L: int, R: int) -\u0026gt; int: if not root: return 0 elif root.val \u0026lt; L: return self.rangeSumBST(root.right, L, R) elif root.val \u0026gt; R: return self.rangeSumBST(root.left, L, R) else: return root.val + self.rangeSumBST(root.right, L, R) + self.rangeSumBST(root.left, L, R)","title":"Leetcode_938: Range Sum Of BST"},{"location":"https://codenow.me/tips/goland-switch-project/","text":"给 manager project 设置快捷键,就可以快速切换项目\n","title":"Goland Switch Project"},{"location":"https://codenow.me/articles/db-without-fk/","text":"数据库的本质是存储数据,在这个之上还要维护数据的完整性。在维护完整性数据库提供几种方法,一种是事务,一种是外键 FK。这两种方式是分别处理两种情况,事务处理的是多个表中记录的原子性,FK 是处理多条有关系的记录。\n举个外键例子来说,假如有一个资源管理系统,你申请了许多资源,有电脑、账号、书籍、笔记本等好多东西,在你离职时候,这些记录需要全部删除,同时你的账号也要从这个系统中删除。这时候如果存在外键,user 表中的 id 字段关联了各种的资源 id,这时候需要一条 SQL 就可以删除这些记录。\n所以这么看外键貌似很有用,真实情况是真的很有用。但是我们从给一个角度来看,一条 SQL 需要级联多个表的操作,在 DB 引擎中可能同时需要多个行锁或者表锁,来保证完整性。同时还会会涉及到 IO 问题,我们都知道 IO 的随机读写其实是比较慢的,在访问频繁的的时候数据库往往不足以支撑那么大量的请求。\n一般这种观点的人会给出以下几个理由:\n 性能好 修改开放,易于维护 坚持使用外键的人也有几个理由:\n 数据完整性 表结构清晰 约束性好 无论怎么看都貌似很有道理,事实就是都对,但是要看在什么情况下使用,要从架构层面上取长补短。假如我们使用了无外键的设计,那么就要从架构补足缺点。举个例子来说,无外键设计一般会上移逻辑层到程序中,由程序要维护数据完整性,随着程序重启崩溃总是会出现一些游离数据。这些游离数据对系统究竟没有影响就很关键了,如果说这些游离数据不会影响系统正常运行,那么无外键的设计就很成功。典型的就是只需要根据 id 来获取某些数据,而不是通过某些条件来范围查询数据。\n这个其实很好理解,对于只用 id 获取的数据我只要保证这个 id 有没有没有被删除就 ok 了,那些关联的数据在这个 id 被删除以后就再也无法被找到了。\n如果说你正在做的是一个账号相关的系统,往往和账号相关的数据要求的完整性都很高,无法容忍出现游离数据,那么你只能使用外键来维护这种关系,虽然说上移到程序也能做,但是总不如数据库来的彻底。\n对于访问量大的表来说,一般会对这个表分片处理,分片的原理是根据某个字段把数据分散到多个片当中,这时候外键就无法起作用了,对于互联网公司来说分片做的都很多,外键设计变得越来越少。精明的架构师会用各种方法来补足无外键的缺点。\n总结一下,关于是不是要外键的回答可以肯定的是,要和不要都可以。具体要看业务如何拆分,数据容忍度有多高,以及架构师是否精明。如果以上没有一个,那么还是老老实实使用外键设计。\n","title":"数据库到底要不要外键"},{"location":"https://codenow.me/algorithm/leetcode_3_longest-substring-without-repeating-characters/","text":"class Solution: def lengthOfLongestSubstring(self, s: str) -\u0026gt; int: st = {} i, ans = 0, 0 for j in range(len(s)): if s[j] in st: i = max(st[s[j]], i) ans = max(ans, j - i + 1) st[s[j]] = j + 1 return ans;","title":"leetcode_3: Longest Substring Without Repeating Characters"},{"location":"https://codenow.me/translation/golang-inject/","text":" 原文地址\n 你可以在 github.com/steinfletcher/func-dependency-injection-go 找到完整的代码。这个例子包含了一个完整的 REST 的 HTTP 服务器。\n简介 在这篇文章中介绍了一种在 go 的实现依赖注入的方法——使用高阶函数和闭包\n下面是一个返回用户 profile 的接口。\nfunc GetUserProfile(id string) UserProfile { rows, err := db.Query(\u0026#34;SELECT ...\u0026#34;) ... return profileText } 我们希望将处理用户数据的代码和访问数据库的代码分开。在这个例子中我们希望对业务逻辑代码进行单元测试,同时为访问数据库提供 mock。让我们把这些问题分开,以满足单一原则。\n// domain layer function containing any business logic or mapping code func GetUserProfile(id string) User { ... } // database access layer function func SelectUserByID(id string) UserProfile { ... } 我们可以复用 SelectUserByID 函数在其他接口中。为了要对 GetUserProfile 进行单元测试和 mock 访问数据库,我们需要一种把 SelectUserByID 注入到 GetUserProfile 中的方法。一种方法利用 go 中对函数定义的类型别名实现。\nType aliases 使 GetUserProfile 依赖于一种抽象意味着我们可以在测试中注入 mock 的数据库访问。在 go 中实现这种操作的两中方法是 interface 机制或者 type alias。type alias 比较简单,不需要生成 struct,我们将这两个函数定义成另一种类型。\ntype SelectUserByID func(id string) User type GetUserProfile func(id string) UserProfile func NewGetUserProfile(selectUser SelectUserByID) GetUserProfile { return func(id string) string { user := selectUser(id) return user.ProfileText } } func selectUser(id string) User { ... return User{ProfileText: userRow.ProfileText} } SelectUserByID 是一个函数通过参数 user ID 返回 User 结构。我们不需要定义它的实现。NewGetUserProfile 是一个工厂方法依赖于参数 selectUser,然后返回一个可调用的函数。这种模式使用了闭包,让内部的函数可以从外部的函数中获取依赖项。闭包可以捕获上下文中变量和常量,这被称为对那些变量或者常量的 closing over。\n然后我们可以像这样使用这些函数\n// wire dependencies somewhere in the application getUser := NewGetUserProfile(selectUser) user := getUser(\u0026#34;1234\u0026#34;) 另一种观点 如果你熟悉像 Java 这样的语言,在创建累的时候将依赖类注入构造函数,然后在调用方法的时候调用依赖的方法。这些方法不存在功能上的差别 —— 你认为这是一个拥有单个抽象方法的函数 type alias,在 Java 中在构造函数中注入依赖。\ninterface DB { User SelectUser(String id) } public class UserService { private final DB db; public UserService(DB db) { // inject dependency into constructor this.DB = db; } public UserProfile getUserProfile(String id) { // access method User user = this.DB.SelectUser(id); ... return userProfile; } } 在 go 使用高阶函数等价的是\ntype SelectUser func(id string) User type GetUserProfile func(id string) UserProfile func NewGetUserProfile(selectUser SelectUser) { // factory method to inject dependency return func(id string) UserProfile { // access method user := selectUser(id) ... return userProfile } } 测试 我们现在可以对接口进行单元测试,为数据库访问提供 mock。\nfunc TestGetUserProfile(t *testing.T) { selectUserMock := func(id string) User { return User{name: \u0026#34;jan\u0026#34;} } getUser := NewGetUserProfile(selectUserMock) user := getUser(\u0026#34;12345\u0026#34;) assert.Equal(t, UserProfile{ID: \u0026#34;12345\u0026#34;, Name: \u0026#34;jan\u0026#34;}, user) } 你可以在 https://github.com/steinfletcher/func-dependency-injection-go 上找到更全面的代码示例。该示例包含一个 REST 的 http 服务器。\n","title":"「译」Golang 使用高阶函数实现依赖注入"},{"location":"https://codenow.me/tips/python_grave_accent/","text":"python 里的 `a` 等价于 repr(a)\n","title":"Python_grave_accent"},{"location":"https://codenow.me/articles/sentry-wechat/","text":"这周给 sentry 监控多做了一个发送途径\n目前我常用的 notification 途径有: 1. 邮件 2. slack 3. 微信\n对 sentry 来说,email 和 slack 已经是内置支持的了,甚至 slack 还支持对收到的消息进行操作,比如 assign 给某人,或者标记 resolved\n对微信的推送没有显式的支持,但 sentry 提供了 webhook,可以用这种方式来曲线救国\n因此需要两个东西: 1. 从 sentry 接收消息的服务 2. 能推送微信消息的服务\n对于第二点,现在有几个十分方便的解决方式,从最开始的 server酱,到升级版的 PushBear,使用体验都十分丝滑柔顺,也真的推荐大家前去实验\n那么第一点要怎么做?\nsentry 提供了官方的 webhook 工具:https://github.com/getsentry/webhook\n当然如果使用 docker 安装,或者版本够高的话,这个插件已经内置了。也就是填写一个 url,然后设定规则(比如默认规则是当某个 issue 第一次被触发时),那么当满足这个规则时,sentry 就会往这个链接发一个请求,同时在 json 里带上以下数据:\n{ 'project_name': 'woko', 'message': 'This is an example Python exception', 'id': '1002921092', 'culprit': 'raven.scripts.runner in main', 'project_slug': 'woko', 'url': 'https://sentry.io/organizations/woko/issues/0000000000/?project=00000000\u0026amp;referrer=webhooks_plugin', 'level': 'error', 'triggering_rules': [], 'event': {}, 'project': 'hcmlink', 'logger': None } 注意其中的 event 字段,内容其实超多,这里因为展示不方便我就删掉了,而且对提示来说,我们主要关注的是 project_name, message, culprit, level, 再加上一个跳转用的 url 就好了\n拿到这个数据后,无论是再发邮件,发微信,做错误分析,或者是其他任意什么操作,就都没有任何限制了\n我们可以先在本地启动一个简单的脚本\n from flask import Flask, request app = Flask(__name__) @app.route('/notice/pushbear') def pushbear(): print(request.json) return '' if __name__ == '__main__': app.run('0.0.0.0', 8888, debug=True) 然后在 sentry -\u0026gt; project -\u0026gt; settings -\u0026gt; alerts -\u0026gt; webhook 下填入我们启动的这个服务的链接:\nhttp://0.0.0.0:8888/notice/pushbear 然后尝试给 sentry 上推一个错\n可以在我们刚刚启动的服务里看到,sentry 发来了一个请求,带上了这些参数\n我们手动处理一下这些参数,拼成合适的 text 和 desp,就可以发送给微信了\n到这里,sentry 发送错误提示到微信的功能就已经完成了\n但是,我们可能没有合适的公网地址,来部署这个无关紧要的转发服务,这里有两个方案: 1. 在内网部署,给 sentry 的 webhook 地址里直接写内网地址 2. 把这个功能做成一个 sentry 插件\n对于方式二,要怎么做呢\n还是参考官方文档:https://docs.sentry.io/workflow/integrations/integration-platform\n我简单看了一下,这里允许自己写 integration 作为插件,这周时间不够了,五一期间我尝试写一下看看,如果能用的话,再来分享~\n","title":"Sentry Wechat"},{"location":"https://codenow.me/algorithm/leetcode_16_three_sum_closest/","text":" 题号:16\n难度:medium\n链接:https://leetcode.com/problems/3sum-closest 描述:一串数字中,找出和最接近 target 的三个数的和\n from typing import List class Solution: def threeSumClosest(self, nums: List[int], target: int) -\u0026gt; int: \u0026quot;\u0026quot;\u0026quot;直接参考上一题,target 减一下就跟 closest 0 一样\u0026quot;\u0026quot;\u0026quot; if not nums: return 0 nums.sort() closest = closest_delta = 0 for i in range(len(nums) - 2): if i \u0026gt; 0 and nums[i] == nums[i - 1]: continue l, r = i + 1, len(nums) - 1 while l \u0026lt; r: s = nums[i] + nums[l] + nums[r] delta = s - target if not closest_delta or abs(delta) \u0026lt; abs(closest_delta): closest_delta = delta closest = s if delta \u0026lt; 0: l += 1 elif delta \u0026gt; 0: r -= 1 else: return target return closest if __name__ == '__main__': data = [-1, 2, 1, 4] target = 1 print(Solution().threeSumClosest(data, target)) ","title":"Leetcode_16_three_sum_closest"},{"location":"https://codenow.me/algorithm/find-longest-path-in-dag/","text":"最近在开发 eslint 本地规则,里面遇到个问题,是要查找有向无环图中最长的路径。用了比较简单暴力的方法,backtracking + memo,具体代码看 这篇文章。\n","title":"Find Longest Path in Dag"},{"location":"https://codenow.me/tips/test-local-rule-eslint/","text":"运行本地规则,需要使用 --rulesdir 参数:\neslint --rulesdir rules/ test.js 通过上面这个命令,eslint 扫描 test.js 的时候,会加载 rules 文件夹下面的规则。\n不过,还需要注意一点,需要在 eslint 配置文件中配置要使用的本地规则名称,本地规则才能工作:\nmodule.exports = { \u0026#34;env\u0026#34;: { \u0026#34;browser\u0026#34;: true, \u0026#34;commonjs\u0026#34;: true, \u0026#34;es6\u0026#34;: true }, \u0026#34;extends\u0026#34;: \u0026#34;eslint:recommended\u0026#34;, \u0026#34;globals\u0026#34;: { \u0026#34;Atomics\u0026#34;: \u0026#34;readonly\u0026#34;, \u0026#34;SharedArrayBuffer\u0026#34;: \u0026#34;readonly\u0026#34; }, \u0026#34;parserOptions\u0026#34;: { \u0026#34;ecmaVersion\u0026#34;: 2018 }, \u0026#34;rules\u0026#34;: { \u0026#34;too_long_access_path\u0026#34;: \u0026#34;error\u0026#34; // 需要在 rules 中配置要使用的本地规则 } }; ","title":"让 eslint 运行本地规则"},{"location":"https://codenow.me/articles/tmux_hotkey/","text":" 启动 tmux: tmux\n 创建新命名会话和命名窗口: tmux new -s name -n name\n 恢复会话: tmux at [-t 会话名]\n 会话后台运行 prefix d\n 列出所有会话: tmux ls\n 关闭会话: tmux kill-session -t 会话名\n 关闭所有会话: tmux ls | grep : | cut -d. -f1 | awk \u0026lsquo;{print substr($1, 0, length($1)-1)}\u0026rsquo; | xargs kill\n 触发 tmux: ctr+b\n 新建窗口: prefix c\n 切换窗口: alt+窗口号(1,2,3)\n 退出窗口 prefix x\n 窗口重命名 prefix ,\n 左右分屏 prefix shift+|\n 上下分屏 prefix shift+-\n 左下上右 prefix hjkl\n 切换面板 prefix o\n 切换面板布局 prefix 空格\n 调整窗格大小 prefix HJKL\n 最大化当前窗格,再次执行可恢复原来大小 prefix z\n 复制 prefix y\n 黏贴 prefix p\n 向上翻阅 y 进入黄色模式,k向上 q 退出模式\n 多行缩进 进入 v 模式,选中,shift + \u0026lt; 或 \u0026gt;\n 用户配置(优先级更高) ~/.tmux.conf\n 系统配置 /etc/tmux.conf\n 查看快捷键 prefix ?\n 进入命令模式 prefix :\n \n参考链接: https://www.kancloud.cn/kancloud/tmux/62463 https://www.cnblogs.com/kaiye/p/6275207.html\n","title":"Tmux 快捷键"},{"location":"https://codenow.me/articles/custom-rule-for-eslint/","text":" 本文记录一下如何开发基于 estlint 的自定义检查规则。开发规则的官方文档:文档链接。如何在本地测试规则的方法,请参考 这篇文章。\n下面是一个实际的例子,用这个规则,可以检测下面这种代码:\nif (a.b \u0026amp;\u0026amp; a.b.c \u0026amp;\u0026amp; a.b.c.d \u0026amp;\u0026amp; a.b.c.d) { // send some ajax request } 自定义规则代码如下:\n// 逻辑表达式是一颗二叉树,对其进行 flatten 操作 // 比如 a \u0026amp;\u0026amp; b \u0026amp;\u0026amp; c \u0026amp;\u0026amp; d ,flatten 的结果 [a, b, c, d] function flattenLogicalExpression(node) { if (node.operator !== \u0026#39;\u0026amp;\u0026amp;\u0026#39;) { return [node] } let ret = [] if (node.left.type !== \u0026#39;LogicalExpression\u0026#39;) { ret.push(node.left) } else { ret = ret.concat(flattenLogicalExpression(node.left)) } if (node.right.type !== \u0026#39;LogicalExpression\u0026#39;) { ret.push(node.right) } else { ret = ret.concat(flattenLogicalExpression(node.right)) } return ret } // 对 MemberExpression 进行 flatten 处理 // 比如 a.b.c.d 表达式的 AST 树会被转换成 [a, b, c, d] 这样的节点数组 function flattenMemberExpression(node) { if (node.type === \u0026#39;Identifier\u0026#39;) { return [node.name] } if (node.type !== \u0026#39;MemberExpression\u0026#39;) { // impossible return [] } let ret = [] ret = ret.concat(flattenMemberExpression(node.object)) ret = ret.concat(flattenMemberExpression(node.property)) return ret } function longestPath(graph, start, memo) { if (memo[start] !== undefined) { return memo[start] } const nodes = graph[start] let count = 0 nodes.forEach(n =\u0026gt; { const c = longestPath(graph, n, memo) + 1 if (c \u0026gt; count) { count = c } }) memo[start] = count return count } function matchPattern(nodes) { // build graph const graph = [] nodes.forEach(() =\u0026gt; graph.push([])) nodes.forEach((n1, i1) =\u0026gt; { nodes.forEach((n2, i2) =\u0026gt; { if (i1 === i2) { return } if (n2.startsWith(n1)) { graph[i1].push(i2) } }) }) let count = 0 const memo = {} graph.forEach((_, i) =\u0026gt; { const c = longestPath(graph, i, memo) + 1 if (c \u0026gt; count) { count = c } }) return count \u0026gt;= 3 } const RuleHandlers = { \u0026#39;LogicalExpression\u0026#39;: function (node) { if (node.parent.type === \u0026#39;LogicalExpression\u0026#39;) { return } let nodes = flattenLogicalExpression(node) nodes = nodes.filter(n =\u0026gt; n.type === \u0026#34;MemberExpression\u0026#34;) nodes = nodes.map(n =\u0026gt; flattenMemberExpression(n)).map(n =\u0026gt; n.join(\u0026#39;.\u0026#39;)) if (matchPattern(nodes)) { this.context.report({ node: node, message: \u0026#34;Too long access path\u0026#34; }); } } } class Rule { constructor(context) { this.context = context for (let h in RuleHandlers) { this[h] = RuleHandlers[h].bind(this) } } } module.exports = { meta: { type: \u0026#34;suggestion\u0026#34; }, create: function (context) { return new Rule(context) } }; 核心的代码是 RuleHandlers ,每次遇到 LogicalExpression 的时候它的回调函数都会被执行。代码执行逻辑:\n 对 LogicalExpression 节点组成的树,做 flatten 处理,得到一组 MemberExpression 节点 对 MemberExpression 节点组成的树,做 flatten 处理,得到属性访问路径,比如 \u0026lsquo;a.b.c.d\u0026rsquo; 检测 MemberExpression 是否是嵌套访问了某个属性: 使用 MemberExpression 列表构造一颗有向无环图 求出从每个节点出发能够得到的最长路径,这里使用了回溯算法 判断最长路径是否超过 3 个节点,如果存在的话,那么说明代码存在问题 实际使用 对下面这段代码进行检查:\nconst a = {} if (a.b \u0026amp;\u0026amp; a.b.c \u0026amp;\u0026amp; a.b.c.d \u0026amp;\u0026amp; a.b.c.d.e) { const flag = a.b \u0026amp;\u0026amp; a.b.c \u0026amp;\u0026amp; a.c \u0026amp;\u0026amp; a.b.c.d if (flag) { a.b.c.d = 2 } } 运行结果:\n/Users/yangchen/tmp/too_long_access_path_rule/test.js 2:5 error Too long access path too_long_access_path 3:18 error Too long access path too_long_access_path","title":"自定义 eslint 规则"},{"location":"https://codenow.me/algorithm/jewels_and_stones/","text":" 题号:771 难度:easy 链接:https://leetcode.com/problems/jewels-and-stones/ 描述:看看自己手上的石头有多少是宝石 class Solution: def newJewelsInStones(self,J,S): res = [] for s in S: if s in J: res.append(s) return len(res) if __name__ == \u0026#39;__main__\u0026#39;: s = Solution() Jewels = \u0026#34;aA\u0026#34; Stones = \u0026#34;aAAbbbb\u0026#34; s.newJewelsInStones(Jewels, Stones) ","title":"Leetcode_771: Jewels and Stones"},{"location":"https://codenow.me/tips/mysql_date/","text":"这周发现目前在做的项目的一个地方,就是要读取特定时间的订单数据,发现这个数据并不准确。 后来找到原因是因为前端传过来的数据是 “YYYY-MM-DD” 模式, 但是数据库保存的数据是 “YYYY-MM-DD HH:MM:SS” 格式。\n所以查询语句从\nSELECT * FROM XX WHERE ORDER_DATE BETWEEN ORDER_START_TIME AND ORDER_END_TIME; 变成\nSELECT * FROM XX WHERE ORDER_DATE BETWEEN DATE(ORDER_START_TIME) AND DATE(ORDER_END_TIME); 不过,这样的话如果读取数据量大,会影响性能。假期后我应该会把数据在前端处理一下,而不是在数据库层面操作。\n","title":"Mysql 里的 DATE 函数"},{"location":"https://codenow.me/translation/commit_msg_guide/","text":" 原链接: Commit messages guide\n一个让你了解 commit 信息的重要性以及如何写好它们的指南。 本文可以帮助到你 - 了解什么是 commit - 写好一个 commit 信息为什么如此重要 - 最佳实践 - 一些关于计划、编写或重写好的 commit 历史记录的建议\n什么是 commit? 简单来说,一次 commit 就是对于你的本地文件的一份快照,写在你的本地仓库里。和某些人想法相反,git 不仅保存了文件之间的差异,而是保存了文件的完整版本。对于那些 commit 和 commit 之间没有发生变化的文件,git 保存了一个快捷链接,这个链接指向之前保存好的相同的文件。 下图展示了 git 是如何随着时间存储数据的,其中每个 version 表示一次 commit。\n()\n为什么写好一个 commit 信息如此重要 为了提高代码审核的效率,及简化代码审核的过程; 帮助了解历史变化; 去阐述代码无法解释的原因; 帮助未来的维护人员弄清楚为什么要更改和如何更改的,让排除故障和调试更容易。 为了最大化这些好处,我们接下来会举一些好例子和标准范本。\n好示范 这些都是我从自己经历、网络文章或其它的指南收集的例子。 如果你对它们有任何意见或建议,欢迎新开一个 pull request。\n用命令式 # 好的 Use InventoryBackendPool to retrieve inventory backend # 不好的 Used InventoryBackendPool to retrieve inventory backend 为什么要用命令式? 一次 commit 消息描述了相应改变实际执行的操作及其影响,而不是它做了什么。 Chris Beams 的这篇优秀的文章给了我们提供了一个很简单的句式,可以帮助我们用命令式写出更好的 commit 消息\nIf applied, this commit will \u0026lt;commit message\u0026gt; 例子:\n# 好的 If applied, this commit will use InventoryBackendPool to retrieve inventory backend # 不好的 If applied, this commit will used InventoryBackendPool to retrieve inventory backend 第一个字大写 # 好的 Add `use` method to Credit model # 不好的 add `use` method to Credit model 为什么要这样做?因为要遵守“一句话的第一个字母要大写”这个语法规则。 对于这个做法,人与人之间、团队与团队之间,甚至语言和语言之间都不一定是一样的。无论要不要第一个字母大写,更重要的是,定一个简单的标准,然后坚持遵守它。\n尝试不需要看源码就可以知道有什么变化 # 好的 Add `use` method to Credit model # 不好的 Add `use` method # 好的 Increase left padding between textbox and layout frame # 不好的 Adjust css 这个在很多场景都非常有用处,比如多次提交、多次修改和重构的时候。可以帮助审核者了解提交者的想法。\n用信息去解释为什么、为了什么、怎么做的和一些细节 # 好的 Fix method name of InventoryBackend child classes Classes derived from InventoryBackend were not respecting the base class interface. It worked because the cart was calling the backend implementation incorrectly. # 好的 Serialize and deserialize credits to json in Cart Convert the Credit instances to dict for two main reasons: - Pickle relies on file path for classes and we do not want to break up everything if a refactor is needed - Dict and built-in types are pickleable by default # 好的 Add `use` method to Credit Change from namedtuple to class because we need to setup a new attribute (in_use_amount) with a new value 这些信息的主题和征文是用一个空行分隔的。 一个多余的空行是被看作这个正文的一部分的。 同时,像 -,* ` 是可以提高可读性的元素。\n避免通用信息或没有内容的信息 # 不好的 Fix this Fix stuff It should work now Change stuff Adjust css 限制字数 建议主题少于 50 个字母,正文少于 72 个字母\n保持语言一致性 给项目持有人:选择一个语言,并用那个语言写所有的 commit 信息。更理想的是,和代码的注释、默认翻译区域设置(用本地化项目时)等保持一致。 给项目贡献者:查看项目历史 commit 信息,使用同一种语言写你的 commit 语句。\n# 好的 ababab Add `use` method to Credit model efefef Use InventoryBackendPool to retrieve inventory backend bebebe Fix method name of InventoryBackend child classes # 好的 (葡萄牙语例子) ababab Adiciona o método `use` ao model Credit efefef Usa o InventoryBackendPool para recuperar o backend de estoque bebebe Corrige nome de método na classe InventoryBackend # Bad (混合英语和葡萄牙语) ababab Usa o InventoryBackendPool para recuperar o backend de estoque efefef Add `use` method to Credit model cdcdcd Agora vai 模版 这是来自 Tim Pope 的一个模版,Pro Git Book 上有记载。\nSummarize changes in around 50 characters or less More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. In some contexts, the first line is treated as the subject of the commit and the rest of the text as the body. The blank line separating the summary from the body is critical (unless you omit the body entirely); various tools like `log`, `shortlog` and `rebase` can get confused if you run the two together. Explain the problem that this commit is solving. Focus on why you are making this change as opposed to how (the code explains that). Are there side effects or other unintuitive consequences of this change? Here's the place to explain them. Further paragraphs come after blank lines. - Bullet points are okay, too - Typically a hyphen or asterisk is used for the bullet, preceded by a single space, with blank lines in between, but conventions vary here If you use an issue tracker, put references to them at the bottom, like this: Resolves: #123 See also: #456, #789 Rebase 和 Merge 这个部分是来自 Atlassian 的 TL;DR 的一个很好的教程。Merging vs. Rebasing | Atlassian Git Tutorial\nRebase TL;DR:在你的基本分支基础上,逐步进行你的分支 commit,生成一棵新树。\nMerge TL;DR:创建一个新的 commit,成为一个合并 commit,两个分支间存在一些差异。 为什么有些人喜欢 rebase 多于 merge? 我本人喜欢 rebase 多于 merge,理由有如下: - 它生成一个干净的历史记录,没有不必要的合并 commit; - 所看即所得,比如,在审核代码时,所有的更改都来自一些特定的有根据的 commit,避免了一些隐藏在合并 commit 里的变化; - 提交者能解决更多的合并,以及在一个 commit 里的每个合并更改都有准确的信息; - 挖掘和审核合并 commit 是不寻常的,因此要尽量避免它们,确保所有的更改都有一个所属的 commit。\n什么时候 squash? Squashing 是指将多个 commit 合并成一个 commit 的过程。 在某些场合下很有用处,比如: - 减少一些没什么内容的 commit(拼写纠正、代码格式化、遗忘的东西等) - 把一些分开的更改一起应用,会更有意义 - 进展 commit 的重写工作\n什么时候应该避免 rebases 或 squash? 在公共 commit 或者共享分支上尽量避免用 rebase 和 squash。 rebase 和 squash 会重写历史和覆盖已存在的 commits,在共享分支上这样操作,因为冲突会引起混乱和导致某些人丢失他们的 commits(包括本地和远程)。\n有用的 git 命令 rebase -i 用它来 squash commits,修改信息,重写、删除或重新调整 commit 等。\npick 002a7cc Improve description and update document title pick 897f66d Add contributing section pick e9549cf Add a section of Available languages pick ec003aa Add \u0026quot;What is a commit\u0026quot; section\u0026quot; pick bbe5361 Add source referencing as a point of help wanted pick b71115e Add a section explaining the importance of commit messages pick 669bf2b Add \u0026quot;Good practices\u0026quot; section pick d8340d7 Add capitalization of first letter practice pick 925f42b Add a practice to encourage good descriptions pick be05171 Add a section showing good uses of message body pick d115bb8 Add generic messages and column limit sections pick 1693840 Add a section about language consistency pick 80c5f47 Add commit message template pick 8827962 Fix triple \u0026quot;m\u0026quot; typo pick 9b81c72 Add \u0026quot;Rebase vs Merge\u0026quot; section # Rebase 9e6dc75..9b81c72 onto 9e6dc75 (15 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into the previous commit # f, fixup = like \u0026quot;squash\u0026quot;, but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out fixup 用它可以很方便的清理 commits,没有 rebases 那么复杂的操作。这篇文章介绍了一些例子来展示如何用它和应该什么情况下用它。\ncherry-pick 这个当你在错误分支上进行 commit 时非常有用\n$ git cherry-pick 790ab21 [master 094d820] Fix English grammar in Contributing Date: Sun Feb 25 23:14:23 2018 -0300 1 file changed, 1 insertion(+), 1 deletion(-) add/checkout/reset [--patch | -p] 假如说我们有以下差异:\ndiff --git a/README.md b/README.md index 7b45277..6b1993c 100644 --- a/README.md +++ b/README.md @@ -186,10 +186,13 @@ bebebe Corrige nome de método na classe InventoryBackend `` # Bad (mixes English and Portuguese) ababab Usa o InventoryBackendPool para recuperar o backend de estoque -efefef Add `use` method to Credit model cdcdcd Agora vai `` +### Template + +This is a template, [written originally by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), which appears in the [_Pro Git Book_](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project). + ## Contributing Any kind of help would be appreciated. Example of topics that you can help me with: @@ -202,3 +205,4 @@ Any kind of help would be appreciated. Example of topics that you can help me wi - [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) - [Pro Git Book - Commit guidelines](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines) +- [A Note About Git Commit Messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 我们可以用 git add -p去加我们想要的补丁,不需要去更改已经写好的代码。 在把一个大更改分成几个 commits 时或者 reset/checkout 特定更改时非常有用。\nStage this hunk [y,n,q,a,d,/,j,J,g,s,e,?]? s Split into 2 hunks. hunk 1\n@@ -186,7 +186,6 @@ `` # Bad (mixes English and Portuguese) ababab Usa o InventoryBackendPool para recuperar o backend de estoque -efefef Add `use` method to Credit model cdcdcd Agora vai `` Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? hunk 2\n@@ -190,6 +189,10 @@ `` cdcdcd Agora vai `` +### Template + +This is a template, [written originally by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), which appears in the [_Pro Git Book_](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project). + ## Contributing Any kind of help would be appreciated. Example of topics that you can help me with: Stage this hunk [y,n,q,a,d,/,K,j,J,g,e,?]? hunk 3\n@@ -202,3 +205,4 @@ Any kind of help would be appreciated. Example of topics that you can help me wi - [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) - [Pro Git Book - Commit guidelines](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines) +- [A Note About Git Commit Messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) ","title":"Git Commit 指南"},{"location":"https://codenow.me/translation/docker_networking/","text":" 原文地址\n概况 Docker容器和服务如此强大的原因之一是可以将它们连接起来,或者将它们连接到非Docker工作负载中。Docker容器和服务甚至不需要知道它们部署在Docker上,或者它们的对等端是否也是Docker工作负载。无论Dokcer主机是运行在Linux,Windows还是两者兼有,都可以使用Docker以与平台无关的方式管理它们。\n网络驱动程序 Dokcer的网络子系统是可插拔的,使用网络驱动程序。默认情况下存在多个驱动程序,并提供核心网络功能。\n bridge:默认网络驱动程序。如果未指定驱动程序,则这将是默认创建的网络类型。当应用程序在需要通信的独立容器中运行时,通常会使用桥连网络。查看桥连网络 host:对于独立容器,删除容器和Docker主机之间的网络隔离,并直接使用主机的网络。host仅用于Docker17.06及更高版本的swarm服务。查看host网络 overlay:覆盖网络将多个Docker守护程序连接在一起,并使集群服务嫩巩固相互通信。可以使用覆盖网络来促进集群服务和独立容器之间的通信,或者在不同Docker守护程序上的两个独立容器之间进行通信。此策略消除了在这些容器之间执行OS级别路由的需要。查看overlay网络 macvlan:Macvlan网络允许为容器分配MAC地址,使其显示为网络上的物理设备。Docker守护程序通过其MAC地址将流量路由到容器。macvlan在处理期望直接连接到物理网络的传统应用程序时,使用驱动程序有时时最佳选择,而不是通过Docker主机的网络堆栈进行路由。查看Macvlan网络 none:对于此容器,禁用所有网络。通常与自定义网络驱动程序一起使用。none不适用于群组服务。查看none网络 网络插件:可以使用Docker按照和使用第三方网络插件。这些插件可以从Docker Hub或第三方供应商处获得。\n 网络驱动摘要 当需要多个容器在同一个Docker主机上进行通信时,用户定义的桥接网络是最佳选择。\n 当网络堆栈不应与Docker主机隔离时,主机网络是最好的。\n 当需要在不同Docker主机上运行的容器进行通信时,或者当多个应用程序使用swarm服务协同工作时,覆盖网络是最佳选择。\n 当从VM设置迁移或需要容器看起来像网络上的物理主机时,Macvlan网络是最佳的,每个主机都具有唯一的MAC地址。\n 第三方网络插件允许您将Docker与专用网络堆栈集成。\n ","title":"Dockerfile网络配置概述"},{"location":"https://codenow.me/tips/use_mysql_in_django/","text":"在项目settings.py中修改如下内容\nDATABASES={ 'default': { 'ENGINE': 'django.db.backends.mysql', # 数据库连接引擎 'NAME': '', # 数据库名 'USER': '', # 用户账号 'PASSWORD': '', # 用户密码 'HOST': 'localhost', 'PORT': 3306 # 连接端口 } } ","title":"Django中使用MySQL数据库"},{"location":"https://codenow.me/articles/dockerfile_commands1/","text":" FROM 格式:\nFROM\u0026lt;image\u0026gt;\u0026#91;AS \u0026lt;name\u0026gt;\u0026#93;\nFROM\u0026lt;image\u0026gt;\u0026#91;: \u0026lt;tag\u0026gt;\u0026#93; \u0026#91;AS \u0026lt;name\u0026gt;\u0026#93;\nFROM\u0026lt;image\u0026gt;\u0026#91;@ \u0026lt;digest\u0026gt;\u0026#93; \u0026#91;AS \u0026lt;name\u0026gt;\u0026#93;\n说明:\nFROM指令为后续指令创建一个基础镜像。所以所有的Dockerfile应该都是从FROM指令开始。初始化的镜像可以是任意合法的镜像。\n如果初始化的镜像在本地不存在,则会从公告仓库中获取镜像。\n RUN 格式:\nRUN \u0026lt;command\u0026gt; RUN \u0026#91;\u0026ldquo;executable\u0026rdquo;, \u0026ldquo;param1\u0026rdquo;, \u0026ldquo;param2\u0026rdquo;\u0026#93;\n说明:\nRUN指令会在当前镜像上生成新层,并在新层钟执行命令和提交结果。生成的新的镜像将用于下一步的Dockerfile。\nexec表单(第二种格式)被解析为JSON数组,这意味着必须使用双引号,不能使用单引号。\n\u0026ldquo;executable\u0026rdquo;中涉及到的路径必须转义反斜杠(\\)。\n CMD 格式:\nCMD \u0026#91;\u0026ldquo;executable\u0026rdquo;, \u0026ldquo;param1\u0026rdquo;, \u0026ldquo;param2\u0026rdquo;\u0026#93; (执行形式,这是首选形式)\nCMD \u0026#91;\u0026ldquo;param1\u0026rdquo;, \u0026ldquo;param2\u0026rdquo;\u0026#93; (作为ENTRYPOINT的默认参数)\nCMD command param1 param2 (shell形式)\n说明:\nDockerfile中只能有一个CMD指令,如果使用了多个,则只有在最后的CMD会生效。\n如果CMD用于为ENTRYPOINT指令提供默认参数,则应使用JSON数组格式指定CMD和ENTRYPOINT指令。\n LABEL 格式:\nLABEL \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; \u0026hellip;\n说明:\nLABEL指令将元数据添加到图像,LABEL使用键值对进行传值。\n镜像可以有多个标签,要指定多个标签,Docker建议LABEL尽可能将标签组合到单个指令中。如果使用多个LABEL,每条指令会生成一个新的图层,从而导致镜像效率低下。\n EXPOSE 格式:\nEXPOSE \u0026lt;port\u0026gt; \u0026#91;\u0026lt;port\u0026gt; /\u0026lt;protocol\u0026gt;\u0026hellip;\u0026#93;\n说明:\nEXPOSE指令通知Docker容器在运行时侦听指定的网络端口。如果未指定,默认侦听TCP。\n ENV 格式:\nENV \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;\nENV \u0026lt;KEY\u0026gt;=\u0026lt;value\u0026gt;\n说明:\nENV指令将环境变量\u0026lt;key\u0026gt;设置为\u0026lt;value\u0026gt; 。\n尽量使用单一ENV作为环境变量指令。这样会产生单个缓存层。\n要为单个命令设置值,请使用RUN \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt; \u0026lt;command\u0026gt; 。\n ADD 格式:\nADD \u0026lt;src\u0026gt;\u0026hellip;..\u0026lt;dest\u0026gt;\nADD \u0026#91;\u0026ldquo;\u0026lt;src\u0026gt;\u0026rdquo;,\u0026hellip;. \u0026ldquo;\u0026lt;dest\u0026gt;\u0026ldquo;\u0026#93; (包含空格的路径需要此表单)\n说明:\nADD指令从中复制新文件,目录或远程文件URL, 并将它们添加到路径上镜像的文件系统中。\n\u0026lt;src\u0026gt;可以指定多个资源,但如果它们是文件或目录,则它们必须相对于正在构建的源目录(构建的上下文)。\n\u0026lt;src\u0026gt;中可以包含通配符进行匹配。通配符使用Go的filepath.Match规则完成。\n\u0026lt;dest\u0026gt;是容器中的一个绝对路径,或相对于一个路径WORKDIR,\u0026lt;src\u0026gt;内容将在\u0026lt;dest\u0026gt;目录下进行内容复制。\n当路径中存在特殊字符时,需要使用Golang规则进行转义,以防止被视为匹配模式。\n当复制文件为压缩格式时,ADD会将压缩内容在\u0026lt;dest\u0026gt;下解压缩为目录。\n COPY 格式:\nCOPY \u0026lt;src\u0026gt;\u0026hellip; \u0026lt;dest\u0026gt;\nCOPY \u0026#91;\u0026ldquo;\u0026lt;src\u0026gt;\u0026rdquo;,\u0026hellip;. \u0026ldquo;\u0026lt;dest\u0026gt;\u0026ldquo;\u0026#93; (包含空格的路径需要此表单)\n说明:\nCOPY将\u0026lt;src\u0026gt;中的内容复制到容器的\u0026lt;dest\u0026gt;下。\nCOPY和ADD区别: COPY只能从本机上复制内容,而ADD可以从URL地址处获取内容。\n","title":"Dockerfile常见命令(一)"},{"location":"https://codenow.me/algorithm/leetcode_5_longest_palindromic_substring/","text":" 题号:5 难度:medium 链接:https://leetcode.com/problems/longest-palindromic-substring/\n 如下为Python3代码\nclass Solution: def longestPalindrome(self, s: str) -\u0026gt; str: if len(s) == 0: return s length = len(s) temp_bool = [[0]*length for i in range(length)] left = 0 right = 0 i = length - 2 while i \u0026gt;= 0: temp_bool[i][i] = 1 for j in range(i+1, length): temp_bool[i][j] = s[i] == s[j] and (j-i \u0026lt; 3 or temp_bool[i+1][j-1]) if temp_bool[i][j] and (right-left) \u0026lt; j-i: left = i right = j i = i - 1 return s[left:right+1]","title":"Leetcode: 3 Longest Palindromic Substring"},{"location":"https://codenow.me/tips/stfw/","text":"当有人提了一个可以 STFW 解决的问题时,可以在给出关键词的同时,甩一个这样的链接:\nhttp://lmgtfy.com/?q=如何安装python\n","title":"STFW"},{"location":"https://codenow.me/tips/switchtab_and_swiper_in_wechat/","text":" 场景说明: 在首页面中通过点击事件,跳转到另一个Tab页面中。同时根据点选的内容,在跳转到的Tab页面中,跳转到不同的加载不同的swiper。\n实现方式: 在Tab的wxml中将swiper设置不同的currentID,借此来标识不同的swiper。 在js中撰写swiper切换函数。具体可百度此部分内容。 在app.js中设置全局变量current。并在Tab页面Js中引入app。 将全局变量与Tab页面js中的current进行绑定。然后在首页面中设置跳转函数的同时,修改全局current。 实现效果: 通过首页中修改current,然后在Tab页面中根据全局current来实现不同swiper的转换。\n","title":"在微信小程序中切换Tab的同时挑战Swiper"},{"location":"https://codenow.me/articles/python_i18n/","text":" 这周有一些 API 需要做国际化,于是看了一下通用的解决方案\nGUN 国际化通用方法: gettext 通用的解决方案是,使用 gettext 工具,先将源码中的字符串提取至 .pot 文件中,再复制成各个 .po 文件并填充相应的语言,填充完毕后,再编译成 .mo 文件。\n读取的时候,从一个 .mo 文件里得到的内容会转换成一个简单的 hash map(python 中就是单纯的 dict),使用时根据语言和原始字符串到这里查找,即可得到相应的“翻译后的字符串”并返回。\n我们可以看一下前段时间刚刚上线的 python 官方简中文档,这是术语表的 .po 文件链接:https://github.com/python/python-docs-zh-cn/blob/3.7/glossary.po\n参照这个图片,可以看到, .po 文件里会把原文里的每个字符串,都这样对照着给一个翻译\n说到这里我们插播一下,python 中文文档翻译进度已经达到 30% 了,想参与翻译吗?参照我这个帖子开动吧,或者直接留言,我拉你进翻译群~\n说回到国际化,gettext 是一个 GNU 工具,各种语言对它都有支持,官方文档在这里:https://www.gnu.org/software/gettext/manual/gettext.html\n在 shell 下它一共提供三个命令: 1. xgettext: 从源文件中提取 .pot 文件 2. msginit: 将 .pot 准备成对应语言的 .po 文件 3. msgfmt: 将 .po 文件编译成 .mo 文件\n具体的用法就不详细说了,搜一下有很多,我们这里说一下 python 中对其的实现\npython 中内置了一个 gettext 包,其实是为 GUN gettext 提供一个 python 接口,使用时只要指定好语言以及 .mo 文件,就可以快速拿到 translation 对象,调用其 gettext 方法即可实现翻译\nPython 中的国际化: Babel 然而在 python 中,还有一个更好的库用来实现国际化: Babel,官方文档:http://babel.pocoo.org/en/latest\n 注:不是下一代 js 编译器的那个 babel,虽然名字一样\n Babel 相比内置的 gettext 包提供了更多好用的功能,比如类似于 Java 中的 Locale 类,它详细规定了一个语言包含的各个要素,比如\nfrom babel import Locale a = Locale.parse('zh_CN') 用 debug 模式看一下 a,删掉部分不太直观的内容后,我们至少可以看到这些:\na = {Locale} zh_Hans_CN character_order = {unicode} u'left-to-right' display_name = {unicode} u'中文 (简体, 中国)' english_name = {unicode} u'Chinese (Simplified, China)' first_week_day = {int} 6 language = {unicode} u'zh' language_name = {unicode} u'中文' min_week_days = {int} 1 script = {unicode} u'Hans' script_name = {unicode} u'简体' territory = {str} 'CN' territory_name = {unicode} u'中国' text_direction = {unicode} u'ltr' variant = {NoneType} None variants = {LocaleDataDict} \u0026lt;babel.localedata.LocaleDataDict object at 0x1103c6610\u0026gt; weekend_end = {int} 6 weekend_start = {int} 5 其中有几个参数是比较重要的,也是我们做国际化时需要注意的: 1. language: 语言,最基本的语言代码,比如 zh 是中文,en 是英文。遵循 ISO-639-1 2. territory: 国家和地区代码,例如 CN 是中国,HK 是香港。遵循 ISO 3166-1 3. script: 书写方式,例如 Hans 是简中,Hant 是繁中。遵循 ISO_15924 4. variant: 变体,没找到合适的资料,不过我猜是类似于 cmn 官话,yue 粤语,nan 闽南语这类的。不过这个字段只是存在,但并没有实际支持。\n 如果想具体了解,可以去看 RFC 5646,里面对这个东西做了详细定义\n 而在实际使用时,一般根据 language 和 territory 两项进行区分也就足够了\n同时,Babel 还干了好多事,比如提供了一个 pybabel 命令来代替 gettext 的那三句,以及对日期时间等的支持,用起来还是比较舒服的。\n最后,用在 flask 里,又是怎么做的呢?\nFlask 中的国际化: Flask-Babel 有一个很简单的库,叫 Flask-Babel,官方文档:https://pythonhosted.org/Flask-Babel\n这个库作为一个 flask 插件,也支持一些基本的功能: 1. 设置语言包目录 2. 设置默认语言 3. 设置在 request 上下文中获取语言的 callback 函数\n但是它有一个致命的问题:在每次请求中,如果当前请求中有字符串需要被翻译,它就会去遍历指定的语言包目录,尝试找到对应的 .mo 文件,并且 load 到内存,然后再进行翻译。\n注意,是每次请求都执行一遍整套操作\n我不知道这里为什么被设计成这样,但是脑海中自然地回想起清风老师对 flask 生态的评价“flask 的相关库真是一个赛一个得烂”\u0026hellip;\n好在是,为了保证“同一个进程上下文中只需要 load 一次”,它在执行时也会先判断 request.babel_locale 和 request.babel_translations,如果不存在再去尝试遍历 + load 文件。\n于是我的做法是提前把翻译文件加载上,再在 before_request 里设置上这两个变量,以防止它一直去加载\n以上,就是我在了解 python 国际化方案时查到的资料,放在这里给自己留个备份,也希望有人看见之后能有帮助~\n","title":"Python_i18n"},{"location":"https://codenow.me/algorithm/leetcode_15_threesums/","text":" 题号:15\n难度:medium 链接:https://leetcode.com/problems/3sum 描述:一串数字中,找出\u0026rdquo;和为0的三个数字\u0026rdquo;的所有可能组合\n from collections import defaultdict from typing import List class Solution: def threeSum_TLE(self, nums: List[int]) -\u0026gt; List[List[int]]: \u0026quot;\u0026quot;\u0026quot;这题有两个要点: 1. 遍历并找到所有组合: 尝试直接暴力 n^3 2. 保证最终结果里没有重复的: 尝试用集合来解决 能做出结果,但果然超时了 \u0026quot;\u0026quot;\u0026quot; if not nums: return [] res = [] dup = [] for _i, i in enumerate(nums, 1): for _j, j in enumerate(nums[_i:], 1): for k in nums[_i + _j:]: if not i + j + k: tmp = [i, j, k] if set(tmp) not in dup: dup.append(set(tmp)) res.append(tmp) return res def threeSum_TLE2(self, nums: List[int]) -\u0026gt; List[List[int]]: \u0026quot;\u0026quot;\u0026quot;考虑到超时应该是 n^3 太过分了,尝试用 n^2 来解决 先 n^2 遍历一遍,把\u0026quot;任意两个值的和\u0026quot;存起来 再遍历一遍,找到和为 0 的另外两个值 这里对 [0,0,0] 没法很好的判断,干脆单独给它做了个判断 结果:仍然超时 分析:除了遍历外,耗时最多的是去重 \u0026quot;\u0026quot;\u0026quot; if len(nums) \u0026lt; 3: return [] res = [] dup = [] two = set() # 遍历一遍,存上任意两个值的和 sums = defaultdict(list) for _i, i in enumerate(nums, 1): for j in nums[_i:]: if (j, i) not in sums[-i - j]: sums[-i - j].append((i, j)) pass if i == j: two.add(i) # 再遍历一遍,找出和为0的组合 for i in nums: if i in sums: for k, j in sums[i]: if k == i or j == i and i not in two: continue if {i, j, k} not in dup: res.append([i, j, k]) dup.append({i, j, k}) if nums.count(0) \u0026gt; 2: res.append([0, 0, 0]) return res def threeSum_TEL3(self, nums: List[int]) -\u0026gt; List[List[int]]: \u0026quot;\u0026quot;\u0026quot;意识到 set 里可以存 tuple,考虑这样处理一下 时间比上一个缩短了一半,但仍然超时 现在主要时间花在了构建 sums 字典上 \u0026quot;\u0026quot;\u0026quot; if len(nums) \u0026lt; 3: return [] res = [] dup = set() two = set() # 遍历一遍,存上任意两个值的和 sums = defaultdict(list) for _i, i in enumerate(nums, 1): for j in nums[_i:]: if (j, i) not in sums[-i - j]: sums[-i - j].append((i, j)) pass if i == j: two.add(i) # 再遍历一遍,找出和为0的组合 for i in nums: if i in sums: for k, j in sums[i]: if k == i or j == i and i not in two: continue tmp = sorted([i, j, k]) if tuple(tmp) not in dup: res.append([i, j, k]) dup.add(tuple(tmp)) if nums.count(0) \u0026gt; 2: res.append([0, 0, 0]) return res def threeSum(self, nums: List[int]) -\u0026gt; List[List[int]]: \u0026quot;\u0026quot;\u0026quot;去看了 discuss, 先排序,确实就完全是另一种思路了 不需要构建乱七八糟的辅助变量,按顺序走下去就可以了 去重问题,也因为是有序的,而直接跳过已处理好的相同变量 \u0026quot;\u0026quot;\u0026quot; if not nums: return [] nums.sort() res = [] for i in range(len(nums)-2): if i \u0026gt; 0 and nums[i] == nums[i-1]: continue l, r = i+1, len(nums)-1 while l \u0026lt; r: s = nums[i] + nums[l] + nums[r] if s \u0026lt; 0: l +=1 elif s \u0026gt; 0: r -= 1 else: res.append((nums[i], nums[l], nums[r])) while l \u0026lt; r and nums[l] == nums[l+1]: l += 1 while l \u0026lt; r and nums[r] == nums[r-1]: r -= 1 l += 1; r -= 1 return res if __name__ == '__main__': data = [-1, 0, 1, 2, -1, -4] # data = [3, 0, -2, -1, 1, 2] data = [0,0,0,0,0,0,0,0,0] print(Solution().threeSum(data)) ","title":"Leetcode_15_ThreeSums"},{"location":"https://codenow.me/translation/shoule_that_be_a_microservice/","text":" 这应该是微服务吗?记住这六个因素 原文链接:https://content.pivotal.io/blog/should-that-be-a-microservice-keep-these-six-factors-in-mind\n 2018年1月19日 \u0026gt; NATHANIEL SCHUTTA\n你写的代码比以往任何时候都多。关键是知道什么应该是微服务,什么不应该是。\n这些天里,你一直在听到所有人都在谈论微服务。开发者们正在学习 Eric Evan 有先见之明的书《Domain Driven Design》。团队正在重构单个的应用,寻找上下文界限并定义一个普及的语言。然而虽然已经有了无数的文章,视频和会谈来帮助你把现有服务转换成微服务,但很少有人会花费哪怕一点时间去问一句,这个应用真的应该是微服务吗?\n使用微服务架构有很多好的理由,但世界上没有免费的午餐,微服务的积极因素也增加了复杂性。团队应该乐于接受这个复杂性\u0026hellip;只要有问题的应用能够从微服务中收益。\n请微服务负起责任 Matt Stine 和我最近花了几天时间与客户一起浏览他们的一些应用。就像最近这些天一样,讨论开始于一个观点“一切都应该是微服务”。然后谈话就停滞不前,因为人们都在争论各种实现细节。\n这促使 Matt 在白班上写了一套原则。这些简单的陈述在剩下的日子里一直指导着我们。这些陈述让我们质疑应用架构的每个部分,寻找微服务可以产生价值的地方。这个清单根本性地改变了对话的基调,并且帮助团队设计出了不错的架构决策。\n为了让这个时间摆脱多余的微服务,我们提供了这个清单,来帮助你集中精力。阅读以下原则并且回答你们的应用是否受益于给定的原则。如果在一个或多个原则中你的回答都是“是”,那么微服务会是个不错的选择。如果所有的原则你的回答都是“否”,那么就有可能会在你的系统中引入意外的复杂性\n1. 多种变化率 你的系统中不同的部分是否需要以不同的速度往不同的方向发展?那么把他们分成微服务吧。这将允许每个组件拥有独立的生命周期。\n在任何系统中,都会有一些模块很少被触及,而其他模块似乎每次都会被改动。提了说明,我们举个例子,比如一个用于在线销售的单体电子商务应用。\n我们的购物车和库存函数在日常的开发工作中可能很少会被触及。但是我们可能会不断尝试我们的推荐引擎。我们还希望努力改善我们的搜索功能。将这两个模块拆分成微服务将允许各自的团队以更快的速度进行迭代,以允许我们快速交付业务价值。\n2. 独立的声明周期 如果一个模块需要一个完全独立的生命周期(意味着代码提交多给生产环境的流程),那么它应该是微服务。他应该拥有自己的代码仓库和 CI/CD 管道等。\n叫嚣的范围是的微服务的测试变得更加容易。我记得有一个项目拥有80个小时的回归测试套件!无需多言,我们并不会经常执行完整的回归测试(虽然我们真的很想。)微服务支持细粒度的回归测试。这颗牙节省我们无数个小时。并且我们也可能会更快发现问题。\n测试并不是我们拆分微服务的唯一原因。在某些情况下,一个业务需求也可能会将我们推向微服务架构。让我们看一下 Widget.io Monolith 示例。\n我们的业务领导者可能已经发现了一个新的机会 - 而且推向市场的速度至关重要。如果我们决定将所需的新功能添加到整体中,那会需要太长时间。我们无法按照业务需要的步伐前进。\n但作为独立的微服务,Project X(如下所示)可以拥有自己的部署管道。这种方法使我们能够快速迭代,并利用新的商机。\n3. 独立的可扩展性 如果系统各部分的负载或吞吐量特性不同,则它们可能具有不同的扩展要求。解决方案:将这些组件分离成独立的微服务!这样,服务可以以不同的速率扩展。\n即使粗略地回顾一下典型的架构,也会发现不同模块的不同扩展要求。让我们通过这个镜头回顾我们的Widget.io Monolith。\n大概率来说,我们的帐户管理功能并没有像订单处理系统那么重要。在过去,为了支持我们最易变的组件,我们却必须扩展完整的系统。这种方法导致了更高的基础设施成本,因为我们被迫为我们应用程序的一部分最糟糕的情况来“过度配置”。\n如果我们将订单处理功能重构为微服务,我们可以根据需要进行扩展和缩减。结果如下图所示:\n4. 隔离故障 有时我们希望将应用程序与特定类型的故障隔离开来。例如,当我们依赖于不符合我们的可用性目标的外部服务时会发生什么?我们可能会创建一个微服务来将该依赖关系与系统的其他部分隔离开来。从那里,我们可以在该服务中构建适当的故障转移机制。\n再次转向我们的示例 Widget.io Monolith,Inventory功能恰好与传统仓库系统进行交互,而后者的运行时间并不太好。我们可以通过将 Inventory 模块重构为微服务来保护我们的服务级别可用性目标。我们可能需要添加一些冗余来解决仓库系统的瑕疵问题。我们还可能会引入一些最终的一致性机制,例如 Redis 中的缓存库存。但就目前而言,向微服务的转变可以缓解不可靠的第三方依赖性导致的糟糕表现。\n5. 简化与外部依赖的交互(又名 Façade Pattern) 这个原则类似于“隔离失败”。扭曲:我们更注重于保护我们的系统免受经常变化的外部依赖。(这也可能是供应商依赖,其中一个服务提供商被换成另一个服务提供商,例如支付处理方的更换)。\n微服务可以充当间接层,使你免受第三方依赖。我们可以在核心应用程序和依赖项之间放置一个抽象层(我们可以控制的),而不是直接调用依赖项。此外,我们可以构建此层,以便我们的应用程序可以轻松使用,隐藏依赖项的复杂性。如果将来事情发生变化 - 而且你必须迁移 - 你的更改仅限于外观,而不是更大的重构。\n6. 为工作选择正确技术的自由 借助微服务,团队可以自由使用他们喜欢的技术栈。在某些情况下,业务需求应该适合特定的技术选择。其他时候,它受开发者偏好和熟悉程度的驱动。\n注意:这个原则不是光明正大下使用每种技术的许可!为你的团队提供有关技术选择的指导。太多不同的技术栈会增加认知开销,并且可能比标准化的“一刀切”模型更糟糕。在一个技术栈中为第三方库保持更新已经足够有挑战性了。将这个辛苦乘以四或五,你将会承担相当大的组织负担。做有用的,并且专注于为你懂得维护的技术栈“铺路”。\n在我们的Widget.io示例中,搜索功能可能会受益于与其他模块不同的语言或数据库选择。如果需要,可以直接执行此操作。当然,我们已经因为其他原因重构过它了!\n文化检查 刚才是技术讨论,那么现在,文化呢?\n真空中没有技术决定。因此,在你深入了解微服务的精彩世界之前,请先了解你的组织。你的组织结构是否轻松支持微服务架构?Conway\u0026rsquo; Law 觉得你可能会成功吗?\n五十年前 Mel Conway 认为,任何组织设计的系统都会创建一个反映其组织结构图的系统。换句话说,如果你的团队没有组织成小型的自治团队,那么你的工程师就不可能创建由小型自治服务组成的软件。这种认识促使了 Inverse Conway Maneuver。这鼓励团队更改其组织结构图以反映他们希望在其应用程序中看到的体系结构。\n你还应该考虑你的文化准备。微服务鼓励小的,频繁的变化 - 经常与传统的季度发布周期冲突的节奏。使用微服务,你将不会代码冻结或“大爆炸”代码集成。虽然基于微服务的体系结构肯定可以在传统的瀑布流环境中运行,但你不会看到完全的好处。\n考虑如何配置基础架构。专注于自助服务和优化价值流的团队通常采用微服务范式。像 Pivotal Cloud Foundry 这样的平台可以帮助您的团队快速地部署服务,测试和改进,这只需要几分钟,而不是几周(或几个月)。开发人员只需按一下按钮即可启动实例 - 这种做法可以促进实验和学习。构建自动化依赖关系管理,这意味着开发商和运营商可以专注于提供业务价值。\n最后,让我们问一下应用程序的两个具体问题:\n 此应用程序是否有多个业务所有者?如果一个系统有多个独立的自治企业主,那么它有两个不同的变化来源。这种情况可能会引发冲突。通过微服务,您可以实现“独立生命周期”并取悦这些不同的客户。 该应用程序是否由多个团队拥有?在单个系统上工作的多个团队的“协调成本”可能很高。相反,为它们定义API。从那里开始,每个团队都可以使用 Spring Cloud Contract 或 Pact 构建一个独立的微服务,用于consumer driven contracts测试。 对两个问题中任意一个的肯定回答,都将会将你引入微服务解决方案。\n总结 美好的微服务之路已经铺好。但是不止一些团队在没有首先分析他们的需求的情况下加入了这个行列。微服务很强大。它们绝对应该在您的工具箱中!只要确保你考虑权衡。理解您的应用程序的业务驱动因素是无可替代的,这对于确定适当的架构方法至关重要。\n","title":"Shoule_That_Be_A_Microservice"},{"location":"https://codenow.me/algorithm/leetcode-24-swap-nodes-in-pairs/","text":"24. Swap Nodes in Pairs\n代码如下:\n# Definition for singly-linked list. class ListNode(object): def __init__(self, x): self.val = x self.next = None class Solution(object): def swapPairs(self, head): \u0026#34;\u0026#34;\u0026#34; :type head: ListNode :rtype: ListNode \u0026#34;\u0026#34;\u0026#34; h = ListNode(0) tail = h p = head count = 0 while p != None: n = p.next if count % 2 == 0: tail.next = p p.next = None else: next_node = tail.next tail.next = p p.next = next_node tail = next_node count = count + 1 p = n return h.next","title":"Leetcode 24 Swap Nodes in Pairs"},{"location":"https://codenow.me/articles/how-to-orderby-in-mysql/","text":" 在 MySQL 中经常使用 Order by 对数据进行排序,其实排序这个行为是比较消耗 IO 的过程,有时候需要回表多次才可以完成排序,所以在任何时候都需要对排序的原理要心知肚明。\n在 MySQL 中排序按照是否使用外部存储可以分为,内存排序和外部排序两种。根据排序所需的字段可以分成 rowid 排序和全字段排序两种。\n在 MySQL 执行排序的时候会分配一块内存 sort_buffer,MySQL 把需要排序的字段放入这个 sort_buffer 中,让,后在 sort_buffer 执行排序的过程,如果 sort_buffer 大小不够,就要使用外部存储。\n一般来说 sort_buffer_size 的大小取决于排序的是使用快排(内存排序)还是归并排序(外部排序)。\n以上的排序过程都是使用全字段进行排序的,但是如果 sort_buffer 不足以存放所有排序字段,那么这时候就需要用到 rowid 排序。\n对于 rowid 排序支取 id 和需要排序的字段放入 sort_buffer 中,在 sort_buffer 开始对字段进行排序。根据排序完成后的 id 再回表找到其他字段组合成结果集返回。\n对于 rowid 排序和全字段排序最大差别在于多一次回表的过程,这也是一次 io 消耗过程(不一定是随机读过程)。同时扫描次数 rowid 会多余全字段排序 n 行,这个 n 就是第二次回表过程根据 id 找到的行的数量。\n对于 MySQL 来说如果内存够,就要多利用内存,尽量减少磁盘访问,只有分配的内存不够用的时候才会使用 rowid 排序。\n那么,这个过程如何优化?我们可以发现优化的地方有两个,一个是排序所消耗的时间,一个是回表再次读取的时间。所以优化就可以根据这两个来。\n对于回表这个操作经常和数据量有关系没有什么好办法,一种比较常用的方法就是建立复合索引以减少排序所耗费的时间。如果再 order by 时候字段满足最左匹配原则,那么这时候第一次从表加载到 sort_buffer 中本身就有序的,那么这时候可以直接当做结果返回了,就不需要排序了。\n最后,使用 explain 可以分析 SQL 的排序方式:\n Using index:覆盖索引 Using filesort:使用外部排序 没有 Using index 和 Using filesort:使用联合索引 参考 极客时间《MySQL实战45讲》:“order by” 是怎么工作的? ","title":"MySQL 排序机制"},{"location":"https://codenow.me/algorithm/leetcode_102_binary_tree_level_order_traversal/","text":" 题号:102\nmedium\n链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal/\n Python 递归实现\nclass Solution: def levelOrder(self, root: TreeNode) -\u0026gt; List[List[int]]: result = [] self.traverse(root, 0, result) return result def traverse(self, root, depth, result): if root is None: return if depth == len(result): result.append([]) result[depth].append(root.val) self.traverse(root.left, depth+1, result) self.traverse(root.right, depth+1, result)","title":"Leetcode 102. 二叉树的层次遍历"},{"location":"https://codenow.me/tips/command-line-in-goland/","text":"在 Goland 的 tools 下可以创建命令行工具,可以直接在终端通过 goland . 启动 Goland 并且创建项目。\n","title":"Command Line in Goland"},{"location":"https://codenow.me/translation/emulating-enumerated-types-in-golang/","text":" 原文地址 在这篇文章中,我们将介绍使用 go generate 和 abstract 语法树遍历生成强大的枚举类型。\n这篇文章描述用于生成的 CLI,完全的原代码 可以在 Github 上找到。\nGo 中惯用法 Go 语言实际上没有对枚举类型提供完成的支持。定义枚举类型的其中一种方法就是把一类相关变量定义成一种类型。Iota 可以用于定义连续的递增的整数常量。我们可以像这样定义一个 Color 类型。\nhttps://play.golang.org/p/1Zib29yiuFy\npackage main import \u0026#34;fmt\u0026#34; type Color int const ( Red Color = iota // 0 Blue // 1 ) func main() { var b1 Color = Red b1 = Red fmt.Println(b1) // prints 0 var b2 Color = 1 fmt.Println(b2 == Blue) // prints true var b3 Color b3 = 42 fmt.Println(b3) // prints 42 } 这种写法在 Go 代码中很常见,虽然这种方法很常用,但是有一些缺点。因为任何整数都可以给 Color 赋值,所以无法进行使用静态检查。\n 缺乏序列化——虽然这个不经常使用(开发者想要序列化这个整数,用于传参或者记录到数据库) 缺乏可读性的值——我们需要将 const 值转化成代码中显示的值 了解一种语言的习惯用法以及何时该打破这种习惯很重要。习惯用法往往会限制我们的 \u0026ldquo;视野\u0026rdquo;,这有时候恰恰是缺乏创造力的原因。\n设计枚举类型 简洁是 Go 语言最重要的特性之一,其他语言的开发者可以很快上手。从另一方面看,可能会产生约束,比如缺乏泛型机制导致许多重复的代码。为了克服这些缺点,社区已经使用代码生成作为定义更强大和灵活类型的机制。\n我们就使用这种方法来定义枚举类型,这种方法是使用生成的枚举作为 struct。我们还可以添加方法到 struct 中,struct 还支持 tag,这对于定义显示值和描述很有用。\ntype ColorEnum struct { Red string `enum:\u0026#34;RED\u0026#34;` Blue string `enum:\u0026#34;BLUE\u0026#34;` } 现在我们需要做的是给每个字段生成结构的实例。\nvar Red = Color{name: \u0026#34;RED\u0026#34;} var Blue = Color{name: \u0026#34;BLUE\u0026#34;} 添加方法到 Color struct 支持 JSON 编码/解码,我们实现 Marshaler 的 interface 支持 JSON 的编码。\nfunc (c Color) MarshalJSON() ([]byte, error) { return json.Marshal(c.name) } 在这个类型序列化时候将会调用我们的自定义实现。同样我们可以实现 Unmarshaler 的 interface,这将让我们可以在代码中使用类型——这允许我们直接在 API 的数据传输对象上定义枚举。\nfunc (c *Color) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, c.name) } 我们还可以定义一些辅助的方法来生成显示的值。\n// ColorNames returns the displays values of all enum instances as a slice func ColorNames() []string { ... } 我们也希望从字符串生成枚举实例,所以添加还需要添加这个方法。\n// NewColor generates a new Color from the given display value (name) func NewColor(value string) (Color, error) { ... } 这些行为都是可扩展的,你可以添加其他方法来返回名字,通过显示 Error() string 来支持错误,并且通过 String() string 来支持 Stringer。\n生成代码 遍历抽象语法树 在渲染模板之前,我们需要在源码中解析出 ColorEnum 类型。两种常用的方法是使用 reflet 包和 ast 包。我们需要扫描包级别的 struct。ast 包具有生成抽象语法树的能力——一种可表示 Go 源码的可遍历数据结构。我们可以遍历语法树并且匹配提供的类型,然后可以解析类型和定义的 struct tag,并用在构建模型已生成模板。我们先加载一个 go 包。\ncfg := \u0026amp;packages.Config{ Mode: packages.LoadSyntax, Tests: false, } pkgs, err := packages.Load(cfg, patterns...) pkgs 变量中包含每个文件的语法树。使用 ast.Inspect 方法来遍历 AST。它需要为每个遇到的节点调用一个函数,我们以此遍历每个文件并且处理该文件的语法树。\nfor _, file := range pkg.files { ... ast.Inspect(file.file, func(node ast.Node) bool { // ...handle node, check if it\u0026#39;s something we are interested in }) } 使用者应该定义这个函数,然后按照感兴趣的 token 类型进行过滤。你可以通过节点上的此检查来过滤。\nnode.Tok == token.STRUCT { ... } 在我们的例子中,通过定义了 \u0026ldquo;enmu:\u0026rdquo; 标签的 struct 进行过滤。我们只是处理了源码中每个标记,并根据遇到的数据类型进行模型(自定义 Go struct)的构建。\n生成源代码 有许多生成代码的方法。Stringer 工具使用 fmt 包标准输出。虽然这很容易实现,但是随着代码的生成的扩张,这将会变得难以操作和调试。更合理的方式是使用 text/template 包,并且使用 Go 强大的模板库。它允许从模板中分离生成模型的逻辑,从而可以关注点分离和让代码易于推理。生成的类型定义可能如下所示:\n// {{.NewType}} is the enum that instances should be created from type {{.NewType}} struct { name string } // Enum instances {{- range $e := .Fields}} var {{.Value}} = {{$.NewType}}{name: \u0026#34;{{.Key}}\u0026#34;} {{- end}} ... code to generate methods 然后我们可以使从模型中渲染出源码\nt, err := template.New(tmpl).Parse(tmpl) if err != nil { log.Fatal(\u0026#34;instance template parse error: \u0026#34;, err) } err = t.Execute(buf, model) 当我们在制作模板时候不要担心格式化的问题。format 包中有一个方法,它将源码作为参数并且返回格式化的 Go 代码,所以应该应该 Go 帮你处理这个问题。\nfunc Source(src []byte) ([]byte, error) { ... } 总结 在这篇中文,我们研究了一种通过解析 Go 源码来生成枚举类型的方法。此方法可以作为模板来构建所需要的源码和作为其他代码的生成器。我们使用 Go 的 text/template 库可以维护的方式呈现源码。\n可以在 Github 阅读 完整的代码。\n","title":"实现 Golang 枚举类型"},{"location":"https://codenow.me/articles/tidb-outer-join-elimination-and-column-pruning/","text":"今天整理 TiDB 的一种常见优化手段:列裁剪 (column pruning)\n列裁剪是很常见的一种优化手段,除了能够降低网络开销以外,也能够降低数据库的内存开销和 cpu 开销。比如下面这个查询:\nselect t1.id from t1, t2 where t1.id == t2.t1_id where t2.state = 1; 这个查询比较极端,t2 表在 join 之后,没有任何一列会再需要被使用。假如 t2 表里有十多个字段,而查询没有做列裁剪的话。那这十几个字段要在在整个查询树上流动,内存开销就得大出好几倍。\n代码中,是通过递归调用各个 LogicalPlan 子类的 PruneColumns 来完成列裁剪的,PruneColumns 方法会代用上层节点要求使用的列信息,子节点根据上层节点要使用的列信息以及自己要使用的列信息(比如:上个例子中 的 t2.t1_id 和 t2.state ,上层节点不会使用但是连接过程要使用)来计算哪些列可以被裁剪掉。\nJoin 的 PruneColumns 方法\nfunc (p *LogicalJoin) extractUsedCols(parentUsedCols []*expression.Column) (leftCols []*expression.Column, rightCols []*expression.Column) { for _, eqCond := range p.EqualConditions { parentUsedCols = append(parentUsedCols, expression.ExtractColumns(eqCond)...) } for _, leftCond := range p.LeftConditions { parentUsedCols = append(parentUsedCols, expression.ExtractColumns(leftCond)...) } for _, rightCond := range p.RightConditions { parentUsedCols = append(parentUsedCols, expression.ExtractColumns(rightCond)...) } for _, otherCond := range p.OtherConditions { parentUsedCols = append(parentUsedCols, expression.ExtractColumns(otherCond)...) } lChild := p.children[0] rChild := p.children[1] for _, col := range parentUsedCols { if lChild.Schema().Contains(col) { leftCols = append(leftCols, col) } else if rChild.Schema().Contains(col) { rightCols = append(rightCols, col) } } return leftCols, rightCols } func (p *LogicalJoin) PruneColumns(parentUsedCols []*expression.Column) { leftCols, rightCols := p.extractUsedCols(parentUsedCols) lChild := p.children[0] rChild := p.children[1] lChild.PruneColumns(leftCols) rChild.PruneColumns(rightCols) p.mergeSchema() } 代码逻辑:\n 将 parentUsedCols 拆分出 leftChild 和 rightChild 中的列 extractUsedCols 方法除了将 parentUsedCols 拆分以外,还会将 join 相关条件中的列信息拆分出来。 递归调用子节点的 PruneColumns 调用 mergeSchema 重新生成 join 的 schema (Schema 主要用来保存列信息) Aggregation 的 PruneColumns 方法:\n// PruneColumns implements LogicalPlan interface. func (la *LogicalAggregation) PruneColumns(parentUsedCols []*expression.Column) { child := la.children[0] used := getUsedList(parentUsedCols, la.Schema()) for i := len(used) - 1; i \u0026gt;= 0; i-- { if !used[i] { la.schema.Columns = append(la.schema.Columns[:i], la.schema.Columns[i+1:]...) la.AggFuncs = append(la.AggFuncs[:i], la.AggFuncs[i+1:]...) } } var selfUsedCols []*expression.Column for _, aggrFunc := range la.AggFuncs { // 从聚合函数中解析需要使用的列 \tselfUsedCols = expression.ExtractColumnsFromExpressions(selfUsedCols, aggrFunc.Args, nil) } if len(la.GroupByItems) \u0026gt; 0 { // 从分组表达式中解析需要使用的列 \tfor i := len(la.GroupByItems) - 1; i \u0026gt;= 0; i-- { cols := expression.ExtractColumns(la.GroupByItems[i]) if len(cols) == 0 { la.GroupByItems = append(la.GroupByItems[:i], la.GroupByItems[i+1:]...) } else { selfUsedCols = append(selfUsedCols, cols...) } } // If all the group by items are pruned, we should add a constant 1 to keep the correctness. \t// Because `select count(*) from t` is different from `select count(*) from t group by 1`. \tif len(la.GroupByItems) == 0 { la.GroupByItems = []expression.Expression{expression.One} } } child.PruneColumns(selfUsedCols) } 除了 parentUsedCols,Aggregation 需要从聚合函数和分组表达式解析出聚合查询需要使用的列。然后递归调用子节点的 PruneColumns 方法。\n","title":"TiDB 源码学习:列裁剪"},{"location":"https://codenow.me/translation/how_to_help_people_migrating_from_pip_to_conda/","text":"原文地址: https://discuss.python.org/t/how-to-help-people-migrating-from-pip-to-conda/1474\n有两种方法可以解决这样的迁移问题:减少摩擦,或者提供胡萝卜(有意义的好处)。\nConda已经提供了胡萝卜,它起作用了。一般来说,这足以克服摩擦(尽管并不总是——见我上面的引言)。\n实际上,我们看到的是非Conda用户的挫折感,他们没有胡萝卜,但还不足以克服摩擦。人们越接近孤立的工作包安装而不需要Conda,就会产生更多的虚构。\n简言之,基于venv的工具的“成长故事”很弱,因为如果不重新启动,您将无处可逃。解决这个问题不需要任何其他的软件包管理器——他们完全可以自由地说:“是的,我们告诉过你,当你依赖于系统安装时,错误是正确的,但是我们很高兴你现在在这里,让我们开始工作。”如果他们的论点是,基于venv的工具是最初的问题,那么他们不必从中找到放弃的地方,从中走出来。\n(Conda和其他系统包管理器在这里都是可互换的。没有人希望apt/yum安装的软件包只适用于高度定制的venv,对吗?)\n","title":"如何帮助人们从pip迁移到conda?"},{"location":"https://codenow.me/translation/golang-profile-with-pprof/","text":"原文链接:Profile your golang benchmark with pprof\ngolang 自带的工具非常有用,我们可以使用 go 自带的 pprof 来做性能分析。\n我们用 Dave Cheney 当中的例子\npackage bench import \u0026#34;testing\u0026#34; func Fib(n int) int { if n \u0026lt; 2 { return n } return Fib(n-1) + Fib(n-2) } func BenchmarkFib10(b *testing.B) { // run the Fib function b.N times for n := 0; n \u0026lt; b.N; n++ { Fib(10) } } 在我们的终端中:\ngo test -bench=. -benchmem -cpuprofile profile.out 我们同样可以做内存分析:\ngo test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out 然后我们可以使用 pprof 做分析:\ngo tool pprof profile.out File: bench.test Type: cpu Time: Apr 5, 2018 at 4:27pm (EDT) Duration: 2s, Total samples = 1.85s (92.40%) Entering interactive mode (type \u0026quot;help\u0026quot; for commands, \u0026quot;o\u0026quot; for options) (pprof) top Showing nodes accounting for 1.85s, 100% of 1.85s total flat flat% sum% cum cum% 1.85s 100% 100% 1.85s 100% bench.Fib 0 0% 100% 1.85s 100% bench.BenchmarkFib10 0 0% 100% 1.85s 100% testing.(*B).launch 0 0% 100% 1.85s 100% testing.(*B).runN 在这个例子里,我用的是 profile.out,但是你也可以使用 memprofile.out 做同样的事情。\n然后你也可以使用 list 命令来检查函数当中哪些地方很占时间。\n(pprof) list Fib 1.84s 2.75s (flat, cum) 148.65% of Total . . 1:package bench . . 2: . . 3:import \u0026quot;testing\u0026quot; . . 4: 530ms 530ms 5:func Fib(n int) int { 260ms 260ms 6: if n \u0026lt; 2 { 130ms 130ms 7: return n . . 8: } 920ms 1.83s 9: return Fib(n-1) + Fib(n-2) . . 10:} 或者通过 web 命令生成一张图(也可以使用 png、pdf,都支持的)\n(pprof) web PS:\n如果你只想运行基准测试,不想运行程序中的单元测试,你可以执行下面的命令:\ngo test -bench=. -run=^$ . -cpuprofile profile.out 使用 -run=^$ 表示运行所有符合正则表达是的测试用例,但是 ^$ 不会匹配到任何测试。所以它只运行基准测试。\n","title":"Golang Profile With Pprof"},{"location":"https://codenow.me/tips/perf-cache-miss/","text":"perf stat -e L1-dcache-load-misses PROGRAM","title":"Perf Cache Miss"},{"location":"https://codenow.me/algorithm/reverse_link/","text":" 题目:单链表反转\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; 6 -\u0026gt; 7 -\u0026gt; 8 -\u0026gt; 9 -\u0026gt; None\n9 -\u0026gt; 8 -\u0026gt; 7 -\u0026gt; 6 -\u0026gt; 5 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; None\n单链表的反转可以使用循环,也可以使用递归的方式\n 1. 构造链表 1) 一次性构造无头结点链表 class Node: def __init__(self, data=None, next=None): self.data = data self.next = next link = Node(1, Node(2, Node(3, Node(4, Node(5, Node(6, Node(7, Node(8, Node(9))))))))) 2)循环构造有头结点链表 class LNode: def __init__(self, data=None, next=None): self.data = data self.next = next i = 1 head = LNode() cur = head # 构造单链表 while i \u0026lt;= 9: temp = LNode() temp.data = i temp.next = None cur.next = temp cur = temp i += 1 2. 使用循环的方式实现单链表反转 1) 无头结点的单链表反转 #!/usr/bin/env python # coding = utf-8 class Node: def __init__(self, data=None, next=None): self.data = data self.next = next def reverse_link(link): # 将原链表的第一个节点变成了新链表的最后一个节点,同时将原链表的第二个节点保存在cur中 pre = link # 下面两行不能反过来,反过来后link的next就为None了 cur = link.next pre.next = None # 从原链表的第二个节点开始遍历到最后一个节点,将所有节点翻转一遍 # 以翻转第二个节点为例 # temp = cur.next是将cur的下一个节点保存在temp中,也就是第节点3,因为翻转后,节点2的下一个节点变成了节点1,原先节点2和节点3之间的连接断开,通过节点2就找不到节点3了,因此需要保存 # cur.next = pre就是将节点2的下一个节点指向了节点1 # 然后pre向后移动到原先cur的位置,cur也向后移动一个节点,也就是pre = cur, cur = temp # 这种就为翻转节点3做好了准备 while cur: temp = cur.next cur.next = pre pre = cur cur = temp return pre if __name__ == \u0026#39;__main__\u0026#39;: # 构造链表 link = Node(1, Node(2, Node(3, Node(4, Node(5, Node(6, Node(7, Node(8, Node(9))))))))) # 打印反转前链表 print(\u0026#34;before reverse:\u0026#34;, end=\u0026#34; \u0026#34;) cur = link while cur: print(cur.data, end=\u0026#34; -\u0026gt; \u0026#34;) cur = cur.next else: print(\u0026#34;None\u0026#34;) # 反转链表 reversed_pre = reverse_link(link) # 打印反转后链表 print(\u0026#34;After reverse:\u0026#34;, end=\u0026#34; \u0026#34;) while reversed_pre: print(reversed_pre.data, end=\u0026#34; -\u0026gt; \u0026#34;) reversed_pre = reversed_pre.next else: print(\u0026#34;None\u0026#34;) C:\\python_workspace\\venv\\Scripts\\python.exe C:/python_workspace/reverse_node.py before reverse: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; 6 -\u0026gt; 7 -\u0026gt; 8 -\u0026gt; 9 -\u0026gt; None After reverse: 9 -\u0026gt; 8 -\u0026gt; 7 -\u0026gt; 6 -\u0026gt; 5 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; None 2) 有头结点的单链表反转 #!/usr/bin/env python class LNode: def __init__(self, data=None, next=None): self.data = data self.next = next def reversed_link(head): if head == None or head.next == None: return # 把链表首结点变成尾结点 pre = head.next cur = pre.next pre.next = None while cur: temp = cur.next cur.next = pre pre = cur cur = temp head.next = pre if __name__ == \u0026#39;__main__\u0026#39;: # 初始化变量设置与初始化链表头结点 i = 1 head = LNode() cur = head # 构造单链表 while i \u0026lt;= 9: temp = LNode() temp.data = i cur.next = temp cur = temp i += 1 # 打印逆序前链表 print(\u0026#34;before reverse:\u0026#34;, end=\u0026#34; \u0026#34;) cur = head.next while cur: print(cur.data, end=\u0026#34; -\u0026gt; \u0026#34;) cur = cur.next else: print(\u0026#34;None\u0026#34;) # 反转链表 reversed_link(head) # 打印反转后链表 print(\u0026#34;After reverse:\u0026#34;, end=\u0026#34; \u0026#34;) cur = head.next while cur: print(cur.data, end=\u0026#34; -\u0026gt; \u0026#34;) cur = cur.next else: print(\u0026#34;None\u0026#34;) C:\\python_workspace\\venv\\Scripts\\python.exe C:/python_workspace/reverse_link3.py before reverse: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; 6 -\u0026gt; 7 -\u0026gt; 8 -\u0026gt; 9 -\u0026gt; None After reverse: 9 -\u0026gt; 8 -\u0026gt; 7 -\u0026gt; 6 -\u0026gt; 5 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; None 3) 用简单的链表表示方法: #!/usr/bin/python class LNode: def __init__(self, data=None, next=None): self.data = data self.next = next def reverse_link(head): if not head and not head.next: return 0 cur = head.next temp = cur.next cur.next = None pre = cur cur = temp while cur: temp = cur.next cur.next = pre pre = cur cur = temp head.next = pre if __name__ == \u0026#39;__main__\u0026#39;: list_node = LNode(\u0026#34;head\u0026#34;, LNode(1, LNode(2, LNode(3, LNode(4, LNode(5, LNode(6, LNode(7, LNode(8, LNode(9)))))))))) head = list_node while head.next: print(head.data, end=\u0026#34; \u0026#34;) head = head.next else: print(head.data) reverse_link(list_node) head = list_node while head: print(head.data, end=\u0026#34; \u0026#34;) head = head.next C:\\python_workspace\\venv\\Scripts\\python.exe C:/python_workspace/reverse_link4.py head 1 2 3 4 5 6 7 8 9 head 9 8 7 6 5 4 3 2 1 Process finished with exit code 0","title":"用Python实现单链表反转"},{"location":"https://codenow.me/tips/docker_remove/","text":" 1. docker run \u0026ndash;rm -v 在Docker容器退出时,默认容器内部的文件系统仍然被保留,以方便调试并保留用户数据。 但是,对于foreground容器,由于其只是在开发调试过程中短期运行,其用户数据并无保留的必要,因而可以在容器启动时设置\u0026ndash;rm选项,这样在容器退出时就能够自动清理容器内部的文件系统。示例如下:\ndocker run --rm centos4 # 等价于 docker run --rm=true centos4 显然,\u0026ndash;rm选项不能与-d同时使用,即只能自动清理foreground容器,不能自动清理detached容器 注意,\u0026ndash;rm选项也会清理容器的匿名data volumes。 所以,执行docker run命令带\u0026ndash;rm命令选项,等价于在容器退出后,执行docker rm -v。\nrm只是删除容器,rm -v 不仅删除容器(如果容器有使用卷,卷也会进行相应的删除)。\n2. 范例 [root@lvs-webserver2 run]# docker run --rm=true --name=centos4 -it centos [root@2c1d3bebde29 /]# [root@lvs-webserver2 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2c1d3bebde29 centos \u0026#34;/bin/bash\u0026#34; 23 seconds ago Up 21 seconds centos4 [root@2c1d3bebde29 /]# exit exit [root@lvs-webserver2 run]# [root@lvs-webserver2 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES","title":"Docker容器退出时,自动删除容器"},{"location":"https://codenow.me/articles/docker_process/","text":" 1. 查看Docker进程 在Linux系统中,启动Docker服务,会看到如下进程:\nCentOS7: [root@lvs-webserver2 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c8a999e1b510 ubuntu \u0026#34;/bin/bash\u0026#34; 34 seconds ago Up 32 seconds ubuntu [root@lvs-webserver2 ~]# [root@lvs-webserver2 ~]# ps -ef|grep docker|grep -v grep root 40362 1 1 18:14 ? 00:00:08 /usr/bin/dockerd root 40373 40362 2 18:14 ? 00:00:14 docker-containerd --config /var/run/docker/containerd/containerd.toml root 40758 40373 0 18:23 ? 00:00:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/c8a999e1b510abc2136384742f9ce8fa082d297e83af07d50c8b0d8f47254609 -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd -runtime-root /var/run/docker/runtime-runcUbuntu18 root@lvs-master:/var/run# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e96ce4c8c256 redisexec \u0026#34;/usr/bin/redis-serv…\u0026#34; 11 days ago Up 9 minutes 6379/tcp redisexec root@lvs-master:/var/run# root@lvs-master:/var/run# ps -ef|grep docker |grep -v grep root 1039 1 0 18:24 ? 00:00:03 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock root 2852 884 0 19:10 ? 00:00:00 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/e96ce4c8c2569e5209504626275a5db15f88fbd1d778ae9ac69c79536690c11f -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc 2. 进程说明 2.1 Docker Daemon 或者叫Docker Engine /usr/bin/dockerd是由Docker服务启动的第一个进程,它是整个Docker服务端启动的入口\n2.2 docker-containerd Centos7中有这个过程,Ubuntu18中貌似合并到了/usr/bin/dockerd 它是Dockerd进程的子进程,是Docker服务端的核心进程,负责与Docker客户端进行通信交互,与Docker容器之间进行交互,执行docker run命令,fork出Docker容器进程,几乎所有的核心操作都发生在这里\n两者都有一个参数*.sock。意思是打开一个sock描述符,实现所有的Docker容器和Docker客户端(个人理解就是宿主机)之间的通信 CentOS7中是在/var/run/docker/containerd/containerd.toml中有定义\n[grpc] address = \u0026#34;/var/run/docker/containerd/docker-containerd.sock\u0026#34; uid = 0 gid = 0 max_recv_message_size = 16777216 max_send_message_size = 16777216 Ubuntu18中是在\n/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock 2.3 docker-containerd-shim 如果docker run命令启动一个容器,就会生成一个docker-containerd的子进程docker-containerd-shim,这个进程运行着镜像\n3. 总结 Docker的进程模型为: dockerd守护进程fock出docker-containerd子进程,用来管理所有容器 docker-containerd进程fork出docker-containerd-shim子进程,该进程中运行了具体的镜像\n","title":"Docker的进程"},{"location":"https://codenow.me/articles/about_mysql_locks_2/","text":" 作者:杨一\n原链接: 漫谈死锁\n 一、前言 死锁是每个 MySQL DBA 都会遇到的技术问题,本文自己针对死锁学习的一个总结,了解死锁是什么,MySQL 如何检测死锁,处理死锁,死锁的案例,如何避免死锁。\n二、死锁 死锁是并发系统中常见的问题,同样也会出现在 Innodb 系统中。当两个及以上的事务,双方都在等待对方释放已经持有的锁或者因为加锁顺序不一致造成循环等待锁资源,就会出现\u0026rdquo;死锁\u0026rdquo;。\n举例来说 A 事务持有 x1锁 ,申请 x2 锁,B 事务持有 x2 锁,申请 x1 锁。A 和 B 事务持有锁并且申请对方持有的锁进入循环等待,就造成死锁。\n从死锁的定义来看,MySQL 出现死锁的几个要素:\na 两个或者两个以上事务。 b 每个事务都已经持有锁并且申请新的锁。 c 锁资源同时只能被同一个事务持有或者不兼容。 d 事务之间因为持有锁和申请锁导致了循环等待。\n三、MySQL 的死锁机制 死锁机制包含两部分:检测和处理。 把事务等待列表和锁等待信息列表通过事务信息进行 wait-for graph 检测,如果发现有闭环,则回滚 undo log 量少的事务;死锁检测本身也会算检测本身所需要的成本,以便应对检测超时导致的意外情况。 3.1 死锁检测 当 InnoDB 事务尝试获取(请求)加一个锁,并且需要等待时,InnoDB 会进行死锁检测。正常的流程如下:\n InnoDB 的初始化一个事务,当事务尝试申请加一个锁,并且需要等待时 (wait_lock),innodb 会开始进行死锁检测 (deadlock_mark) 进入到 lock_deadlock_check_and_resolve() 函数进行检测死锁和解决死锁 检测死锁过程中,是有计数器来进行限制的,在等待 wait-for graph 检测过程中遇到超时或者超过阈值,则停止检测。 死锁检测的逻辑之一是等待图的处理过程,如果通过锁的信息和事务等待链构造出一个图,如果图中出现回路,就认为发生了死锁。 死锁的回滚,内部代码的处理逻辑之一是比较 undo 的数量,回滚 undo 数量少的事务。 3.2 如何处理死锁 《数据库系统实现》里面提到的死锁处理:\n 超时死锁检测:当存在死锁时,想所有事务都能同时继续执行通常是不可能的,因此,至少一个事务必须中止并重新开始。超时是最直接的办法,对超出活跃时间的事务进行限制和回滚\n 等待图:等待图的实现,是可以表明哪些事务在等待其他事务持有的锁,可以在数据库的死锁检测里面加上这个机制来进行检测是否有环的形成\n 通过元素排序预防死锁:这个想法很美好,但现实很残酷,通常都是发现死锁后才去想办法解决死锁的原因\n 通过时间戳检测死锁:对每个事务都分配一个时间戳,根据时间戳来进行回滚策略\n 四、Innodb 的锁类型 首先我们要知道对于 MySQL 有两种常规锁模式\n LOCK_S(读锁,共享锁)\n LOCK_X(写锁,排它锁)\n 最容易理解的锁模式,读加共享锁(in share mode),写加排它锁。\n有如下几种锁的属性:\nLOCK_REC_NOT_GAP (锁记录) LOCK_GAP (锁记录前的 GAP) LOCK_ORDINARY (同时锁记录+记录前的GAP,也即 NextKey 锁) LOCK_INSERT_INTENTION (插入意向锁,其实是特殊的 GAP 锁) 锁的属性可以与锁模式任意组合。例如:\nlock -\u0026gt;type_mode 可以是 Lock_X 或者 Lock_S locks gap before rec 表示为 gap 锁: lock -\u0026gt;type_mode \u0026amp; LOCK_GAP locks rec but not gap 表示为记录锁,非 gap 锁: lock -\u0026gt;type_mode \u0026amp; LOCK_REC_NOT_GAP insert intention 表示为插入意向锁: lock -\u0026gt;type_mode \u0026amp; LOCK_INSERT_INTENTION waiting 表示锁等待: lock -\u0026gt;type_mode \u0026amp; LOCK_WAIT 五、Innodb 不同事务加锁类型 例子:\nupdate tab set x=1 where id= 1 ; 索引列是主键,RC 隔离级别 对记录记录加 X 锁\n 索引列是二级唯一索引,RC 隔离级别 若 id 列是 unique 列,其上有 unique 索引。那么 SQL 需要加两个 X 锁,一个对应于 id unique 索引上的 id = 10 的记录,另一把锁对应于聚簇索引上的[name=\u0026rsquo;d\u0026rsquo;,id=10]的记录。\n 索引列是二级非唯一索引,RC 隔离级别 若 id 列上有非唯一索引,那么对应的所有满足 SQL 查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。\n 索引列上没有索引,RC 隔离级别 若 id 列上没有索引,SQL 会走聚簇索引的全扫描进行过滤,由于过滤是由 MySQL Server 层面进行的。因此每条记录,无论是否满足条件,都会被加上 X 锁。但是,为了效率考量,MySQL 做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了 2PL 的约束。\n 索引列是主键,RR 隔离级别 对记录记录加 X 锁\n 索引列是二级唯一索引,RR 隔离级别 对表加上两个 X 锁,唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。\n 索引列是二级非唯一索引,RR 隔离级别 结论:Repeatable Read 隔离级别下,id 列上有一个非唯一索引,对应 SQL:delete from t1 where id = 10;\n 首先,通过 id 索引定位到第一条满足查询条件的记录,加记录上的 X 锁,加 GAP 上的 GAP 锁,然后加主键聚簇索引上的记录 X 锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录 X 锁,但是仍旧需要加 GAP 锁,最后返回结束。\n 索引列上没有索引,RR 隔离级别则锁全表 这里需要重点说明 insert 和 delete 的加锁方式,因为目前遇到的大部分案例或者部分难以分析的案例都是和 delete,insert 操作有关。\ninsert 的加锁方式\n划重点 insert 的流程(有唯一索引的情况): 比如 insert N\n 找到大于 N 的第一条记录 M,以及前一条记录 P\n 如果 M 上面没有 gap/next-key lock,进入第三步骤,否则等待(对其 next-rec 加 insert intension lock,由于有 gap 锁,所以等待)\n 检查 P:判断 P 是否等于 N:\n 如果不等: 则完成插入(结束) 如果相等: 再判断 P 是否有锁, a 如果没有锁:报 1062 错误(duplicate key),说明该记录已经存在,报重复值错误 b 加 S-lock,说明该记录被标记为删除, 事务已经提交,还没来得及 purge c 如果有锁: 则加 S-lock,说明该记录被标记为删除,事务还未提交. 该结论引自: http://keithlan.github.io/2017/06/21/innodblocksalgorithms/\ndelete 的加锁方式\n 在非唯一索引的情况下,删除一条存在的记录是有 gap 锁,锁住记录本身和记录之前的 gap\n 在唯一索引和主键的情况下删除一条存在的记录,因为都是唯一值,进行删除的时候,是不会有 gap 存在\n 非唯一索引,唯一索引和主键在删除一条不存在的记录,均会在这个区间加 gap 锁\n 通过非唯一索引和唯一索引去删除一条标记为删除的记录的时候,都会请求该记录的行锁,同时锁住记录之前的 gap\n RC 情况下是没有 gap 锁的,除了遇到唯一键冲突的情况,如插入唯一键冲突。\n 引自文章 MySQL DELETE 删除语句加锁分析\n六、如何查看死锁 查看事务锁等待状态情况 select * from information_schema.innodb_locks; select * from information_schema.innodb_lock_waits; select * from information_schema.innodb_trx; 下面的查询可以得到当前状况下数据库的等待情况:via《innodb技术内幕中》\nselect r.trx_id wait_trx_id, r.trx_mysql_thread_id wait_thr_id, r.trx_query wait_query, b.trx_id block_trx_id, b.trx_mysql_thread_id block_thrd_id, b.trx_query block_query from information_schema.innodb_lock_waits w inner join information_schema.innodb_trx b on b.trx_id = w.blocking_trx_id inner join information_schema.innodb_trx r on r.trx_id =w.requesting_trx_id 打开下列参数,获取更详细的事务和死锁信息 innodb_print_all_deadlocks = ON innodb_status_output_locks = ON 查看 innodb 状态(包含最近的死锁日志)\nshow engine innodb status; 七、如何尽可能避免死锁 事务隔离级别使用 read committed 和 binlog_format=row ,避免 RR 模式带来的 gap 锁竞争。\n 合理的设计索引,区分度高的列放到组合索引前列,使业务 sql 尽可能的通过索引定位更少的行,减少锁竞争。\n 调整业务逻辑 SQL 执行顺序,避免 update/delete 长时间持有锁 sql 在事务前面,(该优化视情况而定)。\n 选择合理的事务大小,小事务发生锁冲突的几率也更小;\n 访问相同的表时,应尽量约定以相同的顺序访问表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;\n 5.7.15 版本之后提供了新的功能 innodb_deadlock_detect 参数,可以关闭死锁检测,提高并发 TPS。\n ","title":"漫谈死锁"},{"location":"https://codenow.me/tips/linux_tail/","text":"用 tail 命令看实时的日志文件\ntail -f log_path ","title":"用 tail 实时查看日志"},{"location":"https://codenow.me/algorithm/same_tree/","text":" 题号:100 难度:easy 链接:https://leetcode.com/problems/same-tree/ 描述:查看两棵树是否一致 class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None class Solution: def isSameTree(self, p: TreeNode, q: TreeNode) -\u0026gt; bool: if p and q: return p.val == q.val and self.isSameTree(p.right,q.right) and self.isSameTree(p.left,q.left) return p == q if __name__ == \u0026#39;__main__\u0026#39;: s = Solution() tree1 = TreeNode(\u0026#39;s\u0026#39;) tree2 = TreeNode(\u0026#39;s\u0026#39;) s.isSameTree(tree1, tree2)","title":"Leetcode_100: same tree"},{"location":"https://codenow.me/translation/sharingpwswithgopass/","text":" 作者: Woile 原链接: Sharing passwords using gopass, git and gpg \n 不想再把你的密码放在不可靠却方便的地方? 不想再在 slack,notes 这些不可信赖的平台上分享密码? 不想再在不同的地方放你的团队密码? 如果对于上面的问题你的回答如果是 yes,那你应该会觉得这篇文章有用。\n背景 我为了寻找一个安全的方式去储存我的密码花了不少时间。当然,除了安全,我还希望有以下性能:\n 密码存在云里 设备之间可以简单同步 可以很方便地与人共享 我找到的解决方案是 gopass gopass 是怎么工作的 gopass 就像是有多一份电池的 pass(unix 密码管理器) 在这里面,它拥有的且和我相关的性能有\n 用 gpg 进行加密; 用 git 进行密码同步; 不同属性的密码可以放在不同的储存地方(个人、公司等); 每个存储地方指向不同的仓库; 一个存储地址支持多个人运用。接下来我们称他们为收信人 虽然它还缺乏一些文档和命令,但是我们不必害怕去尝试它。 我很高兴 gopass 使用了 gpg。 它唯一的缺点,我想是它正式的 windows 版本还没发布。\n安装 你可以看看这个网站里介绍的安装或者可以在 gopass repo 里得到更多的信息。\n使用 首先,我们需要一个 gpg 密钥。我们需要 gpg cli 去创建一个,如果你安装了 gopass,那你的系统里应该就有了。\ngpg 是怎么工作的 在 gopass 的文本中,我们需要用 gpg 提供的公钥和私钥。 想象一下你有很多的公钥,万一它们被锁了,只能用你手上拥有的私钥才能打开。 从此我们可以总结出两件事情:\n 你可以分发你的公钥并且让任何人对它的信息进行加密。比如说我把公钥给朋友了,他们放了一些东西进去并锁住,只有我可以打开它。当然,我也可以自己加密自己的东西,以防别人窥探。 私钥非常重要。私钥要安全的使用,不要丢失私钥,要备份私钥。你可以用加密的随身硬盘、放在安全地方的小纸条或者 yubikey。\n创建密钥 只要跟着指令操作就可以了,非常简单。 如果你不知道填什么,用默认值也可以\ngpg --full-generate-key 查看生成好的密钥\ngpg -k 初始化 gopass gopass init 我建议在你的终端里加上这个 autocomplete(自动补全指令)\necho \u0026quot;source \u0026lt;(gopass completion bash)\u0026quot; \u0026gt;\u0026gt; ~/.bashrc 使用 gopass gopass 就像 unix,你有一棵树(不同的文件夹),树上有叶子(加密文件)。\ngopass ├── my-company │ └── pepe@my-company.com └── personal └── pepe@personal.com 我们开始来加入加密信息吧。\ngopass insert personal/twitter/santiwilly 你会被提示需要输入两次密码 我是按照这个结构去输入的(其中很多是选择性的) {store}/{org}/{env}/{username or email}\n接下来列出我们所有的信息\ngopass ls 我们可以看到这样\ngopass ├── my-company │ └── pepe@my-company.com └── personal ├── pepe@personal.com └── twitter └── santiwilly 接下来,我给你展示更多简单的指令\n展示密码 gopass personal/twitter/santiwilly 复制密码到剪切板 gopass -c personal/twitter/santiwilly 生成随机的密码 gopass generate my-company/anothername@rmail.com 搜索加密信息 gopass search @gmail.com 使用存储地址 这里有点复杂。 存储地址(AKA mounts)可以让你管理你的密码。比如私人密码和公司密码。每一个存储地址放在不同的仓库里,然后你可以指定公司密码的存储地址分享给你的同事。\n新建新的存储地址 在 ~/.password-store-my-company 里新建一个存储地址\ngopass init --store my-company 同步到远程 gopass git remote add --store my-company origin git@gh.com/Woile/keys.git 克隆已有的存储地址 假如说你有另一个电脑,这个时候 gopass 就派上用场了。你可以用一样的私钥或者选择一个机器一个私钥,去克隆你的仓库。你只需要进入它。\ngopass clone git@gh.com/Woile/keys.git my-company --sync gitcli 指定 gitcli 为 sync 方法非常重要,不然 gopass 会不知道怎么去同步加密信息(默认是用 noop)。gopass 还提供了其它同步方法。 提供了免费的私人仓库的平台有 gitlab,github 和 bitbucket。\n移除已有的存储地址 为了避免有什么冲突,我们首先需要卸载存储地址\ngopass mounts umount my-company 然后操作完上一步后,我们可以安全的移除文件夹了。\nrm -rf ~/.password-store-my-company 同步 在 gopass,同步等同于 git pull 和 git push,可能还有 git commit。\n和远程进行同步 gopass sync 和一个存储地址进行同步 gopass sync --store my-company 团队共享 我们终于到了最后和最美妙的部分了。 共享密信。 假如我们的同事有个邮箱 logan@pm.me。这个人已经在他的电脑里用那个邮箱生成了 gpg 密钥。 他要解析公钥并把它发给我们。\ngpg -a --export logan@pm.me \u0026gt; logan.pub.asc 公钥是可以放在不可靠的地方的。 如果你不是很确信,那就用 firefox send。 记住人们一般在密钥服务器分享公钥的,像 opengpgkeyserver。\n增加公钥到 gopass 里 我们有公钥了,现在把它加进我们的本地\ngpg --import \u0026lt; logan.pub.asc 最后,我们需要添加新的收信人到 gopass 存储地址\ngopass recipients add logan@pm.me 你会见到你的存储地址所有的提示。 选择你想要的方式,它会用新的公钥重新加密你的信息。(除了已存在的) 这样我们就完成了。 你当然还可以移除收信人。 你自己查一下怎么操作吧。 提示: gopass recipients \u0026ndash;help\n结论 我弄了一份 gopass cheatsheet 和 一个 presentation。 gopass 是一个很棒的工具。不幸的是对于非开发者可能有点门槛。 下面是一些我用来加强 gopass 操作用到的其它工具。\nAndroid password store 我建议用 f-droid 来安装它,你需要 OpenKey-chain 来创建一个新的 gpg 密钥\ngopass bridge firefox 或 chrome 上的插件,可以让你登录你的存储地址。\ngopass ui 在命令行里使用 gopass 的基于 electron 的 ui 软件。 提供了丰富的图形界面去搜索和管理你的密码。\n欢迎任何反馈,我不是安全专家,如果有更好的更安全的工作流可以告诉我,我很高兴。\n","title":"使用 gopass,git 和 gpg 来分享你的密码"},{"location":"https://codenow.me/translation/why_django/","text":"原文地址\n如今,许多的后端开发人员选择使用Python。Python已经称为最受欢迎的Web开发语言之一,它的灵活性和多功能行是它能够取得如此成功的重要原因之一。从开发简单代码到数据分析和机器学习,Python已成为许多开发人员的首选语言。\n有许多框架是可以和Python一起使用的,这些框架基本上允许开发人员选择一个平台,他们可以根据自己的喜好自定义并自由测试。在Python的所有框架中,Django似乎是最受欢迎的选择。实际上,在2018年的Stock Overflow Survey中,Django被列为最受欢迎的框架之一,58%的开发者投票支持。\n很明显,许多开发人员更喜欢使用Django进行Web项目开发,如果您尚未使用Django并且想知道热点是关于什么,这里有一些原因可以解释为什么Django在Web开发人员中很受欢迎。\nDjango的流行 Python是最简单的编码语言之一,Django是一个基于Python的Web开发框架,具有Python的所有最佳功能 - 易于维护,编码简单,干净,调试速度更快。 Django保留了Python的真正本质。 它消除了编码的复杂性,并且不希望编码中有太多冗余,从而更容易将开发成本降至最低,并为简化调试铺平了道路。\nDjango如此受欢迎,以至于世界上一些顶级应用程序都是用它开发的。 世界上一些顶级内容驱动的网站,如华盛顿邮报,纽约时报,卫报和许多其他网站使用Django来处理来自其网站的巨大流量并保持其高水平的性能。 Dropbox是世界顶级云存储平台之一,也主要与Django一起运行,用于存储,同步和共享选项。 Django框架也用于Instagram,Spotify,Disqus,YouTube,Mozilla等,帮助开发人员保持更新的新功能和新的更新,使他们能够快速工作。 其中一些应用程序使用Django作为主要平台运行,而其他一些应用程序部分使用它。 尽管如此,Django已成为目前许多成功网站运营的一部分,难怪它正在快速攀升人气图!\n易于扩展和扩展 Django的组件可以被删除,添加或修改,使得使用冗余代码变得简单易行。 此外,它可以完全自由地向应用程序添加第三方软件包,从而减少了开发人员的大量工作。 除此之外,如果代码运行冗长,可以不必担心找到另一个平台,因为Django可以被缩放以容易地包含任何长度的代码。\n互动而有益的社区 Django是一个免费的开源框架,意味着框架将始终更新为最佳版本。 Django有大量文档证明您拥有使用它所需的所有信息和资源。 当您想讨论使用Python Django开发的新可能性或需要一些特定部分的帮助时,Django社区非常庞大且具有交互性,您可以快速获得答案。 解决您对Django的疑虑或问题很简单 - 只需Google搜索您的问题,您就可以立即获得解决方案!\n有时,当一个新的开发人员加入项目的中间时,感觉很难赶上当前的开发阶段,并在编码阶段快速进入正轨。 但是,随着Django基于MTV架构 - 模型 - 模板 - 视图架构 - 工程中不同任务的代码保持分离。 这使得加入团队的新开发人员更容易达到速度并从第一天开始轻松地开始工作。 自动安全\nDjango为其构建的应用程序提供安全保护。 它在您启动开发产品之前使用自动检查来查找任何错过的安全漏洞。 它还有助于识别和减少Python中编码带来的一些最常见的安全错误,并保护应用程序免受错误。\nPython与Django框架相结合,为创建一个可靠且高度安全的平台来开发Web开发项目奠定了基础。 随着新功能和功能的引入,开发人员使用Django并尽快启动他们的项目变得越来越简单。\n","title":"Why Django Is The Popular Python Framework aMONG wEB dEVELOPERS?"},{"location":"https://codenow.me/articles/the_image_container_repository_in_docker/","text":" Docker镜像 Docker镜像类似于虚拟机镜像,可以将它理解为一个只读模板。 例如,一个镜像包含一个基本的操作系统环境,里面仅安装了Apache应用程序(或用户需要的其他软件)。可以把它称为一个Apache镜像。 镜像是创建Docker容器的清楚。通过版本管理和增量的文件系统,Docker提供了一套十分简单的机制来创建和更新现有的镜像,用户甚至可以从网上下载一个已经做好的应用镜像,并直接使用。\nDocker容器 Docker容器类似于一个轻量级的沙盒,Docker利用容器来运行和隔离应用。 容器时从镜像创建的应用运行实例。他可以启动、停止、删除,而这些容器都是彼此相互隔离、互补可见的。 可以把容器看作一个简易版的Linux系统环境(包括root用户系统,进程空间,用户空间和网络空间等)以及运行在其中的应用程序打包而成的盒子。\nDocker仓库 Docker仓库类似于代码仓库,是Docker集中存放镜像文件的场所。 有时候我们会将Docker仓库和仓库注册服务器(Registry)混为一谈,并不严格区分。实际上,仓库注册服务器是存放仓库的地方,其上往往存放着多个仓库。每个仓库集中存放某一类镜像,往往包括多个镜像文件,通过不同的标签(tag)来进行区分。例如存放Ubuntu操作系统镜像的仓库,被称为Ubuntu仓库,其中可能包括不同版本的镜像。\n根据所存储的镜像公开与否,Docker仓库可以分为公开仓库和私有仓库两种形式。目前,最大的公开仓库是官方提供的Docker Hub,其中存放着数量庞大的镜像供用户下载。国内不少云服务提供商(如腾讯云,阿里云等)也提供了仓库的本地源,可以提供稳定的国内访问。 当然,用户如果不希望公开分享自己的镜像文件,Docker也支持用户在本地网络内创建一个只能自己访问的私有仓库。 当用户创建了自己的镜像之后,就可以使用push命令将它上传到指定的公有或者私有仓库。这样用户下次在另一台机器上使用该镜像时,只需要将其从仓库上pull下来即可。\n镜像和容器的区别: 镜像是一个只读系统,在这个只读系统中存在很多只读层,它们按照层次顺序堆叠在一起,中间使用指针连接起来(指针指向下一层)。统一的文件系统将多层只读层统一起来,所以看起来会是一个整体。 容器在镜像的上层添加了一层可读可写层。通过该层,可以经过系统进行写入操作。初次之外,容器几乎是与镜像一样的。\n","title":"Doker核心概念-镜像、容器和仓库"},{"location":"https://codenow.me/tips/build_go_on_windows/","text":" Windows版Go安装 下载Msi版进行安装。 下载地址\n选择Microsoft Windows版下载。\n安装步骤直接根据步骤提示进行。\n安装文件目录说明: |目录名|备注| |-|-| |api|每个版本的api变更差异| |bin|go源码包编译出的编译器(go),文档工具(godoc),格式化工具(gofmt)| |blog|Go博客模板,使用Go的网页模板| |doc|英文版的Go文档| |lib|引用的一些库文件| |misc|杂项用途的文件,例如Android平台的编译、git的提交钩子等| |pkg|Windows平台编译好的中间文件| |src|标准库的源码| |test|测试用例|\n开发环境安装 GoLand下载地址\n具体安装步骤参考: CSDN文章\n","title":"Windows安装Go开发环境"},{"location":"https://codenow.me/algorithm/leetcode_3_longest_substring_without_repeating_characters/","text":" 题号:3 难度:medium 链接:https://leetcode.com/problems/longest-substring-without-repeating-characters/\n 如下为Python3代码\nclass Solution(object): def lengthOfLongestSubstring(self, s): \u0026#34;\u0026#34;\u0026#34; :type s: str :rtype: int \u0026#34;\u0026#34;\u0026#34; b, m, d = 0, 0, {} for i, l in enumerate(s): b, m, d[l] = max(b, d.get(l, -1) + 1), max(m, i - b), i return max(m, len(s) - b) 参考内容\n","title":"Leetcode 3 Longest Substring Without Repeating Characters"},{"location":"https://codenow.me/algorithm/leetcode-14-longest-common-prefix/","text":" 题号:14\n难度:easy\n链接:https://leetcode.com/problems/longest-common-prefix\n描述:多个字符串找公共子串(a-z)\n from typing import List class Solution: def longestCommonPrefix2(self, strs: List[str]) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;简单粗暴的方式\u0026#34;\u0026#34;\u0026#34; res = \u0026#39;\u0026#39; if not strs: return res for char in zip(*strs): if len(set(char)) == 1: res += char[0] else: return res return res def longestCommonPrefix(self, strs: List[str]) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;不需要一个一个字符地加,只需要找到分界点,一次性取出即可\u0026#34;\u0026#34;\u0026#34; if not strs: return \u0026#39;\u0026#39; for i, letter_group in enumerate(zip(*strs)): if len(set(letter_group)) \u0026gt; 1: return strs[0][:i] else: return min(strs) if __name__ == \u0026#39;__main__\u0026#39;: a = [\u0026#34;flower\u0026#34;,\u0026#34;flow\u0026#34;,\u0026#34;flight\u0026#34;] res = Solution().longestCommonPrefix(a) print(res)","title":"Leetcode 14 Longest Common Prefix"},{"location":"https://codenow.me/tips/exmail-smtp-tls/","text":"配置腾讯企业邮箱 exmail 时,虽然官方文档上的 SMTP 端口是 465,但那是支持 SSL 校验验证的,如果客户端只支持 TLS 的话,需要把 SMTP 端口配置成 587,才能配置成功\n","title":"Exmail Smtp TLS"},{"location":"https://codenow.me/articles/sentry-raven-vs-sentry_sdk/","text":" 上周写了 Sentry 的 Python SDK raven 包的工作原理,但这周发现 Sentry Team 把 SDK 又给重构了一版,现在叫做 New Unified Version: sentry-sdk。\n但是更新工作没做彻底, 官网(https://docs.sentry.io) 上的文档虽然已经改成 sentry-sdk 了,但 sentry-web 服务上的各种说明还没改,而且新版 SDK 的版本号还没到 1.0(0.7.10),raven 已经完善到 6.10.0 了\n于是又读了一下这个包的代码,与之前真的大不相同了。\n以下内容分两部分:\n 对 SDK 的用户来说,都有哪些变化 新 SDK 在实现方式上有什么变化 新旧 SDK 变化对比 简单写了个表,功能上的区别基本就是这样了,实际使用上倒是完全感受不到什么\n 对比项 raven sentry_sdk 捕捉错误的方式 flask.got_request_exception + middleware flask.got_request_exception + middleware 获取上下文等信息的方式(flask) 直接访问 flask.request,以及 sys.exc_info 和环境相关信息 使用信号 appcontext_pushed, request_started 和 sys.exc_info,以及一些环境相关信息 同步?异步? 默认异步,用 queue.Queue 来保证 只支持异步,同样用 queue.Queue 来保证 发送方式 默认 urllib2.urlopen,可更换为 requests 使用 urllib3.PoolManager 做 HTTP 连接池 配置方式 默认读取 SENTRY_开头的所有 flask.app 配置及超多环境变量 默认读取 SENTRY_DSN, SENTRY_RELEASE, SENTRY_ENVIRONMENT 环境变量 发送给服务端的信息 正常 多返回了 当前python环境的所有已安装包 感觉比较明显的改动有四个:\n 新版在兼容 flask 时不需要传入 flask app 新版少了很多配置项 新版使用了 HTTP 连接池 发送事件(event)更方便了 兼容 flask 的时候,只需要这样写:\nimport sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init(integrations=[FlaskIntegration()]) flask 是作为一个“插件”一样的东西加进去的,里面完成了信号关联,并且在信号回调里也是直接调用的 sentry API,而没有做更定制化的处理\n少的那些配置,我猜是因为新版 SDK 优先实现了核心功能,剩下那些配置要不要加还不知道\nHTTP 连接池加的倒是正好,因为所有发送给服务端的事件都是走的同一个 API: {scheme}://{host}/api/{project_id}/store\n至于发送 event 更方便,则是因为新版把 API 的概念明确做到了 SDK 里,并且只需要\nfrom sentry_sdk import capture_message capture_message(\u0026#39;this message will be sent to sentry server directly\u0026#39;) 就可以直接发送了。而之前由于所有的一切都与 client 有关,都需要依赖 client 对象,因此必须先获取到 client 对象,才能做后面的事。\n那么新版是怎么做到可以不依赖特定 client 对象的呢?\n新 SDK 实现方式详细说明 还是先看看代码目录\nsentry_sdk ├── integrations # 类似与插件,中间件,钩子的东西,用于与其他框架和环境 ├── __init__.py ├── _compat.py # 为各个 python 版本做兼容 ├── api.py # 用户可直接使用的 API 方法 ├── client.py # 包含 Client 类,用于构建时间并发送给服务端 ├── consts.py # 一些常量定义如版本号 ├── debug.py # debug 支持 ├── hub.py # 新版最关键的类,用于并发管理 ├── scope.py # 保存所有待发送给服务端的周边信息 ├── transport.py # 同之前,实际发消息给服务端的东西 ├── utils.py # 一些内部工具 └── worker.py # 执行发送任务的后台 worker 结构很清晰,其中 client, transport, worker, processor 等都与之前概念相同,这里只说一下几个新增概念\n api scope hub Unified API 关于这个,官方文档里有详细说明,参见:https://docs.sentry.io/development/sdk-dev/unified-api,最后点阅读全文可以直接跳转过去看\npython 新版里给出了 8 个可以全局调用的 API:\n capture_event(event, hit=None): 发送事件给 sentry 服务端 capture_message(message, level=None): 发送一个字符串给 sentry 服务端 capture_exception(error=None): 将一个异常发送给 sentry 服务端 add_breadcrumb(crumb=None, hint=None, **kwargs): 增加 breadcrumb configure_scope(callback=None): 修改 scpoe 配置内容 push_scope(callback=None): 新增一个 scope 层 flush(timeout=None, callback=None): 等待 timeout 秒来发送当前所有事件 last_event_id(): 上一个提交的 event 的 id 对这些 API 来说,任何地方只要 from sentry_sdk import 之后就可以直接用了(前提是已经 init 过了)\nscope 包含要随着事件发送给服务端的各种数据,包括上下文,扩展信息,事件级别等等。另外各个 processor 里获取的信息,也会存在 scope 里,并随着事件发送给服务端\nscope 和 client 是一一对应的,也就是说,一个 client 只会对应一个 scope,因此同一个 client 发送的多条 event,只会获取一次 scope,除非又做了单独配置\nhub 是新版实现中最关键的一个东西,我们先来看一下新版里 Hub 类的描述\nclass Hub(with_metaclass(HubMeta)): # type: ignore \u0026#34;\u0026#34;\u0026#34;The hub wraps the concurrency management of the SDK. Each thread has its own hub but the hub might transfer with the flow of execution if context vars are available. If the hub is used with a with statement it\u0026#39;s temporarily activated. \u0026#34;\u0026#34;\u0026#34; _stack = None # type: List[Tuple[Optional[Client], Scope]] def __init__(self, client_or_hub=None, scope=None): # type: (Union[Hub, Client], Optional[Any]) -\u0026gt; None if isinstance(client_or_hub, Hub): hub = client_or_hub client, other_scope = hub._stack[-1] if scope is None: scope = copy.copy(other_scope) else: client = client_or_hub if scope is None: scope = Scope() self._stack = [(client, scope)] self._last_event_id = None # type: Optional[str] self._old_hubs = [] # type: List[Hub] 怎么理解呢,就是说 Hub 并不参与 sentry 客户端的原有逻辑,捕获异常,发送给服务端,这些东西没有 Hub 也都可以正常完成,Hub 只是做了一个并发管理,让不同线程都可以在任何地方直接获取到当前的 client 和其对应的 scope,从而完成“捕获-发送”流程。\n我们来看一下 Hub 的构造函数,前面的一大段只做了一件事,就是初始化 self._stack,这里面包含有 client 和 scope 两个参数,有了这两个,正常逻辑就可以走通了。剩下的两个参数,self._last_event_id 不用说了,self._old_hubs 也是一个栈,是在用 with 语句使用 hub 的时候,可以在新的上下文里进行操作,而不会影响到原有的上下文。\nclass Hub(with_metaclass(HubMeta)): def __enter__(self): # type: () -\u0026gt; Hub self._old_hubs.append(Hub.current) _local.set(self) return self def __exit__( self, exc_type, # type: Optional[type] exc_value, # type: Optional[BaseException] tb, # type: Optional[Any] ): # type: (...) -\u0026gt; None old = self._old_hubs.pop() _local.set(old) 可以看到 Hub 用在 with 里时十分简单,但是那个 _local 是什么东西?\n_local = ContextVar(\u0026#34;sentry_current_hub\u0026#34;) _local 与 Hub 在同一个文件里定义,这里的 ContextVar 是在 python3.7 引入的新特性,可以理解为线程级别的上下文变量,详细参见 PEP567。他的作用就是保存当前线程的 HUb 对象,使得同一个线程里,任何通过 _local 得到的 Hub 都是同一个。但是我们知道下划线开头的变量属于内部变量,不建议外部使用,这里怎么让其他地方可以获取到他呢?\ndef with_metaclass(meta, *bases): class metaclass(type): def __new__(cls, name, this_bases, d): return meta(name, bases, d) return type.__new__(metaclass, \u0026#34;temporary_class\u0026#34;, (), {}) class HubMeta(type): @property def current(self): # type: () -\u0026gt; Hub \u0026#34;\u0026#34;\u0026#34;Returns the current instance of the hub.\u0026#34;\u0026#34;\u0026#34; rv = _local.get(None) if rv is None: rv = Hub(GLOBAL_HUB) _local.set(rv) return rv @property def main(self): \u0026#34;\u0026#34;\u0026#34;Returns the main instance of the hub.\u0026#34;\u0026#34;\u0026#34; return GLOBAL_HUB class Hub(with_metaclass(HubMeta)): pass ... GLOBAL_HUB = Hub() _local.set(GLOBAL_HUB) 在第一次看到 Hub 类定义的时候,应该就注意到 with_metaclass(HubMeta) 了吧,这里 with_metaclass 我猜可能是为了兼容老版本,实际上就是给他定义了个元类,并且设置了一个只读参数 Hub.current,效果是从 _local 里或者 GLOBAL_HUB 里获取一个 Hub 实例,并且返回。这里有一点要注意,就是 GLOBLE_HUB 是模块级别的变量,新线程找不到 _local 时都会用这个。\n而 HubMeta.main 是直接返回了 GLOBAL_HUB,我看了下这个函数只在 AtexitIntegration 也就是获取到 shutdown signal 时使用,那时会直接调用 hub.client.close 关掉客户端以及 transport,也就是 SDK 完成使命光荣退出了\n看到这里应该要总结一下了,但是稍等,我们再看一下对外暴露的 API 是怎么实现的\ndef capture_message(message, level=None): # type: (str, Optional[Any]) -\u0026gt; Optional[str] hub = Hub.current if hub is not None: return hub.capture_message(message, level) return None 很简单吧,Hub 类持有着 client 和 scope,所有的操作都直接用 Hub 类操作即可\n总结:\n sentry_sdk 明确了 API 的概念,sentry_sdk.init() 完成后,任何地方都可以直接使用相应 API 完成操作 对各种框架的兼容使用了 Integration 的概念,在其内部只做很小的 hook 或关联,实际操作还是通过 API 来完成 调用 API 的时候,通过 Hub.current 获取到当前线程的 Hub 对象,并在 Hub._stack 里拿到最近的 client 和 scope,然后就可以像旧 SDK 一样对 client 进行各种操作了。 最后,关于多线程下各种情况的处理,按说应该是一个比较重要的部分,但是我还没有做测试,就先放一下吧\n","title":"Sentry Raven vs Sentry_sdk"},{"location":"https://codenow.me/translation/example-of-equi-depth-histograms-in-databases/","text":"原文链接:https://stackoverflow.com/a/14284218\n警告:我不是一个数据库底层的专家,所以这只是一个简单泛泛的回答。\n查询编译器会将查询(一般以 sql 的形式)转换成一个查询计划并得到查询结果。查询计划里面包含数据库引擎的低层次指令,比如扫描表 T 在 C 列查询 V 值;在 T 表使用索引 X 来定位 V 等等。\n查询优化是指编译器要在一些列查询计划中判断出哪个代价最小。代价包括始终时间,IO 单宽,存储空间,cpu 等等。从概念上讲,查询编译器是从一组计划空间中衡量每一种计划的代价,选择它能找到的最小代价的。\n上面提到的这些取决于读写的数据条数,数据是否能够被索引定位到,哪些列会被被使用,数据尺寸,以及多少磁盘空间会被占用。\n这些数据很多时候取决于表中到底存了多少数据。比如这样一个查询:select * from data where pay \u0026gt; 100 ,其中 pay 被索引。如果 pay 列没有大于 100 的值,那么这个查询代价会很小。使用索引扫描就行了。相反的,结果可能包含了整张表。\n这块儿,直方图会起作用(等高直方图只是直方图的其中一种)。在前面的查询中,直方图可以在 O(1) 时间复杂度里对查询匹配的数据行数进行一个评估,在不需要了解数据库里到底有哪些数据的情况下。\n实际上,编译器是在数据的摘要之上\u0026rdquo;执行\u0026rdquo;了查询。直方图就是这个摘要。直方图在评估代价和查询算子的结果大小上很有用,比如:表连接结果大小,插入和删除时被影响到的页数等。\n以一个简单的内连接为例,假如我们知道用来连接两张表的列的数据分布:\nBins (25% each) Table A Table B 0-100 151-300 101-150 301-500 151-175 601-700 176-300 1001-1100 可以很容易看到 A 表中 50% 的数据和 B 表中 25% 的数据会参与到查询当中。如果有唯一列的话,我们可以评估连接的结果大小应该是 max(.5 * |A|, .25 * |B|)。这是很简单的一个列子。在很多的场景中,统计分析需要更复杂一些的数学知识。对于连接来说,经常通过算子的直方图技术连接的评估直方图。\n","title":"Example of Equi Depth Histograms in Databases"},{"location":"https://codenow.me/algorithm/leetcode-49-graph-anagrams/","text":"原题链接 ,难度 Medium:\nclass Solution: def groupAnagrams(self, strs): \u0026#34;\u0026#34;\u0026#34; :type strs: List[str] :rtype: List[List[str]] \u0026#34;\u0026#34;\u0026#34; memo = {} for s in strs: s_ = \u0026#39;\u0026#39;.join(sorted(s)) if s_ in memo: memo[s_].append(s) else: memo[s_] = [s] result = [] for key in memo: result.append(memo[key]) return result","title":"Leetcode 49 Graph Anagrams"},{"location":"https://codenow.me/tips/golang-output-table-in-console/","text":"使用 tablewriter 即可。\n","title":"golang 中输出表格到 console"},{"location":"https://codenow.me/translation/what-every-python-project-should-have/","text":" 原文地址\n Python 语言在过去的几年有着突飞猛进的发展,社区也在快速发展。在发展过程中,社区中出现了许多工具保持着资源的结构性和可获取性。在这篇文章中,我将提供一个简短列表,让每个 Python 项目中都具有可访问性和可维护性。\nrequirements.txt 首先, requirements.txt 在安装项目时候是十分重要的,通常是一个纯文本文件,通过 pip 安装,每行一个项目的依赖。\n真是简单又实用。\n你也可以有多个用于不同目的 requirements.txt。例如,requirements.txt 是让项目正常启动的依赖,requirements_dev.txt 是用于开发模式的依赖,requirements_docs.txt 是生成文档的依赖(像 Sphinx 需要的主题)\nsetup.py setup.py 文件在通过 pip 安装时候时候是十分重要的。编写容易,很好的可配置性并且可以处理很多事情,例如导入,项目元数据,更新源,安装依赖项等等。\n可以查看 setuptools 文档获取更多的信息。\n正确的项目结构 项目结构至关重要。有了一个组织良好的结构,它会更容易组织的东西,找到某些源文件,并鼓励其他人贡献。\n一个项目目录应具有类似的结构\nroot/ docs/ tests/ mymodule/ scripts/ requirements.txt setup.py README LICENSE 当然,这不是组织项目的唯一方法,但这肯定是最常用的模板。\n测试 单元测试对项目十分重要,可以保证代码的稳定性。我推荐 unittest 模块,因为它是内置的,并且足够灵活,完成正确工作。\n还有其他可用于测试项目的库,例如 test.py 或 nose。\n文档 如果你开发一个项目,我确信你不只是为你自己写。其他人也要必须知道如何使用你的项目。即使你只是为自己编写的项目(虽然是开源的目的),但是一段时间后不开发后,你一定不会记得你的代码中发生的任何事情(或API)。\n因此,为了实现可重用的代码,你应该:\n 设计一个简单的API,易于使用和记忆 API应该足够灵活,容易配置 记录相关使用例子 例子不要追求 100% ,最合适的是覆盖 80% 。 为了充分的记录你的代码,你应该使用特殊的工具开完成文档工作,例如 Sphinx 或者 mkdocs ,所以你可以使用一个流行的标记语言(rst或markdown)来生成具有适当引用链接的漂亮的文档。\n结论 在熟悉上述话题之后,一定能够生成符合社区标准的漂亮的结构化项目和库。不要忘记总是使用PEP-8!\n","title":"「译」Python 项目应该都有什么?"},{"location":"https://codenow.me/algorithm/word-break/","text":" 题号:139\n难度:中等\n链接:https://leetcode-cn.com/problems/word-break/submissions/\n class Solution: def wordBreak(self, s: str, wordDict: List[str]) -\u0026gt; bool: if not s: return True word_idx = [0] for i in range(len(s) + 1): for j in word_idx: if s[j:i] in wordDict: word_idx.append(i) break return word_idx[-1] == len(s)","title":"Leetcode: 139 Word Break"},{"location":"https://codenow.me/tips/goland-refactor/","text":"Goland 的 Refactor 功能,在修改、移动等操作时候可以对整个文件或者工程生效。\n","title":"Goland Refactor"},{"location":"https://codenow.me/articles/linux-cgroups/","text":" cgroups 是 Linux 内核中的一个功能,用来限制、控制分离一个进程的资源,比如 CPU、内存、IO 等。\ncgroups 是由一组子系统构成,每种子系统即时一种资源,目前可使用的资源如下:\n cpu:限制 cpu 的使用率 cpuacct:cpu 的统计报告 cpuset:分配 cpu memory:分配 mem 的使用量 blkio:限制块设备的 io devices:能够访问的设备 net_cls:控制网络数据的访问 net_prio:网络流量包的优先级 freezer:pause 或者 resume 进程 ns:控制 namespace 的访问 cgroups 中有个 hierarchy 的概念,意思一组 cgroup 是一棵树,cgroup2 可以挂在 cgroup 1 上,这样可以从 cgroup1 中继承设置。\n所以 process、subsystem、hierarchy 存在一些关系。\n 一个 subsystem 只能附加到一个 hierarchy 一个 hierarchy 可以附加到多个 subsystem 中 一个 process 可以作为多个 cgroups 成员,但是要在不同的 hierarchy 中 fork 出的子进程默认和父进程使用一个 cgroups,但是可以移动到其他的 cgroups 中 在 linux 中 /sys/fs/cgroup 中是 cgroups 默认的 hierarchy,可以看到目前的 subsystem\ndr-xr-xr-x 6 root root 0 Dec 17 17:31 blkio lrwxrwxrwx 1 root root 11 Dec 17 17:31 cpu -\u0026gt; cpu,cpuacct dr-xr-xr-x 6 root root 0 Dec 17 17:31 cpu,cpuacct lrwxrwxrwx 1 root root 11 Dec 17 17:31 cpuacct -\u0026gt; cpu,cpuacct dr-xr-xr-x 4 root root 0 Dec 17 17:31 cpuset dr-xr-xr-x 6 root root 0 Dec 17 17:31 devices dr-xr-xr-x 4 root root 0 Dec 17 17:31 freezer dr-xr-xr-x 7 root root 0 Dec 17 17:31 memory lrwxrwxrwx 1 root root 16 Dec 17 17:31 net_cls -\u0026gt; net_cls,net_prio dr-xr-xr-x 3 root root 0 Dec 17 17:31 net_cls,net_prio lrwxrwxrwx 1 root root 16 Dec 17 17:31 net_prio -\u0026gt; net_cls,net_prio dr-xr-xr-x 3 root root 0 Dec 17 17:31 perf_event dr-xr-xr-x 3 root root 0 Dec 17 17:31 pids dr-xr-xr-x 5 root root 0 Dec 17 17:31 systemd 假如我们想给一个进程添加内存限制,第一步需要创建一个 hierarchy 在 /sys/fs/cgroup/memory 中\nsudo mkdir /sys/fs/cgroup/memory/mytestcgroup 系统会帮助我们创建一系列文件,这是因为我们挂载的类型是 cgroup,cgroup 的 hierarchy 目录会被映射成文件目录,方便操作:\n-rw-r--r-- 1 root root 0 Apr 14 21:36 cgroup.clone_children --w--w--w- 1 root root 0 Apr 14 21:36 cgroup.event_control -rw-r--r-- 1 root root 0 Apr 14 21:36 cgroup.procs -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.failcnt --w------- 1 root root 0 Apr 14 21:36 memory.force_empty -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.limit_in_bytes -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.max_usage_in_bytes -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.memsw.failcnt -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.memsw.limit_in_bytes -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.memsw.max_usage_in_bytes -r--r--r-- 1 root root 0 Apr 14 21:36 memory.memsw.usage_in_bytes -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.move_charge_at_immigrate -r--r--r-- 1 root root 0 Apr 14 21:36 memory.numa_stat -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.oom_control ---------- 1 root root 0 Apr 14 21:36 memory.pressure_level -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.soft_limit_in_bytes -r--r--r-- 1 root root 0 Apr 14 21:36 memory.stat -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.swappiness -r--r--r-- 1 root root 0 Apr 14 21:36 memory.usage_in_bytes -rw-r--r-- 1 root root 0 Apr 14 21:36 memory.use_hierarchy -rw-r--r-- 1 root root 0 Apr 14 21:36 notify_on_release -rw-r--r-- 1 root root 0 Apr 14 21:36 tasks 在上面的文件中我们可以看到 tasks,这里面放着就是被限制的进程 pid,我们把当前 session 的 pid 放入 task 中,以后从这个 session 启动的进程将会被限制,比如限制一下内存只能使用 100m。\nsudo bash -c \u0026#34;echo \u0026#34;100m\u0026#34; \u0026gt; memory.limit_in_bytes\u0026#34; sudo bash -c \u0026#34;echo $$\u0026gt; tasks\u0026#34; 然后使用 stress 工具启动一个测压\nstress --vm-bytes 200m --vm-keep -m 1 最后通过 top 等工具可以发现内存被限制到了 100m。\nGo 语言控制 cgroup Go 语言中并没有特殊的 API 接口来处理 cgroup,依然是通过和命令行一样的模式(读写文件)来控制 cgroup。\n所以,在 go 语言中就是创建文件夹,删除文件加,写入文件这三个操作来使用 cgroup 功能。\n","title":"CGroups 控制进程资源"},{"location":"https://codenow.me/articles/tidb-join-performance-optimization-1/","text":" 最近在尝试自己写数据库查询模块,满足 http://sigmod18contest.db.in.tum.de/task.shtml 的功能要求。一边看着 TiDB 的代码,一边写… 这个过程中发现了一些 TiDB 优化点。\njoin reorder 每个数据库系统基本都要实现 join reorder,修改表连接的顺序,从而提高 join 的性能,比如下面这个查询:\nselect a.id, b.id, c.id from a, b, c where a.id = b.a_id and a.id = c.a_id; 查询要连接 a/b/c 三张表,可以先连接 a 和 b,也可以先连接 a 和 c,当然如果你想不开的话,也可以先连接 a 和 c。如果 a join b 产生的数据比 a join c 产生的数据多,那么先计算 a join c 一般性能会更好。\n很多数据库在表少的时候会使用动态规划来解决这个问题,比如 这篇文章 中介绍的算法。大致思路是根据表和用来连接的条件看做是一个无环图,表是节点,筛选条件是边。要计算最优的连接顺序,就是根据这张图计算出一个 sJoin Tree,Join Tree 除叶子节点以外其他的节点都是 Join。动规的过程是将图拆分成各种子图的组合并从中找出最优组合。\n下面是 TiDB 中 join reorder 的主体代码:\nfunc (s *joinReorderDPSolver) solve(joinGroup []LogicalPlan, conds []expression.Expression) (LogicalPlan, error) { // joinGroup 可以简单认为是要连接的 table 列表,代码中先计算出图的邻接表的结构和“边”列表 \tadjacents := make([][]int, len(joinGroup)) totalEdges := make([]joinGroupEdge, 0, len(conds)) addEdge := func(node1, node2 int, edgeContent *expression.ScalarFunction) { totalEdges = append(totalEdges, joinGroupEdge{ nodeIDs: []int{node1, node2}, edge: edgeContent, }) adjacents[node1] = append(adjacents[node1], node2) adjacents[node2] = append(adjacents[node2], node1) } // Build Graph for join group \tfor _, cond := range conds { // 根据筛选条件的列,找到每个条件中连接的表,记录表之间的连接关系 \tsf := cond.(*expression.ScalarFunction) lCol := sf.GetArgs()[0].(*expression.Column) rCol := sf.GetArgs()[1].(*expression.Column) lIdx, err := findNodeIndexInGroup(joinGroup, lCol) //... \trIdx, err := findNodeIndexInGroup(joinGroup, rCol) //... \taddEdge(lIdx, rIdx, sf) } visited := make([]bool, len(joinGroup)) nodeID2VisitID := make([]int, len(joinGroup)) var joins []LogicalPlan // BFS the tree. \t// 使用 BFS 计算出联通子图,如果存在多个子图,子图之间没有连接关系,子图之间 join 结果是他们的笛卡尔乘积 \tfor i := 0; i \u0026lt; len(joinGroup); i++ { if visited[i] { continue } visitID2NodeID := s.bfsGraph(i, visited, adjacents, nodeID2VisitID) // Do DP on each sub graph. \t// 使用 DP 算法找到每个子图的最优 join 顺序 \tjoin, err := s.dpGraph(visitID2NodeID, nodeID2VisitID, joinGroup, totalEdges) if err != nil { return nil, err } joins = append(joins, join) } // Build bushy tree for cartesian joins. \treturn s.makeBushyJoin(joins), nil } 下面是 bp 部分的代码,算法使用位图来表示不同的子图,使用了自下而上的方式,从小到大的计算每个子图的最优 join 顺序,从而最终计算出整个图的最优解。算法中使用了位图,拆分子图和判断子图之间是否连接的代码感觉很棒,非常的简洁高效。\nfunc (s *joinReorderDPSolver) dpGraph(newPos2OldPos, oldPos2NewPos []int, joinGroup []LogicalPlan, totalEdges []joinGroupEdge) (LogicalPlan, error) { // 使用位图来表示不同子图,使用自下而上的方式计算每个子图的最优 join 顺序 \tnodeCnt := uint(len(newPos2OldPos)) bestPlan := make([]LogicalPlan, 1\u0026lt;\u0026lt;nodeCnt) bestCost := make([]int64, 1\u0026lt;\u0026lt;nodeCnt) // bestPlan[s] is nil can be treated as bestCost[s] = +inf. \tfor i := uint(0); i \u0026lt; nodeCnt; i++ { bestPlan[1\u0026lt;\u0026lt;i] = joinGroup[newPos2OldPos[i]] } // 从小到大罗列所有子图 \tfor nodeBitmap := uint(1); nodeBitmap \u0026lt; s \u0026lt;\u0026lt; nodeCnt); nodeBitmap++ { if bits.OnesCount(nodeBitmap) == 1 { continue } // This loop can iterate all its subset. \tfor sub := (nodeBitmap - 1) \u0026amp; nodeBitmap; sub \u0026gt; 0; sub = (sub - 1) \u0026amp; nodeBitmap { remain := nodeBitmap ^ sub if sub \u0026gt; remain { // 由于是无向图,所有相同两个子图的组合,只计算一遍 \tcontinue } // 如果 sub/remain 这两个子图中某一个不是强连通的,不继续计算 \tif bestPlan[sub] == nil || bestPlan[remain] == nil { continue } // Get the edge connecting the two parts. \tusedEdges := s.nodesAreConnected(sub, remain, oldPos2NewPos, totalEdges) if len(usedEdges) == 0 { // 如果 sub 和 remain 是不连通的,也不再继续计算 \tcontinue } join, err := s.newJoinWithEdge(bestPlan[sub], bestPlan[remain], usedEdges) if err != nil { return nil, err } // 更新 nodeBitmap 所代表的子图中最优的 join 顺序 \tif bestPlan[nodeBitmap] == nil || bestCost[nodeBitmap] \u0026gt; join.statsInfo().Count()+bestCost[remain]+bestCost[sub] { bestPlan[nodeBitmap] = join bestCost[nodeBitmap] = join.statsInfo().Count() + bestCost[remain] + bestCost[sub] } } } return bestPlan[(1\u0026lt;\u0026lt;nodeCnt)-1], nil } 需要注意的是,bp 算法中需要估算每个 join 的代价,评估代价的过程当中需要使用统计信息,统计信息有的时候会不准确,这会影响 bp 算法的结果。\n补充知识\n从上面的代码可以看到,评估 join 代价的时候主要还是看 join.StatsInfo().Count() 的数值大小,这个数值表示 join 会产生的数据条数。评估 join 的数据条数和评估单表的数据条数的计算方法不同,这块的知识可以看一下 数据库概念 13.3.3 的讲解和 TiDB 的代码实现。\n复用 Chunk 为了提高查询执行器的执行速度,特别是在数据量比较大的情况下,TiDB 使用了 chunk。在执行查询的过程中,执行器每次不再只返回一条数据,而是返回一组数据。\n除了使用 Chunk 以外,TiDB 的执行器还增加了 Chunk 复用的逻辑,有效的降低了内存的开销。在做一个数据量很大的 HashJoin 时(比如外表有几百万条数据),TiDB 会启动多个 worker 来计算 join 结果,worker 之间通过 Chunk 分发任务、接收计算结果。如果没有复用 Chunk 的话,查询过程会差生大量的 Chunk,GC 势必会影响性能。\nTiDB 中当 worker 使用完了 chunk 以后,会通过特定的 channel 将 chunk 还回从而实现 Chunk 的复用。这块的代码不易拆分出来,暂略。\n","title":"Tidb 源码学习:关于 join 性能优化"},{"location":"https://codenow.me/translation/did_you_know_you_can_easily_extend_and_expand_your_datacenter_footprint/","text":" 您是否知道可以轻松地延伸和扩展您的数据中心占用空间? 原文链接:http://app.learn.vmware.com/e/es?s=279193683\u0026amp;e=2902981\u0026amp;elqTrackId=80b432a7915c49a0a70c7d2846b7ff14\u0026amp;elq=52464596ac594d2d93bd227a6575bdc5\u0026amp;elqaid=25584\u0026amp;elqat=1\n翻译如下:\n现代IT团队承受着难以置信的压力,要以越来越快的速度创新和推出产品和服务,但在快速扩展产品方面存在许多障碍。其中最主要的挑战是如何快速轻松地利用公共云提供的无限可伸缩性、随需应变能力和灵活的消费。\nAWS上的VMware Cloud是一种随需应变服务,它允许您跨基于websphere的云环境运行应用程序,并访问广泛的本机AWS服务。\n该服务由VMware Cloud Foundation™提供支持,集成了VMware的旗舰计算、存储和网络虚拟化产品(VMware vSphere、VMware vSAN和VMware NSX)以及VMware vCenter management,并优化为在弹性的、裸机AWS基础设施上运行。\n有了这项服务,客户可以使用熟悉的VMware工具管理基于云的资源。\n","title":"Did_you_know_you_can_easily_extend_and_expand_your_datacenter_footprint"},{"location":"https://codenow.me/algorithm/leetcode_216_combination_sum_iii/","text":"题号:216 难度:Medium 链接:https://leetcode.com/problems/combination-sum-iii/\n#!/usr/bin/python # -*- coding: utf-8 -*- class Solution: def combinationSum3(self, k, n): result_list = [] def f(k, n, cur, next): if len(cur) == k: if sum(cur) == n: result_list.append(cur) return for i in range(next, n+1): f(k, n, cur+[i], i+1) f(k, n, [], 1) return result_list if __name__ == \u0026#39;__main__\u0026#39;: k = 3 n = 7 print(Solution().combinationSum3(k, n)) k = 3 n = 9 print(Solution().combinationSum3(k, n)) C:\\Python37\\python.exe C:/python_workspace/leecode/array/leecode_216.py [[1, 2, 4]] [[1, 2, 6], [1, 3, 5], [2, 3, 4]]","title":"Leetcode_216_Combination_Sum_III"},{"location":"https://codenow.me/articles/docker_process_about_run_shell_and_exec/","text":" 1. 前言 Docker容器内运行的进程对于宿主机而言,是独立进程,还是Docker容器进程?\nDocker容器内启动的进程全部都是宿主机上的独立进程\nDocker容器内启动的进程是不是Docker进程本身要看Dockerfile的写法\n比如Docker内启动redis,如果用CMD \u0026ldquo;/usr/bin/redis-server\u0026rdquo;,这是用shell启动,会先启动shell,然后再启动redis,所以不是Docker进程本身;\n如果用CMD [\u0026ldquo;/usr/bin/redis-server\u0026rdquo;],这是用exec启动,是直接启动redis,进程号为1,所以是Docker进程本身\n2. shell方式 1) shell方式的Dockerfile\nroot@ubuntu:~# cat Dockerfile FROM ubuntu:18.04 RUN apt-get update \u0026amp;\u0026amp; apt-get -y install redis-server \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* EXPOSE 6379 CMD \u0026#34;/usr/bin/redis-server\u0026#34; 2)shell方式创建容器\nroot@ubuntu:~# docker build -t redisshell -f Dockerfile . 3) shell方式创建并运行镜像\nroot@ubuntu:~# docker run --name redisshell redisshell 4) redisshell容器内进程\nroot@ubuntu:~# docker exec -it redisshell ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 17:36 ? 00:00:00 /bin/sh -c \u0026#34;/usr/bin/redis-ser root 6 1 0 17:36 ? 00:00:00 /usr/bin/redis-server *:6379 root 10 0 0 17:37 pts/0 00:00:00 ps -ef 5) 宿主机进程\nroot@ubuntu:~# ps -ef|grep redis root 4151 2874 0 10:36 pts/0 00:00:00 docker run --name redisshell redisshell root 4202 4176 0 10:36 ? 00:00:00 /bin/sh -c \u0026#34;/usr/bin/redis-server\u0026#34; root 4252 4202 0 10:36 ? 00:00:00 /usr/bin/redis-server *:6379 root 4392 4102 0 10:39 pts/1 00:00:00 grep --color=auto redis 3. exec方式 1) exec方式的Dockerfile\nroot@ubuntu:~# cat Dockerfile FROM ubuntu:18.04 RUN apt-get update \u0026amp;\u0026amp; apt-get -y install redis-server \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* EXPOSE 6379 CMD [\u0026#34;/usr/bin/redis-server\u0026#34;] 2)shell方式创建容器\nroot@ubuntu:~# docker build -t redisexec -f Dockerfile . 3) shell方式创建并运行镜像\nroot@ubuntu:~# docker run --name redisexec redisshell 4)redisexec器内进程\nroot@ubuntu:~# docker exec -it redisexec ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 17:44 ? 00:00:00 /usr/bin/redis-server *:6379 root 9 0 0 17:45 pts/0 00:00:00 ps -ef 5) 宿主机进程\nroot@ubuntu:~# ps -ef|grep redis root 4151 2874 0 10:36 pts/0 00:00:00 docker run --name redisshell redisshell root 4202 4176 0 10:36 ? 00:00:00 /bin/sh -c \u0026#34;/usr/bin/redis-server\u0026#34; root 4252 4202 0 10:36 ? 00:00:01 /usr/bin/redis-server *:6379 root 4442 4102 0 10:44 pts/1 00:00:00 docker run --name redisexec redisexec root 4496 4466 0 10:44 ? 00:00:00 /usr/bin/redis-server *:6379 root 4646 4561 0 10:46 pts/2 00:00:00 grep --color=auto redis 4. 两种方式的区别 除了进程是否独立有一定的区别外,两种启动模式导致进程的退出机制也完全不同,从而形成了僵尸进程和孤儿进程\n4.1 具体说来。Docker提供了docker stop和docker kill两个命令向容器中的1号进程发送信号 1) 当执行docker stop命令时,Docker会首先向容器的1号进程发送一个SIGTERM信号,用于容器内程序的退出。\n如果容器在收到SIGTERM信号后没有结束进程,那么Docker Daemon会在等待一段时间(默认是10秒)后再向容器发送SIGKILL信号,将容器杀死并变为退出状态\n这种方式给Docker应用提供了一个优雅的退出机制,允许应用在收到stop命令时清理和释放使用中的资源\n2)docker kill命令可以向容器内的1号进程发送任何信号,默认是发送SIGKILL信号来强制退出\n3) 从Docker1.9版本开始,Docker支持停止容器时向其发送自定义信号量,并指明容器退出机制,该参数的缺省值是SIGTERM\n4.2 两种方式运行docker stop结果不一样: 1)exec方式\nroot@ubuntu:~# docker stop redisexec redisexec root@ubuntu:~# docker logs -f redisexec 1:C 08 Apr 17:44:48.982 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 1:C 08 Apr 17:44:48.982 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=1, just started 1:C 08 Apr 17:44:48.982 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf 1:M 08 Apr 17:44:48.985 * Running mode=standalone, port=6379. 1:M 08 Apr 17:44:48.985 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 1:M 08 Apr 17:44:48.985 # Server initialized 1:M 08 Apr 17:44:48.985 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add \u0026#39;vm.overcommit_memory = 1\u0026#39; to /etc/sysctl.conf and then reboot or run the command \u0026#39;sysctl vm.overcommit_memory=1\u0026#39; for this to take effect. 1:M 08 Apr 17:44:48.986 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command \u0026#39;echo never \u0026gt; /sys/kernel/mm/transparent_hugepage/enabled\u0026#39; as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 1:M 08 Apr 17:44:48.986 * Ready to accept connections 1:signal-handler (1554746551) Received SIGTERM scheduling shutdown... 1:M 08 Apr 18:02:31.880 # User requested shutdown... 1:M 08 Apr 18:02:31.880 * Saving the final RDB snapshot before exiting. 1:M 08 Apr 18:02:31.892 * DB saved on disk 1:M 08 Apr 18:02:31.892 # Redis is now ready to exit, bye bye... 在容器日志中看到了\u0026rdquo;Received SIGTERM scheduling shutdown\u0026hellip;\u0026ldquo;的内容,说明redis-server进程已经接收到了SIGTERM消息,并优雅地关闭了资源\n2)shell方式\nroot@ubuntu:~# docker stop redisshell redisshell root@ubuntu:~# docker logs -f redisshell 6:C 08 Apr 17:36:54.062 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 6:C 08 Apr 17:36:54.063 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=6, just started 6:C 08 Apr 17:36:54.063 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf 6:M 08 Apr 17:36:54.067 * Running mode=standalone, port=6379. 6:M 08 Apr 17:36:54.067 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 6:M 08 Apr 17:36:54.068 # Server initialized 6:M 08 Apr 17:36:54.068 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add \u0026#39;vm.overcommit_memory = 1\u0026#39; to /etc/sysctl.conf and then reboot or run the command \u0026#39;sysctl vm.overcommit_memory=1\u0026#39; for this to take effect. 6:M 08 Apr 17:36:54.068 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command \u0026#39;echo never \u0026gt; /sys/kernel/mm/transparent_hugepage/enabled\u0026#39; as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 6:M 08 Apr 17:36:54.068 * Ready to accept connections docker stop redisshell 容器停止缓慢,而且容器没有优雅关机的内容\n原因在于,用shell脚本启动的容器,其1呈进程是shell进程,shell进程中没有对SIGTERM信号的处理逻辑,所以它忽略了接收到的SIGTERM信号\n当Docker等待stop命令执行10秒超时之后,Docker Daemon将发送SIGKILL信号强制杀死1号进程,并销毁它的PID命名空间\n其子进程redis-servere也在收到SIGKILL信号后被强制终止并退出。\n如果此时应用中还有正在执行的事务或未持久化的数据,强制退出可能导致数据丢失或状态不一致\n5. 总结 所以,容器的1号进程必须能够正确的处理SIGTERM信号来支持优雅退出,如果容器中包含多个进程,则需要1号进程能够正确地传播SIGTERM信号来结束所有的进程,之后再退出\n当然,更正确的做法是,令每一个容器中只包含一个进程,同时采用exec模式启动进程。\n这也是Docker官方推荐的做法。\n","title":"运行shell和exec命令时,Docker进程的区别"},{"location":"https://codenow.me/tips/change_nginx_group_user/","text":" 1. Nginx默认用户 为了让Web服务更安全没需要尽可能地改掉软件默认的所有配置,包括端口、用户等。\n首先查看Nginx服务的默认用户,一般情况下,Nginx服务启动的用户是Nobody,查看默认的配置文件,代码如下:\n[root@localhost conf]# grep \u0026#39;#user\u0026#39; /etc/nginx/nginx.conf.default #user bobody; 为了防止黑客猜到这个Web服务的用户,我们需要将其更改成特殊的用户名;下面以nginx用户为例进行说明。\n 2. 为nginx服务建立新用户 [root@localhost conf]# useradd nginx -s /sbin/nologin -M #\u0026lt;== 不需要有系统登录权限,应当禁止其登录能力,相当于Apache里的用户 [root@localhost conf]# id nginx #\u0026lt;==检查用户 3. 修改Nginx默认用户的两种方法 3.1 在编译Nginx软件时直接指定编译的用户和组,命令如下\n[root@lnmp nginx-1.14.0]# ./configure --user=nginx --group=nginx ... 3.2 配置Nginx 服务,让其使用刚建立的Nginx用户\n[root@lnmp nginx-1.14.0]# egrep \u0026#34;user\u0026#34; /etc/nginx/conf/nginx.conf user nginx; [root@lnmp nginx-1.14.0]# ps -ef|grep nginx|grep -v grep nginx 56998 56721 0 21:18 ? 00:00:00 nginx: worker process ","title":"更改Nginx默认用户与组"},{"location":"https://codenow.me/algorithm/travelsallistwithoutloops/","text":"之前一次面试上遇到的题,不允许用 for 循环和 while 循环,要求遍历出列表每个元素。 当时没有想法,回去后就查了一下,原来可以用递归解决。\nclass Solution(): def travelsal_list_with_recursion(self, Ls, index=0): if len(Ls) == index: return print(Ls[index], end=\u0026#39; \u0026#39;) self.travelsal_list_with_recursion(Ls, index+1) if __name__ == \u0026#39;__main__\u0026#39;: s = Solution() ls = [\u0026#39;a\u0026#39;, \u0026#39;3\u0026#39;, \u0026#39;r\u0026#39;, \u0026#39;3\u0026#39;] s.travelsal_list_with_recursion(ls) ","title":"使用递归思想进行列表遍历"},{"location":"https://codenow.me/tips/mysql_where11/","text":"这周工作时看了同事的代码,发现他写的很多 sql 语句都在后面加上了 where1=1,研究了一下,才发现了他的作用和好处。\n例子:\nstring MySqlStr=”select * from table where”; if(Age.Text.Lenght\u0026gt;0) { MySqlStr=MySqlStr+“Age=“+“\u0026#39;Age.Text\u0026#39;“; } if(Address.Text.Lenght\u0026gt;0) { MySqlStr=MySqlStr+“and Address=“+“\u0026#39;Address.Text\u0026#39;“; } 如果两个条件都符合,即 sql 语句 \u0026ldquo;select * from table where Age=x and Address=xx\u0026rdquo; 成立。 但是如果两个条件都不符合,则语句变成了 \u0026ldquo;select * from table where\u0026rdquo;,这个时候就会报错。 使用 where 1=1 可以避免上面发生的问题。\n参考链接: mysql中使用 where 1=1和 0=1 的作用及好处\n","title":"在 Mysql 里使用 where1=1 的作用"},{"location":"https://codenow.me/articles/about_mysql_locks/","text":" 作者:柳树 on 美业 from 有赞coder\n原链接: Mysql 锁:灵魂七拷问\n 一、缘起 假设你想给别人说明,Mysql 里面是有锁的,你会怎么做?\n大多数人,都会开两个窗口,分别起两个事务,然后 update 同一条记录,在发起第二次 update 请求时,block,这样就说明这行记录被锁住了: 二、禁锢 问题来了,貌似只有显式的开启一个事务,才会有锁,如果直接执行一条 update 语句,会不会加锁呢?\n比如直接执行:\nupdate t set c = c + 1 where id = 1; 这条语句,前面不加 begin,不显式开启事务,那么 Mysql 会不会加锁呢?\n直觉告诉你,会。\n但是为什么要加锁?\n给你五秒钟,说出答案。\n学过多线程和并发的同学,都知道下面这段代码,如果不加锁,就会有灵异事件:\ni++; 开启十个线程,执行 1000 次这段代码,最后 i 有极大可能性,会小于 1000。\n这时候,用 Java 的套路,加锁:\nsynchornize { i++; } 问题解决。\n同理,对于数据库,你可以理解为 i,就是数据库里的一行记录,i++ 这段代码,就是一条 update 语句,而多线程,对应的就是数据库里的多个事务。\n既然对内存中 i 的操作需要加锁,保证并发安全,那么对数据库的记录进行修改,也必须加锁。\n这道理很简单,但是很多人,未曾想过。\n三、释然 为什么大家都喜欢用第一部分里的例子来演示 Mysql 锁?\n因为开两个事务,会 block,够直观。\n那么问题又来了,为什么会 block,或者说,为什么 Mysql 一定要等到 commit 了,才去释放锁?\n执行完一条 update 语句,就把锁释放了,不行吗?\n举个例子就知道 Mysql 为什么要这么干了: 一开始数据是:{id:1,c:1};\n接着事务A通过 select .. for update,进行当前读,查到了 c=1;\n接着它继续去更新,把 c 更新成 3,假设这时候,事务 A 执行完 update 语句后,就把锁释放了;\n那么就有了第 4 行,事务 B 过来更新,把 c 更新成 4;\n结果到了第 5 行,事务 A 又来执行一次当前读,读到的 c,竟然是 4,明明我上一步才把 c 改成了 3\u0026hellip;\n事务 A 不由的发出怒吼:我为什么会看到了我不该看,我也不想看的东西?!\n事务 B 的修改,居然让事务 A 看到了,这明目张胆的违反了事务 ACID 中的 I,Isolation,隔离性(事务提交之前,对其他事务不可见)。\n所以,结论:Mysql 为了满足事务的隔离性,必须在 commit 才释放锁。\n四、自私的基因 有人说,如果我是读未提交( Read Uncommited )的隔离级别,可以读到对方未提交的东西,是不是就不需要满足隔离性,是不是就可以不用等到 commit 才释放锁了?\n非也。\n还是举例子: 事务 A 是 Read Committed,事务B是 Read Uncommitted;\n事务 B 执行了一条 update 语句,把 c 更新成了 3\n假设事务 B 觉得自己是读未提交,就把锁释放了\n那这时候事务 A 过来执行当前读,读到了 c 就是 3\n事务 A 读到了别的事务没有提交的东西,而事务 A,还说自己是读已提交,真是讽刺\n根因在于,事务 B 非常自私,他觉得自己是读未提交,就把锁释放了,结果让别人也被“读未提交”\n显然,Mysql 不允许这么自私的行为存在。\n结论:就算你是读未提交,你也要等到 commit 了再释放锁。\n五、海纳百川 都知道 Mysql 的行锁,分为 X 锁和 S 锁,为什么 Mysql 要这么做呢?\n这个简单吧,同样可以类比 Java 的读写锁:\nIt allows multiple threads to read a certain resource, but only one to write it, at a time. 允许多个线程同时读,但只允许一个线程写,既支持并发提高性能,又保证了并发安全。\n六、凤凰涅磐 最后来个难点的。\n假设事务 A 锁住了表 T 里的一行记录,这时候,你执行了一个 DDL 语句,想给这张表加个字段,这时候需要锁表吧?但是由于表里有一行记录被锁住了,所以这时候锁表时会 block。\n那 Mysql 在锁表时,怎么判断表里有没有记录被锁住呢?\n最简单暴力的,遍历整张表,遍历每行记录,遇到一个锁,就说明表里加锁了。\n这样做可以,但是很傻,性能很差,高性能的 Mysql,不允许这样的做法存在。\nMysql 会怎么做呢?\n行锁是行级别的,粒度比较小,好,那我要你在拿行锁之前,必须先拿一个假的表锁,表示你想去锁住表里的某一行或者多行记录。\n这样,Mysql 在判断表里有没有记录被锁定,就不需要遍历整张表了,它只需要看看,有没有人拿了这个假的表锁。\n这个假的表锁,就是我们常说的,意向锁。\nIntention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table 很多人知道意向锁是什么,但是却不知道为什么需要一个粒度比较大的锁,不知道它为何而来,不知道 Mysql 为何要设计个意向锁出来。\n知其然,知其所以然。\n七、参考文献 InnoDB Locking ReadWriteLock\n","title":"Mysql 锁:灵魂七拷问"},{"location":"https://codenow.me/translation/4waysdeveloperimprovetools/","text":" 作者: bmusings 原链接: 4 Ways Every Software Developer Should Improve Their Tools\n 工具可以让开发人员的想法转换成为计算机能够理解的确切指令。 从文本编辑器到源代码控制,为了技术和效率,它们是我们每天使用上百次的伙伴。和木匠不同,当我们想要更好的工具时,不能直接去商店里买一把更好的锤子。 这引发了一个问题: 我们如何去改善我们的日常工具。 下面我介绍 4 个能够升级工具的方法。\n学习快捷键 设置快捷键和别名是我们使用现有工具最方便最有效的方法。 它通过使用键盘输入代替鼠标点击操作,从而节省时间。 一个人机交互领域出名的法则 Fitt\u0026rsquo;s Law 指出,点击目标所需的时间和指针的距离成正相关,和目标的大小成反比。也就是说,更小更远的目标比更大更近的目标需要花更多的时间。我见过很多次有些专业的软件工程师使用键盘在三个越来越小的目录花费了很多时间只为了操作一个指令,这真是荒谬。学习快捷键进行指令操控会让你把时间控制到常量级别(0.1 - 0.4 秒之间)\n大多数文字编辑器、浏览器还有其它界面工具都有一套可以让你马上使用的快捷键,不要想着一下子就把它们都学会。从一两个开始,把它们拿下,然后再用多几个再熟练,重复这个过程即可。\n有时候你需要使用一系列键盘操作才能完成一个指令,这时候别名和宏就派上用场了。你只需要把你常用的一系列操作用一个简单明了的命令代替就可以了。做好得了的话,一个月可以节省了几个小时的时间呢。\n自定义 有许多你重要的工具(比如你的文本编辑器)会允许你自定义它们的界面,这样不仅仅可以让界面符合你个人的使用习惯,甚至可以加强与别的工具的协同效应。比如说,你是一个 vim 使用者,你经常使用 j 和 k 进行上下移动,那为什么不尝试安装 chrome 插件让你可以在 chrome 里使用一样的快捷键呢? 你甚至可以不止步于简单的键盘绑定。许多工具都有脚本语言或扩展框架让你可以自己写代码去自定义工具。当然这比简单的键盘绑定花费更多的时间,但是这是一个能够让你明白工具内部运行的好方法,也是一个启动开源项目的好机会,这些都可以让你的简历锦上添花。\n可移植的配置 你安装了所有工具,学了所有的键盘绑定,把它们都自定义化了。 但是突然,你被迫要在一个远程机器或者新电脑上工作,那前面所有努力都白费了,你要从 0 开始,对吗? 那如果你不仅仅自定义了你的工具,还把相应的配置都变得轻便可以移植了呢?许多工具拥有包含工具配置参数的 .rc 文件或者配置文件。这个可以允许你把它们放到源代码控制里面,或分享到其它机器其它仓库里面。你只需要用脚本把它们关联到正确的位置,让你大多数的配置在两步命令中启动-克隆你的仓库,运行脚本,然后,你就可以开始工作了。\n这是一个很好的类似的例子\n减少文本切换 最后一个方法是最小化你切换工具的时间。 在你其实只需要看几行代码的时候,你真的需要把终端放大到占满整个屏幕吗?\n尝试一下使用不同的窗口界面、不同的屏幕数量和不同的工作区设置,来帮你你找到一套最适用于你的解决方案。你可以在不同的工作场景使用几套不同的预设置界面。像我,我写代码和调试的时候会使用一套窗口设置,在读代码和审核代码时会使用另一套。切换它们也是关联了一个热键。\n结论 你的工具就是你的扩展,就像你的其它部位,需要投资和实践去改善。 如果你有时会因为你过时的工作流而感到沮丧,或者只是想挤出更多的时间,我希望我这篇文章可以在如何改善你的日常工作工具使用上给到帮助。\n","title":"只需 4 步,改善你的编程工具"},{"location":"https://codenow.me/tips/run-scheduled-py-on-windows/","text":"Windows下定时执行任务,本质即在任务计划程序中添加自定义的Py文件执行任务。\n操作方法:\n 在计算机管理中打开任务计划程序。 点击创建基本任务,开始创建任务程序 设置启动py文件任务 最后点击完成,即可在活动任务中找到添加的任务。\n","title":"Windows下定时执行Py文件"},{"location":"https://codenow.me/algorithm/leetcode_2_add_two_numbers/","text":" 题号:2\n难度:Medium\n链接:https://leetcode.com/problems/add-two-numbers/\n如下是 python3 代码\n # Definition for singly-linked list. # class ListNode(object): # def __init__(self, x): # self.val = x # self.next = None class Solution(object): def addTwoNumbers(self, l1, l2): \u0026#34;\u0026#34;\u0026#34; :type l1: ListNode :type l2: ListNode :rtype: ListNode \u0026#34;\u0026#34;\u0026#34; if l1.next is None and l1.val == 0: return l2 if l2.next is None and l2.val == 0: return l1 str1 = \u0026#39;\u0026#39; str2 = \u0026#39;\u0026#39; while l1: str1 = str(l1.val) + str1 l1 = l1.next while l2: str2 = str(l2.val) + str2 l2 = l2.next add_num = list(str(int(str1)+int(str2)))[::-1] Nodes = [ListNode(num) for num in add_num] for i in range(len(Nodes)-1): Nodes[i].next = Nodes[i+1] return Nodes[0] ","title":"Leetcode: 2 Add Two Numbers"},{"location":"https://codenow.me/articles/build_api_by_flask/","text":" 前言 最近在学习微信小程序,前后端数据交互时,需要API提供数据操作。便学习通过python建立API。\n示例 创建获取数据API @app.route(\u0026#39;/api/v1.0\u0026#39;, methods=[\u0026#39;GET\u0026#39;]) def get_data(): data = function # function为从数据库中获取内容的操作函数。 return jsonify({\u0026#39;data\u0026#39;:data}) # 返回json格式的数据。 操作结果 创建数据提交API @app.route(\u0026#39;/post/\u0026#39;, methods=[\u0026#39;POST\u0026#39;]) def post_data(): args = request.args.get(\u0026#39;arg_name\u0026#39;) # request.args.get提供了从url获取参数的功能,通过参数将数据传递后后端 status = post_function(args) # post数据的函数,成功返回200,失败返回错误信息。 return jsonify({\u0026#39;status\u0026#39;:status}) # 将信息返回给前端 上述情况中参数传递形式为:\nhttp://127.0.0.1:5000/post/?name=小李\u0026amp;age=20 # name和age为参数名, 后面的值为具体参数值。 动态url规则下可以直接获取参数: app.route(\u0026#39;/post/\u0026lt;id\u0026gt;\u0026#39;, methods=[\u0026#39;POST\u0026#39;]) def post_data(id): # 直接将\u0026lt;id\u0026gt;的值作为函数参数 status = post_function(id) # psot id return jsonify({\u0026#39;status\u0026#39;:status}) 上述情况参数传递形式为:\nhttp://127.0.0.1:5000/post/1 # 1为参数 ","title":"Python下使用Flask建立API"},{"location":"https://codenow.me/tips/awk-de-duplication/","text":"使用 AWK 对数据进行去重:awk '!a[$0]++{print}'\n","title":"Awk De Duplication"},{"location":"https://codenow.me/articles/sentry-python-sdk/","text":" Sentry 是一个开源的实时错误报告工具,支持 web 前后端、移动应用以及游戏,支持 Python、OC、Java、Go、Node、Django、RoR 等主流编程语言和框架 ,还提供了 GitHub、Slack、Trello 等常见开发工具的集成。\n 这周重新用 docker 部署了一下 Sentry server,比 python 部署确实方便多了,docker-compose 官方都给写好了,改改配置就可以直接上\nserver 端部署好之后,又看了一下 client 端是怎么做的。就感觉这个 SDK 做的真好,接入成本极低,以 Flask 为例,只要加上两行即可:\nfrom raven.contrib.flask import Sentry sentry = Sentry(app, dsn='http://00d5b7d6d7f1498687430d160fd48ea8:ae6eaaf3c8d24d3f98537453da1f6a4f@localhost:9000/2') 于是仔细读了读 SDK 的实现,主要研究三个问题: 1. sentry 是怎么把 SDK 写得如此简洁的? 2. sentry 是如何捕捉到要发送的错误信息的? 3. sentry 是如何把错误信息发送到 server 端的?\n注:以下使用的语言及 package 版本为: * python 3.6 * raven 6.10.0 * Flask 1.0.2\n先看一下目录结构:\nraven ├── conf # 配置相关 ├── contrib # 适配各框架的代码 ├── data # 证书等 ├── handlers\t# 日志记录等 ├── scripts # 测试脚本 ├── transport # 实际发消息给服务端的东西 ├── utils # 一些内部工具 ├── __init__.py ├── base.py # 最主要的 Client 类 ├── breadcrumbs.py # 一个特殊概念,还没搞很懂 ├── context.py # 上下文相关 ├── events.py # 事件,是与服务端交互的基本单位 ├── exceptions.py # 异常类 ├── middleware.py # wsgi中间件 ├── processors.py # 数据处理相关 └── versioning.py # 获取 git 版本或 pkg 版本 最核心的,是 base.py 里的 Client 类,所有的操作都是围绕它来展开的。但是由于 SDK 的封装,我们在使用默认配置时并未直接接触到它,而是使用的 raven.contrib.flask.Sentry。下面我们写一段最简单的代码,然后顺着看下去\nfrom flask import Flask from raven.contrib.flask import Sentry app = Flask(__name__) sentry = Sentry(app, dsn='http://00d5b7d6d7f1498687430d160fd48ea8:ae6eaaf3c8d24d3f98537453da1f6a4f@localhost:9000/2') @app.route('/') def index(): return 'index\\n' @app.route('/bad') def bad(): return 1/0 if __name__ == '__main__': app.run('0.0.0.0', 8888, debug=True) 这段代码创建了一个最简单的 Flask app,然后将其传入 raven.contrib.flask.Sentry 中,并传入 dsn。dsn 就是一个字符串,但包含了很多信息,它会在 conf.remote.RemoteConfig 中被处理,处理结果如下:\ndsn schema: 'http',标记给服务端发送请求的方式,同时也可以指示使用哪种 raven.transport,比如写 \u0026lsquo;requests+http\u0026rsquo; 就是用 requests 包直接发送请求,写 \u0026lsquo;gevent+http\u0026rsquo; 就是用 gevent 包异步发送请求 public_key: '00d5b7d6d7f1498687430d160fd48ea8',用户名,没有这个的话服务端不会接收请求 secret_key: 'ae6eaaf3c8d24d3f98537453da1f6a4f',密码,新版已经废弃,不建议继续使用 base_url: 'http://localhost:9000',是服务端地址 project: '2',是在 sentry 的 web 页面里创建的项目的编号 options: 是 url.query,我这里内容为空 transport: 新版建议 Transport 应在初始化 Client 时明确传入,而不是用 schema 的方式配置。但仍然支持着这个功能。 这里详细说一下 transport,顺便就可以解决掉我们刚才提出的第三个问题:sentry 是如何把错误信息发送到 server 端的?\ntransport sentry 中的 transport 有两类,同步的和异步的,异步的需要继承 AsyncTransport 和 HTTPTransport,同步的只需要继承 HTTPTransport,如果想增加新的 transport,只需要实现其规定的方法,然后传入 Client 即可:\n同步的有: * HTTPTransport * EventletHTTPTransport * RequestsHTTPTransport\n异步的有: * ThreadedHTTPTransport * GeventedHTTPTransport * TwistedHTTPTransport * ThreadedRequestsHTTPTransport * TornadoHTTPTransport\n默认 transport 是在 conf.remote.DEFAULT_TRANSPORT 中定义的,当运行环境是 Google App Engine 或 AWS Lambda 时,使用 HTTPTransport(同步),否则使用 ThreadedHTTPTransport(异步)\nThreadedHTTPTransport 内部用 FIFO Queue + 新开线程的方式来实现异步,这里我们就不用太担心其性能问题了。而发送请求用的是 HTTPTransport 的 urllib2,超时时间5秒,没有重试\nbase.Client 接下来,我们来看一看初始化 Client 的时候,都干了些什么,顺便在这里解决掉第二个问题: sentry 是如何捕捉到要发送的错误信息的?\nclass Client(object): def __init__(self, dsn=None, raise_send_errors=False, transport=None, install_sys_hook=True, install_logging_hook=True, hook_libraries=None, enable_breadcrumbs=True, _random_seed=None, **options): 先看看可传入的参数: * raise_send_errors: 没找到合理的用法,无视 * transport: 上面有说,不建议用 dsn.schema 来控制 transport,最好应该在这里传入 * install_sys_hook: 默认是True,作用是修改 sys.excepthook,把自己的异常处理函数放进去 * hook_libraries: 对 httplib 和 requests 库做了些操作,没搞懂用处 * enable_breadcrumbs: 是否开启此功能 * _random_seed: sentry 可以设置只有部分异常会被发送到服务端,这个值是生成随机数的种子 * 其他参数\n其中,install_sys_hook 是重点,Client.__init__ 里除了进行各种初始化外,最重要的一件事就是这个了\ndef install_sys_hook(self): global __excepthook__ if __excepthook__ is None: __excepthook__ = sys.excepthook def handle_exception(*exc_info): self.captureException(exc_info=exc_info, level='fatal') __excepthook__(*exc_info) handle_exception.raven_client = self sys.excepthook = handle_exception 可以看到,这里在原有内置函数的基础上,加了一句 self.captureException,当一个异常未被 catch 住时,就会调用 sys.excepthook,同时也就发出了发出了请求\n至于 self.captureException 内部,简单来说就做三件事: 1. 获取上下文及各种信息,用到了 sys.exc_info(), flask._request_ctx_stack, flask._app_ctx_stack 和 breadcrumbs 等 2. 到处记日志 3. 构建消息体,选择 transport,发送\n于是,第二个问题,我们已经知道 sentry 的思路了:用 hook 来获取所有未处理的异常\n然而,对 flask 而言,事情并没有这么简单,因为在 flask 里,推荐的异常处理是 @app.errorhandler,同时 sys.excepthook 永远不会被调用\n那怎么办?我们看一下 raven.contrib.flask 吧\nraven.contrib.flask.Sentry class Sentry(object): def __init__(self, app=None, client=None, client_cls=Client, dsn=None, logging=False, logging_exclusions=None, level=logging.NOTSET, wrap_wsgi=None, register_signal=True): 这个参数就容易懂多了 * app: flask app * client: 即上文的 Client,可以自己定制后传入,不定制的话就默认生成一个 * client_cls: Client 类,用来默认生成 Client 对象,不过这个参数没什么意思 * dsn: 开头就有说 * logging, logging_exclusions,level: 日志定制 * wrap_wsgi: 是否将 sentry 加入 flask 中间件 * register_signal: 是否将 sentry 异常处理注册至 flask 的 got_request_exception 信号\n这里的关键是最后一个参数\n默认情况下 register_signal 被设置为 True,于是它会把一个 capture_exception 函数注册到 flask 的 got_request_exception 上\n而 flask 的 got_request_exception 会在 flask.app.handle_exception 的开头,把当前异常 send 出去,然后再执行原有逻辑\n到这里,是不是终于可以说,第二个问题也搞明白了?当发生了无法处理的异常时,flask 先用信号把异常发给 sentry.client,然后 sentry 用 sys.exc_info() 获取上下文,再把拼装好的信息传给相应 passport,最后发送给服务端\n但是,在做测试的时候,我们可能会发现,发生一个异常的同时,服务端收到了两个一毛一样的 event,这是怎么回事?\n答案就在 raven.contrib.flask.Sentry 的一个初始化参数里:wrap_wsgi\nif wrap_wsgi is not None: self.wrap_wsgi = wrap_wsgi elif self.wrap_wsgi is None: # Fix https://github.com/getsentry/raven-python/issues/412 # the gist is that we get errors twice in debug mode if we don't do this if app and app.debug: self.wrap_wsgi = False else: self.wrap_wsgi = True if self.wrap_wsgi: app.wsgi_app = SentryMiddleware(app.wsgi_app, self.client) 已知: 1. 在中间件 raven.middleware.Sentry.client 里,对所有未处理的异常,会向 sentry 服务端发送一次请求 2. production 模式下,发送完 got_request_exception 信号后,会 return InternalServerError()。而 debug 模式下则是把异常重新抛出\n因此当 debug 模式 + 有中间件时,raven.contrib.flask.Sentry.client(信号发送) 和 raven.middleware.Sentry.client(中间件异常捕捉发送) 都会捕捉到这个异常,并发送给服务端。而 production 模式下,只有信号的那一次,中间件不会发\n上面的代码中可以看到,这个问题被修复过一次了,但在我最开头的代码里,flask.app.debug = True 是在最后才运行的,传给 Sentry 的时候,还是 app.debug = False,因此 https://github.com/getsentry/raven-python/issues/412 仍然会发生,临时解决方式就是把 app.debug = True 放到初始化 Sentry 前就好\n好像写的有点长了,最后终于说到了第一个问题,sentry 是怎么把 SDK 写得如此简洁的?\n我认为答案是其丰富的配置项和默认设置,以及对环境变量和全局变量的灵活使用,我真是一边看代码,一边感慨这思路我真得赶紧抄过来\u0026hellip;\nSDK 里还有一些内容文章里没有涉及到(主要我也还没看),但比较重要的部分都在这里了,总结一下吧:\n 初始化 Client 类时,会从环境变量里获取各种配置,从 dsn 里确定 remote 服务端,选定 transport 并初始化各种 logger, 上下文等等 设置 hook,或者其他手段,保证在发生异常时,能够获取得到异常及上下文 同步/异步 发送消息给服务端,同时不让 sentry 占用太多资源 TODO: 1. breadcrumbs 到底是干啥的 2. 其他框架都是怎么和 sentry 结合的\n","title":"Sentry Python Sdk"},{"location":"https://codenow.me/algorithm/leetcode-13-roman-to-integer/","text":" 题号:13\n难度:easy\n链接:https://leetcode.com/problems/roman-to-integer\n描述:罗马数字转阿拉伯数字(1-3999)\n class Solution(object): def romanToInt1(self, s: str) -\u0026gt; int: \u0026quot;\u0026quot;\u0026quot;仿照上一题的无脑解法,先直接怼一个出来\u0026quot;\u0026quot;\u0026quot; ten = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'] res = 0 if s.startswith('MMM'): res += 3000 s = s[3:] elif s.startswith('MM'): res += 2000 s = s[2:] elif s.startswith('M'): res += 1000 s = s[1:] if s.startswith('CM'): res += 900 s = s[2:] elif s.startswith('D'): res += 500 s = s[1:] elif s.startswith('CD'): res += 400 s = s[2:] if s.startswith('CCC'): res += 300 s = s[3:] elif s.startswith('CC'): res += 200 s = s[2:] elif s.startswith('C'): res += 100 s = s[1:] if s.startswith('XC'): res += 90 s = s[2:] elif s.startswith('L'): res += 50 s = s[1:] elif s.startswith('XL'): res += 40 s = s[2:] if s.startswith('XXX'): res += 30 s = s[3:] elif s.startswith('XX'): res += 20 s = s[2:] elif s.startswith('X'): res += 10 s = s[1:] res += ten.index(s) return res def romanToInt2(self, s: str) -\u0026gt; int: \u0026quot;\u0026quot;\u0026quot;有一个微妙的规律,当 左\u0026lt;右 时,减掉左,其余为加左\u0026quot;\u0026quot;\u0026quot; res = 0 roman = {'M': 1000, 'D': 500, 'C': 100, 'L': 50, 'X': 10, 'V': 5, 'I': 1} for i in range(0, len(s) - 1): if roman[s[i]] \u0026lt; roman[s[i + 1]]: res -= roman[s[i]] else: res += roman[s[i]] res += roman[s[-1]] return res def romanToInt(self, s: str) -\u0026gt; int: \u0026quot;\u0026quot;\u0026quot;看了看 discuss,有人给了个更简单的写法 这里反向迭代的原因是,可以确定 I 是最小的\u0026quot;\u0026quot;\u0026quot; res, p = 0, 'I' roman = {'M': 1000, 'D': 500, 'C': 100, 'L': 50, 'X': 10, 'V': 5, 'I': 1} for c in s[::-1]: res, p = res - roman[c] if roman[c] \u0026lt; roman[p] else res + roman[c], c return res if __name__ == '__main__': print(Solution().romanToInt('MCMXCIV')) ","title":"Leetcode 13 Roman to Integer"},{"location":"https://codenow.me/translation/control_startup_and_shutdown_order_in_compose/","text":"原文链接:https://docs.docker.com/compose/startup-order/,翻译如下:\n您可以使用“depends_on”选项控制服务启动和关闭的顺序。compose总是按依赖顺序启动和停止容器,依赖性由depends_on、links、volumes_form和网络模式“service:…”确定。\n但是,对于启动,compose不会等到容器“就绪”(对于特定的应用程序来说,这意味着什么)之后才运行。这是有充分理由的。\n等待数据库(例如)准备就绪的问题实际上只是分布式系统中一个更大问题的子集。在生产环境中,数据库可能随时不可用或移动主机。您的应用程序需要能够适应这些类型的故障。\n要处理此问题,请设计应用程序以尝试在失败后重新建立与数据库的连接。如果应用程序重试连接,它最终可以连接到数据库。\n最好的解决方案是在应用程序代码中执行这种签入,无论是在启动时还是在任何时候由于任何原因而丢失连接。但是,如果您不需要这种级别的恢复能力,您可以使用包装脚本来解决这个问题:\n 使用诸如wait for it、dockerize或sh-compatible wait for等工具。这些是小包装脚本,您可以将其包含在应用程序的映像中,以轮询给定的主机和端口,直到它接受TCP连接。 例如,要使用wait-for-it.sh或wait-for-wrap服务的命令:\nversion: \u0026quot;2\u0026quot; services: web: build: . ports: - \u0026quot;80:8000\u0026quot; depends_on: - \u0026quot;db\u0026quot; command: [\u0026quot;./wait-for-it.sh\u0026quot;, \u0026quot;db:5432\u0026quot;, \u0026quot;--\u0026quot;, \u0026quot;python\u0026quot;, \u0026quot;app.py\u0026quot;] db: image: postgres 提示:第一个解决方案有局限性。例如,它不验证特定服务何时真正准备好。如果向命令添加更多参数,请使用带有循环的bash shift命令,如下一个示例所示。\n 或者,编写自己的包装器脚本来执行更特定于应用程序的健康检查。例如,您可能希望等待Postgres完全准备好接受命令: #!/bin/sh # wait-for-postgres.sh set -e host=\u0026quot;$1\u0026quot; shift cmd=\u0026quot;$@\u0026quot; until PGPASSWORD=$POSTGRES_PASSWORD psql -h \u0026quot;$host\u0026quot; -U \u0026quot;postgres\u0026quot; -c '\\q'; do \u0026gt;\u0026amp;2 echo \u0026quot;Postgres is unavailable - sleeping\u0026quot; sleep 1 done \u0026gt;\u0026amp;2 echo \u0026quot;Postgres is up - executing command\u0026quot; exec $cmd 您可以将其用作包装脚本,如前一个示例中所示,方法是设置:\n command: [\u0026ldquo;./wait-for-postgres.sh\u0026rdquo;, \u0026ldquo;db\u0026rdquo;, \u0026ldquo;python\u0026rdquo;, \u0026ldquo;app.py\u0026rdquo;]\n ","title":"Control_startup_and_shutdown_order_in_Compose"},{"location":"https://codenow.me/translation/five-simple-strategies-for-securing-apis/","text":" 保护API的五个简单策略 验证参数 任何弹性API实现的第一步是清理所有传入数据以进行确认它是有效的,不会造成伤害。对参数唯一最有效的防御操作和注入攻击时针对严格的模式验证所有传入的数据有效地描述了被认为是系统允许的输入。模式验证应尽可能具有限制性,尽可能使用输入、范围、集合甚至显性列表。还要考虑从许多开发工具生产的自动生成的模式通常会将所有参数减少到过于宽泛而无法有效识别潜在威胁的模型。手工构建的白名单时更优选的,因为开发人员可以根据他们对应用程序所期望的数据模型的理解来约束输入。基于XML的内容类型的一个选项是使用XML模式语言,该语言在创建受限制的内容模型和高度受约束的结构方面非常有效。对于日益普遍的JSON数据类型,有几种JSON模式描述语言。虽然没有XML那么丰富,但JSON的编写和理解要简单的多,提供透明度使其安全度提高。\n应用显示威胁检测 良好的模式验证可以防止许多注入攻击,但也要考虑显示扫描常见的攻击签名。SQL注入或脚本注入攻击经常通过扫描原始输入容易发现的常见模式来进行攻击。 同时考虑可能采取其他形式,例如拒绝服务(DoS)。利用网咯基础设施俩发现和缓解网络级DoS攻击,还可以检查利用参数的DoS攻击。庞大的信息、严重嵌套的数据结构或过于复杂的数据结构都可能导致有效的拒绝服务攻击,从而不必要地消耗受影响的API服务器上的资源。将病毒检测应用于所有潜在风险的编码内容。文件传输中涉及的API硬解码base64附件并将其提交到服务器级病毒扫描,然后再保存到文件系统,在这些文件系统中可能会无意中激活它们。\n始终开启SSL 使SSL / TLS成为所有API的规则。 在21世纪,SSL并不奢侈; 这是一个基本要求。 添加SSL / TLS并正确应用它可以有效抵御中间人攻击的风险。 SSL / TLS为客户端和服务器之间交换的所有数据提供完整性,包括重要的访问令牌,例如OAuth中使用的令牌。 它可选地使用证书提供客户端身份验证,这在许多环境中很重要。\n应用严格的身份验证和授权 用户和应用程序标识是必须单独实现和管理的概念。 考虑基于广泛身份上下文的授权,包括实际因素,例如传入IP地址(如果已知是固定的或在特定范围内),访问时间窗口,设备标识(对移动应用程序有用),地理位置等。 OAuth正在迅速成为以用户为中心的API授权的首选资源,但它仍然是一个复杂,快速变化和困难的技术。 开发人员应该遵循基本的,易于理解的OAuth用例,并始终使用现有的库而不是尝试构建自己的库。\n使用经过验证的Solutions 安全的第一条规则是:不要发明自己的。 没有理由创建自己的API安全框架,因为API已经存在优秀的安全解决方案。 挑战在于正确应用它们。\n","title":"Five Simple Strategies for Securing APIs"},{"location":"https://codenow.me/algorithm/leetcode_283_move_zeroes/","text":" 题号:283 难度:Easy 链接:https://leetcode.com/problems/move-zeroes/\n #!/usr/bin/python # -*- coding:utf-8 -*- class Solution: def moveZeroes(self, nums): \u0026#34;\u0026#34;\u0026#34; Do not return anything, modify nums in-place instead. \u0026#34;\u0026#34;\u0026#34; empty_index_list = [] for i in range(len(nums)): if i == 0: empty_index_list.append(i) elif empty_index_list: nums[empty_index_list.pop(0)] = nums[i] if empty_index_list: for i in range(len(empty_index_list)): nums[len(nums)-i] = 0","title":"Leetcode_283_Move_Zeroes"},{"location":"https://codenow.me/tips/how_to_set_ip_address_in_ubuntu18/","text":"修改yaml文件\nUbuntu 18.04使用netplan配置网络,其配置文件是yaml格式的。\n安装好Ubuntu 18.04之后,在/etc/netplan/目录下默认的配置文件名是50-cloud-init.yaml或者是01-network-manager-all.yaml\n我们通过VIM修改它\nsudo vim /etc/netplan/50-cloud-init.yaml #Let NetworkManager manage all devices on this system network: #version: 2 #renderer: NetworkManager ethernets: ens33: addresses: [172.20.12.74/24] gateway4: 172.20.12.254 dhcp4: no nameservers: addresses: [103.16.125.251, 103.16.125.252] 重启网络服务使配置生效:\n sudo netplan apply\n 注意事项\n1) 无论是ifupdown还是netplan,配置的思路都是一致的,在配置文件里面按照规则填入IP、掩码、网关、DNS等信息。\n注意yaml是层次结构,需要缩进,冒号(:)表示字典,连字符(-)表示列表。\n比如:\nnetwork: ethernets: ens160: addresses: - 210.72.92.28/24 # IP及掩码 gateway4: 210.72.92.254 # 网关 nameservers: addresses: - 8.8.8.8 # DNS version:2 2)只是针对ubuntu18.04 Server版,对于18.04 desktop它缺省是使用NetworkManger来进行管理,可使用图形界面进行配置,其网络配置文件是保存在:/etc/NetworkManager/system-connections目录下的,跟Server版区别还是比较大的。\n3) 同时,在 Ubuntu 18.04 中,我们定义子网掩码的时候不是像旧版本的那样把 IP 和子网掩码分成两项配置。\n在旧版本的 Ubuntu 里,我们一般配置的 IP 和子网掩码是这样的:\n address = 192.168.225.50 netmask = 255.255.255.0\n 而在 netplan 中,我们把这两项合并成一项,就像这样:\n addresses : [192.168.225.50\u0026frasl;24]\n 4) 配置完成之后保存并关闭配置文件。然后用下面这行命令来应用刚才的配置:\n $ sudo netplan apply\n 如果在应用配置的时候有出现问题的话,可以通过如下的命令来查看刚才配置的内容出了什么问题。\n $ sudo netplan \u0026ndash;debug apply\n 这行命令会输出这些 debug 信息:\nroot@ubuntu:/etc/netplan# vim 01-network-manager-all.yaml root@ubuntu:/etc/netplan# netplan --debug apply ** (generate:27915): DEBUG: 00:58:04.024: Processing input file /etc/netplan/01-network-manager-all.yaml.. ** (generate:27915): DEBUG: 00:58:04.025: starting new processing pass ** (generate:27915): DEBUG: 00:58:04.025: ens33: setting default backend to 1 ** (generate:27915): DEBUG: 00:58:04.025: Generating output files.. ** (generate:27915): DEBUG: 00:58:04.025: NetworkManager: definition ens33 is not for us (backend 1) DEBUG:netplan generated networkd configuration exists, restarting networkd DEBUG:no netplan generated NM configuration exists DEBUG:ens33 not found in {} DEBUG:Merged config: network: bonds: {} bridges: {} ethernets: ens33: addresses: - 192.168.23.129/24 dhcp4: false gateway4: 172.20.12.254 nameservers: addresses: - 103.16.125.251 - 103.16.125.252 vlans: {} wifis: {} DEBUG:Skipping non-physical interface: lo DEBUG:device ens33 operstate is up, not changing DEBUG:Skipping non-physical interface: docker0 DEBUG:Skipping non-physical interface: veth6c57bfa DEBUG:Skipping non-physical interface: vethf518b7b DEBUG:Skipping non-physical interface: veth9faaf09 DEBUG:{} DEBUG:netplan triggering .link rules for lo DEBUG:netplan triggering .link rules for ens33 DEBUG:netplan triggering .link rules for docker0 DEBUG:netplan triggering .link rules for veth6c57bfa DEBUG:netplan triggering .link rules for vethf518b7b DEBUG:netplan triggering .link rules for veth9faaf09 root@ubuntu:/etc/netplan# 5) 在 Ubuntu 18.04 LTS 中配置动态 IP 地址\n其实配置文件中的初始配置就是动态 IP 的配置,所以你想要使用动态 IP 的话不需要再去做任何的配置操作。\n如果你已经配置了静态 IP 地址,想要恢复之前动态 IP 的配置,就把在上面静态 IP 配置中所添加的相关配置项删除,把整个配置文件恢复成之前的样子\n# Let NetworkManager manage all devices on this system network: version: 2 renderer: NetworkManager 然后,运行: \u0026gt; $ sudo netplan apply\n","title":"如何在Ubuntu18中设置静态IP"},{"location":"https://codenow.me/articles/python_usefull_log_method/","text":" 1. 使用logger root@ubuntu:/home/hank# cat test_logger.py #! /usr/bin/python import logging import os class TestLogger: def __init__(self, log_name, log_dir=None, default_level=logging.DEBUG): self.logger = logging.getLogger(log_name) if not self.logger.handlers: log_dir = \u0026#34;/var/log/\u0026#34; if not log_dir else log_dir os.mkdir(log_dir) if not os.path.exists(log_dir) else None absolute_log = os.path.join(log_dir, log_name + \u0026#39;.log\u0026#39;) handler = logging.FileHandler(absolute_log) formatter = logging.Formatter(\u0026#39;%(asctime)-25s%(levelname)-8s%(message)s\u0026#39;) handler.setFormatter(formatter) self.logger.addHandler(handler) self.logger.setLevel(default_level) def debug(self, msg): self.logger.debug(msg) def info(self, msg): self.logger.info(msg) def error(self, msg): self.logger.error(msg) def critical(self, msg): self.logger.critical(msg) if __name__ == \u0026#39;__main__\u0026#39;: log_file, __ = os.path.splitext(os.path.basename(os.path.realpath(__file__))) logger = TestLogger(log_file) logger.info(\u0026#39;information test\u0026#39;) logger.error(\u0026#39;error test\u0026#39;)root@ubuntu:/home/hank# cat test.py #!/usr/bin/python from test_logger import TestLogger logger = TestLogger(\u0026#34;test\u0026#34;) logger.info(\u0026#34;information test\u0026#34;) logger.error(\u0026#39;error test\u0026#39;) formatter = logging.Formatter(\u0026lsquo;%(asctime)-25s %(levelname)-8s %(message)s\u0026rsquo;) 这里定义-25s的原因是asctime为22。所以25正好够和后面的levelname相隔3\nroot@ubuntu:/home/hank# cat /var/log/test.log 2019-04-04 19:18:53,432 INFO information test 2019-04-04 19:18:53,432 ERROR error test 2. 简单的自定义log模块 #! /usr/bin/python class SimpleLogger: def __init__(self, file_name): self.file_name = file_name def _write_log(self, level, msg): with open(self.file_name, \u0026#34;a\u0026#34;) as log_file: log_file.write(\u0026#34;[{0}] {1}\\n\u0026#34;.format(level, msg)) def debug(self, msg): self._write_log(\u0026#34;DEBUG\u0026#34;, msg) def info(self, msg): self._write_log(\u0026#34;INFO\u0026#34;, msg) def warn(self, msg): self._write_log(\u0026#34;WARN\u0026#34;, msg) def error(self, msg): self._write_log(\u0026#34;ERROR\u0026#34;, msg) def critical(self, msg): self._write_log(\u0026#34;CRITICAL\u0026#34;, msg) root@ubuntu:/home/hank# cat test3.py #! /usr/bin/python from simple_logger import SimpleLogger logger = SimpleLogger(\u0026#34;simple_logger.log\u0026#34;) logger.warn(\u0026#34;this is a warm\u0026#34;) logger.info(\u0026#34;this is a info\u0026#34;) root@ubuntu:/home/hank# cat simple_logger.log [WARN] this is a warm [INFO] this is a info 3. log中关于读写的问题 写日志时,如是日志文件不存在,则创建;且可以写日志,且方式是追加,所以用a\n","title":"工作中用到的python写log方式"},{"location":"https://codenow.me/articles/spark_installation_onwindows/","text":" 下载安装Java,安装版本为8 Java8下载地址 安装教程详见:菜鸟教程—Java安装\n下载spark安装包 spark2.3.3下载地址\n建议安装2.3.3版本,高版本的2.4.0在运行时会报错Py4j error。 下载后解压文件夹,并将路径配置到系统变量中。\n系统环境变量中配置路径如下:\n下载Hadoop支持包 百度网盘下载地址 提取码:ezs5\n下载后解压,并添加系统变量:\n下载并安装pycharm和anaconda 具体安装教程可自行百度。\n安装后,将spark下的python中的pyspark拷贝到安装的python路径下的:Lib\\site-packages 然后运行pip install py4j\n配置pycharm运行spark环境 根据上图进行配置后即可运行spark程序。\n配置日志显示级别 在spark\\conf目录下创建log4j.properties配置文件,该目录下有template模板,可以直接复制。\n然后将其中的:log4j.rootCategory=INFO, console 修改为 log4j.rootCategory=WARN, console\n配置cmd下pyspark在jupyter下运行 编辑spark目录下:bin\\pyspark2.cmd 修改其中对应部分为以下格式:\nrem Figure out which Python to use. if \u0026quot;x%PYSPARK_DRIVER_PYTHON%\u0026quot;==\u0026quot;x\u0026quot; ( set PYSPARK_DRIVER_PYTHON=jupyter set PYSPARK_DRIVER_PYTHON_OPTS=notebook if not [%PYSPARK_PYTHON%] == [] set PYSPARK_DRIVER_PYTHON=%PYSPARK_PYTHON% ) ","title":"Windows安装spark"},{"location":"https://codenow.me/algorithm/knn/","text":" K-近邻算法(KNN) 算法思想: 存在一个训练样本集,并且样本数据集中每个数据都存在标签。将新数据的每个特征与样本数据集中数据对应的特征进行比较,然后算法提取前K个相似的数据。\n 优缺点及适用范围: 优点:精度高、对异常值不敏感、无数据输入假定;\n缺点:计算复杂度高、空间复杂度高;\n使用数据范围:数值型和标称型\n 计算距离公式 示例分析:约会人员属性 使用的库 Numpy operator 读取数据:将文本记录转化为Numpy矩阵 def file2matrix(filename): fr = open(filename) arrayOLines = fr.readlines() numberOfLines = len(arrayOLines) returnMat = zeros((numberOfLines, 3)) # 创建numpy矩阵 1000行,3列 classLabelVector = [] # 类标签向量 index = 0 for line in arrayOLines: # 逐行填充矩阵和标签向量 line = line.strip() listFromLine = line.split('\\t') returnMat[index, :] = listFromLine[0:3] classLabelVector.append(int(listFromLine[-1])) index += 1 return returnMat, classLabelVector 处理数据:归一化数值(min-max标准化) def autoNorm(dataSet): minValue = dataSet.min(0) maxValue = dataSet.max(0) ranges = maxValue - minValue normDataSet = zeros(shape(dataSet)) m = dataSet.shape[0] normDataSet = dataSet - tile(minValue, (m, 1)) # titl函数将minValue复制成dataSet矩阵大小 normDataSet = normDataSet/tile(ranges, (m, 1)) return normDataSet, ranges, minValue K近邻算法代码 def classify0(inX, DataSet, labels, k: int): # inX:用于分类的输入向量 DataSet:训练样本 labels:标签向量 k:用于选择的最近邻居的数目 # 距离计算:向量之间的距离公式 DataSetSize = DataSet.shape[0] DiffMat = tile(inX, (DataSetSize, 1)) - DataSet # 将inX与DataSet做向量减法 sqDiffMat = DiffMat ** 2 # 将相减后的结果进行平方 sqDistances = sqDiffMat.sum(axis=1) # 将矩阵进行行相加 distances = sqDistances ** 0.5 # 将矩阵结果开平方 # 选择距离最小的k个点 sortedDistIndicies = distances.argsort() # 得到distances矩阵从小到大排序后的对应index classCount = {} for i in range(k): # 统计前k个距离中各个类别的个数 voteIlabel = labels[sortedDistIndicies[i]] classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 # 排序 sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True) # 将字典按照value值排序 return sortedClassCount[0][0] # 将排序后的第一个key值 测试算法:测试算法正确率 def datingClassTest(): hoRatio = 0.10 datingDataMat, datingLabels = file2matrix(\u0026quot;your dataset path\u0026quot;) normat, ranges, minvalue = autoNorm(datingDataMat) m = normat.shape[0] numTestVecs = int(m*hoRatio) errorCount = 0.0 for i in range(numTestVecs): classifierResult = classify0(normat[i, :], normat[numTestVecs:m, :], datingLabels[numTestVecs: m], 3) print(\u0026quot;the classifier came back with %s, the real answer is: %s\u0026quot; % (classifierResult, datingLabels[i])) if classifierResult != datingLabels[i]: errorCount += 1.0 print(errorCount, numTestVecs) print(\u0026quot;the total error rate is %f\u0026quot; % (errorCount/float(numTestVecs))) 测试结果 percentage of time spent playing video games?10\n frequent flier miles earned per year?10\nliters of ice cream consumed per year?20\nYou will probably like this person: In small does\n","title":"K近邻算法"},{"location":"https://codenow.me/tips/rsshub/","text":"Rsshub 是一个轻量、易于扩展的 RSS 生成器, 可以给任何奇奇怪怪的内容生成 RSS 订阅源\n","title":"万物皆可 RSS"},{"location":"https://codenow.me/articles/golang-namespace/","text":" 总所周知 Docker 最早诞生于 Linux 平台,利用的是 Linux LXC 技术作为基础。Docker 作为一种 “轻量级虚拟机” 跑在通用操作系统中,那么势必就要对容器进行隔离,保证在宿主机内的独立性。\nNamespace Overview 在 Linux Kernel 中有一组名为 Namespace 的系统调用 API。主要作用是封装了全局的系统资源的调用分配,在一个进程中隔离了其他进程的可见性,让自己 “拥有” 整个计算机的资源的能力。一个典型的用途就是容器的实现。\nnamespace 一种只有 4 个 API:\n clone:创建一个隔离的进程,可以通过参数控制所拥有的资源 setns:允许一个进程到现有的 namespace unshare:从现有 namespace 中移除一个进程 ioctl:用法发现 namespace 信息 接下来主要讨论如何创建一个具有隔离性的进程,也就是 clone 这个系统调用的用法。\nclone 创建一个新的 namespace(进程),可以对其控制几个方面的资源(通过 CLONE_NEW* 这系列参数)。\n IPC:CLONE_NEWIPC,System V IPC 和 POSIX message queue Network:CLONE_NEWNET,网络设备等 Mount:CLONE_NEWNS,挂载点 PID:CLONE_NEWPID,进程的 ID User:CLONE_NEWUSER:用户或组的 ID UTS:CLONE_NEWUTS:Hostname 和 NIS domain 这里 CLONE_NEWNS 比较奇特,这是最早的一个参数,后面也想不到还有更多粒度的资源控制,所以这是一个历史遗留问题。\nNamespace Usage 由于 Namespace 是 Linux 的系统调用,所以在其他操作系统是无法编译通过的。可以在 build 时候通过设置 GOOS = linux 解决,但是运行还是要放在 Linux 上运行。\n在 Golang 中创建一个新的进程,通过 CLONE_NEW* flag 设置资源隔离。\n// +build linux package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/exec\u0026#34; \u0026#34;syscall\u0026#34; ) func main() { cmd := exec.Command(\u0026#34;sh\u0026#34;) cmd.SysProcAttr = \u0026amp;syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } } 使用 env GOOS=linux go build -o nsprocess 编译后,copy nsprocess 到 linux 机器上执行。\n先看一下 CLONE_NEWUSER 的功能:\n$ id uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) 我们可以看到,这时候 UID 和我们宿主机上的不同,表明 user 资源被隔离了。\n$ ifconfig $ 网络设备信息也是空的,CLONE_NEWNET 的隔离也生效了。\n# hostname -b zxytest # hostname zxytest 修改 hostname 后到宿主机发现 hostname 并没有被修改,这就是 CLONE_NEWUTS 的隔离性。\n# mount -t proc proc /proc # ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 12:16 pts/0 00:00:00 sh root 3 1 0 12:17 pts/0 00:00:00 ps -ef mount proc 之后发现进程信息都没有了,只有当前的进程信息。\n ps 命名是通过读取 /proc 文件输出的,所以要先 mount proc\n 以上就 Linux Namespace 的基本用法,也是 docker 的基础技术。\n","title":"Golang Linux Namespace Usage"},{"location":"https://codenow.me/translation/beachmark-details/","text":" 我一直在优化我的 go 代码并且一直优化我的性能测试方案。\n让我们先看一个简单的例子:\nfunc BenchmarkReport(b *testing.B) { runtime.GC() for i := 0; i \u0026lt; b.N; i++ { r := fmt.Sprintf(\u0026#34;hello, world %d\u0026#34;, 123) runtime.KeepAlive(r) } } 执行 go test -beach . 会看到这样子的结果:\nBenchmarkReport-32 20000000 107 ns/op 这可能可以初略的估计性能表现,但是彻底的优化需要更详细的结果。\n将所有的内容压缩成一个数字必然是简单的。\n让我向你们介绍我写的 hrtime 包,以便于获取更详细的性能测试结果。\n直方图 第一个推荐使用的是 hrtime.NewBeachmark,重写上面的简单例子:\nfunc main() { bench := hrtime.NewBenchmark(20000000) for bench.Next() { r := fmt.Sprintf(\u0026#34;hello, world %d\u0026#34;, 123) runtime.KeepAlive(r) } fmt.Println(bench.Histogram(10)) } 它会输出:\navg 372ns; min 300ns; p50 400ns; max 295µs; p90 400ns; p99 500ns; p999 1.8µs; p9999 4.3µs; 300ns [ 7332554] ███████████████████████ 400ns [12535735] ████████████████████████████████████████ 600ns [ 18955] 800ns [ 2322] 1µs [ 20413] 1.2µs [ 34854] 1.4µs [ 25096] 1.6µs [ 10009] 1.8µs [ 4688] 2µs+[ 15374] 我们可以看出 P99 是 500ns,表示的是 1% 的测试超过 500ns,我们可以分配更小的字符串来优化:\nfunc main() { bench := hrtime.NewBenchmark(20000000) var back [1024]byte for bench.Next() { buffer := back[:0] buffer = append(buffer, []byte(\u0026#34;hello, world \u0026#34;)...) buffer = strconv.AppendInt(buffer, 123, 10) runtime.KeepAlive(buffer) } fmt.Println(bench.Histogram(10)) } 结果如下:\navg 267ns; min 200ns; p50 300ns; max 216µs; p90 300ns; p99 300ns; p999 1.1µs; p9999 3.6µs; 200ns [ 7211285] ██████████████████████▌ 300ns [12658260] ████████████████████████████████████████ 400ns [ 81076] 500ns [ 3226] 600ns [ 343] 700ns [ 136] 800ns [ 729] 900ns [ 8108] 1µs [ 15436] 1.1µs+[ 21401] 现在可以看到 99% 的测试已经从 500ns 降到了 300ns。\n如果你眼神犀利,可能已经注意到 go beachmark 给出了 107ns/op 但是 hrtime 给了 372ns/op。 这是获取更多测试信息的副作用,他们总是会有开销的。最终结果包括这种开销。\nStopwatch 有时候我们还行测试并发操作,这时候可能需要 Stopwatch。\n假如你想在测试一个多竞争 channel 的持续时间。当然这是一个认为的例子,大致描述了如何从一个 goroutine 开始在另一个 goroutine 结束并且打印结果。\nfunc main() { const numberOfExperiments = 1000 bench := hrtime.NewStopwatch(numberOfExperiments) ch := make(chan int32, 10) wait := make(chan struct{}) // start senders for i := 0; i \u0026lt; numberOfExperiments; i++ { go func() { \u0026lt;-wait ch \u0026lt;- bench.Start() }() } // start one receiver go func() { for lap := range ch { bench.Stop(lap) } }() // wait for all goroutines to be created time.Sleep(time.Second) // release all goroutines at the same time close(wait) // wait for all measurements to be completed bench.Wait() fmt.Println(bench.Histogram(10)) } hrtesting 当然重写所有的测试用例是不现实的。为此有 github.com/loov/hrtime/hrtesting 为测试提供 testing.B。\nfunc BenchmarkReport(b *testing.B) { bench := hrtesting.NewBenchmark(b) defer bench.Report() for bench.Next() { r := fmt.Sprintf(\u0026#34;hello, world %d\u0026#34;, 123) runtime.KeepAlive(r) } } 会打印出 P50、P90、P99:\nBenchmarkReport-32 3000000 427 ns/op --- BENCH: BenchmarkReport-32 benchmark_old.go:11: 24.5µs₅₀ 24.5µs₉₀ 24.5µs₉₉ N=1 benchmark_old.go:11: 400ns₅₀ 500ns₉₀ 12.8µs₉₉ N=100 benchmark_old.go:11: 400ns₅₀ 500ns₉₀ 500ns₉₉ N=10000 benchmark_old.go:11: 400ns₅₀ 500ns₉₀ 600ns₉₉ N=1000000 benchmark_old.go:11: 400ns₅₀ 500ns₉₀ 500ns₉₉ N=3000000 在 Go 1.12 中将会打印出所有的 Beachmark 而不是最后一个,但是在 Go 1.13 中可以输出的更好:\nBenchmarkReport-32 3174566 379 ns/op 400 ns/p50 400 ns/p90 ... 获得的结果也可以和 beachstat 进行比较。\nhrpolt 最后载介绍一下 github.com/loov/hrtime/hrplot,使用我实验性质的绘图包,我决定添加一种方便的方法来绘制测试结果。\nfunc BenchmarkReport(b *testing.B) { bench := hrtesting.NewBenchmark(b) defer bench.Report() defer hrplot.All(\u0026#34;all.svg\u0026#34;, bench) runtime.GC() for bench.Next() { r := fmt.Sprintf(\u0026#34;hello, world %d\u0026#34;, 123) runtime.KeepAlive(r) } } 将会创建一个 SVG 文件 all.svg。其中包括线性图,显示了每次迭代所花费的时间;第二个就是密度图,显示了测量时间的分布图,以及最后一个百分位的详情。\nConclusion 性能优化很有趣,但是有更好的根据可以变得更加有趣。\n去尝试 github.com/loov/hrtime 让我知道你更多的想法。\n","title":"更详细的 Go 性能测试"},{"location":"https://codenow.me/algorithm/leetcode_148_sort_list/","text":" 题号:148\n难度:中等\n链接:https://leetcode-cn.com/problems/sort-list/submissions/\n /** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func sortList(head *ListNode) *ListNode { quickSort(head, nil) return head } func getPartion(start *ListNode, end *ListNode) *ListNode { key := start.Val i := start j := start.Next for j != end { if (j.Val \u0026lt; key) { i = i.Next i.Val, j.Val = j.Val, i.Val } j = j.Next } start.Val, i.Val = i.Val, start.Val return i } func quickSort(head *ListNode, tail *ListNode) { if head != tail { partion := getPartion(head, tail) quickSort(head, partion) quickSort(partion.Next, tail) } }","title":"Leetcode: 148 Sort List"},{"location":"https://codenow.me/translation/how-does-facial-recognition-work/","text":" How does facia recognition work? (原文地址)[https://us.norton.com/internetsecurity-iot-how-facial-recognition-software-works.html]\nBy Steve Symanovich\n面部识别是一种通过技术识别人脸的方式。面部识别系统使用生物识别技术来映射来自照片或视频的面部特征。它将信息与已知面部数据库进行比较以找到匹配项。面部识别可以帮助验证个人身份,但它也会引发隐私问题。\n面部识别市场预计将从2017年的40亿美元增长到2022年的77亿美元。这是因为面部识别具有各种商业应用。它可用于从监控到营销的各个方面。\n但这就是它变得复杂的地方。如果隐私对您很重要,您可能希望控制您的个人信息(您的数据)的使用方式。事情就是这样:你的“面子”是数据。\n面部识别的工作原理 你可能善于识别面孔。您可能会发现很难找到家人,朋友或熟人的面孔。你熟悉他们的面部特征 - 他们的眼睛,鼻子,嘴巴 - 以及他们如何走到一起。\n这就是面部识别系统的工作方式,但是在一个宏大的算法规模上。在您看到面部的地方,识别技术会看到数据。可以存储和访问该数据。例如,根据乔治敦大学的一项研究,一半的美国成年人将他们的图像存储在执法机构可以搜索的一个或多个面部识别数据库中。\n那么面部识别是如何工作的呢?技术各不相同,但以下是基本步骤:\n第1步。从照片或视频中捕获您的脸部照片。你的脸可能会单独或在人群中出现。您的图像可能会显示您正向前看或几乎在剖面图中。\n第2步。面部识别软件可读取您脸部的几何形状。关键因素包括眼睛之间的距离以及从额头到下巴的距离。该软件识别面部标志 - 一个系统识别其中的68个 - 这是区分您的面部的关键。结果:你的面部特征。\n第3步。您的面部特征 - 数学公式 - 与已知面部的数据库进行比较。并考虑到这一点:至少有1.17亿美国人在一个或多个警察数据库中有他们的面孔图像。根据2018年5月的一份报告,联邦调查局已经获得了4.12亿张用于搜索的面部图像。\n第4步。做出决定。您的面部印记可能与面部识别系统数据库中的图像相匹配。\n一般来说,面部识别是如何运作的,但谁使用它?\n谁使用面部识别? 许多人和组织使用面部识别 - 并且在许多不同的地方。这是一个抽样:\n美国政府机场。面部识别系统可以监控机场来来往往的人员。美国国土安全部使用该技术识别逾期签证或可能受到刑事调查的人。华盛顿杜勒斯国际机场的海关官员于2018年8月首次使用面部识别器进行了逮捕,抓住了一名试图进入该国的冒名顶替者。 手机制造商的产品。Apple首先使用面部识别来解锁其iPhone X,并继续使用iPhone XS。面部身份验证 - 确保您在访问手机时成为现实。苹果表示,随机面部解锁手机的可能性大约为百万分之一。 大学在课堂上。面部识别软件本质上可以采取滚动。如果你决定削减课程,你的教授可以知道。甚至不要想让你聪明的室友接受考试。 网站上的社交媒体公司。当您将照片上传到其平台时,Facebook会使用算法来识别面部。社交媒体公司询问您是否要在照片中标记人物。如果您同意,则会创建指向其个人资料的链接。Facebook可以识别98%准确率的面孔。 入口和禁区内的企业。一些公司已经交易面部识别系统的安全徽章。除了安全性,它可能是与老板面对面交流的一种方式。 宗教场所的宗教团体。教会使用面部识别来扫描他们的会众,看看谁在场。这是跟踪常客和不那么常客的好方法,也是帮助定制捐赠请求的好方法。 商店中的零售商。零售商可以结合监控摄像头和面部识别来扫描购物者的面部。一个目标:识别可疑角色和潜在的扒手。 航空公司在登机口。您可能习惯于让代理人在登机口扫描登机牌以登机。至少有一家航空公司扫描你的脸。 营销活动中的营销人员和广告客户。营销人员在针对产品或创意的群组进行定位时,通常会考虑性别,年龄和种族等因素。面部识别可以用来定义那些观众甚至在音乐会之类的东西。\n","title":"人脸识别如何工作"},{"location":"https://codenow.me/translation/tidb-proposal-a-new-aggregation-function-execution-framework/","text":" 原文链接 Proposal: A new aggregate function execution framework\n摘要 这篇 proposal 提出了一种的聚合计算执行框架,用来提高聚合函数的执行性能。\n背景 在 release-2.0 版本中,聚合计算框架在 expression/aggregation 模块中。在这个框架中,所有的聚合函数都实现了 Aggregation 接口。所有的聚合函数使用 AggEvaluateContext 来保存聚合计算的中间结果(partial result)。AggEvaluateContext 中的 DistinctChecker 字段使用 byte 数组作为 key,用来对相同分组中的数据进行去重。在执行过程中, Update 接口会被调,为每行数据计算和更新中间结果。在执行过程中,它为每个聚合函数枚举每种可能的聚合状态,这回带来大量的 CPU 分支检测。\n在这个框架下,可以很简单的实现一个新的聚合函数。但是它也有很多缺点:\n Update 方法会为每条数据被调用。每次调用中都可能会带来大量开销,特别是执行过程中包含了上万条数据的时候。 Update 方法会为每种计算状态调用,这也会带来大量的 CPU 分支检测。比如, AVG 函数在 Partial1 和 Final 状态下行为是不一样的,Update 方法不得不使用 switch 语句来处理所有可能的状态。 GetResult 方法返回 types.Datum 类型作为每个分组的最终结果。在执行阶段,TiDB 目前使用 Chunk 来保存数据。使用了 aggregation 框架,不得不将返回的 Datum 类型转换成 Chunk ,这会带来大量的数据转换和内存分配工作。 AggEvaluateContext 用来保存每组分组数据的最终结果,相比实际所需,这会消耗更多的内存。比如 COUNT 函数原本只需要一个 int64 字段来保存行数。 distinctChecker 用来为数据去重,它使用的是 byte 数组作为 key。针对输入数据的 encoding 和 decoding 操作会带来大量的 CPU 开销,其实这个问题可以通过直接使用输入数据作为 key 来避免掉。 方案 在这个 PR 中 https://github.com/pingcap/tidb/pull/6852 ,提出了一个新的框架。新框架在 executor/aggfuncs 模块中。\n在新的执行框架中,每个聚合函数实现了 AggFunc 接口。使用 PartialResult 作为每个聚合函数的中间结果,PartialResult 实际是 unsafe.Pointer 类型。unsafe.Pointer 允许中间结果可以使用任何数据类型。\nAggFunc 接口包含以下函数:\n AllocPartialResult 分配和初始化某种特定数据结构来保存中间结果,将它转换成 PartialResult 类型并返回。聚合操作的实现,比如流式聚合 (Stream Aggregation) 要保存分配的 PartialResult ,用在后续和中间结果有关的操作上,比如 ResetPartialResult ,UpdatePartialResult 等等。 ResetPartialResult 为聚合函数重置中间结果。将输入的 PartialResult 转换成某种数据结构,用来存中间结果,并将每个字段重置成初始状态。 UpdatePartialResult 根据属于相同分组的输入数据计算并更新中间结果。 AppendFinalResult2Chunk 完成最终的计算并将最终结果直接添加到输入的 chunk 当中。像其他操作一样,它把 PartialResult 先转换成某种数据结构,计算最终的结果,然后将最终结果添加到提供的 chunk 当中。 MergePartialResult 使用输入的 PartialResults 计算最终结果。假设输入的 PartialResults 名称分别是dst 和 srt,它先把 dst 和 src 转换相同的数据结构,合并中间结果,将结果保存在 dst 中。 新的框架使用 Build() 函数来构建可执行的聚合函数。输入参数是:\n aggFuncDesc :在查询优化器层表示聚合函数的数据结构 ordinal:聚合函数的序号。这也是相应的聚合操作输出的 chunk 中输出列的顺序 Build() 方法为具体某种输入参数类型和聚合状态 (aggregate state) 构建可执行的聚合函数,输入数据类型、聚合状态等等信息越具体越好。\n原理 优点:\n 在新框架下,中间结果可以是任何类型。聚合函数可以根据实际需要来分配内存,不会造成浪费。当用在 hash aggregation 时,OOM 的风险也会被降低。 中间结果可以是任何类型,这意味着,聚合函数可以使用 map,并以具体某种输入类型作为 key。比如,使用 map[types.MyDecimal 来对输入的数值进行去重。通过这种方式,旧框架中 decoding 和 encoding 带来的开销被降低了。 UpdatePartialResult 被用来调用批量处理一组输入数据。为每条记录上而调用函数所带来的开销被节省掉了。由于所有的计算都使用 Chunk 来保存输入数据,在 Chunk 中相同列的数据在内存当中被连续存储,聚合函数会一个挨一个的执行,充分利用 CPU 缓存,减少缓存未命中(cache miss),从而提高执行性能。 对于每一种聚合状态和任何输入类型,都要实现对应的一个聚合函数来支持。这意味着在聚合状态和输入类型上面的 CPU 分支检测运算可以在 UpdatePartialResult 执行过程中被减少,更好的利用 CPU pipeline,提供执行速度。 AppendFinalResult2Chunk 直接将最终结果加入到 chunk 当中,不需要将数据转成 Datum 再将 Datum 转换回 Chunk。这减少了大量的对象分配,降低了 golang gc worker 的开销,避免了 Datum 和 Chunk 之间不必要的数据转换。 缺点:\n 每种聚合函数要分别为每一种可能的聚合状态和输入类型,实现对应的计算函数。这可能会带来大量的开发工作。需要做更多的编码工作来支持新的聚合函数。 兼容性 目前,新的框架只支持了流式聚合。如果 Build() 方法返回 nil ,那么系统会使用旧的框架。\n所以,这个新框架可以在开发过程中测试,所有的结果应该和旧框架一样。\n","title":"Tidb Proposal: A new aggregate function execution framework"},{"location":"https://codenow.me/algorithm/leetcode-find-k-pairs-with-smallest-sums/","text":"原题链接: 373. Find K Pairs with Smallest Sums\n典型的 Kth elements 问题,使用堆就行:\nimport heapq class Solution: def kSmallestPairs(self, nums1: \u0026#39;List[int]\u0026#39;, nums2: \u0026#39;List[int]\u0026#39;, k: \u0026#39;int\u0026#39;) -\u0026gt; \u0026#39;List[List[int]]\u0026#39;: if len(nums1) == 0 or len(nums2) == 0: return [] q = [] for n1 in nums1: for n2 in nums2: heapq.heappush(q, (n1 + n2, n1, n2)) i = k ret = [] while i \u0026gt; 0 and len(q) \u0026gt; 0: item = heapq.heappop(q) ret.append([item[1], item[2]]) i = i - 1 return ret","title":"Leetcode Find K Pairs With Smallest Sums"},{"location":"https://codenow.me/tips/golang-type-detecting/","text":"类型直接转换:\nv := value.(*TypeA) 类型检测+转换:\nv, isTypeB := value.(*TypeB) 类型检测 + switch:\nswitch v.(*Type) { case TypeA: //... case TypeB: //... \tdefault: //... }","title":"golang 类型检测"},{"location":"https://codenow.me/articles/tidb-aggregation/","text":"没了解过 Aggregation 的执行细节之前,感觉 Aggregation 比较神奇,它和普通的 SPJ 查询不太一样,Aggregation 会对数据分组并聚合计算,经过 Aggregation,整个数据的 schema 都会发生改变。\n但其实,常见的 Aggregation 也并不复杂,从代码里看,和 Aggregation 相关的数据结构是这样的:\n// LogicalAggregation represents an aggregate plan. type LogicalAggregation struct { logicalSchemaProducer AggFuncs []*aggregation.AggFuncDesc GroupByItems []expression.Expression // groupByCols stores the columns that are group-by items. groupByCols []*expression.Column possibleProperties [][]*expression.Column inputCount float64 // inputCount is the input count of this plan. } type basePhysicalAgg struct { physicalSchemaProducer AggFuncs []*aggregation.AggFuncDesc GroupByItems []expression.Expression } // PhysicalHashAgg is hash operator of aggregate. type PhysicalHashAgg struct { basePhysicalAgg } // PhysicalStreamAgg is stream operator of aggregate. type PhysicalStreamAgg struct { basePhysicalAgg } 无论是逻辑查询计划还是物理查询计划,聚合计算所需的关键信息都主要是聚合函数 (AggFuncs) 和分组规则 (GroupByItems) 。执行查询时,遍历子节点数据,根据 GroupByItems 将数据划分到不同的组中,然后调用聚合函数更新计算结果,直到子节点的数据全部消费完。\n下面是 v2.0.9 版本 HashAggExec 的计算代码:\n// execute fetches Chunks from src and update each aggregate function for each row in Chunk. func (e *HashAggExec) execute(ctx context.Context) (err error) { inputIter := chunk.NewIterator4Chunk(e.childrenResults[0]) for { err := e.children[0].Next(ctx, e.childrenResults[0]) // 获取子节点数据 // ... // no more data. if e.childrenResults[0].NumRows() == 0 { return nil } for row := inputIter.Begin(); row != inputIter.End(); row = inputIter.Next() { groupKey, err := e.getGroupKey(row) // 为每行数据计算 groupKey //... aggCtxs := e.getContexts(groupKey) // 根据 groupKey 获取对应的计算结果 for i, af := range e.AggFuncs { // 遍历聚合函数,针对每行数据调用每一个聚合函数,更新聚合计算的结果 err = af.Update(aggCtxs[i], e.sc, row) //... } } } } HashAggExec 是使用 HashTable 保存不同分组的中间计算结果 (PartialResult) ,等数据全部消费完以后,HashTable 中的结果则是最终结果了。上面代码的主要计算过程:\n 遍历子节点数据(子节点可能是 Join、TableScan、Selection 等等) 为每行数据计算 groupKey ,根据 groupKey 获取中间计算结果 调用聚合函数,使用新遍历到的数据更新计算结果 下面以 avg 函数代码为例,分析一下聚合函数的代码逻辑:\nfunc (af *aggFunction) updateSum(sc *stmtctx.StatementContext, evalCtx *AggEvaluateContext, row types.Row) error { a := af.Args[0] value, err := a.Eval(row) // 计算每行数据对应的 value //... evalCtx.Value, err = calculateSum(sc, evalCtx.Value, value) // 更新总和 //... evalCtx.Count++ // 更新数据行数 return nil } 备注:avg 函数相对特殊一点,和 sum/count 相比,计算过程中要记录 Sum 和 Count 作为中间数据。\n备注:新版本为 HashAggExec 加入了并行计算功能,代码逻辑更加复杂,不过聚合计算逻辑没有太大变化。\n原文链接\n","title":"TiDB 源码学习:聚合查询"},{"location":"https://codenow.me/translation/howtoencryptyourentirelifelessthanonehour/","text":" 不用一小时,加密你的整个人生 作者:Quincy Larson 原链接:How to encrypt your entire life in less than an hour \n 前言略, 全文有精简\n好,让我们开始吧! 首先,解释几个术语。\nAttacker:所有未经本人同意却尝试获取本人数据的人或组织,甚至政府。 Private/secure:理想很丰满,现实是只要人类参与,没有任何系统可以保证百分百隐私或安全。\n只要你的手机、电脑 、账户受到充分保护,它们的内容会保持在一个加密的状态,那无论其他人多么强大,也无法可施。\nTip1: 对你的收件箱进行两步验证 你的收件箱是你生活的主钥。如果入侵者解开了它,那他不仅可以读取你的邮件,还可以通过重置你的密码去做更多的事,包括社交账户甚至银行账户。 你仅仅需要通过一个很简单的操作就可以大大改善你的个人隐私,就是去开启你收件箱的两步验证。 基本上两步验证就是你登录后的第二层保护。通常当你登录你的账户时,你会收到一条验证码。 两步验证基本上可以减少你收件箱被黑的可能性。 如果你用的是 gmail,你应该开启两步验证\n说真的,现在就去开启吧,我在这等你回来。\nTip2: 加密你的硬盘 Windows 和 Macos 都有内置全盘加密。你只需要开启它。\nTip3: 开启你手机的密码保护 指纹验证比其他方法都好,但是还不足够。 第五修订法允许你可以对你的密码进行保密,但是法院可以强迫你用指纹解锁手机(美国法律)。 而且,你的指纹是唯一的,不能在入侵者掌控你的指纹信息后进行更改。 一般入侵者会在手机完全锁住前有十次尝试机会。所以如果你的 4 个数字的密码属于以下常见的这些之一的话,请修改你的密码。\n1234 9999 1111 3333 0000 5555 1212 6666 7777 1122 1004 1313 2000 8888 4444 4321 2222 2001 6969 1010 如果为了方便还是坚持使用指纹验证的话,万一被逮捕,请马上关机。当他们重启手机,因为没有你的密码,他们也没办法解锁你的手机。(接前面美国法律的事)\nTip4: 不同的设备用不同的密码 密码本质上就是不安全的 Mark Zuckerberg 曾经用 ‘dadada’ 作为 他的 Linkedin 账户密码。 早些年,有黑客对外开放 117 百万个邮箱密码关联,他就是受害者之一。 黑客可以使用他的邮箱和密码去获取 Twitter 和 Pinterest 的登录。 所以,一个设备用一个密码。 当然,你没可能记得住那么多的密码,你可以使用工具 password manager\nTip5: 用 Signal 发私人信息 Signal 是一个获得 Electronic Frontier Foundation 组织高分认证的流行社交媒介,你可以像在其他平台一样收发信息、群聊、发照片或发视频。不同的是,所有都是加密保护的。 Signal 是免费的、开源的,有 Android 和 ios 版本。5 分钟都不需要,下载 Signal 后,就可以和亲友们安全对话了。 下载后,恭喜你,你就可以畅所欲言了,基本上没有人可以监视你的对话。 你甚至可以用 Signal 打私密电话。\nTip6: 你的浏览器匿名模式也不足够安全 就算你使用 Chrome 或 Firefox 的匿名模式,下面各方还是可以偷窥你的网络活动:网络服务提供方、掌管所在地网络的系统管理者、google 或其它浏览器制造商等等, 如果你想要更好的安全浏览,你最好用 Tor。\nTip7: 用 Tor 进行私密浏览 Tor 来自 ‘The Onion Router’, 表示使用了像洋葱那么多层的方式来隐藏网络活动。 Tor 是免费的、开源的,使用也很简单。\nTip8: 用安全的搜索引擎 如果你觉得 Tor 不是很方便,那么至少尝试一下用私密的搜索引擎, 像 DuckDuckGo ,这些搜索引擎不会追踪的的网络行为。 DuckDuckGo 没有 Google 那样通过多年和成千上万工程师帮助强大的搜索能力,但是只需要一小步,你就可以兼容私密和 Google 强大的资源,你只需要在搜索关键字前加 !google 即可。\n我建议你看看电脑安全专家 Bruce Schneier 的书 《Data and Goliath: The Hidden Battles to Collect Your Data and Control Your World 》。从这本书里我学到了很多,现在准备听多一次。\n","title":"不用一小时,加密你的整个人生"},{"location":"https://codenow.me/articles/aaron_huoju/","text":" 作者: Jade \u0026amp; 霍炬 链接:互联网之子 Aaron Swarts 想要看到的世界 \n Jade 和我偶尔会聊起一些宏大的话题,最近聊到了 Aaron 和互联网创建者们的一些历史。她觉得应该正经的来一次对话,记录下来分享给其他人。我们约了个时间,原计划聊 2 个小时,实际上聊了 5 个小时。最后形成了一篇交谈形式的文字,她称之为文字版的 Podcast。我很喜欢这种形式,我也更认同文字的价值,更好分享,更好检索,也更好修改或者摘录使用。以后我们应该还会继续这样的对话,这次聊天里面很多东西都可以继续讲下去。希望你也喜欢这个形式。\n网络和 BBS 在我们现在知道的互联网诞生之前就存在了,普通人有机会接触网络的历史,至今也有 30 多年了。虚拟世界的时间进度远远比现实世界快,在中国,也有“互联网是属狗的,一年当作七年用这个说法”。按照这个比例推测,换算到现实世界,互联网实际上走过了相当于 200 年左右的历史了。对它的研究已经可以产生一个“互联网考古学”之类的新学科了,然而没有多少人意识到这件事,也没多少人对这些历史和人物有兴趣。尽管我认为这些非常重要,其中有太多的教训和经验今天仍然可以学习。而且,这些历史也不应该被忘掉。\nUsenet 今天已经变成了下载者的乐园,但是它的废墟里埋葬了太多的历史和欢笑血泪。一些人已经消失,一些人还在积极工作,一些人已经走上了另外一条道路,还有一些人已经离开了我们。如果你从 80 年代就对整个网络世界有所了解,你会发现到今天一切都是相连的,从拨号 BBS 到区块链,有一条暗线始终存在。差不多也到了挖掘这些故事的时候了。\n01 自由的代价 Jade:首先问个题外话,你为什么会选择在寒冷的加拿大生活和开发产品?\n霍炬:几年前我觉得世界似乎变得越来越混乱,我和太太就想找一个“安全的地方”躲起来。于是我去读了所有可能去的国家的历史和政治制度,最后认为最安全的两个地方是新西兰和加拿大。但新西兰太偏远,科技和互联网不够发达,加拿大科技水平很高,创新能力也好,于是,就加拿大了。\nJade:没想到一个八卦问题引发了如此深刻的答案,吓了我一跳。之所以咱们决定有这次对谈,我记得是有一次我们聊起了 Aaron Swarts,你给我推荐了关于他的纪录片(互联网之子 The Internet\u0026rsquo;s Own Boy: The Story of Aaron Swartz (2014)),然后就一发不可收拾地聊起了一大堆宏大的主题。能不能从你的角度再介绍一下 Aaron 这个人,我们都知道他是一个互联网天才,14 岁参与制订 RSS 标准,26 岁在 MIT 事件的压力下自杀。为什么你觉得这个人很重要?他带来了什么?\n霍炬:Aaron 最常见的介绍是“reddit 联合创始人,RSS 参与者,Markdown 标准参与者”。但是这些不是我想说的重点,重点是他是一个承接上一代和下一代的人物。应该是承接互联网创建者们的理念,并且用来改造世界的人。\nAaron 深受 John Perry Barlow(电子前线基金会 EFF 的创始人)的影响。在 Aaron 中学时代,John 到他们学校演讲,Aaron 听了这个演讲之后,深受影响。后来 Aaron 的爸爸说那天他回家就像变了一个人一样。以及后来 Aaron 和 Tim Berners-Lee 在一起工作,等等。按照他的年龄,很难想象和这些互联网的创建者们一起工作和活动。但是他和他们相处很好,这些人也都喜欢他。\n他的死除了是一个巨大的悲剧之外,彻底改变了很多人对美国的看法,很多人因此对奥巴马政府,以及民主党由正面转向负面。一直到现在,2016 年民主党的溃败和川普的上台,和这件事仍然有一些关联,这种关联是通过 wikileaks 体现出来的。\nJade:是的,Aaron 除了是个天才,最迷人的还是他坚定地想要改变世界的气质。他曾经说,他的所有时间都需要花在“创造”上,而且一定对人类社会有价值。到他主导了 MIT 事件的时候,他已经成了一个 activist。Aaron 内心的主张是什么?为什么他会如此奋不顾身?\n霍炬:说 Aaron 的主张之前,我们得引入一个新词,叫做 Technolibertarianism 或者 Cyber-Libertarianism。这个流派的代表人物 Julian Assange。Libertarianism 就是自由意志主义,代表人物安兰德。它看上去是个左派词,实际上是极右翼。在中国,很多人误以为安兰德是白左,这是望文生义。实际上这是保守主义再往右移动的结果。\nCyber-Libertarianism 就是互联网自由意志主义,这是硅谷 90 年代之前就开始的运动。主要是追求版权自由,代码自由,加密算法自由之类的。总之就是尽量自由,少受管控的互联网。这种视角塑造了那个时代的互联网和科技行业,并且影响到了 Aaron。所以他的最基本主张,就是互联网自由和知识平等。\nJade:看来 Aaron 的主张是一代人的延续。在他死后,也有 Alexandra Elbakyan 和她创立的 Sci-Hub,可以说是在继续延续 Aaron 的理念。为什么那个时候会出现这样一群人?是否与互联网发展的阶段有关?那时的互联网世界是什么样子的?\n霍炬:这个故事要从互联网出现之前讲起,那时候还是拨号 BBS 的时代。80 年代吧。网络被当作通信的延伸,所以也受很多通信法规的制约。最早玩这些东西的人,本来是觉得找到了一块没有那么多监管的世界。大家玩的很开心,并且创造了这个词:cyberspace(赛博空间)。\n但是很快,“你不关心政治,政治就来关心你”。很多人开始撞上了执法部门。一些是因为盗版,一些是因为黑客行动,还有一些是因为有人在 BBS 上贴了违法的东西(比如贴了个盗版软件的下载),那个时候所谓的“避风港原则”还没出现,所以很多人因为软件开始被 FBI 调查。\n这种调查没能阻止他们,反而把他们联合起来了。一些早期在软件上赚到钱的人,从 BBS 上知道这些事,就开始提供援助,比如 Mitch Kapor,就开始花钱帮大家找律师打官司。最后这些行为成立的组织就是前面说的 EFF。\n之后大家就开始逐渐清晰了一些原则,比如如何看待虚拟世界,隐私,加密,专有软件,论坛的言论自由…这条路走下去,就成了这种意识形态,即:互联网上的监管越少越好。\nJade:所以当时的互联网原住民有过一段时间是真心想要破除所有可能的监管的,并且非常努力地在促成这件事。你自己怎么看这种意识形态,你内心认同吗?\n霍炬:对,到今天也是一样。wikileaks 至今仍然在运转,虽然现在我们知道它或多或少是受俄罗斯控制的。以及,加密货币的发明者们,最早也是为了让资产流转不受控制。尽管现在我们可以用区块链做去一些另外的“受控制但仍然有意义的事情”。\n我在很多年前确实是认同的。但到后来,大家都意识到了完全没有监管,商业垄断力量就会长大。这样会让事情变的更糟糕,还不如接受政府监管(这里说的是基于代议制民主的政府监管),所以现在我是支持欧盟 GDPR 的。这个在 90 年代看来是不可思议的,90 年代的我也不可能支持它,但是现在社会情况变了,我现在支持它。\n02 屠龙者与龙 Jade:看来“去中心化”也是与“去监管”密不可分的主张了,但它是从技术设计上体现的。比如 RSS,最开始是完全去中心化的订阅管理的,后来衰落了。看看我们现在用的社交媒体,会觉得 RSS 被替代是必然。但是当时互联网人可不是这么认为的,我记得 RSS 还经历过非常激烈的内部辩论。\n霍炬:最早的互联网全是去中心的,人们先确定了协议,大家各自实现。RSS 走的是真正的互联网延续的这条路,它和去中心化的问题一样,就是对”用户存在一定要求“。商业互联网是另外一条路,最好标准和产品都在我这家公司手里,才能避免竞争对手,同时获得最大利润。这样当然同时也有更高的效率,更好的用户体验。用户可以什么都不懂也能用。\n产品上说这样肯定会更好。但是人们到底在乎什么,这是个问题。早期互联网用户极度在意隐私保护和加密(因为被 FBI 骚扰的太多了),但是我们现在说的互联网人们不太在意这些,到今天变得更不在意。虽然 GDPR 的出现似乎预示着开始有一定数量的人“认为这个问题很重要”了。所以下面如何发展,主要还是看用户如何看待这个问题。\nJade:与商业互联网并行的,是开源软件。开源软件在过去 20 年可以说是进展显著的,也孕育了很多优秀的商业和非商业产品。开源软件的今天和最早的互联网原住民想象的未来一样吗?\n霍炬:开源软件算是并行发展的一条线索,它比互联网商用早的多。最早程序员就是共享代码的,之后才有二进制发行,才有软件公司。可以说,我们前面说的互联网发生的事情,在软件上也发生过一次。开始大家都是交换代码的,后来软件公司开始变大,占领更大的市场份额,使得大部分人不得不去用这些软件(当时的代表就是微软),然后开源运动开始进入新的高度,得到了更多支持。\nESR 发表《大教堂与市集》是 1997 年,微软当时几乎是完全垄断的。\nSlashdot 上,当年微软的新闻,旁边的图标都是这个。那时候邪恶帝国就是微软。\n如果从软件角度看,开源软件算是部分实现了当年的想法,大部分基础设施都开源了,操作系统,编译器,编辑器,常见程序库,都是开源的,在此之上可以创建任何你想要的东西,从个人爱好,到巨型企业,都可以。在开源运动开始的时代,这个几乎是不可能的,没可能离开微软使用计算机。\n今天离开任何软件公司,你都可以在计算机上实现一样的功能。微软也变成了开源项目最多,对开源社区贡献最大的企业之一。但是互联网巨型企业创造了新问题,就是:大部分人没办法脱离这些公司了,Facebook 也好微信也好,现在很多用户离开这些的话,互联网对他们就没意义了。\nJade:我觉得这不完全是一个技术问题。不管是开源软件,还是知识平权,都面临一个无法回避的问题,那就是开发者的激励。比如很多人觉得没有专利,没有知识产权,创作者就没有动力去生产。说到底,这是一个资本主义的基础理论。很想知道你对这个反面的观点怎么看?开源世界的商业模式是一个值得探索的问题?没有专利的世界会像今天这样好吗?\n霍炬:商业公司当然存在,即使是 Technolibertarianism 也承认商业公司存在。他们的标准表述是“去除一切监管,然后让大家赚自己的钱”。商业公司从始至终存在,但是开源要解决的是没有商业产品替代品的问题,即,没有开源软件或自由软件的替代品,只有商业软件,就叫不自由。\n所谓的自由包括很多意思,比如没钱的人能不能通过软件获得同样的帮助,希望修改某些功能的用户有没有这种可能性去自行修改。知识产权需要存在,但是知识产权如果阻挡了这些自由,那就出问题了。\n当然在中国谈这个的时间点还不太对,中国知识产权保护是太弱,但美国知识产权保护是过强。这两个都有问题。所以总体来说,我支持开源,也支持 Technolibertarianism,但是同样在微信上会因为别人洗我稿而搏斗几年。\nJade:那我们怎么在“让更多人拥有更好的体验”和“维护有替代品的自有”之间找到平衡呢?这个问题的解决者应该是政府吗?\n霍炬:导致 Aaron 自杀的案件,就是他认为论文的商业利润导致知识不自由,一个人如果没钱,又没上大学,即使他有学习的能力和意愿,也很难看到需要的论文。这在 Technolibertarianism 看来是不可接受,必须改变的。Sci-Hub 的出现同样继承了这个观点,即:支持公司赚钱,但是赚钱到成为阻碍另外一些人的自由的时候,那它就成为障碍了。\n这个平衡很难找。你可以看到前面我们聊了这么多,从 70 年代开始,到 90 年代,到互联网时代,到区块链时代…大家都是在寻找这个问题的答案。而且社会发展,资本主义的逐利性,都会影响这个平衡的变化。一不小心,小公司突然变成了巨型企业,之前大家拥护的屠龙者突然变成了龙。\n上面这个问题的解决者是不是应该是政府,人们的看法也一直是在变化和摇摆的。Technolibertarianism 和 Libertarianism 在这里也有体现。\nTechnolibertarianism 支持的是政府在数字世界放松管制,但是在现实世界并不反对管制。这也是为什么中国人一说起来加州公司,都认为是“白左”,他们支持各种现实世界的平权和平等,支持控枪,但是同时支持数字世界的完全自由,以及政府不得管制加密算法。\n在 Technolibertarianism 看来,枪是无所谓的东西,但是加密算法和隐私才是最重要的。他们对加密算法自由的追求就和美国右派挺第二修正案一样。围绕加密自由,EFF 打了好几个影响巨大的官司。不然今天区块链也不会出现了,因为用的算法几乎都会被美国出口法律管制。\nJade:在互联网出现之前,信息和知识的获取本来就是不平等的。我们刚才触及到了互联网公司专利垄断,数据垄断的问题,以及政府监管的问题。但是换个例子,为什么 Google 这样的公司让信息获取很大程度地平等了?它没有对信息收费,它可以有其他的方式。那我是不是可以理解为,新的生产力,新的商业模式,很可能可以打破我们今天一些思考中的“僵局”,去到达更好的平衡?\n霍炬:Google 是一个特殊的例子,它现在有了很多变化,又因为竞争和利润压力不得不改变很多逻辑。但是它仍然是一家特殊的公司。如果你看 Facebook,那么就不这么乐观了。\n互联网公司促进了一部分信息平等,这没错。但是同时它们带来了一堆新的问题,比如隐私问题,以及获取了过量的个人数据。加上 AI 之后,这些东西产生什么后果难以预测。\n从乐观的角度说,是的,新的商业模式可以提供一些更好的平衡。从悲观的角度说,新的商业模式部分的解决了以往的问题,同时制造了更多更大更难以解决的新问题。\n03 什么才是真正的问题 Jade:“解决了以往的问题,同时制造了更多更大更难以解决的新问题。”这可就是一个哲学问题了。比如从哲学或神学来看,人类社会的发展,尤其是技术发展看起来是不断进步,其实只是螺旋打转。We are not going anywhere. 对此不同的哲学流派还提出了不同的解决方案。比如老庄就建议人类回到最原始朴素的生活状态,拒绝发展。而佛学认为人应该去关注自己的内心,把这些所谓的“进步”都看成不好也不坏的变化。有些西方学者,为了证明人还是在进步的,还举出了大量的数据来证明。\n这个说远了。。其实我想说的是,对事物本质的探索没有尽头。我之前写过一篇文章,本来是探索区块链行业的经济模型,结果最后变成了探讨资本主义。对于我来说,开源xx是难以用经济学解释的谜题,也和资本主义格格不入。你怎么从经济学角度,或人性角度去看开源这件事?\n霍炬:Google 是一个特殊的例子,它现在有了很多变化,又因为竞争和利润压力不得不改变很多逻辑。但是它仍然是一家特殊的公司。如果你看 Facebook,那么就不这么乐观了。\n互联网公司促进了一部分信息平等,这没错。但是同时它们带来了一堆新的问题,比如隐私问题,以及获取了过量的个人数据。加上 AI 之后,这些东西产生什么后果难以预测。\n从乐观的角度说,是的,新的商业模式可以提供一些更好的平衡。从悲观的角度说,新的商业模式部分的解决了以往的问题,同时制造了更多更大更难以解决的新问题。\n开源这件事本身不奇怪,因为程序员希望别人参与改进代码。\n比如高德纳发布 TEX 是开源的,从经济学角度很难解释一套如此强大的软件,已经超过当时商业排版软件水平的软件,为什么会开源。\n但是对于高德纳来说,这是他写 TAOCP 的副产品。他是因为没有工具能帮助他排版自己的书,所以写了个软件。所以,他不靠这个软件生存,又希望更多人使用它,希望别人参与改进它,就开源了。\n大型软件的开源是另外一回事,在有互联网之前,很难想象有 Linux 这个规模的软件是通过开源完成的。分布在世界各地的人们,竟然可以协调一致开发出一个这样的操作系统,GNU 这种组织梦想多年拥有自己的操作系统,但是始终没有成功。\n开源本身是有商业模式的,早期可以提供咨询,后期可以包装商业产品赚钱。所以开源软件是一个商业和个人兴趣,以及对自由的崇尚混合的产物。它也不算不符合经济学规律,只是对程序员的回报链条不一样。\nJade:说是这样说,但我也认识不少苦逼的开源创始人和程序员,经济回报难以衡量。说到底,资本主义和市场经济那一套没法概括所有的人类行为和动机,比如我和你在这聊这些再传播出去,仅仅是因为我们认为有意义,也没有什么回报。去年一些有名的开源公司,虽然不是开源产品本身,被收购了。这算不算开源世界的失败?能不能解释下最近热议的 amazon 损害开源社区的话题?开源有没有可能是个已经过时的“模式”?\n霍炬:说的没错,开源,创作共用,知识共享,这几件事在源头上都是一个。就是一些有余力,或者以其他人认为的“工作”为有趣的人,创造出来的东西分享给别人。这里面不完全包含经济回报。比如我们讨论这些问题,对于很多人这就是工作。我们在这不为报酬的工作,这也不符合经济规律。但是咱们是觉得这些话题有趣,愿意讨论,并没在乎回报这事。\n开源产品被收购不一定算失败,甚至可能算是成功,这说明了开源产品达到了比较好的质量或者潜力。但是收购确实导致了几次大型危机,比如 Oracle 收购了 Sun,这简直是开源社区的历史最大危机,这个危机一直延续到今天仍然有影响。比如,ZFS 成了 Oracle 的独家产品,开源的版本只是旧版,后继开发变得很困难,因为开发人员一旦参与这个开发,就要避免去很多家公司工作。不然的话同时进行这两项工作,就可能导致 Oracle 对他所在的公司发起的知识产权诉讼。\nAmazon 损害开源社区是最新的一次危机。Amazon 从开源社区拿走软件,重新包装之后当作云服务卖掉了。之后大批用户是通过 AWS 使用这些软件,自己不会亲自去用了。Amazon 在这些软件之上的修改也未必开源。这使得 AWS 竞争力会高于开源社区提供的版本,成了恶性循环。\n这些会造成开源社区的萎缩或者部分项目受损,但是开源运动生命力是很顽强的。即使萎缩成了更小的社区,它也仍然会存在和发展。在这个问题上比互联网的问题小一些,因为开源软件的参与者是职业程序员,比起来需要包装成良好用户体验的普通用户,职业程序员的容忍度更好一些。\n04 我们正在 1992 年 Jade:这么看来,如今的互联网世界越来越“中庸”。而在过去 10 年里,最极客,最让人又开始尝试挑战权威的可能就是比特币和区块链了。很多人说现在的区块链特别像过去的互联网,哪儿像了?要是像,那到底是 80 年代,90 年代的,还是 2000 年的?\n霍炬:非常像。这个像可以从很多不同角度来看。\n比如,前面说了,开源社区当年视微软为邪恶帝国,一心希望提供一个替代操作系统和办公软件,彻底打败微软。但最终打败微软的不是他们提供的 Linux 桌面,而是基于开源软件的互联网。人们使用计算机的主要活动变成了通过浏览器完成,软件和操作系统都不重要了。\n区块链和互联网也一样。人们希望解决互联网公司的数据封闭和隐私问题,最终也需要通过一种完全不一样的模式来解决,区块链在目前看来最有可能成为这种模式。\n像哪一年这个问题,我仔细对比过,我个人觉得像互联网的 1992 年。\n1992 年的互联网还只是一个展示简单信息的地方,几乎没有任何交互。1995 年 Javascript 才发明出来,页面上才有了一点交互。那时候 www 还很弱,我们今天意义上这个互联网还没成型(实际上后来对于大多数用户来说,浏览器里的 www 就是互联网)用户基本上都是专业用户和爱好者。差不多就是这时候。\nJade:那么在 90 年代初,互联网有没有经历过现在区块链在“协议层”上的竞争和讨论呢?现在,区块链行业 90% 以上的资源和注意力都还是在公链上,还在 debate 不同的共识机制,隐私强度等,在不可能三角上来回取舍。你觉得最后是会各有各的用户和目的,还是从百花齐放到统一标准?\n霍炬:经历了,之前说到 RSS 协议上的斗争,其实那就是互联网发展一贯的状况,只是到 RSS 这个时代,商业力量太强了,也太快了。所以 SNS 瞬间就拿到了更多用户,RSS 就被抛弃了。\n除了在一个协议标准上的竞争,不同协议之间也在竞争。比如,现在难以想象,Tim Berners-Lee 发明 www 的时候,是有很多竞争者的,其中一个强力对手叫做 Gopher。在 90 年代,Gopher 一度发展的比 www 快的多,大量信息都是在 Gopher 服务器上,在 www 上只有非常简单的内容。而且 Gopher 速度快的多,信息检索能力也强得多。\n但是 Gopher 的发明者,明尼苏达大学,在那个时候把这个协议变成了收费软件,后来就比不上 www 的发展了。今天我们说“Tim Berners-Lee 没有为 www 的发明申请专利,完全免费提供了”,实际上是,如果 www 当时也收费,今天的互联网可能根本就不一样了。搞不好我们用的是增强功能之后发展出来的下一代 gopher。\n这也是发生了 95 年前后的事情,所以今天这些竞争也都正常,大家都在寻找更好的解决方案。最终可能会统一到几个标准上,但是希望人们接受教训,能统一到一个 www 这样自由,开放,扩展性好的标准上,而不是一个专有_垄断_高度受控于某个商业公司的标准。\nJade:现在区块链重新开始提去中心化,自由,隐私等”老概念”,在你看来有任何新颖之处吗?我问这个问题的原因是,我觉得新的技术通常解决的是新的问题,就好像 2000 年前的一批互联网公司,总是尝试把传统行业的一些问题搬到互联网上解决。我不是说这样不对,我只是觉得缺乏创意。\n霍炬:对我来说,没有新颖之处,因为 Cypherpunk 这帮人从 90 年代就在谈这些事情,一直谈到今天。如果从他们的视角看,今天的互联网是一个岔路,他们研究的路线才是正路。从 Cypherpunk 中冒出来一个比特币,就像从开源世界冒出来 Linux。\n就是说,Linux 刚出现的时候,弱小,粗糙,和商业化 Unix 差十万八千里。但是它代表了一种可以与之抗衡的力量登上舞台了。\n区块链这个行业现在混杂了各种人,跟那个时代的互联网也差不多。骗子,野心家,真正的高手,大家都混在一起了。普通观众看着热闹,但是分不清角色。\n你看 David Chaum 最近还经常冒出来对区块链世界发表一些看法和展望。但是大家似乎并不知道他是谁,也不知道他代表了什么…你说相信 XXX(这里我就不提具体名字了,你可以自己脑补)的人,怎么能理解 David Chaum 在说什么呢?\nJade:这是技术层面。虚拟货币也触碰到了很多经济政治层面的主张。可我觉得大家只是在“怀旧”,没有啥新意。比如去年 shut down 的 basis 项目,基本上就是对奥派经济学和哈耶克货币的非国家化的致敬和复制。在这些经济和政治主张里,有哪些是你认同的,哪些不认同?比如,我不太认同区块链是关于“生产关系”的革命,我觉得如果生产关系能被革命,一定还是生产力先被革命了。\n霍炬:经济学我是外行,这方面我很难说哪些是新的。不过说到社会这个层面,思想本身似乎很难说新/旧来概括。一些过去没意义的想法,在今天技术的帮助下,开始变得有意义和可行。\n比特币最早的政治目的,和生产关系/生产力关系不大。它的政治目的仍然就是 Cypherpunk 的追求自由和隐私的体现,即,不依赖于任何大机构,不依赖银行,创造一种货币系统。\n比特币和加密货币最早为了解决的问题没有今天大家认为的这么大,区块链火起来之后,人们赋予了它越来越多的含义和可能性,包括生产力和生产关系的描述。当然这也很正常,互联网刚出现的时候要解决的问题也没有这么多,要解决的也就是数据传递和协作问题。但是之后顺理成章的就出现了更多新问题,进而改变了很多行业,算是间接改善了生产力吧。\n05 我要星辰大海,也要 Facebook Jade:我特别好奇,Aaron Swarts 如果还没去世,他想看到,或者他想创造一个什么样的未来世界?我说的”未来“,不是五年十年,而是 20 年以上的维度。你的”超级想象“是什么?\n霍炬:他想创造的未来,就是他死之前努力的那些,平等的知识,平等的资源,更好的隐私,更好的工具…但是他太年轻了,到他 40 岁的时候,面对那样的世界他会做什么?这个就难以推测了。\n不过我能确信的是,如果他今天还活着,应该会忙着反川普,解决美国国内的各种问题。应该也会活跃在区块链领域。\n从我最早回答加拿大那个问题,你就应该猜到了,我的超级想象是反乌托邦的世界。就不仔细描述了…\nJade:好,观众们自己体会。刘慈欣说,我要的是星辰大海,你却给了我 Facebook。但我有时候想想,Facebook 也许是比宇宙飞船更有意义的发明。我以前读阿西莫夫《永恒的终结》,里面有一种人可以穿越时间修改历史事件,结果一次次地改成人类并没有飞出地球征服宇宙,因为在宇宙中,发生着和地球上类似的贪婪的争夺,而人类在很多个平行时空里都被终结了。\n我不是说探索地球之外不重要,但有时候我看着一些普通人是怎样被互联网这样的工具改变了人生,我就觉得这对他们来说更重要。你觉得互联网到了人们所说的“成熟期”吗?继续下去的发展方向会是什么?\n霍炬:首先,这句话不是刘慈欣说的。10 年前我就在 TED 看到过有人演讲用过这句话(https://www.ted.com/talks/jason_pontin_can_technology_solve_our_big_problems)。\nFacebook 和宇宙飞船都很重要。但是现在互联网公司占有的利润太大了,使得“高科技”几乎被等同于互联网了。这个势头是很不好的。\n互联网应该算成熟期了,因为这些年已经很少有纯粹的互联网创新了,我是说类似 Google 和 Facebook 这样的,属于互联网本身的创新。再继续发展下去,前面说了,它创造的问题已经比它能解决的问题更大的。\n当然另外一方面,互联网在传递信息这个方面还有很多问题没解决,比如,其他行业,工业,医疗,很多领域,目前的互联网渗透率实际上是不够的,还有很多能解决的问题。\nJade:谢谢,我希望我们的谈话是启发性的,我和你主张什么并不重要,重要的是不管一个人生活在哪里,什么时代,都不应该失去提问的勇气。最后能不能推荐你最喜欢的一个互联网产品,一本书,和一部电影?\n霍炬:这个问题可真难回答…\nGithub 吧,它不应该只限于程序员,应该所有人都去试着用它。\n我最近看的最像书的书是…法语教材…\n电影,我可以推荐一部动画片“breadwinners”。\nJade:法语教材…好的。Breadwinners 我也喜欢,在飞机上看的。那你再推荐一个加拿大值得去的地方吧!\n霍炬:Cape Breton,是个岛。这地方不仅有超级好的风景,还是 Alexander Graham Bell 的住所,就是电话的发明人,有个纪念馆。\n很高兴你也喜欢 breadwinners,这个动画片的原作是一个非常神奇的人,加拿大人,没有任何中东血统。完全是因为社会活动,决定到中东去探访他们的生活,走访了很多难民营,最后写出了这些故事。她也是一位社会活动家,只是不算那么纯粹的互联网活动家。\nJade:最后,能不能送一句话给大家的 2019?\n霍炬:希望大家 2019 年能尝试一些以往没试过的东西,我指那些平时觉得“太麻烦了干嘛要去学”的东西。\nJade:真棒,刚才音乐播放器恰好随机播到 Jason Mraz 的 Life is Wonderful,我觉得在这首歌里结束今天的聊天非常美。歌词是这样的:\nIt takes a crane to build a crane It takes two floors to make a story It takes an egg to make a hen It takes a hen to make an egg There is no end to what I’m saying It takes a thought to make a word And it takes some words to make an action It takes some work to make it work It takes some good to make it hurt It takes some bad for satisfaction Life is wonderful. Life goes full circle. ","title":"互联网之子 Aaron Swarts 想要看到的世界"},{"location":"https://codenow.me/tips/pbcopy/","text":" 用 vim 在 terminal 中复制信息 pbcopy \u0026lt; filename.extension # eg. pbcopy \u0026lt; a.txt ","title":"VIM Cheetsheet: pbcopy"},{"location":"https://codenow.me/algorithm/to_lowercase/","text":" 题号:709 难度:easy 链接:https://leetcode.com/problems/to-lower-case/ 描述:使用 ASCII 把字母统一为小写 class Solution: \u0026#34;\u0026#34;\u0026#34;use ASCII to return the same str in lowercase \u0026#34;\u0026#34;\u0026#34; def to_lower_case(self, str: str) -\u0026gt; str: new_str = \u0026#39;\u0026#39; for c in str: if 65 \u0026lt;= ord(c) \u0026lt;= 90: # Uppercase is between 65 and 90 in ASCII table c = chr(ord(c)+32) new_str += c return new_str # one line solution #return \u0026#39;\u0026#39;.join(chr(ord(c) + 32) if 65 \u0026lt;= ord(c) \u0026lt;= 90 else c for c in str) if __name__ == \u0026#39;__main__\u0026#39;: solution = Solution() s = \u0026#39;Hero\u0026#39; solution.to_lower_case(s)","title":"Leetcode_709: To Lower Case"},{"location":"https://codenow.me/tips/github_binds_dominname/","text":"第一步: 1. 新建一个GitHub仓库,取名为your-github_name.github.io 2. 新建一个文件,取名为CNAME,填写内容为域名。不需要添加http或https。\n第二步: 1. 在本地cmd中,ping第一步中新建的仓库名称。会返回一个ip地址,记录下该地址。\n第三步: 1. 打开购买的云服务器,因为我购买的阿里云,所有在这上面进行操作演示。 2. 在控制台中打开域名管理。\n 找到解析。添加如下信息:\nA:对应cmd中ping的地址。\nCNAME:对应新建的GitHub仓库名。 完成上述步骤后,配置完成。\n参考网址\n","title":"GitHub 绑定自己的域名"},{"location":"https://codenow.me/algorithm/leetcode_165_compare-version-numbers/","text":" 题号:165\n难度:medium\n链接:https://leetcode-cn.com/problems/compare-version-numbers/\n class Solution: def compareVersion(self, version1: str, version2: str) -\u0026gt; int: vers1 = version1.split(\u0026#34;.\u0026#34;) vers2 = version2.split(\u0026#34;.\u0026#34;) diff = len(vers1) - len(vers2) if diff \u0026gt; 0: vers2 += [\u0026#39;0\u0026#39;] * abs(diff) elif diff \u0026lt; 0: vers1 += [\u0026#39;0\u0026#39;] * abs(diff) else: pass for i, v1 in enumerate(vers1): v1, v2 = int(v1), int(vers2[i]) if v1 \u0026gt; v2: return 1 elif v1 \u0026lt; v2: return -1 else: continue return 0","title":"Leetcode 165: Compare Version Numbers"},{"location":"https://codenow.me/translation/http-propagation-context/","text":" Go 1.7 引入了一个内置的 context 类型,在系统中可以使用 Context 来传递元数据,例如不同函数或者不同线程甚至进程的传递 Request ID。\nGo 将 Context 包引入标准库以统一 context 的使用。在此之前每个框架或者库都有自己的 context 。它们之间还无法兼容,导致了碎片化,最终在各处 context 的传播上就有不少的麻烦。\n虽然在同一个处理过程中有一个通用的 context 传播机制是非常有用的,但是 Go 的 Context 包并没有提供该功能。就像上面描述的,context 会在网络中被不同的处理过程传递。例如在多服务架构中,一个请求往往会在多个地方被处理 (多个微服务,消息队列,数据库等),直到最后响应给用户。能够在多个处理过程中传递 context 显得尤为重要。\n如果你要在 HTTP 中传播 context ,需要你对 context 进行序列化处理。类似的,在接收端也要解析,同时把值放入当前的 context 中。假设我们希望在 context 中传递 request ID。\npackage request import \u0026#34;context\u0026#34; // WithID 把 request ID 放入当前的 context 中 func WithID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, contextIDKey, id) } // IDFromContext 返回从 context 中获取的 request ID // 如果 context 中没有定义就返回空值 func IDFromContext(ctx context.Context) string { v := ctx.Value(contextIDKey) if v == nil { return \u0026#34;\u0026#34; } return v.(string) } type contextIDType struct{} var contextIDKey = \u0026amp;contextIDType{} // ... WithID 允许我们把 request ID 设置到 context 中,IDFromContext 可以从 context 中读取 request ID。一旦我们有在多个处理过程,就需要手动把到 context 设置到传输中,同时在接受端解析然后写入 context。\n在 HTTP 中我们可以从 header 中获取 request ID。大多数的 context 都可以通过 header 来传播。一些传输层可能不支持 headers 或者 headers 不是传输标准 (例如有大小限制或者缺少加密措施)。在这种情况下,由具体实现来决定如何传递上下文。\nHTTP 传播 目前没有直接的方法可以在 HTTP reuqest 中的值放入 context 中。由于无法遍历出 context 的值,因此也无法一次性转换整个上下文。\nconst requestIDHeader = \u0026#34;request-id\u0026#34; // Transport 把 request context 序列化到 request headers type Transport struct { // Base 是构建请求的真实 round tripper // 如果没有被设置,默认使用 http.DefaultTransport \tBase http.RoundTripper } // RoundTrip 转换 request context 到 headers 中 // 同时构建请求 func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { r = cloneReq(r) // per RoundTrip interface enforces rid := request.IDFromContext(r.Context()) if rid != \u0026#34;\u0026#34; { r.Header.Add(requestIDHeader, rid) } base := t.Base if base == nil { base = http.DefaultTransport } return base.RoundTrip(r) } 在上面的 Transport 中,如果 request ID 存在就会被当做 \u0026ldquo;request-id\u0026rdquo; header 进行传递。\n类似的方法可以解析请求,把 \u0026ldquo;request-id\u0026rdquo; 放入请求的上下文中。\n// Handler 从 request headers 反序列化到 request context 中 type Handler struct { // Base 是完成反序列化调用的真实方法 Base http.Handler } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { rid := r.Header.Get(requestIDHeader) if rid != \u0026#34;\u0026#34; { r = r.WithContext(request.WithID(r.Context(), rid)) } h.Base.ServeHTTP(w, r) } 为了继续传播 context ,请确保在你的方法中把当前的 context 传递到下一个 request 。传入的 context 将会随着 request 传播到 https://endpoint。\nhttp.Handle(\u0026#34;/\u0026#34;, \u0026amp;Handler{ Base: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { req, _ := http.NewRequest(\u0026#34;GET\u0026#34;, \u0026#34;https://endpoint\u0026#34;, nil) // 传播当前的 context req = req.WithContext(r.Context()) // Make the request. }), })","title":"Go Context 在 HTTP 传播"},{"location":"https://codenow.me/articles/2pc/","text":" 两阶段提交协议\n在分布式系统中每个节点都可以知道自己的操作是成功还是失败,但是无法知道其他节点的状态。为了保证一个事务的 ACID 特性,一个节点发生失败就要在所有节点上执行 rollback 操作。需要引入一个 协调者 来维护各个 参与者 的状态,以保证最终一致。\n2pc 并不是万能的,需要满足一定的条件才可以使用:\n 一个节点是协调者,其他节点作为参与者,相互之前可以通信 每个节点要有 redo log 机制,而且存在持久化存储中 节点不会永久损坏,一定时间会重启恢复 Tow-phase Commit Protocol 首先引入几个概念:\n 协调者:维护所有节点 参与者:执行具体操作的节点 prepare phase:准备阶段,写入日志,资源加锁 commit phase:执行阶段,根据协调者指令执行,资源解锁 上图中 ① 和 ② 表示 prepare phase,③ 和 ④ 表示 commit phase。\n在 prepare phase 阶段,协调者发出信息让参与者准备。参与者接受到信息以后一般会做两件事情:\n 根据需要执行的操作生成 redo 日志,用于后续 commit 或者 rollback 给所需的资源上锁,防止其他程序获取 参与者完成这两个操作会把结果通知协调者。\n当协调者接受到参与者在 prepare phase 阶段的响应(无论 Yes 还是 No),就会进入 commit phase 阶段。在该阶段,如果接受到 prepare phase 的响应所有都是 Yes 时候,会发出 Commit 指令;只要收到一个 No,发出的就是 Rollback 指令。\n参与者接受到协调者发出的 Commit 或者 Rollback 指令后会做两件事情:\n 执行 Commit 或者 Rollback 清理资源,解除锁的占用 的当这两部都完成以后,会回复 ACK 个协调者。\n故障时期的处理 前面描述都的都正常情况,如果协调者或者参与者一个或者全部发生宕机时候,2pc 的处理方式在某些时候是无法保证数据一致性。\n协调者和参与者会在 ① ② ③ ④ 的前后阶段,同时发生故障,也有可能其中一方发生故障。\n① 之前发生故障,即操作还没开始:\n 协调者宕机:重新选举出一个协调者继续操作,这个宕机的节点重启后询问新的协调者继续操作 参与者宕机:重启后像协调者询问操作,然后继续即可 协调者和部分参与者宕机:同协调者宕机 ① 之后 ② 之前发生故障,即接收到是否可以 commit 询问,响应之前:\n 未执行操作前(执行过程中中断): 协调者宕机:询问已经发出,重新选择一个新的协调者,根据参与者返结果继续操作 参与者宕机:等待恢复后询问协调者节点状态,然后继续操作 协调者和部分参与者宕机:重新选择一个新的协调者,根据参与者返结果继续操作 操作执行完成之后: 协调者宕机:重新选举一个新协调者,询问参与者当前步骤 部分参与者宕机:重启后 rollback,询问协调者后续步骤 协调者和部分参与者宕机:选举出新的协调者,重启后 rollback ,询问新协调者后续步骤 ② 之后 ③ 之前发生故障:\n 未接到参与者响应(执行过程中中断): 协调者宕机:选出先的协调者,询问各个节点状态,宕机节点重启后 rollback,询问新协调者后续操作 部分参与者宕机:重启后 rollback 然后询问协调者状态,继续操作 协调者和部分参与者宕机:同协调者宕机 操作执行完成之后: 协调者宕机:选出新的协调者,询问各个节点状态,宕机节点重启后 rollback,询问新协调者后续操作 部分参与者宕机:重启后 rollback 然后询问协调者状态,继续操作 协调者和部分参与者宕机:同协调者宕机 ③ 之后 ④ 之前故障:\n 未接到协调者发出的 commit 或者 rollback: 协调者宕机:指令已经发出,选举一个新的协调者询问正常参与者执行情况。宕机节点重启后 rollback,然后询问新的协调者指令情况,然后继续操作 部分参与者宕机:重启 rollback 后询问协调者,继续操作即可 协调者和部分参与者宕机:同协调者宕机 接受到 commit 或者 rollback(或者执行过程中中断): 协调者宕机:重启 rollback 后询问新协调者,继续操作即可 部分参与者宕机:重启后先 rollback,然后询问协调者操作 协调者和部分参与者宕机:这个时候存在一个极端情况,发出 commit 和接收到的参与者同时宕机,剩下的参与者 timeout 触发 rollback。重启后的宕机机器 commit 已经完成,这时候就和剩下的节点发生了数据不一致的场景,已经完成 commit 的节点也无法 rollback 从故障时期的处理可以总结出,如果是单是协调者发生故障,那么重新选举出一个新的,然后询问参与者的状态即可。如果是单是参与者发生故障,重启后询问协调者操作也可以恢复状态。协调者和参与者同时宕机,同时又在 commit phase 执行完成之后,就会发生数据不一致的场景,这个是 2pc 无法解决的。\n2pc 存在的缺点 2pc 组要存在 4 个缺点:\n 同步阻塞:在 commit phase 阶段完成之前,这部分资源无法被其他程序获取 单点问题:commit phase 阶段协调者发生故障,只能全体 rollback 数据不一致:极端情况会导致部分 commit 部分 rollback 缺乏容错机制:发生无法恢复的错误只能依赖 timeout 执行 rollback ","title":"两阶段提交协议"},{"location":"https://codenow.me/tips/dep-visualizing-dependencies/","text":"Linux:\n$ sudo apt-get install graphviz $ dep status -dot | dot -T png | display macOS:\n$ brew install graphviz $ dep status -dot | dot -T png | open -f -a /Applications/Preview.app Windows:\n\u0026gt; choco install graphviz.portable \u0026gt; dep status -dot | dot -T png -o status.png; start status.png","title":"dep package 依赖关系图"},{"location":"https://codenow.me/articles/python3-crontab/","text":"这周需要在容器中跑一个定时脚本\n现成的方式有很多: 1. 直接使用 ubuntu:14.04 的镜像,内置 crontab 和 python3.4 2. 想用 python3.6 的话,可以用 python:3.6 的镜像装一个 crontab 也成 3. dockerhub 上别人应该也有这种需求,捞一个就成\n不过我还是想自己拼一个,要求: 1. 需要包含 crontab 和 python3.6 2. 需要能支持使用 pip 安装其他扩展包 3. 镜像要尽量小\n思路以及需要注意的地方大概是: 1. 装上各种必要的东西 2. 设置时区 3. 配置好 crontabfile 4. 运行时启动 crond,并用 tail -f 来保证容器不退出\n目前只是做了个能用的,用 python3.6-alpine 做源,往上怼了点够自己使用的东西,先实现了需求\n下一步是直接用 alpine 或者 buildpack-deps 来构建镜像,以此精简,留着 TODO 吧\n我写了个 demo 放到了 github 上: https://github.com/WokoLiu/python3-cron ,也同步到了 dockerhub 上 docker pull woko/python3-cron\n文件结构是这样的:\n. ├── Dockerfile ├── crontabfile ├── scripts.py └── requirements.txt Dockerfile 负责构建镜像 crontabfile 是写有 crontab 内容的文件 scripts.py 是要运行的脚本文件 requirements.txt 是需要 pip 安装的扩展 关键内容是 Dockerfile,鉴于还没完善,不好意思在这里详细讲,感兴趣的话可以去看看,后面会再更新\n刚学 docker 没多久,一些地方还不清楚,有问题还请不吝赐教,多谢\n","title":"Python3 Crontab"},{"location":"https://codenow.me/algorithm/leetcode_12_integertoroman/","text":" 题号:12 难度:medium 链接:https://leetcode.com/problems/integer-to-roman 描述:阿拉伯数字转罗马数字(1-3999)\n class Solution: \u0026quot;\u0026quot;\u0026quot;这道题的点在于,1/10/100/1000是可以重复的,其他数字是不可以重复的,如果用循环处理的话,要注意这一点\u0026quot;\u0026quot;\u0026quot; def intToRoman1(self, num: int) -\u0026gt; str: \u0026quot;\u0026quot;\u0026quot;照例先撸一个无脑的出来 这个还算快,不过占用空间比较大 \u0026quot;\u0026quot;\u0026quot; ten = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'] roman = '' m, num = divmod(num, 1000) roman += 'M' * m if num \u0026gt;= 900: roman += 'CM' num -= 900 elif num \u0026gt;= 500: roman += 'D' num -= 500 elif num \u0026gt;= 400: roman += 'CD' num -= 400 c, num = divmod(num, 100) roman += 'C' * c if num \u0026gt;= 90: roman += 'XC' num -= 90 elif num \u0026gt;= 50: roman += 'L' num -= 50 elif num \u0026gt;= 40: roman += 'XL' num -= 40 x, num = divmod(num, 10) roman += 'X' * x + ten[num] return roman def intToRoman(self, num: int) -\u0026gt; str: \u0026quot;\u0026quot;\u0026quot;看了一下 discuss,有个更简洁的,同样够快且占用空间大\u0026quot;\u0026quot;\u0026quot; M = ('', 'M', 'MM', 'MMM') C = ('', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM') X = ('', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC') I = ('', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX') return M[num // 1000] + C[(num % 1000) // 100] + X[(num % 100) // 10] + I[num % 10] if __name__ == '__main__': print(Solution().intToRoman(1994)) ","title":"Leetcode_12_IntegerToRoman"},{"location":"https://codenow.me/tips/git_lg/","text":"git log 可以用来查看提交历史,但格式并不舒服,我们可以通过配置 git config 来解决这个问题:\n运行\ngit config --global alias.lg 'log --pretty=format:\u0026quot;%h - %an, %ad : %s\u0026quot; --date=format:\u0026quot;%Y-%m-%d %H:%M:%S\u0026quot;'\n之后,便可以使用 git lg 来查看精简版本的,更舒服的提交历史了,如本项目现在看的话会变成\n80504ec - jarvis, 2019-03-31 13:58:55 : add origin article link df1bcfd - jarvis, 2019-03-31 13:51:45 : Second week atts of jarvys eede6bf - chuntao.han, 2019-03-30 17:33:10 : hanchuntao translation checkin 1 769b56b - chuntao.han, 2019-03-30 13:07:45 : hanchuntao tips checkin 1 5507805 - chuntao.han, 2019-03-30 12:41:09 : add algorithm for mistake ef5c5a8 - CTubby, 2019-03-30 12:04:58 : tubby checkin d698c58 - chuntao.han, 2019-03-30 00:49:57 : hanchuntao algorithm 1 checkin 05b1e47 - chuntao.han, 2019-03-29 00:12:22 : hanchuntao article checkin 1 4792f15 - Woko, 2019-03-28 23:41:32 : Rename [Go]Exercise of A Tour of Go.md to Exercise_of_A_Tour_of_Go.md 0da03b5 - Woko, 2019-03-28 23:37:26 : Rename [mysql]Is varchar a number?.md to is_varchar_a_number.md 6b112eb - Frost Ming, 2019-03-28 14:11:46 : Quit org c1a8704 - zhengxiaowai, 2019-03-24 23:56:24 : week 1 atts by zhengxiaowai 看起来是不是舒服多了\n","title":"Git_lg"},{"location":"https://codenow.me/translation/tidb-proposal_-support-skyline-pruning/","text":" 原文链接:Proposal: Support Skyline Pruning,翻译如下:\n摘要 这篇建议引入了一些启发式规则和一个针对消除访问路径 (access path) 的通用框架。通过它的帮助,优化器可以避免选择一些错误的访问路径。\n背景 目前,访问路径的选择很大程度上取决于统计信息。我们可能会因为过期的统计信息而选择错误的索引。然而,很多错误的选择是可以通过简单的规则来消除的,比如:当主键或者唯一性索引能够完全匹配的时候,我们可以直接选择它而不管统计信息。\n建议 (Proposal) 目前在选择访问路径时最大的因素是需要扫描的数据行数,是否满足物理属性 (physical property) ,以及是否需要两次扫描。在这三个因素当中,只有扫描行数依赖统计信息。那么在没有统计信息的情况下我们能够怎样比较扫描行数呢?让我们来看一下下面这个例子:\ncreate table t(a int, b int, c int, index idx1(b, a), index idx2(a)); select * from t where a = 1 and b = 1; 从查询和表结构上,我们能够看到使用索引 idx1 扫描能够覆盖 idx2,通过索引 idx1 扫描的数据行数不会比使用 idx2 多,所以在这个场景中,idx1 要比 idx2 好。\n我们如何综合这三个因素来消除访问路径呢?假如有两条访问路径 x 和 y,如果 x 在这几个方面都不比 y 差并且某个因素上 x 还好于 y,那么在使用统计数据之前,我们可以消除 y,因为 x 在任何情况下都一定比 y 更好。这就是所谓的 skyline pruning。\n基本原理 (Rationale) Skyling pruing 已经在其他数据库中实现,包括 MySQL 和 OceanBase。要是没有它,我们可能会在一些简单场景下选择错误的访问路径。\n兼容性 Skyling pruning 并不影响兼容性。\n实现 在为数据寻找最好的查询方式时,由于我们要决定使用哪一个满足物理条件的访问路径,我们需要使用 skyling pruning。大部分情况下不会有太多索引,一个简单的嵌套循环算法就足够了。任何两个访问路径的比较方式已经在 Proposal 章节里介绍过了。\n引用 The Skyline Operator ","title":"TiDB Proposal: Support Skyline Pruning"},{"location":"https://codenow.me/tips/idea-%E5%89%8D%E8%BF%9B%E5%90%8E%E9%80%80%E5%BF%AB%E6%8D%B7%E9%94%AE/","text":"前进:Command + ]\n后退:Command + [\n在追踪比较复杂的代码时,比较有用,可以快速前进后退,不至于迷失在代码中。\n","title":"Idea 前进后退快捷键"},{"location":"https://codenow.me/articles/tidb-subquery-optimization/","text":" 根据 TiDB 中的子查询优化技术 这篇文章的介绍,TiDB 在处理关联子查询时引入了 Apply 算子。然后使用关系代数将 Apply 算子等价转换成其他算子,从而达到去关联化的目的。理论上,所有的关联子查询都可以去关联化,具体的理论知识可以看这篇博客:SQL 子查询的优化。\n本文从代码角度,梳理一下常见关联子查询的优化。处理过程主要有两个阶段:\n 重写阶段:在将语法树转换成逻辑查询计划时,将子查询重写成带有 Apply 算子的查询计划,这部分主要是由 expressionRewriter 负责 去关联化:在优化逻辑查询计划时,尝试将 Apply 算子替换成其他算子,从而去关联化,这部分主要有 decorrelateSolver 负责 expressionRewriter 简介 expressionRewriter 负责将子查询重语法树写成带有 Apply 算子的查询计划。为了实现这一功能,需要能够遍历语法树,expressionRewriter 实现了 Visitor 接口,能够遍历语法树中的各个节点,在遍历过程当中完成重写工作,它的核心的方法主要是 Enter 和 Leave。\nVisitor 接口一般会被这样使用:\nfunc (n *CompareSubqueryExpr) Accept(v Visitor) (Node, bool) { newNode, skipChildren := v.Enter(n) if skipChildren { return v.Leave(newNode) } n = newNode.(*CompareSubqueryExpr) node, ok := n.L.Accept(v) //... n.L = node.(ExprNode) node, ok = n.R.Accept(v) //... n.R = node.(ExprNode) return v.Leave(n) } 每个语法树节点通过调用 Accept、Enter 和 Leave 方法来实现对整个语法树节点的遍历。\nQ1 select t1.a from t t1 where 1000 in (select t2.b from t t2 where t2.a = t1.a) Q1 是最简单常见的一种子查询,通过对 Q1 的分析,我们能够理解子查询优化的框架。\n重写阶段 在 Q1 中,子查询出现在 where 当中,在构建逻辑查询计划时,会被 expressionRewriter 重写。\n构建 select 查询计划过程中,主要会执行以下几个方法,分别用来构建 DataSource、Selection、Aggregation 等逻辑查询节点:\n buildSelect buildResultSetNode resolveGbyExprs resolveHavingAndOrderBy buildSelection buildAggregation buildProjection buildSelect 中被调用的这些函数都使用了 expressionRewriter ,在 Q1 中,子查询重写发生在 buildSelection 当中:\nfunc (b *planBuilder) buildSelection(p LogicalPlan, where ast.ExprNode, AggMapper map[*ast.AggregateFuncExpr]int) LogicalPlan { //... conditions := splitWhere(where) //... for _, cond := range conditions { expr, np, err := b.rewrite(cond, p, AggMapper, false) //... } //... } Q1 的 where 部分比较简单,只包含了一个子查询 和 in 组成条件,对应的是 PatternInExpr 类型,先简单了解一下 PatternInExpr:\ntype PatternInExpr struct { exprNode // Expr is the value expression to be compared. Expr ExprNode // List is the list expression in compare list. List []ExprNode // Not is true, the expression is \u0026#34;not in\u0026#34;. Not bool // Sel is the subquery, may be rewritten to other type of expression. Sel ExprNode } 在关联子查询中主要用到了 Sel 和 Expr 属性,Sel 对应的是子查询 select t2.b from t t2 where t2.a = t1.a ,Expr 对应的是常量 1000。\n根据 expressionRewriter 的 Enter 方法,处理逻辑主要在 handleInSubquery 当中。\n// Enter implements Visitor interface. func (er *expressionRewriter) Enter(inNode ast.Node) (ast.Node, bool) { switch v := inNode.(type) { //... case *ast.PatternInExpr: if v.Sel != nil { return er.handleInSubquery(v) } //... //... } return inNode, false } expressionRewriter 还有 handleCompareSubquery、handleExistSubquery、handleScalarSubquery 等方法分别用来处理其他几种子查询。\n分析 handleInSubquery 代码,简化后的代码如下:\nfunc (er *expressionRewriter) handleInSubquery(v *ast.PatternInExpr) (ast.Node, bool) { //... lexpr := er.ctxStack[len(er.ctxStack)-1] subq, ok := v.Sel.(*ast.SubqueryExpr) //... np := er.buildSubquery(subq) // 构建子查询 //... var rexpr expression.Expression //... checkCondition, err := er.constructBinaryOpFunction(lexpr, rexpr, ast.EQ) // 构建查询条件 //... er.p = er.b.buildSemiApply(er.p, np, expression.SplitCNFItems(checkCondition), asScalar, v.Not) // 创建 Apply 算子 //... return v, true } expressionRewriter 先构建子查询的查询计划,然后根据 In 条件参数创建 Apply 的 conditions,最后调用 buildSemiApply 方法构建 Apply 查询计划。\n小结 为 Q1 构建查询计划过程中,与子查询重写有关的函数调用过程大致如下,expressionRewriter 简写成 er。\n buildSelect() \u0026lt;= 创建 Select 语句的查询计划 buildSelection() \u0026lt;= 创建 Selection 节点 rewrite() \u0026lt;= 重写 Q1 的子查询 exprNode.Accept(er) \u0026lt;= expressionRewriter 从这里开始遍历语法树 er.Enter() er.handleInSubquery() er.buildSubquery() er.constructBinaryOpFunction() er.b.buildSemiApply() \u0026lt;= 创建 Apply 算子 最终得到的查询计划大致如下:\n注意,图中的 Apply 是 SemiApply。\nLogicalApply 类型 TiDB 使用 LogicalApply 来表示 Apply 算子:\ntype LogicalApply struct { LogicalJoin corCols []*expression.CorrelatedColumn } 从数据结构上也能看出,LogicalApply 和 LogicalJoin 很像,Apply 类型其实也是通过 JoinType 类型设置的(比如,SemiJoin、LeftOuterJoin、InnerJoin 等)。\n去关联化 去关联化是在逻辑查询优化过程中完成的,代码逻辑主要看 decorrelateSolver 。\n优化的思路是:尽可能把 Apply 往下推、把 Apply 下面的算子向上提,通过这一方式将关联变量变成普通变量,从而去关联化。虽然这一过程可能看起来会让查询计划的效率降低,但是去关联化以后再通过谓词下推等优化规则可以重新对整个查询计划进行优化。\nQ1 涉及到的代码如下:\n// optimize implements logicalOptRule interface. func (s *decorrelateSolver) optimize(p LogicalPlan) (LogicalPlan, error) { if apply, ok := p.(*LogicalApply); ok { outerPlan := apply.children[0] innerPlan := apply.children[1] apply.extractCorColumnsBySchema() if len(apply.corCols) == 0 { // \u0026lt;= 如果关联变量都被消除,可以将 Apply 转换成 Join join := \u0026amp;apply.LogicalJoin join.self = join p = join } else if sel, ok := innerPlan.(*LogicalSelection); ok { // \u0026lt;= 在这个分支中消除 Selection 节点 newConds := make([]expression.Expression, 0, len(sel.Conditions)) for _, cond := range sel.Conditions { newConds = append(newConds, cond.Decorrelate(outerPlan.Schema())) } apply.attachOnConds(newConds) innerPlan = sel.children[0] apply.SetChildren(outerPlan, innerPlan) return s.optimize(p) // Selection 被消除以后,重新对 Apply 优化,在 Q1 中会触发 Apply 被转换成 Join } else if m, ok := innerPlan.(*LogicalMaxOneRow); ok { //... } else if proj, ok := innerPlan.(*LogicalProjection); ok { //... } else if agg, ok := innerPlan.(*LogicalAggregation); ok { //... } } //... return p, nil } 参考上面提供的查询计划示意图,代码执行过程中:\n 寻找 Apply 节点,找到 Apply 节点以后,尝试对 innerPlan 子节点中的算子往上提,在 Q1 中: 将 Selection(t1.a=t2.a) 节点中的条件提到 Apply 上,消除 Selection 节点 完成这一步以后,Apply 的关联变量就被消除了,这样就可以把 Apply 转换成一个普通的 Join 了,Q1 的去关联化过程也基本完成。 整个过程如下图所示:\nQ2 select t1.a from t t1 where 1000 \u0026lt; (select min(t2.b) from t t2 where t2.a = t1.a) Q2 相对 Q1,子查询稍微复杂了一点,多了聚合函数。\n重写阶段 重写阶段和 Q1 很类似,区别主要在于查询条件不再是 PatternInExpr,变成了 BinaryOperationExpr,重写逻辑主要发生在 handleScalarSubquery 当中:\nfunc (er *expressionRewriter) handleScalarSubquery(v *ast.SubqueryExpr) (ast.Node, bool) { np, err := er.buildSubquery(v) //... np = er.b.buildMaxOneRow(np) if len(np.extractCorrelatedCols()) \u0026gt; 0 { er.p = er.b.buildApplyWithJoinType(er.p, np, LeftOuterJoin) //... return v, true } //... } 另外一点不同是,Apply 类型变成了 LeftOuterJoin (如何选择 Apply 的类型,可以参考开头的几篇文章)。重写完以后得到的查询计划大致如下:\n去关联化 和 Q1 相比,由于有 Aggregation 节点,Q2 的去关联化逻辑更复杂一些。对于 Q2 这类带有 aggr 的查询,decorrelateSolver 尽可能将 Aggregation 向上拉:\nfunc (s *decorrelateSolver) optimize(p LogicalPlan) (LogicalPlan, error) { if apply, ok := p.(*LogicalApply); ok { outerPlan := apply.children[0] innerPlan := apply.children[1] apply.extractCorColumnsBySchema() if len(apply.corCols) == 0 { //... } else if sel, ok := innerPlan.(*LogicalSelection); ok { //... } else if m, ok := innerPlan.(*LogicalMaxOneRow); ok { //... } else if proj, ok := innerPlan.(*LogicalProjection); ok { //... } else if agg, ok := innerPlan.(*LogicalAggregation); ok { if apply.canPullUpAgg() \u0026amp;\u0026amp; agg.canPullUp() { // 尝试将 Aggregation 上提 //... } // 如果 Aggregation 不能上提,尝试将 Aggregation 下面的 Selection 上提,去掉关联变量 if sel, ok := agg.children[0].(*LogicalSelection); ok \u0026amp;\u0026amp; apply.JoinType == LeftOuterJoin { var ( eqCondWithCorCol []*expression.ScalarFunction remainedExpr []expression.Expression ) // 解析 Selection 中的关联条件 for _, cond := range sel.Conditions { if expr := apply.deCorColFromEqExpr(cond); expr != nil { eqCondWithCorCol = append(eqCondWithCorCol, expr.(*expression.ScalarFunction)) } else { remainedExpr = append(remainedExpr, cond) } } if len(eqCondWithCorCol) \u0026gt; 0 { //... if len(apply.corCols) == 0 { join := \u0026amp;apply.LogicalJoin join.EqualConditions = append(join.EqualConditions, eqCondWithCorCol...) for _, eqCond := range eqCondWithCorCol { // 对于被上提的筛选条件,如果 Aggregation 没有包含对应列的分组的话 // 需要在 Aggregation 中添加上分组 clonedCol := eqCond.GetArgs()[1].Clone() // If the join key is not in the aggregation\u0026#39;s schema, add first row function. if agg.schema.ColumnIndex(eqCond.GetArgs()[1].(*expression.Column)) == -1 { newFunc := aggregation.NewAggFuncDesc(apply.ctx, ast.AggFuncFirstRow, []expression.Expression{clonedCol}, false) agg.AggFuncs = append(agg.AggFuncs, newFunc) agg.schema.Append(clonedCol.(*expression.Column)) } // If group by cols don\u0026#39;t contain the join key, add it into this. if agg.getGbyColIndex(eqCond.GetArgs()[1].(*expression.Column)) == -1 { agg.GroupByItems = append(agg.GroupByItems, clonedCol) } } //... agg.collectGroupByColumns() if len(sel.Conditions) == 0 { // \u0026lt;= Selection 的条件都被删除,那么节点可以被消除了 agg.SetChildren(sel.children[0]) } //... return s.optimize(p) // Selection 被消除以后重新对 Apply 节点进行优化,触发 Apply 转换成 Join 的逻辑 } //... } } } } //... return p, nil } 可惜的是,Q2 中的 Aggregation 是无法 pull up 的,貌似 TiDB 并没有完全按照开头文章中提到的方式去做子查询优化。虽然 Aggregation 无法上提,但是 decorrelator 会尝试将子节点 Selection 中的条件合并到 Apply 中,这个过程和 Q1 很像。如果 Selection 中的条件都被合并到 Apply 当中,那么 Selection 节点可以被消除了。\n在 Q2 中 Selection 节点删除后,子查询不再包含关联变量,Apply 可以被转换为 Join。去关联以后得到的查询计划大致如下:\n原博文链接\n","title":"TiDB 源码学习:常见子查询优化"},{"location":"https://codenow.me/algorithm/leetcode-210-course-schedule-ii/","text":"原题链接:210. Course Schedule II 。一道基础拓扑排序题,代码如下:\nclass Solution: def findOrder(self, numCourses: \u0026#39;int\u0026#39;, prerequisites: \u0026#39;List[List[int]]\u0026#39;) -\u0026gt; \u0026#39;List[int]\u0026#39;: degrees = [0] * numCourses graph = [[] for _ in range(numCourses)] for edge in prerequisites: source, dep = edge degrees[source] += 1 graph[dep].append(source) stack = [] for i in range(numCourses): deps = degrees[i] if deps == 0: stack.append(i) ret = [] while len(stack) \u0026gt; 0: node = stack.pop() ret.append(node) deps = graph[node] for dep in deps: degrees[dep] -= 1 if degrees[dep] == 0: stack.append(dep) if len(ret) != numCourses: return [] else: return ret","title":"Leetcode: 210 Course Schedule II"},{"location":"https://codenow.me/translation/intro_to_python_lambda_functions/","text":"原文地址: https://www.pythonforthelab.com/blog/intro-to-python-lambda-functions/\n不久前,Python在其语法中引入了使用lambda而不是def来定义函数的可能性。这些函数称为匿名函数同,在其它语言(如javascript)中非常常见。然后,在Python中,它们看起来有点晦涩,经常被忽略或误用。在本文中,我们将介绍labda函数,并讨论在何处以及如何使用它。\n要定义一个函数,可以使用以下语法:\ndef average(x, y): return (x + y) / 2 然后,如果要计算两个数字的平均值,只需执行以下操作\navg = average(2, 5) 在这种情况下,平均值将为3.5。我们也可以这样定义平均值:\naverage = lambda x, y: (x + y) / 2 如果你测试此函数,您将看到输出完全一样。必须指出,def和lambda之间语法非常不同。首先,我们定义不带括号的参数x, y。然后,我们定义要应用的操作。注意,当使用lambda函数时,返回是隐式的。\n然而,还有更根本的区别。lambda函数只能在一行上表示,并且没有docstring。如果对上面的每个定义尝试help(average),您将看到输出非常不同,此外,无法记录average的第二版的实际操作。\n从功能上讲,定义平均值的两种方法都给出了相同的结果。到目前为止,他们之间的差异非常微妙。lambda(或匿名)函数的主要优点是它们不需要名称。此外,像我们上面所做的那样指定一个名字被认为是不好的做法,我们稍后将讨论。现在让我们看看您希望在什么上下文中使用lambda函数而不是普通函数。\n大多数教程都侧重于lambda函数来对列表进行排序。在讨论其他主题之前,我们也可以这样做。假设您有以下列表:\nvar=[1,5,-2,3,-7,4] 假设您希望对值进行排序,可以执行以下操作:\nsorted_var = sorted(var) #[-7,-2,1,3,4,5] 这很容易。但是,如果您希望根据到给定数字的距离对值进行排序,会发生什么情况呢?如果要计算到1的距离,需要对每个数字应用一个函数,例如abs(x-1),并根据输出对值进行排序。幸运的是,排序后,您可以使用关键字参数key=执行此操作。我们可以做到:\ndef distance(x): return abs(x - 1) sorted_var = sorted(var, key=distance) # [1, 3, -2, 4, 5, -7] 另一种选择是使用lambda函数:\nsorted_var = sorted(var, key=lambda x: abs(x-1)) 这两个例子将产生完全相同的输出。在使用def或lambda定义函数之间没有功能差异。我可以说第二个例子比第一个稍微短一些。此外,它使代码更具可读性,因为您可以立即看到对每个元素(abs(x-1))所做的操作,而不是通过代码挖掘来查看定义的距离。\n另一种可能是与map结合使用。map是将函数应用于列表中的每个元素的一种方法。例如,基于上面的示例,我们可以执行以下操作:\nlist(map(distance, var)) # [0, 4, 3, 2, 8, 3] 或者,使用lambda表达式\nlist(map(lambda x: abs(x-1), var)) # [0, 4, 3, 2, 8, 3] 它给出了完全相同的输出,同样,人们可以争论哪一个更容易阅读。上面的示例是您在其他教程中可能看到的。如果通过stackoverflow,可能会看到。其中一种可能是结合Pandas使用lambda函数。\npandas和lambda函数\n示例数据受此示例的启发,可以在此处找到。创建包含以下内容的文件示例example_data.csv:\nanimal,uniq_id,water_need elephant,1001,500 elephant,1002,600 elephant,1003,550 tiger,1004,300 tiger,1005,320 tiger,1006,330 tiger,1007,290 tiger,1008,310 zebra,1009,200 zebra,1010,220 zebra,1011,240 zebra,1012,230 zebra,1013,220 zebra,1014,100 zebra,1015,80 lion,1016,420 lion,1017,600 lion,1018,500 lion,1019,390 kangaroo,1020,410 kangaroo,1021,430 kangaroo,1022,410 要将数据读取为数据帧,我们只需执行以下操作:\nimport pandas as pd df = pd.read_csv(\u0026#39;example_data.csv\u0026#39;, delimiter = \u0026#39;,\u0026#39;) 假设您希望将数据框中每个动物名称的第一个字母大写,您可以执行以下操作:\ndf[\u0026#39;animal\u0026#39;]=df[\u0026#39;animal\u0026#39;].apply((lambda x:x.capitalize()) print(df.head()) 你会看到结果。当然,lambda函数可能变得更加复杂。您可以将它们应用于整个系列,而不是单个值,您可以将它们与其他库(如numpy或scipy)组合,并对数据执行复杂的转换。\nlambda函数最大的优点之一是,如果您使用的是Jupyter notebools,那么您可以立即看到这些变化。你不需要打开另一个文件,运行一个不同的,单元格等。如果你去Pandas的文档,你会看到,lambdas经常被使用。\nQt Slots\n使用lambdas的另一个常见示例是与qt库结合使用。我们过去写过一篇关于qt的介绍性文章。如果您不熟悉构建用户界面的工作方式,可以随意浏览它。一个非常简单的例子,只显示一个按钮,它看起来像这样:\nfrom PyQt5.QtWidgets import QApplication, QPushButton app = QApplication([]) button = QPushButton(\u0026#39;Press Me\u0026#39;) button.show() app.exit(app.exec()) 如果要在按下按钮时触发某个操作,则必须将该操作定义为一个函数。如果我们想在按下按钮时将某些内容打印到屏幕上,我们只需在app.exit之前添加以下行:\nbutton.clicked.connect(lambda x: print(\u0026#39;Pressed!\u0026#39;)) 如果您再次运行程序,每次按下按钮,您都会看到已按下!出现在屏幕上。同样,使用lambda函数作为信号的插槽可以加快编码速度,使程序更容易阅读。但是,lambda函数也需要谨慎考虑。\nlambda函数的使用位置\nlambda函数只能有一行。这迫使开发人员只能在没有复杂语法的情况下使用它们。在上面的示例中,您可以看到lambda函数非常简单。如果它需要打开一个套接字,交换一些信息,处理接收到的数据等,那么它可能不可能在一条线上完成。\n可以使用lambda函数的自然情况是作为其他需要可调用参数的函数的参数。例如,应用pandas数据帧需要一个函数作为参数。连接qt中的信号还需要一个函数。如果我们要应用或执行的函数很简单,并且我们不打算重复使用它,那么将其编写为匿名函数可能是一种非常方便的方法。\n不使用lambda函数的位置\nlambda函数是匿名的,因此,如果您要为其分配名称,例如在执行以下操作时:\naverage = lambda x,y:(x+y)/2 这意味着你做错了什么。如果需要为函数指定一个名称,以便在程序的不同位置使用它,请使用标准的def语法。在这个博客中有一个关于Python中lambda函数滥用的冗长讨论。我经常看到的,尤其是刚学过lambdas的人,是这样的:\nsorted_var = sorted(var, key=lambda x: abs(x)) 如果这是第一次看到lambda函数,那么这个无辜的示例可能很难包装起来。但是,您所拥有的是将一个函数(abs)包装在另一个函数中。它应该是这样的:\ndef func(x): return abs(x) 与仅仅做abs(x)相比有什么优势?实际上,没有优势,这意味着我们也可以这样\nsorted_var = sorted(var, key=abs) 如果您注意我们前面开发的示例,我们使用abs(x-1)来避免这种冗余。\n结论\nlambda(或匿名)函数是一种在Python程序中逐渐流行的工具。这就是为什么你能理解它的含义是非常重要的。您必须记住,lambda语法不允许您这样做,没有它们是不可能做到的。更重要的是它的方便性、语法经济性和可读性。\n在其他编程语言(如javascript)中,匿名函数的使用频率非常高,并且具有比Python更丰富的语法。我不相信Python也会这样做,但无论如何,它们是一种工具,不仅可以帮助您使用当前的程序,而且还可以帮助您了解如果您修补其他语言的话会发生什么。\n","title":"Intro to Python Lambda Functions"},{"location":"https://codenow.me/tips/python_code_static_analysis_tool_summary/","text":" 1.Pylint Pylint是Python代码的一个静态检查工具,它能够检测一系列的代码错误,代码坏味道和格式错误。\nPylint使用的编码格式类似于PEP-8。\n它的最新版本还提供代码复杂度的相关统计数据,并能打印相应报告。\n不过在检查之前,Pylint需要先执行代码。\n具体可以参考http://pylint.org\n 2. Pyflakes Pyflakes相对于Pylint而言出现的时间较晚,不同于Pylint的是,它不需要在检查之前执行代码来获取代码中的错误。\nPyflakes不检查代码的格式错误,只检查逻辑错误。\n具体可以参考http://launchpad.net/pyflakes\n 3. McCabe McCabe是一个脚本,根据McCabe指标检查代码复杂性并打印报告。\n具体可以参考https://pypi.org/project/mccabe/\n 4. Pycodestyle Pycodestyle是一个按照PEP-8的部分内容检查Python代码的一个工具\n这个工具之前叫PEP-8。\n具体可以参考https://github.com/pycqa/pycodestyle\n 5. Flake8 Flake8封装了Pyflakes、McCabe和Pycodestyle工具,它可以执行这三个工具提供的检查\n具体可以参考https://github.com/pycqa/flake8\n 6. Pychecker PyChecker是Python代码的静态分析工具,它能够帮助查找Python代码的bug,而且能够对代码的复杂度和格式等提出警告。\nPyChecker会导入所检查文件中包含的模块,检查导入是否正确,同时检查文件中的函数、类和方法等。\n具体可以参考https://pypi.org/project/PyChecker/\n 7. Black Black 号称是不妥协的 Python 代码格式化工具。之所以成为“不妥协”是因为它检测到不符合规范的代码风格直接就帮你全部格式化好,根本不需要你确定,直接替你做好决定。而作为回报,Black 提供了快速的速度。\nBlack 通过产生最小的差异来更快地进行代码审查。\nBlack 的使用非常简单,安装成功后,和其他系统命令一样使用,只需在 black 命令后面指定需要格式化的文件或者目录即可。\n具体可以参考https://atom.io/packages/python-black\n ","title":"Python_code_static_analysis_tool_summary"},{"location":"https://codenow.me/articles/spark-broadcast_accumulator/","text":" 累加器 累加器提供将工作节点的值聚合到驱动器程序中的功能,且实现语法简单。\n示例图:\n#python中累加空行 file = sc.textFile(inputfile) blankLines = sc.accumulator(0) # 创建Accumulator(Int) def extractCallSigns(line): global blankLines if line == \u0026#34;\u0026#34;: blankLines += 1 return line.split(\u0026#39; \u0026#39;) callSigns = file.flatMap(extractCallSigns) callSigns.saveAsTextFile(outputPath) print(\u0026#39;blank Lines : %d\u0026#39; %blankLines.value) 实际使用中可以创建多个累加器进行计数\nvalidSignCount = sc.Accumulator(0) invalidSignCount = sc.Accumulator(0) 广播变量 简介 正常情况中,spark的task会在执行任务时,将变量进行拷贝。当每个task都从主节点拷贝时,程序的通信和内存负担很重。 使用广播变量后,主节点会将变量拷贝至工作节点,任务从工作节点获得变量,而不用再次拷贝,此时变量被拷贝的次数取决于工作节点的个数。\n#在Python中使用广播变量 signPrefixes = sc.broadcast(loadCallSignTable()) def processSignCount(sign_count, signPrefixes): country = lookupCountry(sign_count[0], signPrefixes.value) count = sign_count[1] return (country, count) countryContactCounts = (contactCounts.map(processSignCount).reduceByKey((lambda x, y:x+y))) countryContactCounts.saveAsTextFile(ooutputPath) 基于分区进行操作 基于分区对数据进行操作可以让我们避免为每个数据元素进行重复的配置工作。 Spark提供基于分区的map和foreach。\nPython基于分区操作 def func(file): pass def fetchCallSigns(input): return input.mapPartitions(lambda x: func(file=x)) #使用mapPartitons执行对分区的操作 do_func = fetchCallSigns(inputfile) 常见分区操作函数\n 函数名 调用所提供的 返回的 对于RDD[T]的函数签名 mapPartitions() 该分区中元素的迭代器 返回的元素的迭代器 f:(Iter ator[T]——\u0026gt;Iterator[U]) mapPartitionsWithIndex() 分区序号,以及每个分区中的元素的迭代器 返回的元素的迭代器 f:(Int, Iterator[T]——\u0026gt;Iterator[U]) forceachPartitions() 元素迭代器 无 f:(Iterator[T]——\u0026gt;Unit) 数值RDD的操作 简介 Spark对包含数据的RDD提供了一些描述性的操作。 Spark的数据操作是通过流式算法实现的,允许以每次一个元素的方式构建模型。这些统计数据会在调用stats()时通过一次遍历计算出来,并以StatsCounter对象返回。\n数值操作 StatsCounter中可用的汇总统计数据\n 方法 含义 count() RDD中的元素的个数 mean() 元素的平均值 sum() 总和 max() 最大值 min() 最小值 variance() 元素的方差 sampleVariance() 从采样中计算出的方差 stdev() 标准差 sampleStdev() 采用的标准差 Python中使用数据操作\ndistanceNumerics = distances.map(lambda s: float(s)) stats = distanceNumerics.stats() stdev = stats.stdev() #计算标准差 mean = stats.mean() resonableDistances = distanceNumerics.filter(lambda x: math.fabs(x - mean)\u0026lt;3 * stdev) print(resonableDistances.collet())","title":"Spark累加器和广播变量"},{"location":"https://codenow.me/translation/scikit-learn/","text":" Scikit-learn: Machine Learning in Python 最近在学习机器学习算法和深度学习的部分内容。于是将Scikit-Learn的相关介绍论文看了看,翻译了一部分。原文地址\n摘要 Scikit-learn是一个Python模块,集成了各种最先进的机器学习算法,适用于中等规模和无监督的问题。该软件包侧重于使用通用高级语言将机器学习引入非专业人员。重点在于易于使用,性能,文档以及API的一致性。它具有最小的依赖性,并在简化的BSD许可下分发,鼓励在学术和商业环境中使用它。二进制文件和文档可以从http://scikit-learn.sourceforge.net 下载。\n介绍 Python编程语言正在成为最流行的科学计算语言之一。由于其高水平的交互性和成熟的科学库生态系统,Python在算法开发和探索数据分析领域成为极有吸引力的选择。然而,作为一种通用语言,它不仅越来越多的应用于学术领域,也应用于工业。Scikit-learn利用这种环境提供许多出名的机器学习算法的最先进的实现方式,同时保持易于使用的界面以及和Python语言紧密集成。这满足了软件和网络行业的非专业人员以及计算机科学以外领域(如生物学或物理学)对统计数据分析的日益增长的需求。\nScikit-learn不同于其他Python的机器学习库的原因在于:\n 它根据BSD许可证分发。 与DMP和pybrain不同,它结合了编译代码来提升效率。 它仅仅依赖于Numpy和Scipy来促进易于分发。不像pymvpa那样拥有例如R和shogun这样的可选依赖项。 与使用数据流框架的pybrain不同,它侧重于命令式编程。虽然该软件包主要是用Python编写的,但它包含了C ++库LibSVM和LibLinear,它们提供了SVMS的参考实现和具有兼容许可的广义线性模型。二进制包可在包括Windows和任何POSIX平台在内的丰富平台上使用。此外,由于其自由许可,它已被广泛分发为主要的免费软件发行版,如Ubuntu,Debian,Mandriva,NetBSD和商业广告诸如“Enthought Python Distributions”之类的发行版。 项目愿景 代码质量。该项目的目标不是提供尽可能多的功能,而是提供可靠的实施能力。通过单元测试来保证代码质量。在发布的0.8版本,测试覆盖率为81%,与此同时,使用静态分析工具例如pyflakes和PEP8.最后,我们严格遵守Python编程指南和Numpy样式文档中使用的函数与参数命名,努力保持一致性。\nBSD许可。大多数Python生态系统都是用非copyleft许可证进行许可。虽然这种政策有利于商业项目采用这些工具,但它确实施加了一些限制:我们无法使用某些现有的科学代码,例如GSL。\n裸骨设计和API。 为了降低进入门槛,我们避免使用框架代码并将不同对象的数量保持在最低限度,依赖于数据容器的numpy数组。\n社区驱动的发展。 我们的开发基于git,GitHub和公共邮件列表等协作工具。 欢迎并鼓励外部捐助。\n开发者文档。Scikit-learn提供了约300页的用户指南,包括叙述文档,类参考,教程,安装说明,以及60多个示例,其中一些包含实际应用程序。 我们尽量减少机器学习术语的使用,同时主要训练精度与所使用的算法有关。\n基础技术 Numpy:数据和模型参数的基础数据结构用户。 输入数据表示为numpy数组,因此可以与其他科学Python库无缝集成。 Numpy的基于视图的内存模型限制了副本,即使与编译代码绑定也是如此。它还提供基本的算术运算。\nScipy:线性代数的有效算法,稀疏矩阵表示,特殊函数和基本统计函数。 Scipy具有许多基于Fortran的标准数字包的绑定,例如LAPACK。 这对于易于安装和可移植性非常重要,因为围绕Fortran代码提供库在各种平台上都具有挑战性。\nCython:一种在Python中组合C的语言。 Cython使用类似Python的语法和高级操作轻松实现编译语言的性能。 它还用于绑定已编译的库,从而消除了Python / C扩展的样板代码。\n总结 Scikit-learn使用一致的,面向任务的界面,公开了各种机器学习算法,包括监督和非监督,从而可以轻松地比较给定应用程序的方法。 由于它依赖于科学的Python生态系统,因此可以轻松地将其集成到传统统计数据分析范围之外的应用程序中。 重要的是,以高级语言实现的算法可以用作特定于用例的方法的构建块,例如,在医学成像中。 未来的工作包括在线学习,扩展到大型数据集。\n","title":"Scikit-Learn"},{"location":"https://codenow.me/algorithm/decision_tree/","text":" 决策树 学习并构建决策树。\n决策树的一个重要任务是为了数据中心所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则。在这些机器根据数据创建规则时,就是机器学习的过程。专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。\n决策树示例:\n决策树函数组成部分 优缺点 说明 优点 计算复杂度不高、输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据 缺点 可能会产生过度匹配问题 适用数据类型 数值型和标称型 寻找最佳划分特征值\n构造决策树时,需要考虑的第一个问题:当前数据集中哪个特征在划分数据分类时起到决定作用。\n为了找到这个特征,需要评估每一个特征,完成评测后,原始数据就会被划分为几个数据子集。然后遍历每个数据子集,若是都为同类,则该数据集结束分类,否则在该数据集中重新执行评估,二次分类。依次执行,直到数据被划分完毕或特征使用完毕时停止。\n创建分支的伪代码函数createBranch如下图所示:\n检测数据集中的每个子项是否属于同一分类: if so return 类标签: else: 寻找划分数据集的最好特征 划分数据集 创建分支节点 for每个划分的子集 调用函数createBranch并增加返回结果到分支节点中 return 分支节点 信息增益\n划分数据集最大的原则是:将无序的数据变得更加有序。本章选取信息论度量信息。\n在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最好的特征就是最好的选择。\n1).计算给定数据集的香农熵 计算熵的公式: $$ H = -\\sum{i=1}^{n}P(x{i})log{2}^{P(x{i})} $$\nfrom math import log def calcShannonEnt(dataSet): numEntries = len(dataSet) # 获取数据集中实例总数 labelCounts = {} # 创建数据字典,键值为数据集最后一列的值,即标签 for featVec in dataSet: currentLabel = featVec[-1] # 获取标签 if currentLabel not in labelCounts.keys(): # 如果标签不在字典中,则将其添加进去 labelCounts[currentLabel] =0 labelCounts[currentLabel] += 1 # 如果标签存在,则对应的数值加1 shannonEnt = 0.0 for LabelKey in labelCounts: prob = float(labelCounts[LabelKey]) / numEntries # 计算数值进入该分类的概率 shannonEnt -= prob * log(prob, 2) # 计算熵 return shannonEnt 示例数据:\n 不浮出水面 是否有脚蹼 属于鱼类 1 是 是 是 2 是 是 是 3 是 否 否 4 否 是 否 5 否 是 否 对应数据集:\ndataSet = [ [1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no'] ] 划分数据集 划分数据集,度量划分数据集的熵,以便判断当前是否正确划分了数据集。 通过对每个特征划分数据集的结果度量熵,判断哪个特征划分数据集是最好的划分方式。\ndef splitDateSet(dataSet, axis, value): # dataSet:待划分的数据集;axis:划分数据的特征索引;value:对应划分的特征值 retDateSet = [] for featVec in dataSet: if featVec[axis] == value: reduceFeatVec = featVec[:axis] reduceFeatVec.extend(featVec[axis+1:]) retDataSet.append(reduceFeatVec) return retDateSet 选择最好的数据集划分方式 通过该函数选取特征,划分数据集,计算出最好的划分数据集的特征。\ndef chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) -1 bestEntropy = calcShannonEnt(dataSet) # 计算原始香农熵 bestInfoGain = 0.0 # 信息增益 bestFeature = -1 # 原始特征值索引 for i in range(numFeatures): featList = [example[i] for example in dataSet] #创建第i个特征值组成的列表 uniqueVals = set(featList ) #创建特征值集合,去除重复元素 newEntropy = 0.0 for value in uniqueVals: # 依次读取特征值,计算香农熵 subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet)/float(len(dataSet)) newEntropy += prob * calcShannonEnt(subDataSet) infoGain = baseEntropy - newEntropy #计算信息增益 if infoGain \u0026gt; bestInfoGain: # 如果信息增益变大,则代表该特征值更好 bestInfoGain = infoGain bestFeature = i return bestFeature # 返回最佳特征值的索引 递归构造决策树 递归构造决策树原理:根据数据集选择最好的特征划分数据集,由于特征可能多于两个,故分支节点可能有多个。第一次划分后,子数据集中,可能还需要进行划分,故需要在子数据集中,递归调用决策树进行分类。\n 构建叶子节点分类函数 在本章构建决策树时,选择最佳特征值后,会删除特征。假设所有的特征使用完毕后,在某些叶子结点中,并不是都是一类,此时需要使用多数表决来分类。 下述函数将对叶子结点中所有的数据进行分类统计,最后选出数量最多的类别并返回其标签作为叶子结点分类标签。\ndef majorituCnt(classList): classCount = {} for vote in classList: if vote not in classCount.keys(): classCount[vote] = 0 classCount[vote] += 1 sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) return sortedClassCountp[0][0] 构建决策树\ndef createTree(data, labels): classList = [example[i] for example in dataSet] if classList.count(classList[0]) == len(classList): # 当类别完全相同时停止继续划分 return classList[0] if len(dataSet[0]) == 1: # 遍历完所有特征时返回出现次数最多 return majorityCnt(classList) bestFeat = chooseBestFeatureToSplit(dataSet) # 选取最佳划分特征 bestFeatLabel = labels[bestFeat] myTree = {bestFeatLabel:{}} # 构建节点 del(labels[bestFeat]) featValues = [example[bestFeat] for example in dataSet] uniqueVals = set(featValues) for value in uniqueVals: subLabels = labels[:] # 将类标签复制,防止使用过程中类标签被改变。 myTree[bestFFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels) # 递归调用函数 return myTree 使用Matplotlib绘制决策树 import matplotlib.pyplot as plt from pylab import * mpl.rcParams[\u0026#39;font.sans-serif\u0026#39;] = [\u0026#39;SimHei\u0026#39;] decisionNode = dict(boxstyle=\u0026#39;sawtooth\u0026#39;, fc=\u0026#39;0.8\u0026#39;) leafNode = dict(boxstyle=\u0026#39;round4\u0026#39;, fc=\u0026#39;0.8\u0026#39;) arrow_args = dict(arrowstyle=\u0026#39;\u0026lt;-\u0026#39;) def getNumLeafs(MyTree): # 获取叶子节点数 NumLeafs = 0 firstStr = list(MyTree.keys())[0] secondDict = MyTree[firstStr] for TreeKey in secondDict.keys(): if isinstance(secondDict[TreeKey], dict): NumLeafs += getNumLeafs(secondDict[TreeKey]) else: NumLeafs += 1 return NumLeafs def getTreeDepth(MyTree): # 获取树深度 maxDepth = 0 thisDepth = 0 firstStr = list(MyTree.keys())[0] secondDict = MyTree[firstStr] for TreeKey in secondDict.keys(): if isinstance(secondDict[TreeKey], dict): thisDepth = 1 + getTreeDepth(secondDict[TreeKey]) else: thisDepth += 1 if thisDepth \u0026gt; maxDepth: maxDepth = thisDepth return maxDepth def plotMidText(cntrPt, parentPt, txtString): xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0] yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1] createPlot.axl.text(xMid, yMid, txtString) def plotTree(MyTree, parentPt, nodeTxt): numLeafs = getNumLeafs(MyTree) depth = getTreeDepth(MyTree) firstStr = list(MyTree.keys())[0] cntrPt = (plotTree.x0ff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.y0ff) plotMidText(cntrPt, parentPt, nodeTxt) plotNode(firstStr, cntrPt, parentPt, decisionNode) secondDict = MyTree[firstStr] plotTree.y0ff = plotTree.y0ff - 1.0/plotTree.totalD for TreeKey in secondDict.keys(): if isinstance(secondDict[TreeKey], dict): plotTree(secondDict[TreeKey], cntrPt, str(TreeKey)) else: plotTree.x0ff = plotTree.x0ff + 1.0/plotTree.totalW plotNode(secondDict[TreeKey], (plotTree.x0ff, plotTree.y0ff), cntrPt, leafNode) plotMidText((plotTree.x0ff, plotTree.y0ff), cntrPt, str(TreeKey)) plotTree.y0ff = plotTree.y0ff + 1.0/plotTree.totalD def plotNode(nodeTxt, centerPt, parentPt, nodeType): createPlot.axl.annotate(nodeTxt, xy=parentPt, xycoords=\u0026#39;axes fraction\u0026#39;, xytext=centerPt, textcoords=\u0026#39;axes fraction\u0026#39;, va=\u0026#39;center\u0026#39;, ha=\u0026#39;center\u0026#39;, bbox=nodeType, arrowprops=arrow_args) def createPlot(inTree): fig = plt.figure(1, facecolor=\u0026#39;white\u0026#39;) fig.clf() anprops = dict(xticks=[], yticks=[]) createPlot.axl = plt.subplot(111, frameon=False, **anprops) plotTree.totalW = float(getNumLeafs(inTree)) plotTree.totalD = float(getTreeDepth(inTree)) plotTree.x0ff = -0.5/plotTree.totalW plotTree.y0ff = 1.0 plotTree(inTree, (0.5, 1.0), \u0026#39;\u0026#39;) plt.show()","title":"DecisionTree"},{"location":"https://codenow.me/tips/differen_rsa/","text":"在同一个电脑上为不同的GitHub账号创建rsa并实现关联。 操作命令行。\n#为第一个账号创建rsa文件 ssh-keygen -t rsa -C \u0026#34;your email\u0026#34; -f ~/.ssh/id_rsa_for_account1 #为第二个账号创建rsa文件 ssh-keygen -t rsa -C \u0026#34;your email\u0026#34; -f ~/.ssh/id_rsa_for_account2 在.ssh文件夹下创建config文件 输入如下内容:\n#Default GitHub Host account1 HostName github.com User git IdentityFile ~/.ssh/id_rsa_for_account1 Host account2 HostName github.com User git IdentityFile ~/.ssh/id_rsa_for_account2 然后分别在account1和account2GitHub中添加公钥。 验证\nssh -T account1 ssh -T account2 ","title":"Different rsa for different github account in the same computer"},{"location":"https://codenow.me/algorithm/leetcode_905_sort_array_by_parity/","text":" 题号:905\n难度:Easy\n链接:https://leetcode.com/problems/sort-array-by-parity/\n 如下是 python3 代码:\n#!/usr/bin/python class Solution: def sortArrayByParity(self, A: \u0026#39;List[int]\u0026#39;) -\u0026gt; \u0026#39;List[int]\u0026#39;: lens = len(A) store_list = [None] * lens head = 0 tail = lens - 1 for i in range(lens): if A[i] % 2 == 0: store_list[head] = A[i] head += 1 else: store_list[tail] = A[i] tail -= 1 return store_list if __name__ == \u0026#39;__main__\u0026#39;: test_list = [3, 1, 2, 4] print(Solution().sortArrayByParity(test_list))","title":"Leetcode:905 Sort Array ByParity"},{"location":"https://codenow.me/articles/how-to-use-hugo/","text":" 一、介绍 1. 优点 Hugo是一个用Go语言编写的静态网站生成器,它使用起来非常简单,相对于Jekyll复杂的安装设置来说,Hugo仅需要一个二进制文件hugo(hugo.exe)即可轻松用于本地调试和生成静态页面。 Hugo生成静态页面的效率很高,几乎是瞬间完成的,而之前用Jekyll需要等待。 Hugo自带watch的调试模式,可以在我修改MarkDown文章之后切换到浏览器,页面会检测到更新并且自动刷新,呈现出最终效果,能极大的提高博客书写效率。 再加上Hugo是使用Go语言编写,已经没有任何理由不使用Hugo来代替Jekyll作为我的个人博客站点生成器了。 2. 静态网站文件的两种方式: 放到自己的服务器上提供服务:需要自己购买服务器 把网站托管到 GitHub Pages:需要将静态页面文件 push 到 GitHub 的博客项目的 gh-pages 分支并确保根目录下有 index.html 文件。 3. 官网 Hugo语言官方中文文档地址:http://www.gohugo.org/ Hugo官方主页:https://gohugo.io/ 二、安装Hugo 1. 二进制安装(推荐:简单、快速) 到 Hugo Releases (https://github.com/gohugoio/hugo/releases)下载对应的操作系统版本的Hugo二进制文件(hugo或者hugo.exe)\n 下载解压后添加到 Windows 的系统环境变量的 PATH 中即可,不需安装。 可以直接放在C:\\Users\\chunt\\go\\bin下,这样就不需要添加系统环境变量 Mac下直接使用 Homebrew 安装:\n brew install hugo 二进制在 $GOPATH/bin/, 即C:\\Users\\chunt\\go\\bin 2. 源码安装(不好用,go get有些下载不下来) 源码编译安装,首先安装好依赖的工具:\n Git Go 1.3+ (Go 1.4+ on Windows) 设置好 GOPATH 环境变量,获取源码并编译:\n export GOPATH=$HOME/go go get -v github.com/spf13/hugo 源码会下载到 $GOPATH/src 目录, 即C:\\Go\\src\n如果需要更新所有Hugo的依赖库,增加 -u 参数:\n go get -u -v github.com/spf13/hugo The -u flag instructs get to use the network to update the named packages and their dependencies. By default, get uses the network to check out missing packages but does not use it to look for updates to existing packages.\nThe -v flag enables verbose progress and debug output.\n 3. 查看安装结果 可知hugo已经正常安装: 三、创建hugo项目 使用Hugo快速生成站点,比如希望生成到 /path/to/site | C:\\code\\hugo路径:\n linux: $ hugo new site /path/to/site windows: hugo new site C:\\code\\hugo 这样就在 /path/to/site | C:\\code\\hugo目录里生成了初始站点,进去目录:\n cd /path/to/site cd C:\\code\\hugo 站点目录结构:\n ▸ archetypes/ ▸ content/ ▸ layouts/ ▸ static/ config.toml config.toml是网站的配置文件,这是一个TOML文件,全称是Tom’s Obvious, Minimal Language, 这是它的作者GitHub联合创始人Tom Preston-Werner 觉得YAML不够优雅,捣鼓出来的一个新格式。 如果你不喜欢这种格式,你可以将config.toml替换为YAML格式的config.yaml,或者json格式的config.json。hugo都支持。\n content目录里放的是你写的markdown文章,layouts目录里放的是网站的模板文件,static目录里放的是一些图片、css、js等资源。\n 四、创建文章 1. 创建一个 about 页面: 进入到C:\\code\\hugo\n $ hugo new about.md about.md 自动生成到了 content/about.md ,打开 about.md 看下:\n内容是 Markdown 格式的,+++ 之间的内容是 TOML 格式的,根据你的喜好,你可以换成 YAML 格式(使用 \u0026mdash; 标记)或者 JSON 格式。\n2. 创建第一篇文章,放到 post 目录,方便之后生成聚合页面。 $ hugo new post/first.md\n打开编辑 post/first.md :\n五、安装皮肤 去 themes.gohugo.io 选择喜欢的主题,下载到 themes 目录中,配置可见theme说明\n1. 下载方法一 在 themes 目录里把皮肤 git clone 下来: $ pwd /c/code/hugo $ mkdir themes # 创建 themes 目录 $ cd themes $ git clone https://github.com/digitalcraftsman/hugo-material-docs.git 2. 下载方法二 也可以添加到git的submodule中,优点是后面讲到用 travis 自动部署时比较方便。 如果需要对主题做更改,最好fork主题再做改动。 git submodule add https://github.com/digitalcraftsman/hugo-material-docs.git themes/hugo-material-docs 3. 使用皮肤 将\\blog\\themes\\hugo-fabric\\exampleSite\\config.toml 替换 \\blog\\config.toml 注:config.toml文件是核心,对网站的配置多数需要修改该文件,而每个主题的配置又不完全一样。\n4. 修改皮肤 如果需要调整更改主题,需要在 themes/hugo-material-docs 目录下重新 build cd themes/hugo-material-docs \u0026amp;\u0026amp; npm i \u0026amp;\u0026amp; npm start 生成主题资源文件(hugo-fabric为主题名) D:\\git\\blog\u0026gt;hugo -t hugo-fabric Started building sites ... Built site for language en: 0 of 3 drafts rendered 0 future content 0 expired content 8 regular pages created 12 other pages created 0 non-page files copied 2 paginator pages created 1 tags created 1 categories created total in 35 ms 将\\blog\\themes\\hugo-fabric\\exampleSite\\config.toml 替换 \\blog\\config.toml 5. 修改配置文件 根据个人实际情况,修改config.toml 五、启动 hugo 自带的服务器 1. 在你的站点根目录执行 Hugo 命令进行调试: 回到hugo站点目录C:\\code\\hugo $ hugo server \u0026ndash;theme=hugo-material-docs \u0026ndash;buildDrafts 注明:v0.15 版本之后,不再需要使用 \u0026ndash;watch 参数了 浏览器里打开: http://localhost:1313\n2. 在项目根目录下,通过 hugo server 命令可以使用hugo内置服务器调试预览博客。 --theme 选项可以指定主题。也可用-t --watch 选项可以在修改文件后自动刷新浏览器。也可用-w --buildDrafts 包括标记为草稿(draft)的内容。也可以用-D 六、 部署到github 1. 新建仓库 假设你需要部署在GitHub Pages上,首先在GitHub上创建一个Repository, 命名为:hanchuntao.github.io (hanchuntao替换为你的github用户名)。 注意 baseUrl要在仓库setting里面查看,有可能跟仓库名不一样。 例如:https://SYSUcarey.github.io/变成了https://sysucarey.github.io/\n2. 在项目根目录执行Hugo命令生成HTML静态页面 $ hugo --theme=hugo-material-docs --baseUrl=\u0026quot;https://hanchuntao.github.io/\u0026quot; \u0026ndash;theme 选项指定主题, \u0026ndash;baseUrl 指定了项目的网站\n注意 以上命令并不会生成草稿页面,如果未生成任何文章,请去掉文章头部的 draft=true 再重新生成。 文件默认内容在,draft 表示是否是草稿,编辑完成后请将其改为 false,否则编译会跳过草稿文件。\n3. 查看生成的页面 如果一切顺利,所有静态页面都会生成到public目录\n4. 将pubilc目录里所有文件push到刚创建的Repository的master分支。 $ cd public $ git init $ git remote add origin https://github.com/hanchuntao/hanchuntao.github.io.git $ git add -A $ git commit -m \u0026quot;first commit\u0026quot; $ git push -u origin master 浏览器里访问:https://hanchuntao.github.io/ 七、错误处理 1. Unable to locate Config file 启动 hugo 内置服务器时,会在当前目录执行的目录中寻找项目的配置文件。所以,需要在项目根目录中执行这个命令,否则报错如下: C:\\Users\\kika\\kikakika\\themes\u0026gt;hugo server --theme=hugo-bootstrap --buildDrafts --watch Error: Unable to locate Config file. Perhaps you need to create a new site. Run `hugo help new` for details. (Config File \u0026quot;config\u0026quot; Not Found in \u0026quot;[C:\\\\Users\\\\kika\\\\kikakika\\\\themes]\u0026quot;) 2. Unable to find theme Directory hugo 默认在项目中的 themes 目录中寻找指定的主题。所有下载的主题都要放在这个目录中才能使用,否则报错如下: C:\\Users\\kika\\kikakika\u0026gt;hugo server --theme=hugo-bootstrap --buildDrafts --watch Error: Unable to find theme Directory: C:\\Users\\kika\\kikakika\\themes\\hugo-bootstrap 3. 生成的网站没有文章 生成静态网站时,hugo 会忽略所有通过 draft: true 标记为草稿的文件。必须改为 draft: false 才会编译进 HTML 文件。 4. 默认的ServerSide的代码着色会有问题,有些字的颜色会和背景色一样导致看不见。 解决方法:使用ClientSide的代码着色方案即可解决。(见:Client-side Syntax Highlighting) 5. URL全部被转成了小写,如果是旧博客迁移过来,将是无法接受的。 解决方法:我是直接改了Hugo的代码,将URL强制转换为小写那段逻辑去掉了,之后考虑在config里提供配置开关,然后给Hugo提一个PR。如果是Windows用户可以直接https://github.com/coderzh/ConvertToHugo 下载到我修改后的版本myhugo.exe。 Update(2015-09-03): 已经提交PR并commit到Hugo,最新版本只需要在config里增加: disablePathToLower: true 6. 文章的内容里不能像Jekyll一样可以内嵌代码模板了。最终会生成哪些页面,有一套相对固定而复杂的规则,你会发现想创建一个自定义界面会非常的困难。 解决方法:无,看文档,了解它的规则。博客程序一般也不需要特别的自定义界面。Hugo本身已经支持了类似posts, tags, categories等内容聚合的页面,同时支持rss.xml,404.html等。如果你的博客程序复杂到需要其他的页面,好好想想是否必须吧。 7. 如何将rss.xml替换为feed.xml? 解决方法:在config.yaml里加入: rssuri: “feed.xml” 8. 部署到github上后, 无内容 个人原因 hugo \u0026ndash;theme=hyde \u0026ndash;baseUrl=\u0026ldquo;https://hanchuntao.github.io/\u0026quot;生成静态页面后,public中会产生相应的目录,没有把这些目录push 到远端\n9. 部署到github上后一直不显示CSS样试 发现是 \u0026ndash;baseUrl=\u0026ldquo;http://hanchuntao.github.io/\u0026quot;的问题,要用 \u0026ndash;baseUrl=\u0026ldquo;https://hanchuntao.github.io/\u0026quot;\n从github上看到的markdown没有显示图片 原因: 图片要保存在static目录下,并显在引用图片时,使用static的相对位置(例如:/how-to-use-hugo/1.png) 生成静态网页后,需要把图片也上传到github\n","title":"How to Use Hugo"},{"location":"https://codenow.me/tips/go-pptof/","text":"使用 go tool pptof 可以 debug 程序\n需要在程序中先 import\nimport _ \u0026#34;net/http/pprof\u0026#34; 然后启动一个 goroutine 用于远程访问\ngo func() { log.Println(http.ListenAndServe(\u0026#34;localhost:6060\u0026#34;, nil)) }() 最后我们就可使用 http 抓取一些关键指标\n go tool pprof http://localhost:6060/debug/pprof/heap go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 go tool pprof http://localhost:6060/debug/pprof/block wget http://localhost:6060/debug/pprof/trace?seconds=5 go tool pprof http://localhost:6060/debug/pprof/mutex ","title":"Go tool pptof"},{"location":"https://codenow.me/translation/migrating-projects-from-dep-to-go-modules/","text":" 原文地址\nGo Modules 是 Go 管理的未来方向。已经在 Go 1.11 中可以试用,将会是 Go 1.13 中的默认行为。\n我不会在这篇文章中描述包管理工具的工作流程。我会主要讨论的是如何把现有的项目中 dep 迁移的 Go Module。\n在我的实例中,我会使用一个私有的仓库地址 github.com/kuinta/luigi ,它是使用 Go 语言编写,在好几个项目中被使用,是一个绝佳的候选人。\n首先,我们需要初始化 Module:\ncd github.com/kounta/luigi go mod init github.com/kounta/luigi 完成后只会有两行输出:\ngo: create now go.mod: module github.com/kounta/luigi go: copying requirments from Gopkg.lock 是的,这样就对了。这样就已经完成从 dep 迁移了。\n现在你只要看一眼新生成的文件 go.mod 就像下面这样:\nmodule github.com/kounta/luigi go 1.12 require ( github.com/elliotchance/tf v1.5.0 github.com/gin-gonic/gin v1.3.0 github.com/go-redis/redis v6.15.0+incompatible ) 其实在 require 中还有更多的内容,为了保持整洁我把他们删除了。\n就像 dep 区分 toml 和 lock 文件一样。我们需要生成 go.sum 文件,只要执行:\ngo build 现在你可以删除 Gopkg.lock 和 Gopkg.toml 文件,然后提交 go.mod 和 go.sum 文件。\nTravis CI 如果你使用 Travis CI,你需要在 Go 1.13 之前通过设置环境变量来启用该功能。\nGO111MODULE=on 私有仓库 如果你要导入私有仓库,你可以会发现这个错误:\ninvalid module version \u0026quot;v6.5.0\u0026quot;: unknown revision v6.5.0 这是一个误导。它真正想说的,无法识别这个 URL (在这里是指的是 github.com)。无法找到这个仓库是因为 Github 没有权限确认仓库的存在。\n修复这个问题也很简单:\n 登录 Github 账号,然后到 Setting -\u0026gt; Personal access tokens 创建一个有访问私有仓库权限的 token 然后执行 export GITHUB_TOKEN=xxx git config --global url.\u0026#34;https://${GITHUB_TOKEN}:x-oauth-basic@github.com/kounta\u0026#34;.insteadOf \u0026#34;https://github.com/kounta\u0026#34;","title":"把项目从 Dep 迁移到 Go Modules"},{"location":"https://codenow.me/algorithm/leetcode_146_lru_cached/","text":" 题号:146\n难度:hard\n链接:https://leetcode-cn.com/problems/lru-cache/\n 使用双向链表+map,O(1) 时间复杂度内完成 get 和 put 操作\nclass Node: \u0026#34;\u0026#34;\u0026#34; 双链表节点 \u0026#34;\u0026#34;\u0026#34; def __init__(self, key, val): self.val = val self.key = key self.next = None self.prev = None class LRUCache: def __init__(self, capacity: int): self.capacity = capacity self.head = None self.tail = None self.index = {} def get(self, key: int) -\u0026gt; int: node = self.index.get(key) if node == None: return -1 if node.prev == None: # 这是一个表头节点 return node.val if node.next == None: # 这是一个尾节点,需要移动到头结点 if len(self.index) == 2: # 如果这是只有两个节点的链表 self.head = node self.tail = node.prev self.head.next = self.tail self.tail.prev = self.head else: self.tail = node.prev self.tail.next = None node.next = self.head self.head.prev = node self.head = node return self.head.val # 中间节点 node.prev.next = node.next node.next.prev = node.prev node.prev = None node.next = self.head self.head.prev = node self.head = node return self.head.val def put(self, key: int, value: int) -\u0026gt; None: node = self.index.get(key) if node: # 如果存在先删除 if len(self.index) == 1: # 如果只有一个直接删除就好 self.head = None self.tail = None self.index.pop(node.key) elif node.next == None: # 删除尾节点,需要修复一下 self.tail self.tail = node.prev self.tail.next = None self.index.pop(node.key) elif node.prev == None: # 删除头结点,需要修复一下 self.head self.head = node.next self.head.prev = None self.index.pop(node.key) else: # 删除中间节点 node.prev.next = node.next node.next.prev = node.prev self.index.pop(node.key) else: # 如果 capacity 不够要删除尾节点 if len(self.index) \u0026gt;= self.capacity: if len(self.index) == 1: self.head = None self.tail = None self.index = {} else: node = self.tail self.tail = node.prev self.tail.next = None self.index.pop(node.key) # 构建一个新的节点,插入到头部 node = Node(key, value) if len(self.index) == 0: self.head = node self.tail = node self.index[key] = node elif len(self.index) == 1: # 如果当前只有一个节点 self.head = node self.head.next = self.tail self.tail.prev = self.head self.index[key] = node else: node.next = self.head self.head.prev = node self.head = node self.index[key] = node ","title":"Leetcode: 146 LRU Cache"},{"location":"https://codenow.me/articles/rmdbs-tree-datastruct/","text":"在关系型数据库中存储树形结构是比较麻烦的事情,因为数据库都是基于行存储的结构,要满足树形数据结构的添加、删除、查询、修改是一件比较棘手的事情。\n已经有一些解决方案可以解决:\n这篇文章介绍一下,使用「闭包表」来处理树形结构存储。\n选择「闭包表」主要是基于查询、插入、删除、移动都比较简单,更要的是都可以使用一条 SQL 就能处理完成。\nCREATE TABLE Comments ( comment_id SERIAL PRIMARY KEY, comment TEXT NOT NULL ); 树形结构典型就是评论和部门成员关系,以评论为例,我们同时又要支持完整增删改查的功能,大致结构如下: 为了满足这种复杂的关系,需要有另外一个表来存储这种结构。\nCREATE TABLE TreePaths ( ancestor BIGINT NOT NULL, descendant BIGINT NOT NULL, PRIMARY KEY(ancestor, descendant), FOREIGN KEY (ancestor) REFERENCES Comments(comment_id), FOREIGN KEY (descendant) REFERENCES Comments(comment_id) ); ancestor 作为每个评论节点的祖先,descendant 作为每个评论节点的后代。\n 这里的祖先和后代都是泛指所有祖先和后代,而不是特指直接的祖先和后代\n 接着构造一批数据插入 Comments 和 Tree Paths 中\ninsert into comments(comment_id, comment) values (1, \u0026#39;这个 Bug 的成因 是什么\u0026#39;); insert into comments(comment_id, comment) values (2, \u0026#39;我觉得是一个空指针\u0026#39;); insert into comments(comment_id, comment) values (3, \u0026#39;不,我查过了\u0026#39;); insert into comments(comment_id, comment) values (4, \u0026#39;我们需要查无效输入\u0026#39;); insert into comments(comment_id, comment) values (5, \u0026#39;是的,那是个问题\u0026#39;); insert into comments(comment_id, comment) values (6, \u0026#39;好,查一下吧\u0026#39;); insert into comments(comment_id, comment) values (7, \u0026#39;解决了\u0026#39;); insert into treepaths(ancestor, descendant) values (1, 1); insert into treepaths(ancestor, descendant) values (1, 2); insert into treepaths(ancestor, descendant) values (1, 3); insert into treepaths(ancestor, descendant) values (1, 4); insert into treepaths(ancestor, descendant) values (1, 5); insert into treepaths(ancestor, descendant) values (1, 6); insert into treepaths(ancestor, descendant) values (1, 7); insert into treepaths(ancestor, descendant) values (2, 2); insert into treepaths(ancestor, descendant) values (2, 3); insert into treepaths(ancestor, descendant) values (3, 3); insert into treepaths(ancestor, descendant) values (4, 4); insert into treepaths(ancestor, descendant) values (4, 5); insert into treepaths(ancestor, descendant) values (4, 6); insert into treepaths(ancestor, descendant) values (4, 7); insert into treepaths(ancestor, descendant) values (5, 5); insert into treepaths(ancestor, descendant) values (6, 6); insert into treepaths(ancestor, descendant) values (6, 7); insert into treepaths(ancestor, descendant) values (7, 7); 这里需要解释一下 treepaths 存储关系的逻辑:\n 每个节点和自己建立一个关系,也就是 ancestor 和 descendant 都是自己 每个节点和自己祖先建立关系,也就是 ancestor 指向所有祖先节点 每个节点和自己后代建立关系,也就是 descendant 指向所有的后代节点 以上关系建立完毕之后,就能以树形关系查询 comments 表中的数据,比如要查询 comment_id = 4 所有的子节点:\nSELECT c.* FROM Comments AS c JOIN TreePaths AS t ON c.comment_id = t.descendant WHERE t.ancestor = 4; 或者要查询 comment_id = 4 所有的父节点:\nSELECT c.* FROM Comments AS c JOIN TreePaths AS t ON c.comment_id = t.ancestor WHERE t.descendant = 4; 假如要在 comment_id= 5 后插入一个新的节点,先要插入关联到自己的关系,然后从 TreePaths 找出中 descendant 为 5 节点。意思就是找出 comment_id = 5 的祖先和新节点在 TreePaths 关联上.\ninsert into comments(comment_id, comment) values (8, \u0026#39;对的是这个问题,我已经修复了\u0026#39;); INSERT INTO TreePaths (ancestor, descendant) SELECT t.ancestor, 8 FROM TreePaths AS t WHERE t.descendant = 5 UNION ALL SELECT 8, 8; 如果要删除 comment_id = 7 这个节点,只需要在 TreePaths 删除 descendant = 7 的记录即可,这时候不用我们维护节点和节点之间的关系,所以很方便\nDELETE FROM TreePaths WHERE descendant = 7; 假如要删除 comment_id = 4 这颗完整的树,只需要找出这个 root 节点所有的后代删除即可。\nDELETE FROM TreePaths WHERE descendant IN (SELECT descendant FROM TreePaths WHERE ancestor = 4); 如果是移动一个节点,只需要删除然后再添加即可,这时候自身的引用可以不用删除。\n比较复杂的是移动一棵树,要先找到这棵树的根节点,然后移除所有子节点和他们祖先的关系,比如把 comment_id = 6 移动到 commint_id = 3 下。\n首先把在 TreePaths 把所有关系移除\nDELETE FROM TreePaths WHERE descendant IN (SELECT descendant FROM TreePaths WHERE ancestor = 6) AND ancestor IN (SELECT ancestor FROM TreePaths WHERE descendant = 6 AND ancestor != descendant); 然后在 commint_id = 3 插入新关系,同时所有子节点要和 commint_id = 3 的祖先建立关系\nINSERT INTO TreePaths (ancestor, descendant) SELECT supertree.ancestor, subtree.descendant FROM TreePaths AS supertree CROSS JOIN TreePaths AS subtree WHERE supertree.descendant = 3 AND subtree.ancestor = 6; 使用一开始查询的 SQL,可以看出移动过去了\n","title":"使用 RMDBS 存在树结构数据"},{"location":"https://codenow.me/articles/exercise_of_a_tour_of_go/","text":" 这周学了学 golang,做个记录\n学习网站:https://tour.golang.org\n对应的中文版:https://tour.go-zh.org\n这周主要学习内容是刷了一遍上面这个教程,虽然够官方,但讲解并不细致,很多需要自行 google\n顺便,第一次打开教程和在线运行代码都需要科学上网,但打开一次后所有内容就都被缓存下来了,火车上都可以翻页学习。也不方便的话可以用中文版,或者本地安装,教程上也都有说。\n知识点记录 go 项目结构 必须要有 package import 用的是字符串 首字母大写的是导出名(exported name),可以被别的包使用,有点类似于 python 的 all 只有 package main 可以被直接运行 运行入口 func main() {} 基础部件 函数以 func 定义,每个参数后必须带类型,必须规定返回值类型,可返回多个值,返回值可预先命名,函数是第一类对象(first class object) 变量以 var 定义,定义时必须规定类型,可在定义时赋值,函数内的变量可以不用 var 而用 := 来定义+赋值 常量以 const 定义,不能使用 := 语法,仅支持基础类型 基础类型是 bool, string 和各种数字,byte = uint8, tune = int32 类似于 null, None 的,是 nil 语法 if 不需要小括号,但必须有大括号;if 中可以有一条定义变量的语句,此变量仅在 if 和 else 中可用 for 是唯一的循环结构,用法基本等同于 Java 里的 for + while,同样没有小括号,但有大括号,for {} 是无限循环 switch 的每个 case 后等同于自带 break,但可以用 fallthrough 直接跳过判断而执行下一条 case 内的语句;没有匹配到任何一个 case 时会运行 default 里的内容;没有条件的 switch 可以便于改写 if-elseif-else defer 可将其后的函数推迟到外层函数返回之后再执行,多个 defer 会被压入栈中,后进先出执行 select-case 语句可同时等待多个 chan,并在所有准备好的 case 中随机选一个执行 for-range 可以对 array, map, slice, string, chan 进行遍历 make 可用来为 slice, map, chan 类型分配内存及初始化对象,返回一个引用,对这三种类型使用make时,后续参数含义均不同 其他数据类型 pointer 类似 C,没有指针运算 struct 内的字段使用 . 访问 array 必须有长度,且内部所有值类型必须相同 slice 类似数组的引用,可动态调整长度,有 len 和 cap 两个属性,零值是 nil,用 append 函数可以追加元素及自动扩展 cap map 在获取值的时候可以用 value, ok = m[key] 来校验 key 是否存在 method 与 function 略有不同,需要有一个 receiver,若 receiver 为指针,则可以在方法中修改其指向的值。只能为定义在当前包的类型定义 method interface 类型被实现时无需显示说明,任何类型只要实现了其所有方法就认为其实现了此接口,没有 implements 关键字;可以用空接口来接收任意值 interface{} interface value 是一个tuple(value, type),可以用 t, i = i.(T) 来校验类型,switch 中可以用 v := i.(type) 来判断其类型 channel 用来在 goroutines 直接传递信息,被 close 后可以用 for-range 遍历 常见 interface: stringer, error, reader, writer goroutine 用 go 来启动一个 goroutine 用 chan 来在不同 goroutine 之间交流 select-case 语句可以同时等待多个 chan,并在所有准备好的 case 中随机选一个运行 sync.Mutex 互斥锁可用来保证多个 goroutine 中每次只有一个能够访问共享的变量 其他规则 所有的大括号里,前括号不允许单独成一行 推荐使用 tab 而非空格 没有 class 概念,用 struct + method 实现 变量定义了就必须要使用,否则通不过编译 A Tour of Go 里的内容大概就这些,可能还有些遗漏的细节,我也不打算再去补充上了\n这里还提供了 11 个练习,我也顺着做了过来,感觉就跟上学时的课后习题一样,熟悉的感觉让人感动\n我的解答以及相关内容放到了 WokoLiu/go-tour-exercise\n如果有朋友要学 golang 的话,希望能有些帮助\n","title":"[Go]Exercise of a Tour of Go"},{"location":"https://codenow.me/tips/is_varchar_a_number/","text":"判断 MySQL 里一个 varchar 字段的内容是否为数字:\nselect * from table_name where length(0+name) = length(name);","title":"[Mysql]Is Varchar a Number?"},{"location":"https://codenow.me/algorithm/leetcode_11_container_with_most_water/","text":" 题号:11\n难度:medium\n链接:https://leetcode.com/problems/container-with-most-water 如下是 python3 代码\n from typing import List class Solution(object): def maxArea01(self, height: List[int]) -\u0026gt; int: \u0026#34;\u0026#34;\u0026#34;先撸一个暴力的\u0026#34;\u0026#34;\u0026#34; max_area = 0 for i, a1 in enumerate(height): for j, a2 in enumerate(height[i + 1:]): max_area = max(max_area, min(a1, a2) * (j + 1)) return max_area def maxArea02(self, height: List[int]) -\u0026gt; int: \u0026#34;\u0026#34;\u0026#34;从左右往中间压缩。由于总面积是较短的一根决定的 考虑到,如果 height[left] \u0026lt; height[right] 那么即使 right -= 1,max_area 也不会超过当前面积, 反而 left += 1,面积还有可能更大,因此此时应 left += 1 另一个方向的判断同理 \u0026#34;\u0026#34;\u0026#34; max_area = 0 left = 0 right = len(height) - 1 while left \u0026lt; right: if height[left] \u0026lt; height[right]: max_area = max(max_area, height[left] * (right - left)) left += 1 else: max_area = max(max_area, height[right] * (right - left)) right -= 1 return max_area if __name__ == \u0026#39;__main__\u0026#39;: data = [1, 8, 6, 2, 5, 4, 8, 3, 7] print(Solution().maxArea02(data))","title":"Leetcode: 11 Container with most water"},{"location":"https://codenow.me/tips/tips-for-adding-debug-logs/","text":"之所以整理这方面的小技巧,主要是 golang 的开源项目都是像 TiDB、etcd 这种偏低层的分布式服务。用 debugger 来跟踪代码是比较困难的,容易出错,而且还容易遇到坑,比如:有的 golang 版本无法正确输出调试信息,mac 上有些开源项目调试模式无法正常运行等等。用日志的话,更简单直接,不容易遇到坑。只不过,在查看变量、查看调用栈方面是真不太方便,下面几个小技巧能够弥补一些吧。\n查看调用栈\n可以使用 debug.Stack() 方法获取调用栈信息,比如像下面这样:\nlog.Printf(\u0026#34;stack of function xxx: %v\u0026#34;, string(debug.Stack())) 不过,在日志中打印调用栈的方法还是要慎用,输出内容有时候太长了,影响日志的连贯性。可以考虑将栈信息再做一下处理,只保留最上面几层的调用信息。\n查看变量类型\n可以使用 %T 来查看变量类型,很多时候可以像下面这样简单查看一下变量的类型和取值:\nlog.Printf(\u0026#34;DEBUG: node type: %T, value: %v\u0026#34;, n, n) 使用 buffer 来收集要查看的变量信息\n有的时候,我们需要查看的不是一个变量,可能是多个变量或者一个复杂数据结构中的一部分字段,如果代码中没有给出满足需求的 String 方法的话,可以考虑用 buffer,自己一点点收集,就像下面这样:\nbuf := bytes.NewBufferString() fmt.Fprintf(buf, \u0026#34;a: %v, \u0026#34;, a) fmt.Fprintf(buf, \u0026#34;b.child: %v, \u0026#34;, b) fmt.Fprintf(buf, \u0026#34;c.parent: %v, \u0026#34;, c.parent) log.Printf(\u0026#34;%v\u0026#34;, buf.String())","title":"golang 项目添加 debug 日志的小技巧"},{"location":"https://codenow.me/articles/code-reading-coverage/","text":"最近本人在阅读一些开源项目的代码,说到如何阅读开源代码,特别是超出自己能力范围的开源项目,可以说的内容还是挺多的。今天分享一个比较「偏」的:代码「阅读」覆盖率。\n看一个代码库,刚开始可能是一头雾水,再咬咬牙坚持一下,一般能梳理出大致的脉络,比如服务的启动流程是怎样的,服务主要由那几个组件构成,它们之间是如何通信协作的。再往后则是一点一点了解代码是如何支持各种不同场景的,加深对代码的理解。代码「阅读」覆盖率在第三个阶段会有一定的帮助。\n所谓的代码「阅读」覆盖率,和代码测试覆盖率概念类似,后者统计的是运行测试时哪些代码被运行过,所占比例是多少,前者统计的则是哪些行代码已经理解了,哪些还不理解。通过阅读覆盖率的统计,我们能更好衡量对代码库的了解程度,增加我们深入阅读代码的乐趣。\n为了实现阅读覆盖率的统计,我开发了一个简陋的浏览器插件,主要有以下功能:\n功能一:基于 github,支持在 github 代码页面中标记哪些代码已经理解,效果如下图所示:\n直接借助 github 代码页面来显示代码理解情况,直接扩展 github 自带的菜单,增加标记功能 (图中的 mark as read 和 mark as unread 菜单项),这样能够减少一些工作量。\n功能二:统计代码阅读覆盖率\n效果如下所示:\n在文件列表和代码界面显示百分比。\n目前插件还很简陋,不过实现方式很简单,就不分享代码了,感兴趣的同学可以自己试着开发一个。\n小结,阅读学习开源代码是一种比较硬核的游戏,增加阅读覆盖率的统计,是为了给这个硬核游戏添加一些可视化元素,就像塞尔达荒野之息里的地图,你能通过它看到自己探索了哪些神庙。这类手段可以延长游戏成就带来的快感,每次当我理解了一些代码后去把它们标记出来,还是很开心的,每次对代码渐渐失去兴趣时,看到统计的百分比还比较低,就又有了研究的动力。\n","title":"代码「阅读」覆盖率"},{"location":"https://codenow.me/translation/group-by-and-aggregation-elimination/","text":"原文链接:Group-by and Aggregation Elimination 是一篇关于数据库查询优化的文章,有几句话实在不知道咋翻译好,也影响不大,直接留下原句了。翻译如下:\nI get a fair number of questions on query transformations, and it’s especially true at the moment because we’re in the middle of the Oracle Database 12c Release 2 beta program. 有时用户可能会发现在一个执行计划里有些环节消失了或者有些反常,然后会意识到查询发生了转换 (transformation) 。举个例子,有时你会惊讶的发现,查询语句里的表和它的索引可能压根就没有出现在查询计划当中,这是连接消除 (Join Elimination) 机制在起作用。\n我相信,你已经发现查询转换是查询优化中很重要的一环,因为它经常能够通过消除一些像连接(join)、排序(sort)的步骤来降低查询的代价。有时修改查询的形式可以让查询使用不同的访问路径(access path),不同类型的连接和甚至完全不同的查询方式。在每个发布版本附带的优化器白皮书中(比如 Oracle 12c One 的),我们都介绍了大多数的查询转换模式。\n在 Oracle 12.1.0.1 中,我们增加了一种新的转换模式,叫做 Group-by and Aggregation Elimination ,之前一直没有提到。它在 Oracle 优化器中是最简单的一种查询转换模式了,很多人应该都已经很了解了。你们可能在 Mike Dietrich’s upgrade blog 中看到过关于它的介绍。让我们来看一下这种转换模式到底做了什么。\n很多应用都有用过这么一种查询,这是一种单表分组查询的形式,数据是由另一个底层的分组查询形成的视图来提供的。比如下面这个例子:\nSELECT v.column1, v.column2, MAX(v.sm), SUM(v.sm) FROM (SELECT t1.column1, t1.column2, SUM(t1.item_count) AS sm FROM t1, t2 WHERE t1.column4 \u0026gt; 3 AND t1.id = t2.id AND t2.column5 \u0026gt; 10 GROUP BY t1.column1, t1.column2) V GROUP BY v.column1, v.column2; 如果没有查询转换,这个语句可能是下面这样的查询计划。每张表里有十万行数据,查询要运行 2.09 秒:\n------------------------------------------------------ Id | Operation | Name | Rows | Bytes | ------------------------------------------------------ | 0 | SELECT STATEMENT | | | | | 1 | HASH GROUP BY | | 66521 | 1494K| | 2 | VIEW | | 66521 | 1494K| | 3 | HASH GROUP BY | | 66521 | 2143K| | 4 | HASH JOIN | | 99800 | 3216K| | 5 | TABLE ACCESS FULL| T2 | 99800 | 877K| | 6 | TABLE ACCESS FULL| T1 | 99998 | 2343K| ------------------------------------------------------ 从上面的计划中,你会看到有两个 Hash Group By 步骤,一个是为了视图,一个是为了外层的查询。我用的是 12.1.0.2 版本的数据库,通过设置隐藏参数 _optimizer_aggr_groupby_elim 为 false 的方式禁用了查询转换。\n下面我们看一下查询转换生效时被转换的查询,你会发现只有一个 Hash Group By 步骤。查询时间也少了很多,只有 1.29 秒:\n---------------------------------------------------- Id | Operation | Name | Rows | Bytes | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | | | 1 | HASH GROUP BY | | 66521 | 2143K| | 2 | HASH JOIN | | 99800 | 3216K| | 3 | TABLE ACCESS FULL| T2 | 99800 | 877K| | 4 | TABLE ACCESS FULL| T1 | 99998 | 2343K| ---------------------------------------------------- 上面这个例子是相对比较好理解的,因为在视图中分组查询的列信息和外层查询是一样的。不一定非要是这样的形式才行。有的时候即使外层的 Group By 是视图中 Group By 的子集也是可以的。比如下面这个例子:\nSELECT v.column1, v.column3, MAX(v.column1), SUM(v.sm) FROM (SELECT t1.column1, t1.column2, t1.column3, SUM(t1.item_count) AS sm FROM t1, t2 WHERE t1.column4 \u0026gt; 3 AND t1.id = t2.id AND t2.column5 \u0026gt; 10 GROUP BY t1.column1, t1.column2, t1.column3) V GROUP BY v.column1, v.column3; ---------------------------------------------------- Id | Operation | Name | Rows | Bytes | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | | | 1 | HASH GROUP BY | | 49891 | 1607K| |* 2 | HASH JOIN | | 99800 | 3216K| |* 3 | TABLE ACCESS FULL| T2 | 99800 | 877K| |* 4 | TABLE ACCESS FULL| T1 | 99998 | 2343K| ---------------------------------------------------- 你不需要额外做什么操作来开启这个查询转换。它默认是被开启的,当某个查询符合条件的时候就会自动被转换。在实际的企业级系统中,这种方式一定会带来很多显著的优化。不过要注意,这种转换模式在使用了 rollup 和 cube 的分组函数时是不起作用的。\n这种转换有没有什么问题呢?是有的(这也是 Mike Dietrich 提到它的原因)。为了做这个转换,Oracle 优化器必须判断出来什么时候可以用什么时候不可以用,这背后的逻辑可能会很复杂。The bottom line is that there were some cases where the transformation was being applied and it shouldn’t have been. Generally, this was where the outer group-by query was truncating or casting columns used by the inner group-by. This is now fixed and it’s covered by patch number 21826068. Please use MOS to check availability for your platform and database version.\n","title":"Group by and Aggregation Elimination"},{"location":"https://codenow.me/algorithm/leetcode_62._unique_paths_by_jarvys/","text":"62. Unique Paths 是一道基础动规题,递推公式:f(x,y) = f(x+1,y) + f(x, y+1)。我用递归 + memo 的方式完成的,代码如下:\nclass Solution(object): def fn(self, i, j, rows, cols, memo): if j \u0026gt;= cols or i \u0026gt;= rows: return 0 if j == cols - 1 or i == rows - 1: return 1 if memo[i][j] is None: memo[i][j] = self.fn(i+1,j,rows,cols,memo) + self.fn(i,j+1,rows,cols,memo) return memo[i][j] def uniquePaths(self, m, n): \u0026#34;\u0026#34;\u0026#34; :type m: int :type n: int :rtype: int \u0026#34;\u0026#34;\u0026#34; if m == 1 and n == 1: return 1 memo = [] for i in range(n): r = [] for j in range(m): r.append(None) memo.append(r) return self.fn(0, 0, n, m, memo) ","title":"Leetcode: 62. Unique Paths by jarvys"},{"location":"https://codenow.me/joinus/","text":" 如何加入 email 联系: h1x2y3awalm@gmail.com\n发送你的 github 邮箱,到邮件正文,邀请到 github 组织中。\n成员列表 Github Username Email 完成次数 加入时间 zhegnxiaowai h1x2y3awalm@gmail.com 0 2019年3月18日 jarvys leguroky@gmail.com 0 2019年3月18日 tubby tubby.xue@gmail.com 0 2019年3月18日 WokoLiu banbooliu@gmail.com 0 2019年3月18日 hanchuntao h2a0n0k8@gmail.com 0 2019年3月18日 kingkoma kingkoma8@gmail.com 0 2019年4月07日 ","title":"加入我们"},{"location":"https://codenow.me/rules/","text":" 食用指南 这是一个自发的学习小组,主要以学习为目的,提升自己的能力。\n 学习是泯灭人性的行为,所以我们要有一些强制手段来逼迫自己。\n 我们的学习计划以每周为一个周期,分成四个任务:\n 每周一个算法题:算法题可以来自任何 OJ 网站(比如 Leetcode 等),难度根据每人情况不同自己选择。 每周一篇文章翻译:翻译文章可长可短,只要和计算机相关的知识就可以。 每周一个 Tips:Tip 就是一个很小的记录,比如这周学会了什么小技巧。 每周一个分享:需要整理出一篇文章,内容不限,但是要和计算机相关,篇幅在 500 - 800 字最好。分享能成一整个系列最好。 任务时间为,周一 0 点整至周日 24 点整。统计方式以 commit 时间为准,合并到分支后会触发 CI 自动部署到该网站。\n惩罚措施 这是一个 强制 的学习计划,所以我们会有惩罚措施。\u0008惩罚措施 的 Base 为 100 元,每周的 4 个任务未完成一个需要惩罚 25 元。 惩罚的金额将会进入 惩罚基金,基金中所有的钱将会用于购买 学习资料。\n最后就是退出机制,任何人可以在 任何时候 退出或者在你未完成任务时候又不愿意缴纳惩罚时候,将会被强制清理出学习小组。\n惩罚基金在 未使用 时候会一并退回,同时删除 repo 中有关退出者的内容;如果惩罚基金已经被使用,将不会退回,需要注意。\n提交方法 首先要克隆本仓库到本地:\ngit clone --recurse-submodules https://github.com/atts-group/atts.git 任何时间都可以直接 push master 提交。\n 开始使用前你需要先安装 hugo,下载对应平台的版本即可。\n 目前有 4 个分类(type):\n algorithm articles tips translation 新建文章需要使用 hugo new {type}/{title}.md 的方式新建,不能直接创建文件。\n 这里的 {type} 就是上面 4 个分类的其中一个,{title} 就是你写的标题\n 当写完后用使用 hugo server 来观察一下你新添加的是否正确,然后用 git push 推送的 master 分支,即可完成。\n 禁止修改非你创建的文件,特别的隐藏文件,除非你知道为什么要这么修改\n ","title":"玩法"},{"location":"https://codenow.me/","text":"","title":"ATTS Group"},{"location":"https://codenow.me/tags/contribute/","text":"","title":"Contribute"},{"location":"https://codenow.me/tags/efficiency/","text":"","title":"Efficiency"},{"location":"https://codenow.me/tags/git/","text":"","title":"Git"},{"location":"https://codenow.me/tags/github/","text":"","title":"Github"},{"location":"https://codenow.me/tags/hot_keys/","text":"","title":"Hot_keys"},{"location":"https://codenow.me/tags/jquery/","text":"","title":"Jquery"},{"location":"https://codenow.me/tags/kingkoma/","text":"","title":"Kingkoma"},{"location":"https://codenow.me/tags/leetcode/","text":"","title":"Leetcode"},{"location":"https://codenow.me/tags/log/","text":"","title":"Log"},{"location":"https://codenow.me/tags/mysql/","text":"","title":"Mysql"},{"location":"https://codenow.me/tags/opensource/","text":"","title":"Opensource"},{"location":"https://codenow.me/tags/security/","text":"","title":"Security"},{"location":"https://codenow.me/tags/shell/","text":"","title":"Shell"},{"location":"https://codenow.me/tags/","text":"","title":"Tags"},{"location":"https://codenow.me/tags/vim/","text":"","title":"Vim"}]