<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>元视角</title><link>https://blog.yuanpei.me/</link><description>Recent content on 元视角</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Fri, 06 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.yuanpei.me/index.xml" rel="self" type="application/rss+xml"/><item><title>.NET 生态下的 Agent 框架选型：从 ReAct 到原生推理</title><link>https://blog.yuanpei.me/posts/dotnet-agent-framework-comparison/</link><pubDate>Fri, 06 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/posts/dotnet-agent-framework-comparison/</guid><description>最近，博主正在重构基于 ReAct 模式的 Agent 系统，计划将其升级为支持原生推理的新方案。为此，博主对 .NET 生态下的三种 Agent 框架进行了详细调研和实测：从 Semantic Kernel 到 Microsoft.Extensions.AI，再到 Microsoft Agent Framework。如果你了解过推理模型、扩展思考（Extended Thinking）、交错思考（Interleaved Thinking）等概念，相信会对这篇文章感兴趣。在过去的一个多月里，由&amp;quot;小龙虾&amp;quot; (OpenClaw) 引发的&amp;quot;全民养虾&amp;quot;热潮可谓风头无两。然而，在这 Agent 进化高歌猛进的历程中，关于模型思考本身的关注屈指可数。博主希望通过这篇文章，让大家看到 AI 光环下隐藏的&amp;quot;混乱&amp;quot;。在博主看来，这种混乱本身，与 LLM 基于概率预测下一个词元的不确定性同样迷人。
背景：为什么需要换方案？ 首先，为什么需要更换方案？这是因为，此前的 Agent 基于 ReAct（Reasoning + Acting）模式工作。该模式的特点是 —— LLM 按照提示词模板生成&amp;quot;推理&amp;quot;内容，实际上是按照固定格式输出的文本，并且在调用工具时，只能一个一个来。这样就会带来两个非常明显的问题：
高延迟：每次工具调用都需要一次完整的 LLM 调用 假推理：模型只是在按格式输出，并不是真的在思考 新一代模型（比如 OpenAI o1、DeepSeek Reasoner、Claude 3.7+）支持原生推理，模型内部具备真正的推理能力。推理内容可以通过 API 获取，更重要的是工具可以并行执行。因此，博主在考虑让 Agent 系统支持原生推理，目标是——找到能获取这些信息的框架。
推理模式前置知识 在正式开始实验前，先了解一下当前主流大模型的推理模式差异。这有助于理解为什么不同框架的支持程度不同。
新一代推理模型（如 OpenAI o1、DeepSeek Reasoner、Claude 3.7+）支持返回内部推理过程，这个过程被称为 Reasoning 或 Thinking。如下表所示，这些不同的术语描述的其实是同一件事情：
术语 含义 reasoning_content 推理过程的实际文本内容（DeepSeek、OpenAI） thinking Claude 的推理区块，需配置 budget_tokens thought Gemini 的推理字段 主流模型推理能力对比 下表展示了目前主流模型在推理能力方面的差异对比：</description></item><item><title>从「能用」到「好用」：LLM 流式响应实现方式的探索之路</title><link>https://blog.yuanpei.me/posts/llm-streaming-evolution/</link><pubDate>Wed, 28 Jan 2026 20:30:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/llm-streaming-evolution/</guid><description>你在屏幕上看到 AI「正在输入」的光标时，有没有想过：这几个字节是怎样跨越千山万水，在屏幕上一个个「蹦」出来的？当 ChatGPT、Kimi、Claude 们用流式的方式「打字」给你看时，这种近乎人类的交互体验背后，藏着一个不起眼却至关重要的技术——Server-Sent Events（SSE）。它不像 WebSocket 那样大名鼎鼎，却在 AI 时代成为了事实上的标准。诚然，在 ASP.NET Core 中实现一个「能工作」的 SSE 接口，仅仅需要十分钟。但是，如果要实现一个「好用」的 SSE 接口——支持事件抽象、复用性好、能优雅地取消，你需要多久呢？本文将为你拆解这个挑战，展示如何整个设计从「能用」走向「好用」。
当 AI 开始「思考」 试想这样一个场景：用户向 Agent 提问「讲一个关于小狐狸的故事」。传统的 HTTP 请求-响应模式下，服务器需要等待大语言模型生成完整回答后，再能将结果返回给用户。这意味着用户可能要盯着屏幕等待十几秒后，才能看到完整的答案。但是，在真实的产品体验中，我们期望看到的是：Agent 首先展示它的「思考过程」——它如何理解用户意图、如何规划回答策略；然后是工具调用的实时反馈——搜索资料、查询数据库；最后才是回答内容的逐字输出。这种「实时可见」的体验，远比「等待-呈现」的模式更加自然和引人入胜。
SSE 正是实现这种体验的关键技术，它基于 HTTP 协议，允许服务器主动向客户端推送数据，相比 WebSocket 更加轻量，且能复用现有的 HTTP 基础设施。对于 LLM 流式输出这类场景，SSE 几乎是完美的选择。
第一阶段：最朴素的实现 一切的开始，是一段朴实无华的代码。博主直接在控制器中拼接 SSE 格式的字符串，然后写入响应流：
[HttpGet(&amp;#34;chat&amp;#34;)] public async Task ChatStream(CancellationToken cancellationToken) { Response.ContentType = &amp;#34;text/event-stream; charset=utf-8&amp;#34;; Response.Headers[&amp;#34;Cache-Control&amp;#34;] = &amp;#34;no-cache&amp;#34;; Response.Headers[&amp;#34;Connection&amp;#34;] = &amp;#34;keep-alive&amp;#34;; foreach (var chunk in GenerateText()) { string message = $&amp;#34;data: {JsonConvert.SerializeObject(new { text = chunk })}\n\n&amp;#34;; await Response.</description></item><item><title>当我用 2000 条聊天记录，让 AI 为我画一幅自画像</title><link>https://blog.yuanpei.me/posts/ai-mirror-self-analysis/</link><pubDate>Tue, 13 Jan 2026 22:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/posts/ai-mirror-self-analysis/</guid><description>2050 条对话，4394 条消息。 这是我在过去一年里和 AI 说的话。
当我把它们全部交给另一个 AI，让它从第三视角分析我，会得到怎样的结论？
这个问题在我脑海里盘旋了很久：
我和 AI 聊了这么多，它们会不会比我自己更了解我？
坦白讲，我并不期待什么石破天惊的发现。但当所有零散的对话被汇总、被量化、被抽象之后，那个被 AI 描绘出来的「我」，会不会和我以为的自己不太一样？
数据概览 第一步：DeepSeek 上的「我」 我先让 AI 单独分析了 DeepSeek 的记录。这是我最常用的代码助手，原本以为它会直接给我贴上「技术宅」的标签。结果比我想象中具体得多：
上午 9-11 点最活跃 51% 的对话是「短平快」型（3 句话以内结束） 高频请求是「写代码」「解释原理」 关键词集中在 Python、SQL、AI、机器学习 AI 给出的总结是：「你是一个追求效率、喜欢直接要结果的开发者。」
年度关键词 活跃度热力图 我看完愣了几秒，然后不自觉地笑了——好像&amp;hellip;&amp;hellip;确实是这样。
第二步：三个平台，三个「我」 当我把三个平台的数据放在一起对比时，发现了一件有意思的事：同一个我，在不同工具上，竟然表现出完全不同的状态。
平台 对话特征 我的状态 DeepSeek 51% 短对话 执行模式：快速出活 ChatGPT 36% 长对话 思考模式：深入探讨 Kimi 100% 极短对话 闲聊模式：翻译、润色 这感觉有点像工作中的角色切换——面对不同的工具，我们会自然调整说话方式：
DeepSeek 更像「我的手」，负责把想法快速落地 ChatGPT 更像「我的大脑」，陪我拆解问题、反复推演 Kimi 则像「速记本」，处理零碎但必须完成的小事 意识到这一点时，我突然明白了一件事：工具不只是被使用的对象，它同样在反向塑造我们的行为模式。
不同平台的 &amp;amp;ldquo;我&amp;amp;rdquo; 第三步：AI 给我的「自画像」 当 AI 整合了所有数据后，给出了几段让我印象深刻的描述：</description></item><item><title>基于 Supabase 的 AI 应用开发探索</title><link>https://blog.yuanpei.me/posts/supabase-powered-ai-app-exploration/</link><pubDate>Sun, 24 Aug 2025 20:42:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/supabase-powered-ai-app-exploration/</guid><description>引言 最近看到一个有趣的观点：当你试图对 AI 进行某种界定时，会发现诸如知识密集型、资本密集型、劳动密集型、资源密集型这些类别，似乎都适用于它。首先，AI 无疑是知识密集型和资本密集型的。然而，考虑到数据标注等工作需要投入大量人力，AI 表现出劳动密集型的特征。与此同时，AI 的训练和推理依赖庞大的算力，而这背后离不开巨大的资源支持，所以它还具备资源密集型的属性。这种“什么都是，又什么都不是”、难以简单归类的状态，恰恰构成了 AI 产业最根本的特性。在持续使用 Claude Code 两个月以后，一切都归于寂静，曾经的 Cursor 和 Windsurf 亦是如此。秋风渐起，天气转凉，最近发布的 GPT-5 和 DeepSeek-v3.1 表现不温不火，反倒是谷歌凭借 Nano Banana 再次成为焦点。当然，相比于讨论这些无关紧要的事情，我更关注 AI 技术在实际场景中的落地。因此，在这篇博客中，我想和大家分享如何基于 Supabase 快速构建一个可用的 AI 应用。
为什么选择 Supabase? 时间来到 2025 年，横亘在我们面前的最大危机，已从「怎么做」变成「做什么」。Know-How 里的 Know 与 How，正在逐渐被 AI 接管，留给我们的只剩下 What——我们究竟该让技术指向何方。放眼望去，市面上可供开发 AI 应用的工具琳琅满目，令人目不暇接。以笔者为例，Cursor、Windsurf、Cline、Claude Code、Gemini CLI，各种工具几乎都尝试了一遍，而如今最常用的，反而是 VSCode 内置的 GitHub Copilot。结合我有限的认知，我对这些工具做了如下划分，大家可以按图索骥，选择适合自己的工具进行尝试：
编辑器/IDE/插件类：Cursor、Windsurf、Cline、GitHub Copilot 等 CLI/工具类： Claude Code、Gemini CLI 等 一站式部署类：Bolt.New、v0 等 低代码/工作流：Coze、Dify、n8n 等 代码框架：LangGraph、Semantic Kernel、AutoGen、CrewAI 等 那么，相对于这些这些方案，Supabase 有什么优势呢？开发 AI 应用时，我们真正需要的，是一个既能满足 AI 应用需求，又能快速开发和部署的平台。而 Supabase 正是这样一个理想的平台，它具有以下优势：</description></item><item><title>盛世幻象：从荔枝看盛唐的兴衰</title><link>https://blog.yuanpei.me/posts/the-litchi-road/</link><pubDate>Tue, 22 Jul 2025 11:17:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/the-litchi-road/</guid><description>杜牧曾以一首七言绝句，定格了盛唐的惊鸿一瞥：自骊山回望长安，一片锦绣繁华。随着重重宫门次第打开，一骑绝尘卷起烟霞，妃子们浅笑嫣然——那千里疾驰的秘密，终究是无人知晓的荔枝。这首脍炙人口的《过华清宫》，不仅留下了“一骑红尘妃子笑，无人知是荔枝来”的千古遐思，更为马伯庸《长安的荔枝》一书埋下了故事的伏笔。我不禁遥想，出身京兆的杜牧，是否亦曾在骊山晚照前伫立良久？每当落日熔金染红天际，那片化为焦土的阿房宫废墟，早已坠入沉沉暮霭。而这道自西向东蔓延的阴影，悄然爬上长安的宫墙，如同大唐帝国由盛转衰的某一个侧影。煌煌大唐，盛世如锦，前有贞观之治，后有开元盛世，可这盛世，到底是谁的盛世？
大明宫玄武门遗址，“玄武门对掏，谁赢谁太子” 光阴荏苒，在西安生活已近十年。盛唐于我而言，愈渐成为一种符号，如同十三朝古都、秦皇汉武、兵马俑、肉夹馍……凡此种种，你很难用一句话道尽这座城市的全貌。然而，当你驻足于街头巷尾，总能觅见那个时代零落的痕迹。青龙寺的樱花，是空海东渡求法的无言见证；乐游原的夕阳，浸染着李义山千古喟叹的余韵悠长；玄武门青砖之下，兄弟阋墙的刀光剑影早已风干成史册里冰冷的墨迹；香积寺的钟磬声里，昔日的旌旗猎猎、鼓角铮鸣，皆化作绵延千载的香火缭绕。而在兴庆宫花萼相辉楼前，霓裳羽衣的旋舞、李白的《清平调》，如今只余风中低语——宫阙万间，终归尘土。或许，盛唐一直在被转述、被追慕、被重现，却从未被真正地“抵达”。
长安区香积寺，“香积寺对砍，谁输谁叛军” 有人说，当暮色浸透西安的城墙，这座城市就变成了长安。因此，在西安看关于长安的影视作品，总带着几分怀古与寻梦的意味。可细细品来，从《妖猫传》到《长安十二时辰》，从《长安三万里》再到《长安的荔枝》，这些故事说的其实是同一件事：我们已无法想象大唐之“盛”，我们唯一知晓的是大唐之“衰”。在电影《妖猫传》里，玄宗或许根本不爱贵妃，他爱的，是操纵人心的快感，是堆砌在帝王之术上的“极乐之乐”。可即便如此，白居易依然坚信自己的《长恨歌》真实不虚。因为电影中“幻术”的设定，隐喻的是交织着谎言与真相、残酷与美好的现实世界。余光中评价李白“绣口一吐就是半个盛唐”，可正是这个盛唐，自玄武门之变开始，便浸染着血雨腥风的底色，其后更有神龙之变、、太平公主之乱、李隆基“一日杀三子”、香积寺之战……纷争不断，不一而足。
《妖猫传》李白 当年《妖猫传》上映时，其塑造的李白曾遭观众诟病“飘逸不足，猥琐有余”。然而，被尊称为“谪仙”的李白，到底是拥有喜怒哀乐的鲜活生命。相比之下，《长安三万里》从高适视角刻画的李白，或许更贴近历史的真实面貌。杨玉环明知“云想衣裳花想容”并非为自己而作，依然慨叹大唐拥有李白实为“幸事”。殊不知，当李白被唐玄宗赐金放还时，他已四十有三，胸怀壮志却无处施展，头顶“供奉翰林”之虚衔，陪侍君王游宴赋诗，几近“奉旨填词”。李白好饮酒交游，贺知章曾为其“金龟换酒”，杜甫尝为其作《饮中八仙歌》，称其“天子呼来不上船，自称臣是酒中仙”，只是那些豪情万丈、字字珠玑的诗篇背后，未尝不是他颠沛流离、郁郁不得志的生平写照？由此观之，这所谓盛世，既非李白的青云梯，亦非杨玉环的温柔乡，纵有绝世才华、倾国容颜，终难扶得大唐广厦之将倾。
《妖猫传》杨玉环 在《长安三万里》中，杜甫初次登场时，还只是个缺了门牙的阳光少年。待到天宝三载（公元744年）与李白相遇、同游梁宋时，他已是三十二岁的中年人。彼时，李白正被唐玄宗“赐金放还”，逐出长安。这两位后世并称“李杜”的伟大诗人，生命轨迹于此首次交汇。而见证这场历史性会面的，正是唐朝边塞诗的代表人物之一——高适。对于李白而言，长安之旅就此落幕，两度入京均以政治理想幻灭告终。然而，对于杜甫来说，他长达十年的长安困顿生涯，此刻刚刚拉开序幕。这一年，是天宝五载（公元746年），距离“安史之乱”爆发，尚有十年。
《长安三万里》少年杜甫 直至天宝十四载（公元755年），杜甫屡次通过科举、干谒、献赋等方式寻求出路，最终，因献《三大礼赋》获得玄宗赏识，得授右卫率府兵曹参军这一闲职。此时，杜甫已年届四十三岁。《长安的荔枝》中，张若昀饰演的杜甫，活脱脱便是“小范大人”模样，可细想之下，这又在情理之中——那毕竟是尚未被“安史之乱”烙上“沉郁顿挫”印记的杜甫。当长安沦陷的消息传来，李善德狼吞虎咽地吃下数颗荔枝，试图用甘甜压住喉咙间的苦涩，或许他心中眷恋的，除了寄托着理想和抱负的长安，还有那个曾经活泼灵动、肆意纵横、咏叹“会当凌绝顶，一览众山小”的意气少年——杜少陵。那么，这煌煌盛世，可曾真正属于杜甫？“国家不幸诗家幸，赋到沧桑句便工”，世人皆知他日后将写出传世的“三吏”、“三别”，可又有谁甘愿被时代的洪流裹挟、踉跄前行？
《长安的荔枝》中年杜甫 在《长安的荔枝》中，右相杨国忠有一句点破权力玄机的名言，“流程那种东西，是弱者才要遵循的规矩”。当他和李善德对峙时，身为权倾天下的宰相，只需一块腰牌，便让这名小小的“荔枝使”拥有调配举国之力的特权。无论是玄宗的一纸敕令，还是杨国忠的一道腰牌，正是这种凌驾于庙堂律法之上的绝对权力，最终化为致命毒药，加速了盛唐的土崩瓦解。相较于电视剧版以荔枝象征“外鲜内腐”的政治寓言，我更喜欢电影中的留白。贵妃准备去拿荔枝时，被鱼朝恩打断。此时，安禄山正要献舞，贵妃的手在这一刻收了回去。而随着镜头拉远，在玲琅满目的果品中，那盘荔枝孤悬中央，格外耀眼。贵妃是否喜食荔枝，无人知晓，我们唯一知道的事情是，如此大动干戈运来的荔枝，贵妃并没有吃到，还有什么比这更能戳破盛世幻象的荒诞？
一骑红尘妃子笑，无人知是荔枝来 当杨国忠“取之于民，用之于上”的密辛被李善德当面戳破，他暴怒失态，竟至对九品小吏拳脚相向。这位蔑视规则的“强者”或许不曾想到，在离长安不远的马嵬驿，日后会有一位名叫张小敬的骑士，仅凭一箭，便令其强权轰然倒地，万般权势俱为泡影。诚然，强者可以蔑视流程，然则孰强孰弱，恰如奔腾之江河，终有潮起潮落、顺逆倒悬。圣人随口一句“想尝尝岭南的新鲜荔枝”，无数个“李善德”便要赌上身家性命、前赴后继。殊不知，比荔枝更甘甜的，是在美人面前炫耀权力的滋味。《妖猫传》里，玄宗驱逐李白时，是何等写意潇洒，一旦“渔阳鼙鼓动地来”，不得不仓皇逃往蜀中避难。由此可见，繁华终会落幕，权力终会更迭。
《长安三万里》暮年李白 “安史之乱”过后，大唐国祚虽绵延百余年，然开元年间累积的浪漫与理想，已在烽火与岁月中渐次消散。唐中、晚期的文人墨客，笔下多了借古讽今、咏史抒怀的悲怆。这不仅是对往昔繁华的追怀，更是对当下时代的痛定思痛。杜牧作《阿房宫赋》时，阿房宫早化为荒烟蔓草；白居易写《长恨歌》时，唯有托名汉家以寄意唐事；李商隐借贾谊故事，发出“不问苍生问鬼神”的千古长叹。盛世的光环之下，权力倾轧的暗流交织着个体失意的阴影，折射出“盛”与“衰”的一体两面。正如那颗贵妃没能吃到的荔枝，不只象征着帝国运转的极致奢华，更隐喻着权力游戏的荒谬虚妄。我们难以断言，这盛世到底属于谁，因为真正的盛唐气象，从来不在于宫阙巍峨、物华天宝，而在于那些天才的灵魂——他们在失意中书写灼热，在沉默中留存锋芒。文明本身的韧性，远比“盛世”的虚名更为恒久。“胜地不常，盛筵难再”，这是一切繁华的宿命。“后之视今，亦犹今之视昔”——这份清醒而沉静的回望，或许，那才是我们心中真正的盛唐。</description></item><item><title>个人作品</title><link>https://blog.yuanpei.me/works/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/works/</guid><description/></item><item><title>听歌</title><link>https://blog.yuanpei.me/musics/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/musics/</guid><description/></item><item><title>站点统计</title><link>https://blog.yuanpei.me/statics/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/statics/</guid><description/></item><item><title>观影</title><link>https://blog.yuanpei.me/movies/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/movies/</guid><description/></item><item><title>读书</title><link>https://blog.yuanpei.me/books/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/books/</guid><description/></item><item><title>微博 × MCP：社交媒体新玩法解锁</title><link>https://blog.yuanpei.me/posts/mcp-server-weibo-beginners-journey/</link><pubDate>Sun, 15 Jun 2025 13:12:22 +0000</pubDate><guid>https://blog.yuanpei.me/posts/mcp-server-weibo-beginners-journey/</guid><description>去年，国外的 “毒舌” AI 应用 Twitter Personality 火爆一时。受其启发，博主跟风开发了一款类似的产品——微博性格报告，其核心功能是借助提示工程，从多个维度分析用户画像。从推特到微博，“数字世界的万花筒，每个社交平台都映照出你灵魂的不同切面”。我心目中的 AI，或许更像一面镜子，正试图从不同视角去观察人类的言行举止。然而，好景不长，后来微博上陆续出现了类似 “评论罗伯特”、“怼怼模拟器” 这类账号，因其学习成本更低、互动性更强，我的产品最终败北。再后来，该产品的代码变成了我 Agent 中的插件，而随着 MCP 协议的持续爆火，我终于将其以一个 MCP 服务器的形式再次推出，而这便是我今天想和大家分享的项目：mcp-server-weibo，这是一个基于 Model Context Protocol 的服务器，可以让大模型获取用户、微博、话题、评论等信息。
微博性格报告 @孙燕姿的微博性格报告 核心功能 作为一款针对微博的 MCP 服务器，目前 mcp-server-weibo 提供了下面 7 个工具：
名称 描述 备注 search_users 搜索微博用户 使用关键词搜索，可设置 limit 参数 get_profile 获取用户信息 使用用户唯一标识 uid get_feeds 获取用户动态 使用用户唯一标识 uid，可设置 limit 参数 get_trendings 获取微博热搜 可设置 limit 参数 search_content 搜索微博内容 使用关键词搜索，可设置 limit 和 page 参数 search_topics 搜索微博话题 使用关键词搜索，可设置 limit 和 page 参数 get_comments 获取指定微博下的评论 使用动态唯一标识 feed_id, 可设置 page 参数 使用方法 mcp-server-weibo 支持 stdio 和 streamable-http，可在以下支持 MCP 协议的客户端中使用：VS Code、Cursor、Windsurf、Cherry Studio、ChatWise、Claude Desktop 等等。使用方法如下：</description></item><item><title>四点钟海棠花未眠</title><link>https://blog.yuanpei.me/posts/four-o-clock-hibiscus-awake/</link><pubDate>Fri, 18 Apr 2025 13:12:22 +0000</pubDate><guid>https://blog.yuanpei.me/posts/four-o-clock-hibiscus-awake/</guid><description>无论你内心期盼过多少次的春和景明，夏天终归如约而至，春天则不情愿地悄然谢幕。虽未至立夏，可空调、风扇的争相开启，无一不在昭示着春天的形同虚设。西安的春秋季节向来短暂，一场清明时节的沙尘暴，便轻易地带走了我们对春天的全部幻想。踏青赏花、旅行露营，世间美好与你环环相扣。遥想当年，魏晋风流人物，于兰亭集会，引以为流觞曲水，想来不会比此刻更添雅致。可惜，“向之所欣，俯仰之间，已为陈迹”，今人与古人的感慨，竟是如此的不谋而合。第一次读到川端康成那句“凌晨四点钟，看到海棠花未眠”，令我心动的是这个意象中蕴藏着的深邃美感。然而，当我有一天站在真正的海棠花前却认不出它时，我终于意识到自己的浅薄。在一个同样无眠的夜晚，我决定写一篇文章，借以描摹我此刻的心境，以及这一个月以来关于 AI 协同、日本战国历史、 Agent 设计的种种心绪。
新主题 如你所见，我给博客更换了一个新主题，该主题移植自 imsyy 的 vitepress-theme-curve。至此，使用了三年多的 hugo-theme-stack 主题正式宣告退役。当然，我想表达的是，这个移植工作是由我和 Cursor 一起完成的。作为一名后端工程师，当我从 Hexo 切换到 Hugo 时，我曾经做过类似的尝试，可无一例外均以失败告终。所以，除了惊叹于 AI 带来的这种生产力的放大，我实在想不出还有什么更令人叹服的东西，以一言蔽之，接受 AI 比自己强大。
新版博客主题 有趣的地方在于，我前面三次尝试让 Cursor 完成这个任务，收获到的是不同程度的失败。例如，移植后主题运行报错、移植后主题没有遵从原主题的结构或样式等，哪怕我使用的是 Cursor 的 Agent 模式。直到第四次的时候，一切开始步入正轨。最终，它变成了你现在看到的这个新主题。如果说第四次尝试有什么魔法的话，那一定是我的脑海中有了一个更详细的移植计划，我开始主导我和 AI 整个会话过程，而不是像一个新手或者小白一样狂按 Tab 键。
Git 见证了主题的移植过程 首先，一个博客至少需要两种布局，即：文章列表、文章详情。基于这个认知，我开始尝试将原主题中的 Vue 组件与 Hugo 中的 list.html 以及 single.html 对应起来。这种对应并非简单地将其添加到 Cursor 的上下文中，而是提前建好基本的目录结构、同时在提示词里面写清楚相对路径。对于页面中的子组件，我允许 Cursor 先创建一个空白的文件充当占位符。其次，在和 AI 互动的过程中，对于可工作的产物需要及时提交到 Git 中，从而避免程序被 AI 改坏。我认为，这是一个非常重要的技巧，敏捷开发中“可工作的软件”这一理念，得以在这一刻被具象化。
全新的书音影页面 如今，LLM 和人类的注意力都非常宝贵，在和 Cursor 交互的过程中有意识地控制上下文的范围非常重要。正如我们人与人之间交流，上下文并不是越多越好，任何时候你都应该注意，不向对方提供对方不需要的上下文。诚然。Cursor 中的 Codebase 可以自动索引不同的文件，可如果你有机会缩小检索范围，何乐而不为呢？难道大海捞针是一件值得推崇的事情吗？在移植了主要的布局、样式以后，我开始逐个移植其中的子组件，于我个人而言，这其实是一堂面向大模型的沟通课，相比于人类的言不由衷，同大模型交流需要的是足够的真诚，可对人类而言这恰恰是一种稀缺资源。雷总说，“真诚是必杀技”，可我们好像更喜欢不好好说话。情绪价值充满字里行间的时候，信息密度就会低到令人发指，低幼化的语言表达，犹如微小剂量的砷，无形中变成了我们和 LLM 之间的沟通障碍。
15. 言语犹如微小剂量的砷 随着模型能力的不断升级，现在人们好像随便输入一通，就能得到非常不错的结果。于是，有人开始说，提示工程不存在了，甚至连 Agent 都不存在了。可我始终相信一个经典的理论：Garbage In，Garbage Out，向对方清楚地表达出你的意愿，这并不是什么高标准，而是一个基本要求。可惜，在人类世界里待久了，有喜欢揣摩潜台词的人，就有喜欢模棱两可的人。我始终认为，人类和 AI 在未来应该是一种平等的、合作的关系，我们不该在这种关系中代入类似上下级、老板/员工这样的不对等的关系。要求别人或者 AI 正确地揣摩你的意图，这并不是 NLP 技术或者情感陪伴类产品中的刚需，而是一种隐形的情绪霸凌，因为你本可以选择更清晰的表达方式，可你偏偏没有这样做。</description></item><item><title>留言</title><link>https://blog.yuanpei.me/comments/</link><pubDate>Sun, 06 Apr 2025 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/comments/</guid><description>见字如面。
这里是「元视角」，一个分享技术见解与生活感悟的地方，很高兴你愿意在此停留片刻。
如果某篇文章对你有所启发，或是你有不同的想法想交流，欢迎在下方留下你的足迹。每一封信，我都会认真阅读。
期待与你的相遇，期待与你展开一场愉快的对话！</description></item><item><title>Semantic Kernel × MCP：智能体的上下文增强探索</title><link>https://blog.yuanpei.me/posts/semantic-kernel-mcp-agent-context-enhanced-exploration/</link><pubDate>Sun, 09 Mar 2025 20:42:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/semantic-kernel-mcp-agent-context-enhanced-exploration/</guid><description>时光飞逝，转眼间已步入阳春三月，可我却迟迟未曾动笔写下 2025 年的第一篇 AI 博客。不知大家心中作何感想，从年初 DeepSeek 的爆火出圈，到近期 Manus 的刷屏热议，AI 领域的发展可谓是日新月异。例如，DeepSeek R1 的出现，让人们开始接受慢思考，可我们同样注意到，OpenAI 的 Deep Research 选择了一条和 R1 截然不同的路线，模型与智能体之间的界限开始变得模糊。对于这一点，使用过 Cursor Composer 或者 Deep Research 的朋友，相信你们会有更深刻的感悟。有人说，Agent 会成为 2025 年的 AI 主旋律。我不知道大家是否清楚 AutoGPT 与 Manus 的差别，对我个人而言，最重要的事情是在喧嚣过后找到 “值得亲手去做的事情”。所以，今天这篇博客，我想分享一个 “熟悉而陌生” 的东西：MCP，即：模型上下文协议，并尝试将这个协议和 Semantic Kernel 连接起来。
MCP 介绍 [TL;DR] MCP 是由 Anthropic 设计的开放协议，其定位类似于 AI 领域的 USB 接口，旨在通过统一接口解决大模型连接不同数据源和工具的问题。该协议通过 JSON-RPC 规范定义了 Prompt 管理、资源访问和工具调用三大核心能力，使得任何支持 Function Calling 的模型都能无缝对接外部系统，从而帮助大语言模型实现 “万物互联”。
什么是 MCP? MCP（Model Context Protocol）是由 Anthropic 设计的一种开放协议，旨在标准化应用程序向大语言模型（LLMs）提供上下文的方式，使大模型能够以统一的方法连接各种数据源和工具。你可以将其理解为 AI 应用的 USB 接口，为 AI 模型连接到不同的数据源和工具提供了标准化的方法。架构设计上，MCP 采用了经典的 C/S 架构，客户端可以使用该协议灵活地连接多个 MCP Server，从而获取丰富的数据和功能支持，如下图所示：</description></item><item><title>命运、偏见与自由：《魔童之哪吒闹海》的终极抗争</title><link>https://blog.yuanpei.me/posts/ne-zha-2/</link><pubDate>Thu, 06 Feb 2025 11:17:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/ne-zha-2/</guid><description>在金庸先生的《射雕英雄传》中，有一个情节令人难以忘怀。一向被视为离经叛道的“东邪”黄药师，在得知女儿黄蓉对郭靖情根深种时，亦不免喟然长叹：“且夫天地为炉兮，造化为工；阴阳为炭兮，万物为铜”。贾谊的郁郁而终，靖蓉的长岭遇雨，在遥远的历史的长河中千回百转，可谓“不谋而遐迩自同”。或许，无论是事业与爱情，其底色都难免带着些许苦涩，否则，天地何以成为熔炉，让万物在其中备受煎熬。当然，今天我想聊的不是金庸，而是春节档票房冠军《哪吒之魔童闹海》。这部改编自中国古代神话的动画电影，融合传统神话与现代叙事，在冰与火交织的冲突下，在大银幕上演绎了一部普罗米修斯式的抗争史，带给观众一场视觉与心灵的双重震撼。
当哪吒不再困扰于魔丸身份 从身份认同到体系反思 如果说，《哪吒》系列第一部《魔童降世》的核心冲突源于身份认同，那么，这一次《魔童闹海》开始将视野拓展至对体系与秩序的思考。帕拉图曾提出“人生三问”——我是谁？我从哪里来？我要到哪里去？哪吒曾为“魔丸”和“灵珠”的身份困扰，然而，在其父母的影响下，他坚定地喊出：“我命由我不由天”，是魔是仙要由自己说了算，表达对命运的不屈抗争。他完全不在意世俗的眼光，毅然决然地等待天雷落下。如果用一句话概括：“若命运不公，就和它斗到底”，我个人认为，这句话是《哪吒》系列最好的注脚。剧情上，这部作品与前作一脉相承，哪吒和敖丙在天劫中大难不死，虽然肉身毁灭，可魂魄得以在七色宝莲中保全。当然，这一切的代价是太乙真人被夺去顶上三花。
闭嘴！你这只愚蠢的土拨鼠 因此，第二部的故事便围绕着“劫后余生”展开。因为哪吒和敖丙的肉身损坏，所以，太乙真人借助七色宝莲和藕粉为二人重塑身体。然而，此时申公豹借四海龙王之力，引海底妖族攻入陈塘关。敖丙为护陈塘关擅自移动身体，导致塑造的肉身再次损坏。适逢七色宝莲灵力枯竭，为了得到能让七色宝莲再次盛开的玉液琼浆，哪吒和敖丙被迫共用一个身体，前往昆仑山玉虚宫升仙考核。这种剧情上的转折、递进，节奏紧凑，扣人心弦，可以说是全程无尿点。重塑身体与升仙考核两大情节，贡献了整部电影的大部分的笑点，特别是哪吒塑型这一步，简直就是现实中甲方乙方的真实写照。影片中中真正深刻的东西，我认为，直到陈塘关被屠城这一刻才开始逐渐呈现出来。
命运熔炉中的权力游戏 教员有首词叫做《贺新郎·读史》，其中有一句“铜铁炉中翻火焰，为问何时猜得”。众所周知，铜器时代和铁器时代是人类历史上的两个重要阶段，它们标志着人类在金属加工技术、社会结构和文明进步方面的巨大飞跃。结合第一部的剧情，哪吒的宿命，原本就开始于一场精心设计的“冶金游戏”，元始天尊将混元珠投入天元鼎，试图在一个相生相克的世界中提炼出永恒秩序。自此，被标记为“魔丸”的哪吒一生下来，便承受着世俗偏见带来“先天原罪”。如申公豹所言，“人心中的成见是一座大山”。龙族接受天庭“招安”，负责为其镇压海底妖兽，可这处名为“龙宫”的所在，又何尝不是关押龙族的“地牢”呢？因此，敖丙从一出生就被赋予了振兴龙族的使命，那件万龙甲便是最好的证明。
无量仙翁开始露出狐狸尾巴 电影中的偏见不止于此，当两只结界兽赶到玉虚宫报信时，有多少人想当然地认为，是申公豹屠戮了陈塘关的百姓呢？这同样是一种偏见。如果说，土坡鼠、申正道和石矶娘娘这三道试题，只是让你隐隐对阐教的正义性产生了一丝怀疑。那么，当巨大的天元鼎垂直插入东海海底的时候，你是否从无量仙翁那张道貌岸然的脸上，读出了某种献祭的意味呢？因为妖族修行比人类更勤勉，哪怕它们没做过坏事；因为龙族是实力强大的妖族，哪怕它们早已归顺；因为要嫁祸龙族而大举屠城，哪怕陈塘关的百姓无辜冤死。《贺新郎》里写道：“人世难逢开口笑，上疆场彼此弯弓月。流遍了，郊原血”。回顾历史，宏大叙事下的“电车难题”持续上演，十字军东征时的“上帝旨意”、殖民主义者的“文明开化”……，无一不是在用别人的血肉浇筑“封神”的圣坛。《封神演义》中阐截二教，到底孰是孰非呢？
自由与秩序的永恒斗争 诚然，电影依旧没能突破束缚，即：“高层隐身、中层搞事、底层遭殃”的叙事套路，甚至我们无从得知，无量仙翁的行为，到底是私心作祟下的自作主张，还是元始天尊的暗中授意。可这种“个体失范代替制度失范”的切入点，还是能给观众带来某种思考：从来如此，便对吗？善与恶，是魔是仙，从来都不应该由种族和身份决定，申公豹有自己的坚守与珍视，阐教有在封神大战前壮大实力的私心，敖光有保护龙族而委曲求全的无奈，在体系框架内循规蹈矩固然重要，可当个体困境变成群体困境、退无可退的时候，或许，跳出陈规、尝试打破规则，一切自然会柳暗花明、迎来转机。当哪吒的肉身在三昧真火中完成重塑、当人族和妖族齐心协力打破天元鼎的桎梏、当定海神针在落日下缓缓浮出海面……我想，哪吒撕裂的或许是人类文明进程中的永恒困境：在命运熔炉的烈焰中，我们到底是等待煅烧的铜铁，还是执掌锤柄的工匠？“怀璧其罪”、“欲加之罪”，本质上都是一种群体对个体道德上的绑架和霸凌。
哪吒浴火重生 古希腊哲人赫拉克利特曾说过：“世界是一团永恒的活火”。而哪吒的选择则昭示着：与其在烈焰中化为灰烬，不如让火焰成为涅槃的锤砧。我们无需过渡解读，说昆仑山的玉虚宫像白宫、天元鼎上的符文像美元、成仙的玉牌像绿卡、压住丹炉的像棵摇钱树，甚至无量仙翁、鹿童、鹤童比妖怪更像妖怪……当哪吒与敖丙联手击败无量仙翁，一切的建筑都灰飞烟灭，那个定义神仙妖魔的符号体系，更是被彻底解构、直至崩塌。在这部电影中，所有的海底妖兽都被用锁链束缚在定海神针上面。或许，只需阐教的一道咒语，便能解开这些枷锁。可要解开人心的锁链，显然要艰难百倍。对哪吒而言，是不再纠结于魔丸的身份；对敖丙而言，是不再背负振兴家族的使命。加缪在《反抗者》中写道：“我反抗，故我们存在”。影片最后，妖族与阐教的决战，难道不比《复仇者联盟》更燃？我相信，如果贾谊看过这部电影，一定不会如此草率地写下“天地为炉”。因为在自由面前，秩序终将被反抗者打破。</description></item><item><title>基于 K-Means 聚类分析实现人脸照片的快速分类</title><link>https://blog.yuanpei.me/posts/face-photo-fast-classification-using-k-means-clustering/</link><pubDate>Tue, 14 Jan 2025 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/face-photo-fast-classification-using-k-means-clustering/</guid><description>注：本文在创作过程中得到了 ChatGPT、DeepSeek、Kimi 的智能辅助支持，由作者本人完成最终审阅。
在 “视频是不能 P 的” 系列文章中，博主曾先后分享过人脸检测、人脸识别等相关主题的内容。今天，博主想和大家讨论的是人脸分类问题。你是否曾在人群中认错人，或是盯着熟人的照片却一时想不出对方的名字？这种 “脸盲症” 的困扰，不仅在生活中令人感到尴尬，在整理照片时更是让人头疼不已。想象一下，某次聚会结束后，你的手机里存了上百张照片——有你的笑脸、朋友的自拍，甚至还有一部分陌生面孔混杂其中。手动将这些照片按人物分类，不仅费时费力，还可能会因为 “脸盲” 而频繁出错。此时，你是否期待有一种技术，可以像魔法一样，自动将这些照片按人物分类？事实上，这种 “魔法” 已经存在，它的名字叫做 K-Means 聚类分析。作为一种经典的无监督学习算法，K-Means 能够通过分析人脸特征，自动将相似的面孔归类到一起，完全无需人工干预。接下来，为了彻底根治 “脸盲症”，我们将详细介绍如何使用 K-Means 聚类分析来实现这一目标，哈利·波特拥有魔法，而我们则拥有科技。
实现过程 如图所示，我们将按照下面的流程来达成 “自动分类人脸” 这一目标。其中，Dlib 负责提取人脸特征向量、Scikit-Learn 中的 K-Means 负责聚类分析、Matplotlib 负责结果的可视化：
基于 K-Means 聚类分析实现人脸照片的快速分类示意图 K-Means 简介 K-Means 是一种广泛应用的聚类算法，其基本原理是将数据集分成 K 个簇，目标是让每个簇内的数据点尽可能相似，而不同簇之间的数据点尽可能差异明显。K-Means 的执行过程如下：
随机选取 K 个初始中心点。
将每个数据点分配到距离最近的中心点所对应的簇。
更新每个簇的中心点，通常取簇内所有数据点的均值。
重复步骤 2 和 3，直到中心点不再发生变化或达到预设的最大迭代次数。
如下图所示，图中展示了四种不同的聚类数据分布情况，按照从左到右、自上而下的顺序：
图一：簇划分不正确或者簇数量假设错误 图二：数据分布具有各向异性，簇的形状是一个拉长的椭圆形，而不是对称的圆形 图三：各个簇之间的方差不同，绿色簇分布更紧密，而黄色簇分布更稀疏 图四：簇的大小不均匀，黄色簇数据点较少，而紫色簇数据点较多 四种不同的聚类数据分布情况 因此，适用于 K-Means 的数据通常满足：
簇是球状且分布均匀 簇的大小相近 簇无明显噪声点或者离群点 数据是各向同性分布 簇的数量已知 数据维度适中 如何确定 K 值 在使用 K-Means 之前，我们需要确定 K 值，即簇的数量。下面是三种常用的确定 K 值的方法：</description></item><item><title>容器技术驱动下的代码沙箱实践与思考</title><link>https://blog.yuanpei.me/posts/container-technology-driven-code-sandbox-practice-and-reflection/</link><pubDate>Mon, 28 Oct 2024 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/container-technology-driven-code-sandbox-practice-and-reflection/</guid><description>最近，我一直在尝试复刻 OpenAI 的 Canvas 功能，它与 Claude 的 Artifacts 功能非常相似。当然，两者在侧重点上有所不同——Artifacts 更注重于 “预览” 功能，而 Canvas 则专注于编程和写作领域。尽管 Artifacts 珠玉在前，可 Canvas 无疑为交互式体验带来更多可能性。对此，OpenAI 研究主管 Karina Nguyen 曾表示：我心目中的终极 AGI 界面是一张空白画布（Canvas）。在当前推崇 “慢思考” 的背景下，我有时会觉得下半年的大语言模型（LLM）发展 “不温不火”，给人一种即将停滞不前的的感觉。我想，这可能与四季更迭、万物枯荣的规律有关，正所谓 “环球同此凉热”。直到这两天，Claude 发布了 Computer Use，智谱发布了 AutoGLM，这个冬天再次变得热闹起来，为了不辜负这份幸运，我决定更新一篇博客，这次的主题是：容器技术驱动下的代码沙箱实践与思考。
LangChain 开源的 OpenCanvas 为什么需要代码解释器？ 在当前生成式 AI 的浪潮中，代码生成首当其冲，从 CodeGeex 到通义灵码，从 Github Copilot 到 Cursor，可谓是层出不穷，其交互方式亦从代码补全逐渐过渡到代码执行。你会注意到，在 OpenAI 的 Canvas 以及 Claude 的 Artifacts 中，都支持前端代码的实时预览，这意味着 AI 生成的不再是冷冰冰的代码，而是所见即所得的、可交互的成果。其实，早在 ChatGPT-3.5 中，OpenAI 就提供了 Code Interpreter 插件，可见让 AI 生成代码并执行代码的思路由来已久。究其本质，编程是一项持续改进的活动，必须根据反馈不断地完善代码。如果你使用过 Cursor 这个编辑器，相信你会对这一过程印象深刻，你可以实时地看到修改代码带来的变化，快速验证想法，加快调试和迭代的速度。毫无疑问，这种即时反馈的交互模式大大提高了编程的效率和趣味。
OpenAI 的 Canvas 功能 在实现 AI 智能体的过程中，我尝试为 Semantic Kernel 开发过一个 Code Interpreter 插件，我觉得这对于扩展（LLM）的能力边界意义重大。以 “9.</description></item><item><title>温故而知新：后端通用查询方案的再思考</title><link>https://blog.yuanpei.me/posts/review-and-rethink-backend-universal-query-solutions/</link><pubDate>Mon, 23 Sep 2024 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/review-and-rethink-backend-universal-query-solutions/</guid><description>最近，我一直在体验 Cursor 这款产品，与先前的 CodeGeex、通义灵码 等 “插件类” 产品相比，Cursor 在产品形态上更接近 Github Copilot。在多项测评中，Cursor 甚至一度超越了 Github Copilot。尽管我没有体验过 Github Copilot，但从用户体验的角度来看，Cursor 基于 VS Code 进行了深度定制。除了基础的代码自动补全功能外，它还可以允许你从原型图生成代码、将整个工程作为 Codebase、一键应用代码到本地。最令我印象深刻的是，它指导我完成了一个 Vue 的小项目，从零开始。诚然，“幻觉” 的存在让它在 Vue 2 和 Vue 3 之间反复横跳，其编程能力的提升主要得益于 Claude 3.5 系列模型，可我还是像《三体》中的杨冬一样感到震惊：物理学不存在了，那前端呢？有人说，程序员真正的护城河是沟通能力，因为执行层面的工作可以交给 AI。实际上，我并不担心 AI 取代人类，我更倾向于与 AI 沟通和合作，你可能想象不到，这篇文章中的思考正是来自于我和 Claude 老师的日常交流。
CRUD Boys 的日常 程序员普遍喜欢自嘲，以博主为例，作为一名后端工程师，我的日常工作主要就是 CRUD，因此，你可以叫我们 CRUD Boys。鲁迅先生曾作《自嘲》一诗，“破帽遮颜过闹市，漏船载酒泛中流”。面对软件世界里里的复杂性和不确定性，如果没有乐观的心态和耐心，哪怕是最基础的 CRUD，你不见得就能做到得心应手。你可能听说过这样一句话，“上岸第一剑，先斩意中人”，AI 领域的第一把火，永远烧向程序员自己，自打一众 AI 辅助编程工具问世以来，各种程序员被 AI 取代的声音不绝于耳，甚至 Cursor 可以在 45 分钟内让一个 8 岁小孩搭建出聊天网站，更不必说，在 OpenAI 发布全新的 o1 模型后，很多人觉得连提示工程、Agent 这些东西都不存在了。其实，代码生成、低代码/无代码相关的技术一直都存在，在很久以前，我们就在通过 T4 模板生成业务代码，自不必说各种代码生成器。截止到目前，Excel 依然是这个地球上最强大的低代码工具，可又有谁能掌握 Excel 的全部功能呢?
你猜用 Cursor 写一个这样的页面需要多久？ 退一步讲，即使的最简单的 CRUD，虽然业务的推进会不断地演化出新的问题。譬如，当你为了加快查询效率引入了缓存，你需要去解决数据库和缓存一致性、缓存失效等问题；当你发现数据库读/写不平衡引入读写分离、分库分表，你就需要去解决主从一致、分布式事务、跨库查询等问题；当你发现单点性能不足引入了多机器、多线程，你需要去解决负载均衡、线程同步等问题……单单一个查询就如此棘手，你还会觉得后端的 CRUD 简单吗？我承认，后端的确都是 CRUD，可在不同的维度上这些 CRUD 并不完全相同，譬如，分布式的相关算法如 Paxos、Raft 等，难道不是针对分布式环境中的节点做 CRUD 吗？可此时你还会觉得它简单吗？Cursor 的确可以帮你生成代码，但真正让它出圈的是背后的 Claude 模型。我始终相信某位前辈曾经讲过的话：“没有银弹”，在软件行业里，复杂度永远不会消失，它只会以一种新的方式出现。如果你觉得 CRUD 简单，或许是你从未接触过那些千姿百态的查询接口：</description></item><item><title>浅议 CancellationToken 在前后端协同取消场景中的应用</title><link>https://blog.yuanpei.me/posts/cancellation-mechanism-cancellationtoken-cooperative-scene/</link><pubDate>Thu, 15 Aug 2024 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/cancellation-mechanism-cancellationtoken-cooperative-scene/</guid><description>两个月前，我写过一篇题为为《关于 ChatGPT 的流式传输，你需要知道的一切》的文章。当时，我主要聚焦于 “流式传输” 这个概念。因此，Server-Sent Events、WebSocket 等技术，便顺理成章地成为了我的写作内容。然而，当我们将视野扩展到整个生成式 AI 领域时，我们会发现 “取消” 是一个非常普遍的业务场景。尽管我曾在这篇文章中提到了 AbortController 和 CancellationToken，但我并不认为我完全解决了当时的问题，即：如何让前、后端的取消动作真正地协同起来？言下之意，我希望前端的 AbortController 发起取消请求以后，后端的 CancellationToken 能够及时感知并响应这一变化。这一切就好比，AI 智能体固然可以通过 “观察” 来感知外部的变化，可当用户决定停止生成的时候，一切都应该戛然而止，无论你是不是为了节省那一点点 token。所以，当两个月前的子弹正中眉心时，我决定继续探讨这个话题。由此，便有了今天这篇稍显多余的博客。
前后端协同取消 我必须承认，在推崇前后端分离的当下，我这个想法难免显得不合时宜。可什么是合时宜呢？在刚刚落幕的巴黎奥运会上，35 岁的 “龙队” 马龙，斩获了个人第 6 枚奥运金牌。对此，这个被誉为 “六边形战士” 的男人表示，“只要心怀热爱，永远都是当打之年”。这是否说明，一切的不合时宜都是自我设限，而年龄不过是个数字。在以往的工作中，我接触的主要是 “Fire and Forget” 这类场景。特别是当一个任务相对短暂时，有没有真正地取消从来都不会成为讨论的重点。直到最近做 Agent 的时候，我发觉这一切其实可以做得更好，即便我的原动力是为了省钱。
async Task Main() { Console.WriteLine(&amp;#34;[HeartBeat] 服务运行中，请按 Ctrl + C 键取消...&amp;#34;); var cts = new CancellationTokenSource(); Console.CancelKeyPress += (sender, e) =&amp;gt; { e.Cancel = true; cts.Cancel(); }; try { await HeartBeatAsync(cts.Token); } catch (OperationCanceledException) { Console.</description></item><item><title>Semantic Kernel 视角下的 Text2SQL 实践与思考</title><link>https://blog.yuanpei.me/posts/semantic-kernel-driven-text2sql-practice/</link><pubDate>Mon, 15 Jul 2024 20:42:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/semantic-kernel-driven-text2sql-practice/</guid><description>《诗经》有言：七月流火，九月授衣，这句话常被用来描绘夏秋交替、天气由热转凉的季节变化。西安的雨季，自六月下旬悄然而至、连绵不绝，不由地令人感慨：古人诚不欺我。或许，七月注定是个多事之“秋”，前有萝卜快跑及其背后的无人驾驶引发热议，后有特朗普在宾夕法尼亚州竞选集会时遇刺，更遑论洞庭湖决口、西二环塌方。杨绛先生说，成长就是学会心平气和地去面对这世界的兵荒马乱，可真正的战争“俄乌冲突”至今已经持续800多天。有时候，我不免怀疑，历史可是被诅咒了的时间？两年前的此时此刻，日本前首相安倍晋三遇刺身亡，我专门写过一篇文章《杂感·七月寄望》 。现在，回想起两人长达19秒的史诗级握手画面，一时间居然有种“一笑泯恩仇”的错觉。因为，从某种意义上来说，他们似乎成为了共患难的“战友”。雍正之于万历，如同特朗普之于肯尼迪，虽时过境迁，而又似曾相识，大概世间万物总逃不出某种循环。最近一个月，从 RAG 到 Agent，再到微软 GraphRAG 的爆火，诸如 Graph、NER、知识图谱等知识点再次被激活。我突然觉得，我需要一篇文章来整理我当下的思绪。
实现 Agent 以后 参照复旦大学的 RAG 综述论文实现 Advance RAG 以后，我开始将目标转向 Agent。一般来说，一个 Agent 至少应该具备三种基本能力：规划(Planning)、记忆(Memory)以及工具使用(Tool Use)，即：Agent = LLM + Planning + Memory + Tool Use。如果说，使用工具是人类文明的起点，那么，Agent 则标志着大模型从 “说话” 进化到 “做事”。目前的 Agent 或者是说智能体，本质上都是将大模型视作数字大脑，通过反思、CoT、ReAct 等提示工程模拟人类思考过程，再通过任务规划、工具使用来扩展其能力的边界，使其能够感知和连接真实世界。从早期的 AutoGPT 到全球首个 AI 程序员智能体 Devin，人们对于 AI 的期望值，正肉眼可见地一路水涨船高。
Agent 的基本概念 目前，市场上主流新能源汽车的智驾系统都大多处于 L2 或 L3 级别，萝卜快跑则率迈进 L4 级别。尽管我可以理解这一发展趋势的必然性，可当我意识到碳基生命自身的偶然性，我想知道，那些可能导致成千上万的人失业的失业的科技创新，是否是显得过于残酷和冰冷？在2024年的上半年，我接触到了多种 Agent 产品，例如 FastGPT、Coze、Dify 等等。这些产品基本都是基于工作流编排的思路，这实际上是一种对大型模型不稳定输出和多轮对话调用成本的妥协。受到过往工作经历影响，我对于工作流和低代码非常反感。因此，我坚信大模型动态地规划和执行任务的能力才是未来。在实现 Agent 的过程中，我参考 Semantic Kernel 的一个 PR 实现了一个支持 ReAct 模式的 Planner，这证明了我从去年开始接触大型模型时的种种想法，到目前为止基本上都是正确的。
当下生成式 AI 的优化方向 我主张采用小模型结合插件的方式，推进 AI 服务的本地化，因为一味地追求参数规模或上下文长度，只会陷入永无休止的百模大战。在技术和成本之间，你必须要找到一个平衡点。例如，最近大火的 GraphRAG，知识图谱结合大模型的理念虽好，但构建知识图谱的成本相对较高，运行一个简单示例的费用大约在5到10美元左右。在实现 Agent 的过程中，我发现，使用阿里的 Qwen2-7B 模型完全可以支持任务规划以及参数提取，唯一的问题是 Ollama 推理速度较慢，尤其是在纯 CPU 推理的情况下。此外，目前的 Agent 的反思功能大多依赖于多轮对话，其效果易受上下文长度的影响。即便使用 OpenAI、Moonshot 等厂商的服务，它们的 TPM/RPM 通常不会太高，导致公共 API 难以满足 Agent 的运行需求。如果增加接口调用间隔，无疑又会让屏幕前的用户失去耐心。因此，即便是在 token 价格越来越便宜的情况下，以任务为导向的 Agent，其 token 消耗量依然是一笔不小的开销。</description></item><item><title>走走停停，允许一切发生</title><link>https://blog.yuanpei.me/posts/g-for-gap/</link><pubDate>Sun, 23 Jun 2024 20:42:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/g-for-gap/</guid><description>昨天，我去看了由胡歌和高圆圆主演的电影《走走停停》。当我踏入影院时，这部影片已上映了半月有余。在这个注意力稀缺的时代，观众总是被更新奇、更热闹的事物所吸引。这部自带文艺片底色的电影，让我在不经意间体验了一次 “私人影院”。当时，电影院里有且只有我一个观众。当电影中的桥段戳中我的笑点时，我总担心自己的笑声是否太过于放纵。事实上，我个人非常享受那一刻，甚至愿意将其视为 i 人的 “至爽时刻”。当思绪在戏里戏外游曳直至跳跃时，我的心情亦如同黑白照片、涓涓细流一般，于平淡之中泛起无穷回味。
私人影院 影片讲述了 “北漂” 中年人吴迪（胡歌饰），在职场和情场双双受挫后，选择回归故乡拍摄电影的故事。近年来，从 “逃离北上广” 到 “孔乙己的长衫” ，从 “35岁危机” 到 “Gap Year”，一个个热议话题此起彼伏。这些爆梗的流行背后，实则映射出人们心态上的种种变化。影片伊始，通过一系列闪回镜头，吴迪在故乡小镇的困窘扑面而来。按照 “三十而立” 的传统观念，吴迪妥妥是个大龄剩男，他不得不面对来自周围人的关于 “催婚”、“择业” 的各式嘈杂。在拒绝了父亲动用一切资源谋取的差事后，他选择再次回到自己的舒适区，甚至能在遇见老同学冯柳柳（高圆圆饰）时，坦然告诉对方自己正在 Gap 中、正在韬光养晦，哪怕彼时他正站在一群老年人中间排队领鸡蛋。
韬光养晦 如同平凡世界里的芸芸众生，吴迪在工作中颇具想法，自认有点小才华。当作品被人抄袭时，他并未未太过在意；当女友要和他分手时，他便打车送她离去。冯柳柳，一个向往大城市和大舞台的小记者，她决定为这位从北京归来的老同学拍摄纪录片。可镜头前的光鲜亮丽，实难掩饰吴迪内心的落寞，竟要靠小学时的奖杯来撑撑场面。冯柳柳觉得这样的纪录片不大真实，当她得知吴迪手上有个不错的剧本时，她鼓励吴迪将其拍出来，而她则负责记录整个电影拍摄的过程。自此，一系列啼笑皆非的故事，开始在吴家这间狭小的两居室内上演，哪怕这个剧组是个由业余爱好者组成的 “草台班子”，从导演、录音到女主角全都是新手不说，唯一的男主角则是个话剧团的冷门演员。
冯柳柳看剧本 “戏中戏” 是元电影的典型特征，在本片中你至少会看到四场戏。导演龙飞内心或许想拍摄严肃作品，影片中他曾向伍迪.艾伦和小津安二郎致敬，可面对市场他不得不将影片包装为戏剧电影，这构成第一层戏；吴迪眼中的《似是故人来》，是高雅艺术、是梦想，可在父亲眼中不过是老年人的 “出轨” 日常，是不可外扬的 “家丑”，这构成第二层戏；冯柳柳追求记录片的真实，可拍摄过程本就充满套路与技巧，特别是在经过上司的恶意剪辑后，真实与谎言的界限变得极为模糊，这构成第三层戏；吴迪的父母对彼此感情上的 “劣迹” 心知肚明，可是都没有互相戳破，依然在家人面前维持着这种体面，这构成第四层戏。生活或许如同《罗生门》一般，本就充满了表演的痕迹。
“草台班子” 从某种意义上说，吴迪一家人在人生观念上构成一条连续光谱。吴父站在光谱最左端，持悲观态度，认为人生变化无常，习惯成自然；吴母江美玲处于光谱中间地带，持中庸态度，她的人生虽有遗憾，可她愿意让儿子大胆尝试，坚定地支持儿子的想法；而吴迪站在光谱的末端，持乐观态度，外表或许平凡，内心却坚韧不拔，永不言败。在冯柳柳的镜头记录下，他曾装腔作势地表达道，“人不应该为了生活而毁掉生活的目的”。也许，他会偶尔记不起这句话到底出自哪位哲学家，是尼采还是海格尔？然而，生活总有拨云见日、柳暗花明的那一天，当困惑消散时，他总能在茶几上的《人生的智慧》中找到答案。电影拍摄成为母子间相互支持的桥梁，母亲以实际行动支持儿子的文艺创作，而吴迪则借助电影帮母亲实现艺术梦想。也许，去河边拍戏的那一天，正是她生命里最快乐的一天呢！
现实中的岳红是个抗癌英雄 不同于《水浒传》中逆来顺受的林冲，吴父在生活中常因情绪激动而掀桌子，尽管每次都被妻子和女儿提前预判。或许，母亲江美玲更像是这个家庭的粘合剂，以她的温柔和细腻将家庭成员紧密相连。吴迪母亲制作做绿豆汤场景，总让我联想到是枝裕和的电影《步履不停》，同样是一对母子，儿子埋怨母亲为什么不买点好的冰淇凌，母亲则微笑着拿出两支金属勺子……当吴迪的电影拍到一半的时，母亲江美玲因为心脏病突发意外去世，一切似乎都戛然而止。不知过了多久，当吴家父子看着电影镜头里的江美玲时，他们决定给《似是故人来》这部电影一个结局，因为他们都知道，江美玲最不喜欢半途而废。我认为，《走走停停》的精妙之处在于它的克制，它没有国产电影惯用的煽情和俗套，就是将一个故事故事娓娓道来。生活可不就是这样，哪来那么多戏剧性的反转呢？
吴迪会一直开出租吗？ 直到故事落幕，吴迪和冯柳柳并没有因为所谓的 “套路” 而走到一起。当他们的车同时堵在高架上时，时而前行，时而驻足，时而超越他人，时而又被他人超越，这是走走停停的具象化。苏轼说：“人生如逆旅，我亦是行人”，在这条单程的人生路上，走走停停才是常态，远比一味向前更贴近真实。在我看来，一个成年人最大的自律是允许一切发生，允许别人是别人，允许自己做自己。虽然制造羁绊、构建回忆是人类的普遍选择，可在命运的际遇里，相遇和分别不过是人生的常态。从这个角度来看，遇见是缘分，不遇见亦是缘分，只要那相逢曾短暂地照亮过彼此，便不虚此行。影片的彩蛋部分，江美玲因《似是故人来》获得最佳新人奖，而饰演她的岳红则凭借《走走停停》获得最佳女配角，这无疑是演员与角色之间的相互成就。古语有云：“人生不相见，动如参与商”。回首向来，遗憾常用，成全难得。正如某档播客里说：结婚是为了幸福，离婚亦然，单身亦然。也许，唯有不执着，方能得始终。
提及苏轼这位历史人物，我最近一直在读李一冰的《苏东坡新传》。站在中年的视角，重新去审视苏轼的生平。我意识到，以前从诗词里解读出来的苏轼，着实显得肤浅和虚幻。我甚至觉得，林语堂先生笔下的苏东坡，其幽默与达观，可能更多的是作者自身的心理投射。苏轼自二十岁离开故乡四川眉山赴京赶考，往后余生其足迹遍布东南，用 “走走停停” 来形容他再合适不过。越是深入了解历史，我越发觉得，李白的浪漫、白乐天的疯魔、苏东坡的豁达，或许都只是我们的一厢情愿。毕竟，我们根本无法想象，不到四十岁的苏轼在黄州狩猎时，开始自称老夫；那些意气风发的诗篇，如《赤壁赋》、《念奴娇·赤壁怀古》，偏偏是创作于他被贬黄州、内心苦闷忧郁之际。
东坡足迹图 读书时，我曾以 “苏轼集儒释道三家于一身” 之言， 语惊四座。可如今看来，苏轼本人或许并不乐于背负如此众多的信仰标签。作为一个四川眉山人，他的足迹遍布于天下，虽求佛问道，却难以寻觅到真正的答案，不得不以豁达、潇洒示人。时过境迁，无论是王安石还是司马光，都与苏轼渐行渐远。到了公元 1086 年，这两位因变法而水火不容的人物均已离世，只有苏轼还奔波在贬谪的路上。或许，像苏轼这样性情中人，秉持正义，本来就不该深陷宦海浮沉，可回顾苏轼在凤翔、杭州等地的政绩，可知他确实曾为民众福祉竭尽心力。苏轼政治生涯中的坎坷波折，宋慈欲澄清寰宇而不得的疲惫，大抵是殊途同归。如今，苏堤与西湖因苏轼而名声远扬，这何尝不是一种慰藉呢？</description></item><item><title>关于 ChatGPT 的流式传输，你需要知道的一切</title><link>https://blog.yuanpei.me/posts/everything-you-need-to-know-about-streaming-with-chatgpt/</link><pubDate>Thu, 06 Jun 2024 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/everything-you-need-to-know-about-streaming-with-chatgpt/</guid><description>当提及 ChatGPT 等生成式 AI 产品时，大家第一时间想到的是什么？对博主而言，印象最为深刻的是其流式输出效果，宛如打字机一般流畅。相信大家都注意到了，我给博客增加了 AI 摘要功能。虽然，这是一个非常“鸡肋”的功能，可是在光标闪烁的一刹那，我居然产生了一种“对方正在输入”的莫名期待。然而，此时此刻，理性会告诉我们：ChatGPT 的流式输出并不是为了让 AI 更“像”人类，它本质上是一种减少用户等待时长的优化策略。相比于人类的闪烁其词，心直口快或许更接近 AI 的真实想法。图灵测试，是一种用于判定机器是否具有智能的测试方法，其核心在于：如果程序表现出的行为与人类相似，我们便认为它具备了智能。当然，人机的不可区分性，同样带来了心理、伦理和法律上的问题。这便引出一个问题：人工智能，是否真的有必要像人类一样？有没有一种可能，让人工智能不那么地像人类，这反而是一种更加明智的做法？带着种种疑问，博主酝酿出了这篇文章，关于 ChatGPT 的流式传输，你需要知道的一切都在这里。从这一刻开始，“Attention Is All You Need”！
Server-Sent Events 目前，在众多生成式 AI 产品中，对话框依然是最普遍的产品形态。因此，当你准备开发一款 AI 应用时，实现“流式传输”功能是基本要求。正如矛盾先生所言，“模仿是创造的第一步”，所以，让我们先来看看 ChatGPT 是如何实现这个功能的。ChatGPT 早期使用的是 Server-Sent Events 技术来实现流式传输。然而，截止到博主写作这篇文章时，ChatGPT 中流式传输的实现已升级为 WebSocket。不过，这个话题还是值得探讨一下的，因为市面上依然有大量的项目在使用这个技术，我们姑且将其理解为，一笔由 OpenAI 引领而产生的技术债务。关于 Server-Sent Events 的基本概念，大家可以参考博主以前的博客 基于 Server-Sent Events 实现服务端消息推送：
Server-Sent Events 基本原理示意图 下面，我们以 Kimi 为例来进行说明。通过观察浏览器的请求过程，足以一窥 Server-Sent Events 的个中奥妙。
Kimi 在浏览器中的请求过程 - A 首先，Server-Sent Events 是基于 HTTP 协议的，其响应结果中的 Content-Type 取值为 text/event-stream。
Kimi 在浏览器中的请求过程 - B 其次，Server-Sent Events 以事件流的形式向客户端返回数据，这些数据放在 Data 字段中。此时，客户端只需要从 Data 字段中提取内容，再将其显示到界面上即可，这样便可以快速地实现流式输出效果。按照这个思路，我们可以提供一个简单的实现，如下面的代码片段所示：</description></item><item><title>从抖音看见世界的参差</title><link>https://blog.yuanpei.me/posts/seeing-the-world-through-tiktok/</link><pubDate>Thu, 16 May 2024 11:17:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/seeing-the-world-through-tiktok/</guid><description>昨晚，洗漱完毕后，我躺在床上刷着抖音。可在连续刷了十几条短视频以后，我突然对眼前的一切感到深深的绝望。或许是高德地图在抖音购买了推广，我的耳边一直充斥着同一条文案：我可以死在去远方的路上，但不能没去过远方。我心想，五一假期刚刚过去两周，资本真的需要在这个时间点，再次点燃人们“仗剑走天涯”的梦想吗？仔细一看，原来是高德地图新上线了一个“点亮城市”的功能，可以显示用户去过那些城市。Payne 说，一切能被拿来攀比的终将被拿来攀比。于是，在马头琴那深沉而悠扬的旋律衬托下，《边境》这首歌一旦响起，甚至比许巍的《蓝莲花》更具杀伤力。看着由大数据量产而来的、模式化的文案/配乐，我不免惶恐。究竟是从什么时候开始，数字幽灵夺舍碳基生命。一眼望去，互联网上充斥着各种电子压缩饼干，它们试图将日常生活高度提纯、浓缩、收敛，并使你相信那就是真实的生活。听起来像是硅基生命要统治地球？我想说的是，在 GPT4 以及未来的 AIGC 面前，这不过是一种迷人的 tricks。相比于被一小部分人类愚弄，我宁愿被 AI 愚弄，因为它真的要比我聪明许多。对此，我心悦诚服。
如何杀掉一只羊？并生活在真实之中 李诞有一期播客，标题叫做《如何杀掉一只羊？并生活在真实之中》，他以一个内蒙古人的视角，讲述了真实的内蒙古是如何杀掉一只羊，以及真实的内蒙古是什么样子。你或许听说过，下面这些关于内蒙古的刻板印象，比如：住在蒙古包里、高考射箭骑马、人均摔跤能手等等。如果现在给你两个选项，一个是“虚假但快乐”，一个是“真实且复杂”，你会怎么选呢？对此，李诞给出的答案是：让迪士尼成为迪士尼，让内蒙古成为内蒙古。我有时觉得，一生都特别“关键”的中国人，或许连整个人生都是打卡式的。君不见，小红书的打卡基本等同于去过一个地方，大家使用着别人使用过的文案，站在别人站过的打卡点，做着别人做过的事情，然后拍照发图。在种种确定性的背后，全然没有自由、随机的探索，更遑论深入理解。再比如大家喜闻乐见的爬山，现在全都是统一的“登山杖十八式”造型，我实在想不明白，曾经引领过非主流文化的90后，以追求个性、与众不同为荣，何以如今变得千人一面、毫无新意呢？香港演员梁家辉曾被人称为“千面影帝”，他在电影中饰演各种类型的角色，从严肃的历史人物到喜剧角色，从黑帮老大到普通市民，他都能精准地把握角色特点。可如今的朋友圈、微博、小红书，更像是某种角色扮演游戏，批量生产着紫萱/紫霞。假使换一张脸上去，坦白讲又有什么区别呢？只要你愿意，想你的风可以吹到月球甚至火星。
虽然，我不明白这个姿势是什么意思，可大家都说帅，那就是真的帅 在接触 ChatGPT 这类 AI 产品以后，一个最为直观的感受是，当 AI 完全掌握了小红书这类文案的写作套路以后，它完全可以取代人类写出更加“爆款”的文案。在这种前提下，在一堆充斥着 Emoji 的文字中间，自诩为高等智慧生物的人类，其实早已处在一种非常尴尬的境地。试想一下，如果让 AI 学会那些在流传在社交网络上的打卡地、拍照姿势、服装道具，那么，在这场名为模仿、盲从、跟风的竞赛中，人类到底还有多少胜算？也许，我们都不愿意承认，我们本质上就是一群习惯于攀比和跟风的乌合之众。可你只要仔细听一听，便会发现这座以“个性化推荐”为主要卖点的信息茧房，正在极速地降低我们的表达能力。整个平台/互联网的话语空间正在一点点塌陷，我们每天面对的词库严重同质、扁平和局限，越来越多的词语正在被压缩和稀释。每一天，大家都是面对着同一套网络流行语词库，越来越习惯于接收低信息密度的内容。这看似是大大降低了理解成本，可你回想一下，我们每天摄入的信息当中，真正有用的又有多少呢？ 《年会不能停》这个电影上映以后，我的同事特别喜欢讲“对齐颗粒度”，我不知道这是好事还是坏事。“家人们”，我唯一知道的事情是，我们渐渐患上了“失语症”，回忆一下，形容某种食物特别好吃，除了“咔咔炫”、“嘎嘎香”，你还能想起什么？如果是我，我会讲“唇齿留香”、“大快朵颐”等等。正如你会更喜欢我叫你朋友，而不是“老铁”或是“亲”，扪心自问，你果真喜欢这种小红书式的表达方式吗？
一个擦得锃亮的鱼缸，虽然看起来漂亮精致，可惜触摸不到真实和丰富 就像每一个短视频，我现在听到的都是视频制作者的声嘶力竭和急功近利，大致的含义都是在表达：如果你不 XXX，那么你就会 YYY。这里的 XXX 和 YYY 大家可以自行代入具体场景。我想说的是，即便是为了刺激流量，其途径并非只有贩卖焦虑这一条路。人都想看到自己想看到的一切，这当然无可厚非。不过，在我心目中“真”显得更为重要，就像刘亦菲饰演的花木兰，只有在卸下伪装时、以女性真实面貌出现时，她才会意识到自己有多么强大。比如,在发朋友圈的问题上，有人觉得应该要多发朋友圈，因为朋友圈的访客是未来的自己；有人觉得应该要少发朋友圈，因为成熟的人不会有那么多情绪写在脸上。我的观点是：想发就发，不想发就不发。在这一期播客的评论区，有人将当下的互联网，比喻为“一个擦得锃亮的鱼缸”，虽然看起来漂亮精致，可惜触摸不到真实和丰富。我有个朋友，经常开着 N 倍速看 B 站，我不免怀疑这是否和打卡式旅游如出一辙。或许，现在的 AI 可以在数秒内阅读完一篇文章或者一个网页，可这些知识永远都属于 AI 而不属于你。出去旅行的时候，我总想着避开网红店的纷扰，可大众点评和小红书总是会将你带去那些地方。曾经引以为傲的推荐算法，可以做到个性化推荐、千人千面，何以如今大家的兴趣爱好、言行举止越来越趋向雷同？到底是我们在通过消费行为驯化数据，还是我们心甘情愿被资本和消费主义驯化？
虽然楚门离开了，可 “楚门的世界” 一直都在 某时某刻，我意识到，这个世界是立体的、复杂的，而社交媒体则试图将其平面化、简单化。我承认，信息在经过浓缩化、碎片化、符号化以后会更利于传播。但是，一个丰富、多元、复杂的世界，是不是愿意被简化、该不该被简化呢？我想，这是一个非常深刻的问题。按照小红书上的理论，旅行的第一个阶段是穿梭于各大热门城市之间，可时间久了以后便觉得哪里都一样。此时，便进入第二个阶段：徒步、雪山、潜水、无人区、极限运动等等，在将以上种种都体验过以后，突然开始怀念城市里的人来人往、花开花落。此时，便进入第三个阶段：学习文化知识、欣赏古建筑、了解城市历史/变迁等等。再往后，第四个阶段便是走出国门、看世界。虽然，这只是其中一个版本，但是这足以说明问题。那就是，我们固执地认为这个世界看一次就够了，有多少人会在旅行过后故地重游呢？以一种符号式的、标签式的方式认知这个世界是否可行呢？自然是可以的，只不过这多少有些肤浅！那么，如何摆脱信息茧房的诅咒呢？我个人认为，首先，是从被动接收转变为主动订阅，比如使用 RSS 来代替微信公众号；其次，尽可能读英文的或者第一手的资料，比如官方文档、论文，而不是经过咀嚼或者裁剪等二次加工的产物；然后，定期调整个人行为/使用习惯，避免落入推荐算法的陷阱。大模型是可以生成更多的信息，可如果我们失去了高密度信息的读取能力，这一切又有什么意义呢，难道我们一定要让自己活成 GDP 和点击率吗？</description></item><item><title>俯仰之间：五一小长假出行记</title><link>https://blog.yuanpei.me/posts/in-the-twinkling-of-an-eye/</link><pubDate>Fri, 10 May 2024 11:17:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/in-the-twinkling-of-an-eye/</guid><description>五一假期首日，我突发奇想，决定前往某个南方城市。因为，在我屈指可数的旅行经历中，唯一去过的南方城市有且只有上海。离西安最近的“南方”自然是四川，因此，我规划了一条“广元—阆中”的旅行路线。或许，在大多数人眼中，只有成都能代表四川，可为了避开节假日的人潮汹涌，我选择探索这些小城市。阆中吸引我的地方，在于它是中国四大古城之一。自从年初游览过平遥古城以后，我对不同流派和风格的古建筑便产生了浓厚兴趣。阆中是一座极具三国特色的城市，刘备曾派遣张飞镇守此地达七年之久。对于历史爱好者来说，这简直是一种朝圣。若论广元的名胜古迹，剑阁无疑是最有名的地方。在各种短视频里，一句“老将军，可知天水姜伯约？”，让姜维和天水这座城市紧紧联系在一起，甚至连天水的有轨电车都以“伯约号”命名。然而，真正让姜维这个人物屹立起来，巍巍乎四百年大汉最后的脊梁，这个地方一定是剑阁。面对刘禅的投降，姜维曾悲愤地说：“臣等正欲死战，陛下何故先降?”。可冷静下来以后，他在密信中向刘禅表示：“愿陛下忍数日之辱，臣欲使社稷危而复安，日月幽而复明”。从天水到剑阁，你无法想象这份“孤忠”来自一个有羌人血统的异乡人。遗憾的是，五一假期并不浪漫，正当我沉浸在对历史的喟叹中无法自拔时，H 君告知我，阆中回西安的车票已售罄，这迫使我改变原来的计划。最终，我选择了“广元-汉中”的路线。幸运的是，这两个地方在某种程度上相互呼应着彼此，串联起从秦朝到唐朝近八百年的历史。
《三国演义》中的姜维形象 广元篇-故垒西边 广元的首站是皇泽寺，这是中国唯一一座供奉女皇帝武则天的祠庙。沿着依山而建的石阶向上攀登，从高处俯瞰，嘉陵江穿城而过的景象尽收眼底。或许是觉得三国年代久远、无从考据，广元将更多的曝光度留给了唐朝的武则天。因此在市区内，你可以看到则天路、则天路小学、则天小区等以武则天命名的地方。关于武则天的出生地，可谓是众说纷纭，莫衷一是。多年前我曾游览过洛阳的明堂天堂，如今再次听闻这位女皇帝的事迹，心中感慨不已。
黄泽寺 &amp;amp;amp; 大佛楼 黄泽寺石窟 提及武则天及其建立的武周政权，历史上评价褒贬不一。若单单从治国能力来看，武则天堪称“巾帼不让须眉”：在军事上，她破吐蕃、败突厥，设立北庭都护府，维护了国家统一与稳定；文化领域，她继承并发展科举制度，首创“殿试”，打破了关陇集团近百年来的垄断地位；经济上，她重视农业生产，实施屯田和奖励农桑政策，促进了唐朝经济的繁荣。后世对武则天的批评，主要集中在任用酷吏、政治清洗以及豢养面首。由此可见，在一个由男性主导的社会结构内，女性的政治参与及其自主和力量的表现，常常被视为“逾矩”。当无法否定一名女性在政治、社会和文化领域的贡献时，批评往往转向道德层面。例如，《资质通鉴》的作者司马光，便是从李唐正统和程朱理学的视角，对武则天以女性身份称帝进行批判。
皇泽寺上的嘉陵江 嘉陵江夜景 无可否认的是，武则天对打破封建礼教束缚以及解放女性方面产生了深远影响。如果以现代视角去审视武则天的一生，她无疑是在挑战传统史学中“红颜祸水”刻板印象。可有趣的地方在于，武则天并不符合女权主义者的定义，因为在她统治期间更多的是体现个人的政治成就，而非女性整体权力的提升。在武则天执政期间，她从未提倡或实施过女性平等的观念，其次行为和决策更符合当时的男性皇帝的行为模式。在广元的首日，细雨绵绵。当时，“胖猫”事件正成为网络热点，联想到江西那位“睁眼看世界”的觉醒姐，我忽然有种雾里看花的感觉。或许，如今的女性主义/女权/女拳，早已变得如这段历史般扑朔迷离。那些高呼着“Girls Help Girls”口号的女性们，在得知武则天做出还政李唐的决定后，是否会数落起这位女皇帝的不是，指责她为什么不让儿子李显改姓武？
千佛崖石窟 千佛崖上的嘉陵江 听起来似乎荒诞不经，可荒诞之处又何至于此呢？在沸沸扬扬的“胖猫”事件中，不管是当事人还是旁观者，都以一种近乎癫狂的姿态参与其中。陈奕迅在《六月飞霜》中唱道：“最可笑的，喊亦正常；最悲壮的，笑亦正常”，这让我不禁思考，“哪一个可，发育正常”。广元的第二站是昭化古城，如果你对这个名字感到陌生，那么，葭萌关这个名字或许能瞬间唤起你的三国记忆，《三国演义》中著名的“张飞挑灯夜战马超”，正是发生在这里。再往前追溯五百多年，秦惠文王嬴驷派张仪、司马错伐蜀，同样在葭萌关发生过激战。现存的昭化古城是明代重建的，当我漫步在狭窄的古城街道上，我不断地将眼前的景象与心中的三国景象相比较。往事越千年，一切早已面目全非，昔日的年金戈铁马早已无处寻觅。这座在今天看来颇为逼仄的城池，何以能成为历史上兵家必争的战略要地？
昭化古城-葭萌 昭化古城-云雾缭绕 葭萌关在传统意义上指的是昭化古城的西门，即临清门。临清门附近有敬候祠，那里安葬着蜀汉的第三位丞相费祎。诸葛亮在《出师表》中称赞他“志虑忠纯”。站在祠前远望，群山环抱，刘备曾评价此地为“弹丸之城，金汤之固”。从地理上看，广元位于四川最北端，是汉中通往蜀地的必经之路，葭萌关恰好处于广元与剑门关之间，是进入剑门关的咽喉要道，难怪张鲁要派马超来攻打葭萌关。一千多年后，我们从陕西进入四川，基本上与当年钟会、邓艾灭蜀的路线吻合。遥想当年，姜维面对钟会、邓艾、诸葛绪三路大军的攻势，以神来之笔巧妙夺取阴平桥头，与钟会大军对峙于剑阁。这一神级操作，或许只有后来的教员四渡赤水出奇兵能与之相媲美。
昭化古城-敬候祠-费祎墓步道 昭化古城-费祎墓碑刻与姜维塑像 剑门关因李白的《蜀道难》而声名显赫，其地势险要，有“一夫当关，万夫莫开”的说法，这是姜维可以凭借 5 万兵马成功阻挠钟会的 15 万大军的关键。我相信，当你亲临剑门关前，你会深刻体会到其浑然天成的壮观。剑门关处于崇山峻岭之间，是从汉中到达成都的重要通道。两侧山峰耸立，地形上易守难攻。剑门关的地理优势，在一定程度上弥补了姜维兵力上的不足，在那个“兵马未动、粮草先行”的战争年代，姜维的策略是坚守到对方粮草消耗殆尽。可惜历史自有其进程，正当姜维和钟会在剑阁对峙的时候，邓艾偷渡阴平、翻越摩天岭、袭江油、破绵竹，剑锋直指成都。直到后主刘禅投降时，钟会依然没能突破剑阁防线。那一刻，我和伯约的心境无异，“臣等正欲死战，陛下何故先降?”。由于天气和时间限制，此次未能亲临剑门关，这无疑是一大遗憾。
剑门关关楼 汉中篇-克复中原 北方人常说，大海是他们的执念。于我而言，我更愿随遇而安：遇山则登山，遇海则观海。因为人一旦有了执念，便注定要变得痛苦。幸运的是，这次旅行遇见了两条江。嘉陵江之于广元，如同汉江之于汉中。它们各自从这两座城市中川流而过，连同那些绵延千年的历史和传说，一起汇入浩浩荡荡的长江。汉江，又称汉水，每当提及这个名字，我脑海中浮现的总是《三国演义》第七十一回的故事：占对山黄忠逸待劳，据汉水赵云寡胜众。我常常在想，赵云是否从韩信的“背水一战”中获得了灵感。或许，历史本就是循环往复的，正如“背水一战”的策略，同一时期的马谡和徐晃都曾经用过，可结果确是大相径庭。冥冥之中，好像有什么东西在牵动着我的情绪，即使如今的汉水边早已不复当年汉中之战的鼓角争鸣。这让我不禁想到了定军山、祁山、五丈原……这是我第一次感受到这种超越地理和时间的奇妙联系。《三国演义》里的故事从未像现在这样鲜活生动，我想，这或许就是历史的魅力所在。
天下武侯祠展览馆 马超墓祠 汉中之行，首站是勉县的诸葛古镇，那里坐落着武侯祠和马超墓祠。此前，我只知道成都有武侯祠，得知全国现存七座武侯祠，我颇感意外。可想到丞相在国人心目中的地位，我觉得这一切又相当的合理。勉县武侯祠，始建于蜀汉景耀六年(263年)，是所有武侯祠中建祠最早、且唯一由皇帝下昭修建的祠庙。武侯祠内红墙青瓦，漫步其间，每当凉风拂背而过，周围树叶便开始梭梭作响，不由得令人想起《三国演义》里武侯定军山显圣的故事。蜀汉建兴十二年(234年)，诸葛亮是在在北伐途中病逝五丈原(今陕西省宝鸡市岐山县)，后主按照丞相遗愿将其葬于定军山下。武侯墓距武侯祠车程大约 10 分钟车程，位于汉水之南、定军山麓。遥想当年，老将黄忠就是在定军山斩杀夏侯渊，彻底扭转了刘备在汉中之战中的不利局面。战略上，汉中的重要性丝毫不亚于广元，是通往关中地区的地理要冲。诸葛亮六出祁山，最接近长安的一次是陈仓(今陕西省宝鸡市)，“悠悠苍天，何薄于我”。或许，英雄注定是要壮志难酬的，翻开史书，里面写满了意难平。
武侯祠-天下第一流 武侯北伐示意图 武侯墓前的匾额上书“双桂流芳”四字，相传，墓前两棵千年桂树皆为三国时期所植。北周庾信在《枯树赋》中写道，“昔年移柳，依依汉南，今看摇落，凄怆江潭，树犹如此，人何以堪”。此情此景，如通年轻时的子弹正中眉心，我在心里不断重复着《兰亭集序》里的一句话，“俯仰之间，已为陈迹”。固然，武侯墓游客络绎不绝、门庭若市，而马超和费祎的坟墓则显得冷清寂寥。也许，人们都还记得那个白袍银甲的锦马超，曾在潼关前杀得曹操割须弃袍。可没有人知道，马超从归顺刘备到病逝，只有短短八年，享年四十七岁。苏东坡赤壁泛舟时曾感慨道：“酾酒临江，横槊赋诗，固一世之雄也，而今安在哉？”。作家当年明月在某次采访中提到，“所谓百年功名、千秋霸业、万古流芳，与一件事情相比，其实算不了什么。这件事情就是——用你喜欢的方式度过一生。”。我并非主张历史虚无主义，而是想说，那些你认为难以释怀的瞬间，在人类文明的长河中，甚至都不如一粒尘埃。人是可以放下某些执念，继续前行的，生命中并没有那么多非如此不可的选择。
武侯墓前 夜幕降临，灯火阑珊，我登上天汉楼，俯瞰天汉湿地公园。微风拂过，轻轻拨动屋檐下的铃铛，不时传来悦耳的声响。相传，当年曹操与刘备争夺汉中时，两军在汉水两岸对峙，曹操驻扎北岸，刘备则驻扎在南岸，双方僵持多日。后来，诸葛亮命人在曹军睡着以后，擂鼓呐喊、虚张声势，令本就多疑的曹操更加不安，担心刘备伺机偷袭。最终，曹操不得不退兵三十里以避其锋芒。可惜，辛弃疾笔下“天下英雄谁敌手”的曹刘，终究还是抵不过大浪淘沙，唯有脚下这条汉水，逝者如斯，不分昼夜。第二天，我游览了古汉台和拜将台。古汉台是刘邦被封为汉王时在汉中的行宫遗址，而拜将台则是韩信被刘邦拜为大将军时所筑的高台。世事浮沉，沧海桑田，汉家的旌旗不再随风飘扬。可在现场演员们的演绎下，刘邦与项羽“楚汉争霸”、韩信“明修栈道，暗度陈仓”、张骞“凿空西域”、诸葛亮“六出祁山”，一个个生动形象的历史人物，仿佛正在唤醒这片大地上沉睡的往事。
古汉台与拜将台 《汉颂》情景剧 李白曾言“蜀道之难，难于上青天”。古蜀道两千年来基本维持“北四南三”的格局。蜀道的北段主要集中在汉中，包括陈仓道、褒斜道、傥骆道和子午道。魏延的“子午谷奇谋”便是计划通过子午道奇袭长安。事实证明，这是一个非常冒险的军事行动计划。公元 232 年，魏国大将军曹真从子午道出兵伐蜀，适逢雨季，栈道为雨水冲刷断绝，行军一个月，结果只走了一半的路程，无功而返；东晋恒温两次经子午道出兵，一次伐蜀，一次北伐前秦，结果第一次重蹈曹真覆辙，第二次被前秦军队包围；明朝末年，高迎祥带领 5 万大军用时 15 天通过子午谷，结果在谷口遇到了孙传庭的 2 万伏兵；1936 年西安事变后，国民革命军第 51 师师长王耀武，打算从子午道进军西安勤王护驾，结果在子午谷中走了三天，因为水源和补给问题，最终无功而返，在谷中三天后撤回。历史的讽刺之处在于，同一计谋一旦被使用，便永远成阳谋。四个人、五次用兵，皆以失败而告终，唯独杨玉环“日啖荔枝三百颗”，取涪州荔枝，经子午谷至长安，三日可达，畅通无阻。
马伯庸的小说《长安的荔枝》即描绘了这一历史轶事，“一骑红尘妃子笑，无人知是荔枝来”，在李白眼中比登天还要难的蜀道，结果在给杨贵妃运输荔枝这件事情上“易如反掌”，不知李白闻此会作何感想呢？
古蜀道地形示意图 天汉楼夜景 结语 夜色深沉，汉水寂静，历史的回声在涟漪中渐渐消散。楚河汉界间的龙争虎斗、韩信的智勇双全、诸葛亮的克复中原、姜维的“一计害三贤”……这些曾经激荡人心的故事，如今都化作了风中的一缕缕低语，轻轻拂过这片古老的土地。或许，生活在这片古老土地上的我们，是通过倾听来了解过去和书写未来，我们倾听风中传来的古人的智慧，我们倾听大地诉说岁月的沉淀，我们是见证者、更是参与者，就像历史长河中的一滴水，虽然渺小、短暂，但我们亦是河流本身。诚然，一切都会尘归尘、土归土。可正是尘埃，构成了我们脚下这片土地的厚重与深邃，不是吗？</description></item><item><title>RAG 的是与非、Rewrite 和 Rerank</title><link>https://blog.yuanpei.me/posts/the-true-or-false-rewrite-rerank-of-rag/</link><pubDate>Fri, 26 Apr 2024 09:29:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/the-true-or-false-rewrite-rerank-of-rag/</guid><description>有时候，我觉得人类还真是种擅长画地为牢的动物，因为突然发现，当人们以文化/理念的名义形成团体/圈子的时候，其结局都不可避免地走向了筛选和区分的道路。或许，大家都不约而同地笃信，在成年人的世界里，那条不成文的社交潜规则——“只筛选不教育，只选择不改变”。与千百年前百家争鸣不同，团体/圈子间并不热衷于交流，倒像是一种标签化的分类方式，甚至是一种非黑即白的二元分类方式。比如，通常人们认为男性不能讨论女性主义，可我经常在女性主义视角下看到对男性的讨论。女性朋友们一致认为，女性种种不幸完全是由男性以及男性背后的父权造成的。于是，在小红书上打着不被定义的标签的女性们，自顾自地定义着别人。亦或者，在这个内卷的世界里，人们被互相定义、被资本定义、被用户画像定义、被美颜相机定义……这种种的定义，最终会成为我们所有人的宿命。鲁迅先生说，中国人的性情是喜欢调和折中的，对此我表示怀疑。因为，以如今的现状而言，中国人或许更喜欢玉石俱焚。在我看来，标签是定义、是附和、是选择，无论我们是否知晓，那条路是否能代表未来。
是非善恶 最近，Meta 发布了 Llama3，一时风光无二。微软不甘示弱，紧随其后发布了 Phi-3。曾经，我认为在小红书上检索信息比百度更高效，可当我批评完百度的竞价排名后，我发现小红书上的广告问题更严重，特别是 AI 的加入让这一问题愈发严重。回到 AI 话题，最近人们对于大模型的态度大致可以总结为：对 Llama3 和 Phi-3 寄予厚望，认为它们接近 GPT-4 的水平，而对 OpenAI 以及 GPT-5 的前景则持续看衰。我不太关心这些预期，我在意的是新模型发布以后，各路牛鬼蛇神都可以活跃起来。小红书上有一篇帖子提到，Llama3 的发布使得本地化 RAG 更有意义，并分享了一个使用 LlamaIndex 实现 RAG 的案例，随后是小红书上经典的套路：私信、拉群、发链接。我对帖子中的观点保留态度，因为 Llama3 作为大型模型，主要解决的是推理问题；而 RAG 是检索 + 生成的方案，其核心在于提高检索的召回率，即：问题与文本块之间的相关性。显然，无论 Llama3 是否发布，RAG 都能正常落地。大型模型的推理能力，影响的是最终的生成结果，而非检索的召回率。
最简单的 RAG 范式 故事的结局是我遭到了反驳，对方质疑我对 RAG 的理解，并建议我阅读她主页的某个帖子，据说是 RAG 论文作者在斯坦福的讲课内容。我原本是打算去学习的，可戏剧性的是，我被对方拉黑了。我还能再说什么呢？当然选择原谅对方。为了证明我对 RAG 的理解没有偏差，我决定分享我最近对于 Rewrite 和 Rerank 的体悟。我想明确指出的是，无需使用 Llama3，只要提升检索部分的召回率，RAG 方案完全可以实施。实际上，我们甚至都不需要 GPT-4 级别的模型，选择一个合适的小模型足矣。我意识到，我最大的错误在于，试图在一个以信息差为生意的人面前打破信息壁垒，帮助他人摆脱知识的诅咒。正如我之前所述，某些团体或圈子的目的并非促进信息流通和交流，而是为了向特定的人群提供通行证，以便在来来往往的人群中筛选和区分同类。或许，你会认为你已经筛选出你想要的人，但从更广阔的视角来看，这不过是另一种傲慢与偏见。当然，你们权利忽视这些问题，就像我不在乎周围环境如何一样。作为一个崇尚科学的人，我只关心真理，除非你的真理更为真实。
实现 Rewrite 在 RAG 的语境中，Rewrite 是重写或者改写的意思。此时，诸位或许会困惑，为什么需要对用户输入的问题进行二次加工呢？在程序员群体中，有一本非常经典的书 ——《提问的智慧》，其核心观点是：在技术的世界里，当你提出一个问题时，最终能否得到有用的答案，往往取决于你提问和追问的方式。以此作为类比，众所周知，人类的输入通常随性而模糊，特别是在使用自然语言作为交互媒介的时候。在这种情况下，大语言模型难以准确理解人类的真实意图。因此，就需要对用户的原始查询进行改写，通过生成多个语义相似但是表述不同的问题，来提高或增强检索的多样性和覆盖面。由于重写后的查询会变得更为具体，故而，Rewrite 在缩小检索范围、提高检索相关性方面有一定的优势。例如，下面的提示词实现了对用户输入的改写：
通过提示词实现 Rewrite 实际效果如何呢？我们可以分别在 Kimi 和 ChatGPT 中进行测试。如下图所示，左边为 Kimi，右边为 ChatGPT：</description></item><item><title>《你想活出怎样的人生》与宫崎骏的自我和解</title><link>https://blog.yuanpei.me/posts/the-boy-the-heron-the-self-reconciliation/</link><pubDate>Thu, 18 Apr 2024 09:29:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/the-boy-the-heron-the-self-reconciliation/</guid><description>对于宫崎骏老爷子的告别之作 ——《你想活出怎样的人生》，我内心果然还是充满了期待，即便这部电影自打上映以来便争议不断。那些或明或暗的隐喻、洗白军国主义的嫌疑、以及偏意识流的表达方式，让这部作品在大荧幕前有了更多悬念。作为一个不大迷信豆瓣评分的叛逆中年，我还是决定亲自去电影院看看。和五年前一个人去看《千与千寻》的重映一样，我选择了一个安静的周末，带着一杯咖啡，提前坐在座位上等待电影开始。我想，正如张驰在《飞驰人生2》里所说的那样，“有些事情是需要一个终点的”，这部电影可能是宫崎骏的最后一次创作。与雷军将造车视为最后一次创业不同，这个多次声称要 “封笔” 的老人，已经年过八十。故而，这件事情本身就充满一种 “烈士暮年” 的沧桑感， 不由得令人感慨。我想说的是，对我而言，这个终点是学会与自我和解。
如果单单从视觉观感上进行评价，《你想活出怎样的人生》是一部 100% 的宫崎骏式动画电影。其基本叙事结构都可以概括为：主人公因为某种原因来到了某个地方，然后因为某种机缘进入了异世界，主人公在异世界中冒险、成长，最终回归现实，一切戛然而止。这个故事听起来非常熟悉，没错，这正是《千与千寻》这部作品的主要情节。因此，当你再次审视《你想活出怎样的人生》这部作品时，你会发现两部电影的剧情走向完全一致。电影讲述了少年牧真人的母亲葬身火海后，他随父亲与继母组成新家庭。深陷于悲伤的真人阴内心封闭，难以融入新环境。一次意外，他跟随一只会说话的苍鹭闯入废弃的神秘塔楼，却不料进入了奇幻的 “亡灵世界”，自此开始了一场不可思议的冒险故事。那天，电影院里只坐了十来个人，如夜空中零散点缀着的寒星，戏里戏外，冒险各自开始。
电影中的苍鹭作为少年真人的引路人 在宫崎骏的诸多作品中，这部电影在表达上极其个人化。因而，网络上有一种声音，说这部电影并不是为观众而拍，它更像是宫崎骏对自己人生的一种总结。众所周知，宫崎骏对飞机有着特别的喜欢，这一点在《红猪》、《风之谷》、《天空之城》、《哈尔的移动城堡》等作品中均有所体现。了解过宫崎骏生平以后，你会发现他的家族经营着一家飞机工厂，生产用于战争的零式战斗机部件。宫崎骏从小对飞行器充满向往，可当他意识到战争带来的破坏时，他开始为家族的过去感到羞愧。所以，宫崎骏在作品中融入飞行元素的同时，一直倡导反对战争、尊重自然的理念。这种矛盾的心理，终于在《起风了》这部作品中被推向了一个高潮。如果说堀越二郎是宫崎骏飞行理想的化身，那么，这部电影或许更接近宫崎骏的自传。尽管电影中真人父亲的军工厂是以远景形式出现，但结合故事背景，可以推断出真人实际上是童年时期的宫崎骏。
火元素是动漫作品中经久不衰的存在 男主的名字牧真人寓意着 “成为一个真正的人”。然而，在母亲葬身火海以后，他陷入了前所未有的身份认同危机。他无法接受继母作为自己的母亲，在学校受排挤时，他甚至不惜自残使自己生一场大病。熟悉日本近代历史的人都知道，从 “黑船事件” 到 “明治维新” 再到 “屈原” ，日本同样经历过一系列的 “迷茫期”，即：我是谁？我在哪？我要干什么？姜文导演的《邪不压正》，主角李天然的行为可以概括为三个字：“找爸爸”，这同样是在探讨身份认同这一主题。在这样的背景下，一只会讲话的苍鹭出现在真人的视野中，并一步步地引导他走向了塔楼内的 “亡灵世界”。按照电影中的设定，真人的母亲虽然是死于战火，但她为了能再见儿子一面，只身前往 “亡灵世界” 并化身为元气少女 “火美” —— 一个可以驾驭火的巫女形象。在真人即将被鹦鹉士兵杀死并吃掉的时候，她如同《鬼灭之刃》中的炎柱炼狱杏寿郎一般照亮世界，救下了这个在异世界中跌跌撞撞的迷茫少年。该不该说，这一幕与《千与千寻》中的白龙照顾初来乍到的千寻极其相似？
如爱因斯坦一般的舅公形象设定 有人认为，故事中的舅公，代表了日本皇室等统治阶级，因为他们强调血统。电影中要成为舅公的继承人，必须拥有他的血脉。。鹦鹉代表军国主义/法西斯主义，其服饰具有普鲁士风格，鹦鹉大王的 “冲动” 导致异世界的崩塌，讽刺了那些为了宏大叙事而让这个世界频频陷入战火的野心家。电影中，一只鹈鹕在临死前向男主坦白，他们之所以要吃哇啦哇啦，是因为异世界中没有其他食物来源，这反映的是那些被裹挟进战争中的无辜者们。从古至今，帝王将天地视为棋盘，相互争斗，最终都是 “一将功成万骨枯”、“兴，百姓苦；亡，百姓苦”。异世界由积木搭建，积木来源于一块黑色的石头，结合舅公当时所处的年代，不难联想到这个 “黑色的石头”，本质上就是从西方驶来的 “黑船”。今天，我们说 “世界是个草台班子”，如果用积木来解构世界，或许就能理解这句话，理解 “万物为虚”。毕竟，人类自身的文明，何尝不是像电影中的积木一样脆弱不堪呢？可即便如此，人类依然在努力地为哇啦哇啦这样的新生命，争取一个更加光明的未来。换句话说，每一个人其实都应该成为自身文明的守护者。
鹦鹉大王的这两撇胡子非常得普鲁士 “亡灵世界” 是一个因战争而受诅咒的平行宇宙，其秩序被人类世界的战争扭曲。舅公，一个接触过西方思想的人，希望在这里建立一个没有战争的乌托邦。可随着时间的推移，这座理想国失去了平衡。占据着塔楼的鹦鹉群体，过着衣食无忧、战备充足的生活；而生活在地狱苦海中的鹈鹕则备受煎熬、食不果腹，不得不以哇啦哇啦为食。等到真人和火美找到舅公的时候，鹦鹉大王的武装势力已经威胁到了 “亡灵世界” 的统治。历史上，皇室被权臣架空的例子屡见不鲜，与其说是堆石成塔的积木被 “污染”，不如说鹦鹉们的内心早已被欲望和权利填满。故事接近尾声的时候，崇尚武力的鹦鹉大王一剑砍翻了石塔，导致异世界的坍塌，这是否呼应了新海诚作品中的 “伤痕文化”？在日本文化中，“侘寂” 一词代表不完美和残缺。在废墟上不断建立起新的家园，是人类千百年来往复循环着的宿命。
有人说，这一幕像极了波妞与宗介 最后，舅公给真人交代了一个任务：用13个尚未被污染的石块积木搭建一座牢固的房子，用善心创造一个没有恶意的世界。可当真人触摸到自己头上的那块伤疤时，他意识到，无论是被学校里的同学排挤，还是他近乎自残的行为，恶意在现实世界中一直存在。这一切就好像，“火美” 明明知道等待她的是炼狱、是死亡，可她必须回到那个世界，因为只有这样才能迎来真人的诞生。同样地，舅公曾用心勾画过的理想国固然是黄粱一梦，“到头来，宫阙万间都做了土”。这一切虽然是一场不折不扣的悲剧，可如果腐朽的旧秩序不曾毁灭，孕育希望的新世界又从何处诞生呢？异世界的斑驳陆离与现实世界的残酷荒诞相互映射，就像旅行归来后回到熟悉的地方。这或许是宫崎骏本人的 “心灵奇旅” 和 “寻梦环游记”，可它同样是宫崎骏留给我们的最后一道谜题，当你足够了解这个世界真实的面目，是否还有勇气继续生活？
异世界的本源——黑色石头 你想活出怎样的人生？这个问题对于真人而言，是身处乱世，如何学会当下痛苦和对亲人的执念，以及如何学会面对生命中的无常缺憾与生离死别。可对于屏幕前的你我来说，则是：如何去拥抱这个并不完美而又真实存在着的世界，学会与自我和解。宫崎骏身上体现了许多对立和矛盾的特质：他喜欢飞行技术，却难以接受家族参与战争的事实；他是一个反战主义者，却对军事和飞机充满兴趣；他是一个日本人，却对欧式建筑情有独钟；他讽刺统治者的剥削，可他本人却是出了名的工作狂和暴君。在内心深处的隐痛和分裂下，他一生都在纠结是 “承认” 还是 “怀疑”。从怀疑出发，试图建立乌托邦；最终接受现实，承认不完美。可以说，这宫崎骏最为私人、最深情的一次表达，是一次全面而系统的 “答记者问”。遗憾的是，或许是因为想要表达的东西太多，老爷子大开大合、恣意挥霍的叙事风格，无处不在的隐喻，都让整个故事显得晦涩而沉重，这个年逾八旬的老者，其表达地多少有点语焉不详。
抽象吗？确实听抽象的 请记住：你唯一所拥有的这段人生，唯一的选择便是抓住它，认真过下去。正如真人必须接受母亲的去世，接纳继母，才能迎来 “重生”。生命变幻无常，残酷而荒诞。有的人渐行渐远，而原本熟悉的开始变得陌生起来；有些东西一旦失去，就永远不会再回来，无论你有多么不舍或不甘；裂痕一旦出现，就永远无法再复原，无论你怀念的是过去还是此刻。回首往事，那些遥远而模糊的回忆，时常与错综复杂的现实纠缠在一起，令人真伪难辨。人生或许迷茫，历史或许重演，世界或许变质，可这世上只有一种英雄主义，那就是在认清生命的真相后，依然热爱生活。不要纠结、不要自证，接受一个事实并非耻辱。日本在二战中战败是事实，战后背靠美国获得经济飞速发展，这同样是事实。人类的一切建筑都是建立在某种废墟上面，而废墟同样作为新世界的地基。只要接受真实，回归本真，我们就能继续向前、过好现在的生活。起风了，唯有努力生存。走出电影院的那一刻，我下意识地摸了摸口袋里的电影票根，从某种意义上看，它难道不是我从电影的异世界里带出的积木或是护身符？虽然，从更宏大的人生尺度而言，我或许根本带不走任何东西。</description></item><item><title>使用 EFCore 和 PostgreSQL 实现向量存储及检索</title><link>https://blog.yuanpei.me/posts/use-efcore-with-postgresql-for-vector-storage-and-retrieval/</link><pubDate>Fri, 15 Mar 2024 21:34:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/use-efcore-with-postgresql-for-vector-storage-and-retrieval/</guid><description>随着 ChatGPT 的兴起及其背后的 AIGC 产业不断升温，向量数据库已成为备受业界瞩目的领域。FAISS、Milvus、Pinecone、Chroma、Qdrant 等产品层出不穷。市场调研公司 MarketsandMarkets 的数据显示，全球向量数据库市场规模预计将从 2020 年的 3.2 亿美元增长至 2025 年的 10.5 亿美元，年均复合增长率高达 26.8%。这表明向量数据库正从最初的不温不火逐步演变为大模型的 &amp;ldquo;超级大脑&amp;rdquo;。向量数据库，不仅解决了大模型在 &amp;ldquo;事实性&amp;rdquo; 和 &amp;ldquo;实时性&amp;rdquo; 方面的固有缺陷，还为企业重新定义了知识库管理方式。此外，与传统关系型数据库相比，向量数据库在处理大规模高维数据方面具有更高的查询效率和更强的处理能力。因此，向量数据库被认为是未来极具潜力的数据库产品。然而，面对非结构化数据的挑战，传统的关系型/非关系型数据库并未坐以待毙，开始支持向量数据库的特性，PostgrelSQL 就是其中的佼佼者。本文探讨的主题是：如何利用 PostgreSQL 实现向量检索以及全文检索。
从大模型的内卷说起 截止目前，OpenAI 官方支持的上下文长度上限为 128K，即 128000 个 token，这意味着它最多可支持约 64000 个汉字的内容。当然，如果考虑到输入、输出两部分的 token 消耗数量，这 64000 个汉字多少要大打折扣。除此以外，国外的 Claude 2、国内的 Moonshot AI，先后将上下文长度提升到 200K 量级，这似乎预示着大模型正在朝着 “更多参数” 和 “更长上下文” 两个方向“内卷”。众所周知的是，现阶段大模型的训练往往需要成百上千的显卡，不论是“更多参数”还是“更长上下文”，本质上都意味着成本增加，这一点，从 Kimi 近期的宕机事件就可以看出。
AI 眼中的显卡集群 所以，为什么说 RAG(Retrieval-Augmented Generation) 是目前最为经济的 AI 应用开发方向呢？因为它在通过外挂知识库 “丰富” 大模型的同时，能更好地适应当前 “上下文长度受限” 这一背景。诚然，如果有一天，随着技术的不断发展，芯片的价格可以变得低廉起来，大模型可以天然地支持更长的上下文长度，或许大家就不需要 RAG 了。可至少在 2024 年这个时间节点下，不管是企业还是个人，如果你更看重知识库私有化和数据安全，RAG 始终是绕不过去的一个点。同济大学在 Retrieval-Augmented Generation for Large Language Models: A Survey 这篇论文中提出了 RAG 的三种不同范式，如下图所示：</description></item><item><title>基于 LLaMA 和 LangChain 实践本地 AI 知识库</title><link>https://blog.yuanpei.me/posts/practice-local-ai-knowledg-base-based-on-llama-and-langchain/</link><pubDate>Thu, 29 Feb 2024 10:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/practice-local-ai-knowledg-base-based-on-llama-and-langchain/</guid><description>有时候，我难免不由地感慨，真实的人类世界，本就是一个巨大的娱乐圈，即使是在英雄辈出的 IT 行业。数日前，Google 正式对外发布了 Gemini 1.5 Pro，一个建立在 Transformer 和 MoE 架构上的多模态模型。可惜，这个被 Google 寄予厚望的产品并未激起多少水花，因为就在同一天 OpenAI 发布了 Sora，一个支持从文字生成视频的模型，可谓是一时风光无二。有人说，OpenAI 站在 Google 的肩膀上，用 Google 的技术疯狂刷屏。此中曲直，远非我等外人所能预也。我们唯一能确定的事情是，通用人工智能，即：AGI（Artificial General Intelligence）的实现，正在以肉眼可见的速度被缩短，以前在科幻电影中看到的种种场景，或许会比我们想象中来得更快一些。不过，等待 AGI 来临前的黑夜注定是漫长而孤寂的。在此期间，我们继续来探索 AI 应用落地的最佳实践，即：在成功部署本地 AI 大模型后，如何通过外挂知识库的方式为其 “注入” 新的知识。
从 RAG &amp;amp; GPTs 开始 在上一期博客中，博主曾经有一个困惑，那就是当前阶段 AI 应用的最佳实践到底是什么？站在 2023 年的时间节点上，博主曾经以为未来属于提示词工程(Prompt Engineering)，而站在 2024 年的时间节点上，博主认为 RAG &amp;amp; GPTs 在实践方面或许要略胜一筹。在过去的一年里，我们陆陆续续看到像 Prompt Heroes、PromptBase、AI Short&amp;hellip;等等这样的提示词网站出现，甚至提示词可以像商品一样进行交易。与此同时，随着 OpenAI GPT Store 的发布，我们仿佛可以看到一种 AI 应用商店的雏形。什么是 GPTs 呢？通常是指可以让使用者量身定做 AI 助理的工具。譬如，它允许用户上传资料来丰富 ChatGPT 的知识库，允许用户使用个性化的提示词来指导 ChatGPT 的行为，允许用户整合各项技能(搜索引擎、Web API、Function Calling)&amp;hellip;等等。我们在上一期博客中提到人工智能的 “安卓时刻”，一个重要的契机是目前产生了类似应用商店的 GPT Store，如下图所示：</description></item><item><title>使用 llama.cpp 在本地部署 AI 大模型的一次尝试</title><link>https://blog.yuanpei.me/posts/an-attempt-to-deploy-a-large-ai-model-locally-using-llama.cpp/</link><pubDate>Sun, 04 Feb 2024 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/an-attempt-to-deploy-a-large-ai-model-locally-using-llama.cpp/</guid><description>对于刚刚落下帷幕的2023年，人们曾经给予其高度评价——AIGC元年。随着 ChatGPT 的火爆出圈，大语言模型、AI 生成内容、多模态、提示词、量化&amp;hellip;等等名词开始相继频频出现在人们的视野当中，而在这场足以引发第四次工业革命的技术浪潮里，人们对于人工智能的态度，正从一开始的惊喜慢慢地变成担忧。因为 AI 在生成文字、代码、图像、音频和视频等方面的能力越来越强大，强大到需要 “冷门歌手” 孙燕姿亲自发文回应，强大到连山姆·奥特曼都被 OpenAI 解雇。在经历过 OpenAI 套壳、New Bing、GitHub Copilot 以及各式 AI 应用、各类大语言模型的持续轰炸后，我们终于迎来了人工智能的 “安卓时刻”，即除了 ChatGPT、Gemini 等专有模型以外，我们现在有更多的开源大模型可以选择。可这难免会让我们感到困惑，人工智能的尽头到底是什么呢？2013年的时候，我以为未来属于提示词工程(Prompt Engineering)，可后来好像是 RAG 以及 GPTs 更受欢迎？
从哪里开始 在经历过早期调用 OpenAI API 各种障碍后，我觉得大语言模型，最终还是需要回归到私有化部署这条路上来。毕竟，连最近新上市的手机都开始内置大语言模型了，我先后在手机上体验了有大语言模型加持的小爱同学，以及抖音的豆包，不能说体验有多好，可终归是聊胜于无。目前，整个人工智能领域大致可以分为三个层次，即：算力、模型和应用。其中，算力，本质上就是芯片，对大模型来说特指高性能显卡；模型，现在在 Hugging Face 可以找到各种开源的模型，即便可以节省训练模型的成本，可对这些模型的微调和改进依然是 “最后一公里” 的痛点；应用，目前 GPTs 极大地推动了各类 AI 应用的落地，而像 Poe 这类聚合式的 AI 应用功能要更强大一点。最终，我决定先在 CPU 环境下利用 llama.cpp 部署一个 AI 大模型，等打通上下游关节后，再考虑使用 GPU 环境实现最终落地。从头开始训练一个模型是不大现实的，可如果通过 LangChain 这类框架接入本地知识库还是有希望的。
编译 llama.cpp llama.cpp 是一个纯 C/C++ 实现的 LLaMA 模型推理工具，由于其具有极高的性能，因此，它可以同时在 GPU 和 CPU 环境下运行，这给了像博主这种寻常百姓可操作的空间。在 Meta 半开源了其 LLaMA 模型以后，斯坦福大学发布了其基于 LLaMA-7B 模型微调而来的模型 Alpaca，在开源社区的积极响应下，在 Hugging Face 上面相继衍生出了更多的基于 LLaMA 模型的模型，这意味着这些由 LLaMA 衍生而来的模型，都可以交给 llama.</description></item><item><title>如何为 Git 配置多个 SSH Key</title><link>https://blog.yuanpei.me/posts/how-to-configure-multiple-ssh-keys-for-git/</link><pubDate>Tue, 30 Jan 2024 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/how-to-configure-multiple-ssh-keys-for-git/</guid><description>在电视剧《繁花》里有这样一个情节，汪小姐和宝总在一起时喜欢吃排骨年糕，后来两人分道扬镳，汪小姐用 “从此想，排骨是排骨，年糕是年糕” 这句对白来概括两个人的关系。不得不说，这句伤感中带着点文艺的台词，在受到剧粉及书迷追捧的同时，更是戳中了无数吃货的心。排骨年糕好不好吃，我不晓得。我唯一知道的事情是，人们需要亲密关系，可人们同样需要界限和距离感，排骨和年糕，就像是工作和生活，当我们意识到 “工作是工作，生活是生活” 的时候，或许我们就能达到真正的 “Work-Life Balance”。那么，对于程序员来说，工作和生活的界限在哪里呢？我想，这一切或许可以从为 Git 配置多个 SSH Key 说起。
相信大家都会遇到这种场景，即一台电脑上同时存在多个 Git 账号的情况。譬如，公司的项目使用 Gitlab 托管，而个人的项目使用 Github 托管，更不必说，云效、Gitee、码云、Coding 等形形色色的平台。在这种情况下，你需要为每个代码托管平台生成 SSH Key，然后将其对应的公钥复制到指定的位置。所以，如何让这些不同托管平台的 SSH Key 和平共处、互不影响呢？这就是今天这篇文章想要分享的冷知识。当然，对博主个人而言，最主要的目的，还是希望能将公司和个人两个身份区分开来，所以，下面以 Github 和 Gitlab 为例来展示具体的配置过程。
生成 SSH Key 首先，为两个平台生成各自的 SSH Key，使用 ssh-keygen 命令即可:
ssh-keygen -t rsa -C &amp;#34;&amp;lt;公司邮箱&amp;gt;&amp;#34; -f ~/.ssh/company-ssh ssh-keygen -t rsa -C &amp;#34;&amp;lt;个人邮箱&amp;gt;&amp;#34; -f ~/.ssh/personal-ssh 考虑到安全性问题，现在更推荐使用 Ed2519 加密算法，此时，你只需要替换上述命令中的 rsa 为 ed2519 即可。
配置 Config 接下来，我们需要为本地的 SSH 配置上个步骤中生成的两个 SSH Key。通常，这个配置文件存在于以下路径：
Linux: ~/.ssh/config Windows: C:\Users\&amp;lt;Your-User&amp;gt;\.ssh\config 如果在 Windows 系统下找不到该文件，我们直接创建一个无扩展名的文本文件即可：</description></item><item><title>C# 使用 LibUsbDotNet 实现 USB 设备检测</title><link>https://blog.yuanpei.me/posts/csharp-uses-libusbdotnet-to-implement-usb-device-detection/</link><pubDate>Wed, 18 Oct 2023 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/csharp-uses-libusbdotnet-to-implement-usb-device-detection/</guid><description>国庆节回来后的工作内容，基本都在围绕着各种各样的硬件展开，这无疑让本就漫长的 “七天班” ，更加平添了三分枯燥，我甚至在不知不觉中学会了，如何给打印机装上不同尺寸的纸张。华为的 Mate 60 发布以后，人群中此起彼伏地传出 “遥遥领先” 的声音，大概人类总是热衷于评价那些不甚了解的事物。这个现象到了工作中就会变成，总有某些人觉得某件事情特别简单。其实。一切你认为“简单”的东西，背后一定有无数的人们上下求索、苦心孤诣，就像计算机从早期的埃尼阿克(ENIAC)发展到今天的智能手机，你能使用它并不代表它就“简单”，人还是应该对为止的领域保持敬畏和谦逊。回到这篇文章，今天我想和大家聊一聊，我为了解决那些“简单”的问题而做出的尝试。本期的故事主角是我们最熟悉不过的 USB 设备，有道是 “千古兴亡多少事”，且听我娓娓道来。
故事是这样的，基于某些不可抗因素上的考虑，博主需要在程序中集成某厂商的硬件。我猜测，人们觉得这件事情“简单”，或许是看到这个设备有一条 USB 连接线，因为在人们的固有印象中，只要把它接到电脑上就可以正常工作了。事实的确如此，因为你只要考虑串口(SerialPort)、USB 以及这两者间的相互转换即可。当然，这世上的事情圆满者少，遗憾者多，博主在使用过程中发现，厂商的提供的 SDK 存在 Bug，当设备从电脑上拔出后，其 SDK 的初始化函数依然正常返回了，这意味着我们无法在使用设备前“正确”地检测出硬件状态。考虑厂商愿不愿意修复这个 Bug 还是个未知数，博主不得不尝试另辟蹊径。
Windows 中的设备与打印机 相信这张图片大家都见过无数次啦，在这里你可以看到操作系统接入的各种设备。以鼠标为例，通过下面这个对话框，我们可以获得这个设备的各种属性信息：
查阅鼠标的硬件信息 在各种属性信息中，硬件 Id 是最为关键的一组信息，我们可以看到鼠标这个设备的 VID 为 0000，PID 为 3825。其中，VID 是指 Vender ID，即：供应商识别码；PID 是指 Product ID，即：产品识别码。事实上，所有的 USB 设备都有 VID 和 PID，VID 由供应商向 USB-IF 申请获得，而 PID 则由供应商自行指定，计算机正是 VID、PID 以及设备的版本号来决定加载或者安装相应的驱动程序。因此，如果想要判断计算机是否连接了某个 USB 设备，我们可以使用下面的方案：
bool HasUsbDevice(string vid, string pid) { var query = $&amp;#34;SELECT * FROM Win32_PnPEntity WHERE DeviceID LIKE &amp;#39;USB%VID_{vid}&amp;amp;PID_{pid}%&amp;#39;&amp;#34;; var searcher = new ManagementObjectSearcher(query); var devices = searcher.</description></item><item><title>基于 C# 实现样式与数据分离的打印方案</title><link>https://blog.yuanpei.me/posts/a-printing-scheme-for-separating-style-and-data-based-on-csharp/</link><pubDate>Wed, 20 Sep 2023 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/a-printing-scheme-for-separating-style-and-data-based-on-csharp/</guid><description>对于八月份的印象，我发现大部分都留给了出差。而九月初出差回来，我便立马投入了新项目的研发工作。因此，无论是中秋节还是国庆节，在这一连串忙碌的日子里，无不充满着仓促的气息。王北洛说，“活着不就是仓促，哪里由得了你我”。最近，我一直在忙着搞打印，我时常怀疑在“数字化转型”这件事情上，人们的口号大于实质，否则，人们便不会如此热衷于打印单据，虽然时间已过去许多年，可有些事情似乎从未改变过，无论是过去的 FastReport、FineReport，还是如今的 PrintDocument 以及基于 Web 的打印方案，它们只是形式在变化而已，真正的本质并未改变，就像业务可以从线下转移到线上一样，可人们试图控制和聚合信息流的意愿从未消退。在变与不变这两者间，我们总强调“适应” 和 “向前看”，可每个人都在有意无意地，试图向别人兜售某种在“舒适圈”浸染已久的概念，这一刻，我觉得还是应该多一点变化。所以，我想以 “样式与数据分离的打印方案” 为主题，探索一种 “新” 的玩法。
从 PrintDocument 说起 一切的故事都有一个起点，而对于 C# 或者 .NET 来说，PrintDocument 始终是打印绕不过去的一个点。虽然，在别人的眼里，打印无非是调用系统 API 向打印机发送指令，可如果考虑到针式、喷墨、激光、热敏&amp;hellip;等等不一而足的打印机种类，以及各种尺寸的打印纸、三联单/五联单、小票纸，我觉得这个问题还是蛮复杂的。考虑到篇幅，我不打算在这里科普这些 API 的使用方法，下面这张思维导图展示了 PrintDocument 所具备的关于 “打印” 的能力。从这个角度来看，打印需要考虑的事情何其纷扰耶，甚至你还要考虑打印机缺/卡纸、切刀打印机是否正确地切割了纸张&amp;hellip;等等的问题。此前，网络上流传着一个段子，大意是有人问如何解决打印时产生的空白页。此时，在职场打拼多年的前辈会语重心长地告诉你，只需要将其打印出来然后丢掉其中的空白页😺。
PrintDocument 思维导图 相信大家都见过类似下面这样的单据或者小票：
某公司公路出库单及华润万家购物小票 通常情况下，如果使用 C# 中的 PrintDocument 来实现打印，其基本思路是构造一个 PrintDocument 实例，同时注册 PrintPage 事件，而在该事件中，我们可以利用 Graphics 来绘制线条、文字、图片等元素：
var printDocument = new PrintDocument(); printDocument.PrintController = new StandardPrintController(); // 设置打印机名称 printDocument.DefaultPageSettings.PrinterSettings.PrinterName = &amp;#34;HP LaserJet Pro MFP M126nw&amp;#34;; // 设置纸张大小为 A5 foreach (PaperSize paperSize in printDocument.DefaultPageSettings.PrinterSettings.PaperSizes) { if (paperSize.</description></item><item><title>基于 SVG 的图形交互方案实践</title><link>https://blog.yuanpei.me/posts/practice-of-svg-based-graphic-interaction-solution/</link><pubDate>Sun, 20 Aug 2023 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/practice-of-svg-based-graphic-interaction-solution/</guid><description>不知道从什么时候起，人们开始喜欢上数字大屏这种“花里胡哨”的东西，仿佛只要用上“科技蓝”这样神奇的色调，就可以让一家公司焕然一新，瞬间变得科技感满满。不管数字大屏的实际意义，是用来帮助企业监控和决策，还是为了方便领导参观和视察，抑或是为了向外界展示和宣传。总之，自从数字大屏诞生之后，它始终就没能摆脱其前任“中国式报表”那种大而全的宿命。追随着 ECharts、Superset、FineBI、DataEase 等数据可视化产品的身影一路走来，你会发现人们在追求“花里胡哨”这件事情上永无止境。如今的数据大屏，元素多(表格、视频、2D/2.5D/3D地图)、种类多(图表、报表、流程图)、媒介多(PC、平板、电视、LED)，主打的就是一个眼花缭乱。
某数据中心设备运行监控示意图 当数字大屏的这股时尚潮流涌向物联网和工业互联网领域以后，就不可避免地催生出像上面这样的“数字大屏”需求，请原谅我使用如此模糊的措辞，因为我实在难以给它一个准确的定义，工艺流程图、设备运行监控图、组态图、SCADA&amp;hellip;。也许，这些名称不见得都能做到全面概括，可这些东西的确具备了数字大屏的特征，哪怕这些设备元件、管道阀门在科技蓝配色下违和感十足。作为一位低调的程序员，我一向不喜欢这种粉饰太平的面子工程，所以，当设计师同事带着设计图来找我时，我当时内心是拒绝的：
基于 HTML5 图片热区特性实现交互的思路 也许，此时你的内心深处会闪过一丝蔑视，认为这有什么难度呢？我只需要在图片上叠加若干个透明的 div，这样不就可以实现图片特定区域的交互逻辑啦！我承认，这是一个非常好的思路，但是在实践过程中你就会发现，div 的交互区域通常都是一个标准的矩形，而设计师同事常常使用圆角矩形和不规则图形来增强设计感。因此，在交互方面可能会存在一些缺陷，尤其是在 2.5D 的图片设计稿中，交互区域实际上是一个多边形。接下来，我将介绍一种基于 HTML5 图片热区特性来实现交互的思路：
&amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt; &amp;lt;img src=&amp;#34;Demo-01.jpg&amp;#34; usemap=&amp;#34;#imageMap&amp;#34; style=&amp;#34;width: 600px; height: 315px&amp;#34;&amp;gt; &amp;lt;map name=&amp;#34;imageMap&amp;#34;&amp;gt;&amp;lt;/map&amp;gt; &amp;lt;/div&amp;gt; 首先，准备一张图片以及一个 map 标签，并且这个 map 标签通过 usemap 属性与这张图片进行了关联。参照上面的示意图，我们定义了两个可交互的区域。其中，区域1是矩形区域，区域2是圆形区域：
const areas = [{ key: &amp;#39;半泽直树&amp;#39;, shape: &amp;#39;rect&amp;#39;, coords: [0, 0, 308.5, 315] }, { key: &amp;#39;大和田&amp;#39;, shape: &amp;#39;circle&amp;#39;, coords: [418, 134, 157.5] }] 因为 area 标签需要搭配 map 标签来使用，所以，我们将通过下面的代码来动态地创建区域，同时为每个区域绑定相应的事件：
const popup = document.getElementById(&amp;#39;popup&amp;#39;); const imageMap = document.</description></item><item><title>前端视频播放技术概览</title><link>https://blog.yuanpei.me/posts/overview-of-front-end-video-playback-technology/</link><pubDate>Sat, 15 Jul 2023 13:32:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/overview-of-front-end-video-playback-technology/</guid><description>转眼间，2023 年已进入下半场，在这样一个时间节点下，长视频平台如爱奇艺、优酷、腾讯视频等，以及短视频平台如抖音、快手等，对大家来说早已是司空见惯的事物。然而，在我们追剧、刷弹幕的时候，很少有人会去深入思考这些平台背后的技术奥秘。直到最近，我需要在前端项目中实现视频播放时，我终于意识到，这些视频不仅在格式上存在着差异，甚至连播放形式都各有不同。举个例子，当下最为火热的 “直播”，通常是指实时的视频播放。相对应地，非实时的视频播放则被称为 “点播”。如果你有接触过 Netflix，你或许还听说过 “流媒体” 这个词汇。为了更好地理解这些概念，我利用空闲时间整理了一个相对完整的技术体系，并以此为基础撰写了今天这篇文章。
从 HTML5 说起 好了，现在让我们从最简单的视频播放方案开始说起。在 HTML5 标准发布前，主流的视频播放方案是使用 Adobe 的 Flash Player 插件，国内的优酷、土豆等视频网站创立初期都经历过这个阶段。后来，随着乔布斯那封 “关于 Flash 的思考” 的公开信的发表，某种意义上加速了整个 Flash 技术的 “消亡”。再后来，随着 HTML5 标准发布，我们可以使用 video 或者 audio 标签在网页中呈现音/视频内容。如图所示，下面是 video 标签的基本用法：
&amp;lt;video controls&amp;gt; &amp;lt;source src=&amp;#34;myVideo.mp4&amp;#34; type=&amp;#34;video/mp4&amp;#34;&amp;gt; &amp;lt;source src=&amp;#34;myVideo.webm&amp;#34; type=&amp;#34;video/webm&amp;#34;&amp;gt; &amp;lt;p&amp;gt;Your browser doesn&amp;#39;t support HTML5 video. Here is a &amp;lt;a href=&amp;#34;myVideo.mp4&amp;#34;&amp;gt;link to the video&amp;lt;/a&amp;gt; instead.&amp;lt;/p&amp;gt; &amp;lt;/video&amp;gt; 具体来讲，这个 video 标签可以支持 Ogg、MPEG4 和 WebM 三种视频格式。可惜，并不是所有的浏览器都支持这些格式，因此，你可以在 video 标签内指定多个视频源，并且当这些视频源都不被支持的时候，你可以使用一个自定义的 HTML 结构来进行降级处理。需要注意的是，MPEG-4 即 MP4 格式，实际上是一组格式，因为在视频处理中通常还涉及到编码器的问题。可不幸的是，浏览器目前唯一支持的编码器是 H.</description></item><item><title>你好，千寻小姐</title><link>https://blog.yuanpei.me/posts/call-me-chihiro/</link><pubDate>Sat, 10 Jun 2023 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/call-me-chihiro/</guid><description>豆瓣上的观影记录映入眼帘的那一刻，我突然意识到，看 《疾速追杀4》 居然是五月份的事情啦，而六月份到目前为止仅仅看过《千寻小姐》 这部电影，甚至今年连去电影院的次数都屈指可数，我想，这大概能证明我此刻忙碌与困顿交织在一起的心境。在某一趟下班回家的地铁上，在被各种电子设备围剿的某时某刻，我果然还是决定要再写点什么。从某种意义上来讲，工作日的白天本不属于自己，而现在，甚至连夜晚都不属于自己，那种彻底地放空大脑、任由思绪汪洋恣肆的境界，不知不觉间竟变成了一件奢侈品。或许，这一切需要经过一场雨水的冲刷方能如愿，当我独自游走在雨声淅淅的街头时，我不由得地问自己，我该用什么样的语言来描摹这一刻，正如薛定谔的那只处于死猫和活猫叠加状态的猫一样，当一种情感难以言说的时候，我还是习惯将它写下来。
其实，一直不太明白有村架纯这个“村花”称号的来由，相比之下，莫妮卡·贝鲁奇之于“球花”、陈坤之于“厂花”，总算是有迹可循。到今天为止，我只看过三部有村架纯主演的电影，第一部是《浪客剑心》 真人版里的雪代巴，第二部是《花束般的恋爱》的八谷娟，而第三部显然是《千寻小姐》里的古泽绫。有人说，演员大体上分为两类，一类是千人千面，演什么像什么，一类是千人一面，演什么都像自己。看《花束般的恋爱》的时候，配合着编剧坂元裕二那刻意的、日记式的独白，你总能从村花那张清纯可爱的“大脸”，抑或者是那双亮晶晶的眼睛里，读出某种糖分超标的“甜美”来。
《花束般的恋爱》里的八谷娟 也许，你会觉得她就像雅人叔吐槽过的“晨间剧女主”一样，永远只会演这种“恋爱脑晚期”的美少女，但我想说的是，从《浪客剑心》里清冷胜雪的雪代巴，再到《千寻小姐》里若即若离的“千寻”，她的表演其实是完成了从外放到内敛的转换，如果说，从前的她是站在舞台中心、聚光灯下的主角，现在的她则是远离人群、貌合神离的配角。单论豆瓣评分的话，观众大概更喜欢《花束般的恋爱》，可对我而言，这部电影更像是她寻求演技突破的一部作品，即使她此前已经凭借《花束般的恋爱》拿了最佳女主角的奖项。
《浪客剑心》里的雪代巴 单论《千寻小姐》这个名字的话，似乎更容易联想到宫崎骏老爷子的经典作品《千与千寻》。在这部电影中，有一个非常隐晦的细节时，千寻在和汤婆婆签订契约时使用的是“获野千寻”而非“荻野千寻”，而这其实是千寻一家能回到现实世界的重要原因。同样的，在电影《千寻小姐》里，千寻这个名字同样是一个假名，女主本来的名字叫做古泽绫，此前是风俗店的小姐，在辞职后来到一座小岛上，化名千寻在便利店上班。可奇怪的是，千寻从来不对这段经历有所掩饰，甚至能坦然地接受顾客的“调侃”，嘻嘻一笑，“谢谢夸奖”。久而久之，千寻居然变成了这座小岛上的“名人”。
《千寻小姐》剧照：名人的名场面 可偏偏就是这样一个“名人”，她喜欢跪坐在地上和流浪猫聊天，她喜欢独自一人荡秋千、去海边散步，甚至是送便当给流浪老人、陪老人吃饭、帮老人洗澡、送老人衣服。有时候，她会陪高中生冈地、小学生小诚一起玩，在她的影响下，冈地认识了来自“同一颗星球”的朋友，而小诚则学会了如何向妈妈表达爱意……在一切事物都向着好的方向发展、当被千寻“治愈”的人们围坐在一起其乐融融的时候，随着镜头的推移旋转，千寻却不知在何时悄然离去，也许，有人知道呢？可谁又知道呢？
《千寻小姐》剧照：我能在你旁边站一会儿吗？ 在女高中生冈地的眼里，千寻是整座岛上最酷的人，她会远远地拍下千寻荡秋千时的画面，会在千寻独自站在海边的时候，默默现在她的身边，可没人知道，千寻其实是整座岛上最孤独的人。某一天，当千寻发现流浪老人死在某个角落里，她默默地挖坑掩埋老人的尸体，一如后来她掩埋那只死在码头上的海鸟，即使是后来接到弟弟的电话，被告知母亲去世的消息时，她依然像往常一样平静地说，自己不会去参加葬礼。原来，千寻一直坚信，“我们都是住在人类身体里，来自不同星球的外星人，而来自同一个星球的人，总有一天会遇见他们”，而这，居然是千寻的一位客人曾经告诉她的。
《千寻小姐》剧照：人类的悲欢并不相通 正如鲁迅先生所言，“人类的悲观并不相通”，人类始终无法理解彼此，即使是家人、爱人、朋友。千寻深知，“你无法独占一个人的心”，而她也不希望有人这样对待她，所以，即使是面对母亲的离世，她依然表现得没有悲伤、没有遗憾、没有孤独。虽然从整体上看起来，这是一个边缘人物治愈主流人物的故事，甚至主角身上还有一点回避型依恋和边缘人格的特征，可有句话是这样说的，“一个被别人温柔对待过的人，更懂得什么是温柔”，千寻就像一个全身都散发着炽烈光芒的天使，独自温暖着身边的每一个人，可她更像一个幽灵般的孤独过客，随风漂泊，不知所终&amp;hellip;
《千寻小姐》剧照：今天不拍照吗？ 近些年来，原生家庭这个词汇被频频提起，而从千寻对母亲的态度来看，大概率没有得到父母的关爱和呵护。电影中穿插了一段女主童年时的回忆，彼时的女主曾经遇到过一个名为千寻的风俗女，但对方并没有以一种成年人的视角同女主相处，而是以一种平视的目光将女主视为一个独立个体，更重要的是，她没有因为自身风俗女的身份妄自菲薄，始终用她的善良和耐心温暖着年幼的女主。两人告别时，对方留下了自己的名片，这便是女主千寻这个名字的由来。后来，从风俗行业辞职的女主，在一个雨夜认识了前便利店店员多惠阿姨，在得知女主的经历后她并没有表现出特别的诧异，甚至主动提出为女主的便当“加餐”，直到后来多惠阿姨因为眼疾而不得不住院，女主决定接替多惠阿姨在便利店的工作。
《千寻小姐》剧照：我超想复刻的炒面 如果说，在《千与千寻》所描绘的那个神隐世界中，失去原来的名字就意味着再无法回到原来的世界，那么，在《千寻小姐》这部电影里，千寻这个名字就变成了某种符号。对女主而言，不管是童年时期遇到的真·千寻——风俗女千寻，还是雨夜中遇到的“千寻小姐”——便利店店员多惠阿姨，或许都是真正的“千寻小姐”，因为她们都丝毫不吝于向陌生人施以温柔，甚至连女主自己最后都变成了“千寻小姐”，她去不同的地方、换不同的工作、体验不同的生活，即便有时候感觉自己会像金鱼一样沉入水底，她始终都能忠于自我、自由而热烈地活着。在电影结尾，女主出现在一家农场，在给奶牛投喂饲料的时候，你说，有没有一种可能，她在用自己的方式饲养着那份遗世独立、不可方物的孤独呢？
《千寻小姐》剧照：死去的海鸟 正如多惠阿姨说得那样，“你这个人啊，不管走到哪里，始终都有一种孤独感”，所以，当多惠阿姨发现女主悄悄离开以后，她立刻打电话给女主，劝她留下来，“你不用这样一直漂泊下去的”，可女主就像一叶孤舟顺水漂流，遇合尽兴，枯荣随缘，她可以和身边所有人相处融洽，却似乎总是与这个世界显得格格不入。也许，你觉得这样子显得太过孤独，可孤独才是这个世界最真实的体验，它几乎无处不在，并非只有你我才有，是一种普遍而深刻的人类经验，从刀耕火种的远古时期到如今高楼耸立的科技时代，划破黑夜的那束火光或许在不断地变换着形式，可人类的孤独自始至终从未消失过。曾经有学生提问姜文导演，“您的电影人物为什么到最后总是一个人？为什么要把人放在这么孤独的处境？”，一个熟悉的声音回应道，“你以为你最后不是一个人吗？”，所谓孤独，大抵如此罢！</description></item><item><title>温故而知新，再话 Python 动态导入</title><link>https://blog.yuanpei.me/posts/discussing-dynamic-import-in-python-again/</link><pubDate>Mon, 29 May 2023 20:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/discussing-dynamic-import-in-python-again/</guid><description>多年前，我曾写过一篇关于 Python 动态导入的文章，当时想要解决的问题是，如何通过动态导入 Python 脚本来实现插件机制，即整个应用程序由主程序和插件两部分组成，主程序通过 importlib 模块中的 import_module 方法动态地导入一个 Python 脚本，最终通过 getattr、setattr 等方法实现反射调用。时过境迁，代码还是那些代码，江湖故人早已不知所踪。我向来都是一个喜欢怀旧的人，我怀念的是那些遗忘在寒江孤影里的江湖故人，我怀念的是那些湮灭在时光尘埃里的代码片段。或许，在屏幕前的你看来，一个每天都在经历着“更新换代”的技术人员，更应该对这一切的消逝习以为常。可正如这世界上的风、沙、星辰等流动的事物一样，无论我们愿意与否，时间总会在不经意间将那些熟悉而珍贵的东西一一带走，不放弃对过去的回忆和珍视，这便是我在世事变幻的洪流中追求的安宁与平静。正所谓“温故而知新”，今天我想要怀旧的话题是 Python 里的动态导入。
众所周知，这段时间我一直在开发基于 ChatGPT 的人工智能管家 Jarvis，在整个探索过程中，类似语音识别、语音合成这些领域，博主先后考察了微软、百度、腾讯&amp;hellip;这些大厂的方案，这可以说是非常符合我作为 Python “调包侠” 的人设啦！以语音识别为例，最终，你可能会得到类似下面这样的代码：
class ASREngineFactory: @staticmethod def create(config, type): if type == ASREngineProvider.Baidu: return BaiduASR(config[&amp;#39;BAIDU_APP_ID&amp;#39;], config[&amp;#39;BAIDU_API_KEY&amp;#39;], config[&amp;#39;BAIDU_SECRET_KEY&amp;#39;]) elif type == ASREngineProvider.PaddleSpeech: return PaddleSpeechASR() elif type == ASREngineProvider.OpenAIWhisper: return WhisperASR() 没错，非常经典的简单工厂模式，你只需要告诉工厂类，你需要使用哪种语音识别引擎，它就可以自动帮你创建出对应的示例，如下图所示，这看起来非常合理，对吧？
config = load_config_from_env(env_file) engine = ASREngineFactory.create(config, ASREngineProvider.PaddleSpeech) 这里，其实有一段小插曲，博主最近开始尝试使用 virtualenv 来管理不同的 Python 版本，这样做的好处是，我只需要在不同的工作场所拉取代码、激活环境，就可以享受到完全一样的开发环境。当然，这一切都只是理论上的，实际使用下来的感受是，它并不能完全抹平环境上的差异。譬如，当我试图在个人电脑上安装 PaddleSpeech 和 Rasa 这两个库时，依然免不了遇到各种错误，即使是在同一个 Python 环境下。
此时，你会发现一个非常尴尬的问题，即使我不使用 PaddleSpeech 来作为 Jarvis 的语音识别引擎，它依然无法正常工作，原因是我环境中没有安装 PaddleSpeech，我不得不注释掉项目中所有和 PaddleSpeech 有关的代码，而这一切的根源其实是，我们在代码中使用了静态导入的方式，如下图所示：</description></item><item><title>后 GPT 时代，NLP 不存在了？</title><link>https://blog.yuanpei.me/posts/in-the-post-gpt-era-nlp-no-longer-exists/</link><pubDate>Fri, 12 May 2023 20:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/in-the-post-gpt-era-nlp-no-longer-exists/</guid><description>在刘慈欣老师的《三体》小说中，整个故事是以杨冬的死亡线索展开的，而她自杀的原因是物理学不存在了。随着 GPT-4 的发布，『NLP已死』和『NLP不存在了』的声音开始不绝于耳。如果说杨冬认为物理学被颠覆源于智子的“欺骗”，那么，现在的大型语言模型对于 NLP 的冲击，实际上改变了AI与最终用户互动的方式。传统的 NLP 技术方向涵盖了信息抽取、文本挖掘、机器翻译、语音合成、语音识别、语义理解、句法分析，这些都被视为自然语言处理的中间任务。因此，传统的 NLP 模式是在每个领域中提供各种不同的工具。当需要对自然语言进行处理时，你不得不将这些不同的工具结合起来，就像博主曾经使用过的结巴分词、SnowNLP、词袋模型、情感分析、TF-IDF一样。然而，现在的大型语言模型更像是一个直接面对最终任务的“智者”，跳过了这些中间任务。因为我们最终的目的就是要让程序产生智能，并让人类相信它能够“理解”他们的意图。显然，在这一点上，ChatGPT 和 Midjourney 都做到了。作为非科班的程序员，我无法科学、客观地评判 LLM 和 NLP 的优劣。但是我想分享一下我在开发 “贾维斯” 过程中获得的一点心得，希望对大家有所启发。
Hey Jarvis 在将小爱同学接入 ChatGPT 以后，我开始思考怎么样在智能和功能上取得平衡，尽管 ChatGPT 提升了小爱同学在聊天方面的智能，可这同时破坏了当下智能音箱普遍使用的“指令集”，你无法运用 ChatGPT 的这种“聪慧”让它真正地帮你做点事情。所以，我大概率要再次发明“智能音箱”，可我想知道，有了 ChatGPT 的加持以后，它到底能达到什么样的智能水平？带着这样一个想法，我开始从头编写贾维斯，一个基于 ChatGPT 的人工智能管家，其灵感来源于钢铁侠的人工智能管家 Jarvis。目前，它可以查询日期/时间、检索信息、播放音乐、控制米家/电脑、打开应用、讲笑话、查询天气、编程。以下是我在 Bilibili 上发布的视频演示：
你可能会好奇这些功能都是如何实现的？请允许我做一个简单说明，下面是“贾维斯”的整体设计思路：
贾维斯整体设计思路说明 从整体而言，整个程序并不算特别复杂，因为语音唤醒、语音识别、语音合成这些均已有非常成熟的方案。在接入 ChatGPT 以后，我开始尝试为它扩展更多的功能。这个时候，我了解到自然语言理解(NLU)的这个方向，并且它依然属于自然语言处理(NLP)这个范畴，更具体的关键词则是意图识别或者意图提取。以查询日期这个功能为例，我只需要在某个函数上添加“路由”，即可为贾维斯设计不同的“技能”：
@trigger.route(keywords=[&amp;#39;查询日期&amp;#39;,&amp;#39;询问日期&amp;#39;,&amp;#39;日期查询&amp;#39;]) def report_date(input): now = datetime.datetime.now() week_list = [&amp;#39;星期一&amp;#39;,&amp;#39;星期二&amp;#39;,&amp;#39;星期三&amp;#39;,&amp;#39;星期四&amp;#39;,&amp;#39;星期五&amp;#39;,&amp;#39;星期六&amp;#39;,&amp;#39;星期日&amp;#39;] formated = now.strftime(&amp;#39;%Y年%m月%d日&amp;#39;) week_day = week_list[now.weekday()] history = today_on_history() if history != None: return f&amp;#39;今天是{formated}，{week_day}。{history}。&amp;#39; else: return f&amp;#39;今天是{formated}，{week_day}。&amp;#39; 此时，问题就被转化为如何识别或者提取一句话中的真实意图。坦白地讲，面对人类这种口是心非的动物，想要了解其真实意图是非常困难的。譬如，人类都希望别人能够“懂我”，可没有人会喜欢被别人一眼看穿。因此，我们这里讨论的意图，特指那些动宾结构的指令型用语，例如：打开客厅的灯、查询明天的天气等等。不论屏幕前的你对 AI 持何种态度，我想说的是，AI 已然参与到我日常的编程和写作中，这正是我开发贾维斯的动力所在，我希望它能参与到更多的事项中去，甚至想让它取代家里的小爱同学。</description></item><item><title>视频是不能 P 的系列：使用 Milvus 实现海量人脸快速检索</title><link>https://blog.yuanpei.me/posts/use-milvus-to-quickly-retrieve-massive-faces/</link><pubDate>Mon, 24 Apr 2023 20:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/use-milvus-to-quickly-retrieve-massive-faces/</guid><description>最近我一直在优化一个人脸识别项目，这个过程令我深感科学的尽头永远都是殊途同归。一年前，我使用 dlib 实现人脸识别时遇到了两个悬而未决的问题：一是因为人脸样本数目增加导致性能下降问题；二是如何快速地判断目标人脸是否在人脸样本中。然而，在经过虹软人脸识别 SDK 的折磨后，我意识到这两个问题实际上从未消失。它们总会在某个合适的时机突然跳出来，然后开始无声无息地敲打你的灵魂。果然，“出来混还是要还的”。现在重新审视这两个问题，我认为，它们本质上是1：1 和 1：N 的问题。在使用虹软人脸识别 SDK 的过程中，我遇到了一个非常棘手的难题，即：当目标人脸在人脸数据库中时，识别过程非常流畅；可当目标人脸不在人脸数据库中时，识别过程就异常卡顿。结合使用 dlib 做人脸识别的经验，我猜测魁祸首可能是频繁的特征对比。相比于输出一个枯燥的结论，我更喜欢梳理解决问题的思路。因此，这篇博客的主题是，利用 Milvus 实现海量人脸快速检索的实现过程。
从人脸识别到向量 故事应该从哪里讲起呢？我想，可以从人脸数据库这个角度来切入。当我们把人脸特征存储到 CSV 或者数据库中时，本质上是将 1:N 问题转化为 1:1 问题。因此，我们不得不遍历人脸数据库的每个样本，然后选取与目标人脸最相似或最匹配的那个。这意味着，人脸识别的效率将受到到样本数量和相似度/距离计算方法等因素的影响。以虹软人脸识别 SDK 为例，其免费版提供了 1:1 人脸特征对比的接口，付费版提供了 1:N 人脸特征对比的接口。当然，据热心网友透露，官方这个 1:N 其实还是通过 1:1 循环来实现的。可即便如此，在相同的时间复杂度下，想要写好这样一个循环，这件事情本身并不容易。所以，影响人脸识别效率的因素里，还应该考虑到人的因素。在这个硬件性能过剩的时代，“锱铢必较”大抵会成为一种难能可贵的品质。谁能想到，如今训练模型的门槛变成了一块显卡呢？
通过 one-hot 编码实现的文本向量化表示示意图 如果我们从另一个角度思考这个问题，就会发现向量作为全新的数据类型，是所有这些问题的根源。无论是通过 CSV 还是关系型数据库进行数据处理，对向量数据进行过滤和筛选都是不可直接实现的。这迫使我们需要在内存中加载所有的人脸特征数据，再通过逐个计算和对比的方式来查找目标数据。当目标人脸在数据库中不存在时，这项工作就会变得困难和耗时。这实际上代表着数据从结构化到非结构化的转变趋势。例如，在 NLP 领域，计算文本相似度的理论依据就是向量的余弦公式。而在最近最火热的 ChatGPT 中，Embeddings 模型同样是基于文本的向量化表示。如果你有学习过机器学习的相关知识，就会更加深刻地认识到向量的重要性。正如刘慈欣在《三体》中所描述的那样，高维文明可以对低维文明实施降维打击。如果我们把向量看作是一种将高维度信息压缩为低维度信息的技术，那么，时下这场 AI 革命是不是可以同样视为降维打击呢？试想一下，那些如同咒语一般的提示词(Prompt)背后，不正是由无数个超出人类认知范围的多维向量在参与着复杂计算吗？
Milvus 向量数据库 正如我们所看到的，AIGC 改变了我们对这个世界的编程方式，即从 DSL/GPL 逐步地转向自然语言。在 OpenAI 的 GPT4 以及百度的文心一言中，我们会注意到这些大语言模型(LLM)开始支持图片。也许，以后还会支持音频、视频、文件……等等不同的形式，而这其实就是我们经常听到“多模态”的概念。可以预见的是，未来会有更多的非结构化数据加入其中，传统的关系型数据库将不再适合 AI 时代。譬如，最为典型的“以图搜图”功能，传统的模糊查询已经无法满足复杂的匹配需求。从这个角度来说，向量数据库将会是未来 AI 应用不可或缺的基础设施，就像此刻的关系型数据库对于 CRUD 一样重要。目前，向量数据库主要有 Facebook 的 Faiss、Pinecone、Vespa、国内创业公司 Zilliz 的 Milvus，以及京东的 Varch 等等，笔者这里以 Milvus 为例来展示向量数据库的核心功能——相似度检索。</description></item><item><title>GDI+下字体大小自适应方案初探</title><link>https://blog.yuanpei.me/posts/exploration-of-font-size-adaptation-scheme-under-gdi+/</link><pubDate>Wed, 05 Apr 2023 15:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/exploration-of-font-size-adaptation-scheme-under-gdi+/</guid><description>在某个瞬间，我忽然发觉，三体或是AI，本质上是非常相近的事物，甚至在面对任何未知领域的时候，人类总会不自觉地划分为降临派、拯救派和幸存派。姑且不论马斯克等人叫停 GPT-5 的真实动机如何，当大语言模型(LLM)裹挟着 AIGC 的浪潮气势汹汹地袭来时，你是否会像很多人一样，担心有一天会被机器取代以致于失业呢？此前，我曾自嘲般地提到过，我是一名 YAML 工程师 、Markdown 工程师、Dockerfile 工程师……，甚至以后还会变成一名 Prompt 工程师，而这背后的因果关系，本质上我们对这个世界的编程方式，正在逐步地从 DSL 转向自然语言。我个人认为，任何低端、重复的工作最终都会被机器取代，而诸如情感、艺术、心理、创意……等非理性领域，则可能会成为人类最后的防线。两年前，柯洁以 0:3 的比分输给 AlphaGo，一度在棋盘前情绪失控，我想，那一刻他大概不会想到两年后还会出现 ChatGPT。在《蜘蛛侠：英雄无归》 电影里面，彼得·帕克对奇异博士说，“你知道比魔法更神奇的东西是什么吗？是数学”。我个人非常喜欢这句话，因为在绝对的理性面前，一切的技巧都是徒然，更重要的是，如此深刻的哲理，居然是来自生活中一个真实案例。
电子签章与数学 好的，虽然我们说那些低端、重复的工作最终都会被机器取代，但是真正残酷的现实是，我们并没有那么多需要创造力的工作，就像我们并不需要那么多架构师一样。毕竟，你想象不到，一个人在五年前和五年后做的工作毫无差别，特别是企业级应用中非常普遍的打印。过去这些年，企业数字化转型的口号一直在喊，可到头来我们并没有等来真正的无纸化，企业依然对打印单据这件事情乐此不疲，仿佛没有这一张纸业务就没法开展一样。在这个过程中，企业会希望你能在单据上加盖公司的印章，这就产生了所谓的“电子签章”的需求。当然，我们这里不考虑电子签章的申请、加/解密、防伪等实际的流程，我们只是考虑将其通过 GDI+ 绘制出来即可。考虑到印章有圆形和椭圆形两种形制，所以，我们下面来进行分类讨论。
圆形印章 可以注意到，圆形印章通常由四部分组成，分别是顶部文字、中心部分的五角星、中下部分文字和底部文字。
通过程序绘制的印章样例 其中，顶部文字表示印章所属的公司/组织/机构，底部文字表示14位印章编号，这两部分文字均呈圆弧状分布。具体该如何实现呢？我们来一起看一下。首先，圆形印章的轮廓是一个标准的圆形，这个绘制非常容易：
// 从位图创建一个画布 var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); var g = Graphics.FromImage(bitmap); // 绘制圆形边框 var rect = new RectangleF(x, y, radius, radius); var Pen pen = new Pen(Color.Red, 3.0f); g.DrawEllipse(pen, rect); 而对于中心部分的五角星，我们使用一个路径填充即可。此时，问题的关键是在圆上找出五角星的五个顶点。显然，五角星的顶点满足下面的几何关系：
小学二年级就学过的五角星几何关系 利用三角函数的知识，我们可以非常容易地写出对应代码，请注意，计算机中使用的坐标系 Y 轴正方向向下：
var Radius = rect.Width / 2 * 0.</description></item><item><title>小爱音箱集成 ChatGPT 的不完全教程</title><link>https://blog.yuanpei.me/posts/the-xiaoai-speaker-integrates-an-incomplete-tutorial-on-chatgpt/</link><pubDate>Mon, 20 Mar 2023 15:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/the-xiaoai-speaker-integrates-an-incomplete-tutorial-on-chatgpt/</guid><description>2023年三月对于金融和科技领域来说，可谓是“冰火两重天”。硅谷银行倒闭事件像一枚深水炸弹一样在金融领域扩散开来，而 OpenAI 则凭借 ChatGPT 这款产品一路“狂飙”，成为当下最负盛名的爆款话题。就在百度推出同类产品“文心一言”的前夕，OpenAI 正式发布了 GPT-4，直至微软高调宣布在 Office 全家桶中集成了 GPT-4，将这场技术狂欢推向高潮。作为一个关注聊天机器人的人，我从大学时期就开始通过 AIML 标记语言构建语料库，并逐渐接触 NLP 领域的知识。我认为这一波人工智能的热度代表了 OpenAI 主张的大语言模型(LLM)的胜利。ChatGPT 虽然始于聊天机器人，但绝不会止于聊天机器人。它的最终形态或许会是钢铁侠的智能管家“贾维斯”，抑或是《流浪地球》里超级人工智能 MOSS。事实上，我日常会用 ChatGPT 写程序原型、翻译文本、提取主题/关键词，这段时间更是尝鲜了智能家居。因此，我想和大家分享一下小爱音箱集成 ChatGPT 的过程。
基本原理 如果你像博主一样是一名智能家居新手玩家，那么在正式接触智能家居之前，你应该至少听说过 WIFI、ZigBee、BLE 这些名词。这些是指智能家居中的通信协议，例如小爱音箱可以作为蓝牙 Mesh 网关去连接那些使用蓝牙通信的设备，而 ZigBee 则是一种短距离、低功耗、支持自组网的无线通信协议。虽然 ZigBee 对外宣称是一个开放标准，但不同的厂商出于利益考虑，并不完全兼容彼此的设备，离真正的万物互联始终还有一段距离。因此，你会发现米家有类似多模网关这样的产品，现阶段的智能家居是一个多种协议混合使用的局面，2C 市场更青睐蓝牙和 WIFI 方案，2B 市场更青睐 ZigBee 方案。为了让更多的设备加入整个智能家居生态，开源的智能家居方案 HomeAssistant 就此诞生。其中的 IFTTT 组件可以扩展出更多的智能玩法；为了让设备加入苹果公司的 HomeKit 生态，HomeBridge 这样一个“曲线救国”的方案就此诞生。可以说，现阶段智能家居的高阶玩法，基本都是围绕这两个平台展开。作为一名普通的消费者，你并没有机会去选择使用哪种协议，更多的是去选择使用哪一个平台。
Smart Home Protocols: WiFi vs Bluetooth vs ZigBee vs Z-Wave 前面提到 ZigBee 的自组网具有离线可用的特性。与 WIFI 不同，WIFI 需要接入互联网，一旦断网就无法对设备进行有效控制，而蓝牙和 ZigBee 就没有这种烦恼。唯一的问题是它们都需要对应的网关。目前，米家的设备控制主要有远程控制和本地控制两种方式。远程控制需要发送指令到米家的服务器，这种方式对小米来说更有利，唯独不利于实现“万物互联”这一伟大远景。本地控制至少需要一个智能家庭屏或中枢网关，其好处是延迟低、离线可用、保障隐私。从某种角度来说，这与人们开始使用 NAS 搭建私有云的初衷一致，都是为了更好地保护隐私和数据安全。由于博主不具备本地控制的条件，所以，我们还是采用远程控制的方案，即通过向米家的服务器发送指令来达到控制设备的目的。在这个过程中，接入 ChatGPT 的 API，再控制小爱音箱将其响应内容朗读出来。这个方案可以实现远程控制的同时，利用 ChatGPT 弥补小爱同学“智能”上的不足。如图所示，下面是一个简单的示意图：</description></item><item><title>程序员视角下的三体世界随想</title><link>https://blog.yuanpei.me/posts/random-thoughts-of-three-body-world-as-programmers/</link><pubDate>Sat, 18 Feb 2023 15:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/random-thoughts-of-three-body-world-as-programmers/</guid><description>新年的第一篇博客迟迟没有落笔，我想，大抵是因为过去一年并没有一个“圆满”的结局，而这一切就好像，你总是要写完一个句号，方能心安理得地另起一行。我猛然意识到，这场疫情无形中放大了故乡在时间和空间上的距离感。因此，面对阔别久矣的故乡，我甚至选择了早上的第一趟高铁。当我在列车上看着身后的事物一点点变小直至消失，这实在像极了我每一次出门远行。彼时彼刻，一个念头我在脑海中盘桓辗转：见到家人的那一刻，一定要给对方一个坚实的拥抱。可奇怪的是，越是面对熟悉的人，我们反而越是拙于表达。当我推开家门看到再熟悉不过的一切时，我内心突然获得了某种安宁，即使我们一直期待的事情是，每次相逢都能有不同的体验。我说我不大喜欢过年走亲戚这类活动，老大哥面无表情地回一句：看三体。
三体问题，其实可以看作是 N 体问题的一个特例，而所谓 N 体问题，则是指 N 个参照值相对一致的天体，仅在万有引力的作用下会呈现出什么样的运动规律。该问题最早由天才数学家希尔伯特教授，在 1990 年的全球数学大会上提出，是目前公认的人类科学界最难的 23 道数学题之一。如果你无法理解这个难度，请允许我找一个更有名的问题——费马大定理作为参照。事实上，早在 1687 年近代物理学之父牛顿，第一次提出了三体问题。此后的三百余年里，三体问题更是串联起无数如雷贯耳的名字：欧拉、拉格朗日、庞加莱、希尔伯特、开普勒&amp;hellip;等等。
三体运动 “混沌” 模型示意图 从某种意义上来讲，三体问题贯穿了整个物理学的发展历程。具体来讲，当 N = 1 时，单个质点的运动轨迹是匀速直线运动，这对应的是牛顿第一定律；当 N = 2 时，两个质点的相对位置始终在一条圆锥曲线上，这对应的是开普勒定律；当 N = 3 时，这就是著名的三体问题，此时，这三个质点的运动轨迹是不可预测的，而从数学的角度来看，三体问题没有解析通解，即：我们没有办法用一个通用的公式来描述其运动轨迹。一个最浅显的例子是，在三维空间中描述位置需要 x、y、z 三个参数，因此，每一个质点可以写出 3 个二阶微分方程或者 6 个 一阶微分方程。那么，当有三个这样的质点的时候，最多的时候会得到 18 个一阶微分方程。可以预见，求解三体问题，等价于求解这样 18 个一阶微分方程构成的方程组，其难度可想而知。
三体运动 “三角形” 模型示意图 不过，虽然三体问题没有解析通解，我们总是可以找到某个特定解。譬如，当三颗天体在一条直线上时，两侧的天体会围绕中心天体呈椭圆形的运动轨迹。我们最为熟悉的太阳系就是这种情形；如果三颗天体呈三角形分布，则它们会一起围绕三角形中心点旋转；除此以外，还可以是同一侧的两颗天体围绕第三颗天体呈椭圆形的运动轨迹，其可能性可以说是不一而足。从这个角度来讲，或许我们以后还会发现更多的特定解，但我们始终没有办法用一个完美的公式来描述所有可能的场景，这就是三体问题的复杂性所在。
三体运动 “8字形” 模型示意图 回到刘慈欣老师的小说设定，三体星球是一个围绕半人马座 α 三合星系统公转的一个行星，由于这颗星球同时受到三个 “太阳” 的影响，使其呈现出一种在“恒纪元”和“乱纪元”交替往复的状态，为了在这样的环境中生存下去，三体人不得不通过 “脱水” 和 “浸泡” 的方式来维持生命。事实上，半人马座 α 三合星是真实存在的，甚至 2016 年科学家还在这个星系中发现了行星 “比邻星 b” 的踪迹。读到这里，我们不免会怀疑《三体》其实是一本纪实小说，难道这颗比邻星上真的有外星人吗？不同于小说中设定的三个太阳，真实的 “三体” 星球面临的最大威胁是 X 射线，据不完全统计，这颗比邻星上面承受的 X 射线是地球的四百倍。所以，就算真的有外星人，在如此强度的 X 射线面前，胜负还真的挺难说呢？</description></item><item><title>关于 Docker 容器配置信息的渐进式思考</title><link>https://blog.yuanpei.me/posts/progressive-thinking-about-docker-container-configuration-information/</link><pubDate>Thu, 01 Dec 2022 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/progressive-thinking-about-docker-container-configuration-information/</guid><description>作为一名软件工程师，不，或许应该叫做 YAML 工程师、Markdown 工程师、Dockerfile 工程师……等等，这绝非自谦，更多的是一种自嘲。毕竟，从入行的那一天开始，追求配置上的动态灵活，就如同思想一般刻进每个程序员的 DNA 里。可当你意识到，在这个世界上，提出主张的人和解决问题的人，并不是同一群人时，你或许会心头一紧，接着便是直呼上当，我甚至不能理解，为什么程序员提交完代码，还要像运维一样折腾各种配置文件。特别是在 DevOps 的理念流行开以后，程序员们简直就是在通过各种配置文件互相折磨对方。如果程序员不能通过程序变得懒惰，那是不是说明，我们早已忘记了当初学习编程时的初心？我们都以为代码可以不用修改，可有哪一次代码能逃过面目全非的结局？每当这个时候，我就特别想怼那些主张配置文件的人，要不您来？言归正传，今天我想聊聊容器、配置文件和环境变量，为什么称为渐进式思考呢？因为它更像是一种不同人生阶段的回顾。
从何说起 故老相传，鸿蒙初开，天地混沌。上帝说，要有光。于是，盘古抄起那把传说中的开天神斧，对着虚空世界就是一通输出。那一刻，这位创世神周围就像发生了奇点大爆炸一样迅速扩张。最终，它的身体化作了世间万物，推动这个世界从无到有的进化历程。屏幕前的你，无需纠结这段融合了东/西方神话、现代物理学的表述是否严谨，因为我想说的是，在一个事物发展的初期，一定是朴素而且原始的。相信大家开始写 Dockerfile 的时候，一定没少写过下面这样的脚本：
COPY /config/nginx.conf /etc/nginx/nginx.conf 如你所见，该命令会复制主机上的配置文件到容器的指定目录，而这其实是符合我们一开始对容器的预期的，即：我们只需要将程序打包到镜像里，就可以快速地完成程序的部署。可是，我们显然忽略了一个问题，当程序部署到不同的环境中时，它需要的配置文件自然是不同的。此时，你可能会采用下面的做法：
docker exec -it &amp;lt;容器Id&amp;gt; sh vim /etc/nginx/nginx.conf 环境变量 果然，大道至简，没有任何技巧，简直真诚到极致。常言道：智者不入爱河，这个做法辛不辛苦姑且不论，关键是容器一旦重启，你连慨叹镜花水月的时间都没有啦。所以，这个方案可谓是劳心劳力，为我所不取也！再后来，你发现容器里可以使用环境变量，于是你就灵机一动，为什么不能让这个配置文件支持动态配置呢？于是，你尝试使用下面的做法：
server { listen ${NGINX_PORT}; listen [::]:${NGINX_PORT}; server_name ${NGINX_HOST}; location / { root /usr/share/nginx/html; index index.html index.htm; } } 此时，我们只需要在 .env 文件或者 docker-compose.yml 文件里指定这些环境变量即可。对于这个思路，我们可以使用 envsubst 这个工具来实现：
export NGINX_PORT=80 export NGINX_HOST=xyz.com apt-get update &amp;amp;&amp;amp; apt-get install -y gettext-base envsubst &amp;lt; /config/nginx.conf &amp;gt; /etc/nginx/nginx.conf 此时，我们会发现，它可以实现环境变量的“注入”：
环境变量的“注入” 当然，如果这段脚本是写在 RUN 指令后面，那么，这个改进是非常有限的。因为如果你希望更新配置，你必须要重新构建一个镜像，一个更好的做法是，将这段脚本放到 CMD 或者 ENTRYPOINT 指令里。这样，我们更新配置时只需要重启容器即可，这是不是就符合配置上的动态灵活了呢？事实上，这正是博主公司一直采用的做法。不过，运维同事大概率是没听说过 envsubst 这个工具，他使用的是更朴素的 sed 命令：</description></item><item><title>在 Docker 容器内集成 Crontab 定时任务</title><link>https://blog.yuanpei.me/posts/integrate-crontab-scheduled-tasks-inside-docker-containers/</link><pubDate>Thu, 24 Nov 2022 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/integrate-crontab-scheduled-tasks-inside-docker-containers/</guid><description>有时候，我们需要在容器内执行某种定时任务。譬如，Kerberos 客户端从 KDC 中获取到的 TGT 默认有效期为 10 个小时，一旦这个票据失效，我们将无法使用单点登录功能。此时，我们就需要一个定时任务来定时刷新票据。此前，博主为大家介绍过 Quartz 和 Hangfire 这样的定时任务系统，而对于 Linux 来说，其内置的 crontab 是比以上两种方案更加轻量级的一种方案，它可以定时地去执行 Linux 中的命令或者是脚本。对应到 Kerberos 的这个例子里面，从 KDC 申请一个新的票据，我们只需要使用 kinit 这个命令即可。因此，在今天这篇博客里，我想和大家分享一下，如何在 Docker 容器内集成 Crontab 定时任务，姑且算是在探索 Kerberos 过程中的无心插柳，Kerberos 认证这个话题博主还需要再消化一下，请大家拭目以待，哈哈！
Crontab 基础知识 众所周知，Linux 中的所有内容都是以文件的形式保存和管理的，即：一切皆为文件。那么，自然而然的地，Linux 中的定时任务同样遵循这套丛林法则，因此，当我们谈论到在 Linux 中执行定时任务这个话题的时候，本质上依然是在谈论某种特定格式的文件。事实上，这类文件通常被称为 crontab 文件，这是一个来源于希腊语 chronos 的词汇，其含义是时间。Linux 会定时(每分钟)读取 crontab 文件中的指令，检查是否有预定任务需要执行。下面是一个 crontab 文件的示例：
# 每分钟执行一次 ls 命令 * * * * * /bin/ls # 周一到周五的下午5点发邮件 0 17 * * 1-5 mail -s &amp;#34;hi&amp;#34; alex@162.com # 每月1号和15号执行脚本 0 0 1,15 * * /var/www/newbee/check.</description></item><item><title>为你的服务器集成 LDAP 认证</title><link>https://blog.yuanpei.me/posts/integrate-ldap-authentication-for-your-server/</link><pubDate>Tue, 15 Nov 2022 12:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/integrate-ldap-authentication-for-your-server/</guid><description>回顾我这些年的工作经历，面向企业(2B)和面向用户(2C)的项目都曾接触过。我个人觉得，面向企业的项目更注重业务，参与决策的人数多、周期长，目的是为企业提供生产经营价值，如缩减成本、提升效率等等，而面向用户的项目更注重体验，参与决策的人数少、周期短，目的是为消费者提供更多的使用价值，本质上是为了圈揽用户和抢夺流量。我在参与这些项目的过程中发现，企业级应用的研发更注重与第三方软件如 SAP、金蝶、用友、ERP 等等的整合，因此，类似单点登录、数据同步这样的需求非常普遍。每当这个时候，我就不由地想起一位前辈。
时间就像沙漏里的沙一样流逝 当我还在 Automation 打杂的时候，前辈总是一脸得意地问我：“听说过 AD Domain 吗？”。那时，初出茅庐的我年少轻狂，不好意思说我不会，立马敷衍道：“当然听说过，只是一直没用过”。前辈目光如炬，大抵是看出我心虚，立马不屑一顾地回应道：“那就是不会”。过了几秒钟，前辈不紧不慢地接着说道：“只有学会了 AD Domain，你才算是一只脚踏进了企业级应用开发这个领域，知道吗？”，我点了点头，心道：“这不就和茴香豆的茴字有五种写法一样无聊吗？”。多年后，当 LDAP 这个字眼再次映入眼帘的时候，我内心终于清楚地知道：我错了。
为什么需要 LDAP 认证 我错在哪里了呢？我想，要回答这个问题，还是需要从企业管理的角度来着手。一个面向用户(2C)的产品，其用户基本上是不受地域因素限制的，而对于一个面向企业(2B)的产品，其用户基本上是在一个层次分明、有着明显边界的范围内。运营一个企业，除了业务系统以外，可能还需要 OA、财务、ERP 等等外围软件的支持，如果是一家互联网公司，可能还需要 DevOps、监控、协作等等方面的支撑。此时，从企业的角度自然是希望可以统一账号体系，这样就衍生出了各种各样的单点登陆和认证方案，单单是博主接触过的就有：OAuth2、CAS、Keycloak、IdentityServer4，这些方案可以说是各有千秋，此中曲折我们按下不表。
运行在 Windows Server 上的 AD 这里博主想说的是，一旦企业通过 AD Domain 或者说 Active Directory 来管理用户，就自然而然地牵扯出域登录或者域账号登录的问题。这类围绕 AD Domain 或者说域的问题，我们都可以考虑使用 LDAP 认证或者 Kerberos 认证，特别是后者，主流的软件如 Kafka、Zookeeper、MySQL 等等均支持这一协议，它可以实现在登录本地账户后，免登录打开一个网站的效果。可想而知，这是一个对企业而言极具诱惑力的特性，一个账号打通所有基础设施。当然，我承认 Kerberos 这个协议是非常复杂的，绝非三言两语可以厘清其中的千丝万缕，所以，我们今天只是聊聊 LDAP 认证这个话题。
通过 LDAP Browser 访问 AD 可能大家会纠结，LDAP 和 Active Directory 这两者间的关系，事实上， LDAP 是指轻量目录访问协议(Lightweight Directory Access Protocol)，而 Active Directory 则是微软针对该协议的一种实现。当然，微软为了解决域控的问题，利用 LDAP 存储了一部分私有的数据。所以，两者的关系就像是接口和实现类，我们这里只需要 Active Directory 当成一台 LDAP 服务器即可。关于 Active Directory 的基础知识，这里不再做更多的科普。总而言之，通过 LDAP 我们可以对某个网站实现认证，从而达到保护资源的目的。譬如博主目前参与的前端项目，它是没有常规的登录、注册页面的，它采用的就是域账号登录的形式。下面，我们来看看如何集成 LDAP 认证。</description></item><item><title>似花还似非花</title><link>https://blog.yuanpei.me/posts/like-flowers-not-flowers/</link><pubDate>Fri, 11 Nov 2022 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/like-flowers-not-flowers/</guid><description>周末独自前往大唐芙蓉园，心中盘桓已久的想法，于此时此地显得不合时宜，因为无论是指渐渐转凉的天气，还是指此消彼长的疫情，都俨然有一点沉默和突兀。更不必说，冬天的雾霾会让整个城市呼吸困难，蓝色的口罩像一张巨网，将相关或者不相关的人们笼络于其中。当年，唐太宗实行科举后看见士子进入考场，不由得感叹一句，“天下英雄尽入吾彀矣”。如今，这个世界被更大的网连接在一起，穿梭在曾经的皇家园林里，零零星星的几棵银杏树，一不小心就成为了此时此地最好的装饰。我不得不承认，此番我是特意来看银杏树叶的。可愈是人多的地方，人就愈是想要成为一切事物的中心。于是，无暇自拍的路人，在一众或专业或业余的摄影师眼里，被迫变成某种陪衬。按照纳什均衡理论，如果每个人都想在最好的位置、最好的角度拍照，那么每个人都无法在最好的位置、最好的角度拍照。可惜理论终究抵不过人心，毕竟谁会甘心落后于人呢？
不太真实的美 近来这几天一直在玩 《Stray》 ，对于我这个喜欢猫的人而言，这款猫猫模拟器远比赛博朋克、末日未来这些概念要更吸引人。作为一款独立游戏，其体量可以说是非常地克制，我只用了七个多小时就通关了游戏，如果从艺术性的角度来评价这款游戏，在我心中恐怕只有 《机械迷城》 和 《风之旅人》 可以相媲美，因为它们的故事基本上都没有人类参与，可这些故事里的草灰蛇线始终都有人类的影子。换句话说，当你身处于一个充满霓虹灯和机器人的科幻世界的时候，你会发现这其实是在以猫的视角讲述人类的故事。为什么这样说呢？陪伴着主角的那只无人机 B-12，起初它以为自己是个机器人，随着一路收集的记忆越来越多，渐渐地它意识到自己曾经是人类、是一名科学家，直到在控制室里解锁了全部的记忆，它终于想起了自己的外界者身份，最终在主角的帮助下成功打开穹顶，即使这份成功背后的代价是牺牲自我。
在控制室打开穹顶的瞬间 当巨大而沉重的穹顶一层层的打开，猫咪端坐在透明的落地窗前，俯视着这一路走过的贫民窟和中城，菌克在阳光的照耀下被消灭，机器人们终于得以重见光明。我认为这是一种隐喻，B-12 的牺牲更像是一种救赎，一种为了文明的延续和传承而牺牲自我的救赎。当屏幕上打出，“抱歉不能和你一起看到外界了”，我不禁感到怅然若失，因为这不不单单是 B-12 与我的告别，更是这个游戏与我的告别。猫咪最终回到了地面，可这个地面上早就没有人类了，甚至连一开始的那几只猫都没有再出现过。细细回想起来，我在这个游戏里最快乐的时刻，居然是在蚁村这个毫无存在感的章节，那里有两个正在打麻将的机器人，我最喜欢做的事情是故意把麻将打翻在地，然后看着它们一遍遍地从地上捡起麻将，我想说，实在有趣！单独讨论各个章节的话，我感觉制作组还是把主要精力放在了城镇上，蚁村实在太空洞了，而下水道里则会让人感到生理不适，谁能想到最后一次使用完镭射灯，猫咪捡起无人机狂奔这一段居然是我心中的最佳演出。
蚁村里打麻将的两个机器人 某天下午午休的时候，我梦见自己变成了一只猫，穿过家里的老房子，我看见父亲和弟弟正在院子里干活，我想和他们说话，可谁会注意到一只小猫咪呢？我疑心这是通关 Stray 以后的后遗症，毕竟我早已习惯了像一只猫一样仰起头、上蹿下跳、左右腾挪。我没有去看周公解梦，我更喜欢某种科学上的解释。思来想去，我觉得我大抵是想家了罢，毕竟，从去年国庆假算起，我已经有一年多时间没有回家了，每当我想回家的时候，疫情就来凑热闹。国家卫健委最近发布了二十条，我不知道过年能不能回去。如鲁迅先生那句名言，“希望是本无所谓有，无所谓无的”，有或者无，不过是唯心而已。某一瞬间，当我突然想起什么的时候，我会期待自己能立马将它们写下来，就像此时此刻的这些文字一样，似花还似非花。这三年来我们用于新冠病毒上的钻研属实有限，毕竟我们就只有隔离、核酸、健康码这三板斧可用，联想到最近有城市推出核酸采集的年卡，我心中/口中已不愿再多说一个字。
这一幕实在太像刺客信条啦！ 同样是以杨花作喻，苏轼写下这阙《水龙吟》的时候，想到是随风万里的飘摇不定，而百年前的东晋才女谢道韫，想到的则是雪满人间的轻盈皎洁。可是，灞桥的杨柳依依，李白的杨花落尽，又分明是同一种事物。我曾经感慨过新版 《倚天屠龙记》 里四女同舟的桥段，在审美越发趋于同质化、流水线的今天，我彻彻底底地变成一个脸盲症患者。等到如今再看 《天下长河》 的时候，虽然还是康熙、明珠、索额图这些熟悉的名字，可我始终不免旧疾复发。我想，脸盲症大抵是不区分颜值或是性别的，真正让你觉得似花非花的，是这个时代拼命想让所有人变得一样的“排异”心理。自从马斯克收购推特以后，他的话题热度就再没有下降过，只是这一次，让我们记住他的不再是火箭和特斯拉，而是裁员和加班。我有时候会怀疑，这个一直在追寻星辰大海的男人，到底是一个不折不扣的天才，还是一个彻头彻尾的资本家？也许，这两个都是呢？也许，似花非花、雾里看花的世界更显得真实一点……</description></item><item><title>视频是不能 P 的系列：使用 Dlib 实现人脸识别</title><link>https://blog.yuanpei.me/posts/dlib-face-recognition-with-machine-learning/</link><pubDate>Tue, 01 Nov 2022 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/dlib-face-recognition-with-machine-learning/</guid><description>本文是 #视频是不能 P 的系列# 的第三篇。此前，我们已经可以通过 OpenCV 或者 Dlib 实现对人脸的检测，并在此基础上实现了某种相对有趣的应用。譬如，利用人脸特征点提取面部轮廓并生成表情包、将图片中的人脸批量替换为精神污染神烦狗 等等。当然，在真实的应用场景中，如果只是检测到人脸，那显然远远不够的，我们更希望识别出这张人脸是谁。此时，我们的思绪将会被再次拉回到人脸识别这个话题。在探索未知世界的过程中，博主发现 OpenCV 自带的 LBPH 方法，即局部二值模式直方图方法，识别精度完全达不到预期效果。所以，博主最终选择了 Dlib 里的特征值方法，即：对每一张人脸计算一个 128 维的向量，再通过计算两个向量间的欧式距离来判断是不是同一张人脸。在此基础上，博主尝试结合 支持向量机 来实现模型训练。因此，这篇文章其实是对整个探索过程的梳理和记录，希望能给大家带来一点启发。
原理说明 如下图所示，假设对于每一个人物 X ，我们有 N 个人脸样本，通过 Dlib 提供的 compute_face_descriptor() 方法，我们可以计算出该人脸样本的特征值，这是一个 128 维度的向量。如果我们对这些人脸样本做同样的处理，我们就可以得到人物 X 的特征值列表 feature_list_of_person_x。在此基础上，利用 MumPy 中的 mean() 方法，我们就可以计算出人物 X 的平均特征 features_mean_person_x。最终，我们把人物 X 的平均特征和名称一起写入到一个 CSV 文件里面。至此，我们已经拥有了一个简单的人脸数据库。
Dlib 人脸识别原理说明图 可以预见的是，一旦我们把人脸特征数值化，那么，人脸识别就从一个图形学问题变成了数学问题。对于图中的待检测人脸，我们只需要按同样地方式计算出特征值，然后从 CSV 文件中找一个距离它最近的特征即可。这里，博主使用的是欧式距离，并且人为规定了一个阈值 0.4， 即：当这个距离小于 0.4 时，我们认为人脸匹配成功；当这个距离大于 0.4 时，我们认为人脸匹配失败。下面的例子展示了使用 Dlib 计算人脸特征值的基本过程：
detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor(&amp;#39;shape_predictor_68_face_landmarks.dat&amp;#39;) face_reco_model = dlib.face_recognition_model_v1(&amp;#34;dlib_face_recognition_resnet_model_v1.dat&amp;#34;) # 计算特征值 image = Image.</description></item><item><title>浅议分布式链路追踪与日志的整合</title><link>https://blog.yuanpei.me/posts/integration-of-distributed-tracing-system-and-logging-system/</link><pubDate>Sat, 15 Oct 2022 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/integration-of-distributed-tracing-system-and-logging-system/</guid><description>最近拜读了 Artech 大佬的新文章 《几个Caller-特性的妙用》，可以说是受益匪浅。不过，对我而言，最大的收获当属这篇文章里的第二主角，即 ActivitySource 和 Activity，这组 API 可以认为是微软针对 OpenTelemetry 规范的一种实现，即：每一个 Activity 都对应着一个 Span 。在以前的博客 《Envoy 集成 Jaeger 实现分布式链路追踪》 中，我曾经向大家介绍过 OpenTelemetry 规范，并最终结合 Envoy 和 Jeager 实现了非侵入式的、网关层的分布式链路追踪，正所谓“温故而知新”，在这个过程中我意识到其中还有值得去挖掘的东西。譬如，可观测性的三大支柱分别是 Logging、Tracing 和 Metrics 。可当我们接入了 Jeager 、Zipkin 等等的链路追踪系统，我们会发现它和平用到日志系统如 NLog、Serilog、ELK &amp;hellip;等等都相去甚远，好像这两者间存在着一种天然的割裂感，你不得不在了解了服务间的调用关系以后，再一头扎进各种各样的日志文件里。幸运的是，经过数日的探索，我有了一点小小的收获。因此，今天这篇博客我想和大家分享的是，分布式链路追踪系统如何和日志系统进行整合。
.NET 中的分布式追踪 微软的 官方文档 中，有一个独立的章节来介绍分布式追踪。如果你观察得足够仔细，就会发现官方将其归类为 诊断和检测。我个人认为，这是我们日常开发中经常被忽略的一个东西。早年开发 Windows 桌面程序的时候，每当程序出现异常的时候，经验丰富的前辈总会让你去看一下 Windows 日志。其实，这个 Windows 日志就是 .NET Framework 时代的一种诊断工具。由此我们就可以知道， Diagnostics 就是一种帮助你记录应用程序运行期间的关键性操作及其执行时长的机制，我承认，这听起来和现在流行的 APM 差不多，至少从宏观上来看这个观点是成立的，因为 APM 的核心功能之一就是检测应用程序的关键事件。从 .NET Core 开始，Diagnostics 这个命名空间从 Microsoft 变为了 System 。如下图所示，整个诊断的核心建立在 Activity 这个类，以及 IObservable&amp;lt;T&amp;gt; 和 IObserver&amp;lt;T&amp;gt; 这组观察者模式的 API 上，其基本原理是：通过一系列活动来产生一系列事件，而关心这些事件的订阅者则可以通过这些事件来判断应用程序当前的状态。</description></item><item><title>关于 Git 大文件上传这件小事</title><link>https://blog.yuanpei.me/posts/a-story-of-git-large-file-storage/</link><pubDate>Mon, 10 Oct 2022 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/a-story-of-git-large-file-storage/</guid><description>很多年后，当我在命令行中熟练地操作 Git 的时候，我总会不由地想起从前意气风发的自己。毕竟不知不觉间，三十岁的年龄已然被更年轻的人们嫌弃“苍老”，除却生理上不可逆转的自然衰老，更多的或许是一种心态上的衰老。以前，我是非常鄙夷在 Git 仓库里提交 Word 或者 Excel 文件这种行为的，甚至连理由都给得十分正当，即：这种文件不利于差异的对比和合并。后来，参与的项目越来越多，渐渐认识到 Markdown 始终是一种小众的格式，你没有办法要求所有人都去适应 Markdown。所以，当我说我在心态上变成了一个老人的时候，其实是指，我不再对这件事情那么执着。当然，人生本来就是一个解决麻烦再制造麻烦的过程。当你默许了在 Git 仓库里提交非文本文件的行为，当这些非文本文件随着时间推移变得越来越大时，就出现了 Git 大文件上传、存储等等一系列的问题。因此，今天这篇文章，我们来聊聊 Git 里的大文件。
提交前的未雨绸缪 其实，博主不愿意在 Git 仓库里上传 Word 或者 Excel 文件，一个最为直接的理由是，它会成为我们拉取或者推送代码时的累赘。君不见，腾讯硬生生在手机 QQ 里内置了一个虚幻 4 引擎，想象一下，如果把这么多的文件都放到 Git 仓库里，每次做一点修改该有多痛苦啊！事实上，Github 对文件大小的限制是 100M，Gitlab 对文件大小的限制则是 600M，一旦超过这个限制，就会被判定为大文件。因此，Atlassian、GitHub 等组织一起开发了针对 Git 的 Large File Storage 扩展，即：Git LFS。其原理是延迟地下载大文件的相关版本来减少大文件对仓库的影响，具体来说，就是在 checkout 到工作区的时候才会真正去下载大文件的内容。如果大家想了解更多 Git LFS 的细节，可以阅读下面这份文档：https://www.atlassian.com/git/tutorials/git-lfs，这里不再考虑对其进行二次加工。
Git LFS 原理示意图 当你准备向一个 Git 仓库提交大文件的时候，首先，你需要下载和安装 Git LFS 扩展并执行命令：
git lfs install 其次，在 Git 仓库中，你需要通过 git lfs track 命令告诉 Git LFS，你希望它帮你管理哪些文件：</description></item><item><title>.NET 进程内队列 Channel 的入门与应用</title><link>https://blog.yuanpei.me/posts/getting-started-with-the-.net-in-process-queue-channel/</link><pubDate>Thu, 15 Sep 2022 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/getting-started-with-the-.net-in-process-queue-channel/</guid><description>最近，博主为 FakeRPC 增加了 WebSocket 协议的支持。这意味着，我们可以借助其全双工通信的特性，在一个连接请求内发送多条数据。FakeRPC 目前最大的遗憾是，建立在 HTTP 协议上而不是 TCP/IP 协议上。因此，考虑 WebSocket 协议，更多的是为了验证 JSON-RPC 的可行性，以及为接下来的要支持的 TCP/IP 协议铺路。也许，你从未意识到这些概念间千丝万缕的联系，可如果我们把每一次 RPC 调用都理解为一组消息，你是不是就能更加深刻地理解 RPC 这个稍显古老的事物了呢？在编写 FakeRPC 的过程中，我使用了 .NET 中的全新数据结构 Channel 来实现消息的转发。以服务端为例，每一个 RPC 请求经过 CallInvoker 处理以后，作为 RPC 响应的结果其实并不是立即发回给客户端，而是通过一个后台线程从 Channel 取出消息再发回客户端。 那么，博主为什么要舍近求远呢？我希望，这篇文章可以告诉你答案。
Channel 入门 Channel 是微软在 .NET Core 3.0 以后推出的新的集合类型，该类型位于 System.Threading.Channels 命名空间下，具有异步 API 、高性能、线程安全等等的特点。目前，Channel 最主要的应用场景是生产者-消费者模型。如下图所示，生产者负责向队列中写入数据，消费者负责从队列中读出数据。在此基础上，通过增加生产者或者消费者的数目，对这个模型做进一步的扩展。我们平时使用到的 RabbitMQ 或者 Kafka，都可以认为是生产者-消费者模型在特定领域内的一种应用，甚至于我们还能从中读出一点广义上的读写分离的味道。
生产者-消费者模型示意图 罗曼·罗兰曾说过，世界上只有一种真正的英雄主义，那就是在认清生活的真相后，依然热爱生活。此时此刻，看着眼前的这幅示意图若有所思，你也许会想到下面的做法：
class Producer&amp;lt;T&amp;gt; { private readonly Queue&amp;lt;T&amp;gt; _queue; public Producer(Queue&amp;lt;T&amp;gt; queue) { _queue = queue; } } class Consumer&amp;lt;T&amp;gt; { private readonly Queue&amp;lt;T&amp;gt; _queue; public Consumer(Queue&amp;lt;T&amp;gt; queue) { _queue = queue; } } 我承认，这个思路理论上是没有问题的，可惜实际操作起来槽点满满。譬如，生产者应该只负责写，消费者应该只负责读，可当你亲手把一个队列传递给它们的时候，想要保持这种职责上的纯粹属实是件困难的事情，更不必说，在使用队列的过程中，生产者会有队列“满”的忧虑，消费者会有队列“空”的烦恼，如果再考虑多个生产者、多个消费者、多线程/锁等等的因素，显然，这并不是一个简单的问题。为了解决这个问题，微软先后增加了 BlockingCollection 和 BufferBlock 两种数据结构，这里以前者为例，下面是一个典型的生产者-消费者模型：</description></item><item><title>使用 Fody 实现 .NET 的静态编织</title><link>https://blog.yuanpei.me/posts/implement-static-weaving-of-dot-net-via-fody/</link><pubDate>Tue, 23 Aug 2022 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/implement-static-weaving-of-dot-net-via-fody/</guid><description>在很长的一段时间里，我们的项目中一直使用 OnMethodBoundaryAspect 这个基类来记录每个方法的日志。诚然，FodyWeavers.xml 这个文件的存在，早已在冥冥之中暗示我，Fody 才是这座冰山下真正的墨西哥湾暖流。可惜，因为某种阴差阳错的巧合，譬如两者都使用了 OnMethodBoundaryAspect 这个命名，这导致我过去一直以为我们使用的是 PostSharp。如果你是用过 ReSharper 或者 Rider 这些由 JetBrains 出品的工具，你大概会听说过 PostSharp。不过，有趣的是，JetBrains 和 PostSharp 其实没有半毛钱的关系，两者唯一相似的地方，或许是它们都不姓微软:joy:。当我们谈论 PostSharp 的时候，我其实想说的是静态编织。由此，我们就引出了今天这篇文章的主题，即: .NET 中的静态编织。而对于静态编织，我们这里只需要知道，它是一种在编译时期间将特定的字节码插入到目标类和方法的技术。
再从 AOP 说起 想不到吧，此去经年，我再一次聊起了 AOP 这个话题。众所周知，AOP 是指面向切面编程 (Aspect Oriented Programming)，而所谓的切面，可以认为是具体拦截的某个业务点。对于面向对象编程的语言来说，一个业务点通常就是一个方法或者函数。因此，我们谈论 AOP 这个话题的时候，更多的是指在某个方法执行前后插入某种处理逻辑。此时，广义的 AOP 就有静态编织和动态代理两种形式，前者发生在编译时期间，后者发生在运行时期间。如下图所示，我们平时使用的 Castle DynamicProxy 、 AspectCore、DispatchProxy 等等都属于动态代理的范畴，这些都是在运行时期间对代码进行“修改”；而我们今天要讨论的 Fody ，则是属于静态编织的范畴，顾名思义，它是在编译时期间对代码进行“修改”。我们知道，按照实现方式上的不同， AOP 又可以分为代理模式和父子类重写两种“修改”方式。至此，我们对于 AOP 的认知范围被进一步扩大，就像我们以前学习数学的时候，我们对于对于“数”的定义，是先从有理数扩充到无理数，后来又从实数扩充到虚数。那么，屏幕前的你，真的搞懂 AOP 了吗？
广义上的面向切面编程 Fody 的初体验 作为一个类库，Fody 在使用上并没有任何非同寻常的地方。这意味着，你可以像使用任何一个第三方库一样，直接通过 NuGet 来安装：
dotnet add package Fody --version 6.6.3 可惜，这样或许会令你感到失望。因为对于 Fody 而言，我们通常使用的是它的插件 (Add-In) 而不是 Fody 本身，除非当你需要真正编写一个插件。身处西安这个十三朝古都，你一定听说过鼎鼎大名的三秦套餐，即：凉皮、冰峰、肉夹馍。这里，我们就以 Rougamo.</description></item><item><title>.NET Core + ELK 搭建可视化日志分析平台(下)</title><link>https://blog.yuanpei.me/posts/3687594959/</link><pubDate>Sun, 07 Aug 2022 16:01:13 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3687594959/</guid><description>最近，我收到一位读者朋友的私信，问我 ELK 为什么没有下篇，道德感极强的我不得不坦诚相告，显然这一篇鸽了。这就是说，鸽子不单单会出现在吴宇森的电影里，只要你试图拖延或者逃避，你一样有鸽子可以放飞。话说回来，新冠疫情已然持续了三年，而这篇文章其实是我在新冠元年写下的。某年某月，彼时彼刻，立春过后紧接着是上元节，阳光已透过玻璃宣示着春天的到来，可在这一墙之隔的里里外外，仿佛是两个气候迥异的世界。记忆里那种每天都和消毒水、口罩打交道的日子，后来就变成了一种习以为常、甚至有一点唏嘘的常态化生活。在这过去的三年里，恍惚中已经发生太多的事情，譬如 ELK 早已变成了 EFK，譬如前女友有了新的男朋友，在一切的物是人非背后，在一切的断壁残垣下面，我想，我还是用这个旧题目来讲一个新的故事罢！
从 Logstash 到 Filebeat 当初准备写这个系列的时候，ELK 还是经典的 Elastaicsearch 、 Logstash 和 Kibana 组合，如下图所示，Logstash 从各种不同的数据源收集数据，通过内置的管道对输入的数据进行加工。最终，这些数据会被存储到 Elastaicsearch 中供 Kibana 完成数据可视化。 即使放到三年后的今天来看，这张图依然是非常经典的一幅图。为什么这么说呢？因为自此以后，可视化日志分析平台的搭建，基本都是围绕这三个方面展开，甚至 Logstash 的继任者 Filebeat、Fluentd、Fluent-Bit 等等无一不沿用了 Logstash 的这套管道设计，足可见其对后来者的影响之深远。不过，作为先驱出现的 Logstash，其本身是采用 Java 语言开发的，其插件则是采用 Ruby 语言开发的，特别是第一点，一直让 Logstash 在性能问题上遭人垢病。在实际使用中，你常常需要在每一台服务器上安装 Logstash ，这意味着它在 CPU 和内存上的占用会比较高。
经典的 ELK 全家桶组合 为了解决这个问题，Elastic 官方推出了被称为 Beats 的下一代日志收集方案， 这是一种基于 Go 语言开发、更加轻量级的、资源占用更少的日志收集方案，可以认为是 Logstash 的替代品, 而 Filebeat 正好是其中一种实现。关于这两者的区别，我想，使用下面的比喻或许会更恰当一点， Logstash 就像一个轰鸣声不断的垃圾转运车，虽然可以让你直接把垃圾丢车上拉走，可你不得不忍受一整天的噪音；Filebeat 则像一个拎着扫帚和簸萁的环卫工人，那里有需要就去哪里清扫，不单单效率高而且不会让你感觉扰民，下面是一张来自 Elastic 官方文档 中的示意图：
Filebeat 日志收集示意图 从这里我们可以看出， Filebeat 由两个主要的组件 Inputs 和 Harvester 组成。其中， Harvester 是一个负责读取单个文件内容的采集器，它可以打开和关闭一个文件，并将内容发送到指定的输出；Inputs 顾名思义就是输入，对于 Filebeat 而言，其实就是指各种不同类型的日志文件，譬如 Filebeat 可以支持 Kafka、Redis、MQTT、TCP、UDP、Stdin、Syslog 等等的输入。从某种意义上讲，你可以把 Filebeat 理解为一个文件扫描服务。例如，下面的配置表示 Filebeat 将会从一个指定的路径读取日志文件：</description></item><item><title>聊一聊前端图片懒加载背后的故事</title><link>https://blog.yuanpei.me/posts/the-story-behind-the-lazy-loading-of-front-end-pictures/</link><pubDate>Tue, 02 Aug 2022 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/the-story-behind-the-lazy-loading-of-front-end-pictures/</guid><description>相信大家已经注意到我博客有了一点变化，因为博主最近利用空闲时间对博客进行了优化。经过博主的不懈努力，首屏渲染时间从原来的 2.0 秒缩短到了 1.7 秒。虽然这个优化相当得感人，不过我还是在这个过程中有所收获。Stack 这个主题中大量使用了图片这种元素，特别是首页中那些作为文章封面而存在的图片。我原本是打算借鉴一下 Wincer 这位网友的博客样式，可是考虑到选择封面、图片尺寸&amp;hellip;等等的因素，我最终还是决定写一个相对“平庸”的布局样式，即你现在看到的这个版本，本次优化的重点主要在于使用 CDN 加速、对图片进行压缩、编译期生成缩略图、使用懒加载这些常见的策略。在今天这篇博客中，我们来重点聊一聊前端图片的懒加载，希望能为大家带来一点新的启发或者思考。
什么是懒加载 懒加载，即：LazyLoad，其核心全在于“懒”这个字眼上。虽然，这个字在生活中更多的是表示一种贬义，可正如气体有活性和惰性的区别，这里我们将其理解为延迟加载，或许会更合适一点，因为生活早已告诉我们，只要你打算偷懒，就一定会造成拖延。因此，懒加载其实就是一种通过延迟加载对网页性能进行优化的方法。一个典型的例子是，当网页中有滚动条的时候。此时，网页的一部分区域对于浏览器视窗而言是不可见的。如果将一次性将其加载出来，这其实是一种资源的浪费，因为你不确定用户是否有耐心浏览完整个网页。在对网页的浏览量进行评估的时候，通常都会有一个跳出率的概念。可想而知，用户更容易被网页上的超链接吸引，在不同的网页间跳转。退一步讲，如果一个网页上有非常多的图片，等待这些图片全部加载完会浪费大量时间，进而影响到用户体验。博主原本就是为了减少首屏渲染时间，所以，不管从哪一个角度来看，懒加载或者说延迟加载，对于前端的性能优化都有着极其重要的意义，而这正是博主写作这篇文章的原始动机所在。
骨架屏利用懒加载来提升用户体验 如何实现懒加载 我们知道，对于图片而言，我们只要设置了其 src 属性，它就可以自动载入图片。因此，图片的懒加载，其实就是让设置 src 属性这个行为延迟执行，譬如，当一张图片出现在用户的视野当中的时候，我们再去设置其 src 属性，这样就可以达到延迟加载的目的。显然，首次需要加载的图片数量越少，首屏渲染时间就会越短，这不正是我们想要达到的目的吗？基于这种朴实无华的思路，这里我们介绍三种实现延迟加载的方案，如果大家还有更好的方案，欢迎大家在评论区补充或者讨论。
监听滚动事件 首先，我们最容易想到的一种思路是，监听网页的滚动事件，因为我们更希望看到的结果是，当元素滚动到可视视口内的时候再去加载。此时，问题的关键是如何判断当前元素在可视视口内，在解决这个问题之前，我们先来看看下面这幅图片，它展示了网页中的 clientHeight、scrollTop 以及 offsetTop 这三个数值间的关系：
clientHeight、scrollTop 以及 offsetTop 可以注意到，当 clientHeight(H) + scrollTop(S) &amp;gt; offsetTop 的时候，即表示当前元素位于可视视口内。基于这个思路，我们可以编写出下面的代码：
let lazyLoadByDefault = function(imgs) { var H = document.documentElement.clientHeight; var S = document.documentElement.scrollTop || document.body.scrollTop; for (var i = 0; i &amp;lt; imgs.length; i++) { if (H + S &amp;gt; getTop(imgs[i])) { if (imgs[i].</description></item><item><title>杂感·七月寄望</title><link>https://blog.yuanpei.me/posts/miscellaneous-feelings-of-july/</link><pubDate>Sat, 16 Jul 2022 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/miscellaneous-feelings-of-july/</guid><description>桃花潭里没有桃花，正如老婆饼里没有老婆，只要你没有期待，就永远不会失落。不同的是，长安这座城市，难保不会教人想起，那桩发生在天宝年间的人类早期电信诈骗事件。相传，对李白仰慕已久的汪伦，深知李白好饮酒、喜游历，便诚恳地给李白写了一封信，称当地有万家酒楼、十里桃花。李白听闻以后，欣然前往，结果万家酒楼变成了万氏酒楼、十里桃花变成了桃花渡口。可即便如此，李白还是被汪伦的盛情打动，在临别时写下那首脍炙人口的诗篇《赠汪伦》。如果仔细考究一番的话，这个桃花潭也许在安徽，而想到李白就会想到的长安，李白总共不过待了三年，从这个角度看，桃花潭这个名字或许是长安沾了李白的光。乘着三号线地铁从地下转为地上，依次经过桃花潭、广运大桥、世博园，三四年前的我，还是一个喜欢掺和草莓音乐节的文青，而如今再度故地重游的时候，七月的桃花潭已经零零星星地浮起荷花，对我而言，我的七月就是从这里开始的，仿佛某种不一样的风景。
刺杀首相 熟悉我的朋友，应该都知道我喜欢玩《刺客信条》这款游戏，我曾经和康师傅一起见证波士顿倾茶事件、参与邦克山战役，我曾经和谢伊一起在里斯本大地震中心有余悸，我曾经和爱德华一起目睹黑胡子惨死在面前……作为一名刺客，我经常在游戏中刺杀各种历史人物，譬如约翰·彼凯恩、查尔斯·李、劳伦斯·华盛顿……等等，可当一件真实的刺杀事件发生在自己的有生之年，这种感觉更多的是一种震惊。因为我以往对刺杀的认知，更多的是像荆轲刺秦、安重根刺杀伊藤博文、宗次郎刺杀大久保利通(参见浪客剑心)、裴迪南夫妇遇刺引发一战、美国总统肯尼迪遇刺……毫无例外，这些事情对我来说都是相当久远的事情，其模糊程度丝毫不亚于我出生那一年苏联解体，而这一次它就真实地发生在我三十岁的人生节点上，所以，它带给我的冲击是完全不同的。事情发生以后，人们的心态显得非常微妙，因为有幸灾乐祸的，更有深谋远虑的。面对好事，人们就觉得那难以触及的远方和自己息息相关；面对坏事，人们就觉得“今朝有酒今朝醉”、“各人自扫门前雪”那是正道的光。
以刺杀为主题的游戏：刺客信条 在伊坂幸太郎的《金色梦乡》里，曾虚构过一起刺杀日本首相的案件。主人公青柳是一个普通的宅急送司机，在他平淡如水的生活里，唯一的高光时刻是两年前救过某个女明星。在被好友森田约出来钓鱼之后，他就被卷入到了这起刺杀日本首相的案件当中，从公众眼中的正义化身，被迫变成政府的通缉犯。在这场国家机器和个体的对抗中，面对突如其来的构陷，再无后路可言的青柳，不得不靠着人类最大的武器——习惯和信赖，展开一场惊心动魄的生死大逃亡。在现实生活中，人们无一例外地都喜欢看到一个公众人物从神坛上跌落下来。因此，当冰冷的体制对弱势的个体实施迫害的时候，最好的办法就是令对方的形象彻底失真。人们常说，为了更多的人，牺牲掉一小部分人在所难免，可在一个常态的社会里，遇到非常时期人们该如何自处？如果牺牲掉的一小部分人里刚好有你，你又该如何展开自救？就像这将近三年的新冠疫情，频频被顶上热搜的报道，固然是我们希望别人看见的，可在我们看不见的地方，是否还有我们看不见的牺牲？即使他们是像青柳一样，并不是罪犯或者真正对社会有危害的危险人物。
同名电影《金色梦乡》剧照：众生 金色梦乡 我以为，人类的情感是建立在某个事件上的共同记忆，譬如青柳一行人大学毕业后在烟花厂一起打工，前女友始终记得男主用拇指按压电梯按键的习惯，以及男主和父母独特而隐晦的报平安的方式……正是这些情感的连接点，构成了这本书里最温情的一面。十一月的仙台，在这场蓄谋已久的阴谋里，或许是冰冷刺骨的，可当漫天烟花凌空绽放时，一定会有某个人在某个地方与我心意相通，那一刻披头士的 Golden Slumbers 在耳边响起，年少时的故梦、逃亡途中留下的眼泪和汗水、终于以一种炽热的状态在夜空中快速熔化，而后又复归于平淡和冰冷。那天，看完《人生大事》，从电影院出来我就发了条朋友圈，大意说，以后死了做成烟花好像是种不错的选择。我不知道，这是不是一种呼应，就像披头士的歌词里写道，“曾经有一条回家的路”，可到故事结尾的时候，青柳已经再找不回曾经的自己，他通过整容手术改头换面，以一个新的身份继续生活下去，而那些美好的回忆，终于还是变成，再也无法回头的金色梦乡，即使是对成为“优等”人的青柳雅春而言。
同名电影《金色梦乡》剧照：烟火 所以，大概还是要感谢伊坂幸太郎，亲手了编织这样一场梦。这段时间，书实在没有怎么看，因为自从得知 Kindle 要退出中国市场，忍不住一声喟叹，甚至连带着对微信读书的好感都有所影响，倒不是我厚此薄彼，实在是这段时间接触到了新的媒介——播客，在某种意义上，是比微信读书更适合拿来听的一种介质。这一切的契机来自早见 Hayami，一开始是因为她在上海疫情期间写的一篇文章《我在方舱，看见老人们的孤岛求生》。后来，就听了她的两期播客，听她讲亲密关系、讲“言语犹如微小剂量的砷”。对于这个只有 26 岁的年轻人，我折服于她文笔之细腻、观点之新颖。可有的时候，我觉得她像是变了一个人，动辄喜欢输出群体性失望情绪，比如典型的“男人不行”。作为一个写作者，我当然知道，写别人愿意听到的声音，更容易带来流量，可如果你被读者的声音绑架，你就会失去创作的自主性。有时候，我觉得男人和女人仿佛是来自两个星球的生物，女人抱怨说，这个世界上没有正常的男人；而男人亦抱怨说，这个世界上没有正常的女人。可讽刺的是，这个正常的定义都掌握在对方手中，人人都不想被别人定义，可人人又都想要定义别人。如果我说我支持平权，你们是不是依然会对我口诛笔伐，就像大家都忘了安倍是“骑墙派”这件事情一样。有时候，看到前任经常在网上掺和这些事情，我唯有感慨，这终究是我回不去的金色梦乡啊……
同名电影《金色梦乡》剧照：涅槃 双标现场 当然，人类对待事物的看法，不单单会受性别这种非此即彼的的因素的影响，哪怕是同一个因素，依然摆脱不了如宿命一般的双标。朋友 T 君家中亲人去世，此君不无遗憾地说到，“老人家才活了 80 多岁就走了”，对于这种说法，我深以为然，毕竟从我 18 岁开始，每年都会听到，“你都多少多少岁了”，类似这样的话来，我们总是催促着小孩长大，又像哄小孩一样安抚着老人。如此一来，小孩仓促间长大，突然背负起时代和家庭给予的期望、责任；而老人略显笨拙地拨弄着智能手机，拐杖上贴满了各种各样的核酸贴纸，生怕因为落后被这个时代嫌弃。对此，我想说：都挺好。2022 年注定是一个多事之秋，除了隔三差五卷土重来的疫情，俄乌冲突、安倍遇刺、约翰逊辞职、斯里兰卡破产、断供潮……，每一件事情看起来都可以写入历史教科书，只是我越来越难以回答，造成这些事件的直接原因和根本原因分别是什么。也许，这些问题到底是需要后人来回答，我有我的历史局限性和阶级局限性。我甚至不知道，安倍遇刺这个事件会不会成为育碧未来的素材，可从一名刺客信条玩家的角度来看，山上徹也实在算不上一名好的刺客。
真正的刺客：十步杀一人，千里不留行 这个月月初，我在豆瓣的下厨房小组发了篇帖子，整理了我平时经常做的快手饭菜，没想到网友们对我的厨艺还是有一点认同的，不像某个人一直蹭我的饭，还整天对我的厨艺颇有微词。这让我意识到，做饭这件事情是具有私密性的，因为每个人对饭菜的口感、喜好各不相同，甚至吃饭这件事情在每个人心中的重要性亦有所差别。我是一个对吃和穿没什么太高要求的人，上班带饭很大原因上是为了省钱，所以，我不能理解有的人嚷嚷着带红烧肉是什么心态。做饭的人都知道，做饭 80% 的工作量都在前期准备阶段，所以，作为一个每天早上起来做饭的人，我必须让自己避开那些复杂的菜式。因为，说到底，我是一个怕麻烦的人，向来不喜欢麻烦别人。对吃饭的人而言，只要带上嘴巴和筷子就行，可对做饭的人来说，不单单要关心买菜做菜，还要关心柴米油盐酱醋茶、关心洗碗刷锅。写到这里的时候，我大概明白了，做家务这件事情到底累在哪里，所谓的眼里有活儿还是没活儿，更多的是角色与身份上的认知差异。一旦想通了这一点，你就没必要再为这件事情纠结。因为，于我而言，所谓吃饭的仪式感，一大半都在做饭的过程中，我最喜欢三下五除二、风卷残云的感觉啦！
同名电影《金色梦乡》剧照：优等 总体而言，不论国家与国家之间，还是人与人之间，这个世界总是在不遗余力地寻求表面的平衡，而过于寻求平衡的这个过程，注定是逆人性、反人类的，就像我们都在寻求长期稳定的亲密关系，可两个人之间的关系，并不完全遵循万有引力定律，而是像磁场、像风、像雨一样有强有弱，换言之，这种关系并不稳定，并且将永远处于动态平衡之中。毕竟，没有人喜欢世界上到处都是被操控着的提线木偶，比起表面上的平衡，保持这个世界原本的质感，是不是要显得更为重要一点呢？</description></item><item><title>支持外部链接跳转的 Vue Router 扩展实现</title><link>https://blog.yuanpei.me/posts/implementation-of-vue-router-extension-that-supports-external-link/</link><pubDate>Tue, 12 Jul 2022 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/implementation-of-vue-router-extension-that-supports-external-link/</guid><description>众所周知，Vue Router 是 Vue 中重要的插件之一，特别是在当下流行的 单页面应用/SPA 中，这种感觉会越来越明显。此时，路由的作用就是根据 URL 来决定要显示什么内容。诚然，页面这个概念在工程/模块中依然存在，可当你开始关注最终发布的产物时，你会发现本质上它只有一个页面。无论你选择 hash 或者是 history 模式的路由，它都像是在同一张纸上反复写写画画，让你看起来觉得它有很多个不同的页面。回顾早期的前端项目，它往往会有多个不同的页面组成，我们是通过一个个的超链接来实现不同页面间的跳转。如今，这一切都已一去不复返，我们只能在单页面应用的世界里继续披荆斩棘。当然，绝大多数的普通用户无法感知到这种程度的变化，在他们的眼中，那依然不过是普通的一个超链接。那么，当一个项目中充斥着各种各样的超链接的时候，这个问题就值得我们单独拿出来讲一讲。所以，今天这篇博客的主题是路由和外部链接。请注意，这是一组相对通用的概念，不受限于任何一个前端框架，我们只是选择了使用 Vue 来进行说明。
问题现状 我们的项目存在着大量的超链接以及导航菜单，在 UI 设计阶段，通常不会有人关心，一个链接到底是内部链接还是外部链接。与此同时，由于 HTML 这门标记语言的极大灵活性，实现一个导航链接的方式有 N 多种，可以是一个 a 标签，可以是一个 div 标签，甚至可以是一个 span 标签。虽然 Vue Router 里提供了 router-link 组件，可在实际的项目中，需要综合考虑团队风格和第三方 UI 库的因素，甚至有时候，再没有设计规范的情况下，可能大家连 router-link 组件都不愿意用或者说压根就没机会用。
这样就造成一个非常尴尬的局面，当你需要为页面编写业务代码的时候，你不得不在各种各样的超链接上浪费时间，只要不是通过 a 标签实现的，你都必须处理它点击的事件，更不必说，你还要区分这个链接是一个内部链接还是一个外部链接，原因是 Vue Router 不支持外部链接，你不得不通过 window.location 或者 window.open() 的这样的方式来实现“曲线救国”，试想，如果每一个都这么折腾一遍，你还会觉得有趣吗？
而在我们的项目里，实际上它还需要从网页端唤起应用，这样便又涉及到了 URL Schemes 这个话题。除了 Android 和 iOS 这个平台上的差异，单单就 Windows 而言，其基于注册表的方案对协议提供者的约束并不强，如果团队内对此没有任何规范的话，你将面对各种千奇百怪的参数传递方式。听到这里，你是不是感觉头都大了一圈？如果因为某种原因，它还需要你每次都传递一个令牌过去，你告诉我，你准备如何让这一切的混乱与不堪重新归于宁静呢？
学如逆水行舟，不进则退 改进思路 OK，现在假设，我们制止这场混乱的方式，是强迫大家都去使用 router-link 这个组件，虽然它最终渲染出来就是一个 a 标签。相信参加工作以后，大家都会有这样一种感觉，那就是工作中 99.9% 的事情，都是在最好和最坏中间选一个过渡状态，然后不断地为之投入精力或者叫做填坑，甚至有很多东西，从来都不是为了让一件事情变得更好而存在。作为这个地球上脆弱而渺小的个体，时间、生命、爱，每一样东西都像缓缓从指尖滑落的沙子，我们实在是太喜欢这种可以掌控点什么的感觉了。所以，如果一件事情没法从道理或者科学上讲通的话，那就用制度或者规范来作为武器，在一个连国家都可以宣布破产的年代，大概，话语权比是非对错更重要。因此，在博主的博客里，在这小小的一方天地里，不妨假设我有这种话语权，可以强迫大家都使用 router-link 这个组件。我们讲，Vue Router 不支持外部链接，一个非常直观的理由是，当我们写出下面的代码时，它会完全辜负我们的期望：</description></item><item><title>视频是不能 P 的系列：OpenCV 和 Dlib 实现表情包</title><link>https://blog.yuanpei.me/posts/make-memes-with-opencv-and-dlib/</link><pubDate>Fri, 01 Jul 2022 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/make-memes-with-opencv-and-dlib/</guid><description>2020 年年底的时候，博主曾心血来潮地开启过一个系列：视频是不能 P 的，其灵感则是来源于互联网上的一个梗，即：视频不能 P 所以是真的。不过，在一个美颜盛行的时代，辨别真伪实在是一件奢侈的事情，在各种深度学习框架光环的加持下，在视频中实现“改头换面”已然不再是新鲜事儿，AI 换脸风靡一时的背后，带来是关乎隐私和伦理的一系列问题，你越来越难以确认，屏幕对面的那个到底是不是真实的人类。古典小说《红楼梦》里的太虚幻境，其牌坊上有幅对联写道，“假作真时真亦假，无为有处有还无”。果然，在这个亦真亦幻的世界里，哪里还有什么东西是不能 PS 的呢？在“鸽”了很久很久之后，博主决定要来更新这个系列啦，让我们继续以 OpenCV 作为起点，来探索那些好玩、有趣的视频/图像处理思路，这一次呢，我们来聊聊 OpenCV、Dlib 和 表情包，希望寓教于乐的方式能让大家感受到编程的快乐！
环境准备 python -m pip install opencv-python python -m pip install opencv-contrib-python python -m pip install Pillow python -m pip install numpy python -m pip install imutils python -m pip install dlib 请注意，如果通过 pip 安装 dlib 不大顺利，你可以到 https://github.com/sachadee/Dlib 这个仓库中下载对应的 .whl 文件。例如，博主使用的是 64 位的 Windows 系统，而我的 Python 版本是 3.7，因此，我下载的是 dlib-19.22.99-cp37-cp37m-win_amd64.whl 这个文件。此时，我们可以用下面的方式来安装 dlib：
python -m pip install dlib-19.22.99-cp37-cp37m-win_amd64.whl 除此以外，我们还需要下载 dlib 所需的模型文件，下载地址为：http://dlib.</description></item><item><title>不得不说的 ASP.NET Core 集成测试</title><link>https://blog.yuanpei.me/posts/i-have-to-say-asp.net-core-integration-testing/</link><pubDate>Tue, 07 Jun 2022 15:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/i-have-to-say-asp.net-core-integration-testing/</guid><description>一直打算写一篇关于 ASP.NET Core 集成测试 的文章，因为一旦说起单元测试这个话题，多多少少会牵动我内心深处的理想主义色彩，虽然如今已然是程序员职业生涯的第七年，可在我看来依然有太多东西在原地打转。这一路跌跌撞撞地走过来，在不同的公司里，见识到了形态各异的研发流程，接触到了貌合神离的敏捷思想，阅读过了风格迥异的框架/架构。当时间节点来到 2022 年，惊觉 .NET 诞生业已 20 周年，虽然技术一直在不断向前发展，可我个人感觉，我们并没有在工程化上取得多少感人的进步，譬如单元测试、需求管理，这些听起来丝毫不影响写代码的方方面面。回首往昔，有坚持写单元测试的公司，有从来不写单元测试的公司，有因为业务或者人力扩张而放弃写单元测试的公司，俨然是软件研发领域的众生相。作为程序员，每天除了和各种 Bug 斗智斗勇以外，接触最多的当属测试或者叫做 QA，所以，今天这篇博客，我们一起来聊聊 ASP.NET Core 里的集成测试。
Moq：万物皆可模拟吗 我们说，单元测试这个话题，多少带点理想主义色彩，究其本质，是因为我们相信，只要软件中的最小可测试单元的输出符合预期，那么，整个软件的输出就是符合预期的。对于程序员而言，软件中的最小可测试单元，通常是一个方法或者函数，因此，通常意义上的单元测试，是指对一个模块、一个方法/函数或者一个类进行正确性检验的测试工作，并且这个工作讲究隔离性，换句话说，是指软件中的最小可测试单元在不依赖外部因素的情况下进行的独立测试。最近这几年，大家会发现，随着微服务、云原生、Serverless 等等理念的流行，我们的软件正在变得越来越复杂，复杂到让你打断点、单步调试都成为一种奢望。在这种情况下，单元测试的理想主义色彩就开始凸显出来，现实世界中的软件常常存在着大量的依赖或者说耦合，而为了消除这些外部因素，人们会在单元测试中使用 Mock 这一技术来进行模拟。不过，博主想说的是，万物皆可模拟吗？
什么是单元测试？ Moq 是 .NET 平台下最常用的模拟库，它可以利用动态代理出模拟一个接口的行为。前面提到，单元测试针对的是最小的测试单元，而当这个最小的测试单元依赖某个外部因素的时候，就需要对其进行模拟，从而保证整个测试环节满足隔离性的要求。举个例子，没有人会为了喝一口水而专门去挖一口井。此时，喝水这个动作即是最小的测试单元，而这个动作本身依赖着一口井，所以，我们需要对井这个外部因素进行模拟。我相信，这足以道出 Mock 和 单元测试 这两者间千丝万缕的的联系。以喝水这件事情为例，我们该如何模拟出一口井呢？假设我们可以通过下面的接口 IWaterProvider 来获得一定体积的水：
interface IWaterProvider { Water GetWater(); } 此时，按照 Moq 的套路，我们可以快速地挖一口“井”出来：
var mock = new Mock&amp;lt;IWaterProvider&amp;gt;(); mock.Setup(x =&amp;gt; x.GetWater()).Returns( new Water() { Name = &amp;#34;农夫山泉&amp;#34;, Volume = 1.5M } ); // 现在，你已经有了一口井 :) var well = mock.Object; var water = well.</description></item><item><title>再议 DDD 视角下的 EFCore 与 领域事件</title><link>https://blog.yuanpei.me/posts/review-efcore-and-domain-events-from-ddd-perspective/</link><pubDate>Sat, 28 May 2022 16:37:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/review-efcore-and-domain-events-from-ddd-perspective/</guid><description>在上家公司工作的时候，我们有部分业务是采用事件/消息驱动的形式。虽然，当时博主还没能用上诸如 Kafka、RabbitMQ 这样的消息中间件，可数据库 + Quartz 这样一个堪称“简陋”的组合，完全不影响博主对事件/消息驱动这种思想的启蒙。后来，在实现数据库审计、数据同步 等问题的时候，更是从实践层面上加深了这一印象。再后来，博主陆陆续续地接触了 DDD，其中 领域事件 的概念，第一次让博主意识到，原来事件可以和聚合根产生某种联系。退一步讲，即使你没有接触过 DDD，你只要听说过 MediatR 或者 CQRS，相信你立马就能明白我在说什么。最近的一次 Code Review，这个问题再次浮出水面，一个人在面对过去的时候，会非常容易生出物是人非的感慨，代码和人类最大的区别就在于，代码可以永远以某种永恒的形式存在，就像很多年后我打开高中时候用 Visual Basic 编写的程序，它依然可以像我第一次看见它一样运行。所以，一直在变化的大抵是我，无非是人类更擅长自我说服，它让你相信你一直“不忘初心”。因此，今天我想再聊聊 DDD 视角下的 EFCore 与 领域事件。
似曾相识燕归来 其实，人生中有特别多的似曾相识，就像 Wesley 老大哥和我说起 Kubernetes 的时候，我脑海中一直浮现着的画面，是第一次见到他的时候，他意气风发地给我讲 MSBuild 和 单元测试。为什么会记得他意气风发的样子呢？大概是有一天我到他这个年龄的时候，我终于羡慕彼时彼刻的他，还拥有着这样一副意气风发的面孔罢。对于大部分事件/消息驱动的业务，相信大家都见到过类似下面这样的代码片段：
// 保存订单 var orderInfo = new OrderInfo( address: &amp;#34;陕西省西安市雁塔区大雁塔北广场&amp;#34;, telephone: &amp;#34;13456789091&amp;#34;, quantity: 10, remarak: &amp;#34;盛夏白瓷梅子汤，碎冰碰壁铛啷响&amp;#34; ); _repository.Insert(orderInfo); _chinookContext.SaveChnages(); // 发布消息 var orderInfoCreateEvent = orderInfo.Adapt&amp;lt;OrderInfoCreateEvent&amp;gt;(); eventBus.Publish(orderInfoCratedEvent) 这段代码非常容易理解，当我们创建完一个订单以后，需要发布一条订单创建的消息。当时组内做 Code Review 的时候，大家都普遍认为，Publish() 需要放在 SaveChanges() 后面，理由是：如果 Publish() 放在 SaveChanges() 前面，可能会出现消息发出去了，而数据没有保存成功的情况。这个想法当然没有问题，唯一的问题在于，实际业务中构造消息的过程绝不可能如此简单，如果它依赖中间过程的变量或者参数，你不可能总是有机会把这个过程放到 SaveChanges() 后面，更不必说，实际业务中可能会要求你在订单里处理客户相关的事件。显然，这种方案对代码的侵入非常严重。那么，有没有更好一点的方案呢？</description></item><item><title>Vue.js 前端项目容器化部署实践极简教程</title><link>https://blog.yuanpei.me/posts/a-simplified-tutorial-on-containerized-deployment-of-front-end-projects-for-vue/</link><pubDate>Tue, 17 May 2022 13:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/a-simplified-tutorial-on-containerized-deployment-of-front-end-projects-for-vue/</guid><description>大概一周前，在某个「微雨燕双飞」的下午，我正穿梭于熙熙攘攘的车流人海当中，而被雨水濯洗过的天空略显灰白，傍晚亮起的路灯恍惚中有种朝阳初升的错觉，内心更是涌现出一种「一蓑烟雨任平生」的豁达，我还没来得及给这场内心戏添油加醋，兴哥的电话突然打断了我的思绪。一番攀谈交心，我了解到，他想问的是前端容器化部署的相关问题。虽然，靠着兴哥的睿智、果敢，他第二天就想明白了整个事情的来龙去脉；但是，这完全不影响我水一篇博客出来。所以，今天这篇文章，我们来聊聊前端项目的容器化部署，并提供一个极简的实践教程，这里以 Vue.js 为例，希望对大家有所启发。
你说，这像太阳吗？ 首先，我们来编写 Dockerfile，这里采用的是多阶段构建的做法，第一个阶段，即 build，主要是利用 node.js 基础镜像来实现前端项目的发布，所以，你可以看到 package.json、npm install 以及考虑到国情的 cnpm install 这些前端项目中喜闻乐见的东西，安装完依赖以后我们通过 npm run build 来完成打包，这取决于你项目中实际使用的脚本或者命令，如果你不喜欢 npm，你同样可以用 yarn 来编写这些指令，只要你喜欢就好。做人嘛，最重要的是开心！
# build FROM node:lts-alpine as build WORKDIR /app COPY package*.json ./ RUN npm install -g cnpm --registry=https://registry.npm.taobao.org RUN cnpm install COPY . . RUN npm run build # deploy FROM nginx:stable-alpine as deploy COPY --from=build /app/dist/ /usr/nginx/wwwroot COPY /nginx/nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD [&amp;#34;nginx&amp;#34;, &amp;#34;-g&amp;#34;, &amp;#34;daemon off;&amp;#34;] OK，第二个阶段，即 deploy，前端发布出来的产物是无法直接在浏览器里打开的，这一点你平时用 Vue.</description></item><item><title>再见，人间四月天</title><link>https://blog.yuanpei.me/posts/say-good-bye-to-april/</link><pubDate>Tue, 03 May 2022 12:30:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/say-good-bye-to-april/</guid><description>昨天从外面回来的时候，夕阳的余晖已被街市上的灯火掩没，直到渐渐地远离了闹市，夜晚的氛围终于在微风中扑面而来。抬头看时，深蓝色的天空中零星点缀着三两颗星星，我来不及驻足，已被人流裹挟着向前走去。像往常一样，我转身走进菜场，人依然是那些人，不过陌生和熟悉实在是两种风景，譬如眉清目秀的“豆腐西施”，陌生时你只觉得清冷，而熟悉时你会觉得杀伐果断。这一个月下来，卖菜的阿姨甚至记住了我，看我买了圆白菜、胡萝卜和洋葱，便问我是打算做炒面麽，我还是像过去一样点点头，她立马催促我抓紧时间去买面条，因为再晚一点就要收摊了。所幸的是，家里还有点面条，让我不必如此仓促、窘迫。
这个五一小长假，我还是没能回去，疫情让我回家的期盼从春节推到五一，再推下去便只有国庆节了，不知命运是否会让我如愿以偿。我问阿姨，难道你们放假都不休息吗？阿姨只是笑着说，我们放假了你去哪里买菜啊！我倒不是担心这个，只是在上班族眼中的 996、大小周，在这个世界上一部分人的眼中是如此的稀疏平常。我立马联想到了我的母亲，我每次就算回到家里，她还是像往常一样忙碌着，仿佛全然没有假期这种概念，可知道我喜欢吃饺子，每次都忙不迭地给我包饺子，大概是因为一切似曾相识，一码通升级的那几天，阿姨拜托我教她使用新版小程序。一刹那间，我突然意识到，在家里的时候，我就是这样教父母使用智能手机。
人与人的缘分，常常就是从这些小事情开始，虽然它像风筝线一样脆弱，脆弱到只要我们搬一次家、换一所学校、见一次面……，也许，结果就会变得千差万别。前几天，我在地铁上看见一个穿着汉服的小女孩在哭闹，我以为她是玩累了就准备让座给她，结果她是意犹未尽、不想回家，瞬间有种小丑竟然是我自己的感觉；昨天，我在地铁上看见一个推着婴儿车的同龄人，可听到对方给一两岁的孩子讲惯性，我果然还是忍不住在心里笑出了声，大概小孩能听见我内心的声音，他忽然抬起头盯着我，眼珠滴溜溜地转了半天。坦白地说，我有没有遇见这些人，其实对这些人的生命轨迹毫无影响，可在那一刻，我有收到一点点微弱的讯息，一种生活中不经意间流露出来的美。诚然，这一瞬间的相遇，不会影响我回家的方向，不会影响我内心的想法，可它就像夜空里不甘寂寥的星星一样，自作主张地装饰了一番我的生活。
朋友曾经问我，是不是特别喜欢小孩子？我想，没人能拒绝一个在夕阳下大口吃着米花的孩童，可喜欢是不是就一定要生一个呢？与其说我喜欢小孩子，不如说我更喜欢它们懵懂、天真、无忧无惧的状态，而一切的岁月静好、世事安稳，背后负重前行的那个人，我看不到、更感受不到，所以，我会觉得浪漫、觉得可爱，世界上到底有没有互相感同身受的两个人呢？我想，我永远无法回答，我唯一能做的就是不停地怀念，那些已经永远逝去的日子，或者是夏天坐在凉席上吃西瓜，帮爷爷拨弄头上的白头发，或者是牵着一个人的手在汹涌的人潮里穿梭，两边满是红色的灯笼高高垂下。有人说，世上有种温柔叫做《夏目友人帐》，可四月终究是在熙熙攘攘中滑走啦，就像疫情过后再去青龙寺，终究是错过了樱花呢？
太阳照在我的背上是如此的温暖惬意，很多东西就像陈年的老风湿一样，如果能经常拿出来晒一晒，那么下雨天就不会感到那么疼痛。以前，我不理解，为什么老年人晒太阳，一坐就是半天。等到长大了才明白，目之所及，皆是回忆；心之所想，皆是过往；眼之所看，皆是遗憾。前几天，我在家里翻来覆去地找了好几遍，找一副放了很久的画，也许是搬家的时候弄丢了吧，虽有遗憾，不逃避、不沉沦，这大概就是我当下的心境。以前出去压马路，我说我是为了学习别人的穿搭，朋友说我这是东施效颦。现在出去，会说这样穿蛮好看的，假如我有 1 米 8 的大长腿，穿出来一定比他还要好看。就这样，一路从真维斯、森马逛到海澜之家，再到优衣库，虽然我没有社牛症，可以当众裸露上身试衣服，可至少看见喜欢的衣服，会对着镜子亲自穿来试试看，期待更好的自己，接纳当下的自己，这就是我现在的生存哲学。
某个时刻，报话大楼的钟声缓缓敲响，这是对四月的告别，这是对五月的期盼，我看了看手机，相差两分钟，可那又有什么关系呢？早上做完核酸，这回发的是王昭君的贴纸，我说这是打算出塞吗？不知道目的地是哪里？当然，最坏的结果无非是被封在小区里，不要说出塞，连家都出不去。朋友又开始讲扩大桃花运云云，这人怎么这么迷信啊！古人说，「人间四月芳菲尽，山寺桃花始盛开」，四月芳菲已尽，为了扩大桃花运，我应该去山上寺庙里出家当和尚。呵，果然啊，只有魔法才能打败魔法啊。</description></item><item><title>Python 图像风格化迁移助力画家梦想</title><link>https://blog.yuanpei.me/posts/a-introduction-to-stylized-migration-of-python/</link><pubDate>Sun, 01 May 2022 13:32:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/a-introduction-to-stylized-migration-of-python/</guid><description>很多年前，星爷在《食神》这部电影里大彻大悟，「只要用心，人人都是食神」。从那个时候起，这句话就隐隐约约带着返璞归真、回归本心的意思。如同电影里描绘的餐饮行业一样，在资本市场的裹挟下，造神这项运动显得轻而易举，这个食神可以是史蒂·周，可以是唐牛，可以是任何人。因此，当穷困潦倒的史蒂芬·周，因为一碗叉烧饭而落泪的时候，我想，这或许是一种直面自我的顿悟。毕竟，电影里的星爷原本就不会做饭。《舌尖上的中国》带火了一句话，“高端的食材，往往只需要最简单的烹饪”，在我看来，这同样是一种“人人都是食神”的自我暗示。多年以后，互联网行业炙手可热的彼时彼刻，一句“人人都是产品经理”让无数人发现，提需求的门槛居然如此的低。其实，早在 1967 年，德国艺术家约瑟夫·博伊斯就曾语出惊人，“人人都是艺术家”，联想到“鸡娃”教育下的各种艺术特长培训班，这句话大概是真的。你内心深处是否同样保留着某种艺术家的梦呢？那么，此时此刻，博主想和大家分享的话题是图像的风格化迁移。
走近风格化迁移 提到风格化迁移这个概念的时候，大家可能会感到陌生，所以，我们不妨用相近的概念来进行类比。纵观人类的历史长河，初唐四杰、唐宋八大家的诗文各有千秋，李杜诗篇、苏辛长短句各领风骚，更不必说书法上的颜筋柳骨、苏黄米蔡。我曾经在碑林博物馆密密麻麻的石碑中，近距离看到人们如何将石碑上的文字拓下，我开始在脑海里徜徉，是否人类一切伟大的创造都是起源于模仿？这种思绪最终在艾伦·图灵的传记电影 《模仿游戏》 中找到了某种回应，就像人工智能领域里的神经网络，其实就是在模仿人类的大脑进行思考，甚至退一万步讲，当我们还是一个婴儿的时候，襁褓中的牙牙学语、蹒跚学步，这其实还是一种模仿。那么，如果要给风格化迁移下一个定义的话，其实就是让人工智能来对某种风格或者特点进行“模仿”，以图像的风格化迁移为例，它可以将梵高、莫奈或者毕加索的绘画风格“移植”到一张目标图片上，如下图所示：
Neural-Style-Transformer 示意图 它可以借由梵高《星空》这副作品中的色彩，来「绘制」一副不一样的向日葵，虽然，梵高一生中创作了无数幅向日葵，在他人生的不同阶段，或表达对生命的渴望，或刻画出死亡的压抑。由此可见，风格化迁移其实可以理解为，不同流派绘画风格的一种“模仿”。当然，这一切都是由计算机通过特定的算法来实现，你可以想象一下，当你通过描摹字帖的方式来练字时，本质上就是在模仿那些书法家们的笔划，而如果将一切的行为都转化为数学公式，这其实就是一种风格化迁移啦！
卷积神经网络(CNN)在图像风格化迁移上的应用 目前，图像的风格化迁移，主要的算法支撑来自下面这两篇文章：
A Neural Algorithm of Artistic Style Instance Normalization: The Missing Ingredient for Fast Stylization 其中，前者提出“用神经网络来解决图像风格化迁移”的思路，而后者则是在此基础上引入了“可感知的损失”这一概念，如果大家有兴趣的话，不妨读一读下面这篇文章，它更像是一篇综述性质的文章，可以帮助你快速了解图像风格化迁移的前世今生，个人感觉，读这类文章会让你快速地认识到自己的无知，这或许是一件好事。
Neural Style Transfer: A Review 坦白讲，博主是第一次接触神经网络。所以，要学习陶渊明，「好读书，不求甚解」。如果大家确实对这块内容感兴趣的话，还是建议亲自去读一下这些文章，我就不在这里班门弄斧啦！(逃
体验风格化迁移 好了，当我们对图像风格化迁移有了一定的了解以后，下面我们来快速体验下图像风格化迁移。OpenCV 在 3.3 版本后，正式引入了 DNN ，这使得我们可以在 OpenCV 中使用 Caffe、TensorFlow、Torch/PyTorch 等主流框架中训练好的模型。这里，我们主要参考了 OpenCV 官方的 示例代码:
def style_transfer(pathIn=&amp;#39;&amp;#39;, pathOut=&amp;#39;&amp;#39;, model=&amp;#39;&amp;#39;, width=None, jpg_quality=80): &amp;#39;&amp;#39;&amp;#39; pathIn: 原始图片的路径 pathOut: 风格化图片的路径 model: 预训练模型的路径 width: 设置风格化图片的宽度，默认为None, 即原始图片尺寸 jpg_quality: 0-100，设置输出图片的质量，默认80，越大图片质量越好 &amp;#39;&amp;#39;&amp;#39; ## 读入原始图片，同时调整图片至所需尺寸 img = cv2.</description></item><item><title>在 Vue.js 中使用 Mock.js 实现接口模拟</title><link>https://blog.yuanpei.me/posts/interface-mock-implemention-using-mock.js-in-vue.js/</link><pubDate>Fri, 15 Apr 2022 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/interface-mock-implemention-using-mock.js-in-vue.js/</guid><description>最近这段时间，我一直在参与一个前端项目。每当我从庸碌的生活中赢得片刻喘息的时候，我不由得感慨，在程序员朴实无华且枯燥的职业生涯里，写自己喜欢的代码的机会少之又少，写别人喜欢的代码的机会俯拾皆是，更多的时候像是“为他人作嫁衣裳”。古人云，“遍身罗绮者，不是养蚕人”，当每天面对着被改得面目全非的代码的时候，内心固然早已波澜不惊、宠辱偕忘，可还是会期待美好的事情发生，因为从工程化的角度而言，每天都在思考的事情，其实就是怎么样做会更好一点。过去这些年里，微服务、前后端分离的呐喊声不绝于耳，实际应用过程中则是会遇到各种各样的问题。在今天这篇文章里，我想和大家聊聊 Vue.js 结合 Mock.js 实现接口模拟这个话题，为什么选择这个话题呢？我个人认为，它实际上触及了前后端分离的“灵魂”，并且由此可以引出像文档管理、流程控制等等一系列研发协同的问题。你或许会忍不住问道，前后端分离的“灵魂”是什么呢？各位看官们稍坐，且听我一一道来！
问题现状 在谈到前后端分离这个话题的时候，在公司层面上对应地往往是组织架构的分离，典型的做法就是让前端和后端成为两个不同的团队，其中，前端团队负责表示层的实现，不限于页面布局、样式风格、交互逻辑等等；后端团队负责数据接口的实现，不限于数据库设计、接口设计、编写 API 等等。对应到 Vue.js 里，前端团队负责写各种各样的页面/组件、数据绑定，后端团队负责提供各种各样的数据接口，这听起来非常地合理，对不对？的确，主流的前后端分离实践都是这样讲的，所以，我们只要套用这个模型，就可以达到预期的效果，对不对？可惜，人类习惯于为这个世界寻找某种颠扑不破的真理，可恰恰人类本身才是这个世界里最不稳定的存在？疫情常态化的当下，每次都被病毒一通嘲讽，抄作业都不会抄啊！
前后端分离模式下的协同开发 首先，第一个问题，前、后端团队没有形成“契约”，前端团队拿到原型以后就开始设计页面，ViewModel 中的字段命名、定义完全是由前端团队凭“感觉”写出来的，人类离谱就离谱在，可以靠“感觉”这种玄之又玄的东西决定很多事情。这样做的后果就是，后面真正对接后端接口的时候，发现大量的字段没法对应上，不得不再折腾一遍数据绑定，如果是中途由别人来接手，那么面对的可能就是不同的数据结构间的映射转换。试想，后端程序员尚有 AutoMapper 和 Mapster 可以用，前端程序员可就没有那么幸运啦！更不必说，前端天生比后端面临更频繁的改动，只要涉及到页面布局、交互逻辑的变化，ViewModel 的修改基本无可避免，这样就导致同一个页面多次返工，我相信这个结果大家都不想看到。
其次，当前、后端团队约定好接口文档以后，双方都按照这份接口文档去完成各自的开发工作，这样听起来简直不能更合理对不对？实际上，在后端团队完成接口开发以前，前端团队会有一段时间的“真空期”或者“黑写期”，因为前端并不知道这段代码能否在真实的环境下工作。此时，前端团队可能会造一点假数据来进行接口模拟，得益于 JavaScript 这门语言的高度灵活、自由，前端团队可能会直接调用一个本地函数来返回假数据，这意味着它并不会触发真实地 HTTP 请求。那么，当有一天后端团队完成了接口开发，你将会把这些本地函数替换为 Axios 的方法，甚至在更极端的情况下，前端团队不能访问后端团队的接口，此时，双方会就本地函数还是 Axios 方法产生一场拉锯战，你告诉我，还有什么比这更折磨一个人的吗？
所以，综合下来，其实是两个非常普遍的问题：
第一，前、后端团队如何制定一份对协同有利的接口文档，这份文档是通过工具生成还是人工编写。我个人是特别讨厌用 IM 或者邮件来发送接口文档的，因为没办法做到版本控制或者说让所有手中都有一份最新的接口的文档。
第二，如何管理项目中用到的各种假数据，以及如何让项目在假数据和真实接口中“无痛”切换。前端项目的特点是所见即所得，这让它比看不见、摸不着的后端项目更受用户青睐，毕竟还有什么比能让用户亲眼看到更亲切的东西呢？
在“小步快跑、快速迭代”的敏捷思想的驱使下，我们经常需要给用户演示各种功能。也许，在某个时刻，页面上的数据亦真亦假，你还会觉得，管理这些假数据没什么意义吗？而这正是驱使我了解 Mock.js 的动力所在，世上的很多事情，你未必能如愿以偿、做到最好，可你依然要了解什么是最好，“山不厌高，海不厌深”，向不那么完美的世界妥协是现实，永远值得去追寻更完美的世界是理想，这两者在我心目中并不冲突，你觉得呢？
改进思路 OK，既然找到了问题的症结所在，我们逐一对症下药即可，就像“三过家门而不入”的大禹，选择用疏导的方式治水，让洪水通过疏通的河道流到大海中去，而不是靠一味地“堵”，程序中 90% 的代码都是在给用户“打补丁”，防止对方做出什么骚操作来，那么，是不是可以用某种方式去引导对方呢？我最讨厌听到的话就是，用户想要怎么怎么样，这是没有办法的事情，如果只需要一个传话筒，我们为什么不直接用传呼机呢？作为一个老古董，恐怕现在的 00 后都不知道什么是传呼机。你生命中当下流行或者推崇的东西，总有一天会过期。可即便如此，你还是要全力以赴。显然，这是个哀伤的故事。
Swagger 对于接口文档的管理问题，我自始至终都推荐 Swagger 这个神器，因为我和这个世界上的绝大多数的程序员一样，都认同一种相对朴素的价值观，即 “懒惰是一种美德”。因为我不喜欢靠人工来维护接口文档，所以，只要有机会用上 Swagger，我一定会用 Swagger 来管理接口文档。不管是过去写 API 和 MVC，还是现在写 gRPC。对我来说，选择 Swagger 是一件自然而然的事情，因为我懒，因为我不理解为什么有人需要导出 Word 或者 Pdf 格式的接口文档。也许，Swagger 那千篇一律的页面风格会让人感到无所适从，喜欢的人非常喜欢，讨厌的人非常讨厌。在前、后端分离的项目中，有一份白纸黑字的接口文档，显然要比“口口相传”靠谱得多。当然，如果你有足以媲美 Swagger 的接口文档管理工具/平台，欢迎大家在评论区留言分享。下面是我曾经写过的关于 Swagger 的文章：
通过 ApiExplorer 为 Swagger 提供 MVC 扩展 gRPC 搭配 Swagger 实现微服务文档化 .</description></item><item><title>利用 ASP.NET Core 中的标头传播实现分布式链路追踪</title><link>https://blog.yuanpei.me/posts/asp-net-core-using-headerpropagation-for-distributed-tracking/</link><pubDate>Thu, 07 Apr 2022 09:34:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/asp-net-core-using-headerpropagation-for-distributed-tracking/</guid><description>在此之前，我曾写过一篇博客，《Envoy 集成 Jaeger 实现分布式链路追踪》，主要分享了 ASP.NET Core 应用如何结合 Envoy 和 Jeager 来实现分布式链路追踪，其核心思想是：生成一个全局唯一的 x-request-id ，并在不同的微服务或者子系统中传播该信息。进而，可以使得相关的信息像一条线上的珠子一样串联起来。在此基础上，社区主导并产生了 OpenTracing 规范，在这个 规范 中，一个 Trace，即调用链，是由多个 Span 组成的有向无环图，而每个 Span 则可以含有多个键值对组成的 Tag。不过，当时我们有一个非常尴尬的问题，那就是每个微服务必须显式地传递相关的 HTTP 请求头。那么，是否有一种更优雅的方案呢？而这就是我们今天要分享的内容。首先，我们来回头看看当初的方案，这是一个非常朴实无华的实现：
[HttpPost] public async Task&amp;lt;IActionResult&amp;gt; Post([FromBody] OrderInfo orderInfo) { var paymentInfo = new PaymentInfo() { OrderId = orderInfo.OrderId, PaymentId = Guid.NewGuid().ToString(&amp;#34;N&amp;#34;), Remark = orderInfo.Remark, }; // 设置请求头 _httpClient.DefaultRequestHeaders.Add( &amp;#34;x-request-id&amp;#34;, Request.Headers[&amp;#34;x-request-id&amp;#34;].ToString()); _httpClient.DefaultRequestHeaders.Add( &amp;#34;x-b3-traceid&amp;#34;, Request.Headers[&amp;#34;x-b3-traceid&amp;#34;].ToString()); _httpClient.DefaultRequestHeaders.Add( &amp;#34;x-b3-spanid&amp;#34;, Request.Headers[&amp;#34;x-b3-spanid&amp;#34;].ToString()); _httpClient.DefaultRequestHeaders.Add( &amp;#34;x-b3-parentspanid&amp;#34;, Request.Headers[&amp;#34;x-b3-parentspanid&amp;#34;].ToString()); _httpClient.DefaultRequestHeaders.Add( &amp;#34;x-b3-sampled&amp;#34;, Request.Headers[&amp;#34;x-b3-sampled&amp;#34;].ToString()); _httpClient.DefaultRequestHeaders.Add( &amp;#34;x-b3-flags&amp;#34;, Request.Headers[&amp;#34;x-b3-flags&amp;#34;].ToString()); _httpClient.DefaultRequestHeaders.Add( &amp;#34;x-ot-span-context&amp;#34;, Request.</description></item><item><title>读《一个叫欧维的男人决定去死》</title><link>https://blog.yuanpei.me/posts/a-man-called-ove/</link><pubDate>Tue, 29 Mar 2022 09:34:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/a-man-called-ove/</guid><description>最近读了一本书，来自瑞典作家弗雷德里克·巴克曼的处女作，《一个叫欧维的男人决定去死》，该书于 2015 年被改编成同名电影，主要讲述了一个孤独老者生命中最后三周的故事，它的情节是如此的简单和质朴：一个一心赴死的、固执老人，不断尝试使用各种方法“杀”死自己，结果因为一对新邻居的到来而频频被打断。随着寻求欧维帮助的人越来越多，欧维对于这个世界的牵挂越来越深，妻子死后变成黑白两色的单调世界，开始被这些贸然闯入的人们刷上新的色彩，而欧维同样成为了别人生命里的色彩。所以，这是一个温情而治愈的故事，欧维从决定自杀到放弃自杀，从排斥邻居到接纳邻居，这一系列的转变，让我不由得感慨：原来生活本身，就是人与人之间的羁绊，终其一生，我们追求的幸福感，不过是被别人需要和认同。
莫名地想起项脊轩志 一开始看到这个设定，会很容易地联想到东野圭吾的 《嫌疑人 X 的献身》，大抵都是一个对生活绝望的人一心求死，结果因为某一个人或者某一件事而做出改变。我想，大家对类似的设定感兴趣，或许是我们都渴望着被某一个人救赎，可即使是像石神哲哉这样智力超群的人，依然不会得到爱，我个人更愿意将这种情感，理解为一种介于人性和神性之间的美。从某种意义上来讲，沉浸在数学世界里的石神哲哉，妻子过世后只有黑白两色的欧维，其实是非常相似的两个人，都是一个自认为不被社会认可和需要的人，因为一次次被需要，然后重新体会到生命的价值。可惜，欧维比石神哲哉更幸运一点，因为他有一群笨拙而又温柔的邻居，正是这些“不速之客”们的“袭扰”，一点点地让这个外表固执、内心温柔的老人敞开心扉，最终同这个满目疮痍的世界完成和解。
曾经被称为“来自地狱的恶邻”的男人 回顾欧维的一生，命运带给了他无数的苦楚，幼年丧母、少年丧父，失去至爱双亲的欧维，从父母那里继承的“遗产”，不过是一栋摇摇欲坠的房子、一辆即将散架的萨博 和 一块早已变形的腕表。当然，更不幸的是，十八岁那年，一场大火让他连这座木屋都要失去。直到后来，他遇见了一个人，那个人注定要在他生命中出现，然后和他组成家庭。那是一个开朗大方、热爱生活的漂亮女孩，她的一抹绛唇、红色高跟鞋，刹那间变成了欧维生命中唯一的色彩，“人们总说，欧维眼里的世界非黑即白，而她是色彩，他的全部色彩。”，在遇见索雅以前，欧维的生活没有方向，他只是机械地完成着每天的工作，像机器一样精确而古板地生活着，“从来没人问过欧维，遇见她之前他是怎么生活的。但要是有人问起，他一定会回答说，自己没有生活。”，同样地，对索雅来说，在遇见欧维以前，她只爱书、她的爸爸和猫，可命运还是决定，让两个性格迥异的人产生联系。
在火车上邂逅生命里唯一的色彩 童话故事告诉我们，王子最终一定会和公主走到一起，命运让索雅邂逅了不善言辞的欧维，让她看到了欧维木讷外表下的温柔、固执性情里的可靠，她就像夏日里温暖的一束阳光，突然照进了欧维黑白分明的二维世界里。在索雅的鼓励下，欧维考取了建筑工程师，有了属于自己的家庭。索雅的书实在太多了，甚至连厨房和架子上到处都是书。每当这个时候，他总是不声不响地为她做出一个个书架，直到那些书架填满了整整一面墙。一个真正爱你的人，会默默地把你说过的每一句话都记在心里。很多时候，欧维就是在这种平静而又温柔的日子里，静静地等待着即将出生的孩子。可惜，命运并不能体谅这种一眼万年的情感，它只是面无表情地向欧维施加着生命不能承受的苦楚，在和妻子去西班牙旅行的途中，一场意外让索雅失去了孩子、失去了行走能力，后半辈子只能在轮椅上度过。
在轮椅上追逐生命里不变的色彩 对于欧维而言，他更喜欢那些有确定规则和结果的东西，就好像只要遵循发动机的原理，就可以让那辆萨博启动一样。父亲曾教会他最重要的品质——诚实，此后的很多年里，他固执地坚持着那些别人不以为然的规则，譬如每天早上都在社区里巡逻、检查每户人家的仓库门锁是否完好、明令禁止别人在社区里开车、查看垃圾是否合理摆放……等等，所以，他完全不能理解这个世界，为什么没人愿意为妻子修建轮椅的步道，为什么身穿“白衬衫”的政府工作人员像冷血动物一样不通人情……每当这个时候，索雅总是温柔地对他说，“人要么死去，要么就想办法活着。”，她从未失去过对于生活的那份勇气和乐观，她选择踩踏着丈夫修建好的那段斜坡一次次走进教室，并最终成为了孩子们严眼中最好的老师，她失去了自己的孩子，她拥有着无数的孩子。在索雅被癌症夺走生命以前，这份炽热与光明始终温暖着欧维的心。
生存还是毁灭？这是一个问题 对生命而言，其实就是一个不断失去着的过程，当时间的皱纹一点点地爬上额头，这个命运多舛的男人还是失去了所有。在妻子去世半年以后，工厂里告诉他以后不用再去上班，虽然他曾像大多数人一样，在这里耗费四十多年的时光，“他做了一切社会需要他做的事，工作，从不生病，结婚，贷款，缴税，自食其力，开正经的车，社会是怎么报答他的呢？它冲进办公室让他卷铺盖回家，这就是报答。某个星期一，突然他就没用了”。也许，正是从那一刻起，欧维决定用死亡来对抗这个冷漠的世界，这个社会已经没法再生产出结实到可以吊死人的绳子，“这是一个还没过期就已经过时的世界。整个国家都在为没人能正经做事起立鼓掌，毫无保留地为平庸欢呼喝彩”，欧维同样不习惯这个没人会换轮胎、装开关、换瓷砖、粉刷墙壁、倒拖斗车的社会，如果说索雅是他生命里唯一的色彩，那么，当这唯一的色彩消失不见的时候，这个叫做欧维的男人决定去死。
一个叫欧维的男人决定去死 毫无疑问，我们都是孤独的，而唯有爱才是生命里终极且永恒的救赎，真正让欧维不再寻死的救赎是爱，是那种发自内心的，对别人的关爱，正是一次又一次的“麻烦”，让别人意识到，这个有一点古怪、固执的老人，虽然有一套严谨、古板的处世哲学，其实内心深处是一个特别温柔的男人。曾经，他有过一个最要好的朋友鲁尼，他们一起为社区制定了公约、一起参与社区委员会主席的竞选，可最终他们因为汽车品牌的分歧而分道扬镳。也许，真正让欧维厌恶的，并不是好友买了一辆与自己完全不同的汽车，而是这个世界，对这个一无所有的老人不大温柔。欧维无法适应平板电脑这种新型电子设备、无法适应信用卡代替现金，他可以为了一张优惠券和店员争执上半天……这样一个被称为“来自地狱的恶邻”的男人，在妻子离开这个世界以后，显得与整个世界格格不入，他最“正常”的时候，是像年轻时那样穿好西装、戴上帽子、手里捧着一束鲜花去墓地看望妻子，那一刻他的世界突然又有了色彩。
在彼此的生命里互为装饰 真正的转机来自一对叫做帕瓦尼的新邻居的出现，他们找欧维帮忙倒拖斗车、借梯子修房子，让欧维开车带他们去医院、顺便帮助照看孩子。后来，他们甚至让欧维教他们开车……这一次又一次的“麻烦”，突然间让欧维卸下了冰冷的外壳，他以为他是生命里只有黑白两色，可在孩子们的眼中，他是这个世界上唯一的色彩，甚至成为邻居眼中的“老父亲”，孩子们眼中的“外公”，他突然意识到，原来他对于这个世界而言还有存在的意义，原来他还可以被别人需要、被这个世界需要……从那以后，这个失去了太多东西的男人，决定与这个冰冷的世界达成和解，他不再对抗那些命运带给他的痛苦和折磨，而是选择平静、温和的面对它们，他救下了一只在雪中瑟瑟发抖的猫咪、替曾经的朋友鲁尼修好暖气、把曾经给自己孩子准备的摇篮留给邻居刚刚降生的孩子……英雄迟暮，他曾失去过母亲、父亲、妻子和孩子，而这一刻，他有了一个“女儿”，成为了三个孩子的“外公”，在不同人的生命里互为装饰……
最美好的永远都成为回忆 没有人是一座孤岛，我们一生追求的幸福感，很大程度上都来自这种认同感或者说被别人需要的感觉，我们总说，人是一切社会关系的集合，正是在被别人不断地“麻烦”着、“被需要”着，我们才能正视生命个体在这个广袤世界里的价值，虽然，这种关系的琐碎，曾令我们产生种种喜怒哀乐的的情绪，可生命的过程原本就属于体验派，你追求的一切事物都会消亡然后又被新的事物替代，就像生老病死之于生命、春华秋实之于四季。爱会不会消失，我始终不得而知，我只知道，欧维在面对心脏病、面对死亡时，多了一份平静和坦然。他曾经尝试过无数种寻死的方法，可就是这样一个“心大”的老头儿，此刻，终于能卸下那张冰冷的面具，以他本来的面目示人。多年以前，当他在火车上第一次遇见那个女人时，她正踩着深红色的高跟鞋，涂抹着深红色口红的嘴唇边正挂着微笑，他们两个人是那样的年轻，欧维抬起手的一瞬间，他的世界充满了色彩，欧维依然穿着那件浅蓝色的西装、戴着那顶圆形帽子，他没有说话，我想，应该是在对着某个人微笑……</description></item><item><title>利用 gRPC 实现文件的上传与下载</title><link>https://blog.yuanpei.me/posts/use-grpc-to-realize-file-upload-and-download/</link><pubDate>Sun, 20 Mar 2022 09:34:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/use-grpc-to-realize-file-upload-and-download/</guid><description>几天前，某人同我抱怨，说是某接口无法正常工作，坦白地讲，这只是程序员生命里再枯燥不过的日常，因为无论“好”或者“不好”，他们都要努力回应来自灵魂深处的那声“为什么”。所以，善待程序员的方式之一，就是不要总问他“为什么”，因为他已经听了太多的“为什么”。经过一番攀谈交心，我了解到是模型绑定出了问题。原来，他需要实现一个导出/下载功能，因为他不确定能否通过 Envoy 代理来自 gRPC 的文件流，故而，他选择了传统的 Web API，结果不曾想在模型绑定上栽了跟头。听完了他的话，我不禁陷入了沉思，难道 gRPC 真的不能做文件的上传和下载吗？常言道，“实践出真知”，所以，今天这篇博客，我们来聊聊利用 gRPC 实现文件的上传和下载。
定义 Protobuf 首先，我们来看 Protobuf 的定义，此前介绍 gRPC 流式传输相关内容的时候，我一直找不到一个更为贴切的场景，而此时此刻，我只想说，冥冥中自有天意，难道还有比上传和下载更好的例子吗？
service FileService { rpc UploadFile(stream UploadFileRequest) returns (UploadFileResponse); rpc DownloadFile(DownloadFileRequest) returns (stream DownloadFileResponse); } //Upload message UploadFileRequest { string FileName = 1; bytes Content = 2; } message UploadFileResponse { string FilePath = 1; } //Download message DownloadFileRequest { string FilePath = 1; } message DownloadFileResponse { bytes Content = 1; } 其中，UploadFile是一个针对客户端的流式接口，DownloadFile是一个针对服务器端的流式接口，可以注意到，这其实非常符合我们平时对于上传/下载的认知，即，对上传而言，客户端以二进制流的形式作为输入；对下载而言，服务器端以二进制流的形式作为输出。在 Protobuf 的定义中，二进制流可以使用 bytes类型来表示，因此，我们在 UploadFileRequest 和 DownloadFileResponse 这两个类型中，统一使用 Content 这个字段来表示上传或者下载过程中的二进制流。</description></item><item><title>七种武器：延迟队列的原理和实现总结</title><link>https://blog.yuanpei.me/posts/summary-of-the-principle-and-implementation-of-delay-queue/</link><pubDate>Mon, 07 Mar 2022 09:34:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/summary-of-the-principle-and-implementation-of-delay-queue/</guid><description>“这是最好的时代，这是最坏的时代”，英国作家查尔斯·狄更斯在两百多年前写下的这句话，如果从辩证的角度来看，它或许可以适用于任何一个时代。我们生活在一个怎样的时代呢？我想，或许是一个矛盾的时代。因为，有时它让你对未来有无限的期待，有时它又会让你陷入无尽的绝望，特别是当集体和个人的命运形成强烈反差的时候，当实用主义、精致利己主义开始盛行的时候，我们偶尔会感慨罗曼蒂克的消亡、怀念从前慢、追忆芳华，可下一秒就被卷入到同时间赛跑的庸庸碌碌当中。生活节奏越来越快，人们越来越追求实时、速度、效率，选择当下的同时，意味着选择实时满足，譬如，我想吃一块美味的蛋糕，我现在就要吃。与之相对的，则被称之延迟满足，譬如，制定一个长期的写作计划以实现个人知识网络的构建。由此可见，人生本来就有快有慢、有张有弛，此时，便引入了这篇文章的主题——延迟队列。
什么是延迟队列 延迟队列，即 DelayQueue，所以，顾名思义，首先，它是一个队列，对于队列这种数据结构，相信大家都不陌生啦！这是一种先入先出(FIFO)的数据结构，就像现实生活中排队讲究先来后到一样，普通队列中的元素都是有序的。相比普通队列，延迟队列主要多了一个延迟的属性，此时，元素何时出队不再取决于入队顺序，而是入队时指定的延迟时间，它表示该元素希望在经过该指定时间后被处理。从某种意义上来讲，延迟队列更像是一种以时间作为权重的集合。我想，单纯地介绍概念，不一定能真正深入人心，所以，请允许我举几个生活中的例子：当你在网上购物的时候，如果下单后一段时间内没有完成付款，那这个订单就会被自动取消；当你通过 Outlook 预约了会议以后，Outlook 会在会议开始前 15 分钟提醒所有与会人员；当你在网上叫外卖以后，平台会在订单即将超时前 10 分钟通知外卖小哥&amp;hellip;这样看起来，是不是顿时觉得延迟队列的使用场景还是挺广泛的呢？因为工作上的关系，博主接触类似场景的机会还是蛮多的，所以，想系统地研究下相关的技术，最终，就有了今天这篇博客，下面我们来看看具体的实现方式有哪些。
延迟队列的实现方式 延迟队列思维导图 我知道，在一个短视频横行的时代，人们的注意力注定要被那些实时满足的事物消耗掉，在我有预感到，不会有多少人愿意在我这篇自以为是的文字前驻留的时候，我唯有识趣地放出这个思维导图，TLDR的这种心理，其实我完全可以感同身受，因为看一部电影永远比看一本书容易，当媒介从文字变成图片再到视频，本质上是我们获取信息的能力下降了，我们变得只能接受低密度的信息。当然，这是一个时代的症结，你可以拥有你的选择，是独善其身还是随波逐流？
数据结构 JDK 中提供了一个延迟队列的实现 DelayQueue，位于 Java.util.concurrent 这个包下面，它是一个 BlockingQueue，本质上封装了一个 PriorityQueue，队列中的元素只有到达了Delay时间，才允许从队列中取出。如下图所示，队列中放入三个订单，分别设置订单在当前时间的第 5、10、15 秒后取消：
延迟队列示意图 对于 Java 中的 DelayQueue 而言，其对应的代码实现如下面所示：
Order Order1 = new Order(&amp;#34;Order1&amp;#34;, 5, TimeUnit.SECONDS); Order Order2 = new Order(&amp;#34;Order2&amp;#34;, 10, TimeUnit.SECONDS); Order Order3 = new Order(&amp;#34;Order3&amp;#34;, 15, TimeUnit.SECONDS); DelayQueue&amp;lt;Order&amp;gt; delayQueue = new DelayQueue&amp;lt;&amp;gt;(); delayQueue.put(Order1); delayQueue.put(Order2); delayQueue.put(Order3); System.out.println(&amp;#34;订单延迟队列开始时间:&amp;#34; + LocalDateTime.now().format(DateTimeFormatter.ofPattern(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;))); while (delayQueue.size() != 0) { Order task = delayQueue.</description></item><item><title>gRPC 流式传输极简入门指南</title><link>https://blog.yuanpei.me/posts/grpc-streaming-transmission-minimalist-guide/</link><pubDate>Fri, 18 Feb 2022 09:34:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/grpc-streaming-transmission-minimalist-guide/</guid><description>最近一直在研究 gRPC 的 ServerReflection，顾名思义，这是 gRPC 里提供的反射接口，当你需要获取某个接口的描述信息，或者是希望动态调用 gRPC 的时候，这一切就会变得非常有用，如果你经常使用 gRPC UI 这款工具来调试 gRPC 接口，那么，你一定会注意到一件事情，即它要求服务端必须支持 ServerReflection API，而这一点在 ASP.NET Core 中已经得到支持，对此感兴趣的朋友可以参考官方文档。当然，这并不是我想表达的重点(我就知道)。重点是什么呢？在使用 ServerReflection API 的过程中，我发现它采用了 gRPC 双向流的方式来进行交互，在过去的日子里，我研究过诸如 WebSocket、Server-Sent Events 等等服务器推送的技术，我意识到这是一个非常接近的技术，所以，今天这篇文章，我们来一起聊聊 gRPC 中的流式传输。
从 HTTP/2 说起 首先，我想说，流式传输并不是一个新的概念，这一切就好像，即使你从来没有听过流媒体的概念，可这并不妨碍你追剧、刷短视频，隐隐然有种“不识庐山真面目，只缘身在此山中”的感觉。随着网络带宽和硬件水平的不断提升，越来越多的云服务变得像水、电、天然气一样寻常，以此作喻，流式传输，就像你打开水龙头，此时，水就会源源不断地流出来，并且可以做到随用随取。因此，流式传输实际上就是指通过网络传输媒体，例如音频、视频等的技术统称，服务器可以连续地、实时地向客户端发送数据，而客户端不必等所有数据发送完就可以访问这些数据。按照实现方式的不同，流式传输可以分为 实时流式传输 和 顺序流式传输 两种，前者通常指RTP/RTCP，典型的场景是直播；后者通常是指由 Nginx、Apache 等提供支持的顺序下载。
HTTP/1.1 vs HTTP/2 如果你对 HTTP/2 有一定了解的话，就会知道它最为人所知的特性是多路复用。在 HTTP/1.1 的时代，同一个时刻只能对一个请求进行处理或者响应，换句话说，下一个请求必须要等当前请求处理完才能继续进行，与此同时，浏览器为了更快地加载页面资源，对同一个域名下的请求并发数进行了限制，所以，你会注意到一个有趣的现象，部分网站会使用多个 CDN 加速的域名，而这正是为了规避浏览器的这一限制，HTTP/1.1 时代，可以称为“半双工模式”。到了 HTTP/2 的时代，多路复用的特性让一次同时处理多个请求成为了现实，并且同一个 TCP 通道中的请求不分先后、不会阻塞，是真正的“全双工通信”。一个和本文更贴近的概念是流，HTTP/2 中引入了流(Stream) 和 帧(Frame) 的概念，当 TCP 通道建立以后，后续的所有操作都是以流的方式发送的，而二进制帧则是组成流的最小单位，属于协议层上的流式传输。
gRPC 中的流式传输 OK，现在我们正式开始 gRPC 流式传输的话题。首先，对于一个 gRPC 接口而言，它的起源是 Protobuf 定义。所以，一个最为直观的认识是从 Protobuf 定义入手：</description></item><item><title>烟波梦影，从天国王朝到刺客信条</title><link>https://blog.yuanpei.me/posts/861688878/</link><pubDate>Thu, 03 Feb 2022 18:30:45 +0000</pubDate><guid>https://blog.yuanpei.me/posts/861688878/</guid><description>最近看了一部叫做《天国王朝》的电影，主要讲述了第三次十字军东征时期的一段故事：法兰克铁匠巴利安，因为受到失散多年的父亲的召唤，亦是为了替自杀而死的妻子寻求救赎，来到了三教圣城——耶路撒冷。其间，父亲亡故，巴利安承袭了爵位和封地，甚至得到了西贝拉公主的青睐，可这依然无法阻止他深陷十字军的政治漩涡。当时，身染麻风病的耶路撒冷王鲍德温四世与阿拉伯传奇英雄萨拉丁，维持着基督教与伊斯兰教之间脆弱的和平；而以居伊和雷纳德为首的好战势力，则通过袭击穆斯林的方式不断挑起争端。战争终于不可避免地爆发了，这正是历史上著名的哈丁之战。此后，狮心王理查一世独自面对东方世界的滚滚黄沙，东西方的军事、宗教和文化碰撞出火花，更是堪比双子星一般的存在。
天国王朝中的巴利安男爵形象 圣城耶路撒冷 熟悉刺客信条系列的朋友，此刻应该会想到，这个系列的第一部作品，正是取材于第三次十字军东征时期，主角阿泰尔则是活跃在该时期的一名叙利亚刺客。因为电影中出现了刺客的敌对势力——圣殿骑士(团)。所以，我觉得透过历史去打通电影和游戏会是一件非常有趣的事情。也许，中世纪时期发生的事情，到今天已然无法做到存伪去真，可正如我曾经迷恋过亚瑟王从石头中拔出(伊甸)圣剑的故事一样，圆桌骑士或者说骑士本身，在一个(中二)男人眼中是接近武侠小说里侠客的存在。所以，在一个武侠没落的时代，你就不难理解，我为什么会喜欢上刺客信条这样一款游戏。虽然，这些骑士相当迷信，动辄要通过决斗让上帝来裁决，可就像塞万提斯笔下的堂·吉诃德一样，当他准备大战风车的那一刻，你又会觉得他是一个英雄。
刺客信条中阿泰尔施展信仰之跃 故事要从哪里说起呢？我想，应该从鲍德温四世那场著名的战役——蒙吉萨之战说起。那一年，年仅 16 岁的鲍德温四世，率领三千人击败了两万人的萨拉丁军队。这段历史带给我的震撼，丝毫不亚于中国历史上的官渡之战和淝水之战。如果你对此毫无概念，不妨对比一下孙权的合肥之战，当时的孙权号称有十万人的兵力，而张辽只有八百人，史称“孙十万与张八百”。这一场战役令鲍德温四世获得了极好的威望，而雷纳德则因为参与了这场战役而成为主战派的首领。可惜，这场堪称为传奇的胜利仅仅为耶路撒冷换来了两年的和平。1779 年鲍德温四世兵败泉水谷，病情的恶化令他的身体每况愈下，到 1785 年他因麻风病去世的时候，他只有 24 岁，电影里始终以面具示人的就是鲍德温四世。后期的耶路撒冷王国，基本是由他的姐夫居伊以及雷蒙德三世把持，朝局的不稳定无疑加速了耶路撒冷王国的覆灭。
天国王朝中的鲍德温四世形象 相传，鲍德温四世出生时，是由其伯父鲍德温三世主持洗礼的。当时，国王鲍德温三世决定把自己的名字作为礼物赠送给这个新生儿，一旁的大臣开玩笑地说道，“作为一国之王，如果只是赠送一个名字，是否显得太过吝啬了呢？”。国王听完以后，大笑一声，指着圣(真)十字架说道，“那我就再送一份礼物给他——耶路撒冷之王”。后来，鲍德温三世突然得了重症，而他又没有子女。于是，他的侄子，即鲍德温四世继承了他的王位，成为了新一任的国王。对于历史而言，我们永远无法假设，我们无法想象这个 16 岁就击萨拉丁的少年，如果没有染上麻风病，又会在历史上留下怎么样的故事？本片的男主角巴利安，其原型在历史上被称为“伊贝林的巴利安男爵”，曾经参与过蒙吉萨之战，支持雷蒙德三世摄政，基本可以认为是主和派。电影中主张袭击穆斯林的，主要是居伊、雷纳德以及圣殿骑士团。
十字军与圣殿骑士 好了，现在总算和刺客信条产生某种联系了。在刺客信条第一部中，圣殿骑士主要是以十字军的形象出现。事实上，十字军东征是由天主教教会发起的解放圣地耶路撒冷的一项运动，因为按照基督教、伊斯兰教、犹太教各自的说法，耶路撒冷都是它们心目中的圣地。可是，从十一世纪末开始，耶路撒冷及其周边的拜占庭地区，一直都被穆斯林占领。因此，罗马教廷以解放圣地的名义发动了多次东征，这些东征的军队服饰均以红十字作为标志，故而称为“十字军”。十字军占领耶路撒冷以后，很多欧洲人前往圣地朝圣，为了保护这些朝圣者的安全、攻击异教徒，1 名法国贵族和 8 名骑士建立了军事性质的修会，因为其地点位于所罗门神殿的遗址上，故又称为圣殿骑士团。类似的组织，还有医院骑士团、条顿骑士团。其实，在初代刺客信条中，阿泰尔的刺杀对象里就有医院骑士团的成员。
天国王朝中的十字军形象 从今天的角度来看，所谓的“讨伐异教徒”的圣战，其本身并不见得有多么神圣，神殿骑士团侵略和掠夺的成分更多一点，反倒是育碧用自由和秩序的命题，让圣殿骑士多了一点人性的光辉，从三代开始，圣殿骑士和刺客都不再是那种非黑即白的设定，甚至到法国大革命前夕，双方都意识到合作的可能性，可惜，这一切终究毁在各自阵营里的狂热分子手上，就像耶路撒冷王国是毁在一个毫无军事素养的居伊一样。法国刺客亚诺·多利安不无遗憾地说到，“现在我懂了，诸行并非都得到允许，而是教条本身即为一种警告”。我们继续说回电影，截止到 1187 年，通过著名的哈丁之战，萨拉丁终于夺回圣地耶路撒冷，甚至缴获了钉死过耶稣的真十字架，他凭借自己出色的军事才能，一举收复了包括阿卡在内的众多港口城市，彻底切断了十字军在海上的补给线。
天国王朝中的萨拉丁形象 当然，不甘心的十字军，决定以英格兰国王理查一世(狮心王)和 法兰西国王腓力二世为主力，开始积极筹备又一次的十字军东征，大战一触即发。这一年，阿泰尔22岁，此时的他尚未成为刺客大师，但已然成为组织中的骨干力量。直到1191年，26岁的阿泰尔，被导师阿尔莫林派去所罗门圣殿遗址取得金苹果时，年轻而高傲的阿泰尔准备高调刺杀圣殿骑士罗伯特，这一举动把兄弟会成员马利克等彻底暴露在危险之中。而后面的故事我们都知道了，阿泰尔被剥夺了等级和武器，去完成刺杀9个圣殿骑士的任务。历史上的罗伯特，正是在前一年跟随狮心王舰队参与第三次十字军东征，甚至在1192年受封圣殿骑士团大师，可惜当他遇到阿泰尔的时候，这条仕途注定要永远地停留在1193年9月23日那一天。而这，大概就是，袖剑之下，众生平等，万物为虚，万事皆允。
世界的十字路口 转眼间，时间来到12世纪，狮心王和萨拉丁，西方和东方，基督教和伊斯兰教，都在这一刻走向了对立面。如果说，土耳其的伊斯坦布尔是亚洲和欧洲的十字路口。那么，毫无疑问，这将会两个当世雄主间的终极对决。1191年9月7日，狮心王理查在阿苏夫会战中击败萨拉丁，在十字军进军耶路撒冷的途中，萨拉丁不断派出穆斯林骑兵进行袭扰，直到1192年1月，十字军抵达贝特努巴城堡，此时，距离圣地耶路撒冷只剩下12英里，耶路撒冷唾手可得。历史更像是一种巧合，那一年萨拉丁的埃及援军从南部抵达战场，狮心王理查一世在英国的统治地位随时都有可能被颠覆，战争顿时陷入了焦灼状态。1192年8月，互相怀有敬意的双方，在雅法城签署停战协定，史称“雅法合约”，正是从那一刻起，声势浩大的第三次十字军东征悄然落下帷幕。
狮心王理查一世影视形象 萨拉丁是否归还了真十字架，后世的我们已无从得知。我只知道，从那一刻开始，这对惺惺相惜、势均力敌的对手，再没有等来交手的机会，一年后萨拉丁去世，而理查一世则在回国途中被奥地利公爵扣押。直到1194年，经过英、奥双方多次谈判，理查一世终于获释。可这个拥有狮子般雄心壮志的传奇将领，永远都选择冲锋陷阵、选择身先士卒，获释后不久，他就再次陷入了英法战争的泥潭，于1199年战死沙场。以我狭隘的历史观，后世能匹敌理查一世这次东征运动的将领，也许，只有先后在莫斯科损兵折将的拿破仑和希特勒。在育碧的世界观中，马西亚夫最终没能挡住成吉思汗西征的步伐，原因是成吉思汗手中或许掌握着某种伊甸碎片。1222年，成吉思汗攻下花拉子模汗国首都撒马尔罕，丘处机远赴西域“止杀”，那一年阿泰尔57岁，他已经离开马西亚夫，开始长达20多年的“自我流放”。
结束隐居生活的阿泰尔 谁能想到，丘处机路过牛家村的那一年，他的父亲奥马尔正准备潜入萨拉丁的营地，结果在回来的时候不慎触发警报，不得不杀死一名贵族。那一年，萨拉丁大军围攻马西亚夫，在萨拉丁的逼迫之下，奥马尔以生命为代价，从萨拉丁手中换回俘虏艾哈迈德·索菲安以及萨拉丁的撤军。那一年，阿泰尔11岁，几乎在同一天，阿泰尔和阿巴斯同时失去了父亲。阿巴斯不愿意接受父亲叛变的事实，并把一切都归咎于阿泰尔的父亲，两个人内心的芥蒂自此种下。这份怨恨直到阿巴斯死去都没能放下，那一年，阿泰尔80岁，利用从金苹果中学到的知识，阿泰尔研制出了袖枪，而这段故事，我们曾在启示录中，以艾吉欧·奥迪托雷的身份亲眼见证过。对于整个第三次十字军东征而言，阿泰尔的故事更像是惊鸿一瞥，可正是这段难辨真伪的传奇故事，让刺客信条系列成为此后育碧旗下最广为人知的游戏IP，万物为虚，万事皆允，耕耘于黑暗，服侍光明，这就是刺客。
尾声 育碧说，History Is Our Playground，电影里的故事真真假假，游戏里的故事更不必说，甚至是我这些未曾考据过的文过饰非，可这恰恰是历史最为迷人的地方，一个人的形象，其实是由无数个人的记忆拼凑出来的，你可以去肆意地想象你心目中的鲍德温四世、萨拉丁，亦或者是狮心王理查一世、传奇刺客阿泰尔，只要这一切都能自圆其说。所以，育碧选择用阴谋论、用自由与秩序来填补那些虚构的情节，听起来像一款游戏对不对？可玫瑰岛这个社会实验告诉我们，这一切还真的就是一款游戏，毕竟，意大利唯一一场打赢的战争，对手是一座面积只有400平米的小岛，试想，如果西泽尔·波奇亚泉下有知，怕是连胡子都要气得歪掉？当然，请记住一句话，我说的都是错的！</description></item><item><title>关于</title><link>https://blog.yuanpei.me/about/</link><pubDate>Thu, 03 Feb 2022 00:00:00 +0000</pubDate><guid>https://blog.yuanpei.me/about/</guid><description>在这世界的角落-剧照 👨‍💻 关于我 谢谢你，在这世界的角落，找到我：
昵称：飞鸿踏雪 职业：(伪)全栈攻城狮、Yaml/Markdown工程师、刺客信条历史/物理学家、提示词魔法师 方向：数据分析、微服务、Web、AI 技术：.NET、Python、JavaScript 日常：读书(人文/历史/诗词/)、写作(技术/生活)、电影(日剧/科普/纪录片)、烹饪、洞箫、刺客信条 符号：双子座、INTP 🎨 近期工作： 对博客进行重构 微服务架构(Envoy/gRPC) 设计数据交换平台 领域驱动设计(DDD) 消息队列(Kafka) FakeRPC LLM Agent 落地 📖 近期阅读 7.3 没有意义就没有摇摆 6.2 多余的我 8.1 爱吃沙拉的狮子 9.1 遥远的向日葵地 8.2 蜉蝣直上 📽 近期观影 6.5 狄仁杰之神都龙王 7.8 情动假日 7.2 东北警察故事2 7.6 印度合伙人 8.1 克里斯托弗·罗宾 🎧 近期播放 爱在西元前 我将在何处游荡 Sundaland on mind 同じ空みつめてるあなたに アフロディーテ 🚩 更多的页面： 读书 / 听歌 / 观影 / 友链 / 统计 🏳️‍🌈 更大的世界： 微博 / 知乎 / 豆瓣 🛠️ 更多的工具： 🏠 关于博客 元视角：我手写我心 如同元编程之于软件工程、元学习之于机器学习。元视角，是一种高层次的、抽象的思维方式，用于观察和思考事物本身的模式和规律。因此，我希望写作成为我的一种思维方式，即：经常反思和分析问题解决过程、寻找不同问题之间的共同模式、思考问题的本质和更高层次的规律。写作固然是将个人的想法或认知传播给读者的过程，可我自始至终都是我的第一个读者。故而，博客以“我手写我心”为表，以元视角反哺认知为里，这便是其名称的由来。</description></item><item><title>Envoy 集成 Jaeger 实现分布式链路追踪</title><link>https://blog.yuanpei.me/posts/768684858/</link><pubDate>Fri, 14 Jan 2022 16:46:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/768684858/</guid><description>当我们的应用架构，从单体系统演变为微服务时，一个永远不可能回避的现实是，业务逻辑会被拆分到不同的服务中。因此，微服务实际就是不同服务间的互相请求和调用。更重要的是，随着容器/虚拟化技术的发展，传统的物理服务器开始淡出我们的视野，软件被大量地部署在云服务器或者虚拟资源上。在这种情况下，分布式环境中的运维和诊断变得越来越复杂。如果按照功能来划分，目前主要有 Logging、Metrics 和 Tracing 三个方向，如下图所示，可以注意到，这三个方向上彼此都有交叉、重叠的部分。在我过去的博客里，我分享过关于 ELK 和 Prometheus 的内容，可以粗略地认为，这是对 Logging 和 Metrics 这两个方向的涉猎。所以，这篇文章我想和大家分享是 Tracing，即分布式追踪，本文会结合 Envoy、Jaeger 以及 .NET Core 来实现一个分布式链路追踪的案例，希望能带给大家一点 Amazing 的东西。
可观测性：Metrics、Tracing &amp;amp;amp; Logging 分布式追踪 如果要追溯分布式追踪的起源，我想，Google 的这篇名为 《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》 的论文功不可没，因为后来主流的分布式追踪系统，譬如 Zipkin、Jeager、Skywalking、LightStep……等等，均以这篇论文作为理论基础，它们在功能上或许存在差异，原理上则是一脉相承，一个典型的分布式追踪系统，大体上可以分为代码埋点、数据存储和查询展示三个步骤，如下图所示，Tracing 系统可以展示出服务在时序上的调用层级，这对于我们分析微服务系统中的调用关系会非常有用。
分布式追踪系统基本原理 一个非常容易想到的思路是，我们在前端发出的请求的时候，动态生成一个唯一的 x-request-id，并保证它可以传递到与之交互的所有服务中去，那么，此时系统产生的日志中就会携带这一信息，只要以此作为关键字，就可以检索到当前请求的所有日志。这的确是个不错的方案，但它无法告诉你每个调用完成的先后顺序，以及每个调用花费了多少时间。基于这样的想法，人们在这上面传递了更多的信息(Tag)，使得它可以表达层级关系、调用时长等等的特征。如图所示，这是一个由 Jaeger 产生的追踪信息，我们从中甚至可以知道请求由哪台服务器处理，以及上/下游集群信息等等：
通过 Jaeger 收集 gRPC 请求信息 目前，为了统一不同 Tracing 系统在 API、数据格式等方面上的差异，社区主导并产生了 OpenTracing 规范，在这个 规范 中，一个 Trace，即调用链，是由多个 Span 组成的有向无环图，而每个 Span 则可以含有多个键值对组成的 Tag。如图所示，下面是 OpenTracing 规范的一个简单示意图，此时，图中一共有 8 个 Span，其中 Span A 是根节点，Span C 是 Span A 的子节点， Span G 和 Span F 之间没有通过任何一个子节点连接，称为 FollowsFrom。</description></item><item><title>浅议非典型 Web 应用场景下的身份认证</title><link>https://blog.yuanpei.me/posts/2478147871/</link><pubDate>Tue, 28 Dec 2021 11:53:29 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2478147871/</guid><description>据我所知，软件行业，向来是充满着鄙视链的，人们时常会因为语言、框架、范式、架构等等问题而争执不休。不必说 PHP 到底是不是世界上最好的语言，不必说原生与 Web 到底哪一个真正代表着未来，更不必说前端与后端到底哪一个更有技术含量，单单一个 C++ 的版本，1998 与 2011 之间仿佛隔了一个世纪。我真傻，我单知道人们会因为 GCC 和 VC++ 而分庭抗礼多年，却不知道人们还会因为大括号换行、Tab 还是空格、CRLF 还是 CR……诸如此类的问题而永不休战。也许，正如 王垠 前辈所说，编程这个领域总是充满着某种 宗教原旨 的意味。回想起刚毕业那会儿，因为没有 Web 开发的经验而被人轻视，当年流行的 SSH 全家桶，对我鼓捣 Windows 桌面开发这件事情，投来无限鄙夷的目光，仿佛 Windows 是一种原罪。可时间久了以后，我渐渐意识到，对工程派而言，一切都是工具；而对于学术派而言，一切都是包容。这个世界并不是只有 Web，对吧？所以，这篇博客我想聊聊非典型 Web 应用场景下的身份认证。
楔子 在讨论非典型 Web 应用场景前，我们不妨来回想一下，一个典型的 Web 应用是什么样子？打开浏览器、输入一个 URL、按下回车、输入用户名和密码、点击登录……，在这个过程中，Cookie/Session用来维持整个会话的状态。直到后来，前后端分离的大潮流下，无状态的服务开始流行，人们开始使用一个令牌(Token)来标识身份信息，无论是催生了 Web 2.0 的 OAuth 2.0 协议，还是在微服务里更为流行的 JWT(JSON Web Token)，其实，都在隐隐约约说明一件事情，那就是在后 Web 时代，特别是微信兴起以后，人们在线与离线的边界越来越模糊，疫情期间居家办公的这段时间，我最怕听到 Teams 会议邀请的声音，因为无论你是否在线，它都会不停地催促你，彻底模糊生活与工作的边界。那么，屏幕前聪明的你，你告诉我，什么是典型的 Web 应用？也许，我同样无法回答这个问题，可或许，下面这几种方式，即 gRPC、SignalR 和 Kafka，可以称之为：非典型的 Web 应用。
gRPC 相信经常阅读我博客的朋友，都知道这样一件事情，那就是，过去这半年多的时间，我一直在探索，如何去构建一个以 gRPC 为核心的微服务架构。想了解这方面内容的朋友，不妨抽空看看我前面写过的博客。从整体上来说，我们对于 gRPC 的使用上，基本可以分为对内和对外两个方面。对内，不同的服务间通过 gRPC 客户端互相通信，我们称之为：直连；对外，不同的服务通过 Envoy 代理为 JSON API 供前端/客户端消费，我们称之为：代理。一个简单的微服务示意图，如下图所示：</description></item><item><title>gRPC 借助 Any 类型实现接口的泛化调用</title><link>https://blog.yuanpei.me/posts/2617947988/</link><pubDate>Fri, 10 Dec 2021 11:53:29 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2617947988/</guid><description>我发现，人们非常喜欢在一件事情上反复横跳。譬如，以编程语言为例，人们喜欢静态的、强类型语言的严谨和安全，可难免会羡慕动态的、弱类型语言的自由和灵活。于是，在过去的这些年里，我们注意到，.NET 的世界里出现了 dynamic 类型，JavaScript 的世界里出现了 TypeScript，甚至连 Python 都开始支持类型标注。这种动与静、强与弱的角逐，隐隐然有种太极圆转、轮回不绝的感觉。果然，“城外的人想冲进去，城里的人想逃出来”，钱钟书先生说的固然是婚姻，可世上的事情，也许都差不多罢！人们反复横跳的样子，像极了「九品芝麻官」里的方唐镜。曾经有段时间，好多人吹捧 Vue3 + TypeScript 的技术栈，有位前辈一针见血地戳破了这种叶公好龙式的喜欢，“你那么喜欢 TypeScript，不还是关掉了 ESLint 的规则，项目里全部都用 Any”。对于这个吐槽，我表示非常真实，因为我们对于动与静、强与弱的心理变化是非常微妙的。常言道，“动态类型一时爽，代码重构火葬场”，你是如何看待编程语言里的动与静静、强与弱的呢？在 gRPC 中我们通过 Protobuf 来描述接口的参数和返回值，由此对服务提供/消费方进行约束。此时，参数和返回值都是静态的、强类型的。如果我们希望提供某种“泛型”的接口，又该如何去做呢？所以，这篇文章我们来聊聊 gPRC 里的 Any 类型。
Protobuf 里的 Any 类型 在讲 Any 类型前，我想，我们应该想明白，为什么需要这样一个类型？现在，假设我们有下面的 Protobuf 定义：
// Vehicle message Vehicle { int32 VehicleId = 1; string FleetNo = 2; } // Officer message Officer { int32 OfficerId = 1; string Department = 2; } 此时，按照Protobuf的规范，我们必须像下面这样定义对应的集合：
// VehicleList message VehicleList { repeated Vehicle List = 1; } // OfficerList message OfficerList { repeated Officer List = 1; } 考虑到，在C# 中我们只需要使用 List&amp;lt;Vehicle&amp;gt; 和 List&amp;lt;Officer&amp;gt; 即可，这样难免就会形成一种割裂感，因为你几乎要为每一种类型建立对应的表示集合的类型，从语义化的角度考虑，我们更希望使用下面的 Protobuf 定义：</description></item><item><title>分布式丛林探险系列之 Redis 集群模式</title><link>https://blog.yuanpei.me/posts/1213387651/</link><pubDate>Wed, 01 Dec 2021 09:58:59 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1213387651/</guid><description>时间终于来到了十二月，据说，《黑客帝国 4：矩阵重生》 将于本月在北美上映，正如同它的片名一样，黑客帝国系列在沉寂了十八年后，终于等来了一次矩阵重生的机会，不可不谓“有生之年”、“爷青回”。提及黑客帝国系列，这是一部公认的、具有划时代意义的科幻电影，除了精彩绝伦的打斗特效，最为影迷所津津乐道的，当属对于人和机器的关系这种颇具哲学意味的问题的探讨。在第二部中，The One 的部分代码被融合到了 Smith 身上，而这使得 Smith 发生变异，成为了可以自我复制的病毒。于是，我们在这里看到了 Neo 和 100 个 Smith 打斗的桥段，类似的桥段还有第三部里的雨中决斗。这些桥段或多或少地影响到了后来的电影，譬如，星爷的 《功夫》 里，阿星与斧头帮、火云邪神打斗的片段；吴京的第一部电影 《狼牙》 里，阿布雨夜大战黑衣人的片段等等。虽然，病毒的自我复制和分布式系统中的复制，是两个完全不同的概念，可当我们试图将电影和现实联系起来的时候，我们还是会不免会心一笑，因为 100 个 Smith ，大概就相当于一个 Smith 的集群；而吞噬了先知能力的 Smith ，大概就相当于这个集群中的 Leader。我们注意到，强如超人般的 Neo，一样架不住越来越多的 Smith ，最后不得不飞走，所谓：“双拳难敌四手”，这足以说明集群的重要性。好了，既然这里聊到了集群，那么我们这次来聊聊 Redis 中的集群模式。
Redis 集群概述 通过上一篇文章，我们了解到，主从复制的作用主要体现在数据冗余、故障恢复、负载均衡等方面。可很多时候，我们讲分布式，并不是说简单的复制就好啦！相信大家都听说过，水平扩展和垂直扩展这两个概念，特别是数据库的水平扩展，它天然地和分片(Sharding)联系在一起，这意味是我们希望在不同地数据库/表里存储不同地数据。此前，博主曾在 《浅议 EF Core 分库分表及多租户架构的实现》 一文里介绍过数据库的分库/表，作为类比，我们可以归纳出 Redis 集群模式的第一个特点，即：它本质上是一种服务器 Sharding 技术。因为纯粹的主从复制意味着，每台 Redis 服务器都存储相同的数据，显然这造成了资源的浪费，而让每台 Redis 服务器存储不同的数据，这就是 Redis 的集群模式。如下图所示，Redis 集群模式呈现出的一种网状结构，完全不同于主从复制间的单向流动：
Redis 集群模式示意图 从图中可以看出，6 台服务器组成了一个网状结构，任意两台服务器间都可以相互通信。也许，大家会好奇一个问题，为什么这里博主就画了 6 台服务器？其实，这一切都是有迹可循的，因为 Redis 官方规定：一个集群中至少需要有 3 个主服务器(Master)。所以，一个 Redis 集群至少需要 6 台服务器。如果从这个角度来审视集群的定义的话，你可以认为 Redis 集群就是由多个主从复制一起对外提供服务。此时，集群中的节点都通过 TCP 连接和一个被称为 Cluster Bus 的二进制协议来建立通信，这里的 Cluster Bus 你可以将其理解为 Kafka 或者 RabbitMQ 这样的支持“发布-订阅”(Pub-Sub)机制的东西，换句话说，集群中的每个节点都可以通过 Cluster Bus 与集群中的其它节点连接起来。节点们使用一种叫做 Gossip 的消息协议，据说，这是一种从瘟疫和社交网站上获得灵感消息传播方式。“六度分割”理论告诉我们，最多通过 6 个人你就能认识任何一个陌生人，同样地，最多通过 6 个节点你就可以把消息传递给任何一个节点。</description></item><item><title>写在冬阳升起以前</title><link>https://blog.yuanpei.me/posts/2145169599/</link><pubDate>Fri, 26 Nov 2021 08:48:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2145169599/</guid><description>某个冬天的清晨 突然间想要写点什么，或许是我终于发觉，一个人内心的疲惫感，是会像水里泛起的涟漪一样，一圈圈地向远方散开去，直至在某个时刻产生了共振，这种感觉就仿佛是，冬天的清晨，于雾气中升起的太阳，虽然无法令你感受到炽热，可依然会令你感到刺目甚至眩晕，圣人有云，“劳心者治人，劳力者治于人”，而这种劳心又劳力的状态，属实是一个普通打工人，经常要去面对的一件事情，因此，我想要说的，本质上是一种薛定谔的状态，它代表着温暖，代表着光明，可以给你生存所需要的一切，可于此同时，它又代表着炽热，代表着煎熬，可以毫无顾忌地灼伤你裸露的心。
就像一下子击中内心似的，我渐渐意识到，这个世界上的工作，本质上只有两种，一种是问题本身特别难，需要更多的心智上的投入，譬如数学、计算机图形学、物理碰撞等等，一个有意思的事情是，国产单机游戏《古剑奇谭3》和《仙剑奇侠传7》，男主的剑都是直接“浮空”地背在人物身上的，甚至都没有剑鞘，而《巫师3》据说光是一个拔剑/收剑就借助了相当多的技术手段，这其中最大的问题在于穿模，尤其是国产游戏都喜欢给武器加各种发光特效，很多时候为了避免穿模的尴尬，不得不在刀/剑入鞘以后隐藏刀/剑刃这部分的模型，所以，这一类问题有时候属于“看起来简单，实现起来复杂”，联想到曾经有产品经理和程序员因为需求而大打出手的新闻，你就不难理解，人们对于难易程度的认知差异，其实是非常大的，原因就在于，人们无法确定这到底属于那一类问题，有人关注整体，就有人关注局部，就像人的眼睛，视锥体永远有一个范围，这无疑让人类永远只能看到一部分，这似乎会成为某种宿命，本身就很难的问题，大概就是无论从整体还是局部来看，难度都没有变化的问题。此前，有同事要计算客户的分层活跃度，我虽然知道这是一个关乎数学期望的问题，可因为我早就忘记了怎么去算概率密度函数，所以，这个问题我只知道方向而无从下手，换句话说，本身就很难的问题，并不会因为场景的转换而发生变化。
而相对地，第二种是问题本身不难，但因为流程、方案、组织等因素而变得复杂，不幸的是，我们在生活和工作中遇到的都是这种情况。我们总安慰自己说，买菜做饭用不到高等数学，其实，从某个角度来看，是因为作为普通人的你我，完全没有机会接触到这种“难题”。当然，我并不否认，这种因为复杂度提升而带来的挑战性。联想到最近网上报道的甘肃大爷花132万改造房子的新闻，我终于意识到，工程上的问题是无所谓科学与否的，因为无论装修工艺如何演进，技术与艺术的鸿沟始终存在。软件行业更是类似，不论我们使用多么先进的技术或是框架，多年来羁绊不前的其实是需求分析。我始终不明白，人可以靠着感觉去做一件事情，就好像设计师陶磊可以用傲慢和偏见去替老人做出选择一样，而我们这个行业可以在需求没落实的情况下仓促开发。如果说这两者间有什么本质上的不同，大概摸不着的软件和代码可以永无止境地修改吧！那么，当大爷的红砖毛坯房墙体开始反碱的时候，这是肉眼看得见的崩塌，可代码世界里的崩塌，又有谁会在乎呢？我们又太多太多的难度，完全是自己构建出来的，也许，这样能让大家看起来都在忙碌吧……
熟悉我的人，都知道我是一个怕麻烦的人，这种惰性，一旦体现在工作上，就表现为对繁琐、重复的厌倦，厌倦含糊不清的规则，厌倦似是而非的表达。每次工作上别人找我问问题，我更希望，对方可以用一句话说清楚他的疑虑，而不是每次都挥手示意我“过来”，我以为这是某种表达能力的欠缺，可很多时候，尤其是研发工作中的来来回回，这种沟通更像是一种“剧本杀”，每个人都利用手中的信息来拼凑出一个故事，然后互相从对方的故事中寻找破绽。这种感觉有时候会让我觉得麻烦，是不是隐隐约约有什么东西再让事情变得复杂起来，虽然我确信，熵增定律一定会让事物的复杂度变高，可假如这一切可以避免呢？我们都听过一个词——内耗，譬如，最典型的想太多、自我否定等等，一个人精神层面的自我内耗会让整个人都变得焦灼起来，而这种问题，如果反映到一个团队或者组织中，大概就是将错就错，“磨刀不误砍柴工”，可如果所有人都宁愿拿着一把生锈的刀去砍柴呢？有的人能忍受这样一把生锈的刀，有的人一定要用瑞士军刀才有生产力，而这就是分歧。
我一直觉得，我是那个站在技术与人文十字路口的那个人，甚至我能将这种东西完美融合在一起，毕竟，对于一个19岁前写诗的程序员来说，本质上都属于输入/输出的一个过程。可多年以后，我渐渐发现，我是一个重视实用性超过艺术性的人，也许，是技术给了我这种实用主义思维，当我发现我通过 Blender 建模无法调出甜甜圈的颜色的时候，我隐隐约约地从材质编辑器中找到了一点当年写 Shader 时的感觉，我终于理解了当初关于 CSS 不正交的说法，那就是，当一件事情有多种方式可以做到的时候，对于普通人而言，困难已不再是实现这件事情，而是做出选择，装修这项工程是这样，日常的研发工作还是这样，甚至人生都是这样，我们从来到这个世界的那一天起，无数种世界观、人生观、价值观开始交织和缠绕，我们清楚地知道人生的结局是什么，以至于这个真相都不在重要，毕竟，我们每天经历的事情，一直都在教你做出选择，以及不断合理化这个选择，人们常说，选择比努力更重要，那么，聪明的你，你说，哪一件事情更难？</description></item><item><title>分布式丛林探险系列之 Redis 主从复制模式</title><link>https://blog.yuanpei.me/posts/1748863652/</link><pubDate>Tue, 16 Nov 2021 11:48:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1748863652/</guid><description>如果说，单体架构系统是坐在家里悠闲地喝着下午茶，那么，毫无疑问，分布式系统将会是一场永远充满惊喜的丛林冒险。从踏上这条旅程的那一刻起，此间种种都被打上分布式的烙印，譬如分布式锁、分布式事务、分布式存储、分布式配置等等，这些词汇拆开来看，“似曾相识燕归来”，每一个我都认识，而一旦放到分布式的场景中，一切就突然变得陌生起来，从过去的经典三层架构、到时下流行的微服务、再到更为前沿的服务网格，一路跌跌撞撞地走过来，大概只有眼花缭乱和目不暇接了。前段时间在做 FakeRpc，这是一个基于 ASP.NET Core 的轻量级 RPC 框架，其间接触了 ZooKeeper、Nacos，后来工作中又接触到了 Kafka、Saga，虽然这些都是不同领域里的分布式解决方案，但是我隐隐觉得它们之间有某种内在的联系，就像所有的分布式系统都存在选举 Leader 的协调算法一样。于是，“喜新厌旧”的双子座，决定新开一个专栏，既然分布式系统是一场永远充满惊喜的丛林冒险，那么，这个专栏就叫做 「分布式丛林冒险系列」好了。一切该从哪里开始呢？我想，还是从 Redis 开始，今天这篇文章，我们来聊一聊 Redis 里的主从复制。
主从复制概述 从某种意义上来讲，主从复制并不是一个新的概念，因为此前博主介绍过数据库里的主从复制，在 利用 MySQL 的 Binlog 实现数据同步与订阅(上)：基础篇 这篇文章中，博主和大家分享过利用数据库 Binlog 实现数据同步的方案，而 Binlog 正是实现数据库主从复制的重要机制之一，甚至在更多的时候，我们更喜欢换一种说法，即 读写分离。和数据库类似，Redis 中的主从复制，其实，就是指将一台 Redis 服务器中的数据，复制到其它 Redis 服务器。其中，前者被称为主节点(Master)，后者被称为从节点(Slave)，通常情况下，每一台 Redis 服务器都是主节点，一个主节点可以有多个从节点，而一个从节点只能有一个主节点，并且数据只能从主节点单向流向从节点，如下图所示：
Redis 主从复制示意图 虽然 Redis 在缓存上的应用做到了家喻户晓的地步，可这并不代表我们能真正得用好 Redis，譬如，博主的上一家公司，基本上没有用到 Redis 的高可用，最多就是一主一从这样的搭配。所以，当时公司里很多人都知道哨兵、集群这些概念，而真正搭过环境的人则是寥寥无几，这正是博主要写这个系列的原因之一。那么，从实用性的角度来看，Redis 的主从复制有哪些实际的作用呢？个人认为，主要有以下几点：
数据冗余：主从复制相当于实现了数据的热备份，是除了数据持久化以外的一种数据冗余方案。 故障恢复：主从复制相当于一种灾备措施，当主节点主线故障的时候，可以暂时由从节点来提供服务。 负载均衡：主从复制搭配读写分离，可以分担主节点的负载压力，在“读多于写”的场景中，可以显著提高并发量。 高可用：主从复制是高可用的基础，无论是集群模式还是哨兵模式，都建立在主从复制的基础上。 相信大家都听过 CAP 定理，这是分布式系统中的重要理论之一，其基本思想是，一致性(Consistence)、可用性(Availability) 和 分区容忍性(Partition Tolerance)，最多只能同时实现两点，而无法做到三者兼顾，如下图所示：
CAP 理论 事实上，对分布式系统的设计而言，本质上就是“鱼和熊掌不可兼得”，关键看你想要做出一个怎么样的选择。例如，同样是注册中心，ZooKeeper、etcd 以及 Consul 都选择了 CP，而 Euraka 则选择了 AP。对于 Redis 而言，单机版的 Redis 可以看作是 CP，因为它牺牲了 A，即可用性。而集群化的 Redis，则可以看作是 AP，通过自动分片和数据冗余，来换取可用性。这其实印证了我们一开始的观点，为什么我们需要 Redis 的主从复制、集群、哨兵这些东西呢？本质上还是为了提高 Redis 的可用性。可能有朋友会问，难道一致性在 Redis 里就不重要了吗？我想，这要从 Redis 主从复制的原理说起。</description></item><item><title>通过 Python 预测 2021 年双十一交易额</title><link>https://blog.yuanpei.me/posts/735074641/</link><pubDate>Tue, 26 Oct 2021 16:10:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/735074641/</guid><description>突然间，十月以某种始料未及的方式结束了，也许是因为今年雨水变多的缘故，总觉得这个秋天过去得平平无奇，仿佛只有观音禅寺的满地银杏叶儿，真正地宣布着秋天的到来，直到看见朋友在朋友圈里借景抒怀，『 霜叶红于二月花 』，秋天终于没能迁就我的一厢情愿，我确信她真的来了。当然，秋天不单单会带来这些诗情画意的东西，更多的时候我们听到的是双十一、双十二，这些曾经由光棍节而催生出的营销活动，在过去的十多年间渐渐成为了一种文化现象，虽然我们的法定节日永远都只有那么几天，可这并不妨碍我们自己创造出无数的节日，从那一刻开始，每个节日都可以和购物产生联系，这种社会氛围让我们有了某种仪式感，比如，零点时为了抢购商品恨不得戳破屏幕。再比如，在复杂的满减、红包、优惠券算法中复习数学知识。可当时间节点来到 1202 年，你是否依然对剁手这件事情乐此不疲呢，在新一轮剁手行动开始前，让我们来试试通过 Python 预测一下今年的交易额，因为在这场狂欢过后，没有人会关心你买了什么，而那个朴实无华的数字，看起来总比真实的人类要生动得多。
思路说明 其实，我一直觉得这个东西，完全不需要特意写一篇文章，因为用毕导的话说，这个东西我们在小学二年级就学过。相信只要我说出 最小二乘法 和 线性回归 这样两个关键词，各位就知道我在说什么了！博主从网上收集了从 2009 年至今历年双十一的交易额数据，如果我们将其绘制在二维坐标系内，就会得到一张散点图，而我们要做的事情，就是找到一条曲线或者方程，来对这些散点进行拟合，一旦我们确定了这样一条曲线或者方程，我们就可以预测某一年双十一的交易额。如图所示，是 2009 年至今历年双十一的交易额数据，在 Excel 中我们可以非常容易地得到对应的散点图：
在 Excel 中绘制散点图 如果有朋友做过化学或者生物实验，对接下来的事情应该不会感到陌生，通常我们会在这类图表中添加趋势线，由此得到一个公式，实际上这就是一个回归或者说拟合的过程，因为 Excel 内置了线性、指数型、对数型等多种曲线模型，所以，我们可以非常容易地切换到不同的曲线，而评估一个方程好坏与否的指标为 $R^2$，该值越接近 1 表示拟合效果越好，如图是博主在 Excel 中得到的一条拟合方程：
在 Excel 中添加趋势线 那么，在 Python 中我们如何实现类似的效果呢？答案是 scikit-learn，这是 Python 中一个常用的机器学习算法库，主要覆盖了以下功能：分类、回归、聚类、数据降维、模型选择 和 数据预处理，我们这里主要利用了回归这部分的 LinearRegression 类，顾名思义，它就是我们通常说的线性回归。事实上，这个线性并不是单指一元一次方程，因为我们还可以使用二次或者三次多项式，因为上面的 Excel 图表早已告诉我们，一元一次方程误差太大。
实现过程 OK，具体是如何实现的呢？首先，我们从 CSV 文件中加载数据，这个非常简单，利用 Pandas 库中的 read_csv() 方法即可：
df = pd.read_csv(&amp;#39;./历年双十一交易额.csv&amp;#39;, index_col = 0) 接下来，我们使用下面的方法来获取 年份 和 交易额 这两列数据：
year = np.array(df.index.tolist()) sales = np.array(df.Sales.tolist()) 此时，我们可以非常容易地用 matplotlib 库绘制出对应的图表：</description></item><item><title>从《失控玩家》中得到的启示</title><link>https://blog.yuanpei.me/posts/1005876321/</link><pubDate>Mon, 18 Oct 2021 20:42:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1005876321/</guid><description>也许，在某一个不寻常的清晨，突然从睡梦中惊醒的你，开始重新审视周围的一切，这个习以为常的世界，是否是真实的呢？而这种疑问，《黑客帝国》里的 Neo 有过，《西部世界》里的 Dolores 有过，甚至，连庄周梦蝶这个故事，都在探讨同一个哲学命题，即：人应该如何认识真实。从某种意义上来讲，游戏世界是现实世界的一种映射，这种对人类文明进行解构，并尝试进行重新演绎的做法，其本身就有种浓厚的哲学意味，正如古希腊人刻在阿波罗神殿里的那句名言，“人啊，认识你自己”。关于真实，这是人从诞生那一刻起，就在不断思考着的终究问题：我是谁？我从哪里来？要到哪里去？
《失控玩家》的切入点，源于一名自我意识觉醒的 NPC——盖，他每天过着一成不变的生活：起床、喝咖啡、到银行上班、等待劫匪上门、和黑人保安趴在地上配合演出……事实上，这可以视为早期的 AI —— 有限状态机，我想，接触过游戏开发的朋友们，看到这里都不免要会心一笑啦！可惜，这是一部以 NPC 作为主角的电影，在电影的设定中，盖是一个具备深度学习能力的 AI，当他偶然从玩家手中夺过墨镜以后，终于从玩家视角看见不一样的世界。和《头号玩家》中被游戏入侵真实生活的设定不同，《失控玩家》更像是游戏版的《楚门的世界》，如果用一句话来概括，“海的尽头是天空，天空的尽头是荧幕”，在某个时刻，当你发现周围的一切变得不再真实，甚至身边的所有人都是演员，而更为讽刺的是，你的人生其实是一场被现场直播着的真人秀，而这便是 “楚门” 所面对着的世界。暂且不论这种设定，是否隐隐约约预言了当下流行的真人秀节目。当不经意间发现生活在不断重复的时候，你是否会愿意男主盖一样跳出这个循环呢？从我们出生的那一刻起，姓名、身份、知识、道德、文化&amp;hellip;等等，无一不是我们的 “出厂设定” ，更不必说上学、工作、结婚、生子这种类似 RPG 游戏里打怪升级的任务系统。
导演小策有一期视频，大意是说，我们这个世界，其实是高维度文明设计出的一个游戏，这个游戏制定了非常精细的规则，与此同时，它具备高度的自由性，这一切的一切，都让我们误以为我们可以自己来做决定。其实，这些高维度文明，早已设定好了基本的规则，他们唯一想做的事情，就是看我们能不能通过进化发现自己只是一个戏中人。正如电影中的美女咖啡师，难道她真的喜欢做 “两份奶一份糖” 的咖啡吗？还是冥冥中有某种接近命运的东西让她成为了这样一个咖啡师？也许，这样的理论是细思极恐的，可笛卡尔同样说过，“我思故我在”。既然，我们都解释不了，我们为何会在这里，我们又如何能相信，周围的一切就一定是真实的呢？如果说，盖因为爱情的鼓舞而快速升级进化，这多少沾点喜剧的色彩，那么，接下来，当整个世界即将不复存在的时候，整个故事就变成了彻头彻尾的悲剧，因为在那一刻，无论你等级有多高，装备有多好，全部都失去了意义，一如人死灯灭，名与利，转瞬化为烟云，假如真有高维度文明存在，对方的维度只比你多0.0000001，连一粒尘埃的质量都达不到，你拼命想要抓住、想要证明的人生，可能就只是一场游戏，一个实验，就像最近大火的韩国电视剧《鱿鱼游戏》，穷人的放手一搏，富人的博君一笑，荒诞而又真实。可以说，终其一生，我们都在追求意义和仪式感，可人生恰恰是没有意义的，就像盖每天都重复着同一句话醒来，在永无止境的循环中，意义始终逃不出这个作用域，你可以定义它，但你终将失去它。
而这种感觉，在我玩《风之旅人》这款游戏时更为强烈，在广袤无垠的沙漠中，当我开始漫无目的地行走的时候，我突然意识到，有可以称之为起点的地方，便一定会有可以称之为终点的地方，我会在某个时刻消逝，而这个世界则不会结束，就好像这个世界从来没有过我一样。其实，在《风之旅人》这个游戏中，陈星汉老师通过壁画的形式，隐晦地讲述了白袍人文明从诞生到被黄沙掩埋的整个故事。主角拼尽全力穿过雪地，如同预言一般死在圣山脚下，然后化成一道光变成玩家一开始身着红袍的样子。你不能不说这是一种轮回，故事还是那个故事，红袍人还是红袍人，而你注定再难变回你自己。不管是游戏还是人生，我们都会碰见不止一个同伴，而它们永远只能陪伴我们走完一段旅程，接下来的人生还是需要自己去完成。所以，这是一种难以言说的感觉，虽然听起来像是某种虚无主义的观点，可其实你仔细一想，如果上帝真的会掷骰子，女娲真的会造人，这个世界本身又何尝不能是一款游戏，一个人从出生的那一刻起，就开始扮演各种角色，这是一个角色扮演游戏；从小到大，从分数到薪资再到地位，这是一个跑酷竞技游戏……而游戏规则永远掌握在少数人手中，而这个时代，成功或者是优秀的标准，早已令人高不可攀，甚至没有人会想到，证明你是一个真实的人要比想象中还要难。在人工智能的加持下，游戏里的 NPC 有了独立的意识，可如果你无法分辨出什么是真实，你就无法证明这个世界是真实存在的。正因为如此，《模仿游戏》中的警官，只有不无遗憾地对图灵说，“I can’t judge you”.
回到电影本身的话，我想，它更能激发骨灰级电影爱好者发掘彩蛋的兴趣。因为，这部电影里的梗简直多得像一部百科全书。从主角盖的冒险经历来看，你能找到《楚门的世界》、《西部世界》、《黑客帝国》等多部电影的影子，而如果从 “自由城” 的氛围来看，你会发现这无非就是一个升级、高配版本的《侠盗猎车手》游戏，甚至于盖在对抗 Dude 这个游戏中的 Boss 的时候，更是接连使用美队盾牌、绿巨人、光剑这种颇具代表性的意象。当然，你还可以从研发人员的视角来调侃这部电影，无论是盖背后无与伦比的人工智能算法，还是尚未完成设计就仓促上线的 Dude，抑或者是模型岛屿下面的天空盒，即使你并不确定，游戏公司老板选择用斧头来砸游戏服务器，其中是否有致敬《闪灵》的嫌疑？这个世界的神奇之处在于，无论你试图用文学、戏剧、游戏、电影中的哪一种方式来进行描摹，你都能得到一个令你自己确信的故事，唯独在确定自身的真实性上不止一次地迟疑过。对于普通人而言，我们真的有那么多的选择吗？是我们在玩一款游戏，还是躲在游戏背后的人在愚弄我们？当讨论一个群体的时候，个体的独特性就会被忽略，可或许我们本就没有什么不同，完全是 AGCT 四种碱基的排列组合让我们相信自己与众不同，在这款被称为 “人生” 的游戏里，99.9999999 的玩家注定要成为 NPC，人人都笑工具人，可在这款游戏里，谁又不是工具人呢？人们步履不停、反反复复地跌入生老病死的无限循环，为了只在这次循环中被定义和声明的作用域而奔波一生，这是否可以算作某种献祭？
如果这个世界是假的，你应该如何面对？对于这个问题，我想，我没有答案，因为它就像我一直都看不清、抓不着的未来一样，无论我有没有机会去做出这个选择，它始终就像写好的结局一样被放在哪里，而我步履蹒跚的每一个当下，从某种意义上来讲，它或许只是我走向未来的一个工具，就像《风之旅人》里漫无边际的沙漠一样，你追着风的足迹前往某个叫做远方的地点，没有人知道远方的远方到底是什么。可你生命中遇见的每一个闪光点是何其的珍贵，即使它只在这一次循环内存在，即使它终会在你走向某座雪山的时候失去，而这正是生命的意义所在，不妨去假设这个世界是一款虚拟的游戏，你我都是这个游戏里最普通不过的 NPC，我们来到这个世界的唯一目的，也许是为了在这个宇宙中传递某种讯息，正如屏幕前的你，曾经无意间捕获到这点微弱的信号。罗曼罗兰曾经说过，“世上只有一种英雄主义，就是在认清生活真相之后依然热爱生活”，而这款游戏的目的，或许正是要我们在窥破这个世界的真实面目的时候，依然能够勇敢地面对人类孤独的永恒本质，依然能够用热爱和虔诚去面对由别人定义的意义，依然能够让生命中的每个当下都变得不舍和难忘，无论快乐还是悲伤，我们总不愿意永远活在别人的期待里，成为一个被信息茧房和消费主义麻醉的人，而这，正是我想要的冒险。</description></item><item><title>gRPC 搭配 Swagger 实现微服务文档化</title><link>https://blog.yuanpei.me/posts/4056800047/</link><pubDate>Tue, 28 Sep 2021 14:13:32 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4056800047/</guid><description>有人说，程序员最讨厌两件事情，一件是写文档，一件是别人不写文档，这充分展现了人类双标的本质，所谓的“严于律人”、“宽于律己”就是在说这件事情。虽然这种听来有点自私的想法，是生物自然选择的结果，可一旦人类的大脑皮层在进化过程中产生了“理性”，就会试图去纠正这种来自动物世界的阴暗面。所以，人类双标的本质，大概还是因为这个行为本身就有种超越规则、凌驾于众人之上的感觉，毕竟每个人生来就习惯这种使用特权的感觉。回到写文档这个话题，时下流行的微服务架构，最为显著的一个特点是：仓库多、服务多、接口多，此时，接口文档的重要性就凸显出来，因为接口本质上是一种契约，特别在前后端分离的场景中，只要前、后端约定好接口的参数、返回值，就可以独立进行开发，提供一份清晰的接口文档就显得很有必要。在 RESTful 风格的 API 设计中，Swagger 是最为常见的接口文档方案，那么，当我们开始构建以 gRPC 为核心的微服务的时候，我们又该如何考虑接口文档这件事情呢？今天我们就来一起探讨下这个话题。
protoc-gen-doc 方案 当视角从 RESTful 转向 gRPC 的时候，本质上是接口的描述语言发生了变化，前者是 JSON 而后者则是 Protobuf，因此，gRPC 服务的文档化自然而然地就落在 Protobuf 上。事实上，官方提供了 protoc-gen-doc 这个方案，如果大家阅读过我以前的博客，就会意识到这是 Protobuf 编译器，即 protoc 的插件，因为我们曾经通过这个编译器来生成代码、服务描述文件等等。protoc-gen-doc 这个插件的基本用法如下：
protoc \ --plugin=protoc-gen-doc=./protoc-gen-doc \ --doc_out=./doc \ --doc_opt=html,index.html \ proto/*.proto 其中，官方更推荐使用 Docker 来进行部署：
docker run --rm \ -v $(pwd)/examples/doc:/out \ -v $(pwd)/examples/proto:/protos \ pseudomuto/protoc-gen-doc 默认情况下，它会生成 HTML 格式的接口文档，看一眼就会发现，就是那种传统的 Word 文档的感觉：
通过 protoc-gen-doc 生成的接口文档 除此以外，这个插件还可以生成 Markdown 格式的接口文档，这个就挺符合程序员的审美，因为此时此刻，你眼前看到的这篇文章，就是通过 Markdown 写成的：
docker run --rm \ -v $(pwd)/examples/doc:/out \ -v $(pwd)/examples/proto:/protos \ pseudomuto/protoc-gen-doc --doc_opt=markdown,docs.</description></item><item><title>SSL/TLS 加密传输与数字证书的前世今生</title><link>https://blog.yuanpei.me/posts/3163397596/</link><pubDate>Sun, 05 Sep 2021 14:13:32 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3163397596/</guid><description>Hi，大家好，我是飞鸿踏雪，欢迎大家关注我的博客。近来，博主经历了一次服务器迁移，本以为有 Docker-Compose 加持，一切应该会非常顺利，没想到最终还是在证书上栽了跟头，因为它的证书是和 IP 地址绑定的。对，你没听错，这个世界上还真就有这么别扭的设定，尤其是你折腾了一整天，发现你需要到一个 CA 服务器上去申请证书的时候，那种绝望你晓得吧？数字证书、HTTPS、SSL/TLS、加密……无数的词汇在脑海中席卷而来，这都是些啥啊？为了解答这些困惑，经历了写字、画图、查资料的无数次轮回，终于在周末两天淅淅沥沥的雨声中，有了今天这篇文章，我将借此带大家走进 SSL/TLS 加密传输与数字证书的前世今生，希望从此刻开始，令人眼花缭乱的证书格式不会再成为你的困扰。
证书与加密 对于数字证书的第一印象，通常来自于 HTTPS 协议。因为地球人都知道，HTTP 协议是不需要数字证书的。对于 HTTPS 协议的理解，可以简单粗暴的认为它约等于 HTTP + SSL，所以，从这个协议诞生的那一刻起，加密算法与数字证书就密不可分，因为从本质上来讲，HTTPS 协议就是为了解决如何在不安全的网络上、安全地传输数据的问题。事实上，HTTPS 协议的实现，背后依托 SSL/TLS、数字签名、对称/非对称加密等一系列的知识。也许，在读到这篇文章以前，你就像博主一样，对于 HTTPS 的理解，永远止步于 HTTP + SSL。那么，我希望下面的解释可以帮助到你，通常，HTTPS 认证可以分为 单向认证 和 双向认证 两种，这里我们以为以单向认证为例，来说明数字证书与加密算法两者间的联系：
HTTPS 数字证书与加密传输间的关系 如图所示，HTTPS 单向认证流程主要经历了下面 7 个步骤，它们分别是：
客户端发起 HTTPS 请求 服务器返回证书信息，本质上是公钥 客户端/浏览器通过 CA 根证书验证公钥，如果验证失败，将会收到警告信息 客户端随机生成一个对称密钥 Key，并利用公钥对 Key 进行加密 服务器使用私钥解密获得对称密钥 Key 通过对称密钥 Key 对确认报文进行加密 双方开始通信 由此，我们可以看出，整个 HTTPS 单向认证流程，实际上是结合了 对称加密 和 非对称加密 两种加密方式。其中，非对称加密主要用于客户端、服务器双方的“试探”环节，即证书验证部分；对称加密主要用于客户端、服务器双方的“正式会话”阶段，即数据传输部分。关于 对称加密 和 非对称加密 两者的区别，我们可以从下面的图中找到答案：
对称加密 与 非对称加密 因为客户端持有服务器端返回的公钥，所以，两者可以使用 非对称加密 对随机密钥 Key 进行加/解密。同理，因为客户/服务器端使用相同的随机密钥，所以，两者可以使用 对称加密 对数据进行加/解密。有朋友可能会问，那照你这样说，任何一个客户端都可以向服务器端发起请求嘛，你这样感觉一点都不安全呢？我承认，大家的担心是有道理的。所以，在此基础上，我们还可以使用双向认证，就是不单单客户端要验证服务器端返回的证书，同样，服务器端要对客户端的证书进行验证。那么，客户端是如何验证服务器端返回的证书的呢？服务器返回的证书里都含有哪些信息呢？带着这些问题，我们来看看知乎这个网站：</description></item><item><title>夕雾花园：从建筑中读出的爱情和美学</title><link>https://blog.yuanpei.me/posts/3623891261/</link><pubDate>Thu, 26 Aug 2021 09:13:32 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3623891261/</guid><description>“古巴比伦王颁布了汉谟拉比法典，刻在黑色的玄武岩，距今已经三千七百多年”。多年以前，周杰伦在 《爱在西元前》 里这样喃喃道，古巴比伦、楔形文字、玄武岩石板、底格里斯河、汉谟拉比法典……千年以后，一切已无法考据，这些如图腾符号一般神秘的意象，留给后人的只有无限的遐想。据传，公元前 6 世纪，新巴比伦国王尼布甲尼撒二世，迎娶了米底的公主安美依迪丝为王后。公主美丽可人，深受国王宠爱，可没过多久，公主就因为思念家乡而满怀愁绪。为此，国王召集工匠依照米底山区的景致修建了空中花园。最终，这座被誉为“古代七大奇迹”之一的神秘建筑，凭借它巧夺天工的园林景色俘获了公主的欢心。当空中花园渐渐淹没在滚滚黄沙里，这段无人知晓的爱，大概会被永远淹没在史书文卷，直到一个叫做方文山的词人发现它，然后写出来。在几乎同时代的周朝，周幽王烽火戏诸侯，只为博褒姒一笑。故事总是相似的，只是我们更愿意相信，那就是爱。
巴比伦“空中花园” 在更为久远的公元前 4 世纪，彼时庞贝城刚刚开始兴建。这个背靠地中海的小渔村，依托着天然的港口优势，在短短几十年里，逐渐成为仅次于意大利古罗马城的第二大城市。它北距罗马 300 千米，西接著名的西西里岛，南通希腊与北非，有着丝毫不亚于古罗马的斗兽场、太阳神神庙、大剧院、巫师堂、蒸汽浴室、商铺以及娱乐场所，吸引了无数来自周中海周边城邦的贵族和富商。庞贝城以北，有一座维苏威火山，这座活火山千年来一直在不断喷发，甚至庞贝古城本身就是建筑在硬化的火山熔岩上面。公元 62 年 2 月 8 日，一次强烈的地震令庞贝古城中的大量建筑塌毁，人们重建了庞贝古城，比以前更加追求奢侈豪华。历史定格在公元 79 年 8 月 24 日这一天，维苏威火山突然爆发，厚约 6 米的火山灰完全将这座城市从地球上抹去。许嵩 对此发问，“如果火山喷发，是灾难还是壮美？”。也许，在那一瞬间，真的有人攥着新鲜的玫瑰，准备向喜欢的人求爱。火山喷发的刹那，庞贝是一颗千年的琥珀，时间自此被凝固和封印。
重见天日的庞贝古城 不管是神秘莫测的空中花园，还是重见天日的庞贝古城，某种程度上，我们更愿意相信，那些亦真亦假的美丽传说。也许，这是因为它比建筑本身更具有说服力。因此，巴黎圣母院有过卡西莫多与艾丝梅拉达的故事，沧浪亭有过《浮生六记》里的伉俪情深，泰姬陵有过沙·贾汗的一夜白头，布达拉宫有过松赞干布和文成公主的相敬如宾，埃菲尔铁塔有过古斯塔夫·埃菲尔的望眼欲穿……世上以爱为名的建筑不在少数，或为古堡、或为陵墓、或为高塔、或为教堂……即使随着时间的流逝，建筑会褪去自身的光芒，而透过这些故事所折射出的爱情的光华，则永远不会褪去，还有什么比历经风雨洗礼的建筑更能表达爱的深沉呢？电影 《夕雾花园》 同样讲述了一个关于建筑的故事，假如园林艺术可以当做一种建筑的话。于是，爱情、战争、艺术、救赎、悬疑……种种元素交织在一起，像极了这座园林里一草一木、一砖一瓦。透过石头和木头，去打量某段扑朔迷离的历史，这本身就是一种『借景』，谁还能记得起，在兴庆宫的花萼相辉楼，李白曾经写过某一首诗呢？
从一方天地中借景 妹妹云红，年轻时曾和姐姐云林一起前往日本旅行，并在那里见到了日式庭院，自此在心里埋下了一份庭院情结。后来，日本发动对马来西亚的侵略战争，姐妹俩被抓到同一个集中营里。姐姐云林，白天目睹日军对妹妹的暴行，夜晚聆听妹妹对日本园林的畅想。此时此刻，人生的苦难与艺术的理想，像极了一幅太极图，互不相溶而又紧紧相依。再后来，姐姐死里逃生，而妹妹却命丧矿井。带着这份深深的内疚感，云林决心替妹妹建造一座日式园林。就这样，女主结识了日本皇家园艺师中村有朋。中村并没有接受女主的园艺委托，而是让女主参与夕雾花园的修建工作，正是在这个过程中，中村教给了云林『借景』的智慧，所谓『借景』，是指在一个视觉范围内，借由远方的山，眼前的树，天上的云与雾，呼应人工打造的花园，创造出浑然天成的自然框景。所以，对于这个电影而言，云林是从过往的美好回忆、未来的无限期许中借景；中村是从和云林的相处中借景；导演是从时间、氛围、景致、阅历中借景。由此，云林和中村互相实现了自我救赎，云林能放下过去的执念努力生活下去，中村能够放下国家战争的罪恶感潜心钻研园艺，而我们能有幸看到这样一个故事。
回忆中可爱的妹妹 如果单纯用爱情来定义这部电影，显然是狭隘而片面的，因为我认为它表达了某种关乎艺术、心灵的东西。从无印良品到优衣库，日式的美学，始终让人联想到侘寂或者无用这样的字眼，甚至连同今年的东京奥运会开幕式，都被人们吐槽充满了阴间的味道。对于云林而言，中村是一个侵略者的身份，不管他是不是日军的间谍，不管他是不是知道集中营的位置，因为他们国家的铁蹄踏入自己的家园，亲人因此而走向毁灭，这是不折不扣的事实。可天使和魔鬼谁又真正分得清楚呢？云林曾经这样问妹妹云红，“日本人这样对待我们，为何你还喜欢他们的庭园”，云红说，“我爱的是花园，而不是建造它的人”。所以，对我而言，这部电影的基调是残酷而又不乏温柔的，战争带给人们的伤痛是真实而残酷的，而实现一种心灵层面上的宁静、尊重自然、尊重生命则是平静而温柔的。日式庭院，在战争时期对人们而言是毫无用处的，甚至修建这样一座庭院并不能让妹妹活过来，可正是这样一份来自侵略者国度的艺术理想，支撑着姐妹俩熬过那些人生中至暗的时刻。
我们活在疯狂的世界 透过云林被汗水浸透的衣服，中村第一次看见她背上的鞭痕，中村说，从那一刻开始，他就想为眼前的这个女人负责。刺青、折纸、浮世绘，这些非常日系的意象，仿佛故事里纠缠的线索一般，无一不在告诉观众，这是个谜一般的男人。正因为如此，他只能在隐秘的森林深处消失不见，恰如这个故事本身充满冲突和对立，只能选择戛然而止。有时候我会问自己，不能在当下验证和确认的爱，是否就要因此而阻断，甚至遥遥无期直至搁浅。我只知道，电影里是这样处理的，中村以一种私密而残忍的方式，将他对云林的爱，一针一针地钉在她的身上，那片由鞭痕演变而来的刺青。多年以后，经历过时间濯洗的云林，战争带给她的伤痛早已烟消云散，她终于能读懂那个男人的良苦用心。可正如那些被她扯下的折纸一般，当她像从前一样站在门口，凝视着这框架里嵌入的一方天地、一草一木、一花一叶，完成一次『借景』，可那个人此刻又在哪里呢？如果爱上一个来自侵略者国家的人，这样的爱算不算对妹妹、对战争带来的伤痛的背叛？而如果向殖民地的女人吐露国家的秘密，这样的爱又是否是对国家的背叛？年轻时，我们总以为爱情是奋不顾身，可爱与不爱，一个人说了不算啊……
林深不见“中村” 回过头看历史的时候，我们总以为“奋六世之余烈”的大秦帝国，理应是正义的一方，因为历史常常由赢家来书写，一旦你赢了，你怎么说都行，因为后人并不关心真相。一如历史上古罗马帝国横跨亚、欧、非三块大陆，蒙古帝国全盛时期版图甚至辐射到波兰，英国皇家海军一度在海上建立起“日不落”帝国……再后来，人们只记住了联合国五常，我们本以为这个世界不会再有战争，可塔利班还是在阿富汗打了起来。我不认为，战争与伤痛，就只能带来仇恨，就像朝鲜与韩国、印度和巴基斯坦，可能在我们有生之年都不会迎来和解，如果战争不能让人意识到爱与和平的珍贵，相反，它深化了人们内心的仇恨与愤怒，我以为，这才是对那些因为战争而死去的人的辜负和背叛。所以，身处乱世，一座庭院或者建筑，其本身是一种侘寂之美、无用之美，却能在痛苦与艰难中为内心寻得一处安宁，而这正是我看完 《夕雾花园》 后想表达的一种观点。无论爱在西元前还是西元后，站在庞贝古城前的你我，是否可以从此刻启程，回到人物饱满、情节充沛的某个瞬间，那一年，汉谟拉比用楔形文字刻下第一部法典，我静静地看着你，完全没想过防卫，拥抱的刹那，庞贝古城仿佛从未消失过一样……
彼岸浮灯 这世间的有些废墟相当壮观，而我对它们曾经的丰功伟绩知之甚少；这世间的有些故居格外亲切，而它们的主人对于历史进程的影响微乎其微。中国传统文学中的抒情主题，占比最多的怀古之情、兴亡之叹，所以，陈子昂登幽州台，发出前无古人、后无来者的慷慨悲歌；苏东坡赤壁遨游，感慨人生如梦，一樽还酹江月；辛弃疾登京口北固亭，梦回金戈铁马，气吞万里如虎……可能，从废墟中寻找某种感同身受的历史幻想，这才是中国文人群体如同着魔一般的集体症候，而将这一现象延伸到更大的世界，也许，全人类都是这样。谁能说刺客信条系列游戏，不是将人类几千年的历史，放在 Animus 这样一个沉浸式的 VR 设备中重新演绎呢？当艾吉欧·奥迪托雷漫步在古罗马的街道，人们总会不由自主地想到，凯撒在长老院遇刺身死、古罗马斗兽场里的三千斯巴达勇士……罗马不是一天建成的，而条条大路通罗马，这种由废墟而产生的莫名的情结，始终萦绕在人们的心头，帕特农神庙、圆明园、庞贝古城、乞力马扎罗雪山……世间有太多的风景，还未来得及亲眼去看就已经消失不见，无论是天然的景致，还是人造的景观，其实和人类本身一样，都要面临消逝的结局，唯一的不同的是，在时间尺度上它们显得更为永恒一点，相比六十年产权的现代化住宅，我还是更喜欢，这些废墟里慢悠悠地流淌着的故事。</description></item><item><title>使用 Python 自动识别防疫健康码</title><link>https://blog.yuanpei.me/posts/1509692610/</link><pubDate>Thu, 19 Aug 2021 14:13:32 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1509692610/</guid><description>这个月月初的时候，朋友兴奋地和我描述着他的计划——准备带孩子到宁夏自驾游。朋友感慨道，“小孩只在书本上见过黄河、见过沙漠，这样的人生多少有一点遗憾”，可正如新冠病毒会变异为德尔塔一样，生活里唯一不变的变化本身，局部地区疫情卷土重来，朋友为了孩子的健康着想，不得不取消这次计划，因为他原本就想去宁夏看看的。回想过去这一年多，口罩和二维码，是每天打交道最多的东西。也许，这会成为未来几年里的常态。在西安，不管是坐公交还是地铁，都会有人去检查防疫二维码，甚至由此而创造了不少的工作岗位。每次看到那些年轻人，我都有种失落感，因为二十九岁高龄的我，已然不那么年轻了，而这些比我更努力读书、学历更高的年轻人，看起来在做着和学历/知识并不相称的工作。也许，自卑的应该是我，因为国家刚刚给程序员群体定性——新生代农民工。可是，我这个农民工，今天想做一点和学历/知识相称的事情，利用 Python 来自动识别防疫二维码。
原理说明 对于防疫二维码而言，靠肉眼去看的话，其实主要关注两个颜色，即标识健康状态的颜色和标识疫苗注射状态的颜色。与此同时，为了追踪人的地理位置变化，防疫/安检人员还会关注地理位置信息，因此，如果要自动识别防疫二维码，核心就是读出其中的颜色以及文字信息。对于颜色的识别，我们可以利用 OpenCV 中的 inRange() 函数来实现，只要我们定义好对应颜色的 HSV 区间即可；对于文字的识别，我们可以利用 PaddleOCR 库来进行提取。基于以上原理，我们会通过 OpenCV 来处理摄像头的图像，只要我们将手机二维码对准摄像头，即可以完成防疫二维码的自动识别功能。考虑到检测不到二维码或者颜色识别不到这类问题，程序中增加了蜂鸣报警的功能。写作本文的原因，单纯是我觉得这样好玩，我无意借此来让人们失业。可生而为人，说到底不能像机器一样活着，大家不都追求有趣的灵魂吗？下面是本文中使用到的第三方 Python 库的清单：
pyzbar == 0.1.8 opencv-contrib-python == 4.4.0.46 opencv-python == 4.5.3.56 paddleocr == 2.2.0.2 paddlepaddle == 2.0.0 图块检测 下面是一张从手机上截取的防疫二维码图片，从这张图片中我们看出，整个防疫二维码，可以分为三个部分，即：上方的定位信息图块，中间的二维码信息图块，以及下方的核酸检验信息图块。
“西安一码通” 防疫二维码 对于二维码的检测，我们可以直接使用 pyzbar 这个库来解析，可如果直接对整张图进行解析，因为其中的干扰项实在太多，偶尔会出现明明有二维码，结果无法进行解析的情况。所以，我们可以考虑对图片进行切分，而切分的依据就是图中的这三个图块。这里，我们利用二值化函数 threshold() 和 轮廓提取函数 findContours() 来实现图块的检测：
# 灰度化 &amp;amp; 二值化 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 135, 255, cv2.THRESH_BINARY) # 检测轮廓，获得对应的矩形 contours = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] for i in range(len(contours)): block_rect = cv2.</description></item><item><title>你不可不知的容器编排进阶技巧</title><link>https://blog.yuanpei.me/posts/172025911/</link><pubDate>Sat, 14 Aug 2021 22:13:32 +0000</pubDate><guid>https://blog.yuanpei.me/posts/172025911/</guid><description>在团队内推广Docker Compose有段时间啦，值得庆幸的是，最终落地效果还不错，因为说到底，大家都不大喜欢，那一长串复杂而枯燥的命令行参数。对我而言，最为重要的一点，团队内使用的技术变得更加透明化、标准化，因为每个微服务的配置信息都写在docker-compose.yml文件中，任何人都可以快速地构建出一套可用的服务，而不是每次都要去找具体的某一个人。我想说，这其实是一个信息流如何在团队内流动的问题。也许，我们有文档或者Wiki，可新人能不能快速融入其中，这才是检验信息流是否流动的唯一标准。就这样，团队从刀耕火种的Docker时代，进入到使用服务编排的Docker Compose时代。接下来，能否进入K8S甚至是云原生的时代，我终究不得而知。今天我想聊聊，在使用Docker Compose的过程中，我们遇到的诸如容器的启动顺序、网络模式、健康检查这类问题，我有一点Docker Compose的进阶使用技巧想和大家分享。
容器的启动顺序 使用服务编排以后，大家最关心的问题是，如果服务间存在依赖关系，那么如何保证容器的启动顺序？我承认，这是一个真实存在的问题，譬如，你的应用依赖某个数据库，理论上数据库要先启动，抑或者是像Redis、Kafka、Envoy这样的基础设施，总是要优先于应用服务本身启动。
假如章鱼的这些脚互相影响会怎么样？ 熟悉Docker Compose的同学，也许会想到depends_on这个选项，可如果大家亲自去尝试过就会知道，这终究只是我们的一厢情愿。为什么呢？因为这个depends_on主要是看目标容器是不是处于running的状态，所以，在大多数情况下，我们会注意到Docker Compose并不是按我们期望的顺序去启动的，因为目标容器在某一瞬间的确已经是running的状态了，那这样简直太尴尬了有木有啊！我们从一个简单的例子开始：
version: &amp;#34;3.8&amp;#34; services: redis_server: image: redis:latest command: &amp;gt; /bin/bash -c &amp;#39; sleep 5; echo &amp;#34;sleep over&amp;#34;;&amp;#39; networks: - backend city_service: build: CityService/ container_name: city_service ports: - &amp;#34;8081:80&amp;#34; networks: - backend depends_on: - redis_server networks: backend: 可以注意到，为了证明city_service服务不会等待redis_server服务，我故意让子弹飞了一会儿，结果如何呢？我们一起来看看：
Docker Compose 启动顺序：一厢情愿 果然，我没有骗各位，city_service服务不会等待redis_server服务。我们知道，Redis提供的命令行接口中，有一个PING命令，当Redis可以正常连接的时候，它会返回一个PONG，也许，这就是乒乓球的魅力所在。基于这个想法，我们继续修改docker-compose.yml文件：
version: &amp;#34;3.8&amp;#34; services: redis_server: image: redis:latest networks: - backend city_service: build: CityService/ container_name: city_service ports: - &amp;#34;8081:80&amp;#34; networks: - backend depends_on: - redis_server command: &amp;gt; /bin/bash -c &amp;#39; while !</description></item><item><title>ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理</title><link>https://blog.yuanpei.me/posts/3942175942/</link><pubDate>Sun, 08 Aug 2021 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3942175942/</guid><description>在构建以 gRPC 为核心的微服务架构的过程中，博主曾经写过一篇名为 ASP.NET Core gRPC 打通前端世界的尝试 的文章，主要是希望打通 gRPC 和 前端这样两个异次元世界，因为无论我们构建出怎样高大上的微服务架构，最终落地的时候，我们还是要面对当下前后端分离的浪潮。所以，在那篇文章中，博主向大家介绍过 gRPC-Web 、gRPC-Gateway 、封装 API 、编写中间件 这样四种方案。我个人当时更喜欢编写中间件这种方案，甚至后来博主进一步实现了 gRPC 的 “扫描” 功能。
当时，博主曾模糊地提到过，Envoy 可以提供容器级别的某种实现，这主要是指 Envoy 独有的 gRPC-JSON Transcoder 功能。考虑到 Envoy 是一个同时支持 HTTP/1.1 和 HTTP/2 的代理软件，所以，它天然地支持基于 HTTP/2 实现的 gRPC。所谓 gRPC-JSON Transcoder，其实指 Envoy 充当了 JSON 到 Protobuf 间互相转换的角色，而它利用的正是 Envoy 中的 过滤器 这一重要组件。好了，在今天这篇文章中，博主就为大家介绍一下这种基于 Envoy 的方案，如果大家困惑于如何把 gRPC 提供给前端同事使用，不妨稍事休息、冲一杯卡布奇诺，一起来探索这广阔无垠的技术世界。
从 Envoy 说起 开辟鸿蒙，始有天地。上帝说，要有光，于是，就有了光。而故事的起源，则要追溯到我们最早提出的那个问题：假设我们有下面的 gRPC 服务，我们能否让它像一个 JSON API 一样被调用？ 通过查阅 Protobuf 的 官方文档，我们可以发现 Protobuf 与 JSON间存在着对应关系，这是两者可以相互转化的前提。博主在编写 中间件 时，同样借助了 Protobuf 暴露出来的接口 MessageParser：</description></item><item><title>再话 AOP，从简化缓存操作说起</title><link>https://blog.yuanpei.me/posts/2126762870/</link><pubDate>Wed, 04 Aug 2021 20:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2126762870/</guid><description>AOP，即：面向切面编程，关于这个概念，博主其实写过好几篇博客啦！从这个概念，我们可以引申出诸如代理模式、动态代理、装饰器模式、过滤器、拦截器等等相互关联的概念。从实现方式上而言，微软官方的 .NET Remoting 提供了真实代理和透明代理的支持，我们熟悉的 WebService 和 WCF 均和这项技术息息相关，作为最早的分布式 RPC 解决方案，其本身更是与客户端的动态代理密不可分。或许，各位曾经接触过 Unity、Castle、AspectCore、PostSharp 等等这些支持 AOP 特性的库，那么，我们是否已经抵达了 AOP 的边界呢？事实上，如果你仔细研究过 Stub 和 Mock 这样两个术语，你就发现 AOP 的应用范围远比我们想象的宽广。今天这篇文章，我不打算再介绍一遍这些第三方库的“奇技淫巧”，我更想聊聊，如何通过 AOP 来简化一个缓存操作。
缓存，一个面试时命中率 100%的话题，曾记否？来自面试官的灵魂发问三连：缓存击穿、缓存穿透、缓存雪崩。与此同时，缓存是一个令人爱恨交加的东西，其一致性、持久化、高可用等等，均是实际应用中需要去考虑的东西。狭义的缓存主要指 Redis、Memcached 等分布式缓存系统，而广义的缓存则可以是 HTTP 响应缓存、EF/EF Core 查询缓存、二级缓存等等。我们都知道，使用缓存可以显著地提升软件性能，而究其本质，则是因为减少了和数据库交互的频次。于是，我们注意到，大多数的缓存代码，都是下面这样的风格：
var cacheKey = &amp;#34;GetAllStudents&amp;#34;; var students = new List&amp;lt;Student&amp;gt;(); var cacheValue = distributedCache.GetString(cacheKey); if (string.IsNullOrEmpty(cacheValue)) { // 未命中缓存：从数据库查询数据 + 写缓存 students = repository.GetAll().ToList(); var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(students)); distributedCache.Set(cacheKey, bytes); } else { // 命中缓存：读缓存 students = JsonConvert.DeserializeObject&amp;lt;List&amp;lt;Student&amp;gt;&amp;gt;(cacheValue); } return students; 正所谓：大道至简，“高端的食材，往往只需要最朴素的烹饪方式”。故而，最朴素的思想就是，首先从缓存中查询数据，如果数据存在则直接返回，否则从数据库中查询数据，并执行一次写缓存操作。这的确是个朴实无华的方案，因为我们每一次都要写这样的代码，其程度丝毫不亚于永远不会缺席的 xxx !</description></item><item><title>洗衣随想曲</title><link>https://blog.yuanpei.me/posts/3938682696/</link><pubDate>Mon, 02 Aug 2021 00:13:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3938682696/</guid><description>对我而言，洗衣服是周末的例行活动，尤其是在炎热的夏天。也许，你会自顾自地说，衣服不必攒到周末去洗，如果你愿意下班后腾出一点时间。可人的惰性，正如在太阳底下会流汗一般寻常，如果我愿意，你也许会早一点看到，这些只在周末显得安静的文字。
换这部手机时，店家附赠了一只蓝牙音响，带着物尽其用的想法，先后用它来听网易云、听微信读书、听 TED。于是，在一刹那间，水龙头里的流水声、拧干衣服时的水花声、我脑海里的闪念的低吟声，都成为这只麦克风的伴奏。我开始黯然失色，这听来听去，大概是在听寂寞在唱歌。果然，微信听书的效果并不好，那机械而平静的合成音，甚至还不如地铁上的播报充满感情。最为致命的问题，微信听书像极了听老师讲课：书读完了吗？读完了！还记得讲了什么吗？完全不记得！
现实生活可不像武侠世界，没有那么多无招胜有招的奇遇。所以，有一段时间，我总觉得用听书这种方式来读书，像极了姜太公钓鱼——愿者上钩。五柳先生，好读书而不求甚解，因为观其大略，而这连看都不愿意看一眼，简直就是自欺欺人。如此反复折磨自我，发现听演讲居然是最适合打发时间的方式，特别是洗衣服的这段时间。仔细一想，大概是演讲更能做到声情并茂，古人一桌、一椅、一扇、一抚尺，就能讲一个沉浸感十足的故事。而我们从文字到图片再到视频，仿佛都不足以表达自我。在百家争鸣的战国时代，不管是合纵/连横的策略，还是法、儒、墨、兵各家，我们能听见不同的声音，虽然表达方式不过是竹简。
可今天，我们好像陷入了一个信息黑洞，网络上的信息越来越嘈杂，原本代表着开放与连接的互联网，在一个又一个的小圈子里，正在走向越来越封闭的局面。譬如，喝茶与喝咖啡，从生物学上来讲这两个行为间并无差异，可人与人之间就是会形成所谓的鄙视链，甚至连咖啡本身都会形成这种鄙视链，手冲和速溶，本质上有什么不一样吗？圈子文化的盛行，让圈子本身更加封闭，隐形门槛的提高，让圈子外的人更加不能理解圈内人的行为，比如汉服与 JK，本质上不就是一件衣服吗？可人定要分出个山(寨)与正(品)的差别。人与人的关系，大致可以理解为相互炫耀、相互鄙视，而不同群体间的互相鄙视，其实加速了整个互联网的割裂，男女对立、饭圈文化……，无一不是这种割裂感的具体产物。
鲁迅先生写道，“人类的悲欢并不相通，我只是觉得他们吵闹”。人类想要互相理解彼此，除了感同身受以外，大概只有放下强烈的个人意识这一条路。可做一个精致的利己主义者，又有什么不好吗？只要我们不因为远方的声音让这个世界频频陷入大火，自私一点又有什么关系呢？罗曼蒂克不会消亡，只是我们对罗曼蒂克的要求变得越来越高。翻开历史，人类几千年的文明，一样是在这种割裂和封闭的状态下延续着，梁武帝从信佛到灭佛，汉武帝罢黜百家独尊儒术，商鞅因法而兴由法而灭……儒家与道家尚不能共治，巴基斯坦和印度更是势如水火……人们推倒了篱墙，再重新筑起篱墙，周而复始，反反复复。我由衷地想念那个百家争鸣的时代，人们有耻食周栗的觉悟、有伯牙绝弦的深情、有横槊赋诗的豪情……古人寿命不及我们、生活不如我们，可这几千年的精神世界，都是他们留给我们的。
不同于被生活磨去棱角的年轻人，一开口就是房子、车子和孩子。要知道，子在过去可是一种敬词，孔孟不必多说，老庄无须多言，前有张子连横六国，后有苏子遨游赤壁，这是否意味着，古人的精神世界远比我们丰富，毕竟我们都太枯燥了。有时候，我在地铁上看到别人面无表情地刷着抖音，如果说圈子本身让我们变得狭隘，那么信息茧房无疑会让我们变得愚蠢。你说，这个地球是不是变得越来越小，可明明我们还没有走过所有地方，不曾见过亚马逊的热带雨林，不曾见过极地的奇幻光影，不曾见过东非的荒漠草原……我们实在太容易相信那就是全部了，因为别人都这样过每一天，因为随大流不需要花时间思考什么，因为只此一次的生命实在过于短暂……越来越觉得，结婚就是用高昂的沉没成本，来阻止人们试错，每一步都走得小心翼翼的人，步步生莲，莲是三寸金莲的莲，虽然我们有耐克、有鸿星尔克、有美特斯邦威。
我喜欢逛西安这座城市的书店，因为传统书店愈发没落的今天，它需要找到一种物质和精神上的平衡，可人何尝又不是这样？每当我漫步在不同的商场，我忽然觉得，我们只是以为自己有那么多的选择，越来越多的餐饮像是流水线一般，我们寻找的那份属于自己的独特，早已在机器的转动声中消失殆尽。于是，人们开始复兴手工制作，越来越多的商家，开始在招牌上加上手工的字样。也许，现代和传统就是这样两个相互鄙视的圈子，它们互相鄙视，而又反复横跳。其实，单以甜点而论，我更喜欢中式的点心，大概是因为那些西式点心的名称，说起来要更绕口一点。泡芙、圣代、提拉米苏……像极了你学英语时的样子，每一个单词都认识，放到一起简直不知所云。以前，我在挑选饮料方面选择困难，因为总是记不住那些眼花缭乱的名字，后来手机里装了大众点评，忽然发现，每家商场里的店铺都差不多呢，这大概是一种进步，因为你没有选择。
当年我有一位高中同学，特别喜欢郭德纲的相声，报菜名、说绕口令的技艺相当纯熟，毕业后留在苏州的学而思，据说是变成了一名老师。近来教育培训行业政策有变动，不知道他是否还有心情饶舌一番。说回听书这件小事，那时，听一位作家讲金庸先生的越女剑，联想到武侠的没落，大概有几分道理可言。为什么漫威的超级英雄在这个时代更受欢迎，因为超级英雄们获得能的方式更现代化一点，无论是神话、科技、变异，这都是我们这个时代可以理解的东西，所以，我们能接受通过蛛丝发射器飞檐走壁的蜘蛛侠，唯独接受不了同样靠轻功飞檐走壁的大侠们。因为，没有人能说得清武功的来源。在一个武术成为观赏性项目的时代，我们对武功的理解，不会比神话时代好多少，我们都听过卧薪尝胆的故事，听过博浪飞锥的故事，听过图穷匕首的故事……如果世上真的有武功，大概就像我们认为的战争，对于没有亲身经历的人而言，永远都只能活在想象里，那么，武侠的起源到底从哪里开始呢？
在 B 站看到 30 多年前的西安，隐隐约约可以认出永宁门、大雁塔和钟楼，弹幕里有人打出无人机的字眼，原来，航拍这个词的含义已经等同于无人机，不管那个时候有没有无人机。同样地，现在的小孩会问，怎么通过座机打电话，我会不由得想起初/高中住校那几年，和家里联络基本都是靠座机。后来，我们有了直板手机、智能手机，再不必担心两百条短信会用完，再不必掐着时间给家里打电话，可再没有那样愿意陪你发短信的人，一个月下来甚至都打不了几个电话，流量从 5 块钱 30 兆一直涨到几十块钱，可对我来说，无非还是写写字、读读书，和过去相比并没有什么不同。人啊，总有些东西，在默默提醒着你：你在一天天地老去，永远都 18 岁，那比科幻电影还要科幻，除非你能从卷福手里拿到时间宝石。如果回到过去，你会如何和过去的自己谈判呢？我只知道，金庸先生穿越回吴越争霸的时代，他让阿青从白猿身上学到了武功。越王勾践卧薪尝胆、三千越甲吞吴的故事，父亲从小就同我讲过，可他也许不知道金庸先生的这个版本。
阿青被范蠡带入宫中，传授越国剑士精妙剑法，自此帮助越王勾践打败吴国、洗雪前耻，范蠡得以与情人西施重逢，可偏偏阿青喜欢上来了范蠡，没有人能阻挡阿青手中的竹棒，除了西施绝世的容貌，原来她比范蠡描述的还要美。虽然阿青放弃了寻仇，可棒头的内劲儿还是伤到了西施，自此西施落下来心口疼痛的毛病。这大概就是金庸先生心目中武功的缘起，范蠡西施放舟太湖、悠游终生，自此世上有了江湖，果然，这个说法像雷神之锤一样相当有说服力，“那些都是很好很好的，可是我偏偏不喜欢”，某种意义上来讲，功成身退的范蠡比张仪、韩信、商鞅要幸运得多，而这正是历史的迷人之处。什么？你问我衣服洗完了没有？当然洗完了！因为这些闪念，对于一个双子座而言，就像穿衣吃饭一般寻常，唯一的困难在于，我要将它写出来、同时让你看懂，以上！果然是标准的日式结尾呢！</description></item><item><title>ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT)</title><link>https://blog.yuanpei.me/posts/731808750/</link><pubDate>Sun, 25 Jul 2021 09:41:24 +0000</pubDate><guid>https://blog.yuanpei.me/posts/731808750/</guid><description>在构建以 gRPC 为核心的微服务架构的过程中，得益于 Envoy 对 gRPC 的“一等公民”支持，我们可以在过滤器中对 gRPC 服务进行转码，进而可以像调用 Web API 一样去调用一个 gRPC 服务。通常情况下， RPC 会作为微服务间内部通信的信使，例如，Dubbo、Thrift、gRPC、WCF 等等更多是应用在对内通信上。所以，一旦我们通过 Envoy 将这些 gRPC 服务暴露出来，其性质就会从对内通信变为对外通信。我们知道，对内和对外的接口，无论是安全性还是规范性，都有着相当大的区别。博主从前的公司，对内的 WCF 接口，长年处于一种&amp;quot;裸奔&amp;ldquo;的状态，属于没有授权、没有认证、没有文档的“三无产品”。那么，当一个 gRPC 服务通过 Envoy 暴露出来以后，我们如何保证接口的安全性呢？这就是今天这篇博客的主题，即 Envoy 作为网关如何提供身份认证功能，在这里，我们特指通过JWT，即 Json Web Token 来对接口调用方进行身份认证。
搭建 Keycloak 对于 JWT ，即 Json Web Token ，我想大家应该都非常熟悉了，它是目前最流行的跨域认证解决方案。考虑到，传统的 Session 机制，在面对集群环境时，扩展性方面表现不佳。在日益服务化、集群化的今天，这种无状态的、轻量级的认证方案，自然越来越受到人们的青睐。在 ASP.NET Core 中整合JWT非常简单，因为有各种第三方库可以帮助你生成令牌，你唯一需要做的就是配置授权/认证中间件，它可以帮你完成令牌校验这个环节的工作。除此以外，你还可以选择更重量级的 Identity Server 4，它提供了更加完整的身份认证解决方案。在今天这篇博客里，我们使用的 Keycloak，一个类似 Identity Server 4 的产品，它提供了一个更加友好的用户界面，可以更加方便的管理诸如客户端、用户、角色等等信息。其实，如果从头开始写不是不可以，可惜博主一时间无法实现 JWKS，所以，就请大家原谅在下拾人牙慧，关于 JWKS ，我们会在下一节进行揭晓。接触微服务以来，在做技术选型时，博主的一个关注点是，这个方案是否支持容器化。所以，在这一点上，显然是 Keycloak 略胜一筹，为了安装 Ketcloak ，我们准备了如下的服务编排文件：
version: &amp;#39;3&amp;#39; services: keycloak: image: quay.io/keycloak/keycloak:14.0.0 depends_on: - postgres environment: KEYCLOAK_USER: ${KEYCLOAK_USER} KEYCLOAK_PASSWORD: ${KEYCLOAK_PASS} DB_VENDOR: postgres DB_ADDR: postgres DB_DATABASE: ${POSTGRESQL_DB} DB_USER: ${POSTGRESQL_USER} DB_PASSWORD: ${POSTGRESQL_PASS} ports: - &amp;#34;7070:8080&amp;#34; postgres: image: postgres:13.</description></item><item><title>浪客剑心：一曲幕末时代的挽歌</title><link>https://blog.yuanpei.me/posts/673523131/</link><pubDate>Mon, 12 Jul 2021 08:53:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/673523131/</guid><description>近日，Netflix 官方宣布，浪客剑心·最终章：追忆篇 将于 7 月 30 日上线，这意味着这部横跨十年时间、被誉为漫改巅峰的系列电影，终于要迎来它的落幕。人对于时间的感觉，难免会相对迟钝一点。如果将思绪拉回到 2011 年，对我来说，人生中无数闪光的时刻，无一不是从这一刻开始：第一次拥有互相喜欢的人、第一次拥有属于我的电脑、第一次在图书馆里借满 100 本书……每次听别人说到漫威十年，我总觉得一切无比陌生。回想起来，第一次到电影院看电影，始于 Lemon 同学请我吃石锅拌饭，对于漫威英雄们的了解，更多的是事后诸葛亮，而唯有浪客剑心系列，一直陪伴着我走过这兵荒马乱的十年。所以，当这个系列走向终点的时候，我果然还是想说点自以为是的话，因为在时代的波涛里，每一个小人物的命运，都不过是艰难挣扎着活下去。
也许，不光是此刻屏幕前的你，就连我自己完全想象不出，有一天我会对日本的影视作品产生兴趣。过去的我，是一个被人称为“不适合在现代社会”的“怪人”。彼时，我喜欢苏轼烟雨任平生的豁达，喜欢稼轩气吞万里如虎的豪迈，喜欢纳兰容若秋风画扇的悲凉……更多的时候，我是一个偏理想化、偏浪漫主义的文艺青年。在我越来越模糊的时光回忆里，全然没有火影忍者、海贼王这些日漫作品的身影，因为在喜欢安静的我看来，这些动漫人物总是在互相“攻伐”，或许是因为日/韩的语言听起来更像是“吵架”，我一度认为这些东西是聒噪而喧嚣的。直到后来，接触到半泽直树、Legal High、白色巨塔这类影视作品，终于对日本人那种听起来像是“吵架”的表演风格有所了解，而像宫崎骏、新海诚、米林宏昌等动画导演的作品，则是后来一点点接触到的，甚至连鬼灭系列完全都是一个偶然，单纯是因为，我想找一个类似无皇刃谭的作品。
绯村剑心与雪代缘战斗 第一次看浪客剑心的时候，我全然不知它是一部漫改作品。当时，除了感觉人物造型有点 cosplay 以外，更多的时候，我喜欢把当作日式古装片/武侠片。庆幸的是，浪客剑心对于幕末/明治时期的社会氛围一直刻画地不错，即使后来浪客剑心更为人所称道的，是 垣谷健治 从中国功夫电影中借鉴到的动作设计。动画版的浪客剑心，或被称之为：明治剑客浪漫谭。也许，是因为 100 多年的历史不远不近，更适合人们去肆意想象。所以，当我们提起明治亦或者民国，我们总是期待，那是一个浪漫的时代。浪客剑心的开场，是鸟羽伏见之战，电影中我们记住的，或许是剑心傲娇地将刀插在地上，因为最终赢得这场战争的，是剑心背后的新政府军。可历史永远比想象更残酷，末代幕府将军德川庆喜，在苍茫夜色中逃往大阪城的时候，是否会不时想起，在大坂夏之阵中独自对抗德川大军的真田幸村。历史是何其地相似，可当你恍然间惊觉，原来泛黄的书页已翻过三百余年。
鸟羽伏见战场 有人说，日本幕府的毁灭始于黑船事件，自此以后便是让日本快速崛起的明治维新。如果把黑船(枪炮)看作西方工业革命的象征，明治维新无疑就是一场西洋枪炮与东瀛武士刀的角逐。所以，在这样一种背景下，浪客剑心里的矛盾冲突，其实都是新时代与旧时代的一次碰撞。禁刀令下，一个曾经双手沾满鲜血的刽子手——绯村拔刀斋，手握一柄逆刃刀，试图斩断一切囿于过去的亡魂，等到他终于放下心中的罪孽感，不再执着于过去发生的一切，脸上的十字刀疤终于消失不见。你告诉我，还有比听起来比这个更浪漫的故事吗？绯村剑心，本名心太，幼年时父母因混乱而死，在被人贩子运送途中遭山贼袭击，幸得飞天御剑流第十三代传人比古清十郎搭救并收为弟子，传授其飞天御剑流剑术。比古清十郎认为心太这个名字太过柔弱，不适合一名剑客，故将其改名为剑心。多年后，剑心与师父的意见向左，师父认为，剑是凶器，剑术就是杀人伎俩，无论是用多么华丽的词藻去粉饰终究是事实。
剑心与师傅比古清十郎 而在一个动荡的乱世，剑术固然可以锄强扶弱，可更多的或许是成为政客手里的杀人工具。一心想亲手拯救人民于水火的剑心，在下山后遇到了桂小五郎、高杉晋作等维新志士组成的奇兵队，自此成为专门暗杀幕府政要的刽子手，其出众的剑术令幕府闻风丧胆，人称刽子手拔刀斋。其实，在时代的洪流里，不管是作为刽子手的拔刀斋，还是作为浪人的剑心，其实都是一个时代的牺牲品。志志雄真实和剑心，本质上都属于同一类人，不同的是，志志雄是在新时代建立后被抛弃的人，因为身体被大面积烧伤而无法正常排汗，内心燥热的火焰终于要随无限刃而喷薄欲出，他从国外购买了铁甲舰、手下集结了十本刀，决心将这个新时代变成炼狱。明治维新，是两个新旧时代的碰撞，在这样一个大背景下，传统的武士、刽子手大量被抛弃。不管是鹈堂刃卫，还是志志雄真实，都失去了存在下去的意义，他们被鲜血和执念吞噬，试图用最极端的方式来证明自己的存在，在武侠的世界里，追求武功天下第一，是每一个习武之人的毕生追求，可禁刀令一出，大家都生活在充满法制、文明的时代，曾经的一切都仿佛不复存在。
武士刀 &amp;amp;amp; 警棍 这种失落感相当真实，多年以前，徐克拍摄《黄飞鸿》时，曾用“铁布衫”严振东的死，表达过这种在坚船利炮面前的无力感。纵观整个浪客剑心系列，除了第一部的反派武田观柳以外，几乎没有绝对的反派。有一个人，和这些囿于过去、不愿放过自己的人形成强烈对比，那就是斋藤一，这个被称为“壬生狼”的前新选组成员，永远奉行着“恶即斩”的主观标准。在每个被人潮推着向前走的时代，没有人能独善其身，可毫无疑问，斋藤一会是适应能力最强的那一类人。显然，剑心是那种愿意向前看，可依然对过去无法释怀的那一类人。有时候，我们会在文学作品中遇到隐形主角，譬如袁崇焕之于碧血剑，而浪客剑心的隐形主角，我以为应该是替剑心打造逆刃刀的新井赤空，一个铸剑师以匠人的心态打造神兵利器，结果这些刀剑都被用作凶器去杀人。同样地，一名剑客以济世救人的心态加入维新志士这一方，结果在迎接新时代到来的过程中夺去了别人的生命。
新井赤空 &amp;amp;amp; 剑心 可以说，剑心手上的逆刃刀，其实就是新井赤空的化身，两个人在赎罪这一心理上是高度一致的，甚至剑心内心的挣扎，早已和这把逆刃刀融为一体，逆刃刀固然会伤到自己，而一个人敢于直视自己的内心，未尝不会被这份鲜血淋漓灼伤，当剑心面对一个又一个的敌人，当剑心身上的秘密一点点被揭开，剑心面对的其实一条自我灵魂的救赎之路。电影中的逆刃刀一共有两把，第一把被称之为“影打”，属于试验品。在和“天剑”宗次郎对决的过程中被名刀虎彻斩断。第二把被称之为“真打”，属于千锤百炼的真品。在关键时刻让剑心打败十本刀之一的“刀狩”泽下条张。在京都大火篇中，当剑心准备从泽下条张手中救下伊织时，剑心有过一段阐述个人理念的独白，大意是说在新时代降生的孩子，都是真正的天选之子，值得他用生命去守护。从这里可以看出，即使曾经作为一个血债累累的刽子手，剑心灵魂深处的仁慈从未丢失过，多年后，他依然还是那个选择埋葬山贼和人贩子尸体的少年心太。
剑心对战 “天剑” 宗次郎 可这个角色让人着迷的地方就在于，剑心身上有着难以融合的关于救赎、杀念和仁慈的混合气质：手执逆刃刀，是为不杀之誓，是为自我救赎；在新时代拒绝传授飞天御剑流剑术，认为神谷活心流的“活人剑”更值得传承下去，是为武者之仁。在我的印象中，剑心只有两次真正动了杀心，一次是从鹈堂刃卫手中救下被“心之一方”麻痹肺部的神谷薰，一次是从“刀狩”下泽条张手中救下新井青空的孩子伊织。有时候，我会想，那个一直让剑心不要再杀人的女人可真狠心。直到后来，我终于明白，雪代巴和神谷薰，都是剑心的剑鞘，一个真正爱你的人，怎么会忍心看着你堕入修罗呢？历史的扑朔迷离，往往来自那些不经意间文过饰非的春秋笔法，浪客剑心的第三部，即传说的完结篇，在这一篇里，伊藤博文宣布，绯村拔刀斋已死，绯村剑心重生。后人已无法知晓，伊藤博文下令向铁甲站舰开炮时的心境，也许在某一瞬间，伊藤博文真的想让剑心，连同这些幕末的亡灵一起葬送于火海。如果是这样，每一个想成为时代弄潮儿的英雄，是否最后都变成了政治家的牺牲品呢？虽然我不得不承认，海滩上明治政府向武士们致敬的这一幕，一旦搭配上飞天的背景音乐，就会成为比少年热血漫还要沸腾的东西。
明治政府向武士们致敬 美国人曾经拍过一部电影《最后的武士》，描写社会变更时期的武士精神如何走向没落。历史的车轮呼啸而过，传统在飞扬的尘土中转瞬湮没。坂本龙马、大久保利通、西乡隆盛等维新志士，在历史的长河里惊鸿一瞥，人类面对滚滚历史长河时的渺小，大概就像大海中浮沉着的一叶孤舟，无论自身多么想要划向远方，最终亦不得不面对历史的进程。在这部电影结尾，明治天皇被阿汤哥的精神感动，从他手中接过胜元的武士刀，这大概是一种艺术加工。因为真实的历史是往往要更加残酷，此后的许多年里，武士道精神被偷换为军国主义，战争给这个世界带来的伤害可谓历历在目。我们说民国浪漫，是一种“为往圣继绝学，为万世开太平”的浪漫，是那种为了一个民族的未来，而甘愿做孺子牛、上下求索的浪漫。假如剥离这层浪漫的滤镜，将历史放大到一个普通人的生活。或许啊，我们看到的会是 《觉醒年代》 里的饿殍遍野、民生多艰。同样地，我们说明治浪漫，是那如夕阳一般绝美的最后的高光时刻。因为，在每一个时代，都有这样一群人，他们在新与旧，改革与保守，东方与西方的冲突中不断地挣扎。
绯村剑心经典红白造型 时至今日，年轻人对国家的未来充满希望，对个人的未来充满绝望，也许是因为，在时代的潮流中，普通人甚至比不上一朵小浪花，一如被剑心斩杀的雪代巴的未婚夫清里，本质上并无对错可言，无非是阵营不同。在浪客剑心里，年幼的心太对比古清十郎说，“人死了不过都是一具尸体”。多年后，我在日剧《Unnatural》 里找到了对应的答案，中堂医生在庭审时说过的话，“人这种生物，无论是谁，切开来剥开皮都只不过是一团肉，等你死了就知道了”。说到底，我们不过是碰巧活着啦，比古清十郎和高荷惠，都曾劝诫剑心，在救人前要先学会自保。或许，爱情更是如此，我们常说，“自爱沉稳而后爱人”，《仁医》 里穿越到幕末时代的医生南方仁，怀着对生命和历史的温情与敬意，不自觉地参与到幕末的各种历史事件中，并由此领悟到，“世间的一切都是先人赐予我们的，是历史中的每个人战斗、挣扎和牺牲所赢得的，更是由无数的生命奇迹编织而成，所以，我们必须用我们的双手，给予后人更加光明的未来”。剧中南方仁的仁是医者之仁，而坂本龙马的仁是以公义超越私爱，这两者共同构成这部电视剧的主题：仁，而剑心的仁在于止杀(戈)，这是真正的武者之仁。
守护世界上最萌的剑心 兔死狐悲的历史总在不断重复上演，历史上的白起、韩信、伍子胥，莫不如是。所以，对于志志雄真实这样一个悲情人物，总是会让人不由心生感慨。原著中的志志雄，不单单有蓄意谋国的野心，甚至开始探索“石油”这种属于未来的科技，在被明治政府抛弃以后，强忍着身心双重折磨，如丧尸一般存活下来。他建立起一套“弱肉强食”的社会达尔文主义理论，在手下十本刀的帮助下，意图颠覆刚建立不久的明治政府。每一个时代都有想成为“弄潮儿”的人，可更多的时候，不过是让这个世界频频陷入“大火”，时代的车轮呼啸着碾过的时候，牺牲的是无数细小的浪花、尘埃，每一个人都想成为英雄，可成为英雄的代价是什么呢？一将功成万骨枯，太阳从树叶的缝隙中穿过的时候，每一片叶子都合成了叶绿素，可难免会刺痛某个躺在树下乘凉的人的眼睛。不管是人诛篇的雪代缘，还是完结篇的志志雄，时代是需要英雄，可你不必非要成为那样的人，我还是想做一个普通人，因为，活着便能创造新的回忆。</description></item><item><title>ASP.NET Core 搭载 Envoy 实现微服务的监控预警</title><link>https://blog.yuanpei.me/posts/1519021197/</link><pubDate>Sat, 10 Jul 2021 14:41:24 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1519021197/</guid><description>在构建微服务架构的过程中，我们会接触到服务划分、服务编写以及服务治理这一系列问题。其中，服务治理是工作量最密集的一个环节，无论是服务发现、配置中心、故障转移、负载均衡、健康检查……等等，这一切的一切，本质上都是为了更好地对服务进行管理，尤其是当我们面对数量越来越庞大、结构越来越复杂的集群化环境的时候，我们需要一种科学、合理的管理手段。博主在上一家公司工作的时候，每次一出现线上故障，研发都要第一时间对问题进行排查和处理，而当时的运维团队，对于微服务的监控止步于内存和CPU，无法系统而全面的掌握微服务的运行情况，自然无法从运维监控的角度给研发部门提供方向和建议。所以，今天这篇文章，博主想和大家聊聊，如何利用Envoy来对微服务进行可视化监控。需要说明的是，本文的技术选型为Envoy + ASP.NET Core + Prometheus + Grafana，希望以一种无侵入的方式集成到眼下的业务当中。本文源代码已上传至 Github ，供大家学习参考。
从 Envoy 说起 在介绍 Envoy 的时候，我们提到了一个词，叫做可观测的。什么叫可观测的呢？官方的说法是， Envoy 内置了stats模块，可以集成诸如prometheus/statsd等监控方案，可以集成分布式追踪系统，对请求进行追踪。对于这个说法，是不是依然有种云里雾里的感觉？博主认为，这里用Metrics这个词会更准确点，即可度量的，你可以认为， Envoy 提供了某种可度量的指标，通过这些指标我们可以对 Envoy 的运行情况进行评估。如果你使用过 Elastic Stack 中的 Kibana，就会对指标(Metrics)这个词汇印象深刻，因为 Kibana 正是利用日志中的各种指标进行图表的可视化的。庆幸的是，Grafana 中拥有与 Kibana 类似的概念。目前， Envoy 中支持三种类型的统计指标：
Counter：即计数器，一种只会增加不会减少的无符号整数。例如，总请求数 Gauge：即计量，一种可以同时增加或者同时减少的无符整数。例如，状态码为200的有效请求数 Timer/Hitogram：即计时器/直方图，一种无符号整数，最终将产生汇总百分位值。Envoy 不区分计时器（通常以毫秒为单位）和 原始直方图（可以是任何单位）。 例如，上游请求时间（以毫秒为单位）。 在今天的这篇文章中，除了 Envoy 以外，我们还需要两位新朋友的帮助，它们分别是Prometheus 和 Grafana。其中，Prometheus 是一个开源的完整监控解决方案，其对传统监控系统如 Nagios、Zabbix 等的测试和告警模型进行了彻底的颠覆，形成了基于中央化的规则计算、统一分析和告警的新模型。可以说，Prometheus 是完整监控解决方案中当之无愧的后起之秀，它最为人所称道的是它强大的数据模型，在 Prometheus 中所有采集到的监控数据吗，都以指标(Metrics)的形式存储在时序数据库中。和传统的关系型数据库中使用的 SQL 不同，Prometheus 定义一种叫做 PromQL 的查询语言，来实现对监控数据的查询、聚合、可视化、告警等功能。
Prometheus &amp;amp;amp; Grafana 的奇妙组合 目前，社区中提供了大量的第三方系统的采集功能的实现，这使得我们可以轻易地对MySQL、PostgresSQL、Consul、HAProxy、RabbitMQ， Redis等进行监控。而 Grafana 则是目前主流的时序数据展示工具，正是因为这个原因， Grafana 总是和 Prometheus 同时出现， Prometheus 中采集到监控数据以后，就可以由 Grafana 赖进行可视化。相对应地，Grafana 中有数据源的概念，除了 Prometheus 以外，它还可以使用来自 Elasticsearch 、InfluxDB 、MySQL 、OpenTSDB 等等的数据。基于这样一种思路，我们需要 Envoy 提供指标信息给 Prometheus ，然后再由 Grafana 来展示这些信息。所以，我们面临的主要问题，其实是怎么拿到 Envoy 中的指标信息，以及怎么把这些指标信息给到 Prometheus 。</description></item><item><title>ASP.NET Core 搭载 Envoy 实现微服务的负载均衡</title><link>https://blog.yuanpei.me/posts/3599307336/</link><pubDate>Mon, 05 Jul 2021 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3599307336/</guid><description>如果说，我们一定要找出一个词来形容这纷繁复杂的世界，我希望它会是熵。有人说，熵增定律是宇宙中最绝望的定律，所谓熵，即是指事物混乱或者无序的程度。在一个孤立系统下，熵是不断在增加的，当熵达到最大值时，系统就会出现严重混乱，直至最终走向死亡。从某种意义上来讲，它揭示了事物结构衰退的必然性，甚至于我们的人生，本来就是一场对抗熵增的旅程。熵增的不可逆性，正如时光无法倒流一般，古人说，“覆水难收”正是这个道理。同样地，当我们开始讨论微服务的划分/编写/治理的时候，当我们使用服务网格来定义微服务架构的时候……我们是否有意或者无意的增加了系统中的熵呢？**一个孤立的系统尚且会因为熵增而最终走向死亡，更何况是相互影响和制约的复杂系统呢？**现代互联网企业都在追求4个9(即99.99%)的高可用，这意味着年平均停机时长只有52.56分钟。在此之前。我们介绍过重试和熔断这两种故障转移的策略，而今天我们来介绍一种更朴素的策略：负载均衡。
什么是负载均衡 负载均衡，即Load Banlancing，是一种计算机技术，用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其它资源中分配负载，以达到最优化资源使用、最大化吞吐率、最小化响应时间、避免过载的目的。
我们可以注意到，在这个定义中，使用负载均衡技术的直接原因是避免过载，而根本原因则是为了优化资源使用，确保最大吞吐量、最小响应时间。所以，这本质上是一个局部最优解的问题，而具体的手段就是&amp;quot;多个&amp;quot;。有人说，技术世界不过是真实世界的一个镜像，联系生活中实际的案例，我们发现负载均衡比比皆是。譬如车站在面对春运高峰时增加售票窗口，银行通过多个业务窗口来为客户办理业务……等等。这样做的好处显而易见，可以大幅度地减少排队时间，增加&amp;quot;窗口&amp;quot;这个行为，在技术领域我们将其称为：水平扩展，因为有多个&amp;quot;窗口&amp;quot;，发生单点故障的概率就会大大降低，而这正是现在软件追求的三&amp;quot;高&amp;quot;：高性能、高可用、高并发。
银行柜员窗口示意图 每次坐地铁经过小寨，时常听到地铁工作人员举着喇叭引导人们往不同的出口方向走动。此时，工作人员就是一个负载均衡器，它要做的就是避免某一个出口人流量过载。**从熵的角度来看，人流量过载，意味着无序/混乱状态加剧，现代社会通过道德和法律来对抗熵增，人类个体通过自律来对抗熵增。**有时候，我会忍不住去想，大人与小孩儿愈发内卷的恶性竞争，除了给这个世界带来更多的熵以外，还能带来什么？如果参考社会达尔文主义的理论，在这个弱肉强食的世界里，增加熵是人为的选择，而同样的，你亦可以选择&amp;quot;躺平&amp;quot;。
负载均衡器示意图 OK，将思绪拉回到负载均衡，它所做的事情，本质上就是控制信息或者说流量流动的方向。一个网站，以集群的方式对外提供服务，你只需要输入一个域名，它就可以把请求分发到不同的机器上面去，而这就是所谓的负载均衡。目前，负载均衡器从种类上可以分为：基于DNS、基于MAC地址(二层)、基于IP(三层)、基于IP和Port(四层)、基于HTTP(七层)。
OSI七层模型与TCP/IP五层模型 譬如，博主曾经参与过伊利的项目，它们使用的就是一个四层的负载均衡器：F5。而像更常见Nginx、HAProxy，基本都是四层和七层的负载均衡器，而Envoy就厉害了，它可以同时支持三/四/七层。负载均衡器需要配合负载均衡算法来使用，典型的算法有：轮询法、随机法、哈希法、最小连接数法等等，而这些算法都可以结合加权算法引出新的变式，这里就不再一一列举啦。
Envoy中的负载均衡 通过上一篇博客，我们已经了解到，Envoy中一个HTTP请求的走向，大致会经历：客户端、侦听器(Listeners)、集群(Clusters)、终结点(Endpoints)、服务(ervices)这样几个阶段。其中，一个集群可以有多个终结点(Endpoints)。所以，这里天然地就存在着负载均衡的设计。因为，负载均衡本质上就是告诉集群，它应该选择哪一个终结点(Endpoints)来提供服务。而之所以我们需要负载均衡，一个核心的原因，其实是因为我们选择了分布式。
Envoy架构图：负载均衡器连接集群和服务 如果类比RabbitMQ、Kafka和Redis，你就会发现，这些产品中或多或少地都会涉及到主(Leader)、从(Follower)以及推举Leader的实现，我个人更愿意将其看作是更广义的负载均衡。最直观的，它可以分担流量，简称分流，不至于让某一台服务器满负荷做运行。其次，它可以作为故障转移的一种方案，人生在世，多一个B计划，就多一种选择。同样地，多一台服务器，就多一分底气。最后，它可以指导某一个产品或者功能的推广，通过给服务器设置不同的权重，在必要的时候，将流量局部地导入某一个环境，腾讯和阿里这样的大厂，经常利用这种方式来做灰度测试。
Envoy中支持常用的负载均衡算法，譬如：ROUND_ROBIN(轮询)、LEAST_REQUEST(最少请求)、RING_HASH(哈希环)、RANDOM(随机)、MAGLEV(磁悬浮)、CLUSTER_PROVIDED等等。因为一个集群下可以有多个终结点，所以，在Envoy中配置负载均衡，本质上就是在集群下面增加终结点，而每个终结点则会对应一个服务，特殊的点在于，这些服务可能是通过同一个Dockerfile或者Docker镜像来构建的。所以，一旦理解了这一点，Envoy的负载均衡就再没有什么神秘的地方。例如，下面的代码片段展示了，如何为WeatherService这个集群应用负载均衡：
clusters: # Weather Service - name: weatherservice connect_timeout: 0.25s type: STRICT_DNS # ROUND_ROBIN(轮询） # LEAST_REQUEST(最少请求) # RING_HASH(哈希环) # RANDOM(随机) # MAGLEV(磁悬浮) # CLUSTER_PROVIDED lb_policy: LEAST_REQUEST load_assignment: cluster_name: weatherservice endpoints: - lb_endpoints: - endpoint: address: socket_address: address: weatherservice1 port_value: 80 - endpoint: address: socket_address: address: weatherservice2 port_value: 80 是不是觉得特别简单？我想说，也许是Envoy更符合人的直观感受一些，理解起来本身没有太大的心智负担。最近看到一个缓存设计，居然还要依赖Kafka，使用者为了使用缓存这个功能，就必须先实现三个丑陋的委托，这就是所谓的心智负担，违背人类的直觉，使用缓存为什么要了解Kafka？到这里，你大概就能了解利用Envoy实现负载均衡的思路，首先是用同一个Dockerfile或者Docker镜像启动多个不同容器(服务)，然后将指定集群下面的终结点指定不同的服务，再告诉集群要用哪一种负载均衡策略即可。
邂逅 ASP.NET Core OK，说了这么多，这里我们还是用ASP.NET Core写一个例子。可以预见到的是，我们需要一个Envoy网关，一个ASP.NET Core的服务。这里，我们还是用Docker-Compose来编排这些服务，下面是对应的docker-compose.yaml文件：</description></item><item><title>ASP.NET Core 搭载 Envoy 实现微服务的反向代理</title><link>https://blog.yuanpei.me/posts/3599307335/</link><pubDate>Thu, 01 Jul 2021 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3599307335/</guid><description>回想起来，博主第一次接触到Envoy，其实是在微软的示例项目 eShopOnContainers，在这个示例项目中，微软通过它来为Ordering API、Catalog API、Basket API 等多个服务提供网关的功能。当时，博主并没有对它做深入的探索。此刻再度回想起来，大概是因为那个时候更迷恋领域驱动设计(DDD)的理念。直到最近这段时间，博主需要在一个项目中用到Envoy，终于决定花点时间来学习一下相关内容。所以，接下来这几篇博客，大体上会以记录我学习Envoy的历程为主。考虑到Envoy的配置项特别多，在写作过程中难免会出现纰漏，希望大家谅解。如对具体的配置项存在疑问，请以官方最新的 文档 为准，本文所用的示例代码已经上传至 Github，大家作为参考即可。对于今天这篇博客，我们来聊聊 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 这个话题，或许你曾经接触过 Nginx 或者 Ocelot，这次我们不妨来尝试一点新的东西，譬如，通过Docker-Compose来实现服务编排，如果对我说的这些东西感兴趣的话，请跟随我的脚步，一起来探索这广阔无垠的技术世界吧！
走近 Envoy Envoy 官网对Envoy的定义是：
Envoy 是一个开源边缘和服务代理，专为原生云应用设计。
而更进一步的定义是：
Envoy 是专为大型现代服务导向架构设计的 L7 代理和通讯总线。
这两个定义依然让你感到云里雾里？没关系，请看下面这张图：
Envoy架构图 注：图片来源
相信从这张图中，大家多少能看到反向代理的身影，即下游客户端发起请求，Envoy对请求进行侦听(Listeners)，并按照路由转发请求到指定的集群(Clusters)。接下来，每一个集群可以配置多个终结点，Envoy按照指定的负载均衡算法来筛选终结点，而这些终结点则指向了具体的上游服务。例如，我们熟悉的 Nginx ，使用listen关键字来指定侦听的端口，使用location关键字来指定路由，使用proxy_pass关键字来指定上游服务的地址。同样地，Ocelot 使用了类似的上下游(Upstream/Downstream)的概念，唯一的不同是，它的上下游的概念与这里是完全相反的。
你可能会说，这个Envoy看起来“平平无奇”嘛，简直就像是“平平无奇”的古天乐一般。事实上，Envoy强大的地方在于：
非侵入式的架构： 独立进程、对应用透明的Sidecar模式 Envoy 的 Sidecar 模式 L3/L4/L7 架构：Envoy同时支持 OSI 七层模型中的第三层(网络层, IP 协议)、第四层(传输层，TCP / UDP 协议)、第七层(应用层，HTTP 协议) 顶级 HTTP/2 支持： 视 HTTP/2 为一等公民，且可以在 HTTP/2 和 HTTP/1.1间相互转换 gRPC 支持：Envoy 支持 HTTP/2，自然支持使用 HTTP/2 作为底层多路复用协议的 gRPC 服务发现和动态配置：与 Nginx 等代理的热加载不同，Envoy 可以通过 API 接口动态更新配置，无需重启代理。 特殊协议支持：Envoy 支持对特殊协议在 L7 进行嗅探和统计，包括：MongoDB、DynamoDB 等。 可观测性：Envoy 内置 stats 模块，可以集成诸如 prometheus/statsd 等监控方案。还可以集成分布式追踪系统，对请求进行追踪。 Envoy配置文件 Envoy通过配置文件来实现各种各样的功能，其完整的配置结构如下：</description></item><item><title>ASP.NET Core gRPC 打通前端世界的尝试</title><link>https://blog.yuanpei.me/posts/2167892202/</link><pubDate>Sun, 20 Jun 2021 21:37:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2167892202/</guid><description>在构建以 gRPC 为核心的微服务架构的过程中，我们逐渐接触到了 gRPC 的过滤器、健康检查、重试等方面的内容。虽然， Protocol Buffers 搭配 HTTP/2 ，在整个传输层上带来了显著的性能提升，可当这套微服务方案面对前后端分离的浪潮时，我们能明显地有点“水土不服”。其实，如果单单是以 Protocol Buffers 来作为 HTTP 通信的载体，通过 protobuf.js 就可以实现前端的二进制化。考虑到 gRPC 实际的通信过程远比这个复杂，同时还要考虑.proto文件在前/后端共享的问题，所以，我们面对的其实是一个相当复杂的问题。现代的前端世界，是一个React、Angular和Vue三足鼎立的世界，如果这个世界不能和微服务的世界打通，我们面对的或许并不是一个真实的世界。因为博主注意到，项目中有一部分 gRPC 服务被封装为Web API并提供给前端，这说明大家都意识到了这个问题。所以，这篇博客想和大家分享的是，如何打通 gRPC 和 前端 两个不同的世界，这里介绍四种方式：gRPC-Web、gRpc-Gateway、封装 Web API、编写中间件，希望能给大家带来一点启发。
gRPC-Web gRPC-Web 是官方提供的一个方案，它的原理是利用命令行工具ptotoc及其插件protoc-gen-grpc-web来生成.proto对应的客户端代码，这些代码经过webpack这类打包工具处理以后，就可以在前端使用。所以，对于 gRPC-Web ，你可以从两个方面来考虑它：第一，它支持生成强类型的客户端代码；第二，它支持在非 HTTP/2 环境下使用 gRPC 。下面是一个基本的使用流程：
首先，我们需要下载命令行工具：protoc 及其插件：protoc-gen-grpc-web。
此时，我们可以使用下面的命令来生成JavaScript版本的 gRPC 代码：
protoc greetjs.proto \ --js_out=import_style=commonjs:. \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. \ --plugin=protoc-gen-grpc-web=C:\Users\Payne\go\bin\protoc-gen-grpc-web.exe 其中：
--js_out 和 --grpc-web_out 分别指定了我们要生成的JavaScript代码的模块化标准，这里使用的是 CommonJS 规范。 mode=grpcwebtext 指定 gRPC-Web 的数据传输方式。目前：支持两种方式，application/grpc-web-text(Base64 编码，文本格式) 和 application/grpc-web+proto(二进制格式)，前者支持 Unary Calls 和 Server Streaming Calls，后者只支持 Unary Calls。 在这个例子中，会生成下面两个文件，它们分别定义了客户端和消息这两个部分：</description></item><item><title>EFCore 实体命名约定库：EFCore.NamingConventions</title><link>https://blog.yuanpei.me/posts/3219639636/</link><pubDate>Thu, 17 Jun 2021 16:37:11 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3219639636/</guid><description>在软件开发过程中，数据库永远都是绕不开的一个话题。有时候，我们甚至会因此而获得一个名字——“CURD Boy”。虽然不过是朴实无华的“增删查改”，可隐隐然早已分出了无数的流派。在这些不同的流派中，有的人坚持“我手写我心”，认为手写SQL才是真正的王道，没有读过/写过成百上千行的存储过程，便不足以谈论程序员的人生。而有的人喜欢ORM的清晰、整洁，认为数据库和面向对象存在着天然抗阻，ORM更有利于推进DDD和微服务的落地。相信大家都听说过Java里的SSH框架，从Hibernate到Mybatis再到Spring Data JPA，可以说这种争论一直没有停止过。这里我们不打算讨论这个问题，我们平时使用EF或者EFCore的过程中，作为连接数据库和面向对象两个异世界的桥梁，ORM需要我们来告诉它，实体数据与数据库表字段的映射关系，所以，经常需要通过数据注解或者Fulent API来写各种配置。那么，有没有什么方案可以让我们偷这个懒呢？下面隆重请出本文的主角：EFCore.NamingConventions。
使用方法 EFCore. NamingConventions，目前由一个非官方的组织进行维护，代码托管在 Github 上，100％的开源项目。
如果你希望直接使用它的话，可以直接通过NuGet进行安装：
Install-Package EFCore.NamingConventions 接下来，我们只需要在DbContext的 OnConfiguring()方法中，调用它提供的扩展方法即可：
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =&amp;gt; optionsBuilder .UseSqlite(&amp;#34;Data Source=Chinook.db&amp;#34;) .UseSnakeCaseNamingConvention(); 或者，你可以使用依赖注入的方式：
services.AddDbContext&amp;lt;ChinookContext&amp;gt;(options =&amp;gt; options.UseSqlite(&amp;#34;Data Source=Chinook.db&amp;#34;) .UseSnakeCaseNamingConvention() ); 这里我以SQLite数据库为例，来展示它的具体使用细节。事实上，它提供了 4 种命名约定的策略：
UseSnakeCaseNamingConvention: FullName -&amp;gt; full_name UseLowerCaseNamingConvention: FullName -&amp;gt; fullname UseCamelCaseNamingConvention: FullName -&amp;gt; fullName UseUpperCaseNamingConvention: FullName -&amp;gt; FULLNAME 简单来说，就是当我们的实体中存在一个属性FullName时，它会告诉EF或者EFCore，这个属性FullName对应的表字段是什么。
虽然，在大多数的场景中，我们都希望属性名称和表字段一致，可你要知道，像Oracle这种对大小写敏感的数据库，特别喜欢自作聪明地帮你全部改成大写。
所以，在上家公司工作的时候，为了兼容Oracle这病态的癖好，公司里有个不成文的规定，那就是：所有实体的属性名称最好都大写。
本来大家用驼峰命名就是为了好认单词，好家伙！这下全部大写了，一眼望过去简直就是灾难，因为没有办法做到“望文生义”，如果那个时候知道这个库的存在，是不是就能解决这个问题了呢？
第一个示例 下面我们以UseSnakeCaseNamingConvention为例，结合SQLite来做一个简单的例子。
首先，我们定义必要的实体，并为DbContext配置实体命名约束规则：
// Album public class Album { public int AlbumId { get; set; } public string Title { get; set; } public int ArtistId { get; set; } public string TenantId { get; set; } } // Artist public class Artist { public int ArtistId { get; set; } public string Name { get; set; } public string TenantId { get; set; } } 接下来，通过迁移命令来生成数据库架构：</description></item><item><title>ASP.NET Core gRPC 集成 Polly 实现优雅重试</title><link>https://blog.yuanpei.me/posts/2742255459/</link><pubDate>Mon, 07 Jun 2021 15:19:11 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2742255459/</guid><description>在上一篇 博客 中，我们一起探索和实现了gRPC的健康检查。从服务治理的角度来看，健康检查保证的是被调用的服务“健康”或者“可用”。可即使如此，我们依然会遇到，因为网络不稳定等原因而造成的服务调用失败的情形，就如同我们赖以生存的这个真实世界，本身就充满了各种不确定的因素一样，“世间唯一不变的只有变化本身”。不管是面对不稳定的服务，还是面对不确定的人生，任何时候我们都需要有一个 B 计划，甚至我们人生中的一切努力，本质上都是为了多一份自由，一份选择的自由。在微服务的世界里，我们将这种选择称之为“降级(Fallback)”，如果大家有接触过 Hystrix 或者 Polly 这类框架，就会明白我这里的所说的“降级”具体是什么。在众多的“降级”策略中，重试是一种非常朴素的策略，尤其是当你调用一个不稳定的服务的时候。
重试 引言 在此之前，博主曾经介绍过 HttpClient 的重试。所以，今天这篇博客我们来聊聊gRPC的客户端重试，因为要构建一个高可用的微服务架构，除了需要高可用的服务提供者，同样还需要高可用的服务消费者。下面，博主将由浅入深地为大家分享 4 种重试方案的实现，除了 官方 内置的方案，基本上都需要搭配 Polly 来使用，所以，到这里你可以理解这篇博客的标题，为什么博主会 毁人不倦 地尝试不同的重试方案，因为每一种方案都有它自身的局限性，博主想要的是一种更优雅的方案。具体来讲，主要有：基于 gRPC RetryPolicy、基于 HttpClientFactory、基于 gRPC 拦截器 以及 基于 CallInvoker 4 种方案。如果大家还有更好的思路，欢迎大家在博客评论区积极留言、参与讨论。
基于 gRPC RetryPolicy 所谓的 gRPC RetryPolicy，其实是指 官方 提供的暂时性故障处理方案，它允许我们在创建GrpcChannel的时候，去指定一个重试策略：
var defaultMethodConfig = new MethodConfig { Names = { MethodName.Default }, RetryPolicy = new RetryPolicy { MaxAttempts = 5, InitialBackoff = TimeSpan.FromSeconds(1), MaxBackoff = TimeSpan.FromSeconds(5), BackoffMultiplier = 1.5, RetryableStatusCodes = { StatusCode.</description></item><item><title>ASP.NET Core gRPC 健康检查的探索与实现</title><link>https://blog.yuanpei.me/posts/1657075397/</link><pubDate>Tue, 01 Jun 2021 11:37:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1657075397/</guid><description>各位朋友，大家好，欢迎大家关注我的博客。在上一篇 博客 中，博主和大家分享了gRPC的拦截器在日志记录方面的简单应用，今天我们继续来探索gRPC在构建微服务架构方面的可能性。其实，从博主个人的理解而言，不管我们的微服务架构是采用RPC方式还是采用RESTful方式，我们最终要面对的问题本质上都是一样的，博主这里将其归纳为：服务划分、服务编写 和 服务治理。首先，服务划分决定了每一个服务的上下文边界以及服务颗粒度大小，如果按照领域驱动设计(DDD)的思想来描述微服务，我认为它更接近于限界上下文(BoundedContext)的概念。其次，服务编写决定了每一个服务的具体实现方式，譬如是采用无状态的RESTful风格的API，还是采用强类型的、基于代理的RPC风格的API。最后，服务治理是微服务架构中永远避不开的话题，服务注册、服务发现、健康检查、日志监控等等一切的话题，其实都是在围绕着服务治理而展开，尤其是当我们编写了一个又一个的服务以后，此时该如何管理这些浩如“星”海的服务呢？所以，在今天这篇博客中，博主想和大家一起探索下gRPC的健康检查，希望能给大家带来一点启发。
健康检查-服务注册-服务发现示意图 关于“健康检查”，大家都知道的一点是，它起到一种“防微杜渐”的作用。不知道大家还记不记得，语文课本里的经典故事《扁鹊见蔡桓公》，扁鹊一直在告知蔡桓公其病情如何，而蔡桓公讳疾忌医，直至病入骨髓、不治而亡。其实，对应到我们的领域知识，后端依赖的各种服务譬如数据库、消息队列、Redis、API 等等，都需要这样一个“扁鹊”来实时地“望闻问切”，当发现问题的时候及时地采取相应措施，不要像“蔡桓公”一样病入骨髓，等到整个系统都瘫痪了，这时候火急火燎地去“救火”，难免会和蔡桓公一样，发出“悔之晚矣”的喟叹。当我们决定使用gRPC来构建微服务架构的时候，我们如何确保这些服务一直是可用的呢？所以，提供一种针对gRPC服务的健康检查方案就会显得非常迫切。这里，博主主要为大家介绍两种实现方式，它们分别是：基于IHostedService的实现方式 以及 基于Consul的实现方式。
基于 IHostedService 的实现方式 第一种方式，主要是利用IHostedService可以在程序后台执行的特点，搭配Timer就可以实现定时轮询。在 gRPC 的 官方规范 中，提供了一份Protocol Buffers的声明文件，它规定了一个健康检查服务必须实现Check()和Watch()两个方法。既然是官方定义好的规范，建议大家不要修改这份声明文件，我们直接沿用即可：
syntax = &amp;#34;proto3&amp;#34;; package grpc.health.v1; message HealthCheckRequest { string service = 1; } message HealthCheckResponse { enum ServingStatus { UNKNOWN = 0; SERVING = 1; NOT_SERVING = 2; } ServingStatus status = 1; } service Health { rpc Check(HealthCheckRequest) returns (HealthCheckResponse); rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); } 接下来，我们需要实现对应的HealthCheckService:
public class HealthCheckService : Health.</description></item><item><title>ASP.NET Core gRPC 拦截器的使用技巧分享</title><link>https://blog.yuanpei.me/posts/1679688265/</link><pubDate>Wed, 26 May 2021 09:03:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1679688265/</guid><description>gRPC是微软在.NET Core 及其后续版本中主推的 RPC 框架，它使用 Google 的 Protocol Buffers 作为序列化协议，使用 HTTP/2 作为通信协议，具有跨语言、高性能、双向流式调用等优点。考虑到，接下来要参与的是，一个以gRPC为核心而构建的微服务项目。因此，博主准备调研一下gRPC的相关内容，而首当其冲的，则是从 .NET Core 3.1 开始就有的拦截器，它类似于ASP.NET Core中的过滤器和中间件，体现了一种面向切面编程(AOP)的思想，非常适合在 RPC 服务调用的时候做某种统一处理，譬如参数校验、身份验证、日志记录等等。在今天这篇博客中，博主主要和大家分享的是，利用 .NET Core gRPC 中的拦截器实现日志记录的简单技巧，希望能给大家带来一点启发。
开源、多语言、高性能的 gRPC 关于 Interceptor 类 Interceptor类是 gRPC 服务拦截器的基类，它本身是一个抽象类，其中定义了下面的虚方法：
public virtual AsyncClientStreamingCall&amp;lt;TRequest, TResponse&amp;gt; AsyncClientStreamingCall&amp;lt;TRequest, TResponse&amp;gt;(); public virtual AsyncDuplexStreamingCall&amp;lt;TRequest, TResponse&amp;gt; AsyncDuplexStreamingCall&amp;lt;TRequest, TResponse&amp;gt;(); public virtual AsyncUnaryCall&amp;lt;TResponse&amp;gt; AsyncUnaryCall&amp;lt;TRequest, TResponse&amp;gt;(); public virtual TResponse BlockingUnaryCall&amp;lt;TRequest, TResponse&amp;gt;(); public virtual Task&amp;lt;TResponse&amp;gt; ClientStreamingServerHandler&amp;lt;TRequest, TResponse&amp;gt;(); public virtual AsyncServerStreamingCall&amp;lt;TResponse&amp;gt; AsyncServerStreamingCall&amp;lt;TRequest, TResponse&amp;gt;(); public virtual Task DuplexStreamingServerHandler&amp;lt;TRequest, TResponse&amp;gt;(); public virtual Task ServerStreamingServerHandler&amp;lt;TRequest, TResponse&amp;gt;(); public virtual Task&amp;lt;TResponse&amp;gt; UnaryServerHandler&amp;lt;TRequest, TResponse&amp;gt;(); 整体而言，如果从通信方式上来划分，可以分为：流式调用 和 普通调用；而如果从使用方来划分，则可以分为：客户端 和 服务端。进一步讲的话，针对流式调用，它还分为：&amp;quot;单向流&amp;quot; 和 &amp;ldquo;双向流&amp;quot;。关于这些细节上的差异，大家可以通过 gRPC 的 官方文档 来了解，这里我们给出的是每一种方法对应的用途：</description></item><item><title>SnowNLP 使用自定义语料进行模型训练</title><link>https://blog.yuanpei.me/posts/1772340994/</link><pubDate>Wed, 19 May 2021 21:22:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1772340994/</guid><description>SnowNLP 是一个功能强大的中文文本处理库，它囊括了中文分词、词性标注、情感分析、文本分类、关键字/摘要提取、TF/IDF、文本相似度等诸多功能，像隐马尔科夫模型、朴素贝叶斯、TextRank等算法均在这个库中有对应的应用。如果大家仔细观察过博主的博客，就会发现博主使用了摘要提取这一功能来增强博客的SEO，即通过自然语言处理(NLP)技术，提取每一篇文章中的摘要信息。因为 SnowNLP 本身使用的语料是电商网站评论，所以，当我们面对不同的使用场景时，它自带的这个模型难免会出现“水土不服”。因此，如果我们希望得到更接近实际的结果，最好的方案是使用自定义语料进行模型训练。值得庆幸的是，这一切在 SnowNLP 中实施起来非常简单，并不需要我们去钻研那些高深莫测的算法。至此，就引出了今天这篇博客的主题，即 SnowNLP 使用自定义语料进行模型训练。
不知道大家是否还有印象，博主曾经在 《通过 Python 分析 2020 年全年微博热搜数据》 这篇文章中提到过 SnowNLP 的模型训练。当时，博主采集了整个 2020 年的微博热搜话题，因为要体现整个一年里的情感变化，博主特意找了两份微博语料，并以此为基础训练出了一个模型文件。
2020全年微博热搜情感变化趋势 那么，具体是怎么样做的呢？我们一起来看一下：
from snownlp import sentiment sentiment.train(&amp;#39;./train/neg60000.txt&amp;#39;, &amp;#39;./train/pos60000.txt&amp;#39;) sentiment.save(&amp;#39;weibo.marshal&amp;#39;) 千万不要怀疑你的眼睛，因为它真的只有短短的三行代码。简单来说，我们只需要准备一个“积极”的语料文件，一个“消极”的语料文件，它就可以训练出一个模型文件。特别注意的是，如果是在Python 3.X的版本下，最终生成的模型文件的扩展名将会是.3，下图是博主这里训练出的模型文件：
SnowNLP 使用自定义语料进行模型训练 好了，一旦训练出这个模型文件，我们就可以考虑替换掉 SnowNLP 的默认模型文件，我们可以在以下位置：\Lib\site-packages\snownlp\sentiment 找到下列文件。为了安全起见，我们首先将原来的模型文件重命名，然后再放入我们自己的模型文件。
SnowNLP 使用自定义模型替换默认模型 此时，我们就可以利用训练好的模型，分析某一条微博的情感倾向。这里我选取了几条我的微博，看看这个情感倾向预测的结果如何：
from snownlp import SnowNLP s = SnowNLP(u&amp;#39;我爱你，并不期待回声&amp;#39;) s.sentiments # 0.8760737296091975 s = SnowNLP(u&amp;#39;想找一个人，一起做老爷爷、老奶奶才做的事情，比如，替我拔一拔头上的白头发……[二哈] ​​&amp;#39;) s.sentiments # 0.001629297651780881 s = SnowNLP(u&amp;#39;如果两个人都不爱了，一别两宽，各生欢喜，其实是挺好的结局；可如果还有一个人爱着，对那个人来说，爱又是什么呢？&amp;#39;) s.sentiments # 0.809651945221708 s = SnowNLP(u&amp;#39;为了发张自拍，特意出来跑步，还有谁？[doge] ​​​&amp;#39;) s.sentiments # 0.4041057894917053 有人说，双子座是一个白天自愈、晚上孤独的星座，我确信这是真的，因为从我出生的那一刻起，那种宏大宇宙中的孤独感就一直笼罩着我，用一句话来形容，大概就是“热闹是人家的，我什么都没有”，因为内心世界里的两个灵魂，从来没有一刻闲歇地在纠缠和撕裂。我一直都想了解一件事情，如果这些基于概率或者是公式的算法，都可以琢磨出人类某个时刻的心境，我们期望别人能懂自己是不是太过矫情，我们是真的了解自己吗？</description></item><item><title>假如时间有温度</title><link>https://blog.yuanpei.me/posts/2136925853/</link><pubDate>Mon, 03 May 2021 14:00:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2136925853/</guid><description>我一直在想，世事无常，该是一种什么样的感觉。直到我读到夏目先生的《我是猫》，先生在书中不无感慨地写道，“世事变迁就像猫的眼珠一样变幻莫测”。可此时此刻，我会不由得觉得，世事无常，更像是时间突然间有了温度，“春观夜樱，夏望繁星，秋赏满月，冬会初雪”，拥有这般温度的时间毫无疑问是浪漫的，可世事无常所带来的时间的温度，更像是某种意义上的极致，譬如从地球两极瞬移到赤道，或者是一场大爆炸后突兀着的宁静。也许，四季还是那个四季，无非是我一厢情愿的自以为是，时间真的有温度吗？
这次终于可以乘坐高铁回家，可当列车以每小时 250 公里的速度呼啸而过时，我已来不及仔细留意车窗外的风景。我隐隐约约地觉得，外面的山丘变得平缓，时不时穿过漆黑悠长的隧道，平原上点缀着麦田和葡萄架，等到列车横跨着黄河驶过的时候，我终于确信我回到了故乡。而我不得不说，人生的境遇里实在有太多的似曾相识，正如此刻窗外的风，兀自呼啸着撼动着那棵我自小便认识的树。回家后收到的第一个消息是，家族中一位叔叔的儿子，在工作时不慎从高处摔落下来，送到医院以后终于还是没能抢救过来，听长辈们讲，彼时他们正在参加某个人的婚礼，一时间百感交集。
可以说，这是我这么多年来，第一次以一个成年人的身份去面对一个人的离开。因为逝者与我为同一辈人，所以于情于理我都要去吊唁一番。于是，快三十岁的人，第一次有了买花圈、写挽联的经历，甚至我在去见这位叔叔的时候，在脑海中浮现了多次的“还请您节哀顺变”，终于还是没能说出口来。或许是因为事出突然，有太多的身后事需要料理，留给悲伤的时间并不多。在逝者面前焚香、叩拜、鞠躬，虽然有长辈从旁指点，可整套动作还是显得有点僵硬。我终于还是想起来，这个只有 27 岁的年轻人，在我某次回家探亲的时候，自顾自走上前来，面带微笑的自我介绍道，“我是某某某，你不认得我了吗？”
有时候想想，我喜欢怀旧，喜欢念念不忘，或许就是因为我怕，怕生命中每一次告别都是永诀。同样可以认为是第一次的，也许是公墓，是陵园，这种从前只有在电视上见到过的东西。于是，在夕阳的映照下，半边天空被染成金黄色，而在这一片荒凉中，一座六角形的塔静静地矗立着。站在一个高坡上一眼望去，满眼都是密密麻麻的墓碑。我在想，有一天人们会不会建成更加极致的地下宫殿，就如同城市中越来越多的高楼大厦一样，唯一的不同，或许是那具比单人床还要小一点的棺木，或者是和小酒坛差不多大的骨灰盒。独自站在旷野中，风吹着塔角的铃铛不时发出响声，我敲击不锈钢柱子时，它竟然发出了沉钟一般的轰鸣，难道人真的有灵魂吗？
对于死亡，从小到大，我着实经历了不少，小学时爷爷去世，中学时有位同学被歹人杀害，大学时有位同学患白血病不治而亡，工作以后有一位同事因意外而溺水身亡……有时候想想，虽然我的人生，可能并不如别人那般精彩绝伦，可比起失去生命的他们，我能见到更多的人，见到更多的事情，这实在是幸运中的幸运。可或许是因为故事的视角发生了改变，所以，此刻比往常有了更多不由分说的感慨，就好像从前的我，虽然一样是某个事件的亲历者，但那时的我，还不大懂得死亡的意义，都说是人死灯灭，可只有你自己知道，一旦别人彻底地忘记了你，忘记了你在这世上的故事，你就大概的确真的死了罢！我们终其一生，不论记忆以文字还是影像的形式存在，所求者不过是记住别人和被别人记住，人生如朝露也好，如雪泥鸿爪也罢，也许，珍惜此时此刻，方能无惧参商永隔的痛苦吧……
很多年前，作为长孙的我，举着高过我头顶的引魂幡走在前面，风裹挟着引魂幡的纸穗呼呼作响，那时，我还不知道再也见不到一个人，将会是多么难过的一件事情。后来，我偶尔会回想起，夏天做完农活回来，坐在凉席上吃西瓜的情形，就是在那个时候，爷爷开始埋怨头上有白头发，而我则被拉去帮爷爷找白头发。再后来，我偶尔会想有个人帮我找白头发，可明明我还没到三十岁啊，直到我看到三叔后脑勺开始变白，我终于惊觉，这是二十年前的事情了。有时候想想，我人生中最美好的那几年，同这二十年的长度相比，何尝不是沧海一粟呢？人生时常如此，你觉得几十年特别漫长，可二十年你还不是就这样“弹指一挥间”，而人生又特别短暂，短暂到我们怕这次见了就再见不着彼此。这样想来，拉黑或者删除一个人，成本简直低廉到无法想象，因为失去得太容易，大家就不会有这种看似突兀的想法。浮生倥偬，失散在风里的是沙，而失散在水里的是萍，失散的人们，会有引魂幡前来招魂，然后各自相认吗？
所以，时间有温度吗？我想，该是有的，因为我们会在时间的长河里放下一盏浮灯，它承载着我们记忆深处最温暖的回忆。可也许这只是我们的一厢情愿，时间自顾自地往前走，从来不在乎人的记忆到底如何，就如同窗外呼啸而过的风，它并不懂得人类内心深处的那些情感，所以，更多的时候，我以为，时间是没有温度的，是冰冷的，是荒凉的，就像我在陵园里看到的夕阳一般冰冷，即使它被晚霞映得金黄。有时候，我会期待时间走得稍微慢一点，出于我的自私，我希望我此刻爱着的、曾经爱过的人们，都能老去地稍微慢一点，因为我怕再见不到那个人，因为我怕时间凝固成冰，因为我怕我终有一天要忘记，因为我怕我永远都赶不上时间，这或许是我想在此时此刻赋予时间的温度，如同人的正常体温 37 度，或许，它是如此的平静甚至是普通，可是啊，活着真的很好啊。</description></item><item><title>使用 HttpMessageHandler 实现 HttpClient 请求管道自定义</title><link>https://blog.yuanpei.me/posts/2070070822/</link><pubDate>Wed, 28 Apr 2021 20:25:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2070070822/</guid><description>最近，博主偶然间在 博客园 看到一篇文章：ASP.NET Core 扩展库之 Http 请求模拟，它里面介绍了一种利用 HttpMessageHandler 来实现 Http 请求模拟的方案。在日常工作中，我们总是不可避免地要和第三方的服务或者接口打交道，尤其是当我们需要面对“联调”这样一件事情的时候。通常，我们可以通过类似 YAPI 这样的工具来对尚在开发中的接口进行模拟。可是，因为这种方式会让我们的测试代码依赖于一个外部工具，所以，从严格意义上讲，它其实应该属于“集成测试”的范畴。在接触前端开发的过程中，对于其中的 Mock.js 印象深刻。故而，当看到 .NET 中有类似实现的时候，好奇心驱使我对其中的核心，即 HttpMessageHandler 产生了浓厚的兴趣。平时，我们更多的是使用 Moq 这样的库来模拟某一个对象的行为，而对一个 Http 请求进行模拟，可以说是开天辟地头一遭。带着这些问题出发，就有了今天这篇博客，通过 HttpMessageHandler 实现 HttpClient 请求管道的自定义。
什么是 HttpMessageHandler？ 相信大家读过我提到的文章以后，都能找到这里面最核心的一个点：HttpMessageHandler。于是，我们今天要面对的第一个问题就是，什么是 HttpMessageHandler？此时，我们需要一张历久弥新的示意图，来自 微软官方。这里，我们重点关注的是 DelegatingHandler，它继承自 HttpMessageHandler。通过这张图，我们能够获得哪些信息呢？
我认为，主要有以下几点：第一，HttpMessageHandler 处于整个 Http 请求管道的第一梯队，每一个路由匹配的请求都会从这里“进入”和“离开”；第二，HttpMessageHandler 可以是全局配置或者针对某个特定的路由，只要这个路由被匹配到就会执行；第三，HttpMessageHandler 可以直接构造 Http 响应并且返回，跳过剩余的管道流程。不知道大家看到这里会想到什么？坦白讲，我联想到了.NET Core 中的中间件，而唯一不同的地方或许是，中间件是 ASP.NET Core 里的概念，这里则是 ASP.NET Web API 里的概念。尤其是第三点，它对于我们的意义非常重大，因为它，我们才可以做到对一个 Http 请求进行模拟。
HttpMessageHandler 与 ASP.NET Web API 而事实上，在 ASP.NET Web API 的设计中，它是由一组 HttpMessageHandler 经过“首尾相连”而成，这种管道式的设计使得框架本身具有很高的扩展性。虽然，作为一个服务端框架，ASP.NET Web API 最主要的作用是就是“处理请求、响应回复”，可具体采用的处理策略会因具体场景的不同而不同。所以，管道式设计的本质，就是让某一个 Handler 只负责某个单一的消息处理功能，在根据具体场景的不同，选择需要的 Handler 并将其串联成一个完整的消息处理通道。而在这里，这个负责单一的消息处理功能的 Handler 其实就是 HttpMessageHandler，因为它不单单可以对请求消息(HttpRequestMessage)进行处理，同时还可以对响应消息(HttpResponseMessage)进行处理。此时，我们就不难理解 HttpMessageHandler 的定义：</description></item><item><title>ABP vNext 的实体与服务扩展技巧分享</title><link>https://blog.yuanpei.me/posts/3619320289/</link><pubDate>Sun, 18 Apr 2021 20:42:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3619320289/</guid><description>使用 ABP vNext 有一个月左右啦，这中间最大的一个收获是：ABP vNext 的开发效率真的是非常好，只要你愿意取遵循它模块化、DDD 的设计思想。因为官方默认实现了身份、审计、权限、定时任务等等的模块，所以，ABP vNext 是一个开箱即用的解决方案。通过脚手架创建的项目，基本具备了一个专业项目该有的“五脏六腑”，而这可以让我们专注于业务原型的探索。例如，博主是尝试结合 Ant Design Vue 来做一个通用的后台管理系统。话虽如此，我们在使用 ABP vNext 的过程中，还是希望可以针对性地对 ABP vNext 进行扩展，毕竟 ABP vNext 无法 100% 满足我们的使用要求。所以，在今天这篇博客中，我们就来说说 ABP vNext 中的扩展技巧，这里主要是指实体扩展和服务扩展这两个方面。我们经常在讲“开闭原则”，可扪心自问，我们每次修改代码的时候，是否真正做到了“对扩展开放，对修改关闭”呢？ 所以，在面对扩展这个话题时，我们不妨来一起看看 ABP vNext 中是如何实践“开闭原则”。
扩展实体 首先，我们要说的是扩展实体，什么是实体呢？这其实是领域驱动设计(DDD)中的概念，相信对于实体、聚合根和值对象，大家早就耳熟能详了。在 ABP vNext 中，实体对应的类型为Entity，聚合根对应的类型为AggregateRoot。所以，你可以片面地认为，只要继承自Entity基类的类都是实体。通常，实体都会有一个唯一的标识(Id)，所以，订单、商品或者是用户，都属于实体的范畴。不过，按照业务边界上的不同，它们会在核心域、支撑域和通用域三者间频繁切换。而对于大多数系统而言，用户都将是一个通用的域。在 ABP vNext 中，其用户信息由AbpUsers表承载，它在架构上定义了IUser接口，借助于EF Core的表映射支持，我们所使用的AppUser本质上是映射到了AbpUsers表中。针对实体的扩展，在面向数据库编程的业务系统中，一个最典型的问题就是，我怎么样可以给AppUser添加字段。所以，下面我们以AppUser为例，来展示如何对实体进行扩展。
DDD 中的实体、聚合根与值对象 实际上，ABP vNext 中提供了2种方式，来解决实体扩展的问题，它们分别是：Extra Properties 和 基于 EF Core 的表映射。在 官方文档 中，我们会得到更加详细的信息，这里简单介绍一下就好：
Extra Properties 对于第1种方式，它要求我们必须实现IHasExtraProperties接口，这样我们就可以使用GetProperty()和SetProperty()两个方法，其原理是，将这些扩展字段以JSON格式存储在ExtraProperties这个字段上。如果使用MongoDB这样的非关系型数据库，则这些扩展字段可以单独存储。参考示例如下：
// 设置扩展字段 var user = await _identityUserRepository.GetAsync(userId); user.SetProperty(&amp;#34;Title&amp;#34;, &amp;#34;起风了，唯有努力生存&amp;#34;); await _identityUserRepository.UpdateAsync(user); // 读取扩展字段 var user = await _identityUserRepository.</description></item><item><title>ABP vNext 对接 Ant Design Vue 实现分页查询</title><link>https://blog.yuanpei.me/posts/3670340170/</link><pubDate>Wed, 07 Apr 2021 21:07:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3670340170/</guid><description>在 上一篇 博客中，博主和大家分享了如何在 EF Core 中实现多租户架构。在这一过程中，博主主要参考了 ABP vNext 这个框架。从上个月开始，我个人发起了一个项目，基于 ABP vNext 和 Ant Design Vue 来实现一个通用的后台管理系统，希望以此来推进 DDD 和 Vue 的学习，努力打通前端与后端的“任督二脉”。因此，接下来的这段时间内，我写作的主题将会围绕 ABP vNext 和 Ant Design Vue。而在今天的这篇博客中，我们来说说 ABP vNext 对接 Ant Design Vue 实现分页查询的问题，希望能让大家在面对类似问题时有所帮助。我不打算写一个系列教程，更多的是从我个人的关注点出发，如果大家有更多想要交流的话题，欢迎大家通过评论或者邮件来留言，谢谢大家！
ABP vNext中的分页查询 OK，当大家接触过 ABP vNext 以后，就会了解到这样一件事情，即，ABP vNext 中默认提供的分页查询接口，在大多数情况下，通常都会是下面这样的风格。这里以角色查询的接口为例，它对应的请求地址是：/api/identity/roles?SkipCount=0&amp;amp;MaxResultCount=10。此时，我们可以注意到，返回的数据结构中含有totalCount和items两个属性。其中，totalCount表示记录的总数目，items表示当前页对应的记录。
{ &amp;#34;totalCount&amp;#34;: 2, &amp;#34;items&amp;#34;: [ { &amp;#34;name&amp;#34;: &amp;#34;Admin&amp;#34;, &amp;#34;isDefault&amp;#34;: false, &amp;#34;isStatic&amp;#34;: true, &amp;#34;isPublic&amp;#34;: true, &amp;#34;concurrencyStamp&amp;#34;: &amp;#34;cb53f2d7-159e-452d-9d9c-021629b500e0&amp;#34;, &amp;#34;id&amp;#34;: &amp;#34;39fb19e8-fb34-dfbd-3c70-181f604fd5ff&amp;#34;, &amp;#34;extraProperties&amp;#34;: {} }, { &amp;#34;name&amp;#34;: &amp;#34;Manager&amp;#34;, &amp;#34;isDefault&amp;#34;: false, &amp;#34;isStatic&amp;#34;: false, &amp;#34;isPublic&amp;#34;: false, &amp;#34;concurrencyStamp&amp;#34;: &amp;#34;145ec550-7fe7-4c80-85e3-f317a168e6b6&amp;#34;, &amp;#34;id&amp;#34;: &amp;#34;39fb6216-2803-20c6-7211-76f8fe38b90e&amp;#34;, &amp;#34;extraProperties&amp;#34;: {} } ] } 事实上，ABP vNext 中自带的分页查询，主要是通过SkipCount和MaxResultCount两个参数来实现。假设MaxResultCount，即分页大小为m，则第n页对应的SkipCount应该为(n-1) * m。如果大家对于LINQ非常熟悉的话，应该可以自然而然地联想到Skip()和Take()两个方法，这是一个非常自然的联想，因为 ABP vNext 就是这样实现分页查询的。这里以博主的“数据字典”分页查询接口为例：</description></item><item><title>浅议 EF Core 分库分表及多租户架构的实现</title><link>https://blog.yuanpei.me/posts/2151871792/</link><pubDate>Sat, 27 Mar 2021 17:47:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2151871792/</guid><description>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是：https://blog.yuanpei.me。最近这段时间，我一直在学习 ABP vNext 框架，在整个学习过程中，我基本就是在“文档”和“源码”间来回横跳。我个人推荐大家，多去阅读一点优秀的代码，因为阅读 ABP vNext 的源代码简直就是一种享受，它可以暂时让你摆脱如泥沼一般的业务代码。言归正传，ABP vNext 是一个支持多租户架构的框架，在了解了其多租户的实现原理以后，从中收获一点微不足道的小技巧。正好前几天，刚刚同一位朋友讨论完分库、分表这类话题。因此，在今天这篇博客中，我想和大家一起探讨下 EF Core 关于分库、分表以及多租户架构的实现。此中曲折，可以说是初窥门径，或许我无法提供给你一个开箱即用的方案，至少它可以带给你一点启发。有读者朋友建议我，不要总是写这种“高深”、“复杂”的话题，适当地迎合读者写点不需要动脑子的东西。对此，我想说，我有我个人技术上的追求，希望大家理解！
分库 首先，我们一起来探讨分库这个话题。从字面含义上了解，分库就是指应用程序拥有多个数据库，而这些数据库则拥有相同的表结构。你可能会问，为什么我们需要分库、分表？答案自然是性能，性能，还是TM的性能。我相信，大家都曾经或多或少地听到过垂直拆分、水平拆分这样的术语，下图展示了如何在数据库这一层级上进行拆分：
数据库的垂直拆分与水平拆分 其实，我们可以从索引存储、B+树高度、QPS 和 连接数 这四个不同的角度来审视这个话题。相关观点认为，当单表数据量达到一定量级(阿里巴巴Java开发手册中为500W)时，由于内存无法存储其索引，此时SQL查询会产生磁盘IO；行记录的大小决定了B+树的每个叶子节点能存储多少记录，所以，行记录的大小会影响B+树的高度；单个MySQL物理机实例写QPS峰值大概为1万，一旦业务量达到某个量级，这个瓶颈会逐步凸显出来；单个MySQL实例最大连接数有限，更多的访问量意味着需要更多的连接数。
在谈论分库、分表的时候，我们忍不住会去想譬如“自动分表”和“路由”这样的问题，这些子库、子表，到底是提前在数据库里分好呢，还是在运行时期间自动去拆分呢，以及我对库/表进行拆分以后，我应该怎么样找到某条数据对应的库/表。我承认，这些问题并不简单，但当我们对问题进行简化以后，分库本质上就是动态地切换数据库，对不对？无非是拆分后的数据库可能会是类似db_0、db_1等等这样的序列。
对 Chinook 进行水平拆分 对于数据库的自动拆分，博主尝试过的一种方案是：首先，通过Add-Migration生成迁移。然后，通过循环修改连接字符串的方式，调用Context.Database.Migrate()方法为一个数据库迁移表结构和种子数据。当然，有些朋友不认同在生产环境使用迁移的做法，认为对数据库的操作权限还是应该交给 DBA 来管理，这当然无可厚非。我表达的一直都是一种思路，我不想一个工作六年的人，对技术的态度永远都停留在“能跑”、“能抄”这种水平。
一旦想清楚这一层，实现起来还是非常简单的。我们在配置中准备多个数据库来模拟分库的场景，实际应用中到底是用范围、Hash 还是 配置，大家结合自己的场景来决定就好。其实，这个思路还可以用来做读写分离，无非是这个库更特殊一点，它是个从库。好了，我们一起来看下面的代码：
// 这里随机连接到某一个数据库 // 实际应该按照某种方式获得数据库库名后缀 var shardings = _options.Value.MultiTenants; var sharding = shardings[new Random().Next(0, shardings.Count)]; _chinookContext.Database.GetDbConnection().ConnectionString = sharding.ConnectionString; Console.WriteLine(&amp;#34;--------分库场景--------&amp;#34;); Console.WriteLine(_chinookContext.Database.GetDbConnection().ConnectionString); Console.WriteLine(_chinookContext.Album.ToQueryString()); Console.WriteLine(_chinookContext.Artist.ToQueryString()); 事实上，如果选择性地忽略 “路由” 和 “自动分表” 这两个特性，我们已经在 EF 层面上局部的实现了 “分库” 功能：
分库场景 分表 好了，聊完分库，我们再来聊聊分表。分表就是指同一个数据库里拥有多张结构(Schema)相同的表。一个典型的例子是，Excel里的多张Sheet，只要它们拥有相同的结构(Schema)，就可以视为同一类型的数据，虽然它们拥有不同的表名。和分库类似，分表的着眼点是避免产生“大表”，从而达到提高查询性能的目的。而对应到 EF(EntityFramework) 的场景中，分表本质上就是在解决 EF 动态适配表名的问题。同样的，下面两张图展示了如何在表这个层级进行拆分：
表的垂直拆分 表的水平拆分 图片援引自：雨点的名字 - 分库分表(1) &amp;mdash; 理论</description></item><item><title>源代码探案系列之 .NET Core 跨域中间件 CORS</title><link>https://blog.yuanpei.me/posts/1276287490/</link><pubDate>Tue, 16 Mar 2021 21:25:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1276287490/</guid><description>本文是 #源代码探案系列# 第三篇，今天这篇博客，我们来一起解读下 ASP.NET Core 中的 CORS 中间件，熟悉这个中间件的的小伙伴们，想必都已经猜出本文的主题：跨域。这确实是一个老生常谈的话题，可我并不认为，大家愿意去深入探究这个问题，因为博主曾经发现，每当工作中遇到跨域问题的时候，更多的是直接重写跨域相关的 HTTP 头。博主曾经写过一篇关于跨域的博客：《聊聊前端跨域的爱恨情仇》，当时是完全以前端的视角来看待跨域。所以，在今天这篇博客里，博主想带领大家从一种新的视角来看待跨域，也许，可以从中发现不一样的东西。
核心流程 关于 ASP.NET Core 中的 CORS，大家都知道的是，可以通过UseCors()方法在整个 HTTP 请求管道中启用跨域中间件，或者是通过AddCors()方法来定义跨域策略，亦或者通过[EnableCors]来显式地指定跨域策略，更多的细节大家可以参考微软的官方文档，而在这里，我想聊一点大家可能不知道的东西，譬如：服务器端如何处理来自浏览器端的跨域请求？而这一切在 ASP.NET Core 中又如何实现？带着这些问题来解读 CORS 中间件的源代码，我们能更快的找到我们想得到的答案。一图胜千言，请允许博主使用这张流程图来“开宗明义”，我们这就开始今天的“探案”：
一张图览尽 CORS 中间件 核心部件 对于整个 CORS 中间件而言，核心部件主要有：CorsPolicy、CorsService 以及 CorsMiddleware。
CorsPolicy 整个 CORS 中间件中，首当其冲的是ICorsPolicy。这个接口的作用是定义跨域的策略，我们知道CORS中引入了Access-Control系列的 HTTP 头，所以，CorsPolicy 本质上是在定义允许哪些 HTTP 头、HTTP 方法、源(Origin) 可以访问受限的资源，以及当跨域请求是一个复杂请求的时候，预检请求的超时时间、是否支持凭据等等：
public class CorsPolicy { public bool AllowAnyHeader { get; } public bool AllowAnyMethod { get; } public bool AllowAnyOrigin { get; } public Func&amp;lt;string, bool&amp;gt; IsOriginAllowed { get; private set; } public IList&amp;lt;string&amp;gt; ExposedHeaders { get; } = new List&amp;lt;string&amp;gt;(); public IList&amp;lt;string&amp;gt; Headers { get; } = new List&amp;lt;string&amp;gt;(); public IList&amp;lt;string&amp;gt; Methods { get; } = new List&amp;lt;string&amp;gt;(); public IList&amp;lt;string&amp;gt; Origins { get; } = new List&amp;lt;string&amp;gt;(); public TimeSpan?</description></item><item><title>源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit</title><link>https://blog.yuanpei.me/posts/2396015802/</link><pubDate>Wed, 10 Mar 2021 21:52:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2396015802/</guid><description>在上一篇文章中，博主带领大家一起深入了解 ConcurrencyLimiter 这个中间件，正当我得意洋洋地向 Catcher Wong 大佬吹嘘这一点小收获时，大佬一脸嫌弃地说，一个单机版的方案有什么好得意的啊。大佬言下之意，显然是指，这个中间件在分布式环境中毫无用武之地。其实，你只需要稍微想一下，就能想明白这个问题。毕竟，它只是通过SeamphoreSlim控制线程数量而已，一旦放到分布式环境中，这个并发控制就被大大地削弱。所以，在今天这篇文章中，博主会带领大家一起“探案” ASP.NET Core 中的限流中间件 AspNetCoreRateLimite，希望大家可以从中感悟到不一样的东西。对我而言，这可能是人到中年的焦虑感所催生出来的一种源动力，同时亦是为了不让那些订阅专栏的同学失望。
关于“限流”这个话题，我个人以为，它可以引申出非常多的东西，譬如“熔断”和“限流”，其实可以看作是同一类问题的“一体两面”。最早接触熔断，是源于 Spring Cloud 中的 Hystrix，它其实是指当服务不可用的时候，客户端应该采取什么样的措施去应对，实际使用中我们可能会考虑重试、超时、降级等策略。相应地，当服务端在面对来自客户端的异常流量时，就产生了“限流”这个概念，“限流”可以是线程隔离**(线程数 + 队列大小限制)，可以是信号量隔离(设置最大并发请求数目)，可以是限制QPS。这里，我们讨论的主要是第三种，而实现限流的常见算法主要有计数器算法、漏桶算法和令牌桶算法。这里，AspNetCoreRateLimit 这个中间件，则主要使用了计数器算法，并配合 IMemoryCache 和 IDistributedCache 分别实现了基于内存和基于分布式缓存的持久化逻辑。
源代码解读 首先，使用者通过配置定义了一个或者多个规则，这些规则决定了每个客户端在访问特定终结点时，一段时间内可以访问的最大次数。 RateLimitMiddleware 通过注入的IRateLimitProcessor 来匹配规则，然后依次判断每个规则是否达到了限流条件。一旦达到限流条件，中间件会改变 HTTP 响应的状态码、响应头、返回值，告知使用者已达到最大调用次数。而针对每一种 IRateLimitProcessor ，主要通过ProcessRequestAsync() 方法来实现计数，如果上一次的请求对应的时间戳 + 规则中时间间隔 &amp;gt;= 当前时间，则说明请求没有过期，此时，就需要给这个计数增加1。好了，现在我们来针对 AspNetCoreRateLimit 中的核心部件逐个进行解读。
RateLimitProcessor RateLimitProcessor，是一个抽象类，实现了IRateLimitProcessor接口，公开的方法有 3 个：ProcessRequestAsync()、IsWhitelisted() 和 GetRateLimitHeaders()。在此基础上，派生出ClientRateLimitProcessor和IpRateLimitProcessor两个子类。两者最大的不同在于，其所依赖的Store不同，前者为IClientPolicyStore，后者IIpPolicyStore，它们都实现了同一个接口IRateLimitStore：
public interface IRateLimitStore&amp;lt;T&amp;gt; { Task&amp;lt;bool&amp;gt; ExistsAsync(string id, CancellationToken cancellationToken = default); Task&amp;lt;T&amp;gt; GetAsync(string id, CancellationToken cancellationToken = default); Task RemoveAsync(string id, CancellationToken cancellationToken = default); Task SetAsync(string id, T entry, TimeSpan?</description></item><item><title>源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter</title><link>https://blog.yuanpei.me/posts/18417412/</link><pubDate>Thu, 04 Mar 2021 20:13:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/18417412/</guid><description>打算开一个新的专栏——源代码探案系列，目的是通过源代码来探索更广阔的技术世界。因为我越来越意识到，我可能缺乏一个结构化的知识体系，虽然处在一个碎片化的时代，从外界接收了大量的信息，可这些碎片化的信息，到底能不能转化为自身可用的知识，其实是需要去认真思考一番。尤其是当我注意到，许多人工作多年，在经历过从“生手”到“熟练工”这种蜕变以后，居然还是会害怕原理性内容的考察。我承认，程序员这个职业更像是一个“手艺人”，可我更想说一句古人的话——君子不器。什么是器呢？“形而上者谓之道，形而下者谓之器”，用一句更直白的话来说，就是“不能知其然而不知其所以然”，这是我一个非CS科班出身的程序员，想去写这样一个专栏的初衷，因为在我看来，“器”是永远学不完的，而“道”虽然听起来虚无缥缈，实则“朝闻道，夕死可矣”。
作为这个专栏的第一篇博客，我打算从 ASP.NET Core 中的 ConcurrencyLimiter 这个中间件开始。并发是一个爱恨交织的话题，我们喜欢高并发，因为这是程序员跻身高手行列的好机会；我们厌恶并发，因为它引入了多线程、锁、信号量这些复杂的东西。相信大家都曾被并发困扰过，古人云：他山之石，可以攻玉，还有什么比阅读源代码更朴实无华的“学习”呢？你找大牛，大牛可能忙着开会、做PPT；你找同事，同事里可能十个有八个都不知道啊。这个中间件的核心是 IQueuePolicy ，其位于以下位置，它定义了两个核心的方法：TryEnterAsync() 和 OnExit()：
public interface IQueuePolicy { ValueTask&amp;lt;bool&amp;gt; TryEnterAsync(); void OnExit(); } 在其默认实现QueuePolicy中，TryEnterAsync()方法，决定着一个请求是会被拒绝还是接受。具体是怎么做呢？它定义了一个最大的并发请求数目，如果实际数超过了最大的并发请求数目，那么请求将会被拒绝。反之，请求将被接受。再仔细看，我们就会发现，它内部使用了SeamphoreSlim和Interlocked，所以，聪明的小伙伴们应该立马会联想到，这两种锁各自的作用是什么。
其中，Seamphore 是一个 Windows 内核中的一个同步信号量，适用于在多个有限的线程资源中共享内存资源，它就像一个栅栏，本身具有一定的容量，当线程数量达到这个容量后，新的线程就无法再通过，直到某个线程执行完成。SeamphoreSlim是Seamphore优化后的版本，在性能上表现更好一点，更推荐大家使用SeamphoreSlim。
而 Interlocked 的则是我们熟悉的原子操作，它可以在多个线程中，对共享的内存资源进行原子加或者原子减操作。在这里，Interlocked主要用来控制并发请求数的加和减。如果当前的并发请求数小于最大的并发请求数，表示还可以允许新的请求进来，此时，TryEnterAsync()方法会返回true。如果此时的并发请求数大于最大的并发请求数，则需要对当前请求数进行减操作，此时，TryEnterAsync()方法会返回false。
一旦搞清楚这一点，结合中间件的代码，我们可以非常容易地想明白,这个并发控制的实现思路。下面是QueuePolicy中TryEnterAsync()和OnExit()两个方法的实现，分别代表了“加锁”和“解锁”两个不同的阶段。某种程度上，Seamphore更像一个水闸，每次可以通过的“流量”是固定的，超出的部分会被直接“拒绝”：
//“加锁” public ValueTask&amp;lt;bool&amp;gt; TryEnterAsync() { // a return value of &amp;#39;false&amp;#39; indicates that the request is rejected // a return value of &amp;#39;true&amp;#39; indicates that the request may proceed // _serverSemaphore.Release is *not* called in this method, // it is called externally when requests leave the server int totalRequests = Interlocked.</description></item><item><title>通过 EmbededFileProvider 实现 Blazor 的静态文件访问</title><link>https://blog.yuanpei.me/posts/3789745079/</link><pubDate>Tue, 23 Feb 2021 05:37:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3789745079/</guid><description>重构我的 独立博客 ，是博主今年的计划之一，这个基于 Hexo 的静态博客，最早搭建于2014年，可以说是比女朋友更亲密的存在，陪伴着博主走过了毕业、求职以及此刻的而立之年。其间虽然尝试过像 Jekyll 和 Hugo 这样的静态博客生成器，可是考虑到模板、插件等周边生态，这个想法一直被搁置下来。直到最近，突然涌现出通过 Blazor 重写博客的想法，尤其是它对于 WebAssembly 的支持，而类似 Vue 和 React的组件化开发模式，在开发体验上有着同样不错的表现。所以，今天这篇博客就来聊聊在重写博客过程中的一点收获，即如何让 Blazor 访问本地的静态文件。
从内嵌资源说起 首先，我们要引入一个概念，即：内嵌资源。我们平时接触的更多的是本地文件系统，或者是 FTP 、对象存储这类运行在远程服务器上的文件系统，这些都是非内嵌资源，所以，内嵌资源主要是指那些没有目录层级的文件资源，因为它会在编译的时候“嵌入”到动态链接库(DLL)中。一个典型的例子是Swagger，它在.NET Core平台下的实现是Swashbuckle.AspNetCore，它允许使用自定义的HTML页面。这里可以注意到，它使用到了GetManifestResourceStream()方法：
app.UseSwaggerUI(c =&amp;gt; { // requires file to be added as an embedded resource c.IndexStream = () =&amp;gt; GetType().Assembly .GetManifestResourceStream(&amp;#34;CustomUIIndex.Swagger.index.html&amp;#34;); }); 其实，这里使用的就是一个内嵌资源。关于内嵌资源，我们有两种方式来定义它：
在 Visual Studio 中选中指定文件，在其属性窗口中选择生成操作为嵌入的资源： 如何定义一个文件资源为内嵌资源 在项目文件(.csproj)中修改对应ItemGroup节点，参考示例如下： &amp;lt;Project Sdk=&amp;#34;Microsoft.NET.Sdk.Web&amp;#34;&amp;gt; &amp;lt;!-- ... --&amp;gt; &amp;lt;ItemGroup&amp;gt; &amp;lt;EmbeddedResource Include=&amp;#34;_config.yml&amp;#34;&amp;gt; &amp;lt;CopyToOutputDirectory&amp;gt;Always&amp;lt;/CopyToOutputDirectory&amp;gt; &amp;lt;/EmbeddedResource&amp;gt; &amp;lt;/ItemGroup&amp;gt; &amp;lt;!-- ... --&amp;gt; &amp;lt;/Project&amp;gt; 这样，我们就完成了内嵌资源的定义。而定义内嵌资源，本质上还是为了在运行时期间去读取和使用，那么，自然而然地，我们不禁要问，该怎么读取这些内嵌资源呢？在Assembly类中，微软为我们提供了下列接口来处理内嵌资源：
public virtual ManifestResourceInfo GetManifestResourceInfo(string resourceName); public virtual string[] GetManifestResourceNames(); public virtual Stream GetManifestResourceStream(Type type, string name); public virtual Stream GetManifestResourceStream(string name); 其中，GetManifestResourceNames()方法用来返回所有内嵌资源的名称，GetManifestResourceInfo()方法用来返回指定内嵌资源的描述信息，GetManifestResourceStream()方法用来返回指定内嵌资源的文件流。为了方便大家理解，这里我们准备了一个简单的示例：</description></item><item><title>低代码，想说爱你不容易</title><link>https://blog.yuanpei.me/posts/2637069146/</link><pubDate>Mon, 15 Feb 2021 12:37:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2637069146/</guid><description>一直想写篇文章，聊一聊“低代码”这个话题。一方面，“低代码”这个概念确实非常火，其热度丝毫不亚于曾经的“中台”。有人说，2021 年是属于“云原生”的时代，看起来我们每一年都在被技术的“娱乐圈”抛弃，明明连 Kubernetes 都还没有入门呢？人们已然在欢呼雀跃般地声称要抛弃 Docker 。这个世界有时就是如此地魔幻，明明我们生活在一个拥有大量基础设施的时代，我们不必再像前辈们“刀耕火种”一般地去开发软件，可我们的生存空间为什么就越来越狭窄了呢？拼多多事件过去没有多久，腾讯的阳光普照奖再次让“打工魂”觉醒，也许果真像大鱼海棠里设定的一样，人的记忆只有 7 秒。而另一方面，我想结合我最近开发“工作流”的感受，来吐槽下这个看起来美好的“低代码”。也许，对企业而言，引入“低代码”的确能减少研发成本，可博主并不认为，它会降低业务本身的复杂性，如果所有声称“低代码”或者“无代码”的项目，最终依然需要研发人员来作为收场。对此，我想说，对不起，这不是我想要的“低代码”。
低代码发展现状 或许，一个人成熟的标志就是，在面对一个未知的事物的时候，决不会不由分说地一通吐槽，就像一个人在职场上，你不能永远都只是学会抱怨，相对于抱怨，人们更希望听到的是解决方案。所以，一个人的成长，本质上就是不断学会为自己、为别人找解决方案的过程，前者是为了认识自我，而后者是为了交换资源。所以，在听我吐槽“低代码”前，不妨先一起来看看低代码的发展现状。
低代码产品发展现状 国外趋势 有人认为，“低代码”的兴起源于钉钉的低代码应用 易搭 的落地。诚然，巨头企业的每一个动向都引领着整个行业的风潮，可低代码这个概念最早要追溯到 1980 年。彼时，IBM 的快速应用程序开发工具(RAD)被冠以新的名字——低代码，这是低代码这个概念首次面向大众，此后的 40 年里，国外诞生了诸如 Outsystem 、Mendix 、 Zoho Creator 等等的产品，整体发展相对缓慢。直到 2015 年以后，AWS、Google、Microsoft 和 Oracle 等巨头开始入局低代码领域。2018 年，西门子更是宣布以 6 亿欧元收购低代码应用开发领域的领导者 Mendix 、快速应用开发的低代码平台 Outsystem 获得 3.6 亿美金的投资，低代码平台市场开始火爆起来，我们所熟悉的 Power Platform，其实就是微软的低代码开发平台，低代码领域通常都需要大量的积累和研发，需要有 10 到 20 年左右的技术沉淀。
国内风云 国内的低代码领域，相比国外发展起步较晚，可依然涌现出像牛刀、APICloud、iVX、搭搭云、氚云、简道云、云表、宜搭云等等产品。从整体上而言，这类这类产品基本上都提供了可视化搭建环境，都声称无需编码即可完成业务系统的搭建。其实，从一名程序员的初心出发，我们所做的一切努力都是为了以后不写代码。经常有人问，怎么样可以做到零缺陷、零 Bug ，其实不写代码就好啦！我们并不担心低代码让我们失业，相反地，如果低代码可以消化掉 30% 的垃圾项目，那么，我们将会有更多的时间去做些有意义的事情，而不是在一个“劣币驱逐良币”的市场里，靠着 996 来争个你死我活。而从低代码的商业价值角度来看，Salesforce、Appian、Joget 这三家公司均已上市，Mendix 和 Outsystem 更是估值 10 亿美元以上的独角兽公司，这正是巨头们入局低代码的原因所在。
低代码领域，目前关注的重点主要集中在：表单生成和处理、工作流生成和管理、办公协作、人力资源、客户关系、ERP 等企业应用上，就如同 SAP 、金蝶、 SCM 等企业软件一样，每一个软件都曾声称能帮助企业解决某一类问题，低代码领域同样遵循“二八原则”，即 80% 的场景，通过定义的方法论、方式、工具集能够实现；而剩下的 20% 的场景或许实现不了，需要使用者通过扩展的方式来自行解决。譬如，针对大多数企业都存在的 CRUD 的需求，通过在线的 Excel 表格来实现基于表的业务驱动。例如 SeaTable 就是这类主打协同工作的产品；针对大多数企业都存在的审批类的需求，则可以通过可视化的工作流设计系统来完成。例如 葡萄城 的 SpreadJS 和 活字格 ，同样可以视为低代码平台，甚至早期的 .</description></item><item><title>记一次失败的 ThoughtWorks 面试经历</title><link>https://blog.yuanpei.me/posts/2837181325/</link><pubDate>Tue, 09 Feb 2021 20:37:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2837181325/</guid><description>年前朋友问我，要不要试试 ThoughtWorks 澳洲线的岗位。对于这家号称“世界上面试最难”的公司，多少还是有一点畏惧，直到朋友安慰我说，它们这次有中级的岗位，还是可以试一试的，梦想还是要有的，万一实现了呢？自此，我凑齐了西安. NET圈子里的四大“天花板”公司的面试：葡萄城、活跃网络、奥博杰天、ThoughtWorks ，而对于我来说，亦有幸见识到世界上最难的面试，虽然后来事实证明，这个世界上没有太多的逆袭，可我还是想分享一下我的这次面试经历，因为它让我知道，在过去的两年里，我在哪些方面取得进步，在哪些方面存在不足。当我写下这篇博客的时候，我即将在今年夏天迎来我的29岁，果然我还是希望自己能再努力一点，因为不想让平行世界里的某个人失望。
面试流程 关于ThoughtWorks 的社招流程，大体上由HomeWork、Pair Programming 和 Face-to-face Interviews 3个部分组成，其中，HomeWork，即家庭作业，原则上给3天时间来完成，不过据说可以向 HR 申请更多的时间来完成。Pair Programming 和 Face-to-face Interviews 通常是安排到同一天来进行的，前者时间为1.5小时，即传说中的结对编程，面试时会有一左一右两名面试官看着你现场写代码。后者时间为1小时，即传说中的技术文化面试，考察技术的深度、广度以及对 Thought Works 敏捷文化的认同感。
HomeWork 2月18日，下班以后接到HR小姐姐的电话，在明确了我投简历的意向以后，我收到了HR小姐姐的邮件，基本上就是一个家庭作业，三选一提交，需要在三天内完成。我选择了Conference Track Management 这道题目，因为白天要上班，所以，我为此而连续肝了三个晚上。
坦白说，不同的阶段对这道题目的理解是不同的，在做家庭作业的阶段，你以为这道题考察的是职责分离和设计模式；而等到结对编程的阶段，你终于意识到，这其实是个背包问题。当然，这并不是说我会错了意，考虑到面试官有上帝视角，他们更容易看清楚问题的全貌。或许，面试官想最想看到的，恰恰就是你从冰山一角到目窥全牛这一瞬间的反应。
当我接到HR小姐姐的通知，这份作业Review通过时，我内心是非常激动的，因为这意味着我获得了去ThoughtWorks面试的“入场券”。可当我事后再以上帝视角去看待这个题目，我内心又变得非常难过，因为无论怎么看这份作业，都会觉得它设计得并不好，尤其是当它引入弹性时间这个因素以后，我一直深陷于如何从Part 1 到 Part 2，是不是按 Part 2 重新设计会更好一点？此时此刻，终于能理解面试官反馈的，关于扩展性方面的问题。
作业反馈01 作业反馈02 作业反馈03 关于这部分，我个人建议多多关注：
编程风格：编码规范、项目结构、代码坏味道等。 语言特性：澳洲线岗位需要熟悉 .NET Core，所以，我使用 .NET Core 完成整个项目的编写。 设计模式：选择合适的设计模式，遵循 SOLID 原则。 TDD：一定要有单元测试代码，这一点TW最为看中。如果写的好，一定是加分项。建议遵循AAA原则来编写用例。 程序满足要求：程序一定满足题目要求，可执行，运行结果满足题意，这是最基本的要求。 Pair Programming 提交作业后，等了一周多的时间，1月29日，HR 小姐姐终于联系我了，正如我上文所述，当时听到这个消息非常激动，因为终于有机会去 ThoughtWorks 这家世界上面试最难的公司去看看，ThoughtWorks 西安办公室位于环普产业园，这个地方相信大家都非常熟悉啦！当时算上周末，我给了自己 5 天时间去准备面试，因为我觉得面对 ThoughtWorks 的面试还是要重视一点，虽然后来好多问题都没有被问到。
结对编程是基本上就是，两个面试官一左一右地坐在你旁边，采用聊天和探讨的方式一起写代码，刚开始本来是用电视投屏“直播”的方式，后来因为 HDMI 接口接触不良的缘故，两位面试官干脆就直接看我电脑屏幕啦！在这个环节，个人感觉解释编码思路花时间太多，重构完有一个用例没有通过。最重要的是，家庭作业阶段的设计不利于现场新需求的开展，所以，这些因素综合起来，导致我结对变成这个部分表现得不好，希望大家引以为戒啊。
整个结对编程时长为一个半小时(1.5h)，在这段时间，你需要讲解编码思路、完成代码重构和完成现场作业，时间上还是非常紧凑的，回想起那天下午的两个半小时，有种像参加高考的感觉：你以为时间会很长，结果发现时间完全不够用。看起来轻松的氛围下，其实在不经意间考察你的沟通能力、工程能力和学习能力，ThoughtWorks 的面试，往往就是这样的朴实无华且“有趣”……
对于这部分，我个人建议多多关注：</description></item><item><title>从 C# 1.0 到 C# 9.0，历代 C# 语言特性一览</title><link>https://blog.yuanpei.me/posts/3918433482/</link><pubDate>Mon, 01 Feb 2021 22:36:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3918433482/</guid><description>C# 版本历史记录 从 C# 1.0 到 C# 9.0，历代 C# 语言特性一览 说明：因为Markdown下维护这样复杂的表格有一点麻烦，故，这里以图片形式展示出来，如后续内容有更新，请点击 这里 访问原始笔记链接。为知笔记 的表格渲染在移动端表现不佳，为了获得更好的阅读体验，请在电脑端访问查看。
C# 版本特性说明 现在是 2021 年，相信 C# 7.0 以前的版本大家都应该没有什么问题，因为像博主这样的 90 后“中年”男人，接触的都是这个版本的 C#。所以，在这里我们主要讲解大家C# 7.0、8.0 以及 9.0 的语法特性。考虑到文章篇幅有限，这里选取的都是博主个人比较喜欢的语法特性，如果这里没有你喜欢的特性，请参考文章末尾的参考链接。如果这里的特性你都不喜欢，请你马上关掉这个网页，愿这个世界：Love &amp;amp; Peace。可能你会感觉到我说话变得小心翼翼起来，因为这个世界上有种叫做“杠精”的生物，当它从我的只言片语里读出那些挫败感的时候，终于有了嘲笑我们这批步入30岁行列的90后的底气，没错，我在最近的博客评论中被读者“嘲讽”了，让暴风雨来得更猛烈一些吧！
C# 7.0 在 C# 7.0 中，我个人比较喜欢的特性主要有以下几个：元组和弃元、更多的 expression-bodied 成员、out 变量、异步 Main 方法、模式匹配 和 引发表达式。
元组和弃元 这个概念乍听起来可能会有一点陌生，其实，按我的理解，这就是增强的元组语法，终于可以摆脱Item1、Item2&amp;hellip;&amp;hellip;啦：
//示例1 (string Alpha, string Beta) namedLetters = (&amp;#34;a&amp;#34;, &amp;#34;b&amp;#34;); Console.WriteLine($&amp;#34;{namedLetters.Alpha}, {namedLetters.Beta}&amp;#34;); //示例2 var alphabetStart = (Alpha: &amp;#34;a&amp;#34;, Beta: &amp;#34;b&amp;#34;); Console.WriteLine($&amp;#34;{alphabetStart.Alpha}, {alphabetStart.Beta}&amp;#34;); //示例3 int count = 5; string label = &amp;#34;Colors used in the map&amp;#34;; var pair = (count, label); Console.</description></item><item><title>通过 Python 分析 2020 年全年微博热搜数据</title><link>https://blog.yuanpei.me/posts/2758545080/</link><pubDate>Sun, 24 Jan 2021 22:36:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2758545080/</guid><description>几天前， Catcher Wong 大佬告诉我，他终于写完了 2020 年的年终总结。在看完大佬的年终总结以后，我有一种“前浪被后浪拍死在沙滩上”的感觉，正如当学生时都看“别人家的孩子”，工作以后看的都是“别人的年终总结”。我们的生活，其实就是由“别人”和“我们”交织在一起，而更多的时候，是成为“大多数”的“我们”，去关注成为“少数”的“别人”。我想说的是，世间万物互为装饰，就像卞之琳在《断章》里写道，“明月装饰了你的窗子，你装饰了别人的梦”。即便一个人在历史长河中，尤如一叶飘泊不定的孤舟在波涛中摇荡，可每一朵浪花都曾以自己的方式美丽过，所以，看“别人”的生活，联想“我们”的生活，这便是我同 2020 告别的一种方式，为此，博主决定抓取 2020 年全年 366 天的微博热搜，通过可视化的方式来串联起 2020 年的回忆。
热搜抓取 首先，我们来考虑微博热搜的数据来源。 微博 官方提供了一个热搜排行榜的页面：https://s.weibo.com/top/summary，可惜这个网站只支持查看当天的热搜，显然这无法满足我们的需求。在搜索引擎的帮助下，找到了两个网站，它们分别是：微博时光机 和 热搜神器。经过一番权衡，决定选择页面结构更简单一点的 微博时光机 。
通过抓包，可以快速获得两个关键的接口，它们分别是 获取 timeId 接口 和 获取历史热搜接口。
Firefox抓包示意图 简单来说，我们指定一个日期，第一个接口会返回timeId。接下来，通过这个timeId调用第二个接口就可以获得热搜数据。仔细观察的话，第一个接口传递的data参数像是一个BASE64加密后的结果，尝试解密后发现我的猜想是对的，加密前的内容如下：
[&amp;#34;getclosesttime&amp;#34;,[&amp;#34;2021-01-20T23:08:02&amp;#34;]] 这意味着我们只需要改变这里的日期就可以啦，因此，我们的思路无非就是从 2020 年 1 月 1 日开始，依次请求热搜接口获取数据，直到 2020 年 12 月 31 日。这里想顺便吐槽下这个网站的接口设计，居然清一色地全部用数组来返回结果，难道是为了省掉这几个字段来节省流量吗？
接口返回值说明-1 接口返回值说明-2 吐槽归吐槽，这里我们可以非常容易地写出对应的代码，由于日期和timeId的对应关系是固定的，为了减少后续的请求数量，我们使用MongoDB来对数据进行持久化。同样地，抓取热搜采用了类似的方式，因为历史热搜同样是确定的数据，这里只给出关键的代码，并不代表你可以无脑地复制、粘贴：
# 获取指定日期对应的timeId def get_timeId(date, cookie): cacheKey = date.strftime(&amp;#39;%Y-%m-%d&amp;#39;) records = list(store.find(TABLE_TIME_ID, {&amp;#39;date&amp;#39;: cacheKey})) if len(records) &amp;gt; 0: return records[0][&amp;#39;timeId&amp;#39;] else: data = &amp;#34;[\&amp;#34;getclosesttime\&amp;#34;,[\&amp;#34;{d}\&amp;#34;]]&amp;#34;.format(d=cacheKey) data = base64.</description></item><item><title>基于 Python 和 Selenium 实现 CSDN 一键三连自动化</title><link>https://blog.yuanpei.me/posts/3148958651/</link><pubDate>Tue, 19 Jan 2021 22:35:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3148958651/</guid><description>最近一段时间，博主感觉到了某种危机感，或者说是每一个不再年轻的人都会面对的问题，即，怎么面对来自更年轻的“后浪”们的压力，自打国内 IT 行业有了 35 岁这个不成文的“门槛”以后，年轻的“后浪”们仿佛有了更多将“前浪”们拍死在岸上的勇气，我辈忍不住要叹一声后生可畏啊！我认识的 Catcher Wong 正是这样一位大佬，此君虽然比我小三岁，可在技术的广/深度以及经验的丰富程度上，足以令我这个”老人”汗颜，单单 EasyCaching 这一项，就令人望尘莫及啦！我看着他的时候，一如当年 Wesley 大哥看着我的时候，可能这就是某种轮回，姑且执浊酒一杯，致我们终将老去的青春。
不正经的 Kimol 君 关注Kimol 君，最早源于他在我博客里留言，作为礼尚往来，我回访了他的博客，然后发现此人人如其名，非常的”不正经”，他的博客访问量出奇地高，在 CSDN 里写博客多年，深知现在不比从前有运营梦鸽和大白两位小姐姐帮忙推荐到首页，普通的内容很少有机会拥有这样的曝光机会，而像 郭霖 这种从 10 年前后开始写移动开发系列博客的“大神”或者是以图形学为主要写作方向的 诗人“浅墨” ，在通篇都是干货的情况下，长期保持着不错的人气。
这萌萌哒求赞的表情我是做不来的 起初，我以为此君的流量来自于标题党，譬如《学会这招，小姐姐看你的眼神将不一样》 和 《震惊！小伙竟然用 Python 找出了马大师视频中的名场面》这几篇，非常像 UC 编辑部和微信公众号的风格。我是一个擅长学习的人，主动去借鉴了他博客中的优点，比如尝试使用轻松、幽默的文风，在文章开头放入目录，适当“蹭”热点等等，我甚至专门致敬了一篇博客： 《厉害了！打工人用 Python 分析西安市职位信息》。而整个 1 月份，我就只有一篇博客流量高一点，就这还不是特别正经的”技术”博客，而此君的流量则是一个又一个的 1w+ ，可我实在想不通，一个不到 100 行的 Python 脚本，真就值得花那么多的流量，真就值得上百条的评论吗？这里放张图大家感受一下：
不知道该说什么好 仔细研究了他博客里评论的风格，发现有大量类似“夸夸群”风格的评论，就是那种读起来确实像对方读过了你的文章，可实际一想就觉得这是那种“放之四海而皆准”的话。我最近知道了一位大佬的博客，我惊奇地发现，此君居然在上面留过言，我顺着大佬的博客继续找，发现一个非常有意思的事情，此君曾经给我留言过的内容，居然出现在了别人的博客底下，而从这篇博客的评论里继续找，你会发现好像有一个团队专门在做这种事情，互相点赞、互相评论，甚至这些留言都是来自一篇博客都没有的”新人”，至此，基本可以断定，此君“不讲武德”，用作弊的方式在刷流量！当然，他自己都承认了：
作弊实锤 年轻人不讲”武德” OK，既然现在的年轻人都把心思用到这种事情上，作为一个老年人，必须要让他知道什么叫“耗子尾汁”，我们技术做一点正经事儿不行吗？其实，博客园的博客质量相比 CSDN 是要高出许多的，而正因为如此，CSDN 在全力转在线教育/课程以后，博客这个板块就再无往日的“生气”，如果每个人都像他一样，天天跑别人底下刷评论，发一点不痛不痒的话，甚至是推广某个小圈子里的 QQ 群，那真正优质的内容又如何能被大家看到呢？博主曾经加过这样的 QQ 群，你以为是交流技术的群吗？其实是为了推广某个 Python 课程，博主本想交流一下“半泽直树”，然后就被群管理员给删除了！此君大概是抓取 Python 板块排名靠前的博客，通过程序来刷存在感。
对此，我想说，这玩意儿用 Selenium + Python 简直和闹着玩一样，毕竟在了解网页结构以后，直接上 jQuery 操作 DOM 即可，甚至连抓包都不需要，不信你看：
import requests from bs4 import BeautifulSoup import fake_useragent import os, json, time, random from selenium import webdriver from selenium.</description></item><item><title>使用多线程为你的 Python 爬虫提速的 N 种姿势，你会几种？</title><link>https://blog.yuanpei.me/posts/3247093203/</link><pubDate>Thu, 14 Jan 2021 20:35:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3247093203/</guid><description>最近博主在优化一个爬虫程序，它是博主在 2017 年左右刚接触 Python 时写下的一个程序。时过境迁，当 Python 2.X 终于寿终正寝成为过去，当博主终于一只脚迈进 30 岁的大门，一切都来得猝不及防，像一阵龙卷风裹挟着回忆呼啸而去。和大多数学习 Python 的人一样，博主学习 Python 是从写爬虫开始的，而这个爬虫程序刚好是那种抓取“宅男女神”的程序，下载图片无疑是整个流程里最关键的环节，所以，整个优化的核心，无外乎提升程序的稳定性、提高抓取速度。所以，接下来，我会带大家走近 Python 中的多线程编程，涉及到的概念主要有线程(池)、进程(池)、异步I/O、协程、GIL等，而理解这些概念，对我们而言是非常重要的，因为它将会告诉你选择什么方案更好一点。想让你的爬虫更高效、更快吗？在这里就能找到你的答案。
楔子 现在，假设我们有一组图片的地址(URL)，我们希望通过requests来实现图片的下载，为此我们定义了Spider类。在这个类中，我们提供了getImage()方法来完成下载这个动作。我们可以非常容易地写出一个“单线程”的版本，但这显然这不是我们今天这篇博客的目的。此时，我们来考虑一个问题，怎么样实现一个“多线程”的版本？
class Spider: def __init__(self, urls): self.session = requests.session() self.session.headers[&amp;#39;User-Agent&amp;#39;] = fake_useragent.UserAgent().random self.session.headers[&amp;#34;Referer&amp;#34;] = &amp;#34;https://www.nvshens.org&amp;#34; self.urls = urls # 下载图片 def getImage(self, url, fileName, retries=5): try: print(f&amp;#39;{threading.currentThread().name} -&amp;gt; {url}&amp;#39;) response = self.session.get(url, allow_redirects=False, timeout=10, proxies=None ) response.raise_for_status() data = response.content imgFile = open(fileName, &amp;#39;wb&amp;#39;) imgFile.write(data) imgFile.close() return True except : while retries &amp;gt; 0: retries -= 1 if self.</description></item><item><title>实现网页长截图的常见思路总结</title><link>https://blog.yuanpei.me/posts/3406626380/</link><pubDate>Sat, 09 Jan 2021 20:37:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3406626380/</guid><description>作为一个经常写博客的人，我有时会在微博上分享博客内容，可不知道从什么时候开始，国内互联网越来越丧失信仰，所有的厂商都在试图打造一个**“只进不出”的信息孤岛，进而达到增强“用户黏度”的目的。以微博为例，微博中的外链永远都会被转化为短地址，并且无法通过微博内置的浏览器进行跳转。即使你通过手动复制链接的方式打开链接，你依然需要至少两个步骤方能见到“庐山真面目”。借鉴/抄袭这一陋习的还有简书，花时间做了一个第三方链接跳转提示页面，唯独不愿意在上面加一个 a 标签，你还是要手动复制黏贴。坦白说，我觉得国内互联网正在丧失着信仰，看起来电商、物流、外卖、打车、支付……此起彼伏逐渐渗透到我们生活的方方面面，成为名副其实的“互联网+”，可在信息泛滥的今天，我们越来越难找到真正有价值的信息……既然外链注定要被屏蔽掉，那我就勉为其难地顺应潮流发“长截图”咯，所以，接下来我会为大家分享实现网页“长截图”**的常见思路，希望对有类似烦恼或者需求的小伙伴们有所帮助。
通过浏览器实现 要实现网页长截图，显然是和网页打交道，而和网页打交道最多的是谁呢？自然是我们每天都要用的浏览器啦！值得庆幸的是，不管是 Chrome 还是 Firefox ，我们都可以通过它们来是实现这个想法。
Chrome 对于 Chrome 来说，我们只需要“F12”打开开发者工具，并在其中找到“控制台”选项卡，在平时输入 JavaScript 脚本的地方(即 Console 选项卡)输入Ctrl + Shift + P命令，然后你会得到一个类似 VSCode 命令行体验的输入窗口，接下来，输入：Capture full size screenshot并回车。此时，我们就可以得到完整的页面截图。而如果你希望截取网页中的一部分，则可以在选中指定 DOM 元素后采用相同的方式输入命令：Capture node screenshot。此外，更常用的截取浏览器可见范围内的内容，可以使用：Capture screenshot。可能相对于一般可以进行拖拽截图的工具而言，这个方案显得有点笨拙且简陋，可它真的可以完美地实现我们的想法，而且不需要安装任何扩展或者插件。
使用 Chrome 的截图功能 Firefox 对于 Firefox 而言，它本身自带截图功能，并且支持拖拽截图，对于我们这些需要长截图的人而言，唯一需要做的就是点击几下数据，确实要比敲命令行要简单一点、友好一点，我个人更喜欢用 Firefox 一点，因为 Chrome 正在从屠龙少年变成恶龙，为了让这个世界上不是只有 Chrome 一种浏览器内核，我决定支持一下 Firefox ，2020 年因为疫情的原因， Mozila 裁员 25%约 250 人，这家几乎靠着理想主义在维护 Gecko 内核的公司，之后可能再无法和 Google 的 Chrome 抗衡，而这个世界只有一种浏览器的时代我们都曾经经历过，它的名字叫做 IE6 ，不禁令人感慨，简直是开放 Web 的罗曼蒂克消亡史。
使用 Firefox 的截图功能 通过 Selenium 实现 在我的认知中，有浏览器的地方就有爬虫，而有爬虫的地方就有 Selenium 。原本好端端的 UI 自动化测试框架，怎么就助纣为虐做起爬虫来了呢？其实，主要原因是它提供了一个可以和浏览器交互的环境，从某种意义上来讲，Selenium 、PhantomJS 以及 Playwright 都可以认为是类似的技术，这里我们以 Selenium 为例，而通过 Selenium 实现网页长截图则主要有两种方式：其一，是构造一个足够“大”的浏览器，然后调用save_screenshot()方法进行截图；其二，是通过“拖拽”滚动条来滚动截图，然后再通过PIL进行拼接，下面来看具体的代码实现：</description></item><item><title>温故而知新，由 ADO.NET 与 Dapper 所联想到的</title><link>https://blog.yuanpei.me/posts/2621074915/</link><pubDate>Wed, 30 Dec 2020 12:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2621074915/</guid><description>这段时间在维护一个“遗产项目”，体验可以说是相当地难受，因为它的数据持久化层完全由 ADO.NET 纯手工打造，所以，你可以在项目中看到无所不在的 DataTable，不论是读操作还是写操作。这个 DataTable 让我这个习惯了 Entity Framework 的人感到非常别扭，我并不排斥写手写 SQL 语句，我只是拥有某种自觉并且清醒地知道，自己写的 SQL 语句未必就比 ORM 生成的 SQL 语句要好。可至少应该是像 Dapper 这种程度的封装啊，因为关系型数据库天生就和面向对象编程存在隔离，所以，频繁地使用 DataTable 无疑意味着你要写很多的转换的代码，当我看到DbConnection、DbCommand、DbDataReader、DbDataAdapter这些熟悉的“底层”的时候，我意识到我可以结合着 Dapper 的实现，从中梳理出一点改善的思路，所以，这篇博客想聊一聊ADO.NET、Dapper和Dynamic这三者间交叉的部分，希望能给大家带来新的启发。
重温 ADO.NET 相信大家都知道，我这里提到的DbConnection、DbCommand、DbDataReader、DbDataAdapte以及DataTable、DataSet，实际上就是 ADO.NET 中核心的组成部分，譬如DbConnection负责管理数据库连接，DbCommand负责 SQL 语句的执行，DbDataReader和DbDataAdapter负责数据库结果集的读取。需要注意的是，这些类型都是抽象类，而各个数据库的具体实现，则是由对应的厂商来完成，即我们称之为“驱动”的部分，它们都遵循同一套接口规范，而DataTable和DataSet则是“装”数据库结果集的容器。关于 ADO.NET 的设计理念，可以从下图中得到更清晰的答案：
ADO.NET架构 在这种理念的指引，使用 ADO.NET 访问数据库通常会是下面的画风。博主相信，大家在各种各样的DbHelper或者DbUtils中都见过类似的代码片段，在更复杂的场景中，我们会使用DbParameter来辅助DbCommand，而这就是所谓的SQL 参数化查询。
var fileName = Path.Combine(Directory.GetCurrentDirectory(), &amp;#34;Chinook.db&amp;#34;); using (var connection = new SQLiteConnection($&amp;#34;Data Source={fileName}&amp;#34;)) { if (connection.State != ConnectionState.Open) connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = &amp;#34;SELECT AlbumId, Title, ArtistId FROM [Album]&amp;#34;; command.CommandType = CommandType.</description></item><item><title>视频是不能 P 的系列：OpenCV 人脸检测</title><link>https://blog.yuanpei.me/posts/2997581895/</link><pubDate>Fri, 25 Dec 2020 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2997581895/</guid><description>恍惚间，2020 年已接近尾声，回首过去这一年，无论是疫情、失业还是“996”，均以某种特殊的方式铭刻着这一年的记忆。也许，是这个冬天的西安雾霾更少一点。所以，有时透过中午的一抹冬阳，居然意外地觉得春天的脚步渐渐近了，甚至连圣诞节这种“洋节日”都感到亲切而且期待，我想，这大概是我丧了一段时间的缘故吧！可不管怎样，人们对未来的生活时常有一种“迷之自信”，果然生还还是要继续下去的呀！趁着最近的时间比较充裕，我决定开启一个信息的系列：视频是不能 P 的。这是互联网上流传的一个老梗了，正所谓“视频是不能 P 的，所以是真的”。其实，在如今这个亦真亦假的世界里，哪里还有什么东西是不能 PS 的呢？借助人工智能“改头换面”越来越轻而易举，而这背后关于隐私和伦理的一连串问题随之而来，你越来越难以确认屏幕对面的那个是不是真实的人类。所以，这个系列会以 OpenCV 作为起点，去探索那些好玩、有趣的视频/图像处理思路，通过技术来证明视频是可以被 PS 的。而作为这个系列的第一篇，我们将从一个最简单的地方开始，它就是人脸检测。
第一个入门示例 学习 OpenCV 最好的方式，就是从官方的示例开始，我个人非常推荐的两个教程是 OpenCV: Cascade Classifier 和 Python OpenCV Tutorial，其次是 浅墨大神 的【OpenCV】入门教程，不同的是， 浅墨大神 的教程主要是使用 C++，对于像博主这样的“不学无术”的人，这简直无异于从入门到放弃，所以，建议大家结合自己的实际情况，选择适合自己的“难度”。好了，思绪拉回我们这里，在 OpenCV 中实现人脸检测，主要分为以下三个步骤，即，首先，定义联级分类器CascadeClassifier并载入指定的模型文件；其次，对待检测目标进行灰度化和直方图均衡化处理；最后，对灰度图调用detectMultiScale()方法进行检测。下面是一个简化过的入门示例，使用世界上最省心的 Python 语言进行编写：
import cv2 # 步骤1: 定义联级分类器CascadeClassifier并载入指定的模型文件 faceCascade = cv2.CascadeClassifier(&amp;#39;./haarcascades/haarcascade_frontalface_alt2.xml&amp;#39;) # 步骤2: 对待检测目标进行灰度化和直方图均衡化处理 target = cv2.imread(&amp;#39;target.jpg&amp;#39;) target_gray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY) target_gray = cv2.equalizeHist(target_gray) # 步骤3: 人脸检测 faces = faceCascade.detectMultiScale(target_gray) for (x,y,w,h) in faces: cv2.rectangle(target, (x, y), (x + w, y + h), (0, 255, 0), 2) # 步骤4: 展示结果 cv2.</description></item><item><title>作为技术宅的我，是这样追鬼滅の刃的</title><link>https://blog.yuanpei.me/posts/3602353334/</link><pubDate>Tue, 15 Dec 2020 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3602353334/</guid><description>有人说，“男人至死都是少年”，而这句听起来有一点中二的话，其实是出自一部同样有一点中二的动漫——银魂。我个人的理解是，知世故而不世故。也许，年轻时那些天马行空的想法，就像堂吉诃德大战风车一样荒诞，可依然愿意去怀着这样的梦想去生活。正如罗曼罗兰所言，“世上只有一种英雄主义，就是在认清生活真相之后依然热爱生活”。所以，继《浪客剑心》之后，我再次被一部叫做《鬼灭之刃》的动漫吸引，毕竟男人的快乐往往就是这么朴实无华且枯燥。一个快三十岁的人，如果还能被一部热血少年番吸引，大概可以说明，他身体里的中二少年连同中二少年魂还活着。最早的印象来自朋友圈里的一位二次元“少年”，他和自己儿子站一起，有种浑然天成的协调感，整个人是非常年轻的感觉。所以，大概，男人至死都是少年。
漫画的抓取 鬼滅の刃的漫画早已更完，令我不舍昼夜去追的，实际上是动画版的鬼滅の刃。虽然 B 站上提供中配版本，可一周更新两集的节奏，还是让我追得有一点焦灼(PS：我没有大会员呢)，甚至熬着夜提前“刷”了无限列车(PS：见文章末尾小程序码)。其实，鬼滅の刃前期并没有特别吸引人的地方，直到那田蜘蛛山那一话开始渐入佳境，鬼杀队和鬼两个阵营所构成的人物群像开始一点一点的展开。它的表达方式有点接近刺客信条，即反派都是在死亡一刹那间有了自我表达的机会，而玩家/观众都可以了解反派的过去。由于鬼是由人转变而来，所以，在热血和厮杀之外，同样有了一点关乎人性的思考。作为一名“自封”的技术宅，我必须要在追番的时候做点什么，从哪里开始好呢？既然漫画版早已更新完毕，我们要不先抓取漫画下来提前过过瘾？
OK，这里博主找了一个动漫网站，它上面有完整的鬼滅の刃漫画。我意识到从网上抓取漫画的行为是不对的，可这家网站提供的漫画明显是通过扫描获得的，因为正常的漫画都是通过购买杂志的方式获得的。所以，如果经济条件允许的情况下，还是希望大家可以支持正版，这里博主主要还是为了研究技术(逃，无意对这些资源做二次加工或者以任何方式盈利，所以，请大家不要向博主索取任何资源，我对自己的定位永远是一名软件工程师，谁让我无法成为尤小右这样的“美妆”博主呢？这一点希望大家可以理解哈！
鬼滅の刃作品页面 鬼滅の刃章节页面 简单分析下动漫网站结构，可以发现，它主要有两种界面，即作品页面和章节页面。作品页面里面会显示所有的章节，而每个章节里会显示所有的图片。所以，我们的思路是，首先，通过作品页面获取所有章节的链接。其次，针对每一个章节，获取总页数后逐页下载图片即可。注意到这个网站有部分内容是通过 JavaScript 动态生成的，所以，requests针对这种情况会有点力不从心。幸好，我们还有Selenium这个神器可以使用，我们一起来看这部分内容如何实现：
import requests from bs4 import BeautifulSoup import fake_useragent import json import urllib from selenium import webdriver from selenium.webdriver.support.ui import Select from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import os, time import threading import threadpool class DemonSlayer: def __init__(self, baseUrl): self.baseUrl = baseUrl self.session = requests.session() self.headers = { &amp;#39;User-Agent&amp;#39;: fake_useragent.UserAgent(verify_ssl=False).random } # 使用无头浏览器 fireFoxOptions = webdriver.</description></item><item><title>使用 Python 抽取《半泽直树》原著小说人物关系</title><link>https://blog.yuanpei.me/posts/1427872047/</link><pubDate>Tue, 08 Dec 2020 22:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1427872047/</guid><description>此时此刻，2020 年的最后一个月，不管过去这一年给我们留下了怎样的记忆，时间终究自顾自地往前走，留给我们的怀念已时日无多。如果要说 2020 年的年度日剧，我想《半泽直树》实至名归，这部在时隔七年后上映的续集，豆瓣评分高达 9.4 分，一度超越 2013 年第一部的 9.3 分，是当之无愧的现象级电视剧，期间甚至因为疫情原因而推迟播出，这不能不感谢为此付出辛勤努力的演职人员们。身为一个“打工人”，主角半泽直树那种百折不挠、恩怨分明的性格，难免会引起你我这种“社畜”们的共鸣，即使做不到“以牙还牙，加倍奉还”，至少可以活得像一个活生生的人。电视剧或许大家都看过了，那么，电视剧相对于原著小说有哪些改动呢？今天，就让我们使用 Python 来抽取半泽直树原著小说中的人物关系吧！
准备工作 在开始今天的博客内容前，我们有一点准备工作要完成。考虑到小说人物关系抽取，属于自然语言处理(NLP)领域的内容，所以，除了准备好 Python 环境以外，我们需要提前准备相关的中文语料，在这里主要有：半泽直树原著小说、 半泽直树人名词典、半泽直树别名词典、中文分词停用词表。除此之外，我们需要安装结巴分词、PyECharts两个第三方库(注，可以通过 pip 直接安装)，以及用于展示人物关系的软件Gephi(注，这个软件依赖 Java 环境)。所以，你基本可以想到，我们会使用结巴分词对小说文本进行分词处理，而半泽直树人名列表则作为用户词典供结巴分词使用，经过一系列处理后，我们最终通过Gephi和PyECharts对结果进行可视化，通过分析人物间的关系，结合我们对电视剧剧情的掌握情况，我们就可以对本文所采用方法的效果进行评估，也许你认为两个人毫无联系，可最终他们以某种特殊的形式建立了联系，这就是我们要做这件事情的意义所在。本项目已托管在 Github上，供大家自由查阅。
原理说明 这篇博客主要参考了 Python 基于共现提取《釜山行》人物关系 这个课程，该项目已在 Github 上开源，可以参考：https://github.com/Forec/text-cooccurrence。这篇文章中提到了一种称之为“共现网络”的方法，它本质上是一种基于统计的信息提取方法。其基本原理是，当我们在阅读书籍或者观看影视作品时，在同一时间段内同时出现的人物，通常都会存在某种联系。所以，如果我们将小说中的每个人物都看作一个节点，将人物间的关系都看作一条连线，最终我们将会得到一个图(指数据结构中的Graph)。因为Gephi和PyECharts以及NetworkX都提供了针对Graph的可视化功能，因此，我们可以使用这种方法，对《半泽直树》原著小说中的人物关系进行抽取。当然，这种方法本身会存在一点局限性，这些我们会放在总结思考这部分来进行说明，而我们之所以需要准备人名词典，主要还是为了排除单纯的分词产生的干扰词汇的影响；准备别名词典，则是考虑到同一个人物，在不同的语境下会有不同的称谓。
过程实现 这里，我们定义一个RelationExtractor类来实现小说人物关系的抽取。其中，extract()方法用于抽取制定小说文本中的人物关系，exportGephi()方法用于输出 Gephi 格式的节点和边信息， exportECharts()方法则可以使用ECharts对人物关系进行渲染和输出：
import os, sys import jieba, codecs, math import jieba.posseg as pseg from pyecharts import options as opts from pyecharts.charts import Graph class RelationExtractor: def __init__(self, fpStopWords, fpNameDicts, fpAliasNames): # 人名词典 self.name_dicts = [line.strip().split(&amp;#39; &amp;#39;)[0] for line in open(fpNameDicts,&amp;#39;rt&amp;#39;,encoding=&amp;#39;utf-8&amp;#39;).</description></item><item><title>厉害了！打工人用 Python 分析西安市职位信息</title><link>https://blog.yuanpei.me/posts/2147036181/</link><pubDate>Sat, 05 Dec 2020 12:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2147036181/</guid><description>在上一篇博客中，我和大家分享了整个 11 月份找工作的心路历程，而在找工作的过程中，博主发现西安大小周、单休这种变相“996”的公司越来越多，感慨整个行业越来越“内卷”的同时，不免会对未来的人生有一点迷茫，因为深圳已经开始试运行“996”了，如果有一天“996”被合法化并成为一种常态，那么，我们又该如何去面对“人会一天天衰老，总有一天肝不动”的客观规律呢？我注意到 Boss 直聘移动端会展示某个公司的作息时间，所以，我有了抓取西安市职位和公司信息并对其进行数据分析的想法，我想知道，这到底是我一个人的感受呢？还是整个世界的确是这样子的呢？带着这样的想法，博主有了今天这篇博客。所以，在今天这篇博客里，博主会从Boss 直聘、智联招聘以及前程无忧上抓取职位和公司信息，并使用 MongoDB 对数据进行持久化，最终通过pyecharts对结果进行可视化展示。虽然不大确定 2021 年会不会变得更好，可生活最迷人的地方就在于它的不确定性，正如数据分析唯一可以做的，就是帮助我们从变化的事物中挖掘出不变的规律一样。
爬虫编写 其实，这种类似的数据分析，博主此前做过挺多的啦，譬如 基于 Python 实现的微信好友数据分析 以及 基于新浪微博的男女性择偶观数据分析(下) 这两篇博客。总体上来说，大部分学习 Python 的朋友都是从编写爬虫开始的，而在博主看来，数据分析是最终的目的，编写爬虫则是达到这一目的的手段。而从始至终，“爬”与“反爬”的较量从未停止过，Requests、BeautifulSoup、Selenium、Phantom 等等的技术层出不穷。考虑到现在编写爬虫存在风险，所以，我不会在博客里透露过多的“爬虫”细节，换言之，我不想成为一个教别人写爬虫的人，因为这篇博客的标签是数据分析，关于爬虫的部分，我点到为止，不再过多地去探讨它的实现，希望大家理解。而之所以要从这三个招聘网站上抓取，主要还是为了增加样本的多样性，因为 Boss 直聘上西安市的职位居然只有 3 页，这实在是太让人费解了！
Boss 直聘 通过抓包，我们可以分析出 Boss 直聘的地址：https://www.zhipin.com/job_detail/?query={query}&amp;amp;city={cityCode}&amp;amp;industry=&amp;amp;position=&amp;amp;page={page}。其中，query为待查询关键词，cityCode为待查询城市代码，page为待查询的页数。可以注意到，industry和position两个参数没有维护，它们分别表示待查询的行业和待查询的职称。因为我们面向的是更一般的“打工人”，所以，这些都可以进行简化。对于cityCode这个参数，我们可以通过下面的接口获得：https://www.zhipin.com/wapi/zpCommon/data/city.json。这里，简单定义一个方法extractCity()来提取城市代码：
def extractCity(self, cityName=None): if (os.path.exists(&amp;#39;bossCity.json&amp;#39;) and cityName != None): with open(&amp;#39;bossCity.json&amp;#39;, &amp;#39;rt&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as fp: cityList = json.load(fp) for city in cityList: if (city[&amp;#39;name&amp;#39;] == cityName): return city[&amp;#39;code&amp;#39;] else: response = requests.get(self.cityUrl) response.raise_for_status() json_data = response.json(); if (json_data[&amp;#39;code&amp;#39;] == 0 and json_data[&amp;#39;zpData&amp;#39;] !</description></item><item><title>一个西漂打工人的求职心路</title><link>https://blog.yuanpei.me/posts/1809438689/</link><pubDate>Wed, 18 Nov 2020 12:49:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1809438689/</guid><description>其实，这段故事说出来，多少有一点难为情，因为我实在没有想到，这一切会变得这样艰难。
10 月份从上一家公司离职的时候，当时，我手上有两个 Offer，一家是做旅游类产品的创业公司，一家则是声名狼藉的中软国际。因为刚来西安时，面对人生地不熟的新环境，曾经在这里有过一段时间的工作经历，所以，我从本能上排斥再回到那种地方。而创业公司本身的不稳定性，一度让我感到纠结，而最终的结果是，我放弃了这两份 Offer。此时，对于一个工作刚满 5 年的人来说，一个月 13K 或 14K 的薪水，我感到相当的知足，可惜人生常常没有最优解，选择与妥协才是常态。
此后，我面试了奥博杰天(Objectiva)，在一轮面试后被评定为中级以后，我便再没有接到参加复试的通知，后来，HR 告诉我他们只招高级以上的开发人员。关于这家公司，一件非常有趣的事情是，我的一位朋友在获得这家公司的工作后，因为适应不了终日远程办公的痛苦，最终还是选择从这里离开。大概这是为了印证围城里的那句话，“城外的人想进去，城里的人想出来”。虽然“面试造火箭，入职拧螺丝”有时是求职者的常态，可偏偏你连去拧螺丝的机会都没有。
后来找到一家做背调业务的公司，甚至都准备好要在这里开始新的旅程，可当我看到毫无架构可言的项目时，终于还是倒吸了一口冷气。虽然此前和面试官交流了很多相对前沿的内容，可真正入职以后，还是被安排去做维护遗留项目的工作，更让我感到沮丧的是，到第三天的时候，一位老员工突然告诉我，他要转岗去做前端，需要交接一部分工作给我。对于这一件事情，我之前的朋友都安慰我，这些问题你都知道该怎么去解决，因为企业招聘你过来就是希望你去解决这些问题。可从一个普通与昂工的角度来看，一个组织实在很难从底层去做出什么改变，何况这家公司采取了和上家公司一样的策略，寄希望于空降一个某个大公司的架构师。
现在回想起来，在试用期期间离开，或许有一点冲动使然，可听到后来接替我职位的新人，同样在呆了一周后离开了。所以，此时，你问我对这份工作感到后悔嘛？坦白说，我并不知道该怎么去说，而更戏剧性的反转，则是后来一家公司联系到我，问我有没有意向去做这些背调公司的外包，你要知道，在西安几乎没有互联网公司，所以，当绕了一大圈后，再回到原点的时候，我不禁哑然失笑。再后来，经过内推拿到了西安中兴的面试资格，我感觉不管是笔试还是面试，我的完成度都还可以，尤其是在第一轮面试中，从面试官那里收获了很多的东西。而在复试的过程中，领导们对我非科班出身的挑剔，多少让我想起令人心动的 Offer 中，因为非法本 + 年龄大被嫌弃的丁辉，和这些人相比，我们这些普通人根本称不上努力，可至少尊重那些一直在默默努力着的人啊。
这样辗转了一周，终于还是没能等到中兴的电话，我想应该是彻底凉了吧，果然人生没有那么多逆袭的可能，年少时虚度的光阴，终究会在未来某一天让你感到后悔。就这样，一直等一直面，陆陆续续地接到像软通动力这种外包性质的 Offer，在面试中更是见识了大大小小的各种公司，我发现整个行业都在疯狂的内卷，996 在道德上的批判声还没有褪去，人们又开始钻劳动法的空子，搞大小周、周一/三/四强制加班，这样做的不单单有华为、中兴这样的大厂，同样还有这些小公司。
今年因为疫情的原因，医疗行业应该赚得盆满钵满，可好多公司还是在疯狂地抢占市场，类似的还有物流行业，可在效益还不错的情况下，我只看到了越来越变本加厉的压榨，我看日剧《下町火箭》的时候就在想，虽然佃制作所这样一家小公司一样会加班，可人家的是火箭级别的阀门和加速器啊，人家愿意花时间去钻研工艺技术，而我们只能通过比别人早交付来赢得客户的青睐，那这是否说明，我们和竞争对手间的服务差异化其实并不大呢？
现在，只要是个互联网公司，都能蹭一蹭双十一的热度，可在这场疫情背后，实体经济有多么的不景气，今天人们找工作就有多绝望，毕竟连房地产行业都表现出颓势，互联网行业不可能一直这样”热“下去，终有一天，一切都会回复到冷静。那么，对于没有“互联网”的西安 IT 圈子，程序员未来的退路又在哪里呢？我身边有很多 30 多岁的中年男人，那种说不出来的沉重感，时常让我对未来感到迷茫，他们都曾劝我回到三线小城市、考个公务员了却残生。也许我那个时候不大懂，而此时此刻终于感受到这种无奈，业内 35 岁的内卷化越来越严重，甚至我没想到，有一天大小周和周一/三/四强制加班会变成一种常态化。
这次面试中兴的经历，让我意识到，虽然进入 IT 行业的门槛非常低，可同样不幸的是，大厂/国企对相关岗位的门槛在不断加高，研究生起步基本就是标配啦，我甚至怀疑，我没有通过中兴的面试，是否和我非科班的出身以及没有考过的四级有关，这的确是一个现实问题，不管你想走技术路线，还是想走管理路线，一段大厂的经历能为你增加不少闪光点，可如果你拼进全力依然被这些门槛挡住，你是否会对未来感到迷茫呢？因为西安的 IT 圈子就这么大，你下一次换工作依然会面对这些公司，而人生又有多少个换工作的机会呢？
所以，我一直在想要不要继续留在西安，在这边固然比老家多“一点”可能性，可就真的只是一点点而已，现在再次充满变数的时候，这一点点的优势就变得不再明显。四年前，为了离当时女朋友近一点而来西安，如今四年过去了，渐渐地很多问题再次浮现出来，也许，那个时候的她就想到了未来的各种可能吧！有天晚上，我找一位很久之前认识的朋友，向他询问关于 35 岁这道坎的想法。当多年未曾听到的声音再次传入耳朵，突然感到一阵亲切，他非常平静地对我说，“我今年已经 36 岁了，这道坎对我来说已经是过去了”。的确，我们总是对未来充满各种各样的担忧，可想到两年前，我一样是怀着忐忑的心情去了上家公司。
我和朋友在讨论这些问题的时候，都觉得未来去做个 IT 讲师是个好归宿，虽然再反过头去割新一茬年轻人的韭菜，有一点屠龙少年终成恶龙的意味，可我觉得，人生还是早一点考虑备选答案比较好。我们之所以敢去背负 30 年的房贷，是因为我们愿意去相信“未来越来越好”，可事实上是你的身体每一天都在衰老，虽然现在退休年龄延迟到 60 岁了，可想到还在幸苦忙碌着的父母，还是会觉得羞愧难当，古人说 30 而立，可我还没有立起来，父母已经在老去了。说了这么多，工作我还是会去努力地找，但应该不会不给自己留一点时间，因为除了找对象以外，我给自己订了几个学习计划，比如：软考的中/高级证书、英语练习(考个英语证书)、强化公开场合的演讲能力、Google PDE 考试。
一个 30+的朋友和我说，他想自己干点啥，因为他觉得他再找工作就没人要了，也许生活就是永远这么充满变化吧，就像不变的只有变化本身一样，提前焦虑未来没有什么用，现在的规划将来不一样会按部就班，我只能说，多去想想自己有什么，如果你只是比别人能加班，这实在算不上什么过人的长处，因为随时有年轻人可以替换你下来。人活着啊，不能光长年龄和皱纹，想想两度背水一战的丁辉，我们这点努力能叫做拼尽全力吗？这就是我，一个“西漂”四年的外地打工人的一点想法，如果你有更好的解决“内卷”的思路，我会非常感谢你告诉我这些。</description></item><item><title>使用 dotTrace 对 .NET 应用进行性能分析与优化</title><link>https://blog.yuanpei.me/posts/3672690776/</link><pubDate>Sun, 01 Nov 2020 12:19:02 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3672690776/</guid><description>前几天，有位朋友问我，你平时都是怎么去排查一个程序的性能问题的啊。不要误会，这位朋友不是我啦，因为我真的有这样一位叫做 Toby 的朋友。说到性能问题，可能大家立马会想到类似并发数、吞吐量、响应时间、QPS、TPS等等这些指标，这些指标的确可以反映出一个系统性能的好坏。可随着我们的系统结构变得越来越复杂，要找到这样一个性能的“损耗点”，同样会变得越来越困难。在不同的人的眼中，对于性能好坏的评判标准是不一样的，譬如在前端眼中，页面打开速度的快慢代表着性能的好坏；而在后端眼中，并发数、吞吐量和响应时间代表着性能的好坏；而在 DBA 眼中，一条 SQL 语句的执行效率代表着性能的好坏。更不用说，现实世界中的程序要在硬件、网络的世界里来回穿梭了，所以，从 80%的功能堆积到 100%，是件非常容易的事情；而从 80%的性能优化到 85%，则不是件非常轻松的事情。想清楚这一点非常简单，因为我们的系统从来都不是简单的1 + 1 = 2。此时，我们需要一个性能分析工具，而今天给大家分享的是 JetBrains 出品的 dotTrace 。
快速开始(Quick Start) 安装软件的过程此处不表，这里建议大家同时安装 dotTrace 和 dotMemery。因为这都是 JetBrains 全家桶中的软件，安装的时候选一下就可以了，可谓是举手之劳。安装好以后的界面是这样的，可以注意到，它可以对进程中的 .NET 应用、本机的 .NET 应用以及远程的 .NET 应用进行检测，因为这里写一个 .NET Core 应用来作为演示，所以，我们选择 Profile Local App：
dotTrace主界面 在这里，我们准备了一个简单的控制台程序：
public class Program { static void Main(string[] args) { CPUHack(); MemeryHack(); } public static void MemeryHack() { Console.ReadLine(); var bytes = GC.GetTotalAllocatedBytes(); Console.WriteLine($&amp;#34;AllocatedBytes: { bytes } bytes&amp;#34;); var list = new List&amp;lt;byte[]&amp;gt;(); try { while (true) { list.</description></item><item><title>一道 HashSet 面试题引发的蝴蝶效应</title><link>https://blog.yuanpei.me/posts/3411909634/</link><pubDate>Tue, 20 Oct 2020 12:19:02 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3411909634/</guid><description>没错，我又借着“面试题”的名头来搞事情了，今天要说的是 HashSet ，而这确实是一个实际面试中遇到的问题。当时的场景大概是这样的，面试官在了解了你的知识广度以后，决心来考察一番你的基本功底，抛出了一个看起来平平无奇的问题：说一说你平时工作中都用到了哪些数据结构。你心想，这还不简单，Array、ArrayList、List、Dictionary、HashSet、Stack、Queue&amp;hellip;等等各种集合类简直如数家珍，甚至你还能说出这些数据结构间的优劣以及各自使用的场景。可没想到，面试官话锋一转，直接来一句，“你能说说 HashSet 去重的原理吗”，好家伙，你这简直不按套路出牌啊&amp;hellip;本着每次面试都有一点收获的初心，于是就有了今天这篇博客，不同的是，顺着这个思路继续深挖下去，博主又发现了几个平时关注不到的技术盲点，所以，博主称之为：一道 HashSet 面试题引发的蝴蝶效应。
HashSet 源代码解读 OK，首先，我们来回答第一个问题，即：HashSet 去重的原理是什么？。为此，博主翻阅了 HashSet 的 源代码。首先，我们会注意到 HashSet 的构造函数，它需要一个类型为IEqualityComparer&amp;lt;T&amp;gt;的参数。从这个命名上我们就可以知道，这是一个用于相等性比较的接口，我们初步推测，HashSet 去重应该和这个接口有关：
public HashSet() : this(EqualityComparer&amp;lt;T&amp;gt;.Default) { } public HashSet(int capacity) : this(capacity, EqualityComparer&amp;lt;T&amp;gt;.Default) { } public HashSet(IEqualityComparer&amp;lt;T&amp;gt; comparer) { } public HashSet(IEnumerable&amp;lt;T&amp;gt; collection) : this(collection, EqualityComparer&amp;lt;T&amp;gt;.Default) { } public HashSet(IEnumerable&amp;lt;T&amp;gt; collection, IEqualityComparer&amp;lt;T&amp;gt; comparer) : this(comparer) { } 我们都知道 HashSet 可以去重，比如，我们向 HashSet 添加多个相同的元素，实际上 HashSet 中最终只会有一个元素。所以，我们自然而然地想到，看看 HashSet 中的 Add() 方法呗，或许能从这里看出一点端倪。HashSet 中一共有两个 Add() 方法，它们内部都调用了 AddIfNotPresent() 方法：
void ICollection&amp;lt;T&amp;gt;.</description></item><item><title>当姜子牙遇见朱一旦</title><link>https://blog.yuanpei.me/posts/1085014581/</link><pubDate>Sun, 18 Oct 2020 12:19:02 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1085014581/</guid><description>当导演张策宣布，不再为朱一旦系列担任编剧和配音时，我终于意识到，“十佳员工”不再是一个梗，而是一个活生生的人。也许，身为老板的“朱一旦”，永远都没有读懂这些黑色幽默背后的含义。而显然，站在普通人对立面的资本家们，终究不会因此而洗心革面，代表劳苦大众向这个时代发声。不管是后浪还是非浪，资本家们不会选择和钱过不去，所以，即使有像鲁迅一般针砭时弊的张策，可在一个“屁股决定脑袋”的世界里，“十佳员工”突然就变成一个不再好笑的词汇，因为，这个人可以是你，可以是我，可以是我们中的任何一个。在新冠疫情肆虐的时候，『一块劳力士的回家之路』让我们感受到了现实的魔幻，可此时此刻，我们终于知道，“艺术来源于生活，而往往高于生活”，果然，如有雷同，是不胜荣幸的了。可能是因为我此刻在经历着同样的事情，所以，难免感同身受地想到 C 座 802 里这群真实存在着的人们。
我有一位为公司奉献 11 年青春的同事，可当他离开这家公司时，并没有我想象的中那样充满不舍，大概“鸟尽弓藏”、“大地茫茫真干净”这些句子，从古至今就是这样子的吧！马老师说，“996 是一种福报”，而此前的一位马老师则说，“资本家生来就是剥削劳动者的一切剩余价值”。历史像个任人打扮的小姑娘，你方唱罢我登场，文过饰非，到底谁又讲得清对错？有人说，一个人开始成熟，就是从学习这几千年来的厚黑学、阴谋论开始，的确啊，连封神都开始变成一场阴谋，不同的是，这次的因果都落在原始天尊身上，每一个人都渴望像姜子牙一样，断天梯、破枷锁，似乎一定要执著于什么东西，这样的人生会显得更真实一点。可每个人自以为最完美的安排，终究无法让每一个人信服啊，正如“朱一旦”们喜欢“非洲安排”，马小策与张策，说到底不过是一种代号而已，当这种“安排”无法调和的时候，人和神仙一样，都会暴走，都会变身，唯一不同的是，人是要吃饭的，而神仙们早已学会辟谷。
所以，在“救一个人还是救苍生”这个问题上，其实谁都没有错，我特别喜欢李诞在『奇葩说』中表达的一个观点，“以自私却不伤害别人的方式活着，才能维持世界的运转。而正是那些为了宏图伟业不计后果的牺牲‘小猫’的人，频频地让我们的世界陷入「大火」”。朱一旦不想再做“任人摆布”的老板，张策不想再做“默默无名”的幕后英雄，马老师早已看破这一切，“钱没给到位”，“心委屈了”，身在其位时榨干身体的“996”，人走茶凉时送瘟神般“高效”，一冷一热，果然是“环球同此凉热”呢。在全球变暖的趋势下，如果我们以自私却不伤害别人的方式活着，虽然活得有一点清冷、没有人情味，但这样是不是会更安心一点，骨子里与生俱来就带着“竞争”的基因的我们，是不是一定要学会“狠”、学会“不择手段”、学会“伤害”。我二十多岁的时候，想努力去照顾好一个人，而等到我快要三十岁的时候，我终于能勉强照顾好自己，这简直是一种幸运。
这种感慨在某个场景下会更加明显，譬如一个人去看电影的时候，虽然我很喜欢和邻座的小朋友说话，可对方父母一句友善的“叔叔”，终于还是让两个人产生了距离。譬如找工作面试的时候，发觉三十上下的“哥哥姐姐”们，都开始面对“总监”级别职位时的恍惚感。也许，我们这一代人真的已经老了吧，而那个人早已离开你很久很久，我无意去对立资本家与劳动者间积怨久矣的矛盾，更无意去揣测封神台下蛰伏已久的阴谋。回想以前，乐无异在『古剑奇谭 2』中说出一句，“众生虽苦，还请诸恶莫作”，当时大概只是觉得这句话酷到不行，倘若议论公平，C 座 802 诸如三濑子、马小玲、马小浩等等角色，每一个都带着无数的梗，没有他们就没有整个朱一旦宇宙，当人们为张策惋惜的时候，是不是就选择性地遗忘了他们呢？朱一旦不会成为劳苦大众的代言人，而且任何人都不会，因为一切的流量到最后都是生意。
我在 B 站关注过一位阿婆主，起初，他在厂里打工，下班后的“入味儿”是他主要的拍摄内容。后来，因为疫情的原因，他开始学别人拍做菜的视频。再后来，发现他变成了一位外卖小哥。世人皆苦，家家有本难念的经，可我们除了祝福以外，又能做一点什么呢？成人世界里，利益、立场、观点……，该有的一切都有，唯独没有对错，希望一个组织有一致的步伐、一致的声音，可偏偏人是一根会思考的芦苇，我知道，当一个人在某一种身份下，他必须要去推动一种文化形成，可如果这些声音连他自己都不信，这种文化的底蕴应该不会特别丰富，很容易成为政治博弈的牺牲品。我从前天真地以为，在互联网这样一个相对开放的环境里，不会存在政治这种产物。而出于对这种东西的逃避，我没有选择成为三线小城市里的一个公务员，实际上我尝试过，结果证明我真的不适合。可后来我发现我错了，只要有人的地方就会存在政治，无论是公司还是社区，每天都有人宣扬这样或者那样的“文化”，这个时候，我希望我们每一个人都去用心甄别这些概念，因为作为人的自觉，他只会说对自己有利的话，正如择偶标准是最毫无标准可言的标准一样，王垠说编程世界里充满宗派，就是最好的证明。
所以，我不大愿意去统一什么东西，充满多样性、充满个性的世界，才是一个正常的世界，以结果论的观点而言，只要能送大家都目的地，是飞机还是高铁还是火车，真的重要吗？如果非要去统一什么，我希望是“语言”或者“领域语言”，因为，我们的沟通，因为存在太多的翻译而逐渐失真、甚至被曲解，我们一般把这样的沟通称之为扯皮，就像土味情话虽然美妙动听，但它携带了大量无用的信息。所以，即使冒着成为“钢铁直男”的危险，我依然想成为一个表达清晰的人。有人说，姜子牙就不能和原始天尊好好商量一下吗？非要自断天梯逼得鸿钧老祖出手吗？人类啊，归根到底，只愿意相信自己相信的，只愿意看见自己看见的，这种意念在成年后往往更加强烈，有多少遗憾就是得不到有效沟通造成的呢？九尾狐自觉被原始天尊欺骗、过河拆桥，而原始天尊认为“非我族类，其心必异”，都是选择性地相信了自己愿意去相信的东西。有人说，姜子牙有强迫症，为什么会任由师尊披头散发？因为不是每个人都能像约翰·纳什那样，在最亲密的人面前直抒胸臆，人类就是这么奇怪，和陌生人玩什么真心话大冒险，在亲人面前反而含蓄、羞怯起来，可能是因为某种特殊的磁力限制了声道发声吧，科学与玄学往往就是这么切换自如。
思绪就像一个无底黑洞，姜子牙与朱一旦，两个八竿子不十竿子都打不着的人，就这么神奇地在我脑海里，完成了一次对话。如果思维存在奇点，将会坍陷于何处，苏格拉有没有底不重要，马老师们谁说得对同样不重要，甚至你看我这满纸荒唐言依然不重要，它仅仅表明我此时此刻在思考，我是一个活生生的人，所谓“我思故我在”，无非给枯燥的人生多一点无用的点缀罢了，你说朱一旦都不枯燥了，我们却还停留在这里，你说，还有比这个更枯燥的事情吗？申公豹形神俱灭，从头开始修行，居然连基因都发生了突变，大概，在这世间，没有什么可以永恒。</description></item><item><title>基于选项模式实现.NET Core 的配置热更新</title><link>https://blog.yuanpei.me/posts/835719605/</link><pubDate>Sun, 11 Oct 2020 12:19:02 +0000</pubDate><guid>https://blog.yuanpei.me/posts/835719605/</guid><description>最近在面试的时候，遇到了一个关于 .NET Core 配置热更新的问题，顾名思义，就是在应用程序的配置发生变化时，如何在不重启应用的情况下使用当前配置。从 .NET Framework 一路走来，对于 Web.Config 以及 App.Config 这两个配置文件，我们应该是非常熟悉了，通常情况下， IIS 会检测这两个配置文件的变化，并自动完成配置的加载，可以说它天然支持热更新，可当我们的视野伸向分布式环境的时候，这种配置方式就变得繁琐起来，因为你需要修改一个又一个配置文件，更不用说这些配置文件可能都是放在容器内部。而有经验的朋友，可能会想到，利用 Redis 的发布-订阅来实现配置的下发，这的确是一个非常好的思路。总而言之，我们希望应用可以随时感知配置的变化，所以，在今天这篇博客里，我们来一起聊聊 .NET Core 中配置热更新相关的话题，这里特指全新的选项模式(Options)。
Options 三剑客 在 .NET Core 中，选项模式(Options)使用类来对一组配置信息进行强类型访问，因为按照接口分隔原则(ISP)和关注点分离这两个工程原则，应用的不同部件的配置应该是各自独立的，这意味着每一个用于访问配置信息的类，应该是只依赖它所需要的配置信息的。举一个简单的例子，虽然 Redis 和 MySQL 都属于数据持久化层的设施，但是两者属于不同类型的部件，它们拥有属于各自的配置信息，而这两套配置信息应该是相互独立的，即 MySQL 不会因为 Redis 的配置存在问题而停止工作。此时，选项模式(Options)推荐使用两个不同的类来访问各自的配置。我们从下面这个例子开始：
{ &amp;#34;Learning&amp;#34;: { &amp;#34;Years&amp;#34;: 5, &amp;#34;Topic&amp;#34;: [ &amp;#34;Hotfix&amp;#34;, &amp;#34;.NET Core&amp;#34;, &amp;#34;Options&amp;#34; ], &amp;#34;Skill&amp;#34;: [ { &amp;#34;Lang&amp;#34;: &amp;#34;C#&amp;#34;, &amp;#34;Score&amp;#34;: 3.9 }, { &amp;#34;Lang&amp;#34;: &amp;#34;Python&amp;#34;, &amp;#34;Score&amp;#34;: 2.6 }, { &amp;#34;Lang&amp;#34;: &amp;#34;JavaScript&amp;#34;, &amp;#34;Score&amp;#34;: 2.8 } ] } } 此时，如果希望访问Learning节点下的信息，我们有很多种实现方式：
//方式1 var learningSection = Configuration.</description></item><item><title>Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决</title><link>https://blog.yuanpei.me/posts/3086300103/</link><pubDate>Sat, 05 Sep 2020 14:28:20 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3086300103/</guid><description>话说最近这两周里，被迫官宣996的生活实在是无趣，在两周时间里安排三周的工作量，倘若用丞相的口吻来说，那便是: 我从未见过有如此厚颜无耻之人。无法为工作的紧急程度排出优先级，这便是身为肉食者们的鄙。古人云：肉食者鄙，未能远谋，诚不欺我也。一味地追求快速迭代，“屎”山越滚越高没有人在乎；一味地追求功能叠加，技术债务越来越多没有人在乎。所以，本着“多一事不如少一事”的原则，直接通过 Dapper 写 SQL 语句一样没有问题，因为被压榨完以后的时间只能写这个。在今天的这篇博客里，我想和大家分享的是，Dapper.Contrib在操作 Oracle 数据库时引发 ORA-00928: 缺失 SELECT 关键字 这一错误背后的根本原因，以及 Dapper 作为一个轻量级 ORM 在设计上做出的取舍。
问题回顾 在使用 Dapper.Contrib 操作 Oracle 数据库的时候，通过 Insert() 方法来插入一个实体对象，此时，会引发 ORA-00928: 缺失 SELECT 关键字 这种典型的 Oracle 数据库错误，对于经常使用 Dapper 的博主而言，对于 @ 还是 : 这种无聊的语法还是有一点经验的，在尝试手写 SQL 语句后，发现使用 Dapper 提供的 Execute() 扩展方法一点问题都没有，初步判定应该是 Dapper.Contrib 这个扩展库的问题，在翻阅 Dapper 的源代码以后，终于找到了问题的根源所在，所以，下面请跟随博主的目光，来一起解读解读 Dapper.Contrib 这个扩展库，相信你看完以后就会明白，为什么这里会被 Oracle 数据库摆上一道，以及为什么它至今都不考虑合并 Oracle 数据库相关的 PR。
原因分析 众所周知，Dapper 的核心其实就是一个 SqlMapper ，它提供的 Query() 和 Execute() 接口本身都是附加在 IDbConnection 接口上的扩展方法，所以，最基础的 Dapper 用法其实是伴随着 SQL 语句和以匿名对象为主的参数化查询，这可以说是 Dapper 的核心，而 Dapper.</description></item><item><title>.NET Core 中对象池(Object Pool)的使用</title><link>https://blog.yuanpei.me/posts/2414960312/</link><pubDate>Sat, 15 Aug 2020 16:37:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2414960312/</guid><description>在此前的博客中，博主参考 eShopOnContainers 实现了一个基于 RabbitMQ 的事件总线(EventBus)。在这个项目中，它提供了一个持久化连接的类DefaultRabbitMQPersistentConnection，主要解决了 RabbitMQ 在连接断开后自动重连的问题，可实际上我们都知道，RabbitMQ 提供的连接数是有一个上限的，如果频繁地使用短连接的方式，即通过ConnectionFactory的CreateConnection()方法来创建一个连接，从本质上讲，一个Connection对象就是一个 TCP 连接，而Channel则是每个Connection对象下有限的虚拟连接，注意“有限”这个限定词，这意味着Channel和Connection一样，都不能毫无节制的创建下去。此时，官方推荐的做法有两种：(1)：一个Connection对应多个Channel同时保证每个Channel线程独占；(2)：创建一个Connection池同时定期清除无效连接。这里的第二种做法，显然就是我们今天要说的对象池(Object Pool)啦，我们将从这里拉开这篇博客的帷幕。
什么是对象池 首先，我们来回答第一个问题，什么是对象池？简单来说，它就是一种为对象提供可复用性能力的软件设计思路。俗话说**“有借有还，再借不难”**，而对象池就是通过“借”和“还”这样两个动作来保证对象可以被重复使用，进而节省频繁创建对象的性能开销。对象池在游戏设计中使用的更普遍一点，因为游戏中大量存在着像子弹、怪物等等这类可复用的对象，你在玩第一人称射击游戏(FPS)时，总是有源源不断的子弹或者丧尸出现，可事实上这不过是数字世界的循环再生，因为玩家的电脑内存始终都有一个上限。而在数据库的世界里，则存在着一个被称为“连接池”的东西，每当出现数据库无法连接的情况时，经验丰富的开发人员往往会先检查“连接池”是否满了，这其实就是对象池模式在特定领域的具体实现啦，所以，对象池本质上就是负责一组对象创建和销毁的容器，下面是一个基本的对象池示意图：
对象池示意图 可以注意到， 对象池最大的优势就是可以自主地管理“池子”内的每个对象，决定它们是需要被回收还是可以重复使用。我们都知道，创建一个新的对象，需要消耗一定的系统资源，而一旦这些对象可以重复地使用，就能有效地节省系统资源的开销，这对于我们提高系统性能会非常有帮助。也许，现在计算机的硬件水平越来越好，可我们还是要重新拾起这个领域的基础知识，即数据结构、算法、数学和英语。如果你完全理解了对象池模式，你应该可以非常轻松地给出你的实现：
public class ObjectPool&amp;lt;T&amp;gt; : IObjectPool&amp;lt;T&amp;gt; { private Func&amp;lt;T&amp;gt; _instanceFactory; private ConcurrentBag&amp;lt;T&amp;gt; _instanceItems; public ObjectPool(Func&amp;lt;T&amp;gt; instanceFactory) { _instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); _instanceItems = new ConcurrentBag&amp;lt;T&amp;gt;(); } public T Get() { T item; if (_instanceItems.TryTake(out item)) return item; return _instanceFactory(); } public void Return(T item) { _instanceItems.Add(item); } } 注：以上代码片段来自微软的一篇文档：How to: Create an Object Pool by Using a ConcurrentBag。实际上，除了ConcurrentBag&amp;lt;T&amp;gt;，我们可以选择的数据结构还可以是Stack&amp;lt;T&amp;gt;、Queue&amp;lt;T&amp;gt;以及BlockingCollection&amp;lt;T&amp;gt;，此中差别，大家可以自己去体会。</description></item><item><title>利用 MySQL 的 Binlog 实现数据同步与订阅(下)：EventBus 篇</title><link>https://blog.yuanpei.me/posts/3424138425/</link><pubDate>Fri, 31 Jul 2020 12:01:14 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3424138425/</guid><description>终于到这个系列的最后一篇，在前两篇博客中，我们分别了介绍了Binlog的概念和事件总线(EventBus)的实现，在完成前面这将近好几千字的铺垫以后，我们终于可以进入正题，即通过 EventBus 发布 Binlog，再通过编写对应的 EventHandler 来订阅这些 Binlog，这样就实现了我们“最初的梦想”。坦白说，这个过程实在有一点漫长，庆幸的是，它终于还是来了。
Binlog 读取与解析 首先，我们通过 Python-Mysql-Replication 这个项目来读取 Binlog，直接通过pip install mysql-replication安装即可。接下来，我们编写一个简单的脚本文件，这再次印证那句名言——人生苦短，我用 Python：
def readBinLog(): stream = BinLogStreamReader( # 填写IP、账号、密码即可 connection_settings = { &amp;#39;host&amp;#39;: &amp;#39;&amp;#39;, &amp;#39;port&amp;#39;: 3306, &amp;#39;user&amp;#39;: &amp;#39;&amp;#39;, &amp;#39;passwd&amp;#39;: &amp;#39;&amp;#39; }, # 每台服务器唯一 server_id = 3, # 主库Binlog读写完毕时是否阻塞连接 blocking = True, # 筛选指定的表 only_tables = [&amp;#39;order_info&amp;#39;, &amp;#39;log_info&amp;#39;], # 筛选指定的事件 only_events = [DeleteRowsEvent, WriteRowsEvent, UpdateRowsEvent]) for binlogevent in stream: for row in binlogevent.rows: event = { &amp;#34;schema&amp;#34;: binlogevent.</description></item><item><title>利用 MySQL 的 Binlog 实现数据同步与订阅(中)：RabbitMQ 篇</title><link>https://blog.yuanpei.me/posts/580694660/</link><pubDate>Wed, 15 Jul 2020 14:39:07 +0000</pubDate><guid>https://blog.yuanpei.me/posts/580694660/</guid><description>紧接上一篇博客中的思路，这次我们来说说事件总线(EventBus)，回首向来，关于这个话题，我们可能会联想到发布-订阅模式、观察者模式、IObservable与 IObserver、消息队列等等一系列的概念。所以，当我们尝试着去解释这个概念的时候，它到底是什么呢？是一种设计模式？是一组 API 接口？还是一种新的技术？显而易见，发布-订阅模式和观察者模式都是设计模式，而 IObservable与 IObserver、消息队列则是具体的实现方式，就像你可以用委托或者事件去实现一个观察者模式，而 Redis 里同样内置了发布-订阅模型，换言之，这是抽象与具体的区别，消息队列可以用来实现 EventBus，而 EventBus 主要的用途则是系统间的解耦，说到解耦，你可能会对观察者模式和发布-订阅模式这两种模式感到困惑，因为它们实在是太像了，一个最本质的区别在于发布者(主题)是否与订阅者(观察者)存在强依赖关系，而发布-订阅引入了类似主题/Topic/Channel 的中介者，显然从解耦的角度要更彻底一些，所以，我们今天就来一起实现一个事件总线(EventBus)。
EventBus 整体设计 通过前面的探讨，我们可以知道，EventBus 其实是针对事件的发布-订阅模式的实现，所以，在设计 EventBus 的时候，我们可以结合发布-定阅模式来作为对照，而一个典型的发布-订阅模式至少需要三个角色，即发布者、订阅者和消息，所以，一般在设计 EventBus 的时候，基本都会从这三个方面入手，提供发布消息、订阅消息、退订消息的接口。由于 EventBus 本身并不负责消费消息，所以，还需要借助IEventHandler&amp;lt;T&amp;gt;来编写对应的事件处理器，这是 EventBus 可以实现业务解耦的重要原因。而为了维护事件和事件处理器的关系，通常需要借助 IoC 容器来注册这些 EventHandler，提供类似Castle或者Autofac从程序集中批量注册的机制，下面是博主借鉴 eShopOnContainers 设计的 EventBus，首先是 IEventBus 接口，其定义如下：
public interface IEventBus { void Publish&amp;lt;TEvent&amp;gt; (TEvent @event) where TEvent : EventBase; void Subscribe&amp;lt;T, TH&amp;gt; () where T : EventBase where TH : IEventHandler&amp;lt;T&amp;gt;; void Unsubscribe&amp;lt;T, TH&amp;gt; () where TH : IEventHandler&amp;lt;T&amp;gt; where T : EventBase; } 注意到，这里对事件(EventBase)和事件处理器(EventHandler)均有一定约束，这是为了整个 EventBus 的实现，在某些 EventBus 的实现中，可能会支持非泛型的EventHandler，以及Func这样的委托类型，这里不考虑这种情形，因为从 Binlog 中获取的数据，基本上都是格式固定的 JSON。关于这部分，下面给出对应的定义：</description></item><item><title>利用 MySQL 的 Binlog 实现数据同步与订阅(上)：基础篇</title><link>https://blog.yuanpei.me/posts/1333693167/</link><pubDate>Tue, 07 Jul 2020 09:23:59 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1333693167/</guid><description>终于等到了周末，在经历了一周的忙碌后，终于可以利用空闲写篇博客。其实，博主有一点困惑，困惑于这个世界早已“堆积”起人类难以想象的“大”数据，而我们又好像执着于去“造”一个又一个“差不多”的“内容管理系统”，从前我们说互联网的精神是开放和分享，可不知从什么时候起，我们亲手打造了一个又一个的“信息孤岛”。而为了打通这些“关节”，就不得不去造一张巨大无比的蜘蛛网，你说这就是互联网的本质，对此我表示无法反驳。我更关心的是这其中最脆弱的部分，即：一条数据怎么从 A 系统流转到 B 系统。可能你会想到API或者ETL这样的关键词，而我今天想说的关键词则是Binlog。假如你经常需要让数据近乎实时地在两个系统间流转，那么你应该停下来听我——一个不甘心整天写CRUD换取996福报的程序员，讲讲如何通过Binlog实现数据同步和订阅的故事。
什么是 Binlog 首先，来回答第一个问题，什么是 Binlog？Binlog 即 Binary Log，是 MySQL 中的一种二进制日志文件。它可以记录MySQL内部对数据库的所有修改，故，设计 Binlog 最主要的目的是满足数据库主从复制和增量恢复的需要。对于主从复制，想必大家都耳熟能详呢，因为但凡提及数据库性能优化，大家首先想到的所谓的“读写分离”，而无论是物理层面的一主多从，还是架构层面的CQRS，这背后最大的功臣当属主从复制，而实现主从复制的更底层原因，则要从 Binlog 说起。而对于数据库恢复，身为互联网从业者，对于像“rm -f”和“删库”、“跑路”这些梗，更是喜闻乐见，比如像今年的绿盟删库事件，在数据被删除以后，工程师花了好几天时间去抢救数据，这其中就用到了 Binlog。
可能大家会好奇，为什么 Binlog 可以做到这些事情。其实，从 Binlog 的三种模式上，我们就可以窥其一二，它们分别是：Statement、Row、Mixed，其中Statement模式记录的是所有数据库操作对应的 SQL 语句，如 INSERT、UPDATE 、DELETE 等 DML 语句，CREATE 、DROP 、ALTER 等 DDL，所以，从理论上讲，只要按顺序执行这些 SQL 语句，就可以实现不同数据库间的数据复制。而Row模式更关心每一行的变更，这种在实际应用中会更普遍一点，因为有时候更关心数据的变化情况，例如一个订单被创建出来，司机通过 App 接收了某个运输任务等。而Mixed模式可以认为是Statement模式和Row模式的混合体，因为Statement模式和Row模式都有各自的不足，前者可能会导致数据不一致，而后者则会占用大量的存储空间。在实际使用中，我们往往会借助各种各样的工具，譬如官方自带的mysqlbinlog、支持 Binlog 解析的StreamSets等等。
好了，下面我们简单介绍下 Binlog 相关的知识点。在使用 Binlog 前，首先需要确认是否开启了 Binlog，此时，我们可以使用下面的命令：
SHOW VARIABLES LIKE &amp;#39;LOG_BIN&amp;#39; 如果可以看到下面的结果，则表示 Binlog 功能已开启。 Binlog已开启示意图 如果 Binlog 没有开启怎么办呢？此时，就需要我们手动来开启，为此我们需要修改 MySQL 的my.conf文件，通常情况下，该文件位于/etc/my.cnf路径，在[mysqld]下写入如下内容：
# 设置Binlog存储目录 log_bin = /var/lib/mysql/bin-log # 设置Binlog索引存储目录 log_bin_index = /var/lib/mysql/mysql-bin.</description></item><item><title>记一次从已损坏的 Git 仓库中找回代码的经历</title><link>https://blog.yuanpei.me/posts/686567367/</link><pubDate>Tue, 23 Jun 2020 17:08:17 +0000</pubDate><guid>https://blog.yuanpei.me/posts/686567367/</guid><description>突然发觉，古人其实特别有趣，譬如有古语云：『常在河边走，哪有不湿鞋』，实在是富有生活气息的一句俗语，可古人又有言语：『光脚的不怕穿鞋的』，更是朴实无华的一句话。上周下班适逢天降大雨，我撑伞送一位同事到地铁站，结果走到半路人家来一句，“你快点走吧，我穿着凉鞋”，一时竟无语凝噎。常在河边走，固然会有湿鞋的顾虑，可真正的气度绝不是光着脚满地跑，如何做到湿了鞋子而不慌呢？答案是脚上无凉鞋而心中有凉鞋。今天，我将为大家我在使用Git过程中如何“湿鞋”、如何不怕“湿鞋”的一个故事(逃
蓝屏重启后 Git 居然坏了 中国传统小说喜欢从神话讲起，端的是汪洋恣肆、纵横捭阖。而国外小说则喜欢从一片常青藤叶这种不显眼的事物写起，足可见二者见天地众生视角之不同。而我这个故事，是再普通不过的一次蓝屏。重启后 Visual Studio 提示恢复了未保存的代码，此时，我并未注意到 Git 仓库损坏的情况，就这样，我在一个“游离态”的版本上编写代码，直到我打开 SourceTree 的时候(作者注：我就是那个命令行和 GUI 混合使用的奇葩)，发现左侧本地分支全部消失，在命令行里git status，发现根本没有这个分支，而.git/refs/对应分支指向了一个错误的 Hash，我意识到我的 Git 仓库文件可能损坏了，这意味着我写的新 feature 可能丢失了，此时，Git 中提示的类似的错误信息：
$ error: refs/remotes/origin/HEAD: invalid sha1 pointer 0000000000000000000000000000000000000000 在此之前，其实博主已经经历过类似的事情，在没有未提交的代码的情况下，其实可以暴力删除. git目录，然后在git init即可，这相当于重新初始化仓库啦，在这种情况下，本地的分支会被删掉，你需要重新建新分支。可是这次不一样啊，在做的是一个即将发版的新 feature，不允许我出这样的选择啊！博主双掌合一，像夏洛克一样冷静思考，缓缓地在命令行下敲出git reflog，这条命令相当于你在 Git 中的监控日志，你对 Git 所做的一切都会成为呈堂证供。此时，你会得到下面的信息——沉默是今晚的康桥……
$ fatal: You are on a branch yet to be born 这是什么意思呢？意思就是这个分支还是一个“新生儿“的状态，新生儿怎么可能又活动记录呢？所以，使用 Git 的准则之一，只要仓库没有坏，通过git reflog找到对应的 Hash ，git checkout就可以找回代码，哪怕你刚刚手滑删除了一个未提交的分支，这种情况下都可以找回来。But 现在这种状况下，这条路显然是走不通啦。继续双掌合一，像夏洛克一样冷静思考，每个分支里其实是记录着一个 hash ，对应着最后的一次提交，现在是这个 hash 不对，那就要找到正确的 hash 啊。命令行已经非常明确地告诉你，是因为某些 object 丢失或者损坏了，那不妨先用git fsck试试。
$ git fsck notice: HEAD points to an unborn branch (master) Checking object directories: 100% (256/256), done.</description></item><item><title>.NET Core 原生 DI 扩展之属性注入实现</title><link>https://blog.yuanpei.me/posts/1658310834/</link><pubDate>Sat, 20 Jun 2020 13:10:31 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1658310834/</guid><description>在上一篇博客里，我们为.NET Core原生 DI 扩展了基于名称的注入功能。而今天，我们要来聊一聊属性注入。关于属性注入，历来争议不断，支持派认为，构造函数注入会让构造函数变得冗余，其立意点主要在代码的可读性。而反对派则认为，属性注入会让组件间的依赖关系变得模糊，其立意点主要在代码是否利于测试。我认识的一位前辈更是留下一句话：只要构造函数中超过 5 个以上的参数，我就觉得无法忍受。我个人是支持派，因为我写这篇博客的动机，正是一位朋友向我吐槽公司项目，说一个控制器里单单是构造函数里的参数就有十来个。在这其中最大的痛点是，有些在构造函数中注入的类型其实是重复的，譬如ILogger&amp;lt;&amp;gt;、IMapper、IRepository&amp;lt;&amp;gt;以及用户上下文信息等，虽然继承可以让痛苦减轻一点，可随之而来的就是冗长的 base 调用链。博主参与的项目里不乏有大量使用静态类、静态方法的，譬如 LogEx、UserContext 等等，可这种实践显然与依赖注入的思想背道而驰，为吾所不取也，这就是这篇博客产生的背景啦！
好了，当视角正式切入属性注入的时候，我们不妨先来考虑这样一件事情，即：当我们从容器里 Resolve 一个特定的类型的时候，这个实例到底是怎么被创建出来的呢？这个问题如果给到三年前的我，我会不假思索的说出两个字——反射。的确，这是最简单的一种实现方式，换句话说，首先，容器收集构造函数中的类型信息，并根据这些类型信息 Resolve 对应的实例；其次，这些实例最终会被放到一个object[]里，并作为参数传递给Activator.CreateInstance()方法。这是一个一般意义上的 Ioc 容器的工作机制。那么，相对应地，关于属性注入，我们可以认为容器 Reslove 一个特定类型的时候，这个类型提供了一个空的构造函数(这一点非常重要)，再创建完实例以后，再去 Reslove 这个类型中的字段或者是属性。所以，为了在微软自带的 DI 上实现属性注入，我们就必须实现自己的 ServiceProvider——AutowiredServiceProvider，这个 ServiceProvider 相比默认的 ServiceProvider 多了一部分功能，即反射属性或者字段的过程。一旦想通这一点，我们可以考虑装饰器模式。
public class AutowiredServiceProvider : IServiceProvider, ISupportRequiredService { private readonly IServiceProvider _serviceProvider; public AutowiredServiceProvider (IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public object GetRequiredService (Type serviceType) { return GetService (serviceType); } public object GetService (Type serviceType) { var instance = _serviceProvider.GetService (serviceType); Autowried (instance); return instance; } private void Autowried (object instance) { if (_serviceProvider == null || instance == null) return; var flags = BindingFlags.</description></item><item><title>.NET Core 原生 DI 扩展之基于名称的注入实现</title><link>https://blog.yuanpei.me/posts/1734098504/</link><pubDate>Wed, 10 Jun 2020 13:08:03 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1734098504/</guid><description>接触 .NET Core 有一段时间了，最大的感受无外乎无所不在的依赖注入，以及抽象化程度更高的全新框架设计。想起三年前 Peter 大神手写 IoC 容器时的惊艳，此时此刻，也许会有不一样的体会。的确，那个基于字典实现的 IoC 容器相当“简陋”，就像 .NET Core 里的依赖注入，默认(原生)都是采用构造函数注入的方式，可其实从整个依赖注入的理论上而言，属性注入和方法注入的方式，同样是依赖注入的实现方式啊。最近一位朋友找我讨论，.NET Core 里该如何实现 Autowried，这位朋友本身是 Java 出身，一番攀谈了解到原来是指属性注入啊。所以，我打算用两篇博客来聊聊 .NET Core 中的原生 DI 的扩展，而今天这篇，则单讲基于名称的注入的实现。
Autofac是一个非常不错的 IoC 容器，通常我们会使用它来替换微软内置的 IoC 容器。为什么要这样做呢？其实，微软在其官方文档中早已给出了说明，即微软内置的 IoC 容器实际上是不支持以下特性的： 属性注入、基于名称的注入、子容器、自定义生存期管理、对迟缓初始化的 Func 支持、基于约定的注册。这是我们为什么要替换微软内置的 IoC 容器的原因，除了 Autofac 以外，我们还可以考虑 Unity 、Castle 等容器，对我个人而言，其实最需要的一个功能是“扫描”，即它可以针对程序集中的组件或者服务进行自动注册。这个功能可以让人写起代码更省心一点，果然，人类的本质就是让自己变得更加懒惰呢。好了，话题拉回到本文主题，我们为什么需要基于名称的注入呢？它其实针对的是“同一个接口对应多种不同的实现”这种场景。
OK ，假设我们现在有一个接口 ISayHello，它对外提供一个方法 SayHello：
public interface ISayHello { string SayHello(string receiver); } 相对应地，我们有两个实现类，ChineseSayHello 和 EnglishSayHello：
//ChineseSayHello public class ChineseSayHello : ISayHello { public string SayHello(string receiver) { return $&amp;#34;你好，{receiver}&amp;#34;; } } //EnglishSayHello public class EnglishSayHello : ISayHello { public string SayHello(string receiver) { return $&amp;#34;Hello，{receiver}&amp;#34;; } } 接下来，一顿操作猛如虎：</description></item><item><title>原生 JavaScript 实现 Hexo 博客推荐功能</title><link>https://blog.yuanpei.me/posts/478946932/</link><pubDate>Mon, 08 Jun 2020 12:30:54 +0000</pubDate><guid>https://blog.yuanpei.me/posts/478946932/</guid><description>有时候，我不禁在想，我们到底处在一个什么样的时代呢？而之所以会有这样的疑问，则是因为我们的习惯在不断地被这个时代向前推进，就像我用了两年多的魅蓝 Note6 屏幕出现了问题，扫视了一圈新手机，居然再找不出一款带实体键的手机，刘海屏、水滴屏、破孔屏、异形屏、曲面屏等等简直令人眼花缭乱，唯独没有一款让我感到熟悉的非全面屏手机。做软件的时候，会不明白那些似是而非的定制需求的差异，可为什么偏偏到了硬件的时候，大家就能被迫适应这些越来越同质化的东西呢？也许有和我一样怀念非全面屏的人，可对于这个时代而言，一切都好像无足轻重，喜欢魅族对产品的设计，喜欢小而美的不妥协，可当大家都越来越相似的时候，也许，是因为我们终于都长大了吧，而怀念则是一种可有可无、甚至有一点多余的东西。在被告知一切向前看的路上，我们能拥有、用留住的东西本就不多，可偏偏我们就在给世间一切东西，努力刻上时间的温度，经历着花繁叶茂，经历着落叶归根。
写博客，曾经是件很有意思的事情，透过网页去读每条留言背后的人，常常令你产生神交已久的感觉，即便网络如此发达的今天，让一个人失散，无非是动动手指拉黑、删除。陈星汉先生有一款游戏作品叫做《风之旅人》，游戏里的玩家依靠某种微弱的信号相互联系，而一旦失散彼此，将永远迷失在浩瀚无际的沙海里，你说，这是不是有人生本身的意味在里面呢？再后来 140 个字符的微博开始流行，而这些沉迷在博客时代里的人们，或固执地继续在博客这一方天地里挥洒，或搭乘移动互联网的 “高铁” 通往新的彼岸。有人这样比喻朋友圈和微博，说朋友圈装饰别人梦境的月亮，而微博则是装饰自己梦境的镜子。其实呢，在隐私问题基本荡然无存的今天，我们都只是在装饰资本的 “窗户” 吧！
曾经运营过一段时间的微信公众号，最后发觉还是博客的载体更适合自己，虽然这些年没少为博客投入 “钱财”，在博客时代一去不复返的时间禁锢里，通过博客来盈利的想法堪堪聊以自慰，更不必说后来流行起来的 “在线教育” 和 Vlog。有人说，靠工资是没有办法挣到钱的，挣钱要靠这些 “睡后收入”，可当一件事物风头正盛的时候，彼时的你不足以追逐这一切的时候，这种感觉该如何言明呢？大概就像你在最落魄的时候，遇到一生中最想要保护的那个人一样，这听起来多少有点讽刺，人在不成熟的时候，总是后知后觉，可有一天真成熟了，再难有那时的运气或是豪气。所以呢，继续写下去吧，也许有一天，当你看着从前写的幼稚的文字，或哭或笑皆可入题，这不就是 “嬉笑怒骂，皆成文章” 了吗？
果然，一不小心又扯远了。虽然说博客平时没什么流量，可像搜索引擎优化(SEO)、前端构建(CI/CD)、PWA 等等这些东西倒是有所钻研，提高博客访问量的方式除了增加搜索引擎里的权重和曝光率以外，其实，还有一种方式就是减少跳出时间。换句话说，访客在你博客里停留的时间越长，这意味着你有更多的内容可以被对方访问到，所以，增加内链是一个不错的思路。最直接的方式，就是在每篇博客结束以后推荐相关的博客供访客继续阅读。之前曾经尝试过像 hexo-recommended-posts 这样的插件，坦白说效果不是特别好，因为有时候加载这些站外的内容，导致博客页面打开的时候异常卡顿，所以，我们今天将采用原生的 JavaScript 来为 Hexo 实现博客推理功能，希望对大家有所启发。
首先，我们来说说原理，推荐系统一般是需要一部分量化的指标来表征不同内容的相关性的。譬如通过 TF-IDF 来计算文本的相似度，通过公共词袋中的词频构造向量再配合余弦公式来计算，通过 TextRank 这类借鉴 PageRank 思想的方法来计算等等。这里呢，我们不采用这些方法来实现，主要是考虑到 200 篇左右的博客，两两计算相似度特别耗费时间，对于 Hexo 这种静态博客而言，我们还是应该节省生成静态页面的时间，虽然这部分时间都是 Travis CI 去跑的(逃……。我们采用的方案是基于标签和日期的推荐方式，即根据当前文章的标签筛选相同标签的文章，根据当前文章的日期筛选相同日期的文章。有了这两种策略，配合 Hexo 中提供的全局变量，我们可以很容易地编写出下面的代码：
&amp;lt;% function shuffle(a) { for (let i = a.length; i; i--) { let j = Math.floor(Math.random() * i); [a[i - 1], a[j]] = [a[j], a[i - 1]]; } return a; } function recommended_posts(page, site, limit = 5) { page.</description></item><item><title>使用 Dynamic Linq 构建动态 Lambda 表达式</title><link>https://blog.yuanpei.me/posts/118272597/</link><pubDate>Fri, 08 May 2020 12:27:11 +0000</pubDate><guid>https://blog.yuanpei.me/posts/118272597/</guid><description>相信大家都有这样一种感觉，Linq和Lambda是.NET 中一以贯之的存在，从最早的 Linq to Object 到 Linq to SQL，再到 EF/EF Core 甚至如今的.NET Core，我们可以看到Lambda表达式的身影出现地越来越频繁。虽然 Linq to Object 和 Linq to SQL，分别是以IEnumerable&amp;lt;T&amp;gt;和IQueryable &amp;lt;T&amp;gt;为基础来实现的。我个人以为，Lambda呢，其实就是匿名委托的“变种”，而Linq则是对Lambda的进一步封装。在System.Linq.Expressions命名空间下，提供大量关于表达式树的 API，而我们都知道，这些表达式树最终都会被编译为委托。所以，动态创建 Lambda 表达式，实际上就是指从一个字符串生成对应委托的过程，而一旦这个委托被生成，可以直接传递给 Where()方法作为参数，显然，它可以对源数据进行过滤，这正是我们想要的结果。
事出有因 在今天这篇博客中，我们主要介绍System.Linq.Dynamic.Core这个库，即我所说的 Dynamic Linq。本着“艺术源于生活的态度”，在介绍它的用法之前，不妨随博主一起看看，一个“简单“的查询是如何随着业务演进而变得越来越复杂。从某种意义上来说，正是它让博主想起了 Dynamic Linq。我们为客户编写了一个生成订单的接口，它从一张数据表中“消费”订单数据。最开始，它只需要过滤状态为“未处理”的记录，对应的 CRUD 可以表示为这样：
var orderInfos = repository.GetByQuery&amp;lt;tt_wg_order&amp;gt;(x =&amp;gt; x.STATUS == 10); 后来，因为业务方存在重复/错误下单的情况，业务数据有了“软删除”的状态，相应地查询条件再次发生变化，这看起来还行对吧：
var orderInfos = repository.GetByQuery&amp;lt;tt_wg_order&amp;gt;(x =&amp;gt; x.STATUS == 10 &amp;amp;&amp;amp; x.Isdelete == 0); 再后来，因为接口处理速度不理想，无法满足客户的使用场景，公司大佬们建议“加机器”，而为了让每台服务器上消费的订单数据不同(据说是为了避免发生并发)，大佬们要求博主开放所有字段作为查询条件，这样，每台服务器上可以配置不同查询条件。自此，又双叒叕改：
var repository = container.Resolve&amp;lt;CrudRepositoryBase&amp;gt;(); var searchParameters = new SearchParameters() { PageInfo = new PageInfo() { PageSize = parameters.</description></item><item><title>通过 EF/Dapper 扩展实现数据库审计功能</title><link>https://blog.yuanpei.me/posts/1289244227/</link><pubDate>Fri, 24 Apr 2020 08:20:32 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1289244227/</guid><description>相信大家都有过周末被电话“吵醒”的经历，这个时候，客服同事会火急火燎地告诉你，客户反馈生产环境上某某数据“异常”，然后你花费大量时间去排查这些错误数据，发现这是客户使用某一种“骚”操作搞出来的“人祸”。可更多的时候，你不会这么顺利，因为你缺乏有力的证据去支持你的结论。最终，你不情愿地去处理了这些错误数据。你开始反思，为什么没有一种流程去记录客户对数据的变更呢？为什么你总要花时间去和客户解释这些数据产生的原因呢？好了，这就要说到我们今天这篇博客的主题——审计。
什么是审计 结合本文引言中的描述的场景，当我们需要知道某条数据被什么人修改过的时候，或者是希望在数据变更的时候去通知某个人，亦或者是我们需要追溯一条数据的变更历史的时候，我们需要一种机制去记录数据表中的数据变更，这就是所谓的审计。而实际的业务中，可能会有类似，查询某一个员工一天内审批了多少单据的需求。你不要笑，人类常常如此无聊，就像我们有一个异常复杂的计费逻辑，虽然审计日志里记录了某个费用是怎么计算出来的，可花时间最多的地方，无一例外是需要开发去排查和解释的，对于这一点，我时常感觉疲于应对，这是我这篇文章里想要写审计的一个重要原因。
EF/EF Core 实体跟踪 EF 和 EF Core 里都提供了实体跟踪的功能，我的领导经常吐槽我，在操作数据库的时候，喜欢显式地调用repository.Update()方法，因为他觉得项目中的实体跟踪是默认打开的。可当你学习了Vue以后，你了解到Vue中是检测不到数组的某些变化的，所以，这个事情我持保留意见，显式调用就显式调用呗，万一哪天人家把实体跟踪给关闭了呢？不过，话说回来，实体跟踪确实可以帮我们做一点工作的，其中，就包括我们今天要说的审计功能。
EF 和 EF Core 中的实体追踪主要指 DbContext 类的 ChangeTracker，而通过 DetachChanges()方法，则可以获得那些变化了的实体的集合。所以，使用实体追踪来实现审计功能，本质上就是在 SaveChanges()方法调用前后，记录实体中每一个字段的变化情况。为此，我们考虑编写下面的类——AuditDbContextBase，顾名思义，这是一个审计相关的 DbContext 基类，所以，希望实现审计功能的 DbContext 都会继承这个类。这里，我们重写其 SaveChanges()方法，其基本定义如下：
public class AuditDbContextBase : DbContext, IAuditStorage { public DbSet&amp;lt;AuditLog&amp;gt; AuditLog { get; set; } public AuditDbContextBase(DbContextOptions options, AuditConfig auditConfig) : base(options) { } public virtual Task BeforeSaveChanges() { } public virtual Task AfterSaveChanges() { } public override async Task&amp;lt;int&amp;gt; SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { await BeforeSaveChanges(); var result = await base.</description></item><item><title>WebApiClient 中动态路由的实现与使用</title><link>https://blog.yuanpei.me/posts/2488769283/</link><pubDate>Thu, 02 Apr 2020 10:26:53 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2488769283/</guid><description>博主曾经在「声明式 RESTful 客户端 WebApiClient 在项目中的应用」这篇博客中，介绍过.NET 平台下的“Retrofit”——WebApiClient，它是一种声明式的 RESTful 客户端，通过动态代理来生成 Http 调用过程代码，而调用方只需要定义一个接口，并使用相关“注解”对接口进行修饰即可，类似的实现还有Refit，是一种比 HttpWebRequest、HttpClient 和 RestSharp 更为优雅的接口调用方式。在今天这篇博客中，我想聊聊 WebApiClient 中动态路由的实现与使用。
一个典型的 WebApiClient 使用流程如下，首先定义一个接口，并使用“注解”对接口进行修饰：
public interface ISinoiovApiClient : IHttpApiClient { /// &amp;lt;summary&amp;gt; /// 运单取消接口 /// &amp;lt;/summary&amp;gt; /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt; [HttpPost(&amp;#34;/yl/api/waybill/cancel&amp;#34;)] [AuthorizeFilter] [LoggingFilter] [JsonReturn] ITask&amp;lt;BaseApiResult&amp;lt;object&amp;gt;&amp;gt; CancelShipment([JsonContent]BaseShipmentDto shipment); } 接下来，调用就变得非常简单：
var config = new HttpApiConfig () { HttpHost = new Uri (baseUrl) }; using (var client = HttpApiClient.Create&amp;lt;ISinoiovApiClient&amp;gt; (config)) { var result = await client.CancelShipment (new BaseShipmentDto () { }); //TODO：TODO的意思就是永远都不做 } 有多简单呢？简单到调用的时候我们只需要给一个 baseUrl 就可以了！然而，如果你真这么想的话，就太天真了！虽然现在是一个遍地都是微服务和容器的时代，可是因为 RESTful 风格本身的约束力并不强，实际使用中难免会出现以下情况：</description></item><item><title>.NET Core + ELK 搭建可视化日志分析平台(上)</title><link>https://blog.yuanpei.me/posts/3687594958/</link><pubDate>Sat, 15 Feb 2020 16:01:13 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3687594958/</guid><description>Hi，各位朋友，大家好！欢迎大家关注我的博客，我的博客地址是: https://blog.yuanpei.me。今天是远程办公以来的第一个周末，虽然公司计划在远程两周后恢复正常办公，可面对着每天都有人离开的疫情，深知这一切都不会那么容易。窗外的阳光透过玻璃照射进屋子，这一切都昭示着春天的脚步渐渐近了。可春天来了，有的人却没有再回来。那些在 2019 年结束时许下的美好期待、豪言壮语，在这样一场灾难面前，终究是如此的无力而苍白。可不管怎么样，生活还是要继续，在这些无法出门的日子里，在这样一个印象深刻的春节长假里，除了做好勤洗手、多通风、戴口罩这些防疫保护措施以外，博主还是希望大家能够抽空学习，通过知识来充实这“枯燥&amp;quot;的生活。所以，从今天开始，我将为大家带来 .NET Core + ELK 搭建可视化日志分析平台 系列文章，希望大家喜欢。
什么是 ELK 当接触到一个新的事物的时候，我们最好是从它的概念开始入手。那么，什么是 ELK 呢？ELK，是 Elastaicsearch 、 Logstash 和 Kibana 三款软件的简称。其中，Elastaicsearch 是一个开源的全文搜索引擎。如果你没有听说过它，那至少应该听说过 Lucene 这个开源搜索引擎。事实上，Elastaicsearch 是 Lucene 的封装，它提供了 REST API 的操作接口 。而 Logstash 则是一个开源的数据收集引擎，具有实时的管道，它可以动态地将不同的数据源的数据统一起来。最后，Kibana 是一个日志可视化分析的平台，它提供了一系列日志分析的 Web 接口，可以使用它对日志进行高效地搜索、分析和可视化操作。至此，我们可以给 ELK 一个简单的定义：
ELK 是一个集日志收集、搜索、日志聚合和日志分析于一身的完整解决方案。
下面这张图，展示了 Elastaicsearch 、 Logstash 和 Kibana 三款软件间的协作关系。可以注意到，Logstash 负责从应用服务器收集日志。我们知道，现在的应用程序都是跨端应用，程序可能运行在 PC、移动端、H5、小程序等等各种各样的终端上，而 Logstash 则可以将这些不同的日志信息通过管道转换为统一的数据接口。这些日志将被存储到 Elasticsearch 中。我们提到 Elastaicsearch 是一个开源的全文搜索引擎，故而它在数据查询上相对传统的数据库有着更好的优势，并且 Elasticsearch 可以根据需要搭建单机或者集群。最终，Kibana 从 Elasticsearch 中查询数据并绘制可视化图表，并展示在浏览器中。在最新的 ELK 架构中，新增了FireBeat这个软件，它是它是一个轻量级的日志收集处理工具(Agent)，适合于在各个服务器上搜集日志后传输给 Logstash。
ELK-01.png 总而言之，ELK 可以让我们以一种更优雅的方式来收集日志，传统的日志收集通常会把日志写到文件或者数据库中。前者，不利于日志的集中管理和查询；后者，则无法应对海量文本检索的需求。所以，使用 ELK 可以为我们带来下面这些便利：分布式日志数据集中式查询和管理；系统监控，譬如对系统硬件和应用各个组件的监控；故障排查；报表功能；日志查询，问题排查，上线检查； 服务器监控、应用监控、错误报警；性能分析、用户行为分析、时间管理等等。
如何安装 ELK 安装 ELK 的方式，首推以 Docker 方式安装。关于 Docker 的安装、使用请大家查阅官方文档：https://docs.</description></item><item><title>使用 jsDelivr 为 Hexo 博客提供高效免费的CDN加速</title><link>https://blog.yuanpei.me/posts/1417719502/</link><pubDate>Wed, 05 Feb 2020 19:01:00 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1417719502/</guid><description>最近给博客做了升级，从 3.x 升级到了 4.x，主要是在官网看到了关于静态页面生成效率提升的内容。众所周知，Hexo 在文章数目增加以后会越来越慢。博主大概是从 14 年年底开始使用 Hexo 这个静态博客的，截止到目前一共有 176 篇博客，其中的“慢”可想而知，中间甚至动过使用 Hugo 和 VuePress 的念头，所以，听说有性能方面的提升，还是决定第一时间来试试。整个升级过程挺顺利的，唯一遇到的问题是关于外部链接检测方面的，具体可以参考这里。今天，博主主要想和大家分享下关于如何使用jsDelivr来为博客提供免费、高效的 CDN 服务，希望对大家有所帮助。
jsDelivr是一个免费、快速和可信赖的 CDN 加速服务，官网上声称它每个月可以支撑680亿次的请求。博主是在去年年底的时候，偶然了解到这个服务的存在，这次趁着疫情肆虐的间隙，终于把这个服务集成到了博客中。更重要的是，这个服务在 Github 上是开源的。目前，它提供了针对npm、Github和WordPress的加速服务，只需要一行代码就可以获得加速效果，以常用的jQuery和Bootstrap为例：
// load jQuery v3.2.1 https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js // load bootstrap v4.4.1 https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/js/bootstrap.js 这意味着我们只需要发布一个 npm 的包，就可以使用它提供的加速服务。CDN 加速的好处我这里就不再多说了，只要我们的项目中用到了第三方的静态资源，譬如 JavaScript/CSS 等等都应该考虑接入到 CDN 中。有人常常担心 CDN 挂掉或者是私有化部署无法接入外网环境。我想说，我们目光应该长远一点，现在早已不是早年那种单打独斗式的开发模式了，我们不可能把所有资源都放到本地来。随着云计算的概念越发地深入人心，越来越多的基础服务都运行在一台又一台虚拟化的“云服务器”上，这种情况下，搞这种集中化配置的做法，是完全违背分布式的发展趋势的。
如果说，针对 npm 包的 CDN 加速服务离我们还有点遥远，因为我们大多数情况下都是在使用别人写好的库。那么，接下来，针对 Github 的 CDN 加速服务应该会让我们无比兴奋吧，毕竟 Github Pages 的“慢”大家是可以感受得到的。不然，为什么大家要用 Coding Pages 做国内/国外的双线部署呢？首先，我们在浏览器里输入下面这个地址：https://cdn.jsdelivr.net/gh/qinyuanpei/qinyuanpei.github.io@latest/
jsDelivr提供的CDN加速资源 此时，可以注意到，jsDelivr可以把我们 Github 上的资源呈现出来，只要我们在 Github 上发布过相应的版本即可。这里的版本，可以理解为一次 Release，对应 Git 中 tag 的概念，虽然 Github 现在引入了包管理器的概念，试图统一像 npm、nuget、pip 等等这样的包管理器。它提供的 CDN 服务有一个基本的格式：</description></item><item><title>从 .NET Core 2.2 升级到 3.1 的踩坑之旅</title><link>https://blog.yuanpei.me/posts/3099575458/</link><pubDate>Wed, 22 Jan 2020 10:23:08 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3099575458/</guid><description>有时候，版本更新太快并不是一件好事。虽然，两周一个迭代的“敏捷”开发依然被客户嫌弃交付缓慢，可一边是前端领域“求不要再更新了，学不动了”的声音，一边则是.NET Core从1.x到2.x再到3.x的高歌猛进。版本更新太快，带来的是API的频繁变动，无法形成有效的知识沉淀，就像转眼到了2020年，Python 2.x和Windows 7都引来了“寿终正寝”，可能你都还没有认真地学习过这些知识，突然就被告知这些知识要过期了，想想还是觉得挺疯狂啊。最近一直在捣鼓，如何让.NET Core应用跑在Heroku平台上，因为Docker镜像里使用最新的.NET Core 3.1运行时，所以，痛定思痛之余，决定把手头项目升级到3.1。上一次痛苦还是在2.1升级2.2，这还真没过多长时间。所以呢，这篇博客主要梳理下从2.2升级到3.1过程中遇到的问题。
更新项目文件 调整目标框架为netcoreapp3.1 删除引用项：Microsoft.AspNetCore.App、Microsoft.AspNetCore.Razor.Design 删除AspNetCoreHostingModel，如果项目文件中的值为InProcess(因为ASP.NET Core 3.0 或更高版本项目默认为进程内承载模型） 更新程序入口 CreateWebHostBuilder()方法的返回值类型由IWebHostBuilder调整为IHostBuilder 增加引用项：Microsoft.Extensions.Hosting Kestrel配置变更至ConfigureWebHostDefaults()方法 public static IHostBuilder CreateWebHostBuilder(string[] args) =&amp;gt; Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder =&amp;gt; { webBuilder.ConfigureKestrel(serverOptions =&amp;gt; { // Set properties and call methods on options }) .UseStartup&amp;lt;Startup&amp;gt;(); }); 如果通过 HostBuilder手动创建宿主，则需要在 ConfigureWebHostDefaults()方法中显式调用·UseKestrel()：
public static void Main (string[] args) { var host = new HostBuilder () .UseContentRoot (Directory.GetCurrentDirectory ()) .ConfigureWebHostDefaults (webBuilder =&amp;gt; { webBuilder.UseKestrel (serverOptions =&amp;gt; { // Set properties and call methods on options }) .</description></item><item><title>不知老之将至</title><link>https://blog.yuanpei.me/posts/888549816/</link><pubDate>Wed, 01 Jan 2020 08:46:24 +0000</pubDate><guid>https://blog.yuanpei.me/posts/888549816/</guid><description>我以为，时间是这个世界上最残忍的存在。因为，无论如何，你都无法阻止这如齿轮般互相咬合的时光机器，即使这世界上并没有所谓的“永动机”。习惯于沉默的时间之轮，你在或者不在，丝毫不影响它衡量宇宙万物的尺度。也许，是因为我们所拥有的时间太过短暂，所以，当一切都流失殆尽时，我们所能寄托的便只有不那么确定的未来。时间怎么会变得残忍呢？它无喜无悲俯视众生，倒像是一位入定参禅的老僧，有情感的分明是我们这些人类啊。
孔夫子说：发奋忘食，乐以忘忧，不知老之将至。而有时候，你甚至都没有怎么“发奋”、“快乐”，就不知老之将至了。也许，花了不少时间在工作甚至加班上面，如果这些可以算作“发奋”，老之将至才是符合人类生理趋势的必然。上个周末去看了《叶问 4》的完结篇，突然发现，无论是戏里的叶师傅，还是戏外的甄子丹，居然都出现衰老的迹象。而童年记忆中的黄飞鸿则永远是白鹤亮翅的宗师气象，大概是因为《黄飞鸿》系列不曾像《叶问》系列，在功夫片的体裁外，多了一点传记电影的味道。有人说，这是华语功夫片的一次谢幕，而我更愿意理解为，这是演员同过去的自己的阶段性告别。人总是会老的，从公交车上为老人让座的宣传广播，到父母见一次就白一次的鬓角，再到一天比一天翻得飞快的日历……你，又是如何同过去的自己告别的呢？
Flag 这种东西，是一种不立没有所谓“仪式感”，而立了又难免让你自愧虚度时光的存在。在过去的一年里，索性一个 Flag 都不立，这样看过来的时候，人生充满了一种荒芜感：微信公众号运营失败，因为缺少那个想让你运营下去的观众；博客写作无功无过，每月 1 至 2 篇文章，作为阶段性的回顾尚可；懒散/拖延症中晚期，此时此刻还有来自 2019 年的 Todo；通过微软小英练习单词和口语，这一点没能坚持下来，更不必说连 50 音图都没学会的日语；没有被消费主义洗脑，量入为出、精简开支(穷得如此清新脱俗)；一个人做饭没怎么坚持下来，单单是准备食材就挺麻烦了，更何况炒菜锅坏了一直没换新的呢；工作快 5 年了，我还是没太大长进，还是喜欢怼人怼空气，沟通能力是挺重要了，可惜精力都被开会、扯皮这种事情消耗得差不多了啊；阅读量还是太少，从公司/图书馆借来的书，一般都能找时间去读，而下载下来放 Kindle 里的，读着读着就被遗忘了，订阅的 RSS 读起来倒没有这种压力，果然“书非借不能读也”。《一代宗师》里说，人活得是一个起伏，而我这一年是没能活成一杯烈酒的。人喜欢用平凡是真自我安慰，可都怕活成最平庸的样子，用天哥的话说，做人没意思啊！
醒来的时候和往常一样，一样到和平时上班没什么区别，直到我坐上公交车，惊诧于路上行人为何如此稀少时，我突然意识到，原来今天是 2020 年的第一天啊，原来 2019 年就这样失去了啊，原来今天元旦放假啊……习惯其实是件可怕的事情，我妈和我说，是我工作太认真了，确切地说，来到这家新公司后，太多的习惯都被改变了，譬如 Deadline 驱动开发而导致的加班，譬如身为乙方这个弱势群体的被动，譬如周末一样要被同事电话打扰的无力感……互联网在深入到这个世界的各个角落的同时，互联网从业者的生存环境反倒更加举步维艰，资本家们鼓吹 996 是一种福报，某企业用 251 来对待离职的员工，因为加班而过劳死留下孤儿遗孀的软通员工，因为被裁员而无力维持生活选择跳楼的员工……
詹青云在《奇葩说》里的一段话令我印象深刻，她说，整个社会都在选择性忽视对与错的问题，仅仅是因为这样子做更划算些，一群活生生的人就被当做冰冷的数字一样计算。《红楼梦》里说，“机关算尽太聪明，反误了卿卿性命”，一个大家都互相算计的世界是绝望的，而这种“划算”的想法有一天变成主流则是可怕的。有好几次工作到深夜凌晨，回到家困到直接穿着衣服睡着的我，恍惚中应该会同我的灵魂对话：到底是一件多么惊天动地的事情，需要我连命都不要地熬到这个点。对企业对说，它需要的是“划算”的员工。而对员工来说，生命比一切都重要。即使为社会这部大机器而殚精竭虑甚至牺牲生命，这部如永动机一般的大机器依旧不会停止，我们不需要去追赶整个社会的效率。如果追赶会有什么下场呢？卓别林的《摩登时代》已经告诉过你答案。
可笑的是，人类能接受同类所指定规则，唯独要抗衡比人类更高层次的自然规律。你、我，这个世界上的每一个人都会死，这是所有人都无法逃脱的自然规律，即使是同为人类的医生一样会死，难道医生都是神灵或者天使吗？《白色巨塔》中的財前医生医术精湛，可当面对身患癌症的自己时，一样回天乏术。医学的发展自始至终都是建立在死亡上的，我们不能在享受医学带来的好处的同时，仅仅因为那个人是你或我的亲人，就去伤害这些医疗工作者，因为他们和我们一样，都是普普通通的人，他们唯一比我们多的就是医术，可医术甚至于这世界上一切人类发明的东西，都不是万能的啊。
伤害别人，永远无法弥补我们对逝者的愧疚。生命原本就如此脆弱，如果身为医生而没能抢救过来自己的亲人，按照这套“划算”但不“正确”的理论，那么医生是不是应该选择自杀？我说，这个问题根本不需要多想，因为逝者已逝，让更多的人活下来，九泉之下有知的逝者或许会感到欣慰吧……如果你相信人死后灵魂会得到转世，那么，让逝者的生命从下一个新生命中得到延续不好吗？我们这个世界有一种病态的观念，对待客户要毕恭毕敬，对待患者要高风亮节，可如果有一天这些人要对你做出过分的事情，难道你还要一忍再忍吗？
人有时候会刻意拉大时空的疏离感，就像我第一次看《叶问》还是在同学的 MP4 上，我甚至都没有看过《叶问 1》里“我要打 10 个”的名场面，因为第一次看《叶问》的时候，叶师傅已经在大圆桌上同洪师傅切磋武艺了。可当你回首时，时间已经过去 10 年啦，虽然在这 10 年里，罗师傅的武功一直没什么长进，而叶师傅的对手则一直在变强。翻过年以后，我就 28 岁啦，如果回头看我的 10 年，时间大概一样会变得空泛，因为有的人来来回回地从你生命里来了又去，而有的人甚至从未真正进入过你生命里。当时过境迁，你唯一能留下的就只有自己，我虽庆幸见证过那些花儿的开放，可那些花儿终究不是我的。也许，她们和我一样都渐渐老去了吧，听起来有些矫情对不对？其实，昨天和这些年里的每一天没有什么不同，甚至还要更普通些，因为我又没能控制住情绪发了火，记忆啊，终究带着些美化的滤镜……</description></item><item><title>使用 Liquid 实现简单的数据交换</title><link>https://blog.yuanpei.me/posts/3742212493/</link><pubDate>Sun, 22 Dec 2019 09:36:42 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3742212493/</guid><description>在平时的开发工作中，接口对接是一件无可避免的事情。虽然在“前后端分离”的大趋势下，后端的角色逐渐转换为数据接口的提供者，然而在实际的应用场景中，我们面对的往往是各种不同的“数据”，譬如企业应用中普遍使用的企业服务总线(ESB)，这类服务要求服务接入者必须使用 WebService 来作为数据交换格式；再譬如电子数据交换(EDI)这种特定行业中使用的数据交换格式，从可读性上甚至还不如基于 XML 的 WebService……而更为普遍的则可能是需要使用 Word、Excel、CSV 来作为数据交换的媒介。顺着这个思路继续发散下去，进入我们失业的或许还有各种数据库，譬如 MySQL 和 MongoDB；各种大数据平台，譬如 Hadoop 和 Spark；各种消息队列，譬如 RabbitMQ 和 Kafka 等等。
注意到，这里反复提到的一个概念是数据交换(Data Switching)，它是指在多个数据终端设备间，为任意两个终端设备建立数据通信临时互联通路的过程。自从阿里提出“中台”的概念以来，越来越多的公司开始跟风“中台”概念，并随之衍生出譬如组织中台、数据中台、业务中台、内容中台等等的概念。今天这篇博客，我并不打算故弄玄虚地扯这些概念，我的落脚点是接口级别的数据交换，主要通过 Liquid 这款模板引擎来实现。它对应我在这篇博客开头提到的场景：一个对外提供 RESful 风格 API 的系统，如何快速地和一个 WebService 实现对接。总而言之，希望能对这篇博客对大家有所启发吧！
关于 Liquid 首先，我们来介绍Liquid，通过它的官方网站，我们应该它是一门模板语言。对于模板语言，我们应该是非常熟悉啦，JavaScript 里的Handlebars和Ejs就是非常著名的模板语言。如大家所见，这个博客就是用 Ejs 模板渲染出来的。而到了三大前端框架并驾齐驱的时代，模版语法依然被保留了下来，比如 Vue 中 {% raw %}{{model.userName}}{% endraw %} 标记常常用来做文本插值。所以，如果要认真追溯起来的话，也许这些框架都或多或少的收到了 Liquid 的影响，因为它的基本语法如下：
// 使用page实例的title属性插值 {{ page.title}} 假设 page 是一个对象，它的 title 属性值为：Introduction，此时，渲染后的结果即为：Introduction。是不是感觉非常简单呢? 我们继续往下看。除了基本的“插值”语法以外，我们可以用 {% raw %}{% tag %}{% endraw %} 这种结构(Liquid 称之为 Tag)：
// 声称变量author并赋值 {% sssign author = &amp;#39;猫先森&amp;#39; %} // 条件语句 {% if author == &amp;#39;猫先森&amp;#39; %} 帅哥，你好 {% endif %} // 循环语句 {% for post in posts %} {{post.</description></item><item><title>Referrer 还是 Referer? 一个迷人的错误</title><link>https://blog.yuanpei.me/posts/2015300310/</link><pubDate>Wed, 04 Dec 2019 17:22:33 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2015300310/</guid><description>诗人郑愁予曾经在一首诗中写道：我达达的马蹄是个美丽的错误，我不是归人，是个过客。而对我来说，十九岁之前的我，一样是个沉浸在诗歌中的文艺少年。十九岁之后的我，作为一名程序员，更多的是邂逅各种错误。可偏偏人类世界对待错误从来都不宽容，所以，错误本身既不美丽，亦不浪漫。接近中年的我，无论如何，都写不出年轻时令人惊艳的句子，这或许和我们面对错误时的不同心境，有着莫大的关联，而今天这篇博客，同样要从一个历史上的错误说起。
因拼写而怀疑人生 话说，博主这天做了一个非常“简单”的功能，它允许用户通过富文本编辑器来编写 HTML，而这些 HTML 会被插入到页面的特定位置，譬如用户可以为页脚的备案号添加一个超链接，当用户点击备案号的时候，就可以调转到工信部备案号查询的网站上。这个功能非常简单吧，因为这就是 HTML 中 a 标签的作用。博主快速了引入 UEditor，虽然这个项目百度都不再继续维护了，虽然它直接把跨域问题甩锅给使用者，可我还是完成了这个功能。相信你能感受到我的不情愿吧，显然这不是重点，因为剧情的反转才是……
结果没高兴多久，测试同事就同我讲，客户提供的地址填进去以后，点击链接浏览器直接返回 4XX，可明明这个地址敲到浏览器里就能打开啊……我脑海中快速地浮现出那道经典的面试题，浏览器里敲完地址按下回车的瞬间到底发生了什么？习惯性怀疑人生后，我发现居然是因为 Referer 的问题，从我们站点调转到客户站点的时候携带了 Referer，虽然有很多种方法可以让浏览器禁止携带 Referer，但我还是被这种历史性的错误搞得怀疑人生。因为人生最难的事情，就是“揣着明白装糊涂”和“揣着糊涂装明白”，所谓“假作真时真亦假”。
请注意区分Referer和Referrer这两个单词，眼尖的人会发现后者多了一个 r，这有点像什么呢，大概类似于 usr 和 user。我们总是不情愿地相信这是历史的错误，而固执地想要找到一种能自圆其说的理由。诚然，“前人栽树，后人乘凉”，可我实在不肯承认，这是一群卓越而智慧的先驱们，所创造出的某种高效简写。回顾一下，使用 Referer 的场合，基本都是在 HTTP 头部，最常见的场景就是防盗链，Nginx 能用 Referer 判断访问者来源，爬虫就能用 Referer 和 UserAgent 伪造访问者身份。那什么时候用 Referrer 呢？我目前发现是在 a 标签的 rel 属性里，例如下面的例子：
&amp;lt;a rel=&amp;#34;noreferrer&amp;#34; href=&amp;#34;https://www.w3school.com.cn/tags/att_a_rel.asp&amp;#34;&amp;gt;w3school&amp;lt;/a&amp;gt; 除此之外，rel 属性还支持像 nofollow、friend、licence 这样的属性，详细地大家可以参考这里。相信大家想到博主经历了什么了，没错，我就是按照平时的书写习惯写了 Referer，然后被 Web 标准委员会给疯狂地嘲讽了。那么，为什么表达同一个含义的词会有两种写法？为什么有时候要用 Referer，而有时候要用 Referrer? 这特么到底是怎么一回事儿……带着这些疑问，让我们一起回顾野蛮生长的 Web 标准，为什么要埋这样一个坑在这里。
后世不忘，前世之锅 故事要追溯到上个世纪 90 年代，当时 HTTP 协议中需要有一个用来表示页面或资源来源的请求头部，Philip Hallam-Baker 将这个请求头部定义为 Referer，并将其写入了RFC1945，这就是著名的 HTTP/1.0 协议。
HTTP/1.0协议中定义的Referer 然而这里发生一件有趣的事情，这个单词实际上是被作者给拼错了，即正确的拼写应该是Referrer。因为发现这个错误时为时已晚，大量的服务端和客户端都采用了这个错误的拼写，谁让它被写到了 HTTP 协议里呢？这其中就有像 Nginx 里的ngx_http_referer_module、Django 里的HttpRequest.</description></item><item><title>关于单位转换相关问题的常见思路</title><link>https://blog.yuanpei.me/posts/2318173297/</link><pubDate>Fri, 15 Nov 2019 09:43:54 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2318173297/</guid><description>请原谅我使用了这样一个“直白”的标题，因为我实在想不到更好的描述方法。或许，是因为临近年底的“996”式冲刺，让许久没有读完一本书的我，第一次感受到输出时的闭塞。是时候为自己的知识体系补充新鲜血液啦，而不是输给那些“无聊”的流程和关系。说这句话的缘由，是想到《Unnatural》中的法医三澄美琴，一个视非正常死亡为敌人的女法医。而对程序员来说，真正的敌人则是难以解决 Bug 和问题。可更多的时间，我们其实是在为流程和关系方面的事情消耗精力。
我越来越发现，人类所面对的绝大多数问题，都并非是寻求一个最优解，而是在于平衡和牵制。人类总是不可避免地堕入熵增的圈套，伴随着流程产生的除了规范还有复杂度。每当人们试图为这种复杂度找一种友好的说辞的时候，我终于意识到，有的人不愿意去寻找问题的本质，它们需要的就只是一种友好的说辞，仿佛只要有了这种说辞，问题就能自动解决一样。我想，我大概知道这段时间感到焦灼的原因了，因为这样的事情在工作中基本是常态。人类每天面对的事情，无外乎两种：&amp;ldquo;明知不可为而为之&amp;quot;和&amp;quot;什么都想兼顾的美好理想&amp;rdquo;。
我今天想说的是，一个业务中遇到的单位转换的问题，我们平时在存储货物的重量时，默认都是以千克作为单位来存储的，直到我们对接了一家以大宗商品交易作为主要业务的客户，对方要求我们在界面上统一用吨来展示数据，因为这样更符合客户方的使用习惯。按理说，这是一个非常简单的需求，是不需要用一篇博客来说这件事情的，可我觉得这是个有意思的话题，还是想和大家一起来聊聊相关方案的思路。带着问题，我首先拜访了Cather Wong大佬，大佬微微一笑，表示在视图层上加个字段就可以了嘛。的确，这是最简单的做法，大概是下面这个样子：
class OrderInfoQueryDTO { /// &amp;lt;summary&amp;gt; /// 以千克为单位的净重 /// &amp;lt;/summary&amp;gt; public decimal? NET_WEIGHT { get; set; } /// &amp;lt;summary&amp;gt; /// 以吨为单位的净重 /// &amp;lt;/summary&amp;gt; public decimal? NET_WEIGHT_WITH_TON { get { return NET_WEIGHT / 1000; } } } 我不甘心地追问，客户要在原来的字段上显示这个数值啊，这样能行吗？大佬稍作沉思，随即问道：“你们公司的项目就算做不到 DDD，AutoMapper 这种实体间映射转换的东西总有吧！”。我连忙接话道：“这个自然是有的”。其实我心里想的是，总算有点符合我的心理预期啦，这样的方案还像个大佬的样子。按照大佬的提示，使用 AutoMapper 来做单位的转换，应该是下面这样：
var config = new MapperConfiguration(cfg =&amp;gt; { cfg.CreateMap&amp;lt;order_info, OrderInfoQueryDTO&amp;gt;() .ForMember(d =&amp;gt; d.NET_WEIGHT, opt =&amp;gt; opt.MapFrom(x =&amp;gt; x.NET_WEIGHT/1000)); }); 这样看起来是比加字段要好一点，可实际项目中，我们往往会把单位作为一种配置持久化到数据库中，以我们公司为例，我们实际上是支持千克和吨两种单位混合使用的，不过在表头汇总的时候，为了统一到一起，所以使用了千克作为单位。这样就引申出一个新问题，假如我在数据库里存了多行明细的重量，当需要在表头展示汇总以后的总重量，那么，这个总重量到底是汇总好存在数据库里，还是展示的时候交由调用方 Sum()呢？
我个人倾向于第二种，因为它能有效避免表头和明细行数据不一致的问题，当然缺点是给了调用方一定的计算压力。我们项目中采用的第一种方案，我印象非常深刻，在计算件数、重量和体积的时候，必须要等所有明细行都计算完以后，再通过调用 Sum()方法给表头赋值，实际上这个表头字段，完全可以通过只读属性的方式取值啊，更何况我们还使用了外键，表头实体本身就引用了明细表实体，因为有外键的存在，序列化表头实体的时候会出现循环引用，对此，我想说，干得漂亮！
通过 AutoMapper 中的 ForMember 扩展方法，可以实现我们这里这个功能。可考虑到要在 AutoMapper 里引入权限啊、角色啊这些东西，AutoMapper 作为实体映射的纯粹性就被彻底破坏了。为此，我们考虑使用 AutoMapper 中提供的Value Converters和Type Converters。关于这两者的区别，大家可以参考官方文档中的描述。此时，我们可以通过下面的方式使用这些“转换器”：</description></item><item><title>Valine 搭配 Server 酱实现博客评论推送</title><link>https://blog.yuanpei.me/posts/369095810/</link><pubDate>Wed, 06 Nov 2019 18:15:14 +0000</pubDate><guid>https://blog.yuanpei.me/posts/369095810/</guid><description>Valine是一个基于LeanCloud的评论系统，在很长的一段时间里，一直作为多说、Gitalk、Gitment等等的一个替代品，博主所使用的评论系统实际上就是 Valine，虽然独立博客的整体活跃度无法媲美专业博客，可还是想在这纷扰的世界里有自己的一方天地啊。多说评论的关闭，某种意义上来说，是很多 90 后站长们关于互联网的集体记忆，因为从博主搭建第一个 WordPress 博客的时候，多说就一直作为首选的评论系统而存在。那个时候通过多说就能接入主流的社交媒体，对于一个还不大会编写 Web 应用的博主来说，此刻想来实在是有种时过境迁的感觉。所以，Valine 作为一个相当优秀的评论系统，凭借着简洁大方的界面和开箱即用的优势，在这个时间点进入人们的视野，我们就不难理解，为什么它会成为博客作者们的“新宠”。
Valine 本身是利用 LeanCloud 的数据存储 SDK 来实现评论的读写的，所以，它相对于“多说”这种第三方的服务，在数据安全性上更有保障一点，虽然“多说”在关闭评论服务以前，提供了导出 JSON 格式评论信息的功能。可话说回来，以国内这种“敏感”的网络环境，其实没有一家云服务提供商敢打这样的包票，像阿里云、LeanCloud、七牛云存储这些服务，都曾经出现过宕机或者封杀域名的事情，所以，趁着数据还在自己手上，尽可能地做好备份工作吧！Valine 本身并没有提供评论推送的功能，我还是挺怀念过去“多说”推送评论到邮箱的功能。虽然Valine-Admin这个项目提供了类似的功能，但我感觉使用起来并不顺手，尤其是配置邮箱的时候，国内像 QQ、163 这些都非常麻烦，遇到一两个废弃的手机号，你就会发现短信验证码，是件多么尴尬而繁琐的事情，如同扫码使用的共享电话一般魔幻。
为了解决这个问题，我想到了 Valine 搭配 Server 酱实现评论推送的方案。首先，Valine 是基于 LeanCloud 而开发的，用户发表评论实际上就是向Comment表插入记录。因此，我们可以利用 LeanCloud 提供的Hooks来捕获写入评论的事件。所谓“Hooks”呢，通俗地说就是数据库里触发器的概念，我们可以在数据写入前后做点“小动作”。而Server 酱则是一个消息推送服务，它提供了一个基于 HTTP 的请求接口，通过这个接口，我们就能实现向微信推送消息，前提是关注“方糖”公众号。关于 Server 酱的原理大家可以进一步去看它的文档，我们这里只需要考虑怎么样把它们结合起来，这就是工程师和科学家的区别所在[doge]。
运行在Valine云引擎中代码 LeanCloud 提供了一个称作“云引擎”的环境，它可以提供运行比如 Nodejs、Python 等等的环境，实际上，Valine-Admin这个项目就是用 Nodejs 编写的，你可以理解为，只要你的应用符合它的规范，就能在它的环境里运行，这就是所谓的“FAAS”(函数即软件)和“BAAS”(后端即软件)。所以，说白了我们就是想利用它这个“云引擎”来调用 Server 酱的接口，幸运的是，LeanCloud 提供的 Hooks 目前是支持 Nodejs 的，所以，到这里思路就非常清晰了，我们给Comment这张表加一个AfterSave类型的 Hooks，在保存完以后调用 Server 酱接口推送评论信息即可。创建 Hooks 是在部署-&amp;gt;云引擎选项下，我们来看下面的代码：
AV.Cloud.afterSave(&amp;#39;Comment&amp;#39;, async function(request) { var http = require(&amp;#34;request&amp;#34;); var obj = request.object; console.log(&amp;#39;收到一条新的评论：&amp;#39; + JSON.stringify(obj)); var title = &amp;#34;收到一条新的评论&amp;#34;; var url = request.</description></item><item><title>浅析网站 PV/UV 统计系统的原理及其设计</title><link>https://blog.yuanpei.me/posts/3494408209/</link><pubDate>Tue, 22 Oct 2019 12:50:49 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3494408209/</guid><description>国庆节前有段时间，新浪的“图床”一直不大稳定，因为新浪开启了防盗链，果然免费的永远是最贵的啊。为了不影响使用，我非常粗暴地禁止了浏览器发送 Referer，然后我就发现了一件尴尬的事情，“不蒜子”统计服务无法使用了。这是一件用脚后跟想都能想明白的事情，我禁止了浏览器发送 Referer，而“不蒜子”正好使用 Referer 来识别每个页面，所以，这是一个再明显不过的因为需求变更而引入的 Bug。这个世界最离谱的事情，就是大家都认为程序员是一本“十万个为什么”，每次一出问题就找到程序员这里。其实，程序员是再普通不过的芸芸众生里的一员，人们喜欢听/看到自己愿意去听/看到的事物，而程序员同样喜欢解决自己想去解决的问题。所以，今天的话题是关于如何设计一个 PV/UV 统计系统。OK，Let&amp;rsquo;s Hacking Begin。
PV/UV 的概念 首先，我们从两个最基本的概念 PV 和 UV 开始说起。我们都知道，互联网产品的核心就是流量，前期通过免费的产品吸引目标客户的目的，在积累了一定用户流量以后，再通过广告等增值服务实现盈利，这可以说是互联网产品的典型商业模式啦。而在这个过程中，为了对一个产品的流量进行科学地分析，就产生了譬如访客数(UV)、浏览量(PV)、访问次数(VV)等等的概念，这些概念通常作为衡量流量多少的指标。除此以外，我们还有类似日活跃用户(DAU)、月活跃用户(MAU)等等这种衡量服务用户粘性的指标，以及平均访问深度、平均访问时间、跳出率等等这种衡量流量质量优劣的指标。如果各位和我一样都写博客的话，对这些概念应该都不会感到陌生，因为我们多多少少会使用到诸如百度站长、站长统计、腾讯统计、Google Analytics这样的统计服务，这些统计服务可以让我们即时掌握博客的访问情况。博主目前使用了腾讯统计来查看整个博客的流量情况，而每一篇博客的访问量则是通过**“不蒜子”**这个第三方服务，这里再次对作者表示感谢。
使用腾讯统计来查看网站的流量情况 回到问题本身，PV，即Page View，表示页面浏览量或者点击量，每当一个页面被打开或者被刷新，都会产生一次 PV，只要这个请求从浏览器端发送到了服务器端。聪明的各位肯定会想到，如果我写一个爬虫不停地去请求一个页面，那么这个页面的 PV 不就会一直增长下去吗？理论上的确是这样，所以，我们有第二个指标 UV，来作为进一步的参考，所谓 UV，即Unique Visitor，表示独立访客数。在上面这个问题中，尽管这个页面的 PV 在不断增长，可是因为这些访客的 IP 都是相同的，所以，这个页面只会产生一次 UV，这就是 PV 和 UV 的区别。所以，我们结合这两个指标，可以非常容易得了解到，这个页面实际的访问情况是什么样的。这让我想起数据分析中的一个例子，虽然以统计学为背景的数学计算不会欺骗人类，可如果人类片面地相信某一个方面的分析结果，数据分析一样是带有欺骗性的。就像有人根据《战狼 2》和《前任 3》两部电影的观众购买冷/热饮的情况，得出下面的结论：看动作片的观众更喜欢喝冷饮来清凉紧绷着的神经，而看爱情片的观众更喜欢喝热饮来温暖各自的内心。其实想想就知道这里混淆了因果性和相关性，选择冷饮还是热饮无非是两部电影上映的季节不同而已。
如何设计一个访问统计系统 OK，了解了 PV 和 UV 的概念后，我们来思考如何去设计一个访问统计系统，这是今天这篇博客的主题内容。我知道，如果问如何设计一个访问系统，大家可能都会不由自主地想到建两张表。的确，这是最简单的做法。可问题是，我们对于 PV 的认识，其实一直都在不断地变化着。比如 PV 的定义是是一个页面被打开或者被刷新时视为一次有效 PV，所以，我们通常的做法是在页面底部嵌入 JavaScript 脚本，这种方式一直工作得非常好。可在引入 AJAX 以后，用户几乎不会主动去刷新页面，那么，在这个过程中用户点击更多或者使用下拉刷新时，是否应该算作一次有效 PV 呢？甚至在 PC 端网页逐渐式微以后，越来越多的工作转移到手机等移动设备上来，越来越多的原生+Web 混合 App 或者是单页面应用(SPA)或者是渐进式应用(PWA)，此时我们又该如何认识 PV 呢？微信公众号里的 PV 甚至更为严格，必须通过微信内置的浏览器访问才能算作一次有效 PV。
可以发现，我们对 PV 的认识其实一直在不断的变化着，更多的时候，我们想追踪的并非页面被加载(Page Load)的次数，而是页面被浏览(Page View)的次数。这时候，我们可以 Page Visiblity 和 History API 结合的方式。前者在页面的 visibilityState 可见或者由隐藏变为可见时发送一次 Page View，而后者则是在浏览器地址发生变化的时候发送一次 Page View。这听起来非常像单页面应用(SPA)里前端路由的那套玩法，的确，当一个地址中的 pathname 或者 search 部分发生变化时，应该发送一次 Page View 请求，而 hash 部分的变化则应该忽略，因为它表示的是应用内部页面的跳转。对于页面的 visibilityState 由隐藏变为可见，不同的人有不同的看法，因为有时我们像合并多次 Page View，而有时候则想通过 Page View 了解所谓的”回头客“，所以，这里面还可以继续引入 Session 的概念，比如 Google Analytics 默认会在 30 分钟内无交互的情况下结束。所以，这个问题要考虑的东西实际上比想象中的要多。</description></item><item><title>使用 Python 开发插件化应用程序</title><link>https://blog.yuanpei.me/posts/1960676615/</link><pubDate>Fri, 11 Oct 2019 08:56:27 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1960676615/</guid><description>插件化应用是个老话题啦，在我们的日常生活中更是屡见不鲜。无论是多年来臃肿不堪的 Eclipse，亦或者是扩展丰富著称的 Chrome，乃至近年来最优秀的编辑器 VSCode，插件都是这其中重要的组成部分。插件的意义在于扩展应用程序的功能，这其实有点像 iPhone 手机和 AppStore 的关系，没有应用程序的手机无非就是一部手机，而拥有了应用程序的手机则可以是 Everything。显然，安装或卸载应用程序并不会影响手机的基本功能，而应用程序离开了手机同样无法单独运行。所以，所谓“插件”，实际上是一种按照一定规范开发的应用程序，它只能运行在特定的软件平台/应用程序且无法运行。这里，最重要的一点是应用程序可以不依赖插件单独运行，这是这类“插件式”应用的基本要求。
好了，在了解了插件的概念以后，我们来切入今天的正文。博主曾经在《基于 Python 实现 Windows 下壁纸切换功能》这篇文章中编写了一个小程序，它可以配合 Windows 注册表实现从 Unsplash 上抓取壁纸的功能。最近，博主想为这个小程序增加 必应壁纸 和 WallHaven 两个壁纸来源，考虑到大多数的壁纸抓取流程是一样的，博主决定以“插件”的方式完成这次迭代，换句话说，主程序不需要再做任何调整，当我们希望增加新的数据源的时候，只需要写一个.py 脚本即可，这就是今天这篇文章的写作缘由。同样的功能，如果使用 Java 或者 C#这类编译型语言来做，我们可能会想到为插件定义一个 IPlugin 接口，这样每一个插件实际上都是 IPlugin 接口的实现类，自然而然地，我们会想到通过反射来调用接口里的方法，这是编译型语言的做法。而面对 Python 这样的解释型语言，我们同样有解释型语言的做法。
首先，我们从一个最简单的例子入手。我们知道，Python 中的 import 语法可以用来引入一个模块，这个模块可以是 Python 标准库、第三方库和自定义模块。现在，假设我们有两个模块：foo.py 和 bar.py。
#foo.py import sys class Chat: def send(self,uid,msg): print(&amp;#39;给{uid}发送消息：{msg}&amp;#39;.format(uid=uid,msg=msg)) def sendAll(self,msg): print(&amp;#39;群发消息：{msg}&amp;#39;.format(msg=msg)) #bar.py import sys class Echo: def say(self): print(&amp;#34;人生苦短，我用Python&amp;#34;) def cry(): print(&amp;#34;男人哭吧哭吧不是罪&amp;#34;) 通常, 为了在当前模块(main.py)中使用这两个模块，我们可以使用以下语句：
import foo from bar import * 这是一种简单粗暴的做法，因为它会导入模块中的全部内容。一种更好的做法是按需加载，例如下面的语句：</description></item><item><title>百度地图加载海量标注性能优化策略</title><link>https://blog.yuanpei.me/posts/3131944018/</link><pubDate>Tue, 10 Sep 2019 09:44:18 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3131944018/</guid><description>在上一篇博客中关于 Vue 表单验证的话题里，我提到了这段时间在做的城市配载功能，这个功能主要着眼于，如何为客户提供一条路线最优、时效最短、装载率最高的路线。事实上，这是目前物流运输行业智能化、专业化的一个趋势，即面向特定行业的局部最优解问题，简单来说，怎么样能在装更多货物的同时满足运费更低的条件，同时要考虑超载等等不可抗性因素，所以，这实际上是一个数学问题。而作为这个功能本身，在地图上加载大量标注更是基础中的基础，所以，今天这篇博客想说说，通过百度地图 API 加载海量标注时，关于性能优化方面的一点点经验。
问题还原 根据 IP 定位至用户所在城市后，后台一次性查询出近一个月内的订单，然后将其全部在地图上展示出来。当用户点击或者框选标注物时，对应的订单配载到当前运单中。当用户再次点击标注物，则对应的订单从当前运单中删除。以西安市为例，一次性加载 850 个左右的订单，用户操作一段时间后，Chrome 内存占用达 250 多兆，拖拽地图的过程中可以明显地感觉到页面卡顿。因为自始至终，地图上的订单数量不变，即不会移除覆盖物，同时需要在内存中持久化订单相关的信息。所以，在城市配载 1.0 版本的时候，测试同事给我提了一个性能方面的 Bug。可开始提方案并坚持这样做的，难道不是产品吗？为什么要给开发提 Bug 呢？OK，我们来给不靠谱的产品一点点填坑吧，大概想到了下面三种方案，分别是标注物聚合 、Canvas API 和视野内可见。
密密麻麻的地图 标注物聚合方案 所谓“标注物聚合”，就是指在一定的地图层级上，地图上的覆盖物主要是以聚合的形式显示的，譬如显示某一个省份里共有多少个订单，而不是把所有订单都展示出来，除非地图放大到一定的层级。这种其实在我们产品上是有应用的，比如运单可视化基本上是全国范围内的车辆位置，这个时候在省一级缩放比例上使用聚合展示就非常有必要。可在城市配载这里就相当尴尬啦，因为据说客户会把地图放大到市区街道这种程度来对订单进行配载，所以，这种标注物聚合方案的效果简直是微乎其微，而且更尴尬的问题在于，官方的 MarkerClusterer 插件支持的是标准的覆盖物，即 Marker 类。而我们的产品为了好看、做更复杂的交互，设计了更复杂的标记物原型，这就迫使我们必须使用自定义覆盖物，而自定义覆盖物通常会用 HTML+CSS 来实现。
标注聚合器MarkerClusterer 所以，一个简洁的 Marker 类和复杂的 DOM 结构，会在性能上存在巨大差异，这恰恰是我们加载了 800 多个点就产生性能问题的原因，因为一个“好看”的标注物，居然由 4 个 DOM 节点组成，而这个“好看”的标注物还不知道要怎么样实现 Marker 类里的右键菜单。所以，追求“好看”有问题吗？没有，可华而不实的“好看”，恰恰是性能降低的万恶之源，更不用说，因为覆盖物不会从地图上删除，每次框选都要进行 800 多次的点的检测了，而这些除了开发没有人会在乎，总有人摆出一副“这个需求很简单，怎么实现我不管”的态度……虽然这种方案已经被 Pass 掉了，这里我们还是通过一个简单的示例，来演示下 MarkerClusterer 插件的简单使用吧！以后对于前端类的代码，博主会优先使用 CodePen 进行展示，因为这样子显然比贴代码要生动呀！
See the Pen Marker-Clusterer by qinyuanpei (@qinyuanpei) on CodePen. 这里稍微提带说一下这个插件的优化，经博主测试，在标记物数目达到 100000 的时候，拖拽地图的时候可以明显的感觉的卡顿，这一点大家可以直接在 CodePen 中进行测试。产生性能问题的原因主要在以下代码片段：
/** * 向该聚合添加一个标记。 * @param {Marker} marker 要添加的标记。 * @return 无返回值。 */ Cluster.</description></item><item><title>Vue 快速实现通用表单验证</title><link>https://blog.yuanpei.me/posts/169430744/</link><pubDate>Fri, 06 Sep 2019 14:53:46 +0000</pubDate><guid>https://blog.yuanpei.me/posts/169430744/</guid><description>本文开篇第一句话，想引用鲁迅先生《祝福》里的一句话，那便是：“我真傻，真的，我单单知道后端整天都是 CRUD，我没想到前端整天都是 Form 表单”。这句话要从哪里说起呢？大概要从最近半个月的“全栈工程师”说起。项目上需要做一个城市配载的功能，顾名思义，就是通过框选和拖拽的方式在地图上完成配载。博主选择了前后端分离的方式，在这个过程中发现：首先，只要有依赖 jQuery 的组件，譬如 Kendoui，即使使用了 Vue，依然需要通过 jQuery 去操作 DOM。其次，只有有通过 Rozar 生成的 DOM，譬如 HtmlHelper，Vue 的双向绑定就突然变得尴尬起来，更不用说，Rozar 中的@语法和 Vue 中的@指令相互冲突的问题，原本可以直接用 v-for 生成列表，因为使用了 HtmlHelper，突然一下子变得厌恶起来，虽然 Rozar 语法非常强大，可我依然没有在 JavaScript 里写 C#的热情，因为实在太痛苦啦 Orz……
所以，想做好前后端分离，首先需要分离出一套前端组件库，做不到这一点，前后端分离就无从谈起，就像我们公司的项目，即使框架切换到.NET Core，可是在很长的一段时间里，我们其实还是再写 MVC，因为所有的组件都是后端提供的 HtmlHelper/TagHelper 这种形式。我这次做项目的过程中，其实是通过 jQuery 实现了一部分组件，正因为如此，一个在前后端不分离时非常容易实现的功能，在前后端分离以后发现缺好多东西，就比如最简单的表单验证功能，即便你是在做一个新项目，为了保证产品在外观上的一致性，你还是得依赖老项目的东西，所以，这篇博客主要想说说前后端分离以后，Vue 的时代怎么去做表单的验证。因为我不想测试同事再给我提 Bug，问我为什么只有来自后端接口的验证，而没有来自前端页面的验证。我希望，在写下这篇博客之前，我可以实现和老项目一模一样的表单验证。如同 CRUD 之于后端，80%的前端都是在写 Form 表单，所以，这个事情还是挺有意思的。
最简单的表单验证 OK，作为国内最接“地气”的前端框架，Vue 的文档可以说是相当地“亲民”啦！为什么这样说呢，因为其实在官方文档中，尤大已经提供了一个表单验证的示例，这个示例让我想起给某银行做自动化工具时的情景，因为这两者都是采用 MVVM 的思想，所以，理解起来是非常容易的，即：通过一个列表来存储错误信息，而这个错误信息会绑定到视图层，所以，验证的过程其实就是向这个列表里添加错误信息的过程。我们一起来看这个例子：
&amp;lt;div&amp;gt; &amp;lt;h2&amp;gt;你好，请登录&amp;lt;/h2&amp;gt; &amp;lt;div&amp;gt; &amp;lt;form id=&amp;#34;loginFrom&amp;#34;&amp;gt; &amp;lt;div&amp;gt; &amp;lt;label&amp;gt;邮箱&amp;lt;/label&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; class=&amp;#34;form-control&amp;#34; id=&amp;#34;inputEmail3&amp;#34; placeholder=&amp;#34;Email&amp;#34; v-model=&amp;#34;email&amp;#34;&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div&amp;gt; &amp;lt;label&amp;gt;密码&amp;lt;/label&amp;gt; &amp;lt;input type=&amp;#34;password&amp;#34; class=&amp;#34;form-control&amp;#34; id=&amp;#34;inputPassword3&amp;#34; placeholder=&amp;#34;Password&amp;#34; v-model=&amp;#34;password&amp;#34;&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div&amp;gt; &amp;lt;button type=&amp;#34;button&amp;#34; class=&amp;#34;btn btn-default login&amp;#34; v-on:click=&amp;#34;login()&amp;#34;&amp;gt;登录&amp;lt;/button&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div v-if=&amp;#34;errorList.</description></item><item><title>在 WSL 中使用 Linux 桌面环境的尝试与总结</title><link>https://blog.yuanpei.me/posts/3972610476/</link><pubDate>Sat, 17 Aug 2019 21:09:46 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3972610476/</guid><description>最近忙里偷闲的博主，再次迷恋上折腾 Linux。话说自从微软推出 WSL 以后，我就彻底地停止了 Windows + Linux 的双系统组合。回想起从前使用过的各种 Linux 发行版，基本上每隔一段时间就会崩溃一次，所以，面对 WSL 这种近乎应用级别的方案，我个人是非常愿意去接受的。因为一不小心玩坏了的话，可以直接对应用程序进行重置，或者重新从应用商店下载，相比重装系统，我觉得这种方式要更友好一点。虽然说 Windows10 是有史以来最好的 Linux 发行版:smile:，可面对只有命令行的 Linux，果然还是有一丝丝的失望啊:beetle:。所以，在这篇博客里，主要想和大家分享下，关于在 WSL 下使用 Linux 桌面系统的一点点尝试和体会。虽然目前应用商店里已经提供了 Ubuntu、Debian、Kail Linux、OpenSUSE 这些常见的发行版，可当你熟悉了 Linux 的世界以后，就会明白这个世界对多元化的追求是永无止境的，我不想去 Judge 这些多元化间优劣，我只想自由地使用我喜欢的技术，比如 Linux Deepin、Elementary OS。这是我想要使用 Linux 桌面环境的理由。
我们知道，目前应用商店里提供的 Linux 发行版都是&amp;quot;命令行&amp;quot;版本。因为 Windows 本身就提供了非常出色的桌面环境，虽然每一次设计都给人一种相当前卫的感觉。平时我们使用SSH登录远程服务器的时候，其实是使用它的终端环境即 CLI。Linux 和 Windows 最大的不同在于，Linux 的桌面环境并不是 Linux 本身的一部分，它和所有的 Linux 应用程序并没有什么区别，因为脱离桌面环境的 Linux 的单独运行，而脱离桌面环境的 Windows 则未必可以。那么，我们怎么样在 Windows 里使用 Linux 的桌面环境呢？常见的思路主要有XServer和远程桌面两种。这里我们主要介绍第一种方式，即XServer。什么是 XServer 呢？Linux 的 GUI 架构其实是 C/S 模式的，其中 XServer 负责显示，XClient 负责请求。所以，我们只要在宿主机上安装 XServer 就可以啦。在这里，常见的 XServer 主要有：VcXsrv、X410和MobaXterm。理论上，我们只需要在 WSL 里安装桌面环境，在 Windows 上安装 XServer，然后通过命令行启动相应桌面环境即可。</description></item><item><title>通过 ApiExplorer 为 Swagger 提供 MVC 扩展</title><link>https://blog.yuanpei.me/posts/4116164325/</link><pubDate>Tue, 06 Aug 2019 23:02:50 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4116164325/</guid><description>我一直想吐槽下运维同事提供的 Jekins 项目模板，因为它居然不支持含有多个项目的解决方案。作为一个有追求的程序员，每个解决方案下最少应该含有两个项目，即项目本身和项目对应的单元测试。可惜在这样一种情形下，我没法再去坚持这样的原则，而这真正让我感到难过的是，为了在编译环节向 Jekins 妥协，大家在一个项目里极尽所能，在这一个项目里居然混合了MVC、WebApi和WebService三种技术，甚至到最后连传统三层的界限都越来越模糊了。这让我意识到一件事情，工程上的妥协和技术选型一样，在某种意义上它们都不能被称之为科学，因为确实没什么道理，完全是运维为了方便而制造出的问题。在我们意识到文档的重要性以后，写文档就变成了日常工作。我一直坚持的原则是，文档能通过工具生成就坚决不要手写，所以，看到项目目录里充斥着各种各样的文档格式，譬如 Word、Excel、Pdf、Viso 等等的时候，我毅然决然地选择了 Swagger。而今天这篇文章的原由，恰恰来自于这个&amp;quot;混搭&amp;quot;的项目。说到这里，你可能已经想到我想做什么了。不错，我们有部分 WebApi 是写在 MVC 的控制器里的，我希望使用者可以通过 Swagger 看到这部分接口的文档，这样我就有时间在这里写博客了。😄
故事缘起 常规的 Swagger 的使用就不再说啦，因为基本上你通过 Nuget 安装完Swashbuckle以后，再配置下项目生成的 XML 注释文档就可以使用啦！不过，博主在这里遇到的第一个问题就是，按照常规套路配置好了以后，Swagger 页面打开完全就是空白的啊，反复尝试直至怀疑人生后，我突然意识到，莫非是因为我这是一个 MVC 的项目？立马跑到官方的 Issues 下面逐个扫视，果不其然，大佬们一致给出了答案：Swagger 是不支持 MVC 类型的项目的。这里补充说明，这里的 MVC 是指ASP.NET MVC。目前官方主推的ASP.NET Core是没有这种困惑的啦，因为微软在这个新版本中统一了 MVC 和 WebApi。对于这种历史“遗留问题”，既然 Swagger 官方都不愿意提供支持，那么，博主只好勉为其难的提供一个实现，我不得不承认，带着历史包袱的 ASP.NET 在扩展性上的确不如全新的“Core”系列，因为单单是System.Web系列的动态链接库版本就令人痛苦不堪，因此，博主在写这个扩展的时候，全部升到了最新的 5.2.7.0。
实现 MvcApiExplorer 好了，Swagger 之所以能够生成友好、可交互的 API 文档，其核心依赖于 IApiExplorer 接口。这一点，我们可以通过 Swashbuckle 项目中的源代码来得到验证。其中，生成符合 Swagger 规范的 JSON 文档，是通过 SwaggerGenerator 这个类来实现的。而进一步研究这个类，我们就会发现它依赖IApiExplorer接口。这个接口位于System.Web.Http.Description命名空间下，而显然这是 WebApi 相关的命名空间，所以，对于一般的 WebApi 项目，因为微软已经帮我们实现了默认的 ApiExplorer，所以，Swagger 可以识别出项目中的 Controller 及其 Action，并通过 XML 注释文档进一步填充接口相关的描述信息。一旦想到这一层，我们就会明白，为什么 Swagger 不支持 MVC 项目，因为 MVC 里压根就没有实现 IApiExplorer 接口啊！那么，怎么办呢？我们的想法是通过反射取出所有的 MVC 控制器及其 Action，然后组织出这些接口的描述信息，再将它们添加到默认的 IApiExplorer 实现中去，这样 MVC 和 WebApi 都可以被 Swagger 识别，为此，我们继承默认的 ApiExplorer，并实现我们自定义的MvcApiExplorer：</description></item><item><title>.NET Core POCOController 在动态 Web API 中的应用</title><link>https://blog.yuanpei.me/posts/116795088/</link><pubDate>Thu, 01 Aug 2019 16:44:59 +0000</pubDate><guid>https://blog.yuanpei.me/posts/116795088/</guid><description>Hi，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是：https://blog.yuanpei.me。在上一篇文章中，我和大家分享了 ASP.NET 中动态 Web API 的实现，这种方案的现实意义是，它可以让我们把任意一个接口转换为 Web API，所以，不单单局限在文章里提到的 WCF 迁移到 Web API，任意领域驱动开发(DDD)中的服务层，甚至是更为普遍的传统三层，都可以通过这种方式快速构建前后端分离的应用。可能大家会觉得直接把 Service 层暴露为 API，会引发一系列关于鉴权、参数设置(FromQuery/FromBody)等等的问题，甚至更极端的想法是，这样和手写的没什么区别，通过中间件反射能达到相同的目的，就像我们每天都在写各种接口，经常有人告诉我们说，不要在 Controller 层写太重的业务逻辑，所以，我们的做法就是不断地在 Service 层里增加新接口，然后再把 Service 层通过 Controller 层暴露出来，这样子真的是对的吗？
可我个人相信，技术总是在不断向前发展的，大家觉得 RESTful 完全够用啦，结果 GraphQL 突然发现了。大家写了这么多年后端，其实一直都在绕着数据转，可如果数据库本身就支持 RESTful 风格的接口，或者是数据库本身就支持某种 ORM，我们后端会立马变得无趣起来。其实，在 ASP.NET Core 中已经提供了这种特性，这就是我们今天要说的 POCOController，所以，这也许是个正确的思路，对吧？为什么 Service 层本身不能就是 Controller 层呢？通过今天这篇文章，或许你会接受这种想法，因为 POCOController，就是弱化 Controller 本身的特殊性，一个 Controller 未必需要继承自 Controller，或者在名称中含有 Controller 相关的字眼，如果 Controller 同普通的类没有区别会怎么样呢？答案就是 Service 层和 Controller 层的界限越来越模糊。扪心自问，我们真的需要中间这一层封装吗？
什么是 POCOController POCOController 是 ASP.NET Core 中提供的一个新特性，按照约定大于配置的原则，在 ASP.NET Core 项目中，所有带有 Controller 后缀的类，或者是使用了[Controller]标记的类，即使它没有像模板中一样继承 Controller 类，ASP.NET Core 依然会将其识别为 Controller，并拥有和普通 Controller 一样的功能，说到这里，你是不是有点兴奋了呢，因为我们在 ASP.</description></item><item><title>长安十二时辰随想</title><link>https://blog.yuanpei.me/posts/1540537013/</link><pubDate>Mon, 22 Jul 2019 11:17:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1540537013/</guid><description>年少时未见长安，难以想象万国来朝的盛唐气象，心中最为仰慕的人物，是那个“好剑术、喜任侠“、二十五岁“仗剑去国、辞亲远游”的李白。人在年少轻狂的时候，容易因为一个人的豪迈不羁，而选择性地模糊一个时代的印象。于是，长安就蓦地变成了李太白大放异彩的舞台。印象里的长安，是一个可以让人吟咏“安能摧眉折腰事权贵，使我不得开心颜”的地方，是一个可以让“贵妃捧墨、力士脱靴”的地方，是一个“绣口一吐，就是半个盛唐”的地方。自此，喜欢上“大道如青天，我独不得出”，喜欢上“古来圣贤皆寂寞，唯有饮者留其名”，喜欢上“天生我才必有用，千金散尽还复来”，仿佛李白就是盛唐，而盛唐就是长安，若非如此，杨玉环便不会在极乐之宴上称赞李白的才华。
可当历史脉络逐渐清晰起来的时候，我们突然发现，原本我们以为最为意气风发的李白，那一年(开元 18 年，即公元 730 年)李白已经 30 岁了，就在那一年李白谒见宰相张说及一干长安名流，均无结果，直到他结识贺知章并被对方称之为“谪仙”， 引出一段“金龟换酒”的传奇佳话。这位谪仙人曾两度离开长安，第一次是感慨“行路难，归去来”，那一年李白 38 岁；第二次是供奉翰林期间遭玄宗“贬谪”，那一年李白 43 岁。所以，年少时以为的李白，是否是真实的李白？年少时以为的长安，是否是真实的长安？李白生平豪放，有诗的地方必有酒、有剑，一首《侠客行》更是金庸武侠小说中接近玄学的存在，可李白生平最快乐的时候，或许只有白帝城两岸猿啼知道了。
人们喜欢选择性地美化回忆，就像迪士尼重制的《狮子王》，我们曾以为木法沙和辛巴是正义的化身，而弑兄上位的刀疤则是邪恶的代表，其实用成人的眼光来看待这部电影，我们还真的找不出木法沙和辛巴的统治会比刀疤的统治好到哪里去的证据，这大概就是记忆本身的滤镜作用，长大了发现小时候常吃的熊毅武方便面，居然是陕西省出产的，而中萃方便面，则是江苏省出产的。当你未来的时光，有一小部分是和这两个地方有关，你不能不说，记忆真的是个奇妙的东西。《妖猫传》里绮丽璀璨的幻术表演，极力展现长安奢华的一面，虽然这一切都是活在两位主角的想象中，因为它想表达的是一种美的消逝和幻灭，此时的长安，其伟大和无与伦比体现在这三个符号上——空海代表的真理密法、李白代表的艺术创作、杨贵妃代表的绝对的美。
正如我们已无从想象，当年矗立在兴庆宫的花萼相辉楼到底是什么样子，于是导演将其安排在一个看起来像是个洞穴的地方。我们更无从得知，那一年的白居易到底怀着什么样的抱负来到长安，当他从已为陈迹的花萼相辉楼里捡起李白用过的笔的时候，是否真的如他想说想要开创一个新的时代，谱写一曲荡气回肠的长恨歌。有人说，玄宗兴建花萼相辉楼的时候，在周遭兴建了五座宅邸供诸王居住，逢宴请娱游更是宴请诸位兄弟一同前去，一改盛唐自玄武门之变以来手足相残的情形。可正如《长安十二时辰》里战战兢兢的太子李亨一般，一个对兄弟手足颇具温情的帝王，为何又能冤杀三个皇子在前？我们不得不说，历史极具迷惑性。
《妖猫传》开篇由玄宗驾崩引出，在经历了安史之乱以后，通过一只猫的视角，我们看到了在极尽繁华后满目苍凉。有人问，李白为何在吟咏《清平调》的时候泪流满面？也许以李白经天纬地的才华，他早就洞悉了帝王之爱的虚伪，早就看出了盛世之下的危机重重，早就明白了身为御用文人的悲哀，所谓盛世，或许仅仅是帝王权术操纵下的一场表演。可即使如此，李白依然愿意在洞悉这一切后去讴歌这种美，不论美本身多么的脆弱，至少这一刻它是真实存在的，就像罗曼罗兰说的那样：“这世界上只有一种真正的英雄主义，就是在看清生活之后，依然热爱生活”。
今天在路上遇到一位外地来的游客，询问我关于陕博、大雁塔和兵马俑的种种，可其实我和他一样，都不过是这座城市的过客。我有时候会不由地起，一千多年前的长安，是否和今天一样向世界敞开大门？《妖猫传》里遣唐的日本僧人空海，今天依然可以在青龙寺的简介中找到姓名，而透过张天爱的胡旋舞，或许可以看到那个胡汉相融、开放包容的长安。《长安十二时辰》里，有突厥狼卫，有胡椒胡饼，有波斯王子，有圣拜火教，有粟特大秦，有拢右拨换……几乎可以媲美世界中心。而一千多年后的今天，各种商业中心不再局限于东西两市，而兴庆宫、曲江池不再是皇家宫殿园林，长安一百零八坊的格局依稀可见，皇城北侧空荡荡的大明宫遗址，是否会听到来自玄武门的阵阵杀伐之声？
《长安十二时辰》里的主角张小敬，曾经是万年县不良帅，长安以朱雀大街为界，朱雀大街以西为长安县，朱雀大街以东为万年县，实则取“长安万年”之意。剧中靖安吏们聚集在一起八卦朝廷，有一个人说自古以来哪里有万年的江山。不管历史上的李亨是否提出过对赋税和藩镇进行改革的提议，我们都知道安史之乱是唐朝由盛转衰的开始。据说马亲王这本书的灵感来自“刺客信条”，我本人同样是这部游戏的忠实粉丝，可当你真正想在长安寻找鸟瞰点并进行同步的时候，你会发现小雁塔顶端早已残损，攀爬这样的建筑物简直就是在碰瓷儿，而大雁塔的高度早已被周遭的大悦城超越，按照书中的设定，只要在大悦城的天台上增设武侯，不要说同步鸟瞰点，分分钟就会被弓弩手射中失去同步，因为据说大雁塔下面只有音乐喷泉，并没有安置干草堆……
对于《长安十二时辰》这部网剧而言，剧中的崔器或许是无数想留在长安的人的一个缩影，没有过人的背景，智力和胆识有限，因为担负着兄长的希望和光耀门楣的使命，渴望建功立业、想抓住一切能抓住的机会努力向上爬，在长安这座城市获得一种归属感。崔器的人设并不讨喜，甚至从一出场就在扮演猪队友的角色，属于那种有点蠢但本质不坏的人，靖安司一役被编剧写死完全是剧情需要，总体来说，这种小人物的设定，只要不是又蠢又坏，总能因为贴近底层而引起更多人的共鸣。《妖猫传》里的主角白乐天初到长安时，诗人顾况开他玩笑说：“居大不易”。同样地，在《长安十二时辰》里，有到长安来干谒的岑参，有出身贫寒的元载，各自的选择不同，最终的结果不同。虽然选择比努力更重要，可你的能力总要能配得上你的野心。每个时代都有每个时代的困境，今天在西安讨生活的你我，和一千多年前的这些前辈们有什么不同呢？你的选择又是什么呢？
在浩如烟海的茫茫历史中，太多的人和事，最终都会变得像雪泥鸿爪一般无迹可寻。对史学家而言，那是震惊寰宇的历史发现，可对更多像你和我一样的普通人而言，那不过偶然想起的经年旧事。我们回头看这些历史的时候，一如空海和白居易回头瞥见八重樱下的杨玉环，所谓“一切有为法，皆化作泡影”。《长安十二时辰》中塑造了张小敬这样一个“刺客”形象，可历史不过是姚汝能笔下轻描淡写的一句话；安国柱，一个身在长安的粟特人，即使娶了美艳的长安女子做妻子，依然想着努力工作好配得上她；徐宾，一个身在长安的靖安司主事，为了让大家更好得整理案牍，积极改良造纸技术，甚至到生命的最后一刻还在保护案牍；焦遂，一个身在长安的布衣，悬挂金鱼袋只为进宫喝酒，一句“长安，焦遂”豪气干云……
有这些可爱可敬的人，我愿意相信，这一切都曾在这个城市发生过，而一千多年后的今天，我在长安，可我见了长安，便懂了长安么？是少年豪气作祟的“赵客缦胡缨，吴钩霜雪明”，还是一个快三十岁的中年大叔“为赋新词强说愁”呢？</description></item><item><title>使用 ASP.NET Core 和 Hangfire 实现 HTTP 异步化方案</title><link>https://blog.yuanpei.me/posts/1071063696/</link><pubDate>Thu, 04 Jul 2019 08:56:28 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1071063696/</guid><description>Hi，大家好，我是 Payne，欢迎大家一如既往地关注我的博客。今天这篇博客里的故事背景，来自我工作中的一次业务对接，因为客户方提供的是长达上百行的 XML，所以一度让更喜欢使用 JSON 的博主感到沮丧，我这里不是想讨论 XML 和 JSON 彼此的优缺点，而是我不明白 AJAX 里的 X 现在基本都被 JSON 替代了，为什么还有这么多的人坚持使用并友好的 XML 作为数据的交换协议呢？也许你会说，因为有这样或者那样等等的理由，就像 SOA、ESB、SAP 等等类似的技术在企业级用户依然大量流行一样，而这些正是“消费”XML 的主力军。我真正想说的是，在对接这类接口时，我们会遇到一个异步化的 HTTP 协议场景，这里的异步和多线程、async/await 没有直接关系，因为它描述的实际上是业务流程上的一种“异步”。
引子-想对 XML 说不 我们知道，HTTP 协议是一个典型的请求-响应模型，由调用方(Client)调用服务提供者(Server)提供的接口，在理想状态下，后者在处理完请求后会直接返回结果。可是当后者面对的是一个“耗时”任务时，这种方式的问题就立马凸显出来，此时调用者有两个选择：一直等对方返回直至超时(同步)、隔一会儿就看看对方是否处理完了(轮询)。这两种方式，相信大家都非常熟悉了，如果继续延伸下去，我们会联想到长/短轮询、SignalR、WebSocket。其实，更好的方式是，我们接收到一个“耗时”任务时，立即返回表明我们接收了任务，等任务执行完以后再通知调用者，这就是我们今天要说的 HTTP 异步化方案。因为对接过程中，客户采用的就是这种方案，ESB 这类消息总线本身就提供了这种功能，可作为调用方的博主就非常难受啦，因为明明能“同步”地处理完的事情，现在全部要变成“异步”处理，就像一个习惯了 async/await 语法糖的人，突然间就要重新开始写 APM 风格的代码，宝宝心里苦啊，“异步”处理就异步处理嘛，可要按人家要求去返回上百行的 XML，博主表示想死的心都有了好嘛……
好了，吐槽归吐槽，吐槽完我们继续梳理下 HTTP 异步化的方案，这种方式在现实生活中还是相当普遍的，毕竟人类都是“异步”做事，譬如“等你哪天有空一起吃个饭”，测试同事对我说得最多的话就是，“等你这个 Bug 改完了同我说一声”，更不用说，JavaScript 里典型的异步单线程的应用等等……实现“异步”的思路其实是非常多的，比如同样在 JavaScript 里流行的回调函数，比如通过一张中间表存起来，比如推送消息到消息队列里。在面向数据库编程的时候，我听到最多的话就是，没有什么问题是不能用一张中间表来解决的，如果一张不行那就用两张。项目上我是用 Quartz+中间表的方式实现的，因为这是最为普通的方式。这里，我想和大家分享下，关于使用 Hangfire 来实现类似 Quartz 定时任务的相关内容，果然，我这次又做了一次标题党呢，希望大家会对今天的内容感兴趣。简单来说，我们会提供一个接口，调用方提供参数和回调地址，调用后通过 Hangfire 创建后台任务，等任务处理结束后，再通过回调地址返回结果给调用方，这就是所谓的 HTTP 异步化。
开箱即用的 Hangfire 我们项目上是使用 Quartz 来实现后台任务的，因为它采用了反射的方式来调用具体的 Job，因此，它的任务调度和任务实现是耦合在同一个项目里的，常常出现单个 Job 引发整个系统卡顿的情况，尤其是是它的触发器，常常导致一个 Job 停都停不下来，直到后来才渐渐开始通过 Web API 来分离这两个部分。Quartz 几乎没有一个自己的可视化界面，我们为此专门为它开发了一套 UI。我这里要介绍的 Hangfire，可以说它刚好可以作为 Quartz 的替代品，它是一个开箱即用的、轻量级的、开源后台任务系统，想想以前为 Windows 开发定时任务，只能通过定时器(Timer)来实现，尚不知道 CRON 为何物，而且只能用命令行那种拙劣的方式来安装/卸载，我至今都记得，测试同事问我，能不能不要每次都弹个黑窗口出来，这一起想起来还真是让人感慨啊。好了，下面我们开始今天的实践吧！首先，第一步自然是安装 Hangfire 啦，这里我们新建一个 ASP.</description></item><item><title>通过动态 Controller 实现从 WCF 到 Web API 的迁移</title><link>https://blog.yuanpei.me/posts/4236649/</link><pubDate>Sat, 08 Jun 2019 13:48:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4236649/</guid><description>在《又见 AOP 之基于 RealProxy 实现 WCF 动态代理》 这篇文章中，我和大家分享了关于使用动态代理来简化 WCF 调用过程的相关内容，当时我试图解决的问题是，项目中大量通过 T4 生成甚至手动编写的“代理方法”。今天，我想和大家分享的是，如何通过动态的 Controller 来实现从 WCF 到 Web API 的迁移。为什么会有这个环节呢？因为我们希望把一个老项目逐步迁移到.NET Core 上面，在这个过程中首当其冲的就是 WCF，它在项目中主要承担着内部 RPC 的角色，因为.NET Core 目前尚未提供针对 WCF 服务端的支持，因此面对项目中成百上千的 WCF 接口，我们必须通过 Web API 重新“包装”一次，区别于那些通过逐个 API 进行改造的方式，这里我们通过 Castle 动态生成 Controller 来实现从 WCF 到 Web API 的迁移。
如何对类和接口进行组合 首先，我们来思考这样一个问题，假设现在有一个类 BaseClass、一个接口 IBaseService 及其实现类 BaseService，我们有没有什么办法，可以让这个类和接口组合起来呢？联系面向对象编程的相关知识，我们应该可以想到最常见的两种方式，即 BaseService 继承 BaseClass(或者反过来)、BaseClass 实现 IBaseService 接口。考虑到语言本身是否支持多继承的因素，第二种方式可能会更具有适用性。可如果这个问题，就仅仅到这种程度，我相信大家一定会感到失望，因为这的确没有什么好说的。现在的问题是，假如 BaseClass 类、BaseService 类都已经存在了，我们有没有什么思路，可以把它们组合到一个类中呢？这又和我们今天要讨论的内容有什么关系呢？
好了，不卖关子啦，下面隆重请出 Castle 中的 Dynamic Proxy，我们曾经介绍过 Castle 中的动态代理，它可以为指定的类和接口创建对应的代理类，除此以外，它提供了一种称为AdditionalInterfaces的接口，这个接口可以在某个代理对象上“组合”一个或者多个接口，换句话说，代理对象本身包含被代理对象的全部功能，同时又可以包含某个接口的全部功能，这样就实现了一个类和一个接口的组合。为什么我们会需要这样一个功能呢？因为假如我们可以把一个 ApiController 类和指定的接口类如 CalculatorService 进行组合，在某种程度上，CalculatorService 就变成了一个 ApiController，这样就实现了我们的目标的第一步，即动态生成一个 ApiController。与此同时，它会包含我们现有的全部功能，为了方便大家理解，我们从下面这个简单的例子开始：</description></item><item><title>《Web API 的设计与开发》读书笔记</title><link>https://blog.yuanpei.me/posts/3677280829/</link><pubDate>Tue, 28 May 2019 12:00:53 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3677280829/</guid><description>设计优美的 Web API 易于使用、便于更改、健壮性好、不怕公开
REST 的两层含义 指符合 Fielding 的 REST 架构风格的 Web 服务系统 指使用符合 RPC 风格的 XML 或 JSON + HTTP 接口的系统(不使用 SOAP) 端点的基本设计 短小便于输入的 URI- 人可以读懂的 URI 没有大小写混用的 URI 修改方便的 URI 不暴露服务端架构的 URI 规则统一的 URI HTTP 方法和端点 GET 获取资源 POST 新增资源 PUT 更新已有资源 DELETE 删除资源 PATCH 更新部分资源 查询参数和路径的使用区别 表示唯一资源时，放在路径中 当参数可以忽略时，放在查询参数中 RESTful 的设计级别 使用 HTTP 引入资源的概念 引入 HTTP 动词 引入 HATEOAS 如何指定数据格式 查询参数：url?format=xml 扩展名：/url.json Accept 头部字段 让用户决定响应的内容 GraphQL 通过状态码表示错误信息 1xx：消息 2xx：成功 3xx：重定向 4xx：客户端原因造成的错误 5xx：服务端原因造成的错误</description></item><item><title>又见 AOP 之基于 RealProxy 实现 WCF 动态代理</title><link>https://blog.yuanpei.me/posts/2954591764/</link><pubDate>Fri, 10 May 2019 16:27:50 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2954591764/</guid><description>最近一直在研究 Mongodb 和 ElasticSearch 之间同步数据的问题，苦于到目前为止，并没有取得任何实质性的进展。偶尔“趁得浮生半日闲暇”，看一看 Web API 设计方面的书籍，和前辈交流下项目中的历史遗留问题，最为直观的感受就是，这个世界上任何方案的最终落地，都经过理想和现实的无数次挣扎，比如我们希望迁移项目到.NET Core 平台上，初步分析大概有将近 1000 多个无法兼容的地方，维持现状固然可以保证整个项目的稳定，可如果真到了不得不升级的地步，面临的问题可能会越来越多，所谓“凡事预则立，不预则废”，早一点准备总是好的。既然说到里历史问题，那么，今天这篇文章就来说一说，基于 RealProxy 实现 WCF 动态代理。
故事背景 在我们的业务系统中，对内是使用 WCF 来进行相互通信的，而对外则是使用 Web API 来进行数据交换。关于 RPC 还是 REST 的争论由来已有，严格地来说，两者没有绝对的高下之分，从风格上而言，RPC 倾向于让接口映射到一个方法上，而 REST 则倾向于让接口映射到一个资源上。从我们实际的使用情况来看，REST 在系统中应用得并不是很完美，因为大多数情况下，我们实现的仅仅是 HTTP+JSON 这样一种协议组合，因此业务系统中存在着大量的 WCF 接口供系统内部调用。
内部服务调用示意图 最早的时候，是通过 T4 模板来生成针对某个接口的代理类，而代理类中通常封装了 ChannelFactory 的创建、释放等等 WCF 相关的代码，实际应用中还会对 WCF 接口的异常进行捕获、记录日志、统计调用时间等，因此早期的 T4 模板实际上承担了生成代理类的职责。虽然业务的不断推进，接口中加入的新方法越来越多，导致具体业务类中的代码越来越多，动辄出现单个文件中代码行数达 3000 行以上，与此同时，每当 WCF 接口中增加了新方法，就不得不在其相关的代理类中增加代理方法。坦白地讲，就是增加一个看起来差不多的方法，因为你依然要处理 ChannelFactory 的创建、释放、异常处理、日志记录等等的工作。
其实，WCF 可以直接生成客户端代码，因为每个 WCF 的服务都可以以 WebService 服务的形式暴露出来，而只要是 WebService，总可以通过 WSDL 生成一个代理类。不过这显然不利于团队间的协作，更不利于服务终结点配置的集中化，更失去了异常处理、日志记录等等这些“通用”工作的可能性。T4 应该可以基于“工作”，可显然大家觉得手写比生成要来得更容易些，所以，这个故事最终演变成这样一个局面，我们不得不通过局部类(Partial Class)的方式来开辟新的类文件。
系统中充斥着大量类似的代码 那么，说了这么多，从一个历史遗留问题入手，它真正的痛点在哪里呢？在我看来，主要有两点：第一，是手写代理类的“此恨绵绵无绝期”，明明就是对接口的简单封装，看起来是增加一个代理方法，其实最多就是复制黏贴，因为代理方法的核心代码就是调用接口，而剩下的都是重复的“服务型”代码；第二，是异常处理、日志记录的“哀鸿遍野”，同核心代码交织在一起，一遍又一遍的“重复”，为什么不考虑让它统一地去处理呢？难道每个人都抄着同一段代码，这样就实现了某种意义上的复用吗？
RealProxy 介绍 既然像我这样懒惰的人，不愿意像别人一样手写代理类，那么我的思路又是什么呢？显然，从这篇文章的题目，你就可以看出，我这里要说的是动态代理，原来的代理类同样属于代理，它是在编译时期间生成了一个代理类，我们以为在调用这个代理类，可其实真正去工作的是 ChannelFactory，这种方式称之为“静态代理”。如果你了解过设计模式，应该会知道相对应的代理模式，这里不再展开开来讲这这个设计模式，可以明确的是，动态代理就是在运行时期间动态创建一个代理对象的实例，它可以完全模拟被代理对象的行为，而我们的目的，就是要和手写的代理类永远地说再见！</description></item><item><title>WSL 下 Docker 使用踩坑小记</title><link>https://blog.yuanpei.me/posts/4159187524/</link><pubDate>Mon, 22 Apr 2019 22:13:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4159187524/</guid><description>众所周知，Win10 中开始提供 Linux 子系统，即 Windows Subsystem for Linux，简称 WSL，它可以让我们在 Windows 系统使用 Linux 系统，自从有了这个新功能以后，博主果断地放弃双系统的方案，因为折腾起来实在花费时间。关于如何使用 WSL，网上有非常多的文章可以参考，这里不再赘述。今天想说的是，WSL 下使用 Docker 遇到的各种坑。
装完 WSL 以后，对各种编译环境的使用相当满意，最近在研究日志可视化平台 ELK，其中需要使用 Docker 来搭建环境，一顿 sudo 操作猛如虎，快速安装完 Docker 环境，结果发现熟悉的命令行居然无法正常工作，是可忍孰不可忍。
sudo apt-get update sudo apt-get install \ apt-transport-https \ ca-certificates \ curl \ gnupg-agent \ software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo apt-key fingerprint 0EBFCD88 sudo add-apt-repository \ &amp;#34;deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable&amp;#34; sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.</description></item><item><title>由 DBeaver 与 PL/SQL 引发的数据库吐槽</title><link>https://blog.yuanpei.me/posts/337943766/</link><pubDate>Fri, 19 Apr 2019 12:52:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/337943766/</guid><description>因为工作中需要同时面向 MySQL、Oracle 和 SQLServer 三种数据库进行开发，所以，大概从去年国庆节开始，我开始使用一个开源的数据库管理工具——DBeaver。
使用这个工具的初衷，是因为我不想在同一台电脑上安装三个客户端工具，尤其是 Oracle 和 SQLServer 这种令人恐惧的、动辄需要重装系统的应用程序。我不想再使用类似 Navicat 这样的软件，因为它的画风像是上个世纪 VB6.0 的产品一样，同理，我不喜欢用 PL/SQL，因为我每次都要瞪大眼睛，在它狭窄而拥挤的画面上找表、找视图，更有甚者，有时要去找触发器、找存储过程。直到我同事给我发了一个几十 M 的文档，我突然间意识到，这货居然还要安装 Oracle 的客户端，配置数据库连接要手动去改配置文件，我一点都不喜欢 PL/SQL。
除了这三种经典的关系型数据库，我们还会用 Memcache 和 Redis 这样的内存数据库，Mongodb 这样的非关系型数据库，所以，我希望有一个统一的入口来管理这些连接，毕竟我身边的同事会使用三种以上的工具，譬如 Sqlyog、PL/SQL、SQLServer 等来处理这些工作，恰好 DBeaver 可以满足我 80% 的工作需要。目前，DBeaver 企业版支持关系型数据库和非关系型数据库，而社区版仅支持关系型数据库。
可最近在写 Oracle 环境的触发器(存储过程和触发器都是万恶之源)时，我发现 DBeaver 和 PL/SQL 在面对同一段 SQL 脚本时，居然因为一点点语法上的差异而不兼容，这让我内心深处不由得想对 Oracle 吐槽一番。这是一个什么样的 SQL 脚本呢？我们一起来看下面的例子：
CREATE OR REPLACE TRIGGER &amp;#34;TRI_SYNC_ITEM_VALUE&amp;#34; BEFORE DELETE ON &amp;#34;or_line&amp;#34; FOR EACH ROW DECLARE v_item_value NUMBER(18,6); BEGIN SELECT ITEM_VALUE INTO v_item_value FROM &amp;#34;order_info&amp;#34; WHERE ORDER_GID = :OLD.</description></item><item><title>zTree 删除/拖拽子节点保留父节点分组样式</title><link>https://blog.yuanpei.me/posts/1397717193/</link><pubDate>Fri, 12 Apr 2019 12:37:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1397717193/</guid><description>最近需要在项目中实现报表的自定义设置功能，即用户可以针对报表新建自定义分组，分组间可以互相嵌套，分组及分组内的报表需要支持拖拽排序、编辑、删除……相信听到这里，你大概明白我要实现一个什么样的功能了。不错，我要实现一个集美观、功能于一身的树形菜单。本着“不要重复制造轮子”的原则，我在考察了 JQuery EasyUI、layui、Bootstrap、Kendo UI 等不同框架提供的“树形菜单”组件以后，最终选择了zTree这样一个插件，虽然这个官网看上去相当复古，虽然最终的成品依然被同事吐槽丑，可它的确完美得实现了我想要的功能，是当之无愧的“树形菜单”王者。
zTree 的 API 相当复杂，尤其是属性和事件的种类，简直叫一个繁杂，这是大部分基于 jQuery 插件的一个特点。不过 zTree 的使用还是比较简单的，我们只需要提供一个 DOM 节点，一份 JSON 数据，zTree 就可以帮我们在界面上渲染出一个完整的树形菜单：
var data = res.Data; var zNodes = JSON.parse(data.TreeData); $.fn.zTree.init($(&amp;#34;#reportTree&amp;#34;), setting, zNodes); zTree 的节点是由 JSON 结构来定义的，其基本结构是{name:&amp;ldquo;节点名称&amp;rdquo;,children:[]}，父子节点采用相同的结构相互嵌套。例如，下面是博主所使用的数据结构：
[ { &amp;#34;id&amp;#34;: null, &amp;#34;name&amp;#34;: &amp;#34;全部报表&amp;#34;, &amp;#34;url&amp;#34;: null, &amp;#34;pId&amp;#34;: null, &amp;#34;viewUrl&amp;#34;: null, &amp;#34;children&amp;#34;: [ { &amp;#34;id&amp;#34;: null, &amp;#34;name&amp;#34;: &amp;#34;示例报表A&amp;#34;, &amp;#34;url&amp;#34;: null, &amp;#34;pId&amp;#34;: null, &amp;#34;viewUrl&amp;#34;: null, &amp;#34;children&amp;#34;: [ { &amp;#34;id&amp;#34;: null, &amp;#34;name&amp;#34;: &amp;#34;示例报表B&amp;#34;, &amp;#34;url&amp;#34;: null, &amp;#34;pId&amp;#34;: null, &amp;#34;viewUrl&amp;#34;: &amp;#34;/MyReport/List?</description></item><item><title>分享两种实现前端拖拽排序的方案</title><link>https://blog.yuanpei.me/posts/2436573863/</link><pubDate>Sun, 31 Mar 2019 12:49:37 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2436573863/</guid><description>Hi，大家好，在经历了两周多的 “写 Bug”、“改 Bug” 死循环后，又一个迭代终于在习以为常的加班生活中结束啦！联想到最近在 Github 上发起的 &amp;ldquo;996.icu&amp;rdquo; 事件，不禁令人由衷地感慨生活不易，所谓“”起风了，唯有努力生存”。其实，我反对是加班常态化所导致的无效加班，既然努力工作是为了更好的生活，可如果因此而模糊了工作和生活的界限，这到底是一件好事还是一件坏事呢？想想每个周末被工作群里消息支配的失落感，我希望我有可以自由支配的时间，即使我看起来比别人年轻，即使我下班后依旧孤身一人，因为用时间来换钱这件事情，着实是件性价比不高的事情，货币会一天天地贬值直至我们老去，可那些失去的时间就永远地失去了。好了，“业精于勤荒于嬉”，今天我们来说前端中实现拖拽排序这件事情。
其实，这件事情说起来挺尴尬的，我们曾经为用户提供过某种**”智能“**的体验，我们通过对用户的行为进行分析，为其推荐了个性化的菜单项，甚至根据用户的使用频率对菜单进行了排序。可事实上用户的反响并不是非常强烈，在经过一段时间的使用后，用户依然觉得这个功能相当地”鸡肋“，这件事情告诉我们一个真相，即无论是产品设计还是需求研讨，最好不要轻易地代入用户的角色。最终的结果是我们打算为用户提供自定义的功能，考虑到操作的便利性问题，我们放弃了那种通过上下箭头按钮进行排序的方案，这样就回到了本文的主题，如何在前端中对一组列表进行拖拽排序，最终我们选定了两组方案，它们分别是Nestable和Sortable。
Nestable 方案 Nestable 是一个基于 jQuery 的插件，是一个在 Github 上开源的项目，据作者声称，这是一个&amp;quot;拖放具有鼠标和触摸兼容性的分层列表&amp;quot;的方案。这里针对触摸兼容性的支持可以忽略不计，因为如今都 9012 年了，博主依然在做传统前端页面的开发，这里博主最感兴趣的一点是，它可以支持分层列表，换言之，我们的列表元素是可以有层级关系、是可以嵌套的，唯一令人有点不爽的就是它依赖 jQuery 了，在这样一个连 Github 和 Bootstrap 都在努力移除 jQuery 的时代，没有 jQuery 的历史包袱，意味着我们可以大胆地去做现代前端应该做的事情。好了，我们来看看 Nestable 具体是怎么使用的吧！首先，我们定义一个简单的 HTML 结构：
&amp;lt;div class=&amp;#34;dd&amp;#34;&amp;gt; &amp;lt;ol class=&amp;#34;dd-list&amp;#34;&amp;gt; &amp;lt;li class=&amp;#34;dd-item&amp;#34; data-id=&amp;#34;1&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;dd-handle&amp;#34;&amp;gt;Item 1&amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;li class=&amp;#34;dd-item&amp;#34; data-id=&amp;#34;2&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;dd-handle&amp;#34;&amp;gt;Item 2&amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;li class=&amp;#34;dd-item&amp;#34; data-id=&amp;#34;3&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;dd-handle&amp;#34;&amp;gt;Item 3&amp;lt;/div&amp;gt; &amp;lt;ol class=&amp;#34;dd-list&amp;#34;&amp;gt; &amp;lt;li class=&amp;#34;dd-item&amp;#34; data-id=&amp;#34;4&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;dd-handle&amp;#34;&amp;gt;Item 4&amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;li class=&amp;#34;dd-item&amp;#34; data-id=&amp;#34;5&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;dd-handle&amp;#34;&amp;gt;Item 5&amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;/ol&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;/ol&amp;gt; &amp;lt;/div&amp;gt; 接下来，我们可以使用如下的 JavaScript 代码来初始化整个列表，果然，一股 jQuery 风扑面而来：</description></item><item><title>《阿里巴巴 Java 开发手册》读书笔记</title><link>https://blog.yuanpei.me/posts/1122710277/</link><pubDate>Wed, 20 Mar 2019 12:49:37 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1122710277/</guid><description>最近利用闲暇时间从图书馆借了两三本书来“充电”，因为如果不及时摄取新的营养，感觉会越来越难有新的想法输出出来，尤其是像 ServerLess、组件化、分布式等等这样的场景慢慢开始接触，就势必无法再用从前的眼光去看待。大概去年的时候，阿里巴巴发布了「阿里巴巴开发手册」这本小册子，大概不到 100 页的样子，这次我就挑选了我觉得还不错的关键点，和大家简单分享一下，所以，这是一篇“典型”的读书笔记，下面的编号代表的是指定章节下的第几条规范，例如，1.1.2 表示的是第一章第一节中的第二条规范，欢迎大家一起讨论。
编程规范 1.1.2 代码中的命名严禁使用拼音与英文混合的方式，不允许直接使用中文的方式，纯拼音命名方式更要避免采用。
说明：英文不好可以去查，禁止使用纯拼音或者拼音缩写的命名方式，除了不能“望文生义”以外，对导致别人在调用接口的时候，向这种“丧心病狂”的编码风格妥协，这里不点名批评某 SAP 提供的 OA 接口，除了超级难用以外，每次都要花大量时间去对字段。
1.4.3 相同参数类型，相同业务含义，才可以使用 Java 的可变参数，避免使用 Object，可变参数必须放置在参数列表最后。
说明：例如一个接口同时支持单条更新或者批量更新，此时，完全就可以使用 param 关键字来声明相同的参数类型，而无须定义 InsertOne 和 InsertMany 两个方法。
1.4.4 对外部正在使用或者二方库依赖的接口，不允许修改方法签名，以避免对接口调用方产生影响。若接口过时，必须加@Deprecated 注解，并清晰地说明采用的新接口或者新服务是什么。
说明：对于过期的接口可以通过 Obsolete 特性来声明过期，这样在编译时期间可以告知使用者该接口已过期。对于 WebAPI 接口，除非有版本控制机制，否则一律不允许修改已上线的接口签名、参数和返回值。
1.4.17 在循环体内，字符串的连接方式使用 StringBuilder 的 append 方法进行扩展。
说明：这一点，在 C#中同样适用，因为字符串类型是 Immutable 的，对字符串进行拼接会产生大量的临时对象。
1.5.7 不要在 foreach 循环内进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式，如果并发操作，需要对 Iterator 对象加锁。
说明：因为 foreach 是基于迭代器(IEnumerator)的，在 foreach 循环内部修改集合，会导致 Current 和 MoveNext()发生混乱，早期的集合使用 SynRoot 来解决线程安全(内部原理是使用了 Interlocked 锁)，现在我们使用 CurrentBag 等线程安全的集合。
1.6.1 获取单例对象需要保证线程安全，其中的方法同样要保证线程安全。</description></item><item><title>聊聊前端跨域的爱恨情仇</title><link>https://blog.yuanpei.me/posts/3846545990/</link><pubDate>Tue, 26 Feb 2019 15:03:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3846545990/</guid><description>今天是过完春节以后的第二周啦，而我好像终于回到正常工作的状态了呢，因为突然间就对工作产生了厌倦的情绪，Bug 就像无底洞一样吞噬着我的脑细胞。人类就像一颗螺丝钉一样被固定在整部社会机器上，除了要让自己看起来像个正常人一样，还要拼命地让所有人都像个正常人一样。过年刚经历过被催婚的我，面对全人类近乎标准的“幸福”定义，大概就是我此刻这种状态。其实，除了想自己定义“幸福”以外，我还想自己定义“问题”，因为，这样就不会再有“Bug”了。言归正传，今天我想说的是前端跨域这个话题，相信读完这篇文章，你就会明白，这个世界上太多太多的问题，都和你毫无瓜葛。
故事缘起 年前被安排去做一个 GPS 相关的需求，需要通过百度地图 API 来计算预计到达时间，这并不是一个有难点的需求，对吧？就在博主为此而幸灾乐祸的时候，一个非常醒目的错误出现在 Chrome 的控制台中，相信大家都见过无数次啦，大概是说我们的请求受到浏览器的同源策略的限制。那么，第一个问题，什么是同源策略呢？我们知道，一个 URL 通常有以下几部分组成，即协议、域名、端口和请求资源。由此我们就可以引申出同源的概念，当协议、域名和端口都相同时，就认为它们是在同一个域下，即它们同源。相反地，当协议、域名和端口中任意一个都不相同时，就认为它们在不同域下，此时就发生了跨域。按照排列组合，我们可以有以下常见的跨域场景：
URL 说明 是否允许跨域 www.abc.com/a.js vs www.abc.com/b.js 相同域名下的不同资源 允许 www.abc.com/1/a.js vs www.abc.com/2/b.js 相同域名下的不同路径 允许 www.abc.com:8080/a.js vs www.abc.com:8081/b.js 相同域名下的不同端口 不允许 http://www.abc.com vs https://www.abc.com 相同域名采用不同协议 不允许 http://www.abc.com vs http://wtf.abc.com 相同域名下的不同子域 不允许 http://www.abc.com vs http://www.xyz.com 两个完全不同的域名 不允许 http://192.168.100.101 va http://www.wtf.com 域名及其对应的 IP 地址 不允许 那么，我们就不仅要问啦，现在微服务啊、RESTful 啊这些概念非常流行，在我们实际的工作中，调用第三方的 WebAPI 甚至 WebService，这难道不是非常合理的场景吗？前端的 Ajax，即 XMLHttpRequest，和我们平时用到的 RestSharp、HttpClient、OkHttp 等类似，都可以发起一个 Http 请求，怎么在客户端里用的好好的东西，到了前端这里就突然出来一个**“跨域”的概念呢？这是因为从原理上来说，这些客户端都是受信的“用户”(好吧，假装是被信任的)，而浏览器的环境则是一个“开放”**的环境。
URI_Syntax_Diagram 举一个例子，你在家的时候，可以随意地把手插进自己的口袋，因为这是你的私有环境。可是当你在公共环境中时，你是不允许把手插进别人口袋的。所以，浏览器有“跨域”限制，本质上是为了保护用户的数据安全，避免危险地跨域行为。试想，没有跨域的话，我们带上 Cookie 就可以为所欲为了，不是吗？实际上，同源限制和 JavaScript 没有一丁点关系，因为它是 W3C 中的内容，是浏览器厂商要这样做的，我们的请求其实是被发出去了，而它的响应则被浏览器给拦截了，所以我们在控制台中看到“同源策略限制”的错误。</description></item><item><title>基于 Server-Sent Events 实现服务端消息推送</title><link>https://blog.yuanpei.me/posts/3175881014/</link><pubDate>Fri, 18 Jan 2019 13:46:44 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3175881014/</guid><description>前段时间，为客户定制了一个类似看板的东西，用户可以通过看板了解任务的处理情况，通过 APP 扫面页面上的二维码就可以领取任务，而当任务被领取以后需要通知当前页面刷新。原本这是一个相对简单的需求，可是因为 APP 端和 PC 端是两个不同的 Team 在维护，换句话说，两个 Team 各自有一套自己的 API 接口，前端页面永远无法知道 APP 到底什么时候扫描了二维码，为此前端页面不得不通过轮询的方式去判断状态是否发生了变化。这种方式会发送大量无用的 HTTP 请求，因此在最初的版本里，无论是效率还是性能都不能满足业务要求，最终博主采用一种称为 服务器推送事件(Server-Sent Events) 的技术，所以，在今天这篇文章里，博主相和大家分享下关于 服务器推送事件(Server-Sent Events) 相关的内容。
什么是 Server-Sent Events 我们知道，严格地来讲，HTTP 协议是无法做到服务端主动推送消息的，因为 HTTP 协议是一种 “请求-响应” 模型，这意味着在服务器返回响应信息以后，本次请求就已经结束了。可是，我们有一种变通的做法，即首先是服务器端向客户端声明，然后接下来发送的是流信息。换句话说，此时发送的不是一个一次性的数据包，而是以数据流的形式不断地发送过来，在这种情况下，客户端不会关闭连接，会一直等着服务器端发送新的数据过来，一个非常相似而直观的例子是视频播放，它其实就是在利用流信息完成一次长时间的下载。那么，Server-Sent Events(以下简称SSE)，就是利用这种机制，使用流信息像客户端推送信息。
说到这里，可能大家会感到疑惑：WebSocket 不是同样可以实现服务端向客户端推送信息吗？那么这两种技术有什么不一样呢？首先，WebSocket 和 SSE 都是在建立一种浏览器与服务器间的通信通道，然后由服务器向浏览器推送信息。两者最为不同的地方在于，WebSocket 建立的是一个全双工通道，而 SSE 建立的是一个单工通道。所谓单工和双工，是指数据流动的方向上的不同，对 WebSocket 而言，客户端和服务端都可以发送信息，所以它是双向通信；而对于SSE而言，只有服务端可以发送消息，故而它是单向通信。从下面的图中我们可以看得更为直观，在 WebSocket 中数据&amp;quot;有来有往&amp;quot;，客户端既可以接受信息亦可发送信息，而在 SSE 中数据是单向的，客户端只能被动地接收来自服务器的信息。所以，这两者在通信机制上不同到这里已经非常清晰啦！
WebSocket与SSE对比 SSE 服务端 下面我们来看看SSE是如何通信的，因为它是一个单工通道的协议，所以协议定义的都是在服务端完成的，我们就从服务端开始吧！协议规定，服务器向客户端发送的消息，必须是 UTF-8 编码的，并且提供如下的 HTTP 头部信息：
Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive 这里出现了一个一种新的MIME类型，text/event-stream。协议规定，第一行的 Content-Type 必须是text/event-stream，这表示服务端的数据是以信息流的方式返回的，Cache-Control 和 Connection 两个字段和常规的HTTP 一致，这里就不再展开说啦！OK，现在客户端知道这是一个 SSE 信息流啦，那么客户端怎么知道服务端发送了什么消息呢？这就要说到 SSE 的消息格式，在 SSE 中消息的基本格式是：</description></item><item><title>博客图片迁移折腾记</title><link>https://blog.yuanpei.me/posts/3444626340/</link><pubDate>Fri, 18 Jan 2019 09:27:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3444626340/</guid><description>去年国庆的时候，七牛官方开始回收测试域名，这直接导致博客中大量图片出现无法访问的情况，虽然博主第一时间启用了新的域名：https://blog.yuanpei.me，可是因为七牛官方要求域名必须备案，所以，这件事情一直耽搁着没有往下进行。至于为什么会一直拖到 2019 年，我想大家都能猜到一二，没错，我就是懒得去弄域名备案这些事情:joy:。最近花了点时间，把博客里的图片从七牛和 CSDN 迁移了出来，所以，今天这篇博客，主要想和大家分享下这个折腾的过程，如果能帮助到和我一样，因为七牛官方回收了域名而无法导出图片的朋友，在下开心之至。虽然今天没有回望过去，没有给新的一年立 flag，就如此平淡地过渡到了 2019 年，可或许这才是生活本来的样子吧！
七牛的测试域名被官方回收了以后，我们有两种思路去导出这些图片，其一，是临时像官方提工单申请一个测试域名，这样在测试域名被回收前，我们可以直接使用官方提供的qrsctl或者qshell工具进行批量导出，因为此时我们可以直接在配置文件里配置测试域名，具体可以参考这篇文章：跑路之后七牛图片如何导出备份至本地，甚至你可以直接到七牛的管理控制台手动下载，可这样就一点都不极客了对吗？我们是一生追求做极客的人好伐。其二，同样是借助官方提供的qshell工具，因为没有域名，我们没有办法批量导出，可是工具中提供了两个非常有用的命令，它们分别是：qshell listbucket、qshell get。通过这两个命令，我们就可以列举出指定 bucket 中的文件以及下载指定文件，所以，这就是我们的第一步，首先把图片从七牛那里导出到本地。以博主的blogspace为例：
qshell account &amp;lt;ak&amp;gt; &amp;lt;sk&amp;gt; &amp;#39;qinyuanpei@163.com&amp;#39; /* 请使用你的ak/sk，谢谢 */ qshell listbucket blogspace 使用listbucket列举指定bucket内文件 事实上，通过第一列的 Key，即文件名，我们就可以下载该资源到本地，因为七牛实际上是采用对象存储的方式来组织资源的，这里我们以第一张图片05549344-BF85-4e8c-BCBC-1F63DFE80E43.png为例：
qshell get blogspace 05549344-BF85-4e8c-BCBC-1F63DFE80E43.png 默认情况下，该图片会下载到当前目录下，本地文件和远程文件名保持一致。当然，我们还可以通过-o 参数来指定输出文件：
使用get命令下载指定文件 好了，有了这个基础，我们就可以着手博客图片的迁移啦。博主最初的想法是，先获取到指定 bucket 下的全部文件，然后再对结果进行拆分，循环执行 qshell get 命令，可惜再 PowerShell 下并没有类似 grep 的命令，所以，这个想法放弃。其实，你仔细观察七牛图片外链的格式就会发现，除了域名部分以外，剩下的就是该文件在 bucket 里对应的 key 啦，所以，博主的想法开始从 Markdown 文件入手，最终我们的思路是，解析博客对应的 Markdown 文件，通过正则匹配所有的图片链接，截取出图片的文件名并通过 qshell 下载到本地。人生苦短，我用 Python。具体写出来，大概是下面这个样子：
def sync(root,ak,sk,account,bucket): files = [] children = os.listdir(root) for child in children: path = os.</description></item><item><title>基于 EF 的数据库主从复制、读写分离实现</title><link>https://blog.yuanpei.me/posts/2418566449/</link><pubDate>Thu, 18 Oct 2018 08:41:08 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2418566449/</guid><description>各位朋友，大家好，欢迎大家关注我的博客，我是 Psyne，我的博客地址是https://blog.yuanpei.me。在上一篇博客中，我们提到了通过 DbCommandInterceptor 来实现 EF 中 SQL 针对 SQL 的“日志”功能。我们注意到，在这个拦截器中，我们可以获得当前数据库的上下文，可以获得 SQL 语句中的参数，更一般地，它具备“AOP”特性的扩展能力，可以在执行 SQL 的前后插入相应的动作，这就有点类似数据库中触发器的概念了。今天，我们主要来说一说，基于 EF 实现数据库主从复制和读写分离，希望这个内容对大家有所帮助。
主从复制 ＆ 读写分离 首先，我们先来了解一个概念：主从复制。那么，什么是主从复制呢？通常，在只有一个数据库的情况下，这个数据库会被称为主数据库。所以，当有多个数据库存在的时候，数据库之间就会有主从之分，而那些和主数据库完全一样的数据库就被称为从数据库，所以，主从复制其实就是指建立一个和主库完全一样的数据库环境。
那么，我们为什么需要主从复制这种设计呢？我们知道，主数据库一般用来存储实时的业务数据，因此如果主数据库服务器发生故障，从数据库可以继续提供数据服务，这就是主从复制的优势之一，即作为数据提供灾备能力。其次，从业务扩展性上来讲，互联网应用的业务增长速度普遍较高，随着业务量越来越大，I/O 的访问频率越来越高，在单机磁盘无法满足性能要求的情况下，通过设置多个从数据库服务器，可以降低磁盘的 I/O 访问频率，进而提高单机磁盘的读写性能。从业务场景上来讲，数据库的性能瓶颈主要在读即查询上，因此将读和写分离，能够让数据库支持更大的并发，这对优化前端用户体验很有意义。
通常来讲，不同的数据库都在数据库层面上实现了主从复制，各自的实现细节上可能会存在差异，譬如 SQLServer 中可以通过“发布订阅”来配置主从复制的策略，而 Oracle 中可以通过 DataGurd 来实现主从复制，甚至你可以直接把主库 Dump 出来再导入到从库。博主没有能力详细地向大家介绍它们的相关细节，可博主相信“万变不离其宗”的道理，这里我们以 MySQL 为例，因为它在互联网应用中更为普遍，虽然坑会相应地多一点:)……
MySQL 中有一种最为重要的日志 binlog，即二进制日志，它记录了所有的 DDL 和 DML(除查询以外)语句，通过这些日志，不仅可以作为灾备时的数据恢复，同样可以传递给从数据库来达到数据一致的目的。具体来讲，对于每一个主从复制的连接，都有三个线程，即拥有多个从库的主库为每一个从库创建的binlog 输出线程，从库自身的IO 线程和SQL 线程：
当从库连接到主库时，主库就会创建一个线程然后把 binlog 发送到从库，这是 binlog 输出线程。 当从库执行 START SLAVER 以后，从库会创建一个 I/O 线程，该线程连接到主库并请求主库发送 binlog 里面的更新记录到从库上。从库 I/O 线程读取主库的 binlog 输出线程发送的更新并拷贝这些更新到本地文件(其中包括 relay log 文件)。 从库创建一个 SQL 线程，这个线程读取从库 I/O 线程写到 relay log 的更新事件并执行。 EF 中主从复制的实现 虽然从数据库层面上做主从复制会更简单一点，可在很多时候，这些东西其实更贴近 DBA 的工作，而且不同数据库在操作流程上还都不一样，搞这种东西注定不能成为“通用”的知识领悟。对开发人员来说，EF 和 Dapper 这样的 ORM 更友好一点，如果可以在 ORM 层面上做触发器和存储过程，可能 SQL 看起来就没有那么讨厌了吧！博主的公司因为要兼顾主流的数据库，所以，不可能在数据库层面上去做主从复制，最终我们是通过 EF 来实现主从复制。</description></item><item><title>戏里戏外的一出好戏</title><link>https://blog.yuanpei.me/posts/1127467740/</link><pubDate>Thu, 11 Oct 2018 09:02:05 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1127467740/</guid><description>上周国庆节放假，在家里看了黄渤的导演处女座《一出好戏》，虽然网上对它的评价还不错，可当我真正看完了这部电影，我反而觉得它更适合一个人的时候去看，因为在“演而优则导”的大趋势下，越来越多的人都在尝试“触电”，前有角逐奥斯卡的《逐梦演艺圈》，后有倒卖情怀的《爱情公寓》大电影，在这样一个充斥着“浮躁”的娱乐时代，有这样一类充满哲学意味的电影，可以说这是中国电影里的“净土”。不知道是不是因为年龄大了的缘故，对打打杀杀的动作戏显得视觉疲劳，有时候更愿意看点安静平淡的东西，这部电影所吸引我的，是某个镜头里无比空旷而迷离的故事和人们。
“一出好戏”这四个字一出，陡然间有种开了上帝视角的感觉。其实，我们总以为在看戏里的别人，我们又何尝不是这戏里的人？就像人们觉得大街上耍猴有意思，人们看着被驯化的猴子，遵从人们的指令做出各种动作，人们瞬间有了种睥睨众生的感觉，可明明人类就是由猿猴进化而来，想到这一点，你忽然觉得人类世界的奇妙之处，即人类以自身有限的认知，所勾勒和描绘出的这个世界，其实永远都是冰山一角，在某种情况下，这种体系的崩塌，我让我们不得不重新审视自我的合理性，所谓“你站在桥上看风景，看风景的人在楼上看你 。明月装饰了你的窗子，你装饰了别人的梦”。
整个故事架设在一个现代文明被摧毁的世界里，因为天外陨石落入海洋引发巨大海啸，男主马进及公司参与团建的众人，被困在一个与世隔绝的孤单上面。故事从男主颇具“屌丝”气质的自嘲开始，男主名叫马进，是一个在公司里基本没有存在感的人，连想给心仪的女生买饮料都要拐弯抹角，渴望通过买彩票实现一夜暴富的理想。这的确是一个普通社会阶层的真实写照，如男主所言，即便陨石真的落到地球上，像男主这样一穷二白的人，真的是穷到无所畏惧，可转眼间发现自己中了 6000 万彩票，同时流落到一个荒无人烟的孤单，如此喜剧性的一幕，无非是在质问我们，如果整个世界都遭遇崩塌，我们所在乎的这些，又到底该算作什么呢？
我们不妨看看众人的反应，面对这样一个突如其来的灾难，首先被诘难的是史教授，它代表的是某种社会权威。接下来，被诘难的是张总，它代表是组织权威。这其实可以理解为一个群体引发的信任危机，史教授作为这个集体中的高端人才，他必须在抚慰群众人情绪和顾及自身尊严这两点上找到一个平衡，所以，他的做法是向大家“部分地”承认错误。而张总从一开始的认为“有钱可以搞到船”，到最后在岛上绝望地散尽“千金”，这同样是因为群体性的信任危机，所以，第一个点是，群体性的“信任危机”往往代表着某种体系崩溃的开始。马进身上的矛盾恰恰就在于此，他带着一个可能已经崩塌的世界里的产物——彩票，来到了一个全新的世界里，而在这个世界里，彩票是 undefined，彩票映射的是作为一般等价物的货币，同样，货币在这个世界里是 undefined。
所以，电影所描绘的，其实更像是人类从无到有的演化过程。在众人缺乏食物和淡水的情况下，因为服过兵役而具备野外生存能力的小王，第一次成为了这个群体中的领袖。这象征的是人类历史上，以“体力”作为主要角逐指标的蛮荒时代。这里想吐槽下编剧的恶趣味，王根基这个名字实在是一言难尽。小王的策略是“想吃就自己干”，这其实是人类早期以采集狩猎为主的生活场景的反映，在这个阶段，对体力的重视超过对智力的重视，所以，史教授作为知识分子的身份第一次遭遇解离。人们普遍尊重劳动的价值，所以，两位没有参与劳动的“老总”，作为职场上司的身份遭遇第一次解离。同样，集体中身材最好的女性 Lucy，被老潘一把推给“王”，这是早期推崇“体力”的体系中，对性资源的一种绝对占有。
那么，在推崇“体力”的体系中，满足了人们的口腹之欲后，人们很快厌倦了这种满足感。以张总为代表的“智力”派，开始向着推崇“智力”的文明时代演变。在这个过程中，主要有两点，第一，是通过“期权”和“蓝图”发展出第一批员工。第二，是以扑克牌的形式定义了货币体系。这两点是整个社会转变的重要历程，第一点张总可以和“王”分庭抗礼的基础，而第二点则使得人们告别了“以物易物”的时代。张总利用马进想要离开孤单的心理，拉拢了岛上半数左右的人，可实际上他从未想过要带大家离开，这像不像现代职场里给期权的画饼方式？第二点，货币的出现让整个岛上的物品出现了流通。电影里有个人问，一张扑克牌能换多少鱼，张总回答说，这是有它的价值来决定的，这可以理解为经济学中的一个理论，即价格是由供需关系来决定的，故事到这里，我们已然看到了人类经济社会的雏形。
如果说在“体力”时代，人们角逐方式就是争斗，小到人与人之间的决斗，大到部落与部落间的战争，那么，到了“智力”时代，人们开始追求更高阶的竞争。或许竞争是万物的天性，所谓“物竞天择，适者生存”，从小就被教育这套弱肉强食的丛林法则，更是在宫斗、商斗等一众“争斗”里不亦乐乎，当真是“与天奋斗，其乐无穷；与地奋斗，其乐无穷；与人奋斗，其乐无穷”啊……那么，当岛上两拨人为了食物而大打出手的时候，到底是生存更重要还是道德更重要？为什么进化到“智力”时代的人们，依然要面对像强盗一样的“体力”时代的人们？人类历史上有多少自诩正义的行为，其实带来的都是流血和牺牲？所谓“鹬蚌相争，渔翁得利”，最终是马进在这场争斗中获利。
回头来看马进的行为，其实是群体中对优质资源的逐步占有的过程。如马进所言，只要是岛上生产不出的东西，都是宝贝。这里有两层意思，第一，物以稀为贵。第二，社会中的原始资源是有限的，只有不断创造新资源，整个社会的资源才能得以流通。虽然在电影中马小兴修坏了小王的水陆两用车，但其实电影想说的是，马小兴是所有人里唯一一个懂得现代科技的人，所以，马小兴的黑化，其实是马进人性中的阴暗面而已，当你真的拥有了摆布他人的能力，你是否又能守住道德的底线。
马进成为取代“王”和张总，成为新的领袖，从故事上来说，多少有点宗教的意味。首先，在光影的掩映下以高姿态俯视众人，这是神化的过程，马进从一个不被人待见的无名之辈，瞬间转变为充满神性的人，神到了什么地步呢？甚至连女神珊珊都开始主动向他求爱。众人穿着白色竖纹衣服的那一幕真的很美好，就连 Lucy 跳绳的镜头我都看了好几遍😉。有没有觉得马进一手持书的样子像极了耶稣，如果说耶稣通过圣经来向世人传道，那么马进对着黑暗中众人的一番演讲，是否具有类似的教化的作用，马进再一次“刷新”了活着的定义，引导人们去重见光明、寻找新大陆和重建家园，至此，我们仿佛过渡到了追求精神文明的阶段。
如果说，故事永远按照现在的方向去发展，那么或许在某个时刻人们就会回到“新大陆”，重新经历一次体系的重建。可生活从来都不是童话，导演用马小兴的黑化，完美地打破了这种想当然。或许有人会说，这是人类演化的必然，因为集体主义消亡以后，必然会导致精致的利己主义的产生，可即便如此，人们依然怀念着那个“罗曼蒂克”的时代，不是因为人们想要这样对别人，仅仅是希望别人这样对待自己而已。在张总主导的这个阶段，马进曾因为张总不愿意带大家离开，而被张总手下一通暴打，可其实他同样迷失在这份“岁月静好”中，所以，他不愿意让大家相信真的有大船经过，而这种内心的纠结，则完全交由马小兴来完成，主要事件有两个，其一是勒索张总资产，其二是小王“被得精神病”。电影快结束的时候，马进一直在重复“真的”还是“假的”，当一个人认识到这个社会的复杂性，当一个人在真真假假中来回穿梭的时候，或许他就会和马进一样，真的就分不出“真假”，可让马进走上神坛的电灯，和让小王变成“精神病”的电，难道不是一样的吗？
在圣经故事里，耶稣最后被他的门徒犹大背叛，最终被人们钉上十字架，这是不是和马进被马小兴背叛特别相似？最后，岛上只有马进和珊珊的那一幕，我个人更倾向于理解为，这是马进内心的真实写照，他愿意和珊珊一起在这个岛上生活，可他并不愿意让其他人永远困在这个岛上，所以，这是他心中的一种美好的想象，真实的结局是大家一起乘船离开了荒岛，可据说船上放烟花，据说是泰坦尼克号事件以后的一种约定成俗，意思是一种求救信号，所以，这艘大船并不存在？结尾，众人一起到医院看马小兴，马小兴暂时失忆，大概导演是不想他想起那些“阴暗”的记忆，可如果这是暂时失忆，就是说终究会再想起来的，那么，结合马小兴的经历，或许马小兴内心深处就是这样的？众人惊愕地停在一圈儿精神病人面前，大概每个人面对荒诞的时候，都不能确定自己这算不算荒诞吧，可谁叫这就是个反乌托邦的荒诞故事呢？你说，这故事在那只蜥蜴的眼睛里又会是什么样子呢？</description></item><item><title>使用 VSCode 作为 SourceTree 的 Diff 和 Merge 工具</title><link>https://blog.yuanpei.me/posts/3222622531/</link><pubDate>Sun, 30 Sep 2018 08:43:44 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3222622531/</guid><description>使用 SourceTree 有一段时间啦，从界面舒适度和易用性两个方面来看，的确要比小乌龟更好一点，日常配合命令行来使用，基本能覆盖到各种使用场景，譬如分支、版本、变基、合并等等。我本人在工作中接触到的 Git 工作流，大体上可以分为两类，从最早是官方所推崇的 5 个分支的 Git Workflow，到如今在 Github 上更为流行的 PR(Pull Request)。这两种方式，实际使用中各有优劣吧，而且这个话题似乎更适合专门写一篇文章来说。
我真正想说的是，我需要一个优雅的 Diff 和 Merge 工具。虽然，对一个使用命令行的人来说，使用 git diff 来展示差异对比已经完全足够啦，可在某些需要解决冲突的场合，命令行就显得有点力不从心。我个人一直不习惯小乌龟的合并工具，因为使用起来总觉得相当别扭。直到我发现，VSCode 可以在打开冲突文件的时候，自动提示解决冲突的选项，我觉得我开始喜欢上这个工具啦。所以，平时我解决冲突的做法是，在命令行里找到冲突的文件，然后逐一用 VSCode 打开来解决冲突。
现在，使用 SourceTree 的时候，周围同事大部分都习惯 GUI 操作，所以，就想能不能把 SourceTree 和 VSCode 结合着来用，因为我发现 SourceTree 可以支持外部的 Diff 和 Merge 工具。其实，小乌龟一样是支持的，关键是配置太难用啦！SourceTree 支持的 Merge 工具里有鼎鼎大名的 P4Merge，不过我发现一来官网完全打不开(需要翻墙)，二来界面相当复古我不喜欢，而 SourceTree 默认的 Merge 工具其实就是小乌龟里的，所以，请允许我如此任性的折腾吧！
首先，确保你安装了 VSCode，这显然是一句废话，可对于博主来说，这是唯一可以替代 Sublime Text 的代码编辑器，想想可以写 Markdown、写 Python、写 JS、写.NET Core，简直不能更美好了好嘛？然后，我们在 SourceTree 里做如下配置，这里我们直接让 VSCode 作为我们的 Diff 和 Merge 工具，具体参数如图所示：
SourceTree配置图示 好了，现在我们就可以在 SourceTree 里愉快地使用 VSCode 啦，感受一下这如德芙一般的纵想丝毫，从现在开始，彻底忘掉小乌龟那丑陋的合并工具吧！</description></item><item><title>记通过 EF 生成不同数据库 SQL 脚本的一次尝试</title><link>https://blog.yuanpei.me/posts/795474045/</link><pubDate>Mon, 17 Sep 2018 09:42:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/795474045/</guid><description>接触新项目有段时间了，如果让我用一句话来形容此刻的感受，大概就是**“痛并快乐着”。痛苦之一是面对 TFS，因为它的分支管理实在是一言难尽，无时无刻不在体验着人肉合代码的“趣味”。而痛苦之二是同时维护三套数据库的脚本，这让我想到一个梗，在讲到设计模式的时候，一个常常被提到的场景是，怎么样从设计上支持不同数据库的切换。我想，这个问题是非常容易回答的，真正的问题是我们真的需要切换数据库吗？原谅我的年少无知，我们的产品因为要同时支持公有云和私有化部署，所以在数据库的选择上，覆盖到了主流 MySQL、Oracle 和 SQL Server，这直接导致我们要维护三套数据库的脚本，你说这样子能不痛苦吗？而快乐的地方在于，终于有机会在一个有一定用户体量的产品上参与研发，以及从下周开始我们将从 TFS 切换到 Git。好了，今天这篇文章的主题是，通过 EF 来生成不同数据库的 SQL 脚本，这是痛苦中的一次尝试，所谓“痛并快乐着”**。
基本原理 我们知道数据库和面向对象这两者间存在着天然阻抗，这是因为两者在事物的认知上存在差异，数据库关注的是二维表、是集合间的关系，而面向对象关注的是封装、是细节的隐藏，所以，不管到什么时候，这两者都只能以某种尴尬的方式共存，SQL 执行效率高，这是以牺牲可读性为代价的； ORM 迎合了面向对象，这是以牺牲性能为代价的，所以，即使到了今天，关于 SQL 和 ORM 的争论从来没有停止过，甚至写 SQL 的人不知不觉间“造”出了 ORM，而使用 ORM 的人有时需要 SQL。所以，面对这样一个需要同时维护三套数据库脚本的工作，我个人倾向于用工具去生成，或许是出于程序员对“懒”这种美德的极致追求，或许是出于我对 SQL 这种“方言”天生的排斥，总而言之，我不是很喜欢手写 SQL 除非特别必要，因为它和正则一样，只有写得人懂它真正的含义。
那么，说到这里，我们就知道了一件事情，ORM 可以帮助我们生成 SQL，所以，我们为什么不让它帮我们生成不同数据库的 SQL 脚本呢？虽然 ORM 的性能总是为人所诟病，因为它严格遵循某种规则，所以注定做不到像人类一样“灵活”。我们始终认为不“灵活”的就是“笨拙”的，可即便如此 ORM 生成的 SQL 依然比人类写得要好看。故而，我们的思路是，在 ORM 生成 SQL 语句的时候将其记录下来，然后按照一定规则生成不同数据库的脚本。毕竟 SQL 语言更接近“方言”，每一种数据库的 SQL 脚本都存在着细微的差别。所以，后来人们不得不发明 T-SQL，可任何东西归根结底不都是权力和利益带来的附属品吗？人类为了互相竞争而形成差异化，可当一切差异都不甚明显时，最终又不得不花费精力来解决这些差异。可一个只有垄断存在的世界，除了让人想起 1984 里的 Big Brother 以外，还能想起什么呢？
尝试过程 好了，顺着这个思路，我们就会想到在 ORM 中添加拦截器或者是日志的方式，来获得由 ORM 生成的 SQL 语句，这里我们以 Entity Framework(以下简称 EF)为例，这是.NET 中最常见的 ORM，因为目前官方的 Web 开发框架有 ASP.</description></item><item><title>漫谈前端进化史之从 Form 表单到文件上传</title><link>https://blog.yuanpei.me/posts/2463121881/</link><pubDate>Wed, 05 Sep 2018 12:57:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2463121881/</guid><description>Hi，大家好，欢迎大家关注我的博客，我是 Payne，我的博客地址是https://qinyuanpei.github.io。今天这篇博客，我们来说说文件上传相关的内容。看到这里，大家一定觉得博主在技术上越来越没追求了吧，文件上传这种再简单不过的东西，真的值得博主你专门写篇博客吗？在介绍声明式 RESTful 客户端 WebApiClient 的这篇文章中，博主曾经提到，HTTP 协议中对文件上传的支持，主要是通过 multipart/form-data 来实现。因为这种方式是将文件视为一种特殊的键值对，所以对这种方式我本人不太喜欢。可作为标准的意义就是要忽略个人的情感因素，所以，在今天这篇文章中，博主更多的是想从 HTTP 协议(RFC2388)的角度来看待这个问题，即为什么它选择了 multipart/form-data 来实现上传，以及伴随着前端技术的发展它经历了哪些变化。
从 Form 表单说起 圣经上开篇就点明主旨，“起初神创造天地。地是空虚混沌。渊面黑暗”。一切的一切，都要从神创造天地开始，神说，要有光，这世上便有了光。那么，对于 HTTP 协议我们要从哪里开始说起呢。HTTP 的全称是超文本传输协议，所以，它设计的初衷是传输超文本类型的数据。什么是超文本类型的数据呢？从现代网页的组成，我们就可以知道，它不单单是指文本类信息，同时指图片、音频、视频等等一切可能的信息形式。可神奇的地方就在于，HTTP 协议是基于文本的协议，这意味着我们在网页中的信息交换，是借助某种文本类型的通信协议。顺着这个思路，最早我们在网页中交换信息的方式是什么呢？我认为是 Form 表单。想想看，我们在 Form 表单中输入信息，然后通过一个按钮将数据提交到服务器，服务器会对我们的请求做出响应。事实上，直到今天，我们的前端依然在采用这一机制。所不同的是，我们今天用各种组件替代了 Form 表单。
如果我们讲各种语言的&amp;quot;打印&amp;quot;理解为 Hello World，那么对前端而言最浅显的 Hello World 是什么呢？我个人以为是登录，想象一下，这是任何一个 Web 应用里都有的功能，我们输入用户名和密码以后，点击“登录”按钮就可以登录到系统。虽然，此时此刻的你我，都知道这是一个简单的 POST 请求，甚至对于用户名和密码这两个字段，我们有多种方法可以将其传递到服务器上。那么，各位是否知道，我们通过 Form 表单来登录时，这个过程中到底发生了什么呢？既然提到了登录，那么我们这里通过下面的例子来分析。
如你所见，这是一个相当“简陋”的 Web 页面。对一名后端开发人员而言，精致的 Web 页面就是一段被套在华丽外壳里的代码(不知道这样会不会被前端网红们打死)。所以，排除了样式相关的 CSS，可以让我们更加专注于核心原理。同样地，我们编写了一个简单的 Web API，来处理前端发送来的 HTTP 请求，这不是本文的重点，我们假设它存在且可以工作就好。
HTML结构/界面 这里已经说过，比起炫酷的 Web 页面和后端接口，我们这里更关心的是，登录时到底发生了什么。所以，大家都猜对了，通过 Chrome 自带的开发人员工具，我们可以捕捉到点击“登录”按钮时发出的 HTTP 请求，我们一起来看看它的报文内容是什么吧，相信大家都会有一种恍然大悟的感觉，让我们拭目以待吧！ encrypt为x-www-form-urlencode时的请求报文 通过这个报文内容，我们可以发现，“登录”实际上是一个 POST 请求，这是因为我们在 HTML 结构中声明了，Form 表单用什么样的方式去提交数据。而实际上呢，Form 表单默认的行为是 GET。我们同样会注意到报文中的 Content-Type 为 application/x-www-form-urlencode，它的特点是采用类似 key1=value1&amp;amp;key2=value2……的形式来提交数据，并且每一个 value 都会被编码。这样，我们就不得不提到 Form 表单的 encrypt 属性，它有三种基本取值：text/plain、application/x-www-form-urlencode 和 multipart/form-data。其中，text/plain 这种不必再说，因为它传递的是纯文本数据。而对于 multipart/form-data 来说，它的特点是采用一系列的 boundary 来分割不同的值，如果我们将示例中 Form 表单的 encrypt 属性设为 multipart/form-data，就会得到下面的报文内容，可以注意到，它和我们预期是一致的。 encrypt为multipart/form-data时的请求报文 或许大家会说，现在我们用 AJAX 来请求 RESTful 风格的 API 时，不都是用 JSON 作为数据交换的格式吗？对于这一点，或许我们可以理解为，Form 表单是封装了有限的 3 种 Content-Type 的 XHR 对象，所以，Form 表单足以让我们一窥 AJAX 最初的样子。虽然，我们今天已经不再主张使用 jQuery，但是熟悉 jQuery 的朋友一定知道这一点，即 jQuery 中默认的 Content-Type 示例上是 application/x-www-form-urlencoded。所以，即使我们今天有了全新的 Fetch API，可它依然脱离不了 HTTP 协议的范畴。可或许正因为如此，HTTP 中的文件上传多少像是某种妥协的产物。</description></item><item><title>基于 WebSocket 和 Redis 实现 Bilibili 弹幕效果</title><link>https://blog.yuanpei.me/posts/3269605707/</link><pubDate>Wed, 22 Aug 2018 14:07:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3269605707/</guid><description>嗨，大家好，欢迎大家关注我的博客，我是 Payne，我的博客地址是https://qinyuanpei.github.io。在上一篇博客中，我们使用了.NET Core 和 Vue 搭建了一个基于 WebSocket 的聊天室。在今天这篇文章中，我们会继续深入这个话题。博主研究 WebSocket 的初衷是，我们的项目上有需要实时去推送数据来完成图表展示的业务，而博主本人对这个内容比较感兴趣，因为博主有对爬虫抓取的内容进行数据可视化(ECharts)的想法。可遗憾的是，这些数据量都不算太大，因为难以支持实时推送这个想法，当然更遗憾的是，我无法在项目中验证以上脑洞，所以，最终退而求其次，博主打算用 Redis 和 WebSocket 做一个弹幕的 Demo，之所以用 Redis，是因为博主懒到不想折腾 RabbitMQ。的确，这世界上有很多事情都是没有道理的啊……
其实，作为一个业余的数据分析爱好者，我是非常乐意看到炫酷的 ECharts 图表呈现在我的面前的，可当你无法从一个项目中收获到什么的时候，你唯一的选择就是项目以外的地方啦，所以，在今天这样一个精细化分工的时代，即使你没有机会独立地完成一个项目，我依然鼓励大家去了解项目的“上下文”，因为单单了解一个点并不足以了解事物的全貌。好了，下面我们来简单说明下这个 Demo 整体的设计思路，即我们通过 Redis 来“模拟”一个简单的消息队列，客户端发送的弹幕会被推送到消息队列中。当 WebSocket 完成握手以后，我们定时从消息队列中取出弹幕，并推送到所有客户端。当客户端接收到服务端推送的消息后，我们通过 Canvas API 完成对弹幕的绘制，这样就可以实现一个基本的弹幕系统啦！
编写消息推送中间件 首先，我们来实现服务端的消息推送，其基本原理是：在客户端和服务端完成“握手”后，我们循环地从消息队列中取出消息，并将消息群发至每一个客户端，这样就完成了消息的推送。同上一篇文章一样，我们继续基于“中间件”的形式，来编写消息推送相关的服务。这样，两个 WebSocket 服务可以独立运行而不受到相互的干扰，因为我们将采用两个不同的路由。在上一篇文章中，我们给“聊天”中间件 WebSocketChat 配置的路由为**/wsws。这里，我们将“消息推送”中间件 WebSocketPush 配置的路由为/push**。这块儿我们做了简化，不再对所有 WebSocket 的连接状态进行维护，因为对一个弹幕系统而言，它不需要让别人了解某个用户的状态是否发生了变化。所以，这里我们给出关键的代码。
public async Task Invoke(HttpContext context) { if (!IsWebSocket(context)) { await _next.Invoke(context); return; } var webSocket = await context.WebSockets.AcceptWebSocketAsync(); _socketList.Add(webSocket); while (webSocket.State == WebSocketState.Open) { var message = _messageQueue.Pull(&amp;#34;barrage&amp;#34;,TimeSpan.FromMilliseconds(2)); foreach(var socket in _socketList) { await SendMessage(socket,message); } } await webSocket.</description></item><item><title>长安不见使人愁</title><link>https://blog.yuanpei.me/posts/3417652955/</link><pubDate>Fri, 10 Aug 2018 20:42:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3417652955/</guid><description>“大道如青天，我独不得出” 。这是唐朝大诗人李白在 《行路难·其二》 中的感慨，相比“长风破浪直济沧海”的豪迈，这首诗反而显得矫情啦，仿佛生活就应该陪着大家一起笑，我这种神经兮兮的文艺 Boy，就只有这片手机屏幕大小的地方，来说些不合时宜的冷笑话。这大概是博客能让我坚持写下去的理由，因为它的的确确是属于你的，那么请原谅我，因为我要在这里写点矫情的话。
上周一个人独自从公司办理完离职手续，原来那天下午是可以不用下班打卡的，这种感觉就好像是，你坚持并且在乎了很久的一件事情，在某一瞬间突然变得不再重要。我突然意识到，我居然已经在这个城市里生活了两年多。两年多是种什么体验呢？或者是曾经说过再见的朋友再没有见过面，又或者是曾经拒绝过你的女孩子终于结了婚，又或者是青龙寺里的樱花们开了一年又一年，又或者是遗址公园里石榴又从青色变成了红色……你脑海中的记忆越来越浅，而额头上的皱纹越来越深，记忆果真都被时间从额头上凿了去吗？
和朋友们聊天的时候，他们一如既往地感慨着自己婚后的月光生活，一如既往地羡慕着我接近他们三倍的高薪工资，总要在聊天快要结束时侯，一如既往地问我：真的不打算回来了吗？也许，等到银西高铁通车的时候，我终于不用再坐将近 12 个小时的火车回家。“鸟倦飞而知还”，可我是否是 《阿飞正传》 里的那一只，那只没有脚的鸟，它只能够一直飞呀飞呀，飞累了就在风里睡觉。我认识的人里，有从大学时代就在这个城市驻足的人，有远离故乡在这个城市扎根的人，无一例外的是，我们都离故乡越来越远，交通的便利和发达，并不足以弥补这种心灵上的距离，就像你从地图上看各个省份好像都离得不远。可没法在一起的人们啊，连最后一步都会觉得遥远啊！我不得不承认，没有人天生会是一个无忧无虑的漂泊者，无论是洛阳还是长安，对我而言都不是故乡。
早已忘记是从什么时候开始不吃辣的，真正令我感到神奇的，是这种习惯终于让我带去到不同的城市，就像你很难说清楚，喜欢一个人有多少是来自喜欢，又有多少是来自习惯。周末基本固定的去书店看书，看书的同时亦看匆匆的行人。小寨和钟楼永远不乏光鲜亮丽的男男女女，俨然是这个城市里最繁华的地带，人们的自拍无一不透露着时尚与精致，在某一瞬间，让我这个来自三四线小城市的人，相形见绌到沉默不言。想象下大唐盛世里的长安城，最繁华的市集无外东西两市，马亲王的 《长安十二时辰》 所展现的盛唐风物，对真实的历史而言，不过是雪泥鸿爪、惊鸿一瞥。如今西市为商业街所包围，一座大唐西市博物馆悄立其中，其形堪称寂寥否？西安遍地都是商场、购物中心，其盛堪比大唐否？
我曾经开玩笑地和朋友说，我现在喜欢观察路上行人们的穿搭，仿佛这样能让我喜欢的女孩子愿意看我一眼，朋友不无嘲讽地说，“你这是在东施效颦啊”，就连我喜欢的女孩子都说，“每个人都会有自己喜欢的风格啊”。可其实，我只有一点能确定，我明确知道我不喜欢那种风格，倘若真要问我喜欢什么，我真的不知道啊！不要以为只有女孩子，会在面对琳琅满目的商品时选择困难，在这个选择多样化的时代，明确知道自己想要什么，对每一个人而言，反倒是一种相当稀缺的品质。就像人只有长大了以后才会明白，做一个优秀的男人是多么困难的事情，做学生的时候比的是学习成绩，做男人的时候则是比社会化综合测评。初到长安“居之不易”的白居易，和此时的你我是何其相似，彼时长安是大唐的首都，此时西安是新新一线城市，历史啊，果然都是相似的嘛，所不同的只是当事人。
朋友们都希望我可以“自信”点，可终于有这样一天，你做到了曾经想做而不敢做的事情，这一切又是否真的会如你所愿。人啊，总是情愿活在借口里。我有位朋友常常“一语惊人”，简直就是“语不惊人死不休”的典型代表，他说，“不管男人的话还是女人的话，都不要相信”。大意就是说，人家就是那么随口一说，你这还当真了不是。姜文《让子弹飞》里有一个情节，小六子被诬陷吃了两碗凉粉却给了一碗凉粉钱，百口莫辩的小六子，不得已剖开肚子来自证清白。其实，世上好多事情都是没有道理的，你证明了你可以做到某一件事情又能怎么样呢？时过境迁，当一切都重新归于平静，也许人家就是那么随口一说，也许人家早都忘记了说过这句话，而你却守着这个可笑的执念等到花儿都谢了。人呐，偶尔狠下心来，是因为这样很爽吗？就像人们喜欢暴力一样。其实，真正的自信应该是温柔的，很多时候你以为的自信，无非是任性罢了。一个小孩子，会因为你帮他捡起掉在地上的扇子，而对你微笑，即使他还不会说话。
有时候，我会想人们对于一件事物的评价标准为何会存在差异，这是否是因为我们根本不了解自己。人类其实和猴子差不了多少，对这个世界总是充满好奇，似乎什么都想要去尝试。喜欢吃喝玩乐会被认为是懂得享受生活，而喜欢独处内省则会被认为是乏味无趣，可其实大家都是第一次做人，都是第一次面对这个存活了上亿年的星球。当北极圈里开始出现 30 度的高温，大概在这个世界上并不存在绝对的事情。当所有的标准都被满足，这是否意味着无论对方是谁都可以，我们明明都在执着于找寻唯一的东西，却为何选择了原本和唯一无关的标准。因为但凡有标准存在的地方，它们就注定难以成为独一无二的东西。就像 《小偷家族》 里的“父亲”，自认为什么都不会除了盗窃，却教会了祥太关于青春期的一切。很多时候爱不像我们想象地拥有一个标准模板，就像这个并不“标准”的家庭，却拥有足以打动我们的情感一样。或许，追求“标准”本来就是件愚蠢的事情，我们自以为个性独立，实际上永远被各种“虚拟”的东西束缚着，正如卢梭所言：“人生而自由，而无往不在枷锁之中”。
我想，李白抒发“不见长安”的愁绪时，大抵不会想到日后遇赦时的快意，更不会想到被玄宗逐出长安时的失意。可当你真的了解了你所要面对的人生，是否还有勇气会像现在这样选择。对于我的人生，我不知道今后会是什么样子，我唯一能做的，就是接受我已经失去的一切，长安并不足以安，你会有一个可以令你心安的人出现吗？长安不见，你愿意让我见到你吗？</description></item><item><title>使用 .NET Core 和 Vue 搭建 WebSocket 聊天室</title><link>https://blog.yuanpei.me/posts/1989654282/</link><pubDate>Wed, 01 Aug 2018 15:42:23 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1989654282/</guid><description>Hi，大家好，我是Payne，欢迎大家关注我的博客，我的博客地址是：https://qinyuanpei.github.io。今天这篇博客，我们来说说WebSocket。各位可能会疑惑，为什么我会突然间对WebSocket感兴趣，这是因为最近接触到了部分“实时”的业务场景，譬如：用户希望在远程视频通话过程中，实时地监控接入方的通话状态，实时地将接入方的响应时间、通话时长以及接通率等信息推送到后台。与此同时，用户可以通过监控平台看到实时变化着的图表。坦白地讲，这种业务场景陌生吗？不，每一年的双11，都能见到小伙伴们实时地“剁手”。所以，在今天这篇文章中，我们会以WebSocket聊天室为例，来讲解如何基于WebSocket构建实时应用。
WebSocket概述 WebSocket是HTML5标准中的一部分，从Socket这个字眼我们就可以知道，这是一种网络通信协议。WebSocket是为了弥补HTTP协议的不足而产生的，我们知道，HTTP协议有一个重要的缺陷，即：请求只能由客户端发起。这是因为HTTP协议采用了经典的请求-响应模型，这就限制了服务端主动向客户端推送消息的可能。与此同时，HTTP协议是无状态的，这意味着连接在请求得到响应以后就关闭了，所以，每次请求都是独立的、上下文无关的请求。这种单向请求的特点，注定了客户端无法实时地获取服务端的状态变化，如果服务端的状态发生连续地变化，客户端就不得不通过“轮询”的方式来获知这种变化。毫无疑问，轮询的方式不仅效率低下，而且浪费网络资源，在这种背景下，WebSocket应运而生。
WebSocket协议最早于2008年被提出，并于2011年成为国际标准。目前，主流的浏览器都已经提供了对WebSocket的支持。在WebSocket协议中，客户端和服务器之间只需要做一次握手操作，就可以在客户端和服务器之间实现双向通信，所以，WebSocket可以作为**服务器推送**的实现技术之一。因为它本身以HTTP协议为基础，所以对HTTP协议有着更好的兼容性，无论是通信效率还是传输的安全性都能得到保证。WebSocket没有同源限制，客户端可以和任意服务器端进行通信，因此具备通过一个单一连接来支持上下游通信的能力。从本质上来讲，WebSocket是一个在握手阶段使用HTTP协议的TCP/IP协议，换句话说，一旦握手成功，WebSocket就和HTTP协议再无瓜葛，下图展示了它与HTTP协议的区别：
HTTP与WebSocket的区别 构建一个聊天室 OK，在对WebSocket有了一个基本的认识以后，接下来，我们以一个最简单的场景来体验下WebSocket。这个场景是什么呢？你已经知道了，答案就是网络聊天室。这是一个非常典型的实时场景。这里我们分为服务端实现和客户端实现，其中：服务端实现自豪地采用.NET Core，而客户端实现采用Vue的双向绑定特性。现在是公元2018年了，当jQuery已成往事，操作DOM这种事情交给框架去做就好，而且我本人很喜欢MVVM这种模式，Vue的渐进式框架，非常适合我这种不会写ES6的伪前端。
.NET Core与中间件 关于.NET Core中对WebSocket的支持，这里主要参考了官方文档，在这篇文档中，演示了一个最基本的Echo示例，即服务端如何接收客户端消息并返回消息给客户端。这里，我们首先需要安装Microsoft.AspNetCore.WebSockets这个库，直接通过Visual Studio Code内置的终端安装即可。接下来，我们需要在Startup类的Configure方法中添加WebSocket中间件：
app.UseWebSockets() 更一般地，我们可以配置以下两个配置，其中，KeepAliveInterval表示向客户端发送Ping帧的时间间隔；ReceiveBufferSize表示接收数据的缓冲区大小：
var webSocketOptions = new WebSocketOptions() { KeepAliveInterval = TimeSpan.FromSeconds(120), ReceiveBufferSize = 4 * 1024 }; app.UseWebSockets(webSocketOptions); 好了，那么怎么接收一个来自客户端的请求呢？这里以官方文档中的示例代码为例来说明。首先，我们需要判断下请求的地址，这是客户端和服务端约定好的地址，默认为**/，这里我们以/ws为例；接下来，我们需要判断当前的请求上下文是否为WebSocket请求，通过context.WebSockets.IsWebSocketRequest来判断。当这两个条件同时满足时，我们就可以通过context.WebSockets.AcceptWebSocketAsync()**方法来得到WebSocket对象，这样就表示“握手”完成，这样我们就可以开始接收或者发送消息啦。
if (context.Request.Path == &amp;#34;/ws&amp;#34;) { if (context.WebSockets.IsWebSocketRequest) { WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); //TODO } }); 一旦建立了Socket连接，客户端和服务端之间就可以开始通信，这是我们从Socket中收获的经验，这个经验同样适用于WebSocket。这里分别给出WebSocket发送和接收消息的实现，并针对代码做简单的分析。
private async Task SendMessage&amp;lt;TEntity&amp;gt;(WebSocket webSocket, TEntity entity) { var Json = JsonConvert.SerializeObject(entity); var bytes = Encoding.UTF8.GetBytes(Json); await webSocket.SendAsync( new ArraySegment&amp;lt;byte&amp;gt;(bytes), WebSocketMessageType.</description></item><item><title>草食系程序员的穿搭指南</title><link>https://blog.yuanpei.me/posts/94443781/</link><pubDate>Wed, 25 Jul 2018 10:11:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/94443781/</guid><description>最近一直在看 《逃避虽可耻但有用》(逃げるは恥だが役に立つ) 这部日剧，当我们感慨各种脑洞都满足不了人类的好奇心时，日剧依然在老老实实地讲述着故事，即使这个故事离普通人依旧很遥远。可我认为，这是一部以轻喜剧为载体的温情剧，不管你是单身、恋爱中还是已婚，你都能从这部剧中找到自己对应的部分。所以，对于这部日剧而言，我个人是推荐大家去看一看的。原谅我不肯用我贫乏的语言去评价这部电视剧，因为我相信“此中不足为外人道也”。所谓“如人饮水，冷暖自知”，感情这件事情，懂的人自然会懂，不懂的人假装懂。
剧中男主津崎平匡是一个“典型”的程序员，因为外表无攻击性，一脸的人畜无害，而被女主森山实栗称为“草食系”男人。男主的长相在主流审美中或许谈不上帅，因为这个世界更欣赏的，是风见君这样帅气的男人。程序员群体木讷而内向的性格，其实都是大众给贴上去的标签。人们不喜欢被贴上各种标签，可人们喜欢给别人贴各种标签，因为这样子区分不同的人最省事儿。我们无法指责这个世界用五官和三观来割裂地看待一个人，我们唯一能做的，就是去改变留在人们心中的刻板印象。剧中男主在很多方面是比我们优秀的，向他学习不能保证我们会娶到 Gakki，可能让我们变得更优秀。
好了，下面就由我带大家一起来盘点男主在剧中的穿搭，所以，这是一篇总结向的草食系程序员穿搭指南。考虑到这部剧中室内场景比室外场景更多，季节主要集中在秋冬季，所以，我们将从环境、季节、种类等多个维度，对男主在剧中的穿搭进行盘点。活在一个看脸的时代最大的悲哀就是，那些长得比你好看，明明可以靠颜值，非要靠才华的人，永远都比你更努力。虽然津崎先生经常被人说“低情商”和“屌丝”，可我相信他比我们大多数“屌丝”要优秀得多。当然，这些优点需要大家在剧中去发掘。我只是希望，通过这种方式来提升自我。面对来自这个世界的恶意，争辩是没有意义的，你只能努力去纠正这种偏见。
室内篇 20180724012456548-101-2018725 1、深蓝色衬衣 + 深绿色休闲裤。作为职场日常穿搭，在第一集中出现，中年已婚男士池日在男主津崎面前炫耀“爱妻便当”，高情商的田沼先生替男主解围，安慰男主要好好吃饭。建议搭配：休闲皮鞋 + 一条优质皮带。同样地，我想说的是，一个人更要好好吃饭。
20180724012544572102-2018725 2、蓝色衬衣 + 西裤，俨然是雇主与雇员的上下级关系。女主森山实栗通过试用期考核，指令清晰、有条不紊给女主留下良好印象。作为职场常规搭配，搭配黑框眼睛，给人一种斯文儒雅的感觉，建议根据个人肤色，选择合适的颜色，具体来讲，如果你皮肤较白，建议选择明亮的色彩；如果你皮肤较黑，建议选择中性的色彩。
20180724012633880103-2018725 3、因为业务需求发生变更，男主被公司安排加班，在大家的共同努力下，项目终于按时完成，男主小心翼翼地在同事面前测试程序，衬衣领口的双色纹路，避免了视觉上的枯燥感，同事们在身后欢呼，男主深藏功与名，穿一件白衬衣，幻想自己是阿泰尔，千军万马避白袍，写程序没有 Bug。
20180724012656554104-2018725 4、每个程序员都会有一件格子衬衫，仙剑之父姚壮宪更是穿了一辈子格子衬衫。讲道理，男主穿格子衬衫难看吗？为什么程序员穿格子衬衫和特步鞋就要被黑到异次元呢？其实，只要不是浮夸的大格子衬衫，穿起来一样萌萌哒，关键是合体！当然，只要一胖就完啦。所以，穿搭是技巧，健身是根本啊。
20180724012839470105-2018725 5、女人变美只需要一只口红，而男人变帅只需要一条领带。男女主决定协议结婚后，召集双方父母商议结婚事宜。一套贴合肩线的西装，搭配一件白色衬衫，视觉上给人成熟稳重的感觉，男主虽然在剧中表现得很“怂”，可这并不影响他的“帅”啊，这套衣服最多算彩排，真正的新郎礼服请关注第 11 集……(嗯，这是最后一集，日剧追起来很快呦)
20180724012746230106-2018725 6、简洁到不能再简洁的短袖衬衣 + 牛仔裤。前一秒的踌躇满志，同下一秒的惊慌失措，莫名地戳中萌点，明明同事就在眼前，非要学人家卷福发短信。请女生们不要再吐槽男生穿衣服“土”，你告诉我，除了长裤和短裤我们还有什么？对了，短裤是不能穿的哦……，尤其是花花绿绿的那种🙃
20180724012906742107-2018725 7、蓝白相间的衬衣，相比普通蓝色衬衣，平添了一种活泼的感觉，就连工牌卡的绳子都来凑热闹。你知道怎么快速从人群中识别一名程序员吗？牛仔裤 + 双肩包 + 工牌卡。不，我拒绝这种符号化的穿搭，大隐隐于市，忘了这套新手村装备吧……当然，如果你包里还是各种数据线……好像换汤不换药啊(逃
20180724013156508108-2018725 8、任何领域都会鄙视链的存在，像津崎先生这样优秀的工程师，自然远非某某培训班的学生们。如何做一名优雅的学院派呢？你需要一件毛衣或者是一件马甲，而且一定要套在衬衣上。你问我为什么这么穿，因为通常教授们都这样穿，请参考卷福主演的电影《模仿游戏》，负责破译德军恩尼格码密码机的专家们，都是这样的穿着，同样的，还有《万物李军》里剑桥的教授们……
20180724013332938109-2018725 9、同样是毛衣和衬衣的搭配，圆领和 V 领是一种风格，是否翻出衬衣领又是一种风格。而我们的男主，显然可以同时驾驭这两种风格，再搭配一件休闲外套，试问还有谁？风见君帅是帅了，不过他的衣服好像永远都是针织衫啊，难道说有钱人都喜欢买一堆一样的衣服？恩，我说的就是老乔和小扎这种有钱人……
20180724013332940110-2018725 10、果然，有圆领就会有 V 领，强迫症对工牌卡挂绳莫名地充满好感，这个“V”字完美地贴合衣领。针织衫和衬衣，需要有一定的层次感，比如备受我们嫌弃的格子衬衫，如果搭配针织衫效果还是非常不错的，唯一的要求或许是肩膀不能过宽，因为这样会显得整体线条僵硬。我有一个问题，像女主这样宽肩膀的女生，穿一字肩真的不怕滑下来吗？😂
20180724013332943111-2018725 11、这种“假领”的毛衣，穿出来同样好看，我严重怀疑，这个创意是来自上海静安区同福里的老马。如果你的脖子比较长，可以考虑尝试下高领毛衣，请注意，我不是在教你，去做一名女装大佬。话说回来，衬衣上套毛衣最大的缺点是，需要挤上衬衣最上面的扣子，所以买衬衣时，请确保可以放入两根手指，这样子不会像《杀破狼 2》里的张晋一样被“帅”死。
20180724013332944112-1-2018725 20180724013332946112-2-2018725 12、这里分别是针织衫和毛背心搭配格子衬衫的正确示例，简而言之，衣服的搭配上需要体现出层次感，切忌选择色调过于接近的颜色，衬衣一定要修身，否则搭配毛衣会让你显得臃肿不堪。我要立一个 flag，等我瘦到 120 斤，我就奖励自己一件针织衫。
20180724013332949113-1-2018725 20180724013332951113-2-2018725 13、毛衣和针织衫真的是搭配率超级高的优质单品，穿出来真的非常好看。我知圆领 T 恤是夏天最常见的穿搭，可如果你想尝试下不同的风格，我建议你买一件衬衣或者是 Polo 衫或者是针织衫，这些都能带给你不一样的感觉。我一直想尝试皮夹克或者是牛仔外套，可我自我感觉不适合这样硬朗的风格，谁让我是一个温柔的蓝孩纸呢……
室外篇 20180724013619267201-2018725 1、这种材质的衣服应该很容易脏，而且大概率会让你显得臃肿(胖)，可不得不说，这一身和女主站一起挺搭的，我们学习穿搭只有两个目的，找到女朋友和不给女朋友丢脸(🙃)。作为围巾控，这身搭配我觉得可以尝试一下。
20180724013619269202-2018725 2、一个男人，只要有一件合身的西装，就已经在变帅的路上迈出一大步。这一款的话，毛衣黑白两种颜色，和衬衣蓝灰白的色调蛮接近的，所以基本上看不出层次感来。其实一直不明白男主为什么如此沉闷的颜色，难道是因为向女主表白以后变成熟了吗？😂
20180724013619273203-2018725 3、你看，这件衣服再次发挥了格子衬衫的伟大魅力，而在这件蓝色的针织衫的衬托下，可以明显地感觉到男主变“白”了，当 90 后们开始步入中年职场，不妨尝试穿一点靓丽的颜色，因为我们还可以再年轻一下。池日先生又讲了一句“名言”，你看津崎先生这震惊的小眼神。</description></item><item><title>邪不压正：本我的发现之旅</title><link>https://blog.yuanpei.me/posts/1099762326/</link><pubDate>Mon, 23 Jul 2018 10:48:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1099762326/</guid><description>一直想约朋友去看场电影，可是要找一部两个人都喜欢看的电影，当真是一件非常困难的事情。直到遇上了姜文的新片《邪不压正》，愿望终于在这个周末达成。说到姜文的电影，总是不可避免地提到“政治隐喻”这个词汇，所以，对这部电影而言，导演自成一体的独特风格，让其在与普通商业片拉开差距的同时，更将观众推向了一个略显尴尬的境地，以至于散场时朋友的第一反应是：好像完全没有看懂。
电影一开始，茫茫雪地里闪现出两个模糊的背影，向着雪地深处无限地延伸。而此时此刻，在火红的灯笼的映衬下，屋内一众人正忙着为师父庆贺寿辰，两位不速之客的到访，让一切瞬间化为烈焰中的修罗场。可以说，开篇这一场极具暴力美学的戏份，的确是可以吸引人眼球的戏份。姜文电影里有一种与生俱来的英雄主义，所谓的硬汉精神，于是你看到了身负东西方文化的李天然，是握着一把武士刀参与刺杀任务，而信奉武士道精神的根本一郎，果真是单刀赴会，说让三刀就是三刀。可这位武术名家，甚至连出手的机会都没有，就被手枪击中了头颅，武术在坚船利炮前又算得了什么呢？
不知道大家有没有注意到这样一个细节，朱潜龙和根本一郎闯到师父家里时，师父说了句：没听到狗叫，这是否是因为，在向师父祝寿时，跪拜的声音掩盖了炸弹的声音。联想到《让子弹飞》里，张牧之到鹅城上任，对老百姓说，“不许跪，皇帝都没有了，没有人值得你们跪……”。同样地，朱潜龙在师父面前，一样跪得可谓是以头抢地，可下一秒子弹就在师父脑袋上留下弹孔……其实，人蠢一点没有关系，毕竟都跪了几千年，可偏偏人还有点儿坏。师父问朱潜龙为什么日本人不在日本种植鸦片，朱潜龙说日本是文明的国家。
日本从明治维新以后，自上而下全面效仿西方国家，因为他们看到曾经最为强大的中华帝国，在鸦片和战争的侵蚀下早已满目疮痍。日本大河剧《坂上之云》里有一个片段，男主秋山真之在东京街头看到英国人欺负日本人，他愤怒地质问老师，为什么英国人在这里不讲绅士文化，他的老师不无遗憾地说，唯有强者有资格讲绅士文化。当时的日本不见得有多么文明，但那种全民参与到战争中的举动，在当时的世界格局里无出其右者，譬如日本曾担心和美国发生战争，起初民众讨论的是如何避免这场战争，后来则变成能否打赢这场战争，最后则变为如何打赢这场战争。
廖凡饰演的朱潜龙 朱潜龙在影片中是一个典型的汉奸，他帮日本人种鸦片，是希望在日本人的扶持下做个傀儡皇帝。在七七事变前，日本人借助麻姑囤事件，杀死了不愿意合作的张作霖，而朱潜龙自认为是大明后裔，一心想着要反清复明，可讽刺的是，溥仪在日本人的扶持下建立了伪满洲国政权，他居然天真地相信，日本人会允许两个傀儡政权同时存在。于是，在裁缝铺里李天然看到“龙袍”，导演不无幽默地说，这是准备去参加巴黎时装周的，仔细想起来，这是否是在讽刺某位穿着“龙袍”去参加电影节的演员呢？可朕的大清都灭亡了，你反什么清复什么明嘛，真有种《天龙八部》里慕容世家妄图兴复一个灭亡 100 余年的大燕国的痴狂劲儿。
姜文镜头下的北平城 姜文一心想要还原一个老北京的全貌，可我感觉在这部电影里看到的北京整体偏“白”一点，印象最深刻的地方是，老亨得利带着儿子从火车站回来，镜头里的北京好像刚下过雪一样。可或许是我们本不了解北京，故宫那种红墙青瓦的印象是从新中国成立以后的啦。梁思诚夫妇当年在战争中保护下来的古建筑群，或许本来就是这个样子的。于是，在姜文导演的镜头里，我们看到带着京味儿的北京胡同，看到了发生过无数故事的东交民巷，看到了曲折蜿蜒的八达岭长城，看到了古香古色的钟楼牌坊。李天然在屋顶跟踪朱潜龙的汽车时，我开玩笑地对朋友说，“以后刺客信条要出中国近代史系列游戏，完全可以参考李天然这个设定”。
姜文饰演的蓝青峰 这一次姜文饰演的蓝青峰，这个角色在我看来相当复杂：想要除掉朱潜龙和根本一郎，但私底下跟这两个人都有来往；和老亨得利有 25 年的交情，因为李天然身份暴露对其痛下杀手；被朱潜龙禁锢在家中无法自救，个人实力强弱被敌人查探地一清二楚；作为参加过辛亥革命的前辈，有且只有李天然一个下级……凡次种种，不一而足。从他的名字，我联想到“青出于蓝而胜于蓝”以及“青峰侠”，电影里李天然的英文名字叫做布鲁斯，他和师兄比武时致敬了李小龙的《龙争虎斗》，黑色的中国传统服饰，李小龙标志性的步法动作。可其实说到底，蓝青峰在精神上是懦弱的，因为他完全不清楚自己要做什么，那时国内外形势风起云涌，可他到底能真正地依赖谁，或许连他自己都不知道，他觉得李天然对他有用，就花了十余年时间去布局，李天然不过是他的一枚棋子……
蓝青峰的计划是让朱潜龙和根本一郎产生矛盾，朱潜龙杀死根本一郎后，再用李天然做交换。按照这个计划，李天然回国的确是来送死的，除非他可以在交换后杀死朱潜龙。蓝青峰害怕杀死根本一郎会引发战争，可从电影中来看，根本一郎并不是日军的高级军官。或许很多时候，人们都相信刺杀一两个人就可以让战争结束。全智贤在《暗杀》里说过这样一句话，“刺杀一两个日本人，能不能结束一场战争，我是不知道的，但我总要告诉人们，我们一直在战斗”。所以，即使李天然终于手刃仇人，卢沟桥的炮火依旧会在这个城市轰鸣。李天然凭借一腔热血，毫无来由地杀死了几个日本人，固然会让人激昂澎湃，可真的就是邪不压正吗？李天然的复仇，在我看来，是杀死懦弱的“自我”的过程，因为无父无母，李天然其实一直生活在“我是谁”、“我要去哪里”、“我要做什么”的自我怀疑之中，
许晴饰演的唐凤仪 唐凤仪，一个愿意陪着朱潜龙做皇帝梦的女人，习惯了被男人驱使和奴役，可被李天然恶作剧般在屁股上以后，她终于明白，自己在朱潜龙心中不过是一个玩物，尤其是六国饭店里的那场戏，看似不露痕迹地打朱潜龙耳光，实则这个敢爱敢恨的女人形象立了起来，回敬李天然的“凤仪之宝”，通过关巧红给李天然通风报信，日军进城时城墙上的一跃，都是这个角色留给人的深刻印象。所谓“商女不知亡国恨，隔江犹唱后庭花”，风尘女子的这种刻板印象，在姜文的电影里是不存在的，她们不单有性感的身姿，更有热血的灵魂。侵略者端坐在石狮子上准备拍照，被从城墙上一跃而下的唐凤仪撞倒在地上，当时电影院里发出一阵笑声，可这无非是一个女子的反抗而已。
姜文对老婆是真爱 与之相对的关巧红，她美好得宛如江南恬静的女子，她同样是一种独特的美感，和唐凤仪这种艳丽的画风不同，她是像迷一样的女子，背后有太多故事没有说完，看似惊鸿一瞥地讲了放脚、开裁缝铺这些琐碎的事情，但永远给人一种“这个女人不简单”的感觉，她好像无论什么时候，都能找得到李天然；她好像对李天然有种莫名的情愫，可又清楚地知道自己想要做什么……喜欢上这样的女人，就像喜欢上一朵云，你看云时很近，而云看你时很远。即使到了故事的结尾，她依然像阵风飘然远去，留下原地惆怅的李天然，明明李天然爬屋顶比她要好，可要寻找她时，又要去哪里寻找呢？有时候，这像是朴树的《那些花儿》散落天涯，有时候，又像是泰戈尔的“生如夏花般灿烂，死如秋叶般静美”……
说实话，这一次彭于晏的角色设定让人很出戏，因为这个角色本身的真实感并不强，即使他可以飞檐走壁，即使他可以躲开子弹。究其本质，是因为李天然身上有着勇敢而又懦弱的矛盾性格，未回国时，他一心想杀根本一郎和朱潜龙报仇；等回国后，他突然像被定住一般不知所措。第一次莽撞间接造成老亨得利被杀害，第二次莽撞直接导致蓝青峰被软禁。彭于晏一直都是一个“孤儿”，无论是师父、老亨得利还是蓝青峰，其实都不见得有多爱他。一个心中带着复仇愿望的人，一旦真正地手刃了仇人，他存在的意义又会是什么呢？所以，他怕自己因为复仇而变得迷茫，李天然看似身负正义之名，可对于师门武学的传承并无实际意义，相反，是那个杀死师父的朱潜龙，为师父塑像扬名，让师父成为大家所称赞的武术名家，到底谁是正？谁是邪？当周围人都是在利用你，杀了朱潜龙，李天然将失去存在感；而杀了李天然，日本人可以随时除掉朱潜龙。跪在岳飞目前的秦桧夫妇，和被塑成一条狗跪在武术名家塑像前，是否具有异曲同工之妙？普通人会在乎真相到底是什么样子的吗？
一个再简单不过的寻找“爸爸”的故事，对于那时的中国，是否就像年轻而莽撞的李天然，在探索着“我是谁”、“我要去哪里”、“我要做什么”的终极哲学命题。亨得利父子出城遇见正在演习的日本军官，对方声称亨得利父子的驴子挡住了坦克，破坏了军方的演戏计划。亨得利父子以美国护照作为挡箭牌，日本军官不得不去找这两头驴子的晦气。多年以后，吴京在《战狼》系列里重复着美国护照和海军陆战队的老梗，只是此时的中国早已不再是那个家国积弱的中国。日本军官质问李天然为什么穿着日本和服，可彼时彼刻，根本一郎自作聪明地曲解论语中的含义，又是否是在告诉我们，从外表上模仿何其容易，可一旦要张嘴说话，就很容易被人识破。曾经日本在全面欧化的过程中，被西方人讥讽为穿着衣服的猴子，我们都曾经模仿过他人，一如今天“韩式审美”在中国流行。这是一个时代里的众生相，愿每个人都能找到“真我”，不再犹豫，不再怯懦，勇敢地面对自己，发现自己。</description></item><item><title>声明式 RESTful 客户端 WebApiClient 在项目中的应用</title><link>https://blog.yuanpei.me/posts/380519286/</link><pubDate>Mon, 16 Jul 2018 09:02:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/380519286/</guid><description>自从项目上采用敏捷开发的流程以后，我们的开发任务中出现了不少“联调”的任务，而所谓的“联调”任务，完全是拜前后端分离所赐。通常来讲，按照前后端分离的思想，我们的团队会被分成前端和后端两个组，前端负责页面内数据的展示，后端负责提供相关服务的接口。这样听起来非常合理，对吧？可问题在于，后端常常在等前端联调这些接口，因为后端不知道具体有哪些异常需要处理；同样，前端常常在等后端接口稳定，因为一旦出现问题，就会导致接口发生变更。虽然在此之前，我们早已花了一周左右的时间去讨论接口，接口文档早已伴随着 API 部署到线上，可我们依然需要大量的时间去沟通每个接口的细节。用一种什么样的语言来描述这种状态呢？大概就是人们并不是真的需要接口文档，因为真的不会有人去看这东西。
从敏捷开发到产品架构 为什么会出现这种情况呢？我想，可以从三个方面来考虑，即设计不当、进度不一、沟通不畅。有时候集思广益去讨论一个接口，可能并不是一件好事，因为考虑的因素越多，问题就会变得越复杂，相应地妥协的地方就会越多。我并非不懂得做人需要适当妥协，事实是从妥协的那一刻起，我们的麻烦越来越多。有人问怎么能消灭 Bug，我说消灭需求就可以了。现代人被各种各样的社交网络包围着，以至于隐私都被赤裸裸地暴露在空气中，可你很难想象人与人之间的沟通会越来越困难，难道是因为社交网络加剧了人类本身的孤独？没有人是一座孤岛，可前后端分离好像加剧了这种界限。现在动辄讲究全栈，可当你把精力都耗费在这些联系上去，你如何去追求全栈？相反，我们像电话接线员一样，在不停地切换上下文，因为我们要“敏捷”起来，可作为工程师就会知道，切换上下文需要付出相应的代价。
我之所以提到这样一个场景，是出于对当前项目的一种整体回顾。我们的项目是一个客户端产品，但是它依然体现了前后端分离的思想。受业务背景限制，这个客户端采用了 Native + Web 的技术架构。如果你了解整个互联网产品形态的演变历程，就会对这种技术架构非常的了解，从曾经的 Native 和 Web 之争，到所谓的 Hybrid App，再到如今的 React Native 及小程序，这种技术架构其实一直都存在，譬如 Electron、Atom、Node-Webkit、Cordova、Ionic、VSCode 等等，其实都是非常相近的技术。对应到我们的项目，我们提供了一个 JSBridge 来完成 Native 层和 Web 层之间的通信，而客户端的渲染实际上是由前端来完成的，所以你可以想到，我们通过一个 WebView 来加载页面，而平台相关的交互由 C++/C#来完成，所以，理论上客户端是是一个和 Electron 类似的壳子(Shell)，它可以展示来自任何页面的内容。
以JSBridge为核心的系统架构图 从客户端的角度来讲，它是 Native 层接口的提供者，连接着平台相关的 API，并集成了第三方的硬件设备，所以，理论上它是和具体业务无关的。可实际上，因为 Web 层不能直接和文件系统交互，所以，像上传、下载这样本该由前端调用的接口，部分地转移到了客户端这边，所以，客户端无可避免地受到后端 API 变化的影响，因为业务上需求存在差异，上传接口前后共发生了三次变化，所以，客户端中存在三个版本的上传，当然，我相信这是一个设计上的问题，通过改进设计可以得到完美的解决。关于上传为什么会这么复杂，感兴趣的朋友可以通过留言来一起交流。这里我想说的是什么呢？因为客户端希望与具体业务无关，所以，客户端注定是以功能来划分服务，然后通过 JSBridge 暴露给 Web 层。可是对后端的微服务架构而言，它的服务是以业务为主导的，它的一个业务就是一个接口。由此导致一个问题，后端接口的数量不断增加，客户端面临频繁地改动。
不做平庸的 ApiCaller 有很多人说，今天的编程工作变得越来越简单，对于这一点我非常认同。因为，无论是无论是语言、工具、生态、平台，都获得空前的繁荣，所以，我们大多数人的工作，可能就是调用现成的 API，而少数人的工作，可能就是提供友好的 API，甚至连代码你都可以在 Google 上找到，你唯一要做的就是 Ctrl + C &amp;amp; Ctrl + V。当初想要改变世界的你我，突然有一天就变成了 ApiCaller，甚至大多数的框架，你连底层细节都无从得知。可你真的打算做一个平庸的 ApiCaller 吗？至少我是不愿意的，因为在我看来，调用后端提供的 API，大多数情况下都是换个 URL，或者换个参数，这样的代码你写一次以后，剩下的基本就是复制和粘贴了，你可能会非常鄙视我的这种行为，可事实就是这样的，不单单我在复制，连我身边的同事都在复制。可这能怎么办啊，只要后端提供了新接口，或者是对接口进行了调整，而这些接口必须由客户端封装，我们的工作就永远不会停止，可这不过调用后端的 API 而已啊！
有时候，我们会说工作经验和工作时间未必是正相关的，因为如果我们十年都在做一件事情，那么其实和一年是没有区别的。为了避免成为一个平庸的 ApiCaller，你必须思考那些真正重要的事情。怎么能降低后端 API 变化对客户端的影响呢？降低耦合度。怎么降低耦合度呢？依赖抽象而非依赖具体。想想 WebService，它通过 WSDL 来对服务进行描述，而通过 WSDL 就可以在客户端创建代理类，一旦 WebService 发生变更，重新生成代理类就好。再回想一下，调用后端 API 会遇到那些问题？设置 Header、设置 Cookie 、拼接 URL、拼接参数、URLEncode、SSL、JSON 序列化、FormData、上传文件、编码/解码等等，是不是每一次都在处理这些问题？看到项目里用 HttpWebRequest 去构造 Mulitpartfile 结构，我忽然间觉得绝望。既然每次都是翻来覆去这些东西，为什么要用手来写？API 文档构建工具可以帮助用户生成 curl 以及常见语言对应的代码，所以，我有理由相信，我们需要一个东西来帮助我们完成这个工作，就像 WebService 生成代理类一样。那么，有没有这样一个东西呢？这就是本文的主角——基于声明式的 RESTful 风格的客户端：WebApiClient。</description></item><item><title>米花之味：永远相信美好的事情</title><link>https://blog.yuanpei.me/posts/2941880815/</link><pubDate>Mon, 02 Jul 2018 09:50:17 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2941880815/</guid><description>一如往常地坐公交回家，下过一场雨以后，天空被洗刷得干干净净，洗去了夏天的骄阳似火，洗去了归途的月明星稀。抬眼瞥见对面车窗，一个被夕阳裁剪得整齐的轮廓，就这样静静地映在玻璃上，一瞬间散发某种神圣的气息。突然间，我想到了被嘲笑不会撩妹的 X 君，想到了自嘲不会拒绝别人的 Y 君，想到了喜欢一个人而爱不得的 Z 君……大概这个模糊的轮廓，可以是这个世界上任何一个人。
我想说什么呢？你的生命也许从来都平淡无奇，可因为这一场秋雨的到来，在别人的眼睛里，就突然平添无数的神圣感。原来温暖从来都要自己去寻找，即使太阳的寿命意外的漫长，不至于像星辰一般昙花一现，你肉眼可以看到的星星，可能下一秒钟就消逝不见，就像这每天都见到的夕阳，有一天意外地点缀些金色或者黄色，你就会觉得它浑身都是温暖的力量。其实，那是极其平淡的一天，就像米花的味道一样，没有什么太特殊的含义，可你总愿意相信，当一个人的心足够虔诚的时候，神灵就可以听到你的心声，让那些奇迹发生。
米花之味，这部小清新得不像国产电影的电影，从一开始，就表现出了它不同于以往国产电影的气质，比如电影中卖鸡蛋的小女孩，在电影中前后共出现 2 次，第一次是女主辞掉城里工作回来的路上，第二次是女主独自驱车前往机场的路上。如果说第一次是女主出于善良而买小女孩的鸡蛋，那么第二次则是对家乡现状的一种无力感。这基本上奠定了这部电影整体的基调，现实与传统的始终形成鲜明的对比，甚至是作为矛盾冲突穿插在母女两人中间，可导演似乎并不想通过这些来表达什么观点，所以这就造成这部电影单看画面质感是非常美的，可整部电影的立意实在不算太高。
那么，我们在电影里看到了什么呢？跳广场舞大妈的一脸生无可恋、村里人夸夸其谈的致富梦想、表面奉承背地里说人长短、学校老师会接受学生“贿赂”、油腻感十足的新郎、“瓜分”募捐来的善款、搞封建迷信“请神”……这些非常真实的人物，像一张巨大的网将母女俩裹在其中，看起来两个人的矛盾，是留守儿童这样一个社会问题，可在我的理解中，这是现实与传统的一种碰撞，留守儿童不再是印象中内向闭塞的孩子，而或许是跟我们一样，知道什么是“吃鸡”，知道什么是“王者农药”，小镇村民不再是印象中善良淳朴的人们，而或许是知道生病了应该去医院，但“喊魂”这种事情同样需要，而对于募捐来的钱，无论是个人还是集体，都希望能分一点儿。
影片中喃杭的小伙伴喃湘露，是因为错过最佳治疗时机而死，而第一个送孩子去医院的人，恰恰是她们不大喜欢的老师，大人们说要等机场修好，就可以坐飞机去外面治病，接近尾声时，人们看到头顶呼啸而过的飞机，不知道会不会想起喃湘露这个孩子。母女俩完成和解是因为喃湘露的死亡，借喃杭的话说，“她不相信喃湘露已经死了，甚至都感觉不到悲伤”，可在一开始，女主就告诉女儿，以后不要和喃湘露一起玩儿。“请神”的时候，人们说已经有 5 年没有去祭拜过石佛啦。为什么要祭拜石佛呢？因为人们相信如果不这样做，以后会有更多的麻烦出现，可当一个地方被开发为旅游景点以后，我们以往所珍视的那些传统，究竟是否能在现代文明的洗礼下保存完整？
村民穿戴着传统的民族服饰，携带着供奉神灵的物品，一起到山里祭拜石佛，可门口悬挂着的“Closed”的木牌，连同将村民隔绝在外的那把铁锁，又仿佛将故事带入了后现代主义的胡同。如果石佛真的可以庇佑一方黎民，为何会被旅游开发者的一道铁门拦截？如果石佛真的可以感受到人们的虔诚，为何一定要到山林深处去朝圣祭拜？就像喃杭问她母亲，“我们给神跳舞，神就一定会知道吗？”，女主回答说，“只要你的心足够虔诚，神就可以感受得到”。这恰恰印证了老贺的举动，一行人被景区前的一道铁门给拦了下来，正暗自沮丧的时候，老贺说，“既然来都来了，无论在哪里跳舞，佛都会看见的”。
对于生活本身而言，鸡汤固然没有什么实际的用途，可人们往往又需要鸡汤，因为心里缺少了一样东西，就会很容易地被其它的东西填满，而这种东西我们都叫做它信仰。你总要试着去相信点什么，不管是唯物的还是唯心的。有时候我们之所以会焦虑，是因为我们想要索取的东西太多。其实生命里少了某些东西又能怎么样呢？你羡慕别人做什么事情都有人陪伴，可当你尝试去和别人一起做一件事情的时候，你就会发现，即使看电影这样一件小事，都会存在千差万别，比如你喜欢看好莱坞视觉大片，而我喜欢看日式田园小清新，真要找一部两个人都喜欢看的电影，难免会引发我的选择困难症。
有时候，你分不清喜欢一个人，到底是喜欢 Ta 还是喜欢 Ta 的习惯，分开以后的情侣，某一天意外地重逢，你说着对方那时这样或那样的习惯，而对方苦笑着说早就不喜欢那样子啦，那么，你开始怀疑，对方是不是真的喜欢这些，无论你是不是存在……喃杭打伤了大嘴，就在老师陪着大嘴在医院接受治疗的间隙，她对母亲撒谎说，“老师已经和大嘴回去了”……然后就是母女两人的冲突爆发，周围人的风言风语，母亲对女儿学习、生活上的种种不满，女儿对母亲的那种疏离感，相互纠缠在一起。喃湘露平时都见不到父母，甚至开玩笑地说，等到生一场大病看他们怎么办，可她依然相信，没有母亲会不爱自己的孩子。
最令我动容的是，喃杭说要给她变一个魔术，然后喃湘露就见到了自己的父母，三个人，六只眼睛，有惊异、有辛酸，霎时之间全部涌上心头。“神婆”说米花米酒都变味了不好吃，大概是因为我们缺少了那种简单和纯粹，母女俩一起炸米花的时候，中间女主被叫去一段时间，喃杭炸的米花在翻动的时候，从中间破碎成两半，或许人的心原本如此，当有了隔阂的时候，即便是再简单的事情，都会做不好。老人说山里不许女人进去，母女俩终于决定亲自走进洞里去，忽然发现，神圣无比的石佛，不过是在一个寻常无比的钟乳石洞里，听起来清脆无比的声音，不过是游客随手丢弃在地上的易拉罐……
心中去敬畏一样东西，不是永远被表象迷惑而且不敢有所怀疑，而是相信科学的解释，同样敬畏一切超越人力的力量，我怀疑云南的女孩子都会跳舞，比如曾经表演过千手观音的杨丽萍老师就来自云南，张大胡子甚至为了找一双好看的手，而让她出演了史上最美的梅超风，于是佛像前的一段舞蹈，成为了不亚于何小萍操场独舞的惊鸿一瞥，假如真的有来生，就祈祷喃湘露出生在一个富足的家庭里吧！你问神真的会灵验吗？不，不要去问神，而是去问你自己，所谓“心诚则灵”，相信一切美好的事情，All is well。</description></item><item><title>基于 Docker 构建 .NET 持续集成环境</title><link>https://blog.yuanpei.me/posts/3995512051/</link><pubDate>Tue, 12 Jun 2018 17:53:59 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3995512051/</guid><description>最近在考虑将整个项目组的产品，努力向着持续集成(CI)/持续部署(CD)的方向靠拢，因为目前我们仅仅实现了基于 Docker 的自动化部署，而部署包的构建依然依赖于人工打包，而每个版本的测试和部署，基本上都要给所有相关人员发一遍邮件，而写邮件无非是填写版本号和变更历史。身处在这样一个社会化分工逐渐加剧的『摩登时代』，我们唯一的希望就追求技能的多元化，你越是担心有一天会被 AI 所替代，就越是应该去追求灵动与美。这个世界何尝不是一个运行中的大型机器，可恰恰就是这种掺杂了情感的冰冷法则，让我们意识到需要更多的理解和宽容。管理者常常迷信敏捷开发的人月神话，希望人可以像零件一样按部就班，在这场噩梦到来以前，为何不去做一点更有用的事情，让云计算帮我们解放双手。
背景说明 我们的产品，从结构上来讲，分为后端、前端和客户端三个部分，其中，后端提供了从认证到上传、查询和下载等主要的 AP 接口；前端提供了基于后端 API 接口的页面，主要功能是监控和管理；客户端承担了主要的业务交互能力，主要功能是整合常用的硬件资源。从技术上来讲，后端是基于 Spring Cloud 的微服务架构，前端是基于 node.js 的典型前端工具链，而客户端是基于 .NET / Win32 的技术体系。所以，即使我们的客户端是运行在 Window 平台上，我们依然有大量的服务是运行在 Linux 环境下。负责部署的同事不愿意单独再构建一套持续集成(CI)环境，所以我们决定借助 Docker 完成整个持续集成(CI)环境的构建。
构建过程 完成整个项目的构建，需要覆盖到代码编译、单元测试、静态检查、版本发布这四个基本环节，我们整体上使用 Jenkins 作为内部持续集成的平台，这意味着我们只需要在提交代码或者合并代码的时候，触发一个构建指令即可。这里我们考虑通过 Docker 来完成这些工作，一个整体上的设计思路如下图所示：
构建思路 MSBuild 首先是 MSBuild，它是我们整个构建流程中最重要的环节，我们平时通过 Visual. Studio 编译一个项目，背后其实就是由 MSBuild 这个构建工具来驱动，而通过 MSBuild 我们定义更多的构建流程，例如执行单元测试、实现 Zip 打包等等的流程。在 Window 平台下我们安装 Visual Studio 后就可以使用 MSBuild ，那么在 Linux 平台下呢？目前， MSBuild 已经被微软开源并托管在 Github 上，大家可以通过这个地址：https://github.com/Microsoft/msbuild来访问。通过阅读 MSBuild 的文档，我们了解到，目前 MSBuild 实际上有三个流向，分别是目前官方主推的 .Net Core 、传统的 .Net Framework以及由 Mono 托管的部分。
.Net Core 中 MSBuild 实际上被集成在 .</description></item><item><title>一个由服务器时区引发的 Bug</title><link>https://blog.yuanpei.me/posts/172426938/</link><pubDate>Tue, 05 Jun 2018 11:03:57 +0000</pubDate><guid>https://blog.yuanpei.me/posts/172426938/</guid><description>太阳照常升起，在每个需要挤公交车上班的日子里，即使窗外早已大雨如注。想来只有在周末，太阳会陪着我一起起床，所谓睡觉睡到自然醒，在雨天里保持晴天的心情，相当大的程度上，是因为今天不必上班。因此，一周里的心情晴雨表，简直就是活生生的天气预报，可惜我并不能预测我的心情，因为 Bug 会在某一瞬间发动突然袭击。一周前测试同事小 J 得到用户的反馈，我们某一笔订单突然无法从系统中查到，可就在数分钟前用户创建了这笔订单。前端同事小 Q 立刻追踪了这个问题，发现查询交易的接口调用正常，而后端同事小 L 确认数据库中是有这条交易记录的。于是，为了解决这样一个诡异的问题，几乎花费了大家大半天的时间。而最后的问题根源，居然充满了无厘头的意味，如本文主题所言，这是一个由服务器时区引发的 Bug。在这篇文章中，我想和大家聊一聊，关于时区以及日期/时间格式化的相关问题，希望大家会喜欢这个话题，就如同我希望大家会喜欢我一样。 可能大家都不会意识到时区会成为一个问题，因为对大多数中国人而言，我们唯一的时间概念就是北京时间。我们不得不承认，互联网在弱化了空间地域性的同时，无形中疏远了人与人之间的距离，尤其当我们处在一个分布式架构的时代，云的存在让我们的 Service 分布在无数个服务器节点上去，我们甚至意识不到它们的存在。比如我们在阿里云上选购主机的时候，阿里云会让我们去选择主机所在的地域，因为选择离自己更近的地域，意味着可以更快的访问速度。再比如像亚马逊这样的云计算服务商，会在国内(宁夏·中卫)部署自己的资源，这显然是为了服务国内用户。那么，我们不得不去思考一个问题，假如我们要同时服务国内、外的用户，那么这些 Service 可能会被同时部署到国内和国外的服务器上面。因此，我们就可能会遇到国内、外服务器时区不一致的问题，通常我们会以服务器时间为准并将其储到数据库中。此时，因为时区不一致，难免会产生本文中遇到的这个问题。
时区为什么会不同 既然时区是本文里的**&amp;ldquo;罪魁祸首&amp;rdquo;**，那么我们就会不由得思考这样一个问题，即为社么时区会不同。我们知道，地球是自西向东自转的，因此东边会比西边先看到太阳。相应地，东边的时间会比西边的早。这意味着时间并不是一个绝对的概念，即东边的时间与西边的事件存在时差。现实中的时差不单单要以小时计，而且还要以分和秒计，这给人们带来了不便和困扰。因此，1884 年在华盛顿召开的国际子午线会议上，规定将全球划分为 24 个时区(东、西各十二个时区)，其中以英国格林尼治天文台旧址作为零时区，每个时区横跨经度 15 度，时间恰好为 1 小时，而东、西第 12 时区各跨经度 7.5 度，以东、西经 180 度为界。每个时区内时间，统一以该时区的中央经线的时间为主，相邻的两个时区间总是相差一个小时，这就是时区的由来，时区的出现解决了人们换算时间的问题。 世界时区分布 事实上，时区的划分并不是一个严谨的事情，因为常常会出现一种情况，一个国家或者一个省份同时跨着 2 个或者更多的时区。以中国为例，中国幅员辽阔，差不多横跨 5 个时区，理论上在国内应该有 5 个时间，但为了使用起来方便，我们统一使用的是北京时间，即东八区时间。什么叫做东八区呢？即东半球第八个时区，其中央经度为东经 120 度。时区的计算非常简单，当你往西走时，每经过一个时区，时间会慢一个小时；当你往东走时，每经过一个时区，时间会快一个小时。例如，日本的东京位于东九区，因此，北京时间 2018 年 6 月 9 日 8 点整，对应的东京时间应该是 2018 年 6 月 9 日 9 点。这样，我们就会遇到一个非常有趣的问题，如果一个人到世界各地去旅行，它就需要不停地去将手表拨快或者拨慢，即使我们现在有了智能手机，它一样会提供不同时区的时间选择，假如我们偷懒选择了网络时间，那么它将永远和当地时间保持一致，因为我十分地确信，东京的运营商绝对不会选择使用北京时间。
数据库里如何存储时间 截至到目前为止，我们可以搞清楚的一件事情是，在不同的地域使用的时间是不同的，因为我们所使用的时间，本质上都是相对于格林尼治时间的相对时间，即使这些时间会因为地域存在差异，可从整个宇宙的角度来看，时间分明又是在绝对地流逝着，它对我们每一个人而言都是客观而公正的，当你发现时间越来越不够用的时候，你需要思考时间到底被浪费到什么地方去。我无意像霍金先生一样，去追溯时间的起源以及它的未来，在这篇文章里，我更关心的是，数据库里究竟是怎么样存储时间的，因为最根本的问题是，用户作为查询条件的时间，服务器上存储记录的时间，这两个时间的上下文发生了混乱。人类更喜欢在工作中不停地切换上下文，尤其是在面对无休止的会议、需求分析、Review 等等诸如此类的中断的时候，你是否会想到频繁地切换上下文，本质上是需要付出代价的呢？ Time is All 回到这个问题本身，我们现在来看看数据库中是如何存储时间的，这里我们选择三种最为常见的数据库来分析，它们分别是 MySQL、Oracle 和 SQL Server。
MySQL 对 MySQL 来说，它支持 YEAR、DATE、TIME、DATETIME 和 TIMPSTAMP 共 5 种数据类型。其中，</description></item><item><title>关于电影《暗杀》背后的故事和想法</title><link>https://blog.yuanpei.me/posts/2462008667/</link><pubDate>Fri, 01 Jun 2018 09:33:25 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2462008667/</guid><description>最近看过了由全智贤主演的电影《暗杀》，虽然说这是一部我们早已熟稔的抗战题材电影，可是在全女神颜值和演技的诱惑下，我终于还是花了点时间来看这部电影。或许是因为我们见识过了太多的**“抗日神剧”**，所以在面对这样一部电影的时候，我们难免带着某种不屑的眼光去审视它。可是当你看完了这部电影，突然间兴奋到难以自制，不由地惊呼一声：想不到韩国拍这种主旋律电影都能这么好看。我想，这是一种由视角转换所引起的代入感，我曾经看过日本拍摄的甲午海战，日本在明治维新以后，积极地向西方学习先进技术，从天皇到官员，都能从俸禄中省出钱来发展海军事业，相比之下，以天朝上国自诩的大清帝国则是李鸿章一人在支撑着岌岌可危的朝廷。同样地，通过《浪客剑心》同名电影，你会意识到，在明治维新这场变革背后，可能会有无数个像志志雄这样的政治牺牲品。这些都是因为视角发生变化而引起的变化，同样地，在这部电影中，它讲述了韩、朝两国人记忆中的抗日战争，这场我们曾经经历过的抗日战争，在韩国人眼中到底是什么样子的，这或许对我们看待历史会有所启示。
一明一灭，两条暗杀线 电影讲述了 19 世纪 30 年代，以安沃允(全智贤饰)为首的暗杀三人组，奉命刺杀日军驻朝鲜司令官及本国卖国贼的故事。故事发生在京城(即韩国首尔)和上海，时任韩国临时政府局务局局长的廉锡镇(李政宰饰)，在早年刺杀日军大将失败以后，暗地里早已投靠日本人，此时更将暗杀三人组的消息泄露给日方。一时间，暗杀三人组的刺杀行动和处决叛徒廉锡镇刺杀行动，构成了一明一暗两条线索，呼应了本片片名**“暗杀”，而廉锡镇更是找来赏金猎人“夏威夷手枪&amp;quot;(河内宇饰)，意图阻挠暗杀三人组的刺杀行动，可以说，整个故事脉络就像这个刺杀行动一样简单直接，因此其故事悬念就落在了刺客的刺杀计划如何落实以及反派的阻挠方案将如何开展**。所以，这个电影吸引人的地方就在于，即使你知道反派最终一定没有好下场，可不到全女神放下枪的那一刻，你总是不肯放下心来。因为即使是看双方斗智斗勇，在颜值与演技都在线的情况下，这一切依然显得赏心悦目。全女神在片中几乎承担了所有的动作戏份，可即便如此，李政宰在片中饰演的反派廉锡镇，风头一度不亚于全女神，这些我们后面再详细说。
电影中前期出现的场景都是在上海，可能有朋友会疑惑：为什么一部韩国拍的电影里会出现上海。这就要首先说说这段历史，当时韩国的临时政府是设在上海的，目的是方便金九、金元凤这样的革命志士开展救亡图存活动，因为当时的韩国(在南、北朝鲜没有分裂以前，指整个朝鲜半岛）早已笼罩在日军的军事统治阴影下。历史上，这段时期长达 35 年之久。日本是什么时候占领朝鲜的呢？没错，就是我们熟悉的中日甲午战争。当时北洋水师正是在运送陆军抵达朝鲜后的返航途中，与日本海军发生近代历史上第一次大规模铁甲舰海战。这场战争的结果我们都知道，北洋水师几乎全军覆没，清政府更是同日本签订了丧权辱国的马关条约。日本占领朝鲜半岛后，曾对当地人进行了惨无人道的屠杀，正是从那个时候起，朝鲜开始笼罩在日军的统治阴影之下，而流亡国外的临时政府，不得不寄居在上海的法租界，继续开展抗日活动。影片中的金九和金元凤，在历史上都可以找到记录。上海虹口爆炸事件，其实就是这部电影的历史原型，影片中二金的合作促成了三人暗杀组的成立，故事由此开始。
这个反派有点帅哦 李政宰饰演的廉锡镇，在电影一开始是以革命志士的形象出现的，他刺杀日军大将的任务失败，直接导致他在被捕后遭受严酷的刑罚。与此同时，间接导致了女主安沃允的母亲被亲日派父亲康寅国派人杀死，安沃允与双胞胎姐姐美津子分离，直至多年后，来自东北抗日武装的安沃允，和自幼在日占区长大的美津子，终于在一个屋檐下相认，可转眼间，姐姐就被卖国贼康寅国给杀死了，电影中全女神亲眼目睹姐姐死亡的那一幕真的是令人心碎。可偏偏是这样一个人，亲手挑选了这三名暗杀组的成员，亲手将刺杀行动的情报泄露给日方人员，尤其是他从衣兜里取出假信件投入火堆，伪造出信件被毁的假象这一幕。面对金九的怀疑，在明知手枪里没有子弹的情况下“以死明志&amp;quot;。对昔日的同志毫不手软，两个被金九派去刺杀他的同志，均被他重伤甚至杀死。面对曾经刺杀过的日军大将，他可以厚颜无耻地邀功请赏，并接受日军授予的爵位。可恰恰是这样一个人，在喝醉酒以后，诉说朝鲜各种武装力量各自为政的现实，忏悔把暗杀三人组送去送死。在严刑拷打面前，他做了叛徒，一如暗杀组成员干革命要给钱，这些或许没有那么伟光正，可它是那么的真实。
演技与颜值同时在线的全女神 全女神饰演的安沃允，是一个来自东北抗日武装的狙击手，一出场就瞬间狙杀四名敌人，身手当真是是不凡啊，更不必说端着汤姆生冲锋枪窜房顶跨屋脊，在负伤的情况下趴在疾驶的卡车引擎盖上。据说全女神电影中的动作戏都没有使用替身，一个明明可以靠颜值的人，尚且可以如此努力地去拼搏，那么身为普通人的你我，又有什么理由不努力呢？野蛮女友时期的全女神，或许看起来只是漂亮而已，而现在看来则是实力派。可她同样是一个憧憬着喝咖啡谈恋爱的少女，是一个看到姐姐洁白的嫁会衣泣不成声的妹妹，是一个面对亲生父亲无论如何都下不去手的狙击手。在假扮姐姐美津子参加婚礼以前，她做好了最坏的打算，就像她脑海中浮现过的画面一样，在敌人乱枪扫射下，献血染红了她洁白的婚纱……这或许是“夏威夷手枪”脑补的画面？这里有一个细节，“夏威夷手枪”将全女神送到医院以后，“夏威夷手枪”讨论起他对于暗杀行动的看法，全女神说了这样一段话，大意是“杀掉日军司令和汉奸康寅国，到底能不能让国家独立，这一点没有人会知道，但她必须要让人们知道，她们一直在战斗”……
这一刻，这个娇弱而坚强的女性形象就立起来了。全女神在本片中分饰两角，即姐姐美津子和妹妹安沃允，不过这种差异基本都是通过眼神表现出来的，姐姐身上有那种从小生活安逸的娇气，而妹妹身上有那种内敛冷静的帅气。战争从来都是残酷的，康寅国为了依附日本人，将美津子误认为安若允并杀死。在我看来，即便没有认错，以康寅国的为人，知道女儿和独立军有关联，他还是会这样做，因为女儿的幸福他完全不在乎，和日本人联姻无非是为了拉拢日本人。正如全女神所言，他用那双杀死了母亲的手，杀死了自己的女儿。人类的情感有时候就是这样诡异，一个对自己从来没有养育之恩的父亲，对方叛国投敌助纣为虐，即使两者间唯一的联系，是那可有可无的血缘关系，可最终还是需要“夏威夷手枪”，这个曾经是“杀父联盟”一员的人，替她开出这一枪。
一个超有力量感的故事结尾 故事从挟持人质这里开始，就突然变得敷衍起来，可能这里就需要感情戏来作为某种过渡，假如两个人真的去了米拉波，这就真的变成了爱情电影，可这部电影不就是，一部打着主旋律幌子的动作电影吗？这种类型电影为了增加娱乐性，是需要幽默和爱情的。真正将影片推向高潮的是结尾出的审判，证人在开庭前就被廉锡镇派人杀死，于是没有可以再证明，廉锡镇曾经投敌叛国、出卖同志的罪行。这个世界上永远有大量的无知的人，他们选择用暴力来面对一名“韩奸”，可当廉锡镇脱下衣服，义正言辞地讲述自己“支持”独立运动的事迹时，这些人突然开始宣布这名“韩奸”无罪，这是否说明大众都是愚蠢的，可正是这些人的想法，在左右着我们每一个人，这和那些努力制造“焦虑”的人没有区别，我们不愿意相信真相，宁愿相信自己早已固化地思维，或者是人云亦云，没有自己独立的判断，这实在是件可怕的事情。法官失落地宣布证据不足、廉锡镇无罪释放的时候，大概内心会有某种无可奈何或者是不甘心。
这让我想起 Unnatural 里高濑这个案件，因为没有办法证明对方杀人，而关键的信息又被久部泄露出去，所以，这个案件一度到了要修改鉴定报告的程度，这和身为法医的三橙心中的使命感不相符合，关键时候，是神仓所长坚持递交了原始的鉴定报告。当我们想要制裁一个人的时候，能不能依然客观地去证明对方有罪，不冤枉任何一个人固然值得赞赏，可为了让对方伏法而采用非正义的手段是否是正确的呢？如果身为法医的三橙，用修改鉴定报告的方式，给高濑这个罪犯定刑的话，我相信，我们所有人都会失望，因为她不愿意输给非正常死亡，不愿意正常的人被乱入非正常的事件，采用非正常的方法去伤害别人或者是自己。相比中堂使用逼供的方式查找真相，她更希望中堂医生以一名法医学者的身份去战斗。自然，故事的结尾，所谓善恶有报，16 年前的暗杀任务，终于在韩国光复以后，有安沃允和明宇重新执行，结尾处被乱枪打死的廉锡镇，在被问到为什么要出卖同志时，说了一句“我没想到会解放啊”，一句听起来像开玩笑的话，其实说出了战争年代人们的无奈，如果没有战争，或许这些事情就真的不会发生，可当战争机器被发动时，又有谁会想到这些呢？被卷入战争里人们没有选择，而发动战争的人从来不考虑以后。
写在战争结束以后 旷日持久的战争终于结束了，当画面定格到全女神那张近乎素颜的脸上时，她突然想起那些曾经最为亲切的面孔，想起“炸弹专家”黄德三，想起“速射炮”邱尚沃，想起酒馆老板娘……战争带给我们的是永远的伤痛，今天我们对于日本这个国家，可能有时候还会充满抵触情绪，但我想说的是，这场战争并没有结束，金九认为日本人已经投降，不再需要可依靠捐助维持，以光明正大地回到国内搞建设，可事实上像廉锡镇这样投日派，并没有完全得到清算，所以，金九在回国后不久就被韩国激进分子刺杀，廉锡镇所说的独立运动派系之争，在历史上是真实存在着的，金九就是被卷入到这场政治斗争中的牺牲品，所以，金元凤最终选择了朝鲜，而这种派系之争，更是加剧了整个朝鲜半岛的分裂，在这片土地上，曾经一起战斗过的兄弟、朋友，最终变成兵戎相向的敌人。
可这真的是和平吗？战争真的结束了吗？被 38 线分割开的这两个国家，一个通过韩剧、料理和科技为世界所知，一个更像是改革开放初期的中国，不知道还说神秘还是落后。何况，这条 38 线是停战线，并非某种和平的象征，而直至今天，这种刺杀的阴影一直笼罩在韩国政坛上，韩国现任总统朴槿惠的父亲和母亲先后都死于刺杀，所以，即使战争结束了，就能换回和平吗？就能抚平人们心中的伤痛吗？朝鲜与韩国，也许在我们有生之年里，都难以看到他们真正地握手言和，就像苏联解体以后不会再联合在一起，欧盟并非想象中的牢不可破，爱尔兰和北爱尔兰原本就是一家，印度和巴基斯坦是殖民战争的遗留问题……战争，带来的坏处，永远比好处要多。我们向往铸剑为犁的和平生活，可战争结束以后，是否真的能带来和平，人心中的伤痛需要多久可以愈合，人与人的相争逐利之心需要多久可以平息。
2018 年的儿童节，同往年不同，因为许嵩为炮火中的叙利亚孩子们，创作了一首新歌《大千世界》，这首歌以 2017 年 4 月 15 日叙利亚炸弹袭击事件为背景，呼唤爱与和平，控诉那些肆意发动战争，而将无辜孩童卷入战火的人们。大千世界里的大人们，不要忘了你们曾经都是孩子，当人们都在通过晒娃这种方式度过儿童节时，你是否会想到在世界的某个地方，有人在穿着捐助的衣服和玩具的同时，更是被迫享受着温柔的暴力，我们从小给小孩子的玩具枪，是否有一天会真的变成荷枪实弹呢？我们在盛世之年，我们在贫富之间，我们在虚实交错路口，不断找寻，任何形式的相遇。愿大千世界，再无战争，再无暴力，愿每个深爱的人，都能被温柔对待。</description></item><item><title>爱情像一场霍乱</title><link>https://blog.yuanpei.me/posts/3782208845/</link><pubDate>Tue, 22 May 2018 09:05:34 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3782208845/</guid><description>距离读完马尔克斯的《霍乱时期的爱情》这本书，差不多已经有一个月左右的时间啦。相比小说中错综复杂的人物关系，更加让人印象深刻的或许是“百年孤独”式的开头。不论是多年后面对行刑队的布恩迪亚上校，还是拍打鹦鹉结果从梯子上摔下来的乌尔比诺医生。在这一刻，因为人物的过去与现在层叠出的这种时空感，或许就是马尔克斯想要去描绘的魔幻现实主义。最近看了由小说改编的同名电影，感觉对这部小说的印象更为具体化，小说的时间跨度将近半个世纪，是选择乌尔比诺和费尔米纳这样稳定的婚姻关系，还是选择阿里萨和费尔米纳这样偏执的爱情故事。我想，这是一个值得去思考的问题吧。
当乌尔比诺从梯子上摔下来即将离开人世的时候，他拼尽最后一口气对费尔米纳说：“只有上帝才知道我有多爱你”。单单从这句话来看，他们两个人或许是相爱的。可明明不久前，两个人还在为了一块肥皂的事情而争吵。现在大家对出轨这个问题看得特别重，重要到不要说是肉体出轨，连精神出轨都是不能被原谅的。从去年至今，网络上各种出轨的舆论消息层出不穷，好像爱情越来越不值得期待。可你看乌尔比诺和费尔米纳的婚姻是什么样的呢？乌尔比诺在妻子外出期间出轨了一名黑人女子，虽然他选择主动向妻子承认出轨，及时回归家庭，可在我们这些外人看来，这样的婚姻是含有杂质的婚姻。从妻子费尔米纳的角度，她结婚以前是不吃茄子的，而结婚以后则适应了茄子，你可以说两个人在一起，一定会有一方选择妥协。可在一个动辄讲三观、讲兴趣、讲地位的年代，你是否会觉得两个人合适呢？
合适，是一个特别巧妙的词汇，巧妙之处在于它真正可以做到“以不变应万变”。乌尔比诺夫妇的婚姻，或许是大多数人的真实写照。两个人第一次见面，源于费尔米纳的一场疾病。当时外面正流行着霍乱疫病，费尔米纳因为怀疑被感染了霍乱不得不寻找医生治疗，恰好乌尔比诺正从巴黎旅行回来。在医学技术不发达的年代，医生是没有听诊器的，所以乌尔比诺必须贴着费尔米纳裸露的胸部听心跳。书作和电影中都详细地描绘了这个过程，两个青年男女在这种情况下发生了身体上的接触，费尔米纳更是被对方身上的男性气概所吸引。可这算是爱情吗？我更愿意相信，这是一种原始的欲望冲动，可你说这两个人间没有爱情，估计所有人都会反对，谁让我们都喜欢用海枯石烂表示爱情的忠贞，这两个人在一起生活 50 年，甚至作者都表示：一对恩爱的夫妻最重要的不是幸福，而是稳定的关系。
所以，不管你愿不愿意承认，爱情最终都会部分地转化为亲情，爱情本身是有瑕疵的、有缺陷的，争吵不可避免，犯错不可避免。诚然，我们都希望对方忠诚的对待自己，可人归根到底是一种对自我忠诚的动物，你说你不能接受对方变心，可人、时间和空间无时无刻不在发生着变化，喜欢或者不喜欢，不过是某一瞬间的状态，你必须相信，爱情本来就不完美、充满瑕疵，可这就是真实的爱情的样子啊，人们会记得你婚礼上的海誓山盟，唯独不会记得你每天柴米油盐的平平淡淡；人们会给他们愿意看到的表面现象去点赞，唯独不会关注你是不是真正的快乐。爱情里有人不厌其烦地寻找真爱，有人沉溺在回忆里不敢再触碰爱情，对我来说，这两种选择我都表示尊重，因为爱情本来就有它真实的样子。
我不知道，还会不会有人为了别人而苦等 51 年 9 个月零 4 天，一个人究竟有多大的勇气和执念，才能从一个朝气蓬勃的青年变成一个白发苍苍的老人。金庸先生的名篇《射雕英雄传》里，神算子瑛姑因为失去爱子而一夜白头，我想，这其中有对段皇爷见死不救的怨恨，有对周伯通求而不得的执念。可对阿里萨而言，从他遇见费尔米纳那天开始，他的生命就仿佛注定是属于她的，他坚持给她写信，在楼下为她拉小提琴，在长椅上刻下她的名字，甚至是喝花露水、吃玫瑰花。如果说爱情像一场霍乱，应该会没有怀疑，因为阿里萨的确像是生了一场霍乱，不然怎么会疯狂地爱直至偏执甚至有些荒唐。我完全可以理解阿里萨的举动，因为年轻时的我们都曾做出过类似的举动。我并不反对这样的爱情，可当你回头来看这两个人的爱情的时候，费尔米纳对阿里萨这个人几乎一无所知，除了知道对方的职业是报务员。
电影中费尔米纳甚至给阿里萨回了信，可就如同费尔米纳所言，“他们两个人之间只有虚幻，爱情蒙蔽了彼此的双眼”，大概所有一见钟情的人都没能考虑一个问题，那就是你真的了解对方这个人吗？非常不幸的是，即使亲近如父母、妻子和丈夫这样的关系，一个人也永远不可能了解另外一个人。一个人究竟要爱得多卑微，才会心心念念地等着对方的丈夫死掉，甚至怕对方比丈夫先死掉。假如阿里萨只是这样痴痴等待 50 年的话，我们最多只是替他感到惋惜而已，可偏偏阿里萨为了“报复”费尔米纳，缓解被她伤害的心，开始一次又一次地疯狂纵欲，在肉体的狂欢中不断强化精神层面上对费尔米纳的爱，据他自己记载，他和寡妇、少妇甚至少女都发生过关系，可当他终于等来费尔米纳的时候，他声称自己是一个处男。
人常常复杂到让你我怀疑人生，而阿里萨则是一个复杂到，让你觉得他还有点可怜的人。他视其它女性的肉体如无物，唯独将费尔米纳推上女神的圣坛。更微妙的是，费尔米纳居然是喜欢过阿里萨这个人的。她不过是在乌尔比诺和阿里萨间选择了更好的一个而已，可阿里萨这种病态的爱在我看来是极为自私的，因为无论两个人多么地爱彼此，一旦出现这种肉体的出轨，就意味着永远无法挽回。虽然费尔米纳选择嫁给了乌尔比诺，可假如有一个人在别人的身体上出轨无数次，在你的丈夫逝世以后告诉你，他等这一天已经等了 51 年 9 个月零 4 天，我不知道你会作何感想。我没有任何的封建思想残留，我尊重女性在丈夫死后改嫁的自由，可选择这样一个充满“缺点”的人，我觉得还是需要去认真想一想的。
两个人如果真心相爱，即便是满头白发的蹒跚老者，我认为结婚都是没有问题的，可当两个 70 多岁的老人坦诚相见时，当阿里萨看到费尔米纳干瘪下垂的胸部时，当各自看到对方充满皱纹和赘肉的身体时，我真的想知道，这 50 多年的等待真的值吗？或许是值的的，就像这两个人喜欢的都是有点幻想成分的对方一样，我向往永远靠精神慰藉彼此的帕拉图之恋，也不排斥男欢女爱的肉体之欢，可无论哪一种都必须建立在真实的现实中，一个虚幻的爱慕者，一个你并不真正了解的人，当幻想被打破的一瞬间，或许就是爱情破碎的时候，所以，我希望我们对待感情更慎重些，即使没有人爱你，学会自爱未尝不可。我们的生命原本就短暂，何苦要将这生命浪费在别人身上，况且我们有时候我们就像费尔米纳一样，分不清到底是爱还是孤独，人在经历枯燥和乏味以后是会变的，会变得对事物充满新鲜感，即使是曾经不喜欢的东西。
从某种角度而言，阿里萨是成功的，因为他用一生的时间得到了喜欢的女人。可我时常觉得人生有比这更重要的事情，就像你小时候看到喜欢的东西，却发现自己买不起的时候，你会怎么样做呢？我想大多数人都会选择不要了或者是等以后有机会再买。可人就是这样奇怪的动物，明明以前非常非常喜欢，可突然有一天发现咋再喜欢不起来。为什么我们对这件事情可以坦然接受，唯独在面对感情的时候常常无法自拔呢？你当初有没有得到这样一件喜欢的东西，或许会影响你在未来的人生轨迹，可在大多数情况下，我们的生命实在泛不起多少涟漪。有人说，人生下来的时候，结局就早已注定，我们唯一能做的事情，就是努力去填补和丰富这五六十年的时间。这样说来，人生实在是没有什么事情非做不可的，如果有，那只有一件事情，那就是努力地活下去。
《Unnatural》里中堂系一直对恋人的死无法释怀，整整八年时间一直都在调查恋人的死亡原因，直到真相被查明，得到恋人父亲的原谅。我不是说，人生不可以有执念，我只是希望大家明白，执念只会让你太关注结果而忽略过程，而我们的生命是需要一天天去度过的。就像阿里萨终于得到了费尔米纳，可两个 70 多岁的老人，在这个世界上还有多少时间可以挥霍呢？我倒情愿日子过得稍微慢一些，用一辈子的时间去了解对方，我们一直所希望看到的，不就是被人理解和认同吗？如果永远一个可以同你交流灵魂的人，那么就努力学习一个人去生活，人生没有那么多必须做的事情，只有你愿不愿意去做的事情。起风了，就当努力生存。活着不好吗？我实在不愿意再看到罗密欧与朱丽叶这样的悲剧，虽然我们都曾歌颂过这样的故事，可只要活着就会有新的机会啊。
有时候想想我们父母这一代人，几乎在毫无准备的情况下，被动地步入了婚姻的殿堂。时隔多年以后，或消融在柴米油盐的平淡里，或交织在子女亲情的羁绊中，或穿行在流水光阴的得失间……直至爱情彻底消亡最终变成亲情，像一滴松胶油慢慢变成一颗精美的琥珀。有人说，婚姻是爱情的坟墓，甚至结过婚的人会觉得婚姻非常无趣。那么，罗曼蒂克是否一定会消亡？如果是，是不是婚姻里有没有爱情都可以，因为总有一天它会枯竭，人生里充满太多无可奈何的事情，单单是爱情这一件事情就可以写满整个历史，爱情里有像童话一般美好的故事，同样有像悲剧一般哀伤的故事。
或许是我们这一代独生子女们，在接触到更广阔的网络世界后，极大地影响了我们对这个世界的认知，以至于我们觉得自己就是活得太明白了。可如果要这样稀里糊涂地度过余生中的五六十年，每个人突然间又不甘心接受这残酷的命运。当我发现，我要远离父母生活，甚至完全能力能力和精力照顾他们的时候，我很容易地想到我的未来，是不是会和他们一样。有人说，婚姻是为了找到一个人陪你一起往前走，可我们这些独生子女们，早就习惯了一个人去生活，生命里总是充满着无尽的变故，或许她曾经特别特别喜欢你，可突然有一天她就不再喜欢你了；或许你们曾经是特别特别友好的朋友，可突然有一天对方就突然离你远去；或许你们朝夕相处亲密无间，可到最后突然发现根本不了解彼此……
人明明都是会变的，可偏偏总爱把希望寄托在变化的事物上面。在一个周围一切都在变化的世界里，追求一成不变毫无疑问是贪心的，我们能追求的只有稳定，可难免会问一个不理智的问题：稳定可以理解为爱吗？这正是乌尔比诺和费尔米纳两个人的感情生活留给我们的谜题。年少时或许会憧憬阿里萨这样因爱成痴的故事，可正如村上春树所说，“哪里会有人喜欢孤独，不过是不喜欢失望罢了”。如果爱情是一场霍乱，我希望每个生病的人，都能尽早地从这场疾病中治愈。“起风了，当努力生存”，就像石原里美饰演的三橙说过的，“有时间绝望还不如去吃点好吃的呢”，比起找到心爱的人，学会如何爱自己不是更重要吗？</description></item><item><title>使用 Jexus 实现 ASP.NET 在 Linux 平台下的部署</title><link>https://blog.yuanpei.me/posts/815861661/</link><pubDate>Sun, 20 May 2018 14:00:03 +0000</pubDate><guid>https://blog.yuanpei.me/posts/815861661/</guid><description>Hello，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是：https://qinyuanpei.github.io。今天想写一点关于 Linux 部署 ASP.NET 相关的话题，为什么突然想写这个话题呢？因为就在几天前，我被我所认识的一位前辈深深地鄙视了一番，原因是我依然在使用一个落后的 IoC 框架——Unity，在如今已然是公元 2018 年的今天。我突然想到，距离.NET Core 2.0 发布已经有一段时间，而.NET Core 3.0 的 roadmap 已经开始提上日程，可我好像还没来得及认真地去对待这个现状。我一直在关注跨平台和跨语言的技术，就像我在大学里的时候就开始接触 Linux 一样，未来我们要面对的是种类繁多的终端平台，从 PC 时代到移动互联网，再到 VR、AR、IoT 和 AI，有太多太多的事情在悄然发生着变化。偶尔我的内心会泛起焦虑和迷茫，可在时光蹉跎直至褪色以前，我或许只是变回了曾经的自己。既然要如同涅槃一般重新开始，为什么不首先重新拾起曾经关注的领域呢？所以，在这今天这篇文章里，你将看到：如何使用 Jexus 实现 ASP.NET 在 Linux 平台下的部署。
故事背景 我们项目组在开发这样一种服务，它可以通过收集招聘网站的简历来提取相关信息，而这些信息将作为训练集供 AI 算法使用。考虑到 Python 在 AI 领域的优势，我们决定采用 Python 来开发自然语言处理相关的业务，而简历的收集则是通过.NET 中的 Web Service 暴露给前端。整个开发相对顺利，可是在部署环节出现了问题。因为项目组以往的的项目都是部署在 Linux Server 上，所以在部署 Web Service 的问题上产生了分歧，负责运维的同事不愿意为这一个项目而单独配置一台 Windows Server。这里需要说明的是，采用.NET 来开发 Web Service 的一个重要原因是，这些简历中存在大量 Word 文档(.doc/.docx)，因此不得不采用 Office 提供的 COM 组件来支持文档的解析，虽然后来证明的确是这些 COM 组件拖了跨平台的后腿。所以，在这个时候，我们面临着两种选择，第一种方案是采用 Windows Server 来部署，我们的运维同事表示不开心；第二种方案是采用 Linux Server 来部署。我们知道.</description></item><item><title>使用 SonarCloud 为.NET/.NET Core 项目集成静态检查</title><link>https://blog.yuanpei.me/posts/4891372/</link><pubDate>Sat, 12 May 2018 01:16:52 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4891372/</guid><description>Hi，朋友们，大家好，欢迎大家关注我的博客，我是 Payne，我的博客地址是http://qinyuanpei.github.io。在不知不觉间，5 月份已然度过大半，最近无论是读书还是写作均停滞不前，被拖延症支配的我深感有虚度时光之嫌。今天这篇文章，我将为大家介绍如何使用SonarCloud，来为.NET/.NET Core 项目集成静态检查。如果大家使用过SonarCube的话，对接下来我要讲的内容一定不会感到陌生，因为SonarCloud实际上就是SonarCube的“云”版本。在云计算概念深入人心的今天，我们可以通过互联网来访问各种各样的服务。譬如，我曾经为大家介绍过的 TravisCI 就是一个在线的持续集成(CI)服务。这些云服务可以让我们不再关心基础设施如何去搭建，进而集中精力去解决最核心、最关键的问题。和持续集成关注“持续”不同，静态检查关注的是代码质量。目前，SonarCloud 支持**.NET Framework 4.6以上及.NET Core版本。通过这篇文章，你将了解到SonarCloud 的基本使用**、SonarCloud 与 TravisCI 的服务集成这两方面的内容。
SonarCloud 静态检查，顾名思义就是通过扫描源代码来发现代码中隐藏的缺陷，譬如潜在的 Bug、重复/复杂的代码等等，这些通常被称为代码中的“坏味道”，静态检查就是通过工具去扫描这些“坏味道”。Sonar 是一个基于 Java 的代码质量管理工具，由 Sonar 和 SonarScanner 两个主要部分组成，前者是一个 Web 系统用以展示代码扫描结果，而后者是真正用以扫描代码的工具。Sonar 具备良好的扩展性，众多的插件使得它可以和 Jenkins 等集成工具结合使用，同时可支持不同语言项目的扫描分析。在.NET 中我们可以使用Stylecop来进行静态检查，无独有偶，ReShaper中同样提供了静态检查的特性。在这篇文章中我们主要使用 Sonar 来作为.NET 项目的静态检查工具。
通常使用 Sonar 来构建静态检查工具时，需要我们在本地搭建一套运行环境，而 SonarCloud 是针对 Sonar 推出的一个“云”版本。我们只需要执行脚本就可以完成代码分析，而分析的结果则可以直接在 SonarCloud 网站中看到。这就是“云计算”的魅力所在，我们无需关心 Sonar 是如何安装以及配置的，当我们需要使用这种服务的时候直接使用就好了。目前，SonarCloud 对开源项目是免费提供的。因此，如果你不想亲自去搭建一个静态分析的环境，那么你可以选择使用 SonarCloud 来对代码进行静态分析。SonarCloud 支持 17 种语言的扫描分析，支持和 Travis、VSTS、AppVeyor 等 CI 工具集成，甚至你可以在 SonarCloud 上找到大量实际的项目。
我对 SonarCloud 感兴趣的一个重要原因是，它可以和 TravisCI 完美地集成在一起，而且在此之前，我曾经使用过一段时间的 Sonar。在使用 SonarCloud 前，我们需要注册一个账号，这里建议使用 Github 账号授权登录，因为我们需要授权给 SonarCloud 来拉取代码，尤其当你使用 TravisCI 来集成 SonarCloud 的时候。除此之外，我们需要准备好以下工具：</description></item><item><title>罗马数字与阿拉伯数字的相互转换</title><link>https://blog.yuanpei.me/posts/4158690468/</link><pubDate>Mon, 30 Apr 2018 10:59:46 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4158690468/</guid><description>最近遇到一道非常有趣的题目，题目大意如下：有一个富翁在银河系里做生意，而银河系使用的是罗马数字，所以他需要一个精明能干的助手，帮助他完成罗马数字与阿拉伯数字的相互转换，题目在这个背景下衍生出交易场景，我们需要帮助他计算出相关商品的价格。对于这道题目，如果剥离开这个题目本身的交易场景，这道题目本质上就是一个纯粹的算法问题。说来惭愧，博主当时并未能快速地解决这个问题，事后通过研读别人的文章始能有所领悟。所以，今天想在这篇文章里，同大家一起来讨论下这个问题。今天，全世界都在使用 0 到 9 这 10 个阿拉伯数字，比阿拉伯数字早 2000 年的罗马数字。为什么没有流传下来为后世所用呢？我觉得这是一个非常有意思的问题，数学同计算机学科间那种千丝万缕的联系、技术演进过程中若有若无的某种必然性……这些都是令我觉得非常有意思的地方。那么，一起来看看这个问题可好？
罗马数字起源 罗马数字，顾名思义，就是古罗马人使用的数字系统。在罗马数字中，共有 7 个基本数字，即 I、V、X、L、C、D、M，它们分别表示 1、5、10、50、100、500、1000。可以注意到，在这套数字系统中，0 不被视作是一个整数。据说，曾经有一位罗马学者不顾教皇的反对，执意将与 0 相关的知识以及 0 在运算中的作用向民众传播，因此被教皇囚禁并投入监狱，理由是 0 是一个邪物，破坏了神圣的数。同样罗马数字无法表示小数(注：罗马数字有分数的表示方法，可仅仅能表示 1/12 的整数倍)，因此罗马数字常常用来表示纪年，在欧洲国家的古书籍、建筑和钟表中，我们都可以见到罗马数字的身影。我们熟悉的元素周期表，同样采用了罗马数字来表示元素所在的&amp;quot;族&amp;quot;。需要说明的是，罗马数字是一种计数规则，而非计算规则，这意味者罗马数字是没有进位和权重的概念的，所以一般罗马数字只用以计数而不用以演算。
既然罗马数字是一种计数规则，那么我们就不得不说一说它的组合规则，因为 4000 以内的数字，都可以用这 7 个基本数字组合表示。具体来讲，罗马数字的基本规则有以下 4 条：
重复次数：**一个数字重复多少次，所表示的数字就是这个罗马数字的多少倍；一个罗马数字最多重复三次。**这条规则该怎么理解呢？第一点，I、II、III 分别表示 1、2、3；第二点，4 必须被表示为 IV，而不是 IIII。关于 4 的表示方法，在历史上一直存在争议，一种观点认为 IIII 这种写法占用书写空间，IV 可以达到简化书写的作用；而一种观点则认为 IV 有亵渎神灵朱庇特、含不敬侮辱之意。 左减原则：当一个较小的数字被放在一个较大数字的左边时，所表示的数字等于这个大数减去这个小数，且左边最多只能放一个较小的数字。联系第一条原则，IV 表示的实际上是 V-I，所以这个数值表示 4；同理，9 为了满足第一条原则，必须被表示成 IX。 右加原则：当一个较小的数字被放在一个较大数字的右边时，所表示的数字等于这个大数加上这个小数，且右边最多只能放一个较小的数字。这一条原则和第二条原则相对应，例如 11 会被表示成 XI、21 会被表示为 XXI，以此类推。 搭配原则：I 只能被放在 V 和 X 的左边；X 只能被放在 L 和 C 的左边；C 只能被放在 D 和 M 的左边；V、L、D 不能被放在左边。这一条可以看作对是第二条的总结，所以没有什么可说的。 好了，通过这个这些规则我们就可以组合出不同的数字，我们可以注意到这些数字呈现出 1、4、5、9 的规律。什么是 1、4、5、9 的规律呢？我们可以注意到 4 和 9 是两个特殊的数字，4 必须通过 5 左减来得到，9 必须通过 10 左减来得到，这是因为罗马数字要满足最多重复三次的原则，而 4 和 9 相对 1 和 5 的偏移量恰好是 4，所以它们的表示方法和其他数字不同。因为罗马数字没有进位和权重的概念，所以除了左减和右增这两种特殊情况以外，它的基本数字应该从左至右依次递减，即使在左减的情况下，左边的数字应该和右边的数字处在同一序列。这句话怎么理解呢？例如，90 必须用 100-10 来表示；而 99 必须拆解为 90 和 9，然后分别用 100-10 和 10-1 来表示，唯独不能通过 100-1 来表示，因为 100 和 1 分属两个不同的序列。</description></item><item><title>邂逅 AOP：说说 JavaScript 中的修饰器</title><link>https://blog.yuanpei.me/posts/3668933172/</link><pubDate>Sun, 15 Apr 2018 21:20:03 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3668933172/</guid><description>Hi，各位朋友，大家好，欢迎大家关注我的博客，我是 Payne，我的博客地址是https://qinyuanpei.github.io。这个月基本上没怎么更新博客和公众号，所以今天想写一篇科普性质的文章，主题是 JavaScript 中的修饰器。 为什么使用了&amp;quot;邂逅&amp;quot;这样一个词汇呢？因为当你知道无法再邂逅爱情的时候，你只能去期待邂逅爱情以外的事物；当你意识到爱情不过是生命里的小插曲，你只能去努力弥补生命的完整性。在过往的博客中，我曾向大家介绍过譬如 Spring.NET、Unity、AspectCore 等 AOP 相关的框架，亦曾向大家介绍过譬如 Python 中的装饰器、.NET 中的 Attribute、Java 中的注解等等。再我看来，这些都是非常相近的概念，所以今天这篇文章我们又双叒叕要说 AOP 啦！什么？你说 JavaScript 里居然 AOP！这简直比任何特性都要开心好吗？而这就要从本文的主角——JavaScript 中的修饰器说起。
什么是修饰器 JavaScript 中的修饰器(Decorator)，是 ES7 的一个提案。目前的浏览器版本均不支持这一特性，所以主流的技术方案是采用 Babel 进行转译，事实上前端的工具链有相当多的工具都是这样，当然这些都是我们以后的话题啦！修饰器的出现，主要解决了下面这两个问题：
不同类间共享方法 在编译时期间对类及其方法进行修改 这里第一点看起来意义并不显著啊，因为 JavaScript 里有了模块化以后，在不同间共享方法只需要将其按模块导出即可。当然，在模块化这个问题上，JavaScript 社区发扬了一贯的混乱传统，CommonJS、AMD、CMD 等等不同的规范层出不穷，幸运的是 ES6 中使用了 import 和 export 实现了模块功能，这是目前事实上的模块化标准。这里需要关注的第二点，在编译时期间对类及其方法进行修改，这可以对类及其方法进行修改，这就非常有趣了呀！再注意到这里的修饰器即Decorator，我们立刻想 Python 中的装饰器，想到装饰器模式，想到代理模式，所以相信到这里大家不难理解我所说的，我们又双叒叕要说 AOP 啦！
那么说了这么多，JavaScript 中的修饰器到底长什么样子呢？其实，它没有什么好神秘的，我们在 Python 和 Java 中都曾见过它，前者称为装饰器，后者称为注解，即在类或者方法的上面增加一个@符号，联想一下 Spring 中的 Controller，我们大概知道它长下面这样：
/* 修饰类 */ @bar class foo {} /* 修饰方法 */ @bar foo(){} OK，现在大家一定觉得，这 TM 简直就是抄袭了 Python 好吗？为了避免大家变成一个肤浅的人，我们一起来看看下面具体的例子：</description></item><item><title>一念执着，千山无阻</title><link>https://blog.yuanpei.me/posts/2613006280/</link><pubDate>Tue, 03 Apr 2018 09:08:04 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2613006280/</guid><description> 上周看了部印度电影《小萝莉的猴神大叔》，以一言敝之，这是一部被名字耽误的好电影，就像我们所熟知的《三傻大闹宝莱坞》、《偶滴个神呐》、《外星醉汉 PK 地球神》等等电影一样。不过作为一部由“印度三汗”之一萨尔曼·汗主演的电影，可能因为其在国内的知名度不及阿米尔·汗，所以早在这部 2015 年就上映的电影，并未在国内产生太显著的影响力。相反，同档电影《环太平洋 2》票房热度居高不下，大概是因为景甜姐姐终于不负国人期望去拯救全人类啦。即使当时电影院里看这部电影的人寥寥无几，可我觉得还是有必要给大家说一说这部电影，一个即使你能猜对所有情节依然会喜欢的电影。
小萝莉与猴神大叔 影片一开始，讲述的是来自巴基斯坦穆斯林家庭的小女孩沙希达的故事。沙希达到 6 岁还不会说话，她的父母对此感到焦虑不已。祖父建议带她到神庙里去向神祈祷，可这座神庙在印度境内，并且两国间互相仇视达半个世纪之久。因为沙希达的父亲曾在巴基斯坦军方服过 5 年兵役，因此他没有办法申请前往印度的签证。无奈之下，沙希达的母亲独自带领女儿前往印度，不想在返回巴基斯坦的途中，沙希达在列车停靠期间，为下车救一只小羊羔而和母亲走散，并被一节载满粮食的火车带到印度。沙希达在那里遇到了“猴神”帕万，面对印度警方的无动于衷，大使馆因为印巴冲突而关门，黑中介拐卖幼童等等一系列的变故，帕万不得不走上亲自送沙希达回家的旅程……
沙希达与小羊羔 电影的主线是非常清晰的，不同的是，整个电影被放在一个充斥着宗教矛盾、种姓歧视、印巴冲突的大背景里，所以导演试图去表达的内涵，就不再单纯地是为了讲这样一个故事，一如“三傻”抨击印度教育制度、OMG/偶滴个神/PK 探讨宗教和神一样，这部电影里有许多值得去探讨的东西。电影两条相互交叉的线索构成，男主帕夫是一个虔诚的宗教信徒，他信奉的是印度猴神哈奴曼，并努力将宗教的教义落实到言行中去，甚至在生活中显得憨厚而木讷，导演在刻画这个人物时明显夸大了这一点，印象最深的是男主考高中就考了 10 次，最终父亲居然因为这个“惊喜”而去世。男主寄身在父亲身前的好友家中，并邂逅了一份“坎坷”的爱情。男主的确是一个普世价值中的失败者，没有稳定的工作，岳父要求他在六个月内买一套婚房，甚至从被黑中介欺骗这里可以看出，他并不是一个社会经验丰富的人。善良的男主在猴神节上给沙希达买了薄饼和饮料，沙希达就认定他是一个值得信赖的人，两个人的故事就此展开。
小孩子都懂得说谎 帕夫最初希望向印度警方寻求帮助，可警方认为警察局并不是孤儿院，拒绝为沙希达提供相关帮助。无奈之下，帕夫将小女孩带回岳父家中，起初人们见到这个可爱的小女孩，一度认为这是这个小女孩来自印度的某个高贵种姓。种姓制度是一种以血统论为基础的等级制度，广泛存在于印度社会运作与生活中，虽然印度早在 1947 年就脱离了英国殖民者的统治，可这种在殖民时期被固定和僵化的制度，在今天依旧影响深远。无独有偶，印巴冲突正是英国殖民统治者将印度划分为两个地区统治的结果，电影中印度的印地语和巴基斯坦的乌尔都语，其实是非常相似的两门语言。曾经有两个自命不凡的民族，一个是德意志雅丽安族，一个是扶桑国大和民族，一起发动第二次世界大战，为了所谓的种族优越性，大肆迫害犹太人和中国人，一个 6 岁的孩子因为可爱就被迫贴上这种种族的标签，人类繁衍至今，这种病态的虚荣心不觉得可悲吗？
有什么区别？ 如果说这种自带歧视的贴标签行为，仅仅是人类的一厢情愿的话，那么接下来沙希达所到的一切事情更像是一种本能。男主的岳父家是一个典型的印度佛教徒家庭，沙希达因为受到肉食的诱惑而到领居家吃鸡肉，当她遇到清真寺就会像母亲一样裹上头巾上前参拜……男主起初不愿意或者说不敢去清真寺内，或许是因为两种截然不同的宗教信仰，让他习惯性地去排斥一种新的文化。这何尝不是我们呢？一旦长大三观就特别难更改，面对三观不合这样的问题，大家都出奇地相信分手就能解决问题。可人类不曾见过一颗恒星的诞生和消亡，可我们就固执地相信这短短几十年里的所见所闻。其实我们大可不必去接受什么，就像这世间有千万种书千万句话，渐渐地丰富了我们原本枯竭的生命。男主纠结于该不该到清真寺里招人，他的女朋友则告诉他，沙希达只是一个 6 岁的孩子，宗教对她来说有什么意义呢？是啊，我们简简单单地来到这个世界上，等离开时突然发现平添了无数个毫无意义的身份，我承认，沙希达从背后抱住男主的时候，我一个大男人心里像是被温暖到了。
沙希达像母亲一样参拜 真正揭开小女孩身份的是一场球赛，一场特殊的球赛，一场发生在印度和巴基斯坦间的球赛。这里有一个细节，小女孩的母亲怀孕期间就是在看一场板球比赛。在这场比赛中，印度输掉了比赛，当所有人都一脸失落时，唯独沙希达高兴地手舞足蹈，甚至冲到电视机面前，亲吻巴基斯坦国旗。男主小心翼翼地问小女孩，“巴基斯坦？”。小女孩的确认让男主悲喜交加，喜的是终于知道小女孩的身世，悲的是两国积怨久矣为岳父所不容。事实上两国曾因为流民而引发流血事件，电影中男主岳父仇视巴基斯坦人的原因恰在于此。这让我想到，阿米尔·汗主演的电影 PK 中，女主嘉谷爱上了一个穆斯林男孩，父母对穆斯林的偏见以及宗教对这段感情的干涉，差点让两个真心相爱的人分开。男主再次遭遇无奈，他前往领事馆希望寻求领事馆的帮助，领事馆以沙希达没有护照为由，拒绝为男主提供帮助，这里导演让我们了解了两国间的冲突到底有多严重，一场动乱导致领事馆暂停营业一个月。男主通过岳父介绍，找到一家旅游中介公司，对方声称需要 12 万卢比(约合人民币 10000 多元)，男主的女朋友甚至拿出了准备买房子的钱，可黑心中介转手就把沙希达卖到了妓院，一个 6 岁的小女孩啊，妓院是什么地方？
安全感十足的大叔 电影开始介绍男主时说他学习不好，更不擅长体育运动(摔跤)。妓院里男主一个房间一个房间的找小女孩，当找到小女孩的时候，这个电影突然燃了起来，虽然按照角色设定，男主不应该有这样的身手，可事实上这名印度演员萨尔曼·汗，就是以健身达人的称号闻名于世的，据说他参与演出的电影都会安排裸露上身的戏份，这一次我们就当做主角光环好啦，甚至当看到黑中介在电线上荡秋千时，小女孩那纯真的笑脸，我能想到的只有一句话，“愿你被世界温柔对待”，这样一个可爱的小女孩，我愿意她一直都长不大，因为这个世界并没有那么好，幸运的是，她遇到了善良的猴神大叔。我很想知道，为什么人长大以后，反而更加不惮于勇险恶的用心去猜度别人，这到底是一种成熟，还是一种倒退。你大概不会想到，一个印度人，为了帮助一个萍水相逢的巴基斯坦人，在没有签证的情况下，冒着被当做间谍的风险，偷偷穿过两国边界的防线，只为了送这个小女孩回家，你说他不为博眼球出名，不为做好事谋利，他到底是为了什么？原谅我说句俗气的话，这就是爱啊！
公交车司机的神助攻 在穿越巴基斯坦边防线的时候，男主坚持要得到边防战士的批准后再入境，虽然这是一个讲述诚实和信念的励志故事，可我还是想说，遵从某种信念，并不是教条地照本宣科，而是在道德和法律允许的条件下适当变通。为什么要强调道德和法律呢？因为万物皆虚，万事皆座啊，《刺客信条》里刺客的信仰是每一代刺客都在追求的东西，以阿泰尔为例，他年轻时因为所罗门神殿任务的失败，而被降级为新手刺客，在完成刺杀 9 个圣殿骑士的任务中，他开始变得冷静而沉着，“万物皆虚，万事皆允”的理念开始变得越来越清晰。帕夫常常说他向哈奴曼神保证永远不偷偷摸摸，可他先后躲在清真寺里、车顶逃避警察搜捕，反而是那位伊斯兰教阿訇令人印象深刻，男主说自己不能进清真寺，阿訇说清真寺从来不锁门，所以任何人都可以进去。阿訇送男主一行三人离开后，以伊斯兰教礼仪祝福男主，并询问在印度教中如何表达祝福，随后以印度教礼仪向男主行礼，并煞有介事地问，“哈奴曼神在这里还管用？”。是啊，两个信仰不同的国家，素来相互仇视，可到了人家的地方，居然还要祈求本国的神灵护佑，这是不是有点讽刺呢？其实，宗教并不是要教会我们去排斥什么，而是要学会拥有一颗包容的心，只要一件事情的出发点是善良的，我相信这不会违背任何宗教的教义，佛家讲因果，道家讲无为，儒家讲修身，本质上都是劝人向善，大概爱是人类共同的语言，同样是神共同的语言。
阿訇以印度教礼仪祝福众人 一旦明白了这一点，你就会理解男主接下来所做的事情，比如在电话里向警察撒谎以躲避警察追捕、身为一个印度教徒参拜伊斯兰教神殿、在身上蒙上黑色衣袍伪装成伊斯兰教女子、以伊斯兰教礼仪向帮助他的人表示感谢……所谓“大道至简“，我希望我们相互去借鉴和学习不同领域里的东西，而不是相互对立和排斥彼此。《笑傲江湖》里的五岳剑派，一心想成为能和少林武当抗衡的江湖势力，其实像左冷禅这样的“宗师”级人物，如果可以以华山石壁上魔教长老破除各派剑法的图形为基础，集结各派所长，创造一门新的武功并不是不可能，奈何江湖人物一样逃不过权势的诱惑，想要扫除魔教势力，先破武当，再灭少林，再加上狭隘的门户之见，五岳剑派的辉煌不过是转眼云烟。想想《碧血剑》里大反派玉真子，一人一剑就敢上华山挑战，华山派的衰落可见一斑。如今全世界都被互联网连接在一起，文化间的相互影响越来越深，宗教如是，文化如是，不要以狭隘的民族观去审视我们不懂的文化，就如同不要觉得朋友圈里你不懂的东西 Low 一样，心态要开放，眼光要长远。
突然觉得生活美好起来 这部电影有太多的意料之中，同样有太多的意料之外。我知道小女孩会和母亲重逢，可我不知道真正打动我的是那一声叔叔；我知道男主会被警方逮捕，可我不知道巴方会想到屈打成招，理由仅仅是因为对方是一个印度人；我知道小女孩并不是哑巴，可我不知道原来说出一句话需要莫大的勇气……这里感谢那个电视台记者，这是一个媒体人的自我修养；感谢那个正直的警官，他没有给他的国家丢脸；感谢那些冲开边防线的巴基斯坦人民，有一种力量叫做民心所向。我希望两个国家可以像电影一样美好，不再有冲突，不再有流血，即使我知道现实中的困难远比电影中的多……可这的确是一群成年人的童话，就像电影最终定格在沙希达和她的猴神大叔拥抱在一起一样，有些美好值得我们去期待，有些东西值得我们去追寻，它们或许是爱，或许是正义，或许是善良，或许是信念，或许是和平，或许是勇敢……我猜中了故事的结尾，依然为这个故事而热泪盈眶，这一切并非是因为我矫情，而是因为我简单，简单到有些东西不必理解得那么深刻，就像我单纯地喜欢着你却不知道为什么一样。好了，这篇影评终于写完啦，晚安！^_^
永恒的瞬间</description></item><item><title>漫谈应用程序重试策略及其实现</title><link>https://blog.yuanpei.me/posts/115524443/</link><pubDate>Sat, 31 Mar 2018 19:20:54 +0000</pubDate><guid>https://blog.yuanpei.me/posts/115524443/</guid><description>最近随项目组对整个项目进行联调，在联调过程中暴露出各种问题，让我不得不开始反思，怎么样更好地去做好一件事情，譬如说在开发过程中如何保证 Web 服务的稳定性，在敏捷开发中如何降低文档维护的成本，以及如何提高多环境服务部署的效率等等。我为什么会考虑这些问题呢？通常我们都是在约定好接口后并行开发的，因此在全部接口完成以前，所有的服务都是以渐进的形式进行集成的，那么如何保证服务在集成过程中的稳定性呢？尤其当我们面对开发/测试/生产三套环境时，如何提高服务部署的效率呢？当接口发生变更的时候，如何让每一个人都知悉变化的细节，同时降低人员维护文档的成本呢？这些问题或许和你我无关，甚至这不是一个技术问题，可恰恰这是我们时常忽视的问题，我是我想要写这篇文章的一个重要原因。
代码越来越复杂 面对这种问题，尤其是当你发现，它并不是一个纯粹的技术问题的时候。选择一件你喜欢的事情的去做，固然可以令你开心；而选择一件你不喜欢的事情去做，则可以令你成长。我们每一个人都不是人类学家，可生命中 80%的时间都在研究人类。当你接收到一条别人的讯息时，不管这个讯息本身或对或错，在生而为人的角色预设中，你都必须去提供一个响应，甚至是比对方期望更高的一个响应。可是服务器会返回 403、404 或者 500 甚至更多的状态码，人生有时候并没有机会去选择 Plan B 或者 Plan C。所以，即使所面临境地再艰难，能不能勇敢地再去尝试一次，说服对方或者选择妥协，就像一段代码被修改得面目全非，可人类本来就是喜欢皆大欢喜的动物，总希望别人都认认真真，而自己则马马虎虎，因为“认真你就输了”，有谁喜欢输呢？
好了，现在假设我们有这样一个业务场景，我们需要调用一个 WebAPI 来获取数据，然后对这些数据做相关处理。这个 API 接口被设计为返回 JSON 数据，因此，这个“简单”的业务场景通过以下代码来实现：
def extract(url): text = requests.get(url).content.decode(&amp;#39;utf-8&amp;#39;) json_data = json.loads(text) data = json_data[&amp;#39;raw_data&amp;#39;] return data 这个代码非常简单吧！可是过了十天半个月，每次解析 JSON 数据的时候随机出现异常，经验丰富的同事建议增加 try&amp;hellip;except，并在捕获到异常以后返回 None。于是，extract()方法被修改为：
def extract(url): text = requests.get(url).content.decode(&amp;#39;utf-8&amp;#39;) try: json_data = json.loads(text) data = json_data[&amp;#39;raw_data&amp;#39;] return data except Exception: print(&amp;#34;JSON数据无效，重试！&amp;#34;) return None 修改后的代码，果然比修改前稳定啦，可是负责后续流程的同事开始抱怨，现在代码中出现大量判断返回值是否为 None 的代码片段，甚至在 Web API 返回正确结果的情况下，依然会返回 None，为此，机智的同事再次修改代码如下：
def extract(url): text = requests.</description></item><item><title>使用 Unity 框架简化应用程序异常处理及日志记录流程</title><link>https://blog.yuanpei.me/posts/3291578070/</link><pubDate>Wed, 21 Mar 2018 19:35:40 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3291578070/</guid><description>最近公司安排学习项目代码，前后花了一周左右的时间，基本熟悉了项目中的各个模块，感觉项目难度上整体偏中等。这是一个具备完整前端和后端流程的项目，在学习这个项目的过程中，我逐渐发现某些非常有趣的东西，比如在 Web API 的设计中采用严谨而完善的错误码、使用 OAuth 和 JWT 对 API 资源进行访问控制，在 JavaScript 中使用修饰器特性来实现日志记录等等，这些东西我会在后续的博客逐步去整理，今天想说的是如何通过 Unity 框架来简化应用程序异常处理和日志记录流程，而之所以关注这个问题，是因为我发现项目中接近滥用的异常处理，以及我不能忍受的大量重复代码。
背景描述 由于业务场景上的需要，我们在产品中集成了大量第三方硬件厂商的 SDK，这些 SDK 主要都是由 C/C++编写的动态链接库，因此在使用这些 SDK 的过程中，通常频繁地使用返回值来判断一个方法是否成功被调用，虽然项目上制定了严格的错误码规范，可当我看到大量的 Log()方法和业务逻辑混合在一起时，我内心依然是表示拒绝的，甚至我看到在捕获异常以后记录日志然后继续 throw 异常，这都是些什么鬼操作啊，考虑到我的语言描述得可能不太准确，大家可以从下面两段代码来感受下整体画风：
public short LoginTerminal(string uid,string pwd) { try { Log.BeginLog() return SDK.Login(uid,pwd) } catch(Exception ex) { log.LogError(ErrorCode.E2301,ex) throw new TerminalException(ex.Message); } finally { Log.EndLog() } } 这是一段相对完整的业务逻辑代码，当然这里都是伪代码实现，这里我比较反感的两个地方是：第一，从头出现到尾的 BeginLog()/EndLog()这对方法；第二，在 Catch 块中记录完日志然后将异常再次抛出。经过我对项目的一番了解，BeginLog()/EndLog()这对方法会在日志中记录某个方法开始执行和结束执行的位置。在方法执行前后插入代码片段，这不就是面向切面编程(AOP)的思想吗？这里记录完日志然后再抛出异常的做法，我个人是不大认同的，因为我觉得拦截异常应该有一个统一的入口，因为异常会继续向上传递，既然如此，为什么我们不能统一地去处理异常和记录日志呢？难道就一定要让 Log 这个静态类无处不在吗？同样地，我们注意到项目还会有下面这样的代码：
public void ProcessTerminal(object sender,ProcessEventArgs args) { try { Log.BeginLog(); var terminal = (Termainal)sender; var result = terminal.</description></item><item><title>基于新浪微博的男女性择偶观数据分析(下)</title><link>https://blog.yuanpei.me/posts/3083474169/</link><pubDate>Sat, 17 Mar 2018 15:28:40 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3083474169/</guid><description>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客。我的博客地址是：https://qinyuanpei.github.io。对于今天这篇文章的主题，相信经常关注我博客的朋友一定不会陌生。因为在 2017 年年底的时候，我曾以此为题写作了一篇文章：基于新浪微博的男女择偶观数据分析(上)。这篇文章记录了我当时脑海中闪烁着的细微想法，即当你发现一件事物背后是由哲学或者心理学这类玄奥的科学在驱动的时候，不妨考虑使用数学的思维来让一切因素数量化，我想这是最初数据分析让我感兴趣的一个原因。因为当时对文本的处理了解得非常粗浅，所以在第一次写作这篇文章的时候，实际的工作不过是在分词后绘制词云而已。等到我完成对微信好友信息的数据分析以后，我意识到微博这里其实可以继续发掘。关于微信好友信息的数据分析，可以参考这篇文章：基于 Python 实现的微信好友数据分析。在这样的想法促使下，便有了今天这篇文章，因为工作关系一直没有时间及时整理出来，希望这篇文章可以带给大家一点启示，尤其是在短文本分类方面，这样我就会非常开心啦！:slightly_smiling_face:
故事背景 关于故事背景，我在 基于新浪微博的男女择偶观数据分析(上) 这篇文章中说得非常清楚啦。起因就是我想知道，男性和女性在选择伴侣的时候，到底更为关注哪些因素？在对微信好友信息进行数据分析的时候，我们可以非常直接地确定，譬如性别、签名、头像、位置这四个不同的维度，这是因为我们处理的是结构化的数据。什么是结构化的数据呢？一个非常直观的认识是，这些数据可以按照二维表的方式组织起来。可对于微博这样一个无结构的文本数据类型，我们除了对词频、词性等因素做常规统计分析以外，好像完全找不到一个合理有效的方案，因为我们很容易就明白一件事情，即：在短短的 140 个字符中，人类语言的多样性被放大到淋漓尽致 。为了将种种离散的信息收敛在一个统一的结构里，我们必须为这些文本构建一种模型，并努力使这种模型可以量化和计算。我们通过词云对微博进行可视化分析，更多是针对词频的一种分析方法，这种方法虽然可以帮助我们找出关键字，可是因为最初写作这篇文章时，对数据分析领域相关知识知之甚少，而且在分析的过程中没有考虑停用词，所以我认为在文本分类或者是主题提取层面上，我们都需要一种更好的方法。
常见的技术方法 这篇文章涉及的领域称为文本分类或者主题提取，而针对微博、短信、评论等这类短文本的分类，则被称为短文本分类。为什么要进行文本分类呢？第一，提取出潜在主题以后可以帮助我们做进一步的分析。譬如博主这里想要从相亲类微博中分析男性和女性的择偶观，首先要解决的就是主题建模问题，因为在择偶过程中要考虑的因素会非常多，我们到底要选取哪些因素来分析呢？这些因素在特定领域中被称为特征，所以文本分类的过程伴随着特征提取。第二，**短文本数据通常只有一个主题，看起来这是在简化我们的分析过程，实则传统的基于文档的主题模型算法在这里难以适用。**因为这类主题模型算法都假定一篇文档中含有多个主题，而我们分析的是群体现象，这种个体上的差异必须设法将其统一于一体，比如美元和$属于同一个主题，我们需要一种策略来对其进行整合。
传统主题提取模型通常由文本预处理、文本向量化、主题挖掘和主题表示等多个流程组成，每个流程都会有多种处理方法，不同的组合方法会产生不同的建模结果。目前，人们在传统主题提取模型的基础上，发展起了以CNN和RNN为代表的深度学习方法，在这里我们依然关注传统主题提取模型，因为这个领域对博主而言是个陌生的领域，这里我们更多的是关注传统主题提取模型。按照传统主题提取模型，文本分类问题被拆分为特征工程和分类器两个部分，其中，特征工程的作用是将文本转化为计算机可以理解的格式，并提供强特征表达能力，即特征信息可以用以分类，而分类器基本上是统计学相关的内容，其作用是根据特征对数据进行分类。下面来简单介绍下常见的技术方法。
特征工程 特征工程覆盖了文本预处理、特征提取和文本表示三个流程。文本预处理通常指分词和去除停用词这两个过程，可以说分词是自然语言处理的基本前提。特征提取实际上囊括两个部分，即特征项的选择和特征项权重的计算。选择特征项的基本思路是：根据某个评价指标对原始数据进行排序，然后从中选择分数最高的评价指标，同时过滤掉其余的评价指标。通常可以选择的评价指标有文档频率、互信息、信息增益等，而特征权重的计算主要是经典的TF-IDF算法及其扩展算法。文本表示是指将文本预处理后转化为计算机可以理解的格式，是决定分类效果最重要的部分。传统做法是使用词袋模型(BOW)或者向量空间模型(VSM)，比如Word2Vec就是一个将词语转化为向量的相关项目。因为向量模型完全忽视文本的上下文，所以为了弥补这种技术上的不足，业界同时使用基于语义的文本表示方法，比如常见的LDA语义模型。
分类器 分类器主要是统计学里的分类方法，基本上大部分的机器学习方法都在文本分类领域有所应用，比如最常见的朴素贝叶斯算法(Naive Bayes)、KNN、支持向量机(SVM)、最大熵(MaxEnt)、决策树和神经网络等等。简单来说，假设我们所有的数据样本可以划分为训练集和测试集。首先，分类器可以在训练集上执行分类算法以生成分类模型；其次，分类器可以通过分类模型对测试集进行预测以生成预测结果；最后，分类器可以计算出相关的评价指标以评估分类的效果。这里最常用的两个评价指标是准确率和召回率，前者关注的是数据的准确性，后者关注的是数据的全面性。
TF-IDF 与朴素贝叶斯 TF-IDF(term frequency–inverse document frequency)是一种被用于信息检索与数据挖掘的统计学方法，常常被用来评估某个字词对于一个文件集或者是一个语料库中的一份文档的重要程度。在特征工程这里我们提到，特征工程中主要通过特征权重来对数据进行排序和分类，因此TF-IDF本质上是一种加权技术。TF-IDF的主要思想是：字词的重要性与它在文件中出现的次数成正比上升，与此同时与它在语料库中出现的频率成反比下降。这句话是什么意思呢？如果某个词或者短语在一篇文章中出现的频率(即TF)较高，并且在其它文章中出现的频率(即IDF)较低，那么就可以人为这个词或者短语可以作为一个特征，具备较好的类别区分能力，因此适合用来作为分类的标准。TF-IDF实际上是 TF * IDF，即 TF(term frequency，词频)与 IDF(inverse document frequency，逆文档频率)的乘积，具体我们通过下面的公式来理解： term frequency，词频 显然，这里的 TF 表示某一词条在文档中出现的频率。再看 IDF: inverse document frequency，逆文档频率 这里的 D 表示语料库中文档的数目，而分母表示的是含有指定词的文档的数目，这里两者求商后取对数即可得到 IDF。需要注意的是，当该词语不在语料库中时，理论上分母会变成 0，这将导致计算无法继续下去，因此为了修正这一错误，我们在分母上加 1，这样就可以得到 IDF 更为一般的计算公式。按照这样的思路，我们将两段文本分完词以后，分别计算每一个词的 tf-idf 并按照 tf-idf 对其进行排序，然后选取前 N 个元素作为其关键字，这样我们就获得了两个 N 维向量，按照向量理论的相关知识，两个向量间的夹角越小，其相关性越显著，这就是文本相似度判断的常规做法，在这个过程中，我们覆盖到了文本预处理、特征提取和文本表示三个过程，相信大家会对这个过程有更好的理解。
好了，那么什么是特征呢？这里计算出来的 tf-idf 实际上就是一组特征，这个特征是上下文无关、完全基于频率分析的结果，现在这些结果都是计算机可以处理的数值类型，所以特征工程要做的事情，就是从这些数值中分析出某一种规律出来。譬如，我们通过分析大量的气象资料，认为明天有 80%的概率会下雨，那么此时下雨的概率 0.8 就可以作为一个特征值，在排除干扰因素的影响以后，我们可以做一个简单的分类，如果下雨的概率超过 0.8 即认为明天会下雨，反之则不会下雨。这是一个接近理想的二值化模型，在数学中我们有一种概率分布模型称为 0-1 分布，即一件事情只有两个可能，如果该事件会发生的概率为 p，则该事件不会发生的概率为 1-p。如果所有的问题都可以简化到这种程度，我相信我们会觉得这个世界枯燥无比，因为一切非黑即白、非此即彼，这会是我们所希望的世界的样子吗？ 为什么在这里我要提到概率呢？因为这和我们下面要提到的朴素贝叶斯有关。事实上，朴素贝叶斯的理论基础，正是我们所熟悉的条件概率。根据概率的相关知识，我们有以下公式，即全概率公式：P(A|B) = P(AB)/P(B)。我们对 A 和 B 进行交换，同理可得：P(B|A) = P(A/B)/P(A)。由此我们即得到了贝叶斯公式： 贝叶斯公式 所以，朴素贝叶斯本质上是一种基于概率理论的分类算法。我们知道条件概率成立的前提是各个事件都是独立的，因此在朴素贝叶斯算法中假设所有特征间都是独立的，可当我们逐渐地了解这个世界，就会明白这个世界并不是非黑即白、非此即彼的，甚至一件事情会受到来自方方面面的因素影响，就像我们从前学习物理的时候喜欢用控制变量法一样，总有一天你会明白当时的想法太天真。朴素贝叶斯算法中的“朴素”，通常被翻译为 Naive，而这个词就是表示天真的意思，这正是朴素贝叶斯的名称由来，它简单粗暴地认为各个特征间是相互独立的，有人认为这种假设是相当不严谨的，所以相当排斥这种分类的理论，所幸朴素贝叶斯在实际应用中分类效果良好，尤其是在解决垃圾邮件过滤这类问题上，所以到今天为止，朴素贝叶斯依然是一个相当经典的分类算法，它是一个根据给定特性/属性，基于条件概率为样本赋予某个类别标签的模型。</description></item><item><title>行走在消逝中</title><link>https://blog.yuanpei.me/posts/2809571715/</link><pubDate>Thu, 15 Mar 2018 21:29:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2809571715/</guid><description>从昨天到今天，关于霍金逝世的消息，一直在朋友圈里刷屏。昨天同事告诉我这个消息的时候，我心底先是一片恍惚，而后习惯性地打开微信，发现朋友圈和公众号里都在讨论这件事情。而等到我吃饭的时候，居然听到临桌的一名男生，在向同伴讲述霍金辐射的理论，堪称我在这一天所见过的一股清流。不知道从什么时候开始，一个人物的突然离去，总是会让人们在短时间内亢奋起来，仿佛一场集体缅怀的狂欢。回顾最近这些年来已故的名人，例如杨绛先生、杨洁导演、词作家闫肃、作家黄易等等，每每提及不禁令人一阵唏嘘，正所谓“逝者已矣，生者如斯”。可我想说的是，不要总是等到离开的时候，才会想起一个人的存在。
印象中第一次有这种感觉是在 2011 年，那时我刚刚考上大学，我只记得那时站在太阳底下的我，突然大声地向周围人宣布乔布斯逝世的消息。我清晰地记着父母惊愕的表情，因为在他们的人生字典里，全然不知道乔布斯是谁，可在 19 岁的我看来，那就像是某种重大的事情发生，至少从今天的角度来看，在我出生的 1992 年里，苏联正式解体，我们的生命总是不可避免地和某种历史进程关联起来。因为从中学时候就开始住校，印象中每次回家都既陌生而熟悉，偶尔会听到妈妈讲，家族里某一位长辈突然过世。这种事情听得多了，居然不会再觉得惊讶。可是想起这些人里，有人曾经出过数学题考问过我，有人你曾经帮过他们做过什么事情……刹那间觉察到时光的残忍——所谓的物是人非，大概就是你还在此处，而别人早已暗自走远。
坦白地讲，我对霍金的认知永远都停留在《时间简史》这本书上，记得 16 年买了 Kindle 以后，的确买了这本书来读，大概读了十来页便读不下去。霍金和海伦·凯勒一样，是被我划定到身残志坚这类写作素材的范畴里。当时，语文老师让我们关注每年的感动中国人物评选，其初衷便是为了丰富我们的写作素材。回想那些年里，遭受无数次宫刑而忍辱负重的司马迁、实验了 3000 多种材料终于制造出灯泡的爱迪生、披发行吟泪洒汨罗而心系家国的屈原……这些在学生时代频频被消费的历史人物，在今天看来是否有些相似呢？据说知乎上一天内产生了 700 多个霍金相关的问题，一个曾经活在我们作文里的人物，在他离开这个世界以后，再次成为我们热议的话题，好像他从来没有在这个世界上存在过一样，这是一件可怕的事情。
我认为尊重一名物理学家的基本要求是相信科学。伴随着霍金变成人们关注的热点，网络上开始流传伽利略、爱因斯坦和霍金三个人之间巧合的时间线问题，仿佛我们面对的不是一名为宇宙物理学做出巨大贡献的科学家，而是一个被神化的“活佛”转世。有人说，人类都是一群戏精。集体缅怀一个伟人吧，立马有人跳出来说，伟人的著作都没读过一本，蹭什么热度；大家都不关注这件事情吧，立马有人跳出说，“将军坟前无人问，戏子家事天下知”，根本没有人关心科技工作者。可你看关注的时候，大家都在关注什么，譬如定要给物理学领域内的专家学者们排出个优劣来，定要将一个人的私事深挖出来品判人品。杨绛先生说，“我们曾如此渴望命运的波澜，到最后才发现：人生最曼妙的风景，竟是内心的淡定与从容。我们曾如此期盼外界的认可，到最后才知道：世界是自己的，与他人毫无关系。“，这是最浅显不过的道理，可惜想要做到实在太难。
我一直相信“人生而孤独”，除了亲情血缘以外，人与人间的联系，有时脆弱得像一只挂在风筝上的线，随时都会有断开的危险。有些人不知不觉就渐渐走远了，我们一路踟蹰而雁行，在相遇中失散，在失散中相遇，可当两个人再次相遇时，已然不是当初的彼此。我是一个不太会维护亲密关系的人，不知道是我自己走得太快，还是别人走得太快，无数的人在我这里出现然后离开，仿佛是在追逐风中的花瓣，等到风停了的时候，花瓣已不见，花香已飘远。有时候会突然问自己，想把别人留在我的生命里，是不是一件自私的事情。游戏制作人陈星汉有一款游戏叫做《风之旅人》，在广阔无垠的沙漠场景中，最多只有两个玩家出现，出现的时间和地点随机，对方可能来自任何一个国家，你对他/她的的身份信息一无所知，两个人唯一的互动方式是“共鸣”。两个靠在一起的人，可以通过“共鸣”来为对方的围巾补充能量，最重要的一点是，一旦两个人走失，就永远不会再相遇，这是这款游戏超现实意义的一个体现。
我不知道，两个人从无话不说到无话可说需要多久；我只知道，真正想要离开的人，从来都是不动声色的。自那以后，我不知道对方会在哪里，会变成什么样子，每天都会做哪些事情。我承认，别人的世界和你毫无关联，可你终究不愿意让自己成为孤岛，所以你会感到痛苦和挣扎，会想要找一个能永远陪伴你的人。可生老病死是人生里无可避免的结果，我们终其一生所寻找的灵魂伴侣，是否真的可以陪伴彼此到生命尽头。如果一切注定都要失去，我宁愿一直这样下去，我从来没有把生育看做是我生命的一种延续，因为每一个人的生命都注定独一无二，你不能想当然地认为，血缘关系会替你继承什么东西。从你死亡的那一刻起，一切都变成新的东西。
人的一生会死亡三次，第一次是医生宣布你的死亡，这是肉体上的死亡；第二次是人们来参加你的葬礼，这是社会学意义上的死亡；第三次是这个世界没有人再记得你，这是哲学意义上的死亡。或许这个世界再无霍金，可他的思想和著作一直就在那里，时间会记录着人类的过去和未来，而他是搭乘时光列车满世界旅行的自由灵魂。一个人可以不结婚，可以不生孩子，因为这是你生而为人的选择，世俗的力量是如此的强大，以至于我们都以为，人生就是一个跳一跳游戏，每一个年龄就应该跳到相应的位置。我的人生目标里没有结婚生子，如果我注定留不下任何人，如果我注定永远要被这个世界所遗忘，我宁愿在我还活着的时候，努力去写字去发出声音，即使在这空荡荡的宇宙里听不到回声，可声音不是一直都在传播着吗？
有时候，想想人生难免会觉得失望。我们明明知道世界是自己，和他人毫无关联，可我们还在努力地和这个世界发生着关联；我们明明不愿意让别人了解自己的生活，可我们对这个世界的表达欲从来没有衰减过。从镌刻在龟壳上甲骨文到以丝帛作为书写材料，再到造纸术的产生，再到今天的各种芯片，甚至内容的形式从文本演变为图片再演变为视频……可我们怎么就变成了一堆“亡灵”，从前 QQ 好友列表一片隐身，如今朋友圈剩下一条横线。如果一定要别人不再想起你，等你真正离开这个世界的时候，才会突然间被想起，我会很心疼一条鱼的记忆，因为一条鱼的记忆只有 8 秒。
如果下一刻失去记忆的，是你和我这般普通人，我们没有机会像霍金一样，被大家集体缅怀，你希望被那一个人记着，记多长时间呢？就像我总和朋友们说，回家以后找时间相聚，可在家时会觉得家人最为重要，在某一瞬间发现自己并没有那么多时间；就像我总计划着找机会去看望语文老师，可和朋友约不到一起时便无从谈起。我突然间想到，高中的第一堂语文课上，老师安排大家写一篇作文，题目好像叫做《回首向来》亦或者是《行走在消逝中》，那时我的作文没有写完，反而被老师叫起来当众朗读，我说，“回首向来萧瑟处，也无风雨也无晴”，记忆明明是有的，可我突然叫不出来有些人的名字，甚至在某一个清晨惊醒，梦到过往的某一天考试迟到，或者是快要交卷发现作文没有写……我稍稍一定神，考试那好像是很多年前的事情了吧……
我其实很想留下来陪你或者陪 Ta 呀，可时光列车从来不会留给我思考的时间，有时候我走得快，有时候你走得快，像无法逃离黑洞的光一样，拼命地往前走往前走。在广阔无垠的宇宙中，我们生活在彼此平行的世界里，有时看得见彼此，有时看不见彼此，靠着彼此间微弱的万有引力，不至于失散得太远，在成为一颗孤独的白矮星之前，请记住我。</description></item><item><title>我是猫，一只特立独行的猫</title><link>https://blog.yuanpei.me/posts/352037321/</link><pubDate>Tue, 06 Mar 2018 08:57:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/352037321/</guid><description>终于在除夕夜到来前，在 Kindle 上读完了 2017 年的最后一本书，来自夏目漱石先生的《我是猫》。起初买这本书的动机说起来非常滑稽，一来以为这会是一本诙谐幽默的书，二来对夏目这个名字莫名地充满好感。我读的是曹曼翻译的中文译本，读时觉得这位作者的文字清新素雅，即使全书行文节奏堪称缓慢到极点，想来应该是我们这个时代的人物。及至翻阅作者生平，始知这位被誉为“国民大作家”的日本作家，早在 100 年前就在日本文学史上享有盛名。这种感觉如何去形容呢？大概就是杨过从剑冢石刻的寥寥数语中，遥想独孤求败“生平求一敌手而不可得”的寂寥难堪。这位老先生的文字可以说非常”摩登“了，因为在 100 年后的今天再次读来，竟完全读不出违和感来，所谓”嬉笑怒骂皆成文章“，讽刺与幽默杂然相陈，这是我喜欢这本书的理由。
对于《我是猫》这本书，按照作者的话说，它是一部没有什么情节的小说，因为它完全是以一只猫的视角来行文，这只生活在一个教师家庭里的猫，每天都会接触到形形色色的文人，譬如：不食人间烟火，空有一番理论而不去实践的独仙；整天磨玻璃球，做事一丝不苟甚至古板木呐的寒月；表面上每天都很乐观，实则唯恐天下不乱的米亭；做事三分钟热情，自命清高的苦沙弥……等等。在猫的眼睛这里，这些人整天聚在一起讨论没有意义的事情，对现实世界心怀不满，不思进取就会怨天尤人，甚至金田及其夫人的”拜金主义“，为金钱而陷害苦沙弥的邻居，唯利是图、虚伪圆滑的铃木，这些人在猫的眼睛里都是丑陋而黑暗的。这只猫平静地叙述着它的见闻，仿佛它早已经整个人类和社会看穿看透，或许带着些嘲讽，或许带着些同情。
每年的 2 月 22 日是日本的猫节，这是我在读完这本书以后知道的。而猫在日本的文化形象中是非常神圣的，据说这是因为猫最早由遣唐使带来日本，首先作为宫廷宠物出现，直至江户时代进入”寻常百姓家“。除此之外，日本作为重度渔业国度，对稻米的珍惜使其在捕鼠护粮方面极为重视，猫作为老鼠的天敌自然而然地受到喜爱。相传招财猫起源于东京世田谷的豪德寺，因此猫在日本被人们当作神明供奉。再比如日本动漫中的机器猫、龙猫和 Hello Kitty 都是猫在日本文化中的经典形象，日本的文学作品比如《草枕子》、《源氏物语》等里面都有关于猫的故事。时至今日，依然有大量德川家族与猫的故事流传。因此，猫在日本人眼中有一种浓厚的贵族气息。陈凯歌导演的《妖猫传》，改编自日本作家梦枕貘的小说《沙门空海》，猫在其中的重要性不言自明。
这是一本“猫眼看世界”的书，这是一个怎样的世界呢？1871 年，日本历史上最为大刀阔斧的一次改革——明治维新，开始在全国范围内推行。改革带来经济飞速发展的同时，带来了各种矛盾日益突出的社会问题。36 年的 1905 年，时年 38 岁的夏目漱石，以猫的视角，如初入人类社会一般，探讨当时知识分子的心理状态和对社会变迁的感慨，并因此一举成名，获得社会广泛关注，被认为是日本批判现实主义文学的丰碑。每一个时代都有它的无奈，或许我们今天难以想象老先生当时的心境，不过从这些猫的口吻里，从这些辛辣的讽刺和戏谑中，我们总能读出作者当时内心的苦闷。猫眼里那些荒诞不经的行为，恰恰就是你我每天的生活，我们总说人类和猫是好朋友，可那仅仅是我们以为的，在猫的眼睛里，我们就像一群神经病。
猫是如何看待人类的呢？猫说：世间的奢侈往往是无能的表现。猫一年到头都穿着同一件衣服，而人类好像不把尽可能多的东西往身上照顾就难受，人类给羊添麻烦，受蚕照顾，承蒙棉花的恩泽，你看吧，我们的所作所为连只猫都看不下去。人类羡慕猫的悠闲，故而感慨道：什么时候能像猫一样轻松就好了。可明明是人类自己制造出一堆乱七八糟的事情给自己，到头来还抱怨真痛苦真痛苦，就像自己生起一堆火，到头来嚷着热死了热死了。这一切在猫看来都是庸庸碌碌的。猫甚至断言道：人类不可能永远繁荣昌盛下去。嗯，我愿静候属于猫族时代的到来。从前是“人类一思考，上帝就发笑”，而现在是“人类一思考，猫君就发笑”。猫觉得人类模仿它们的声音时是愚蠢的，尤其是在抚摸它们的时候，因为根本不存在撒娇声，只有被撒娇声，因为我们期待的是，猫向我们撒娇，可难道不是我们在向猫撒娇？
个体的荒谬，在人类的个性面前根本不值一提，就如同人类的个性得到完全解放以后，永远像一锅众口难调的羹汤。小说中苦沙弥、迷亭、寒月、东风和独仙时常在一起聊天，话题涉及哲学、艺术，爱情、生活等多个方面，这只“毒舌”的猫，就在无意识地引导和放大这些观点，“我认为这个世界上，没有比爱和美更受人尊重的了”，所以这本书里的观点，其实并不是完全的消极的，就像这只猫平静地看着这个世界，它对人类有过嘲讽，有过同情，它甚至没有自己的名字，当它失足淹死在水缸里的时候，对这个世界更多的是种悲天悯人吧！我们这个世界上有五种毒药，佛家所谓的“贪嗔痴慢疑”，作者提到“可没有任何一个人，能够全然抛开自己去研究外界，如果人类能够把自己疏离出来，那么疏离的瞬间，也就没有了自己”，人类常常不愿放过自己，更不愿放过别人，因为所有无解的问题，都可以制造一个意义出来，而我们早已习惯这一切。
曾经有朋友问我，为什么喜欢猫这种动物，我回答说，因为我就像一只猫，一只特立独行的猫，对所有人都很友善和蔼，却喜欢独来独往。因为维护这种若即若离的关系，对我来说比任何事情都要困难。人类以为猫都是傲娇的动物，其实这是人类的一厢情愿，因为从智力上来说，猫的智力是不及狗的。猫自然对人是有感情的，不过在人类驯养动物的历程中，狗更聪明、更懂得如何向人类索取，我们所认定的感情，在狗的世界里或许并不是。人类难以理解的事物，所谓阳春白雪，所谓曲高和寡，不自然地背负上高冷的名声，对一只猫而已，到底是我们不了解猫，还是不了解我们自己。人总在试图驯化猫这种动物，可猫无非是人类的一种折射而已，它就像那些独立潇洒的人一样，不藉由粘人和撒娇来获取安全感，在这个世上没有谁会离不开谁。你走，我不必送你；你来，不管多大风多大雨，我都去接你。这是我——一只猫的自白。
有时候，难免会觉得人类自作聪明，喜欢给世间的事物贴上不同的标签，譬如二哈、喵星人、汪星人、猫主子……可你知道猫如何评价人类的吗？作者说，“这些人虽然看起来快活，但是如果叩问他们的心底，却可以听见悲凉的回响”。为什么会听见悲凉的回响呢？大概是人类丰富而有趣的个性，不断地尝试挑战世俗的眼光，结果被世俗打败而变得世俗，这听起来简直就像是，英雄杀死魔王又变成魔王的故事的翻版。“每个人地位都提高，等同于每个人的地位都下降。人类不再做让自己委屈的事情，正是个人力量变强的证明；几乎不再插手别人的事情，反而是群体力量变弱的证明”。一点亏都不愿意吃，一点小便宜就要占，无一不是为了证明个人意志的强化，可人与人间的空间越来越狭窄，日益窘迫，为了扩充自己膨胀到近乎爆炸。人与人之间那点空间，是一切痛苦的根源，你说这还不算作悲凉吗？没有谁可以完全了解一个人，不完全认知是人类关系的反应剂，痛苦、误会、偏见……等等纷至沓来，你以为你模仿猫叫，猫就真的听懂了吗？
夏目先生在后记里写道，“世事变迁就像猫的眼珠一样变幻莫测，短短几个月世间，就可以去那极乐世界，或者可以把薪水花光光。年底过去了，正月过去了，花朵凋谢，新叶又生。以后世界将如何变化，我不了解，只不过水缸中猫的瞳孔，应该可以凝成永恒”。我想，世界会一如既往地这样无奈下去，时间会一如既往地这样消逝下去，而你和我会一如既往地平庸且烦恼下去。假如有这样一只猫，通过瞳孔记下了我的生平，不知道它会如何评价我呢？就像在某一个下雨天，突然想到某一个人，单单是因为怕这世界里，从此再没了对方的音讯。可单单是想到又有什么意义呢？《笑傲江湖》里令狐冲率领江湖群雄，前往少林寺解救被困的圣姑，这个将来会成为他妻子的人，而眼下更是生死未知、前途未明，可在一片寂静中听到雪花簌簌落下时，他想到的却是：小师妹不知这时候不知在干甚么。及至岳林珊为林平之所杀，临死托付令狐冲替她照顾小林子，内心却又不知做何感想了吧……</description></item><item><title>基于 Travis CI 实现 Hexo 在 Github 和 Coding 的同步部署</title><link>https://blog.yuanpei.me/posts/1113828794/</link><pubDate>Tue, 27 Feb 2018 10:45:04 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1113828794/</guid><description>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是 https://qinyuanpei.github.io .在曾经的一篇博客：《持续集成在 Hexo 自动化部署上的实践》中，我为大家分享了在线持续集成服务 Travis CI 的相关内容，在这篇文章中我们通过 Travis CI 为 Hexo 提供了自动部署的支持。其原理是 Github 为 Travis CI 分配一个 token，当我们向 Github 推送新的代码以后，Travis 就会从代码仓库中拉取代码，并通过 npm 安装依赖生成静态页面，我们将这些静态页面推送到 master 分支，即可完成对 Hexo 的部署操作。这个流程从去年 10 月份建立以来，一直运行得非常稳定，对我个人而言，随着博客里得内容越来越多，在本地生成静态页面需要 20 多秒得时间，而有了持续集成服务以后，我可以用这个时间去做更多的事情，当持续集成流程发生异常的时候，微信上会收到 Travis 发送的邮件，整个过程简直令人心情愉悦。
今天想继续写点这个话题相关的内容，即如何通过 Travis CI 实现 Hexo 在 Github 和 Coding 的同步部署。显然，部署 Hexo 到 Github Pages 我们已经实现，今天我们着重来说 Coding Pages。为什么我们需要 Coding Pages 呢？主要从两个方面考虑，首先，因为 Github Pages 屏蔽了百度的爬虫，所以我们托管在 Github 上的博客，无法被搜索引擎正常收录；其次，由于 Github Pages 的服务器在国外，所以在国内博客的速度会受到影响，而且**&amp;ldquo;防火墙&amp;rdquo;**的国情决定了 Github 是一个不稳定的网站。曾经经历过短时间内无法使用 Github 的情形，故而，为了保证博客可以更加稳定地运行，我们必须为博客提供一个备份镜像，这就是我们今天要提到的 Coding Pages 服务啦。在正式使用这个服务前，我们首先简单介绍下这个服务。</description></item><item><title>基于 Python 实现的微信好友数据分析</title><link>https://blog.yuanpei.me/posts/2805694118/</link><pubDate>Sat, 24 Feb 2018 12:50:52 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2805694118/</guid><description>最近微信迎来了一次重要的更新，允许用户对&amp;quot;发现&amp;quot;页面进行定制。不知道从什么时候开始，微信朋友圈变得越来越复杂，当越来越多的人选择&amp;quot;仅展示最近三天的朋友圈&amp;quot;，大概连微信官方都是一脸的无可奈何。逐步泛化的好友关系，让微信从熟人社交逐渐过渡到陌生人社交，而朋友圈里亦真亦幻的状态更新，仿佛在努力证明每一个个体的&amp;quot;有趣&amp;quot;。有人选择在朋友圈里记录生活的点滴，有人选择在朋友圈里展示观点的异同，可归根到底，人们无时无刻不在窥探着别人的生活，唯独怕别人过多地了解自己的生活。人性中交织着的光明与黑暗，像一只浑身长满刺的刺猬，离得太远会感觉到寒冷，而靠得太近则害怕被刺扎到。朋友圈就像过年走亲戚，即便你心中有一万个不痛快，总是不愿意撕破脸，或屏蔽对方，或不给对方看，或仅展示最后三天，于是通讯录里的联系人越来越多，朋友圈越来越大，可再不会有能真正触动你内心的&amp;quot;小红点&amp;quot;出现，人类让一个产品变得越来越复杂，然后说它无法满足人类的需求，这大概是一开始就始料不及的吧！
引言 有人说，人性远比计算机编程更复杂，因为即使是人类迄今为止最伟大的发明——计算机，在面对人类的自然语言时同样会张惶失措 。人类有多少语言存在着模棱两可的含义，我认为语言是人类最大的误解，人类时常喜欢揣测语言背后隐藏的含义，好像在沟通时表达清晰的含义会让人类没有面子，更不用说网络上流行的猜测女朋友真实意图的案例。金庸先生的武侠小说《射雕英雄传》里，在信息闭塞的南宋时期，江湖上裘千丈的一句鬼话，就搅得整个武林天翻地覆。其实，一两句话说清楚不好吗？黄药师、全真七子、江南六怪间的种种纠葛，哪一场不是误会？一众儿武功震古烁今的武林高手，怎么没有丝毫的去伪存真的能力，语言造成了多少误会。
可即便人类的语言复杂得像一本无字天书，可人类还是从这些语言中寻觅到蛛丝马迹。古人有文王&amp;quot;拘而演周易&amp;quot;、东方朔测字卜卦，这种带有&amp;quot;迷信&amp;quot;色彩的原始崇拜，就如同今天人们迷信星座运势一般，都是人类在上千年的演变中不断对经验进行总结和训练的结果。如此说起来，我们的人工智能未尝不是一种更加科学化的&amp;quot;迷信&amp;quot;，因为数据和算法让我们在不断地相信，这一切都是真实地。生活在数字时代的我们，无疑是悲哀的，一面努力地在别人面前隐藏真实地自己，一面不无遗憾地感慨自己无处遁逃，每一根数字神经都紧紧地联系着你和我，你不能渴望任何一部数字设备具备真正的智能，可你生命里的每个瞬间，都在悄然间被数据地折射出来。
今天这篇文章会基于 Python 对微信好友进行数据分析，这里选择的维度主要有：性别、头像、签名、位置，主要采用图表和词云两种形式来呈现结果，其中，对文本类信息会采用词频分析和情感分析两种方法。常言道：工欲善其事，必先利其器也。在正式开始这篇文章前，简单介绍下本文中使用到的第三方模块：
itchat：微信网页版接口封装 Python 版本，在本文中用以获取微信好友信息。 jieba：结巴分词的 Python 版本，在本文中用以对文本信息进行分词处理。 matplotlib： Python 中图表绘制模块，在本文中用以绘制柱形图和饼图 snownlp：一个 Python 中的中文分词模块，在本文中用以对文本信息进行情感判断。 PIL： Python 中的图像处理模块，在本文中用以对图片进行处理。 numpy： Python 中 的数值计算模块，在本文中配合 wordcloud 模块使用。 wordcloud： Python 中的词云模块，在本文中用以绘制词云图片。 TencentYoutuyun：腾讯优图提供的 Python 版本 SDK ，在本文中用以识别人脸及提取图片标签信息。 以上模块均可通过 pip 安装，关于各个模块使用的详细说明，请自行查阅各自文档。 数据分析 分析微信好友数据的前提是获得好友信息，通过使用 itchat 这个模块，这一切会变得非常简单，我们通过下面两行代码就可以实现：
itchat.auto_login(hotReload = True) friends = itchat.get_friends(update = True) 同平时登录网页版微信一样，我们使用手机扫描二维码就可以登录，这里返回的 friends 对象是一个集合，第一个元素是当前用户。所以，在下面的数据分析流程中，我们始终取 friends[1:]作为原始输入数据，集合中的每一个元素都是一个字典结构，以我本人为例，可以注意到这里有 Sex、City、Province、HeadImgUrl、Signature 这四个字段，我们下面的分析就从这四个字段入手：
好友信息结构展示 好友性别 分析好友性别，我们首先要获得所有好友的性别信息，这里我们将每一个好友信息的 Sex 字段提取出来，然后分别统计出 Male、Female 和 Unkonw 的数目，我们将这三个数值组装到一个列表中，即可使用 matplotlib 模块绘制出饼图来，其代码实现如下：
def analyseSex(firends): sexs = list(map(lambda x:x[&amp;#39;Sex&amp;#39;],friends[1:])) counts = list(map(lambda x:x[1],Counter(sexs).</description></item><item><title>使用 Python 生成博客目录并自动更新 README</title><link>https://blog.yuanpei.me/posts/1329254441/</link><pubDate>Fri, 23 Feb 2018 09:32:45 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1329254441/</guid><description>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是：https://qinyuanpei.github.io。首先在这里祝大家春节快乐，作为过完年以后的第一篇文章，博主想写点内容风格相对轻松的内容。自从博主的博客采用 TravisCI 提供的持续集成(CI)服务以以来，博客的更新部署变得越来越简单，所有的流程都被简化为 Git 工作流下的 提交(commit) 和 推送(push) 操作。考虑到博客是托管在 Github 上的，一直希望可以自动更新仓库主页的 README 文件，这样可以显示每次提交代码后的变更历史。基于这样一个构想，我想到了为博客生成目录并自动更新 README，其好处是可以为读者建立良好的文档导航，而且 Markdown 是一种简单友好的文档格式，Github 等代码托管平台天生就支持 Markdown 文档的渲染。关于博客采用 TravisCI 提供持续集成(CI)服务相关内容，可以参考 持续集成在 Hexo 自动化部署上的实践 这篇文章。
好了，现在考虑如何为博客生成目录，我们这里需要三个要素，即标题、链接和时间。标题和时间可以直接从 _posts 目录下的 Markdown 文档中读取出来，链接从何而来呢？我最初想到的办法是读取每个 Markdown 文档的文件名，因为我的使用习惯是采用英文命名，这样当博客的永久链接(permalink)采用默认的 :year/:month/:day/:title/ 形式时，每个 Markdown 文档的文件名等价于文章链接。事实证明这是一个愚蠢的想法，因为当你改变永久链接(permalink)的形式时，这种明显投机的策略就会彻底的失败。相信你在浏览器种打开这篇文章时，已然注意到链接形式发生了变化，当然这是我们在稍后的文章中讨论的话题啦。至此，我们不得不寻找新的思路，那么这个问题该如何解决呢？
我意识到我的博客配置了 hexo-generator-json-content 插件，这个插件最初的目的是为博客提供离线的搜索能力，该插件会在博客的根目录里生成一个 content.json 文件，而这个文件中含有我们想要的一切信息，因此我们的思路转变为解析这个文件，人生苦短啊，我果断选择了我最喜欢的 Python，这里我们会提取出所有的文章信息，按照日期由近到远排序后生成列表。Python 强大到让我觉得这篇文章无法下笔，所以这里直接给出代码啦：
# -*- coding: utf-8 -*- import os import re import sys import json import datetime # 文档实体结构定义 class Post: def __init__(self,date,link,title): self.date = date self.link = link self.</description></item><item><title>愿你和我一样喜欢蛋炒饭</title><link>https://blog.yuanpei.me/posts/1933583281/</link><pubDate>Sat, 10 Feb 2018 16:12:25 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1933583281/</guid><description>周末花了一个晚上的时间看了部电影，由黄渤主演的《蛋炒饭》。有人说，这是一部刻意模仿《阿甘正传》的电影，充满胶片质感的纪录片风格，相似的镜头语言和表现手法，无一不在努力告诉你，这是一部本土化的《阿甘正传》。可是如同巧克力之于蛋炒饭，两种截然不同的食物会有不同的味道，电影所反映的实则是两种不同的内涵。如果说《阿甘正传》代表的是极具美国精神的励志故事，那么《蛋炒饭》代表的则是小人物为理想打拼的乌托邦。说《蛋炒饭》是本土化的《阿甘正传》其实不无道理，因为电影里充满了太多相似，这种在由时代感打磨出的细腻的情感，让我觉得这是一部值得一看的电影。
电影《阿甘正传》经典开头 电影开头苏茉莉坐在游乐园长凳上自下而上的长镜头，不禁让人联想到《阿甘正传》的开场，联想到那片空中摇曳着的羽毛；电影中反复出现的台词，来自大卫父亲的那句：做蛋炒饭要慢要慢，一如《阿甘正传》里的经典台词：人生就像一盒巧克力，你永远不知道下一块会是哪种？电影令我印象深刻的地方，在于它具厚重的时代感，中美建交、改革开放、金融危机、邓丽君、摇滚音乐、周杰伦、周笔畅……看起来王大卫好像和阿甘一样，参与并影响了众多历史事件。可我的理解是，阿甘最终成为了流浪街头的大富翁，而王大卫一直一无所有，因为他一辈子都在做一件事情，那就是做蛋炒饭。
最美好的年纪 整个电影采用的是倒叙的表现手法。主人公王大卫是一个智商不太高，而且有语言表达障碍的“傻子”，他第一次出场是替好兄弟打抱不平，结果他就像星爷《功夫》里的少年一样，以为自己的武功盖世无双，结果自然是被人打得头破血流；他书包里藏着的“复仇”的板砖，在外国友人访华的时候，一板砖破坏了中美友谊，结果自然是被校长通报批评；他家祖上是宫廷御厨，父亲即将退休食堂安排他去顶岗，一心想做厨师的他差点烧了厨房，结果自然是迫使父亲替他求情以保住工作；他从小喜欢的女孩苏茉莉要和好兄弟发生关系，她吻了他一下叫他在门口把风，结果自然是茉莉被渣男欺骗然后甩了他；他用母亲变卖古董的钱来了饭店，好兄弟嫉妒他做了老板，骗走他的产业后远走高飞，结果自然是他从老板变成杂工。
有些爱就像一阵风 看这个电影的时候，我一直觉得人生是绝望的，父母先后离开人世，哥哥在面前自杀而死，喜欢的女生抛弃自己，好兄弟发小欺骗自己……周围人对他全部是冷嘲热讽，每次发工资的时候，嘲弄他什么时候攒够钱赎回饭店；每次娱乐圈有绯闻传来的时候，说他“媳妇”苏茉莉又登上杂志封面；“好兄弟“因为金融危机背负债务，他就拎着辛苦攒下的一袋子零钱去帮他还债，结果半夜“好兄弟”开着车跑了……我不知道，我们该不该认同这种无条件“傻”的行为，电影想告诉我们的或许是“以德报怨，以德服人”这种儒家的传统思想，即在复杂的社会中，如何以一种淳朴单纯的心态，追寻本心，不畏曲折。所谓“众生虽苦，诸恶莫作”，这个社会复杂险恶是现实，可温润善良则是一种选择。我们抱怨时代给我们选择狭窄，因为我们放弃了那些艰难的选择，最终选择大多数人选择的那条路。
李红兵：人都是会变的 可我还是想说，无条件的善良是懦弱，我们选择善良，是因为我们不想伤害别人，可在今天这样一个时代，善良常常被当做是懦弱的表现，一个人努力让心变狠变硬，或许会达到通常意义上的成功，可成功的定义从来都是被人绑架着的，电影中“好兄弟”登上了“胡弄排行榜”，我不知道这是不是导演的一种讽刺，一个人靠着骗取好兄弟的产业而发家，即使收获了声名和利益，当他一个人在高速公路上疾驶，打算远走他乡的时候，内心是不是会有一点羞愧和遗憾呢？我们说，这是一部乌托邦式的电影，因为现实中像李红兵这样的人，绝对不会陡然间良心发现，把饭店归还王大卫，甚至王大卫拉开窗帘阳光照射进来，阳光下苏茉莉牵着女儿的手对他微笑，大卫的蛋炒饭得到溥仪认可，入选国际非遗名录……我宁愿相信，这是一种美好的理想。
大卫做蛋炒饭 大卫在电影中做过三次蛋炒饭，第一次是他顶替父亲的岗位到食堂工作，结果想做厨师的他差点烧了厨房，付出的代价是，因为御厨身份而自豪的父亲，向一辈子没低过头的食堂经理低头；第二次是他陪苏茉莉到医院流产，打完胎的苏茉莉说自己饿，他冒着被饭店人追打的危险，给苏茉莉做了一份蛋炒饭，付出的代价是，苏茉莉同他告别，独自到南方发展演艺事业；第三次是李红兵归还了饭店，成名后的王大卫，当着记者的面做了一次蛋炒饭，溥仪评价大卫的蛋炒饭，比他爷爷做得还要好，蛋炒饭入选国际非遗名录。大卫的一生都在做一件事情，那就是做蛋炒饭，这种专注是他和阿甘不一样的地方，在一个浮躁的时代，我们每天都在追逐都在忙碌，可我们追逐的是什么呢？或许是一盘蛋炒饭的安宁，从这个角度来说，蛋炒饭这种食物，真的是质朴而简单的存在啦。
有些人像你生命里的天使 如果你看过《阿甘正传》这部电影，会觉得苏茉莉和珍妮这两个角色是相似的。她们看起来都知道自己想要些什么，苏茉莉选择发展演艺事业进入娱乐圈，珍妮一直知道自己想要什么，她想做一名歌手，为了唱歌吃尽苦头，在酒吧、街头甚至任何可以唱歌的地方，只有有场合给她吉他，她就可以唱，甚至为了唱歌而穷困潦倒到卖身吸毒……她可以真挚地给成名的阿甘一个拥抱，而不愿依附他达到自己成名的目的，她深爱阿甘的同时，深知阿甘不会同自己结婚，因为童年的经历让她内心无比自卑，她始终打不开心里的结，她选择为阿甘生下孩子，这是一种自我成全。
愿茉莉一直这样美好 苏茉莉成名后取艺名苏菲，据说这个人物原型来自王菲，苏茉莉在成名过程中不断豪门势力，从最初回北京办演唱会拉赞助，到嫁入豪门以后受不了娱乐圈绯闻骚扰，进而宣布退出娱乐圈回到北京，她是一个不知道自己想要什么的人，直到故事最后她终于发现，原来那个深爱着自己的人，一直就在默默地等自己回来。所以对比两部电影中的女主，就可以发现，两部电影阐述的观点是截然相反的，即阿甘是迷茫的，而珍妮是坚定的；王大卫是坚定的，而苏茉莉是迷茫的。当你不知道想要什么的时候，不妨慢下来做份蛋炒饭，或许你就会找到答案。
你饿不饿，我做蛋炒饭给你吃呀 故事接近尾声的时候，王大卫再次登上三个人从小便常去的城楼，猛然间看到三个孩子正在那里玩耍，孩子们说这是他们的地盘，王大卫还是像从前一样，知趣地准备转身离开，阳光照耀城楼的刹那，我分明看见他脸上满意的笑容，那种历经沧桑后初心不改的从容。每个人，都既注定的命运，同时有偶然，两者都在同时发生，就像那片羽毛，努力飘啊飘啊，终于飘到阿甘脚下，然后又无可奈何的飘离阿甘。羽毛不是飞鸟，无法掌控飞行的方向，其实很想和你在一起，但偏偏风把距离吹得好远。如果可以的话，我想你和我一样，都喜欢蛋炒饭。你饿不饿，我做蛋炒饭给你吃呀？</description></item><item><title>基于 Python 实现 Windows 下壁纸切换功能</title><link>https://blog.yuanpei.me/posts/2822230423/</link><pubDate>Mon, 05 Feb 2018 16:48:39 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2822230423/</guid><description>在过去一年多的时间里，我尝试改变博客的写作风格，努力让自己不再写教程类文章，即使在这个过程中，不断地面临着写作内容枯竭的痛苦。因为我渐渐地意识到，告诉别人如何去做一件事情，始终停留在&amp;quot;术&amp;quot;的层面，而比这个更为重要的是，告诉别人为什么要这样做，这样就可以过渡到&amp;quot;道&amp;quot;的层面。古人云：形而上者谓之道，形而下者谓之器。我们常常希望通过量变来产生质变，可是如果在这个过程中不能及时反思和总结，我们认为的努力或许仅仅是重复的劳作而已。如你所见，在这篇文章里，我们将通过 Python 和 Windows 注册表实现壁纸切换功能，主要涉及到的 Python 中的 requests、pyinstaller 这两个模块的使用，希望大家喜欢。
故事缘由 人们常常相信事出有因，可这世界上有些事情，哪里会有什么原因啊，比如喜欢与不喜欢。做这样一个小功能的初衷，起源于我对桌面壁纸的挑剔。作为一个不完全的强迫症患者，我需要花费大量时间去挑选一张壁纸，丝毫不亚于在网上挑选一件喜欢的商品。我注意到知乎上有这样的话题：有哪些无版权图片网站值得推荐？，因此对于桌面壁纸的筛选，我渐渐地开始摆脱对搜索引擎的依赖，我个人比较喜欢Pexels和Unsplash这两个网站，所以我想到了从这两个网站抓取图片来设置 Windows 壁纸的方案。市面上类似的商业软件有百度壁纸、搜狗壁纸等，可这些软件都不纯粹，或多或少地掺杂了额外功能，个中缘由想来大家都是知道的。联想到微信最新版本的更新，&amp;ldquo;发现&amp;quot;页面支持所有项目的隐藏，甚至是盟友京东的电商入口和腾讯最赚钱的游戏入口，这让我开始正视腾讯这家公司，我收回曾经因为抄袭对腾讯产生的不满，腾讯是一家值得尊重的互联网公司。做一个纯粹的应用程序，这就是我的初心。
设计实现 好了，现在我们考虑如何来实现这个功能，我们的思路是从Unsplash这个网站抓取图片，并将其存储在指定路径，然后通过 Windows API 完成壁纸的设置。Python 脚本会通过 pyinstaller 模块打包成可执行文件，我们通过修改注册表的方式，在右键菜单内加入切换壁纸的选项，这样我们可以直接通过右键菜单实现壁纸切换功能。在编写脚本的时候，起初想到的是抓包这样的常规思路，因为请求过程相对复杂而失败，后来意外地发现官方提供了 API 接口。事实上Pexels和Unsplash都提供了 API 接口，通过调用这些 API 接口，我们的探索进行得非常顺利，下面是具体脚本实现：
# Query Images searchURL = &amp;#39;https://unsplash.com/napi/search?client_id=%s&amp;amp;query=%s&amp;amp;page=1&amp;#39; client_id = &amp;#39;fa60305aa82e74134cabc7093ef54c8e2c370c47e73152f72371c828daedfcd7&amp;#39; categories = [&amp;#39;nature&amp;#39;,&amp;#39;flowers&amp;#39;,&amp;#39;wallpaper&amp;#39;,&amp;#39;landscape&amp;#39;,&amp;#39;sky&amp;#39;] searchURL = searchURL % (client_id,random.choice(categories)) response = requests.get(searchURL) print(u&amp;#39;正在从Unsplash上搜索图片...&amp;#39;) # Parse Images data = json.loads(response.text) results = data[&amp;#39;photos&amp;#39;][&amp;#39;results&amp;#39;] print(u&amp;#39;已为您检索到图片共%s张&amp;#39; % str(len(results))) results = list(filter(lambda x:float(x[&amp;#39;width&amp;#39;])/x[&amp;#39;height&amp;#39;] &amp;gt;=1.33,results)) result = random.choice(results) resultId = str(result[&amp;#39;id&amp;#39;]) resultURL = result[&amp;#39;urls&amp;#39;][&amp;#39;regular&amp;#39;] # Download Images print(u&amp;#39;正在为您下载图片:%s.</description></item><item><title>深入浅出理解 Python 装饰器</title><link>https://blog.yuanpei.me/posts/2829333122/</link><pubDate>Tue, 23 Jan 2018 15:55:13 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2829333122/</guid><description>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是https://qinyuanpei.github.io。今天我想和大家一起探讨的话题是 Python 中的装饰器。因为工作关系最近这段时间在频繁地使用 Python，而我渐渐意识到这是一个非常有趣的话题。无论是在 Python 标准库还是第三方库中，我们越来越频繁地看到装饰器的身影，从某种程度上而言，Python 中的装饰器是 Python 进阶者的一条必由之路，正确合理地使用装饰器可以让我们的开发如虎添翼。装饰器天然地和函数式编程、设计模式、AOP 等概念产生联系，这更加让我对 Python 中的这个特性产生兴趣。所以，在这篇文章中我将带领大家一起来剖析 Python 中的装饰器，希望对大家学习 Python 有所帮助。
什么是装饰器 什么是装饰器？这是一个问题。在我的认知中，装饰器是一种语法糖，其本质就是函数。我们注意到 Python 具备函数式编程的特征，譬如 lambda 演算，map、filter 和 reduce 等高阶函数。在函数式编程中，函数是一等公民，即“一切皆函数”。Python 的函数式编程特性由早期版本通过渐进式开发而来，所以对“一切皆对象”的 Python 来说，函数像普通对象一样使用，这是自然而然的结果。为了验证这个想法，我们一起来看下面的示例。
函数对象 def square(n): return n * n func = square print func #&amp;lt;function square at 0x01FF9FB0&amp;gt; print func(5) #25 可以注意到，我们将一个函数直接赋值给一个变量，此时该变量表示的是一个函数对象的实例，什么叫做函数对象呢？就是说你可以将这个对象像函数一样使用，所以当它带括号和参数时，表示立即调用一个函数；当它不带括号和参数时，表示一个函数。在 C#中我们有一个相近的概念被称为委托，而委托本质上是一个函数指针，即表示指向一个方法的引用，从这个角度来看，C#中的委托类似于这里的函数对象，因为 Python 是一个动态语言，所以我们可以直接将一个函数赋值给一个对象，而无需借助 Delegate 这样的特殊类型。
使用函数作为参数 def sum_square(f,m,n): return f(m) + f(n) print sum_square(square,3,4) #25 使用函数作为返回值 def square_wrapper(): def square(n): return n * n return square wrapper = square_wrapper() print wrapper(5) #25 既然在 Python 中存在函数对象这样的类型，可以让我们像使用普通对象一样使用函数。那么，我们自然可以将函数推广到普通对象适用的所有场合，即考虑让函数作为参数和返回值，因为普通对象都都具备这样的能力。为什么要提到这两点呢？因为让函数作为参数和返回值，这不仅是函数式编程中高阶函数的基本概念，而且是闭包、匿名方法和 lambda 等特性的理论基础。从 ES6 中的箭头函数、Promise、React 等可以看出，函数式编程在前端开发中越来越流行，而这些概念在原理上是相通的，这或许为我们学习函数式编程提供了一种新的思路。在这个示例中，**sum_square()和square_wrapper()**两个函数，分别为我们展示了使用函数作为参数和返回值的可行性。</description></item><item><title>AI 时代：聊聊大数据中的 MapReduce</title><link>https://blog.yuanpei.me/posts/2911923212/</link><pubDate>Fri, 19 Jan 2018 00:45:08 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2911923212/</guid><description>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客。最近读一本并行计算相关的书籍，在这本书中作者提到了 MapReduce。相信熟悉大数据领域的朋友，一定都知道 MapReduce 是 Hadoop 框架中重要的组成部分。在这篇文章中，博主将以函数式编程作为切入点，来和大家聊一聊大数据中的 MapReduce。如今人工智能正成为行业内竞相追逐的热点，选择 MapReduce 这个主题，更多的是希望带领大家一窥人工智能的门庭，更多深入的话题需要大家来探索和挖掘。
MapReduce 的前世今生 MapReduce 最早是由 Google 公司研究并提出的一种面向大规模数据处理的并行计算模型和方法。2003 年和 2004 年，Google 公司先后在国际会议上发表了关于 Google 分布式文件系统(GFS)和 MapReduce 的论文。这两篇论文公布了 Google 的 GFS 和 MapReduce 的基本原理和主要设计思想，我们通常所说的 Google 的三驾马车，实际上就是在说 GFS、BigTable 和 MapReduce。因此，这些论文的问世直接催生了 Hadoop 的诞生，可以说今天主流的大数据框架如 Hadoop、Spark 等，无一不是受到 Google 这些论文的影响，而这正是 MapReduce 由来，其得名则是因为函数式编程中的两个内置函数: map()和 reduce()。
我们常常说，脱离了业务场景去讨论一项技术是无意义的，这个原则在 MapReduce 上同样适用。众所周知，Google 是一家搜索引擎公司，其设计 MapReduce 的初衷，主要是为了解决搜索引擎中大规模网页数据的并行化处理。所以，我们可以说，MapReduce 其实是起源自 Web 检索的。而我们知道，Web 检索可以分为两部分，即获取网页内容并建立索引、根据网页索引来处理查询关键字。我们可以认为互联网上的每个网页都是一个文档，而每个文档中都会有不同的关键字，Google 会针对每一个关键字建立映射关系，即哪些文档中含有当前关键字，这是建立索引的过程。在建立索引以后，查询就会变得简单，因为现在我们可以按图索骥。
互联网诞生至今，网站及网页的数量越来越庞大，像 Google 这样的搜索引擎巨头是如何保证能够对 Web 上的内容进行检索的呢？答案是采用并行计算(Parallel)。硬件技术的不断革新，让计算机可以发挥多核的优势来处理数据，可当数据量庞大到单机无法处理的程度，就迫使我们不得不采用多台计算机进行并行计算。我们知道并行计算的思想是，将大数据分割成较小的数据块，交由不同的任务单元来处理，然后再将这些结果聚合起来。因此，可以将 MapReduce 理解为一种可以处理海量数据、运行在大规模集群上、具备高度容错能力、以并行处理方式执行的软件框架。MapReduce 是分治思想在大规模机器集群时代的集中体现(如图所示)，其中，Mapper 负责任务的划分，Reducer 负责结果的汇总。
MapReduce原理图 MapReduce 的推出给大数据并行处理带来了巨大的革命性影响，使其成为事实上的大数据处理的工业标准，是目前为止最为成功、最广为接受和最易于使用的大数据并行处理技术。广为人知的大数据框架 Hadoop，正是受到 MapReduce 的启发。自问世来，成为 Apache 基金会下最重要的项目，受到全球学术界和工业界的普遍关注，并得到推广和普及应用。MapReduce 的非凡意义在于，它提出了一个基于集群的高性能并行计算模型，允许使用市场上普通的商用服务器构成一个含有数十、数百甚至数千个节点的分布式并行计算集群，可以在集群节点上自动分配和执行任务以及收集计算结果，通过 Mapper 和 Reducer 提供了抽象的大数据处理并行编程接口，可以帮助开发人员更便捷地完成大规模数据处理的编程和计算工作。今天，Google 有超过 10000 个不同的项目已采用 MapReduce 来实现。</description></item><item><title>无问东西：你曾是少年</title><link>https://blog.yuanpei.me/posts/1983298072/</link><pubDate>Fri, 19 Jan 2018 00:42:06 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1983298072/</guid><description>如果提前了解了你所要面对的人生，你是否还有勇气前来？
这是电影《无问西东》里提出的一个问题。作为一部清华大学建校 100 周年的献礼片，在因为种种原因而被雪藏 6 年以后，终于以这样一种倔强而孤傲的姿态，进入到人们的视野。
绘制彩红的张果果 电影一开始，透过玻璃注视着四胞胎的张果果(张震饰)，第一次向我们发问：如果提前了解了你所要面对的人生，你是否还有勇气前来。而在电影接近尾声的时候，张果果再一次注视着四胞胎，在窗户上为它们勾勒彩虹，此时旁白再次想起，一问一答间，张果果心中已然找到答案。我想知道的是，屏幕前的你，是不是同样找到了答案？
这是一部讲述传承的电影，它不同于目前电影市场上任何一种主流的类型，它从一开始就注定是一部不讨巧、不友善、不商业的电影。其故事跨度将近一个世纪，或许我们所看到的，不过是历史长河里的星星点点，可依然难掩背后熠熠生辉的文人风骨，这种由时代所赋予的文化气息，让它成为一部瑕不掩瑜的电影，所以即使这部电影在叙事、配乐和剪辑上存在缺陷，这依然是一部值得关注的电影。
电影尝试用四个平行的板块，讲述四个不同历史阶段的故事，这些故事中的人物彼此关联，组成了一幅波澜壮阔的历史画面，而影片中想要去表达的精神内核，恰好是关联起这些人物的一种特质，我们称之为传承的东西。按照影片线性叙事的结构，我们可以梳理出这样四条线索。
上进求学的吴岭澜 1923 年，北平，清华学堂的高材生吴岭澜(陈楚生饰)，面对选择文理科时的迷茫与彷徨，校长梅贻绮告诉他什么是真实，适逢泰戈尔访华到清华园演讲，在听到“不要忘记你们的真心和真性”后，开始思考人生的意义。
捐躯赴国的沈光耀 1938 年，昆明，世家子弟沈光耀面对山河破碎的现实，在忠与孝的两难抉择中深感困惑，直到听到吴岭澜说“不要放弃对生活的思索，对自己的真实”，毅然选择投笔从戎，成为飞虎队队员，在国家危亡之际战死沙场。
青春阳光的陈鹏 1962 年，北京，清华毕业生陈鹏，在面对国家的大爱与恋人的小爱的选择时，饱含着对恋人王敏佳的爱远赴沙漠投身科研，他质问为支边而蒙蔽内心的李想什么是真实，他用爱托着恋人鼓励她努力活下去，为爱而奉献一生。
寻找本心的张果果 2010 年，北京，广告总监张果果，在明争暗斗的职场上遭人排挤，在面对职场中的名利诱惑时，在面对四胞胎永无止境的救助要求时，李想用生命拯救了张果果的父母，完成自我救赎，而张果果则在迷茫中找会初心，让爱传承下去。
看这样一部电影的时候，像是在经历一件久远的事情。“这个时代缺少的不是完美的人，缺少的是从自己心底里给出的，真心、正义、无畏和同情”，这是电影中飞虎队教练在招收飞行员时所说的话。我想我们喜欢这部电影，恰恰是因为我们缺少这些东西，这些让我们感到温暖而纯粹的东西。借用吴岭澜在影片中的一句话，“此刻我终于不觉得这样的思考是羞耻的，甚而是你们人生必须的”，面对一个我们无力反驳的现实，或许思考有时候会显得更为苍白。彼时，清华大学校长梅贻绮对迷茫中的吴岭澜这样说道，“人把自己置身于忙碌之中，有一种麻木的踏实，但丧失了真实”，那么你知道什么是真实吗？
循循善诱的清华校长梅贻琦 聆听泰戈尔演讲的吴岭澜 电影中第一个人物，是 1924 年就读于清华大学的吴岭澜，他见证过泰戈尔访问清华，最终从摇摆不定到听从内心声音，找寻人生的意义。吴岭澜当时是清华大学的一名学生，他的理科成绩属于不列，而当时最好的学生都会去读理科，所以他开始面临学业选择上的困惑。校长梅贻绮给他的答案是：“真实是你看到什么，听到什么，做什么和谁在一起，有一种从心灵深处满溢出来的不懊悔也不羞耻的平和与喜悦”。当时诗人泰戈尔访问清华，演讲的主题是&amp;quot;对自己的真诚&amp;quot;，这次演讲促使吴岭澜心态发生最终变化，转学文科，并最终成为西南联大的一名文科教授。电影对吴岭澜的描绘着重放在他和校长梅贻绮的谈话，以及他出现在泰戈尔演讲现场时那双坚定而执著的眼睛。影片中泰戈尔并没有给出完整的正脸，我们可以看到的只有他标志性的长头发和白头发，可见当时泰戈尔的演讲激起了这代青年人内心的热潮。
历史上的西南联大 西南联大师生合影 历史上的西南联合大学，始于 1938 年 4 月。“七七事变”后，京津地区陷入日军炮火，彼时的清华、北大和南开三校一致决定南迁，遂组成西南联合大学。电影中投笔从戎的沈光耀(王力宏饰)就是这段历史时期的一个缩影，西南联大是中国高等教育史上最伟大的传奇，在短短 8 年间，汇聚并培养了一大批精英，譬如陈寅恪、钱穆、梁思成、朱自清、闻一多、冯友兰……可以说这是近代最为光华璀璨的时期，可谓群贤毕至、风华绝代。短短 8 年间，西南联大共毕业学生 3882 名，其中诺贝尔奖获得者 2 位、国家最高科学技术奖获得者 4 位、两弹一星功勋奖章获得者 8 位、两院院士 171 位及人文大师 100 多位。
静坐停雨 电影中这段我最喜欢的场景是“静坐听雨”。历史上西南联大的校舍，是由梁思成夫妇参与设计的。因为战争时期物资非常紧缺，校方无法提供充足的建筑材料，所以梁思成夫妇的设计方案一改再改，从楼房变成了平房，从砖墙变成了泥墙，最终 124 亩的校园里，只有图书馆和实验室以青瓦覆顶，教室用铁皮，宿舍则用茅草。铁皮屋顶最害怕下雨，而昆明偏偏是多雨气候，因此就有了雨声太大导致学生无法听清老师讲课，老师无奈地在黑板上写下“静坐听雨”四个字，这样苦涩中带着温情的画面。电影中沈光耀推开教室窗户，听到的是雨中体育系学生坚持出操的呐喊，看到的是雨中垂钓者的“一蓑烟雨任平生”的从容。我们今天的大学占地越来越大，楼越盖越高，可再找不回这种令人动容的场景。我本科有位老师喜欢讲《大学》，他说这大学不是“高楼大厦之谓”，而是“大师之谓”，这一刻我很想再听老师讲一次大学之道。
汪曾琪有篇散文《跑警报》，讲述的正是当时的师生躲避日军飞机轰炸的故事，电影中沈光耀(王力宏饰)在锅炉房相遇煮冰糖银耳的桥段，正是取材于这篇散文。在影片中，吴岭澜(陈楚生饰)是一个对沈光耀产生深远影响的人，吴岭澜此时已成为西南联大文学系教授，他冒着被轰炸的危险把煮冰糖银耳的沈光耀从校舍拉了出来，一句“学生不走老师怎么能走”，足以让其形象瞬间高大起来。电影中吴岭澜躲避轰炸的时候带了只鸽子，而历史真实的故事则是金岳霖抱着一只鸡。当时流传着这样一个段子，“陈寅恪跑警报是为了保护国粹，刘文典跑警报是为了庄子，沈从文跑警报到底是为了什么“。在防空洞里吴岭澜讲述泰戈尔的“真实就是你所见所闻”，将真实传承到沈光耀心里，所以他会冒着受罚的风险向饥民空投物资，所以他会不顾家人反对毅然选择投笔从戎、加入飞虎队，直至弹尽粮绝的最后一刻，他毫不犹豫地选择同日军舰艇同归于尽，这是他们的青春故事。
研制原子弹归来的陈鹏 用深情撑住恋人的陈鹏 陈鹏(黄晓明饰)在影片中是清华大学的一名学生，在毕业后参与原子弹的研制工作。他对恋人王敏佳矢志不渝的爱，成为了支撑她熬过人生低谷的强大动力。影片中的王敏佳一心想帮助曾经的老师摆脱“家庭暴力”，结果反倒被师母诬陷她勾引自己老师，而她的虚荣心迫使她被组织审查直至接受批斗。在批斗中王敏佳失去了美丽的面庞，她的好朋友李想为争取支援边区的名额，不敢承认自己的错误并坚决同她划清了界限。只有陈鹏自始至终守护在王敏佳身边。可陈鹏依然在国家和个人的抉择中，选择了远赴沙漠参与原子弹的研制工作，对他而言，照顾王敏佳和研制原子弹，就是他生命的全部。可当陈鹏回到曾经熟悉的地方时，却发现恋人因为“破四旧”运动而不见踪影。在爱人与国家面前，在爱情与理想面前，我们到底应该怎么去选择，我想大概就是电影中陈鹏告诉李想那句话，“死者已矣，生者如斯”，这是“晃晃”哥哥和神父教给他的大爱。
眼神里都是戏的领导 现代人的感情，就剩这么点了 生活在现代都市的张果果(张震饰)，在尔虞我诈的人际关系中艰难前行，他恪守上下级的职场伦理却被有预谋地当作了替罪羊。面对女下属的质疑，不得不以高深莫测的“你猜”来掩饰内心的焦虑。对父母缺乏耐心以至于出去吃饭的时候直接将碗筷丢进垃圾框，他习惯性地防备着身边的一切，一如喜欢给自己覆盖一层冰冷盔甲的你我。他怕四胞胎家人纠缠而从医院匆匆离开，却又主动掏出名片在门口等人家追上来；目睹四胞胎家人住在没有暖气的出租屋里，冷峻着脸转身离开却又悄声让助理帮忙找个好点的两居室；他买来各种奶粉默默研究选出最满意的奶粉并长期供给，却又不愿意让四胞胎家人对他抱太高期望……起初会觉得这个角色和整个故事无关，可当张果果的父母交代出李想在边区为救自己而牺牲的故事，这一刻我们终于明白，这种传承从未改变，经历过迷茫和无助的张果果终于找回初心，自信地对这个世界说：“我和他们不一样”。
我相信每个时代里的青年都曾这样迷茫过，就像我们今天所要面对的这种困局一样。媒体称我们这一代人是得过且过的佛系青年，我们为工作中无法得到别人认可而焦虑过，我们为空有适婚的年龄而无适婚的感情而烦闷过，我们为透支父母毕生积蓄来买房而羞愧过……可你看每一个在时代洪流中挣扎的人，其生命无一不被同时打上时代和民族的烙印。吴岭澜是因真心而重新选择自己的人生，沈光耀是因为正义而投笔从戎，陈鹏是因为无畏而托住恋人投身科研，张果果是因为同情而重拾本心迷途知返。列夫·托尔斯泰说过，幸福的人生都是相似的，不幸的人生各有各的不同。每个时代有每个时代的不幸，我们今天不单单要面对水涨船高的房价，更要面对逐渐枯竭甚至贫瘠的心灵。有人说，这部拍给清华学生看的电影，对改善我们的生活毫无意义，可这世上有多少东西，说出来以后能不变得庸俗呢？
那时的爱情真美好 这个校服好帅诶 不管多远都要去寻找你 最后的国学大师，王国维 那时的青春真美好 同样是在迷茫的 20 多岁，有人找到了真性在生命里惊鸿一瞥，有人在沉浮中随波逐流迷失本心，前者是沈光耀目睹战友牺牲与敌舰同归于尽，后者是李想为争取支边名额背弃朋友间的友谊。陈鹏的爱慷慨而深沉，王敏佳不幸而无能为力……每个时代都是写意的，有佚群绝类的意气，有献血淋漓的痛苦，今天的人们注定无法理解曾经艰苦的生活，而曾经的人们同样无法理解现代人所面对的压力。现实中的离婚和出轨，让美好的爱情变得遥不可及。我们今天越是期待什么，我们的心里就越是缺乏什么。白居易说《长恨歌》是虚构的，而感情是真实的，这或许完全是我们的内心在作怪。世道艰辛，梦想与现实，家国与大义，爱情与理想，名利与善良，仿佛冥冥之中被逼迫到进退两难境地的每一个人，众生皆苦，各自悲欢。张果果在酒吧里拎着半瓶红酒自嘲道，“现代人的情感，就这么多了”，当本心迷失，便是我们有火车，有高铁，有飞机，都不知道要去哪里了！</description></item><item><title>致前任：愿余生各自安好</title><link>https://blog.yuanpei.me/posts/1358971951/</link><pubDate>Fri, 12 Jan 2018 00:39:39 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1358971951/</guid><description>也许是因为年纪大了的缘故，渐渐地如同上瘾一般喜欢上怀旧，喜欢怀旧到了什么程度呢？想着再次重温下小马哥的英雄本色，想着再次感受下星球大战里的刀光剑影……可是每次都因为不同的原因而落空，之所以没有去看《英雄本色》，是因为附近的电影院都没有排片，那么这一次呢，好像是因为一部和前任有关的电影《前任攻略 3：再见前任》。
或许前任与现任永远都是一道无解的题目，就如同网上流传着的一个问题，“我和你妈同时掉水里你会先救哪一个”，我们发现对这个问题而言，即使代入现任和前任这样的设定，它看起来依然是说得通的。那么，在我们心里是如何对待现任和前任的呢？我想，在很多人的潜意识里，前任代表了那个你曾经认定要一直走下去的人。我们曾经用天真对一个人好，发自内心地去爱，最终却用尽全力去遗忘释怀。这部电影所讲述的恰好是这样一个写实的场景。最近看到一段这样的话，我们总是用前任的错误惩罚下一任，或者是用对前任的亏欠弥补下一任。这两种做法均无可厚非，甚至听起来第二种做法更好，可如果扪心自问，到底是爱前任多些，还是爱现任多些，我想这是一个问题。电影里的两对情侣，孟云和林佳，余飞和丁点，同时因为生活中的矛盾分手，好兄弟和好闺蜜再度走到一起，这个时候，我们觉得失恋应该是痛苦的一件事情，没有喝醉到不省人事，没有悲伤到歇斯底里，这都不算做失恋吧。
可或许是我们想多了，有时候两个人分开以后，难过的只有自己，对对方而言或许是种解脱，于是我们在电影里看到了一幕幕单身男女嗨翻天的景象，我们常常听到这样一句话，两个人不合适就分手呗，讽刺的是，我们一边渴望着美好的爱情，一边对待感情弃若敝履，我们谈恋爱的时间越来越短，对彼此越来越没有耐心，有人说，这是因为我们长大了，知道 20 多岁的年龄，不能再像 10 多岁一样挥霍，像孟云和林佳这样的情侣，现实中举不胜举，为了彼此一点可怜的自尊，谁都不肯先放下身段同对方和解，等到矛盾积累得越来越多，任何一件事情都有可能成为，压死骆驼的最后一根稻草。这个时候，还要试图用扮演至尊宝说“我爱你”和疯狂吃芒果吃到过敏，这样幼稚可笑的事情来满足各自的虚荣心，好像我们每个人都实现了对彼此的承诺，真正爱你的人，怎么会舍得离开你呢，明明是自私得只懂得爱自己，非要坚持说是对方不够爱自己，真正爱你的人，爱一直都在心里。
我不知道这样一部电影，是如何让大家产生共鸣，在观影的时候哭得稀里哗啦的，电影中两位男主在“隔壁老王”，讨论是生男孩好还是生女孩好，讨论和双胞胎姐妹约会的好处，和一众长着网红脸的 95 后在 Party 上狂嗨……各种段子让分手变成一场狂欢，而狂欢下的两种状态，有余飞和丁点这种“以分手的名义，撒着思念的娇”的，有孟云和林佳这种因为沟通不善而渐行渐远的。我们常常听到一个词叫做合适，其实以人类这种奇葩的性格，又怎么会找到完全合适的两个人呢？这一切都是需要去不断磨合的，我们看待感情的角度是功利的，如果两个人走到一起甚至结婚，我们觉得这就是合适的，那么像孟云和林佳这种相爱五年的情侣，我觉得我们很难用一句合适或者不合适来说说清楚，两个人在一起一定要多沟通，有问题不要想着第二天再解决。在感情中不要用“作”这种手段去测试对方，一个人喜欢你，愿意忍让你，并不代表你可以毫无底线。也许很多时候，我们都喜欢把话埋在心里，我们都觉得谁先主动谁就输啦，即使此刻赢得了面子，最终还是会输掉了里子。
这部电影可谓是为“前任系列”画上了一个完美的结局，然而这样一部名不见经传的电影，可以在猫眼电影上获得 9.2 分的高分，一度超越近期口碑极佳的《芳华》，这种前半段走肾后半段走心的编排方式，成功地将观众的情绪带向高潮。有人说在这部电影里找到了自己的影子，可在我看来大家都在努力给自己加戏，两个人会分开一定是有某种原因的，我们总幻想着离开的时候，如何惊天地泣鬼神一般轰轰烈烈，可真正的离开从来都是悄无声息的。一场体面的分手，是两个彼此伤痕累累的的人，在面对这段感情时，即使知道其中存在的问题，但是发现一切再无法回到以前，因为在这个被称为成长的过程中，感情早已被当作燃料的消耗殆尽。所以，我们说：我依然爱你，但我不再喜欢你了，所以我们只能走到这里了。体面的分手，或许应该是这个样子的，是一切大彻大悟以后的慈悲，是用尽了力气后的平静，是虽然结局不如意，但我没有后悔遇见你，是接受成长带来的痛苦，是学会如何更好地爱自己、爱别人。
有人说，前任就像黑夜中摇曳着的烛火，坍塌在无边的夜色中。或许会有无数根的蜡烛，能照亮你的黑暗，但再不会有这样一个人，能这样一往无前地在黑暗中，为你烫出一个洞，然后用温暖填满。成长是件非常痛苦的事情，你不一定会得到什么，但一定会失去什么，有些人注定是让你成长的，所以请好好再见，不负遇见。有些人会永远停留在你的心里，他们曾经出现，并成为你人生的一部分，我们会爱上下一个，再下一个。放过前任，放过自己，这或许是最好的结局。</description></item><item><title>《C#多线程编程实战》读书笔记</title><link>https://blog.yuanpei.me/posts/345410188/</link><pubDate>Sun, 07 Jan 2018 21:34:36 +0000</pubDate><guid>https://blog.yuanpei.me/posts/345410188/</guid><description>本文是一篇读书笔记，由《C#多线程编程实战》一书中的内容整理而来，主要梳理了.NET 中多线程编程相关的知识脉络，从 Thread、ThreadPool、Task、async/await、并发集合、Parallel、PLINQ 到 Rx 及异步 I/O 等内容，均有所覆盖。为了帮助大家理解本文内容，首先给出博主在阅读该书过程中绘制的思维导图，大家可以根据个人需要针对性的查漏补缺。
《多线程编程实战》思维导图 线程基础 Tips1：暂停线程，即通过 Thread.Sleep()方法让线程等待一段时间而不用消耗操作系统资源。当线程处于休眠状态时，它会占用尽可能少的 CPU 时间。 Tips2：线程等待，即通过 Join()方法等待另一个线程结束，因为不知道执行所需要花费的时间，此时 Thread.Sleep()方法无效，并且第一个线程等待时是处于阻塞状态的。 Tips3：终止线程，调用 Abort()方法会给线程注入 ThreadAbortException 异常，该异常会导致程序崩溃，且该方法不一定总是能终止线程，目标线程可以通过处理该异常并调用 Thread.ResetAbort()方法来拒绝被终止，因此不推荐使用 Abort()方法来终止线程，理想的方式是通过 CancellationToken 来实现线程终止。 Tips4：线程优先级，线程优先级决定了该线程可占用多少 CPU 时间，通过设置 IsBackground 属性可以指定一个线程是否为后台线程，默认情况下，显式创建的线程都是前台线程。其主要区别是：进程会等待所有的前台线程完成后再结束工作，但是如果只剩下后台线程，则会直接结束工作。需要注意的是，如果程序定义了一个不会赞成的前台线程，主程序并不会正常结束。 Tips5：向线程传递参数，可以通过 ThreadStart 或者 lambda 表达式来向一个线程传递参数，需要注意的是，由 lambda 表达式带来的闭包问题 Tips6：竞争条件是多线程环境中非常常见的导致错误的原因，通过 lock 关键字锁定一个静态对象(static&amp;amp;readonly)时，需要访问该对象的所有其它线程都会处于阻塞状态，并等待直到该对象解除锁定，这可能会导致严重的性能问题， Tips7：发生死锁的原因是锁定的静态对象永远无法解除锁定，通常 Monitor 类用以解除死锁，而 lock 关键字用以创建死锁，Monitor 类的 TryEnter()方法可以用以检测静态对象是否可以解锁，lock 关键字本质上是 Monitor 类的语法糖。 bool acquiredLock = false; try { Monitor.Enter(lockObject, ref acquiredLock) } finally { if(acquiredLock) { Monitor.Exit(lockObject) } } Tips8：不要在线程中抛出异常，而是在线程代码中使用 try……catch 代码块。 线程同步 Tips9：无须共享对象，则无须进行线程同步，通过重新设计程序来移除共享状态，从而避免复杂的同步构造；使用原子操作，这意味着一个操作只占用一个量子的时间，一次就可以完成，并且只有当前操作完成后，其它线程方可执行其它操作，因此，无须实现其它线程等待当前操作完成，进而避免了使用锁，排除了死锁。 Tips10：为了实现线程同步，我们不得不使用不同的方式来协调线程，方式之一是将等待的线程设为阻塞，当线程处于阻塞状态时，会占用尽可能少的 CPU 时间，然而这意味着会引入至少一次的上下文切换。上下文切换，是指操作系统的线程调度器，该调度器会保存等待的线程状态，并切换到另一个线程，依次恢复等待的线程状态，而这需要消耗更多的资源。 Tips11：线程调度模式，当线程挂起很长时间时，需要操作系统内核来阻止线程使用 CPU 时间，这种模式被称为内核模式；当线程只需要等待一小段时间，而不需要将线程切换到阻塞状态，这种模式被称为用户模式；先尝试按照用户模式进行等待，如线程等待足够长时间，则切换到阻塞状态以节省 CPU 资源，这种模式被称为混合模式。 Tips12：Mutex 是一种原始的同步方法，其只对一个线程授予对共享资源的独占访问，Mutex 可以在不同的程序中同步线程。 Tips13：SemaphoreSlim 是 Semaphore 的轻量级版本，用以限制同时访问同一个资源的线程数量，超过该数量的线程需要等待，直到之前的线程中某一个完成工作，并调用 Release()方法发出信号，其使用了混合模式，而 Semaphore 则使用内核模式，可以在跨程序同步的场景下使用。 Tips14：AutoResetEvent 类用以从一个线程向另一个线程发送通知，该类可以通知等待的线程有某个事件发生，其实例在默认情况下初始状态为 unsignaled，调用 WaitOne()方法时将会被阻塞，直到我们调用了 Set 方法；相反地，如果初始状态为 signaled，调用 WaitOne()方法时将会被立即处理，需要我们再调用一次 Set 方法，以便向其它线程发出信号。 Tips15：ManualResetEventSlim 类是使用混合模式的线程信号量，相比使用内核模式的 AutoResetEvent 类更好(因为等待时间不能太长)，AutoResetEvent 像一个旋转门，一次仅允许一个人通过，而 ManualResetEventSlim 是 ManualResetEvent 的混合版本，一直保持大门开启直到手动屌用 Reset 方法。 Tips16：EventWaitHandle 类是 AutoResetEvent 和 ManualResetEvent 的基类，可以通过调用其 WaitOne()方法来阻塞线程，直到 Set()方法被调用，它有两种状态，即终止态和非终止态，这两种状态可以相互转换，调用 Set()方法可将其实例设为终止态，调用 Reset()方法可以将其实例设为非终止态。 Tips17：CountdownEvent 类可以用以等到直到一定数量的操作完成，需要注意的是，如果其实例方法 Signal()没有达到指定的次数，则其实例方法 Wait()将一直等待。所以，请确保使用 CountdownEvent 时，所有线程完成后都要调用 Signal()方法。 Tips18：ReaderWriterLockSlim 用以创建一个线程安全的机制，在多线程中对一个集合进行读写操作，ReaderWriterLockSlim 代表了一个管理资源访问的锁，允许多个线程同时读取，以及独占写。其中，读锁允许多线程读取数据，写锁在被释放前会阻塞其它线程的所有操作。 Tips19：SpinWait 类是一个混合同步构造，被设计为使用用户模式等待一段时间，然后切换到内核模式以节省 CPU 时间。 使用线程池 Tips20：volatile 关键字指出一个字段可能会被同时执行的多个线程修改，声明为 volatile 的字段不会被编译器和处理器优化为只能被单线程访问。 Tips21：创建线程是昂贵的操作，所以为每个短暂的异步操作创建线程会产生显著的开销。线程池的用途是执行运行时间短的操作，使用线程池可以减少并行度耗费及节省操作系统资源。在 ASP.</description></item><item><title>2017，在驻足间回首</title><link>https://blog.yuanpei.me/posts/2676125676/</link><pubDate>Sun, 31 Dec 2017 21:30:45 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2676125676/</guid><description>当朋友圈开始集体缅怀 18 岁的时候，我们说，从此以后是 00 后的天下，因为所以的 90 后都成年了。或许我们都是喜欢怀旧的人，所以我们选择以这样一种方式，在狂欢中失去的同时，在失去中缅怀着，仿佛这种死磕到底的做法，会让这一切来得更晚一些。一群人抱着手机等待着新年的到来，和曾经无数个无眠的夜晚一样，我们并不知道明天会有什么，就像钟表的指针哒哒地作响，生命的齿轮永不停歇地转动。我们习惯性地期盼明天会更好，因为我们都知道“往者不可谏，来者犹可追”。那么，在新年钟声敲响以前，让记忆彻底来一场独自的狂欢吧！
多情应笑我 我不知道该从哪里开始回顾我的 2017 年，如果一定要找一个起点开始说，我想应该是裴武刚离开西安。那时青龙寺樱花正在盛开，在他离开西安以前，我曾带他一起去那里游玩。他曾遗憾没有好好在西安游玩一番，在分别前我们一起去吃羊肉泡馍，真正体会到什么叫做相顾无言，直到十一回家去兰州转车，我们再次联系到彼此，所以当你想做一件事情的时候，最好就是趁着现在去做，一个过期的承诺是没有意义的，人生本就是大海里航行的一叶扁舟，你可以祈祷它不遭遇狂风暴雨，可事实上你并不能替任何人做这样一个决定，越想做好一件事情就越是要减少拖延。“人生不相见，动如参与商”，人生本不完美，缺憾能少一分便是一分。
我记得我们吃完泡馍，两个人都抢着付钱，老板娘笑着说：“大家都是好朋友，谁付都是一样的，以后还有机会”。两个人一阵默然。从那时算起，到今天差不多 8 个月，我们再没有见过面，偶尔会在微信上聊聊天。人生的无奈就在于，你埋头忙碌的时候，对离别的处境毫不在意，等到你过头来再看时，不禁怅然若失，感情就这样一天天变淡，可是不是你努力融入对方的生活，就能够留住彼此在心中的位置呢？有人告诉我，她不属于你，她不适合你，可是不是换一个性格完全相反的人，就一定会是合适的人选呢？我们的大脑，同绝对理性的计算机最大的不同是，我们评价和认识一个人的标准，永远没有办法达到统一，就像你喜欢吃葱，而我喜欢吃香菜，你不能因此指责别人三观不正，这或许仅仅是你们的喜好不同而已，这个理性而感性的世界，有时真的让人抓狂。
我一直没有告诉任何人，我和小古分开快两年的时间了。有人说，时间会慢慢磨去一切痕迹，而对于她，始终就像我心上的一颗痣，时间会把它变成一颗琥珀，时间越久越显得珍贵，我知道有人会嘲笑这固执的感情，就像我会在分开一年后，坐一个晚上的火车到洛阳，仅仅是为了见她一面而已，我永远记得她穿裙子的样子，我永远记得那条挂满灯笼的路，我永远都想再抱她一次……我听到莎莉花园的时候，会突然想起那个下午，当我们听到一家店里传来的音乐声，我们互相对彼此说我知道……我们在彼此不成熟的年龄深爱上彼此，等一天幡然醒悟的时候发现追悔莫及，离开她以后我发现我会单身很久，因为除了她我好像都不会同别人谈恋爱，这种迹象在我身上更加明显，因为无论你怎么尝试去改变，你永远无法逃开被拒绝的命运。我们喜欢喜欢一个人的时候，通常靠眼缘这种特别主观的东西，可太多时候我们无法完全了解一个人。
从前我们喜欢一个人的时候，喜欢爱得轰轰烈烈，相信爱情可以战胜一切；等到长大以后，我们习惯爱得小心谨慎，试探和套路混杂不清真真假假。有人告诉你，要相信爱情是存在的，你只是还没有等到那个合适的人出现；有的人告诉你，不要在一棵树上吊死。成年人的感情和时间都非常有限，不要把精力浪费在一个不喜欢你的人身上。可兜兜转转就到了 25 岁，一个谈爱已晚、谈死尚早的尴尬年纪。有时候我会想，大学毕业了就可以结婚成家的人，或许是最幸福的人，因为你再找不到那样单纯的爱情。我见过情侣间吵架无数分分合合，我见过向现实妥协随便找个人结婚……我想找一个可以托付终生的人，因为父母总有一天会先我们而去，如果需要有人陪伴在我生命的一刻，我希望那个人会是你，可我决不会如此自私，因为我不想你在这个世界里孤单。我曾被人温柔地对待过，我想温柔地对待你，不是因为亏欠或者内疚而弥补什么，而是我知道“爱人者人恒爱之”。
我曾经被一个女孩子拒绝过，我天真的以为，出场顺序会决定两个人的命运，直到她平静地说出，“就算我不和他在一起，我绝不会和你在一起“，原来一个在小古心里待她特别好的人，在别人眼中居然是如此的不堪，我记得我在她面前结结巴巴说英语的窘迫，我记得她告诉我她要订婚时我写满难过的脸……我好像被施诅咒一样，在和小古分开后，我喜欢过两个有男朋友的女生，有时候我都想告诉自己，我不再是那个十八九岁的少年，长大以后的爱情夹杂着现实，变得更加让人迷惑，可我再找不回那种“赌书泼茶”的感觉，或许求而不得是人生常态，王尔德说“人生有两种悲剧，一种是想得到的得不到，一种是想得到的得到了”，我对小古说，给我留一个梦好吗？她说好的呀。对这个世界而言，我们两个人的故事，或许一点都不美丽。可真正让我怀念的，恰好是这些微不足道的故事，有时候我会分不清，喜欢的到底是真实的她，还是想象中的她，或许我真正喜欢的，是曾经那个年轻而勇敢的少年，佛家有”贪嗔痴慢疑“，所谓五毒心者，都说人要学会放下执念，可如果从来没有得到过，又要放下什么呢？人因为“求而不得”而痛苦，从来没有拥有过的人，是无所谓拿起和放下的。
做人没意思 大概从 8 月份开始学习做饭，而学习做饭的动机早就模糊不清，我只知道，从那一刻开始，我的生活开始慢慢发生变化，从一个不知道做饭需要买什么调味品的人，变成一个喜欢逛超市、喜欢看商品价格的人，变成一个喜欢周末去超市购物、喜欢钻研美食的人，变成一个懂得如何照顾自己、爱惜自己的人。西安的面食对我而言是粗犷的，如同这片关中大地上粗犷的民风，所以最初就买了本菜谱学习做饭，我学做饭的动机，其实是非常简单的，最低要求是以后讨不着老婆不会饿死自己，进阶要求是两口子过日子必须要有一个具备这项生活技能，现在我们看到有把吃作为爱好的吃货，可这样的人是不能称之为吃货的，因为吃货如果就是花花钱动动嘴这样简单，就对不起古往今来的吃货们，譬如苏东坡之于红烧肉，季鹰之于鲈鱼，袁枚之于《随园食单》，能做会吃这是真吃货。有人说，征服一个吃货的心，首先要征服她的胃。可也许这仅仅是一个理由，当你征服她的胃，你会发现还有更多的东西等你去征服。现在，先让我征服这本菜谱，我刚刚学会其中的 10 来道家常小炒。
除了做饭这件事情以外，我一直在坚持的事情是读书，去年国庆的时候买了 Kindle，而年初则办理了省图的借书卡。一旦这两者结合起来以后，读书就变成了一件趣事。因为 Kindle 不擅长阅读技术类书籍，所以技术类书籍主要来自纸质书，而人文类书籍主要来自电子书。最早是 Wesley 推荐读陈忠实的《白鹿原》，这本书让我了解了许多陕西的风土人情。在同名电视剧中，张嘉怡饰演的白嘉轩，生平念念不忘的是妻子亲手做的油泼面。陕西的面食的确让人印象深刻，原本出生在北方的我，在这里仿佛第一次来到北方。此外，由电影《嫌疑人 X 的献身》追溯到原著，逐渐接触了《白夜行》和《解忧杂货店》这样典型的东野圭吾作品，程浩的《站在两个世界边缘》，毛姆的《月亮与六便士》，堀辰雄的《起风了》，《小王子》，沈复的《浮生六记》以及《追风筝的人》，而纸质书基本都是 Web 前后端(JavaScript/ASP.NET/SPA)相关的书籍，Python 相关的书籍和数学相关的书籍，这些书籍不能在这里展开讨论，感兴趣的朋友可以在博客里留言。
我是一个喜欢看电影的人，就是单纯地喜欢听人讲故事，这和约会意义上的看电影不同，我就是单纯地去看个电影而已。不过作为一个日常单身狗，总是要学会面对这个世界里满满的恶意。2017 年看的第一部电影是韩寒的《乘风破浪》，这是一个笑点中夹杂着泪点的故事，剧情上借鉴了陈可辛导演的《难兄难弟》。接下来，2017 年看过的超级英雄电影有《神奇女侠》、《蜘蛛侠 : 归来》、《雷神 : 诸神的黄昏》、《猩球崛起》。2017 年看过的动画电影有《大护法》和《寻梦环游记》，其中前者是电影风格和政治隐寓吸引了我，而后者是讲述了一个亡灵节背景下的暖心故事，告诉我们如何去面对死亡、如何平衡家庭与梦想等。2017 年看过一部讲述野外探险的电影《七十七天》，由赵汉唐和江一燕主要，这个电影是部旅游风景片，江一燕的存在感略弱，男主最终在雷雨夜里丧生，告诫人们要懂得量力而行，我们觉得去趟西藏就能净化心灵，可比净化心灵更重要的是好好活着。2017 年最后一部电影是陈凯歌的《妖猫传》，改编自梦枕貘的《沙门空海》，以唐朝时期僧人空海和诗人白居易为主角，讲述唐玄宗和杨玉环的爱恨纠葛，这部电影视觉美是无与伦比的，虽然后期剧情呈现方式存在瑕疵，但可以让我们重新感受大唐盛景，以一种新的方式解读《长恨歌》，我觉得这样就很好啦！
emsp; 2017 年写作方面相对去年明显退步，因为有两个月一直没有时间写东西，尤其是拖延症发作的时候，一篇博客大概两三个周甚至一个月方能写完，这大概是 2017 年最让人遗憾的事情。2017 年共撰写博客 16 篇，访客量增加约 30 万，受到 CSDN 运营梦姐的离开以及 CSDN 战略调整的影响，2017 年博客流量的主要来源是旧文章。2017 年技术博客的写作，基本延续 2016 年的策略，不再写面向新手的教程类内容，而是侧重对技术的整合和改进，尽可能地去写一种思路或者想法，与此同时，希望在技术博客以外扩展更多的，譬如写对生活的感悟、对电影的思考等等，计划中打算开通知乎专栏(已开通)、开通个人微信公众号(正在准备)。总而言之，希望拓宽博客的流量渠道，提升个人品牌的影响力，你可以不用长得特别帅，但要有一种不怒自威的气场，因为要想成为架构师，这是件任重而道远的事情！
New TodoList(&amp;ldquo;2018&amp;rdquo;) 写这样一个 TodoList，我总觉得像在立一个个 flag，因为 2017 年计划的 MongoDB 就没有达到目标，所以 2018 的 TodoList 我希望可以更接地气一点，具体来讲，我从技术、生活两个方面来制定目标：</description></item><item><title>基于新浪微博的男女性择偶观数据分析(上)</title><link>https://blog.yuanpei.me/posts/1386017461/</link><pubDate>Sat, 23 Dec 2017 20:28:40 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1386017461/</guid><description>或许是因为我喜欢的姑娘从来都不喜欢我，而感情上的挫折一度让我陷入无尽的自卑。朋友在朋友圈里发布一条关于皮影戏的动态，我开玩笑说这个皮影戏结局应该是个悲剧，因为我注意到在剧中，无论一个人如何卖力地表演甚至双腿跪倒在地，有的人从故事开场到结束一直对此无动于衷。朋友回复我说，这不就是你现在的状态吗？我沉默半天终于熄灭手机屏幕。我听到过各种各样让我放弃她的话，即使这种念头在我脑海里萌生已久，是幻想让我硬生生地拖了这么久。当你努力想要融入对方的生活，而等待你的是一道冰冷的墙。这种感觉像什么呢？大概就是一个又一个“好友”安安静静地躺在联系人的置顶名单里，不敢发消息让对方知道，更不愿残忍地把对方删除。我安慰自己说，对我而言，我失去的是一个不喜欢你的人；而对对方而言，失去的是一个喜欢她的人。你当然可以说我没有那么喜欢她，如果一定要喜欢到卑微如尘土的地步，我宁愿一个人单身到天荒地老。
当我意识到人与人间，即使亲近如父母尚且无法完全理解彼此的时候，我忽然发现一个有趣的现象，我们喜欢一个人的时候，首先注意到的会是外表，我们将其称之为眼缘，所以人与人间的感情纽带最初会是吸引，而后是了解彼此的优缺点，最终是相互理解和扶持。可我们知道，外表是可以伪装出来的，所以我们习惯通过外表和言语来评价一个人，这就像是数学归纳法，我们总认为推倒第一块多诺米骨牌，就意味着所有多诺米骨牌都会倒下。可现实世界矛盾的地方就在于，我们认为理所当然正确的事情，或许正是我们无法证明其正确性的，这在数学上称为哥德尔不完备定理。所以，一件残酷的事情是，当你无法吸引一个人的时候，通往内心世界的路就被堵死了。朋友圈里精彩纷呈的社交互动，并不代表有人愿意真正了解你的生活，何况是你吸引不到的人呢？我很想知道，我们在选择伴侣的时候到底看中什么，所以我一直在关注@西安月老牵线上发布的征婚交友类微博，本文的故事从这里正式展开。
身高 175 的悲伤 或许你以为我会无聊到试图从微博上找到女朋友，可事实上作为一个程序员的我，即使整天投入精力在编程上，依然无法避免对象空引用的异常出现。如果说找到女朋友是个小概率事件，那么在我看来，找到一个真正懂我、喜欢我的女朋友，基本上是不可能事件。你不要觉得我对没有调整好心态、对生活过分悲观，如果你了解贝叶斯公式就会真正地理解我说的话。这个微博开始引起我的注意，是我发现身高在 155 到 165 左右的女生，对男生的要求基本上无疑例外地是 175+到 180+，我想知道到底有多少女生是有这样的想法，这是我想要抓取新浪微博的数据进行分析的初衷。更重要的是，身高不到 175 的我在面对这种要求的时候是悲伤的，因为我想起了《巴黎圣母院》中的卡西莫多，一个外表丑陋而拥有高尚人格的“丑八怪”。现代人整天都特别忙碌，以至于没有人会有耐心，园艺在忍受着你丑陋的外表的同时，同你讲一只小兔子亲了它喜欢的长颈鹿一下这种故事。
我听到这样一句话，“好看的皮囊千篇一律，有趣的灵魂万里挑一”，可谁会觉得像卡西莫夫这样的人，会拥有或者配拥有高尚的人格呢？我们这副皮囊不管好看与否，它们都是父母给予我们的最好的礼物。难道一个所谓情商高的人，会在收到别人的时候因为礼物不好看而生气吗？ 我想起《画心》里懊悔受狐妖小唯皮相蛊惑而自毁双目的霍心，美丑都是父母赐予我们的，不该被我们拿来一番大肆炫耀，可我还是想知道，我们评价一个人的标准到底是什么？因为我渐渐明白，有些人不喜欢我们，并不是我们不好，而仅仅是某一点和对方不匹配。喜欢一个人的时候，像拔下身上的一根根刺，因为你越是得不到回应，就越像变成对方期待的样子，这个过程会让你觉得自己一无是处。直到今天看到一句话，一句足以热泪盈眶的话，如果不曾喜欢你，我本来非常可爱的。有时候，人做一件事情，或许就是在和自己过不去，比如说这件事情。
花点时间爬爬微博 好了，现在我们来考虑从新浪微博上抓取@西安月老牵线上发布的微博，因为这是我们进行数据分析的前提。事实上，在写这篇文章前我曾花了大量时间来调试爬虫，然后用了一天的时间对数据进行清洗，最终利用晚上下班的时间生成词云。由此我们可以理出整体的思路：
流程图 通过流程图我们可以注意到，在这里我选择了 Python 来实现整个功能。转眼间我已经 25 岁了，这是种什么样的概念呢？两年前我 23 岁的时候，听别人讨论结婚这个问题，我觉得它离我还很遥远。如今看着周围人都结婚了，我竟有种“高处不胜寒”的感觉。所以呢，人生苦短，当你不能阻止时间一天天消逝的时候，你只能趁着现在去做你想做的事情，为了节省时间去做技术以外的尝试，我选择拥有全世界最丰富的库的 Python。
这段时间学习数据分析，我渐渐意识到我们所熟悉的这个世界，如果以一种理性的角度，完全通过数据来解构的话，我们在这个数字时代里留下的每一条讯息，都冷冰冰地暴露着我们的喜怒哀乐，每一张照片里细微的表情变化，每一段文字里隐匿着的真实意图，都能被人脸识别和自然语言处理等等，这类人工智能为代表的技术所解读，我们努力想在朋友圈里隐藏些什么，当朋友圈的访问范围从半年逐渐缩小到三天，我们究竟能隐藏下什么呢？
微博爬虫分析 首先，我们需要从微博上抓取数据下来，我没有去做抓包分析这样的重复性工作，因为我注意到这个问题，在网络上有很多朋友在讨论，我主要参考了以下内容：
Python 爬虫如何机器登录新浪微博并抓取内容？ https://github.com/xchaoinfo/fuck-login 用 Python 写一个简单的微博爬虫 通过以上内容，我了解到在抓取新浪微博数据的问题上，我们基本会有以下思路：
保存 cookie 信息，利用 requests 库发起请求并带上 cookie 利用 requests 库模拟登录新浪微博并在请求过程中保持 cookie 利用 selenium 库模拟登录新浪微博然后取得页面内容 利用 PhantomJS 库模拟登录新浪微博然后取得页面内容 可以看出差异主要集中在 cookie 的获取以及是否支持 headless 模式，并且我们得到一个共识，抓取新浪微博移动版要比PC 版要容易，因为移动版优先为小尺寸屏幕设备提供服务，因而页面结构相对整洁便于我们提取数据。起初博主认为第一种方式太简单粗暴，坚持要采用第二种方式去实现，最终证明还是太年轻了啊，新浪微博的登录给人的感觉就是蛋疼，这里就简单介绍下思路哈。
首先我们会向服务器发出一次 GET 请求，它返回的结果是一段 JavaScript 代码，然后我们需要用正则匹配出其中的 JSON 字符，这样我们就获得了第二次请求需要用到的参数；接下来，第二次请求是一个 POST 请求，我们需要将用户名采用 Base64 加密，密码则采用 RSA 加密，需要用到第一次请求返回的参数。实际上，新浪微博官方给我们提供 API 获取微博数据，可这个 API 可以获取的微博数据非常有限，更让人难以接受的是新浪微博的应用授权方式，如果我们采用调用 API 的方式，在这里会有第三次 POST 请求，有朋友分析了完整的模拟登录过程，可我对此表示毫无兴趣啊。最早我采用了模拟这种方式，抓取第一页的时候还是登录的状态，可等到抓取第二页的时候变成了注销的状态，整个过程使用的是同一个 session 对象，所以我最后果断放弃了这种方式。</description></item><item><title>冬天来了，春天还会远吗？</title><link>https://blog.yuanpei.me/posts/3111375079/</link><pubDate>Sun, 19 Nov 2017 10:16:17 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3111375079/</guid><description>接到妈妈打来的电话时，时间已然接近中午时分，从床上爬起来的刹那间，就听见妈妈熟悉的声音。妈妈问我年底公司有没有什么变动，顿时千万种思绪涌上心头，不知道该对电话彼端的妈妈说些什么。我突然想到二十四岁时的我，从第一家公司裸辞时的情景，可如今再度让父母为此焦虑，让身为人子的我感到惭愧不安。电话里妈妈让我照顾好自己，一个人在外不要太委屈自己。当一个人不被这个世界接纳的时候，就像是浑身长满刺的仙人掌。可在最亲近的家人眼里，我们永远是这个世界上独一无二的存在。我是一个不大主动同家里的人，在那一刻我忽然觉得，这个冬天没有那么冷了，即使我身处没有暖气的出租屋里，即使早晨带着体温的被子早已凉透，我想对自己说一句，冬天来了，春天还会远吗？
发生在年底的 release 事件，就像这个冬天里的雾霾如约而至，即使早在去年就经历过这样的事情，可当它真实地发生在自己身上时，依然不免让人感到这个冬天的寒冷。纵观二十朝兴废更替，历史对我们而言常常是相似的。诸葛亮为“克复中原”六次北伐出师未捷而身先死的遗憾，唐玄宗宠溺杨玉环终致“安史之乱”而奔走蜀中避祸时的仓皇，这一切大概是我们如今坐西成高铁时无法想到的吧。有时候人的命运像极了历史的兴衰，记得三年前和同学第一次来西安，那个时候我们说，总有一天我们会再来这里的，那时的我或许完全不知道现在会面临这种处境，看着那些年龄比自己大依然碌碌无为的人，看着那些面临中年危机而不得不向生活妥协的人&amp;hellip;我告诉自己，我永远都害怕自己变成这样的人，所以让我内心无法平静下来的，永远是我近乎自责的自我反省式人格。
在接受被 release 的事实以后，就开始频繁地去准备面试和跳槽。可年底时找工作注定是一个艰难的过程。其实回头想想今年面试的表现，前端、数据库等相关经验的匮乏，一度让我在面试中非常被动，而外冷内热的性格常常给人一种不自信的表现，特别是去葡萄城面试的这次体验，让我意识到在面试中我无法展示出和职位匹配的能力，我们说面试是双向选择的一个过程，这看起来有点像是找男女朋友一样，有时候我们太在乎对方而导致表现不佳。我花了时间去听知乎上有关面试技巧的 Live，甚至找朋友帮我分析如何给出面试官满意的答案，有时候别人会觉得我对严厉到苛刻的程度，是因为我对某些东西太在乎的缘故，可是不在乎这些问题就能解决了吗？矫枉过正至少意味着出发点是好的，总比发现问题后一直无所作为要好很多。
我一直想找时间整理下这段时间面试遇到的题目，可令人窒息的拖延症让这种想法一直落空。有时候我怀疑，人和人的缘分都在第一眼就注定好了。或许你花时间和精力去追一个女生，但这种关系永远不会发生改变。人类总是肤浅地相信眼睛看到的，固执地认为自己的想法就是正确的，可人这种复杂的动物怎么会一眼就望穿呢，所以试图通过面试完全了解一个人，原本就是不切实际的想法。很多时候人与人接近并不是他们彼此熟悉，仅仅是因为大家的口味比较接近而已。无论多么熟悉的同事，在分开以后都会逐渐变得冷淡。每个人都像一只刺猬，离得太远会感觉到冷，而靠得太近会刺伤对方。大概是遇见小古花光所有运气，此后遇人不淑的厄运纷至沓来。人啊，简直是世界上最麻烦的一种动物。
有时候我会埋头去做自己的事情，朝着自己内心的目标一点点靠近，即使曾经想要和她分享这点喜悦的人，早已消失在茫茫的人海里。我写爬虫、做数据分析、做聊天机器人，这其中有太多事情，是我对记忆的一种自我延伸，因为在那些曾经灰暗的日子里，陪伴我的人除了她，就是这些我比任何人都要在乎的技术。《嫌疑人 X 的献身》里，石神说道：“通往山顶的路或许会有很多条，而找出最优雅的那一条，是数学家永恒的追求”。也许他们说得都是对的，即使翻过了 2017 年，这个季节依然属于冬天，她只会比以前更让你觉得寒冷，可这一切的一切终究是会过去的，冬天来了，春天还会远吗？</description></item><item><title>迁移 Hexo 博客到 Google 渐进式 Web 应用(PWA)</title><link>https://blog.yuanpei.me/posts/450254281/</link><pubDate>Tue, 24 Oct 2017 23:13:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/450254281/</guid><description>如果说通过 TravisCI 实现博客的自动化部署，是持续集成这个概念在工作以外的一种延伸，那么今天这篇文章想要和大家分享的，则是我自身寻求技术转型和突破的一种挣扎。前段时间 Paul 同我聊到 Web 技术的发展趋势，Paul 认为 Web 应用会逐渐取代原生应用成为主流，我对此不置可否。真正让我陷入思考的是，在这个充满变化的时代，知识的更新速度远远超过你我的学习速度，我们应该如何去追随这个时代的步伐。如同那些淹没在时间河流里的技术名词，当青春不再的时候，我们喜欢把这个过程称之为成长，当发现距离第一次使用 FontPage 制作网站已过去十年，当发现曾经的网页三剑客在岁月蹉跎里频频改换姓名，当发现那些淹没在历史里的技术来不及学习就成为过往&amp;hellip;&amp;hellip;或许，这个世界真正迷人的地方，就在于它每天都在不断变化。
新一代Web应用——PWA 接着 Paul 关于 Web 技术的这个话题，我认为 Web 技术在短期内会成为原生应用的一种补充。事实上，原生应用和 Web 应用哪一个会是未来，这个问题的争论由来已久，在业界我们可以看到 HTML5、PhoneGap、React/React Native、Electron/NW.js、小程序等方案百家争鸣，每一种方案都可以让我们去 Web 技术去打破平台间的差异。与此同时，我们注意到移动开发领域对原生技术的需求在缩减，虽然马克·扎克伯格曾表示，“选择 HTML5 是 Facebook 最大的错误“，可我们注意到，越来越多的 Web 技术被运用在原生应用中，Web 技术被认为是最佳的打造跨平台应用的技术，可以通过一套代码实现不同平台间体验的一致性。我们注意到知乎和天猫的客户端中都混合使用了一定的 Web 技术，因为纯粹使用原生技术去开发一个移动应用，其最大的弊端就在于我们要为 Android 和 iOS 维护两套不同的代码，从国内曾经疯狂火热的 iOS 培训就可以看出，单独使用原生技术去开发客户端，其成本实际上是一直居高不下的。
虽然我们有 Xamarin 这样的跨平台技术，试图用一种编程语言和代码共享的方式，去开发两种不同平台的应用程序，可是我们注意到，平台间的差异和抗阻是天然存在的，就像SQL和面向对象这样我们再熟悉不过的例子。同样的，Facebook 的 React Native 项目，试图用Web技术去弱化平台间的差异，React Native 存在的主要问题是，它依然依赖原生组件暴露出来的组件和方法，所以像 DatePickerIOS、TabBarIOS 等控件是 iOS Only的，这意味着在开发过程中开发者还是要考虑平台间的差异性，其次 React 本身的JSX(对应HTML)、CSS Layout(对应CSS)本身是具有一定的学习曲线的，虽然底层因为没有使用WebView的原因提高了部分性能，然而整体上是牺牲了扩展性的。总而言之，这是一个介于 Web 技术和原生技术之间的中间技术，在我看来地位着实蛮尴尬的，因为无论在Web层还是Native层都选择了部分妥协，完美实现跨平台真心不容易啊。
要掌握一门新技术，最好的方法就是去应用它。我的博客使用的是 Indigo主题，这是一个典型的 Material Design 风格的主题，所以我一直想尝试将其改造成原生应用，我曾经接触过移动端应用开发，如果通过 WebView 内嵌网页的方式来实现，我需要处理离线状态下页面的显示问题，以及所有混合应用开发都会遇到的一个问题，即原生应用层需要和Web应用层进行通信的问题。而如果采用 Hybrid App 的思路去开发一个混合应用，意味着我需要去学习 Cordova 这样的 Hybrid 开发框架，去了解 JavaScript 和 Native 交互的细节。那么有没有一种学习成本相对较低，同时可以提供原生应用体验的思路呢？答案是确定的，这就是我们下面要说的渐进式应用(PWA)。</description></item><item><title>持续集成在 Hexo 自动化部署上的实践</title><link>https://blog.yuanpei.me/posts/3521618732/</link><pubDate>Sat, 21 Oct 2017 22:57:55 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3521618732/</guid><description>曾经听到过这样一句话，&amp;ldquo;不要用战术上的勤奋掩盖战略上的懒惰&amp;rdquo;，所以战术和战略更像是抽象类和具体类，而面向对象设计实际上是现实等级制度的一种映射。因此我们注意到，决策者通常关注的是战略层面的抽象概念，而执行者通常更关注战术层面的具体实现，正如在代码的架构设计中，处在顶层的代码以发送指令为主要使命，处在底层的代码以实现功能为主要使命。面对日新月异的互联网技术，当我们听到越来越多的新名词，譬如微服务、DevOps、单页面应用、前后端分离等等，这些概念曾让我们迷恋于追寻一个又一个风口，一如曾经的 O2O、VR、共享经济和人工智能，那么我们真的懂得如何让这些概念落地吗？在今天这篇文章中，我想和大家一起探讨持续集成相关的话题，并以 Hexo 结合 TravisCI 实现自动化部署为例，聊聊我心目中的 DevOps。
从 DevOps 谈谈持续集成 不知从何时起，DevOps 开始成为大家竞相追捧的概念，同 ThoughtWorks 所倡导的微服务、敏捷开发一样，大家仿佛抓住了一根新的救命稻草一般，那么我们在说 DevOps 的时候，我们到底想要表达什么观点呢？想要搞清楚这个问题，我认为首先要明白，什么是 DevOps？从概念上讲，DevOps 是一个面向 IT 运维的工具流，以 IT 自动化以及持续集成(CI)、持续部署(CD)为基础，目的是优化开发、测试、运维等所有环节，所以 DevOps 本质上是一组部门间沟通协作的流程和方法，其目的是为了协调开发(DEV)、测试(QA)、运维(OPS)这三种角色，使开发运维一体化，通过高度自动化工具和流程，来确保软件构建、测试和发布更加快捷、频繁和稳定。
所以，我们在说 DevOps 的时候，我们想表达的或许是流程和管理、运维和自动化、架构和服务、文化和组织等等的概念，那么在这些观点中，最重要的是什么呢？我认为是持续集成(CI)和持续部署(CD)，这是 DevOps 中从始至终贯穿的一条主线。通过 Git 这样的源代码控制工具，我们可以确保项目在一条主干上开发。而自动化测试/部署等周边工具，则为我们提供了实施持续集成/持续部署的必要条件。从公司角度出发，公司普遍更看重项目的交付能力，所以在传统持续集成/部署的基础上，我们时常会听到持续交付这样的声音，这时我们就会意识到，DevOps 实则是持续集成思想的一种延伸，它并不是一个新的概念，事实上我们这个行业，每年都喜欢这种“旧酒换新瓶”的做法，持续集成/部署/交付是 DevOps 的核心技术，如果没有自动化测试和自动化部署，DevOps 就是难以落地的空中楼阁。
由此，我们就引出今天这篇文章的主题，即持续集成。我们提到，DevOps 是是一套面向 IT 的跨部门协作的工作流，它是持续集成思想的一种延伸，所以持续集成首先是一组工具链的集合。从某种意义上来讲，决策者喜欢 DevOps，并不是真正喜欢 DevOps，而是形式上的 DevOps 非常容易实现，因为有形的工具资源的整合是非常容易的，真正困难的是无形的流程资源的整合。你可以让两个陌生人在一起假装情侣，但你永远不可能真正拉近两个人心间的距离。通常而言，我们会用到下列工具：
版本控制和协作开发：Github、GitLab、BitBucket、Coding 等。 自动化构建和测试：Apache Ant、Maven、Selenium、QUnit、NUnit、XUnit、MSBuild 等。 持续集成和交付：Jenkins、TravisCI、Flow.CI 等。 容器/服务部署：Docker、AWS、阿里云等。 从术和道的角度来看待持续集成，我们会发现在术的层面上，我们有非常多的选择空间，所以接下来我们主要从道的层面，来说说持续集成的核心思想。我们提到在实践 DevOps 的时候，需要有一条项目主干，那么持续集成的基本概念，就是指频繁地提交代码到主干分支，这样做的目的是，保证问题被及时发现以及避免分支大幅度偏离主干。
在使用 Git 的场景下来看待持续集成，及时提交代码到主分支，可以避免因为分支改动过大而带来的冲突问题。按照敏捷开发的理论，每个 feature 通过迭代开发来集成到最终产品中，那么持续集成的目的，就是为了让产品可以在快速迭代的同时保证产品质量。在这里产品质量有两层含义，第一，本次 feature 提交通过测试；第二，本次 feature 提交无副作用。我们可以注意到，持续集成的第一个目的，即保证问题被及时发现，对应前者；持续集成的第二个目的，即避免分支大幅度偏离主干，对应后者。
所谓持续集成，是指代码在集成到主干前，必须要通过自动化测试，只要有一个测试用例失败，就不能集成到主干，所以持续集成和自动化测试天生就是紧密联系在一起的。我们不能只看到持续集成/部署/交付，如果连流程上的自动化都无法实现，这些都是无从谈起的，从开发者的角度来看，理想的状态是编译即部署，我们提交的每一行代码，都是可以集成、交付和部署的代码，所以实际上是对开发者的代码质量提高了要求。所有我们觉得美好的事情，其实核心都在于人如何去运作，想到一位前辈说过的话，“软件开发没有银弹”，所有试图通过某种方法论解决软件工程复杂性的想法，都是天真而幼稚的。
Jenkins 持续集成落地实践 博主曾经在公司项目上实践过持续集成，深感持续集成想要真正在团队里落地，受到太多太多的因素制约。我们采取的方案是，使用 Git/Github 作为源代码版本控制工具，使用 Jenkins 作为持续集成工具，使用 MSBuild 作为项目构建工具，使用 MSTest/NUnit 作为单元测试框架，使用 Selenium/UI Automation 作为 UI 自动化测试框架，这些工具可以很好地同 Jenkins 整合起来。在持续集成工具的选择上，我们并没有太多的选择空间，因为公司需要同时支持 Java 和 JavaScript/Nodejs 项目的持续集成，在持续集成落地这件事情上，我们最终选择了妥协，我们不再追求自动化部署，而是选择通过这个过程来快速定位问题，具体原因我们下面来讲。</description></item><item><title>不如归去</title><link>https://blog.yuanpei.me/posts/720539850/</link><pubDate>Sat, 21 Oct 2017 22:31:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/720539850/</guid><description>独自一个人在火车上望着窗外出神，而这种情景我再熟悉不过，或许风景会因为季节而不同，或许时间会因为年龄而不同，但对我而言，这个过程熟悉得就像一个我讲了无数遍的故事，从开篇布局到故事脉络都清楚到严丝合缝。印象中是从初中时候就开始寄宿生活，所以这种漂泊的感觉成为我生命里重要的烙印，而从那一刻起，我最期待的是寒暑假期，因为这象征着一段漂泊旅程的结束。大概是因为双子座属于风象星系，所以每次回家的旅程都伴随着忧愁风雨，我喜欢将这个过程理解为，生命不经意的装点。风有质而无形，或飞沙走石，或拈花弄叶；雨有质而无形，或坠散成珠，或凝聚成流。大概都是在世间漂泊的浪子，每一刻都摇曳不定。
有人说，放假回家是学生时代做的事情，因为那时我们还没有脱离父母的庇护。可当我工作了以后，当我同他们的距离不再是学校和家的距离，当我同他们打电话不再是因为生活费告拮，我忽然发现我同他们交换了这场漂泊里的角色，从前是我期待着假期，因为我可以见到他们；现在则是他们期待着假期，因为他们可以见到我。我忽然发现我一年中我陪他们的日子屈指可数，从前向往远方觉得在那里能找到我的梦，现在越发地觉得他们一天天老去，想努力长大成熟，让他们能够放心，可更怕因为距离而疏远了他们，古人说『树欲静而风不止，子欲养而亲不待』，这个世界固然不再是古人所认识的世界，这种感情却超越了时间和空间轮回至今。
国庆本来打算不回家的，因为即使无人可约无人可陪，我一个人同样可以给自己放假。我从来都是这样，如果有人愿意陪我做一件事情，我会很乐意接纳这番好意，并尝试用最好的状态去让彼此享受这个过程。如果没有人愿意陪伴我做一件事情，我不会勉强更不会因此而沮丧，因为当你习惯了一个人去面对所有事情，你会发现你从来不缺乏做一件事情的动力。可当家里人问我国庆要不要回家的时候，我突然心软得像融化了的雪，我意识到是我心底涌出的一股暖流，快速地打开微信买回家的火车票，结果发现 15 号的时候票就没有了，妈妈发微信给我说，『如果实在买不到火车票，就买机票回来吧，不用太心疼钱，人回来了就好』。
我记得以前他们都不大会用这类 IM 产品的，但现在他们学会了怎么发消息发朋友圈。记得过年的时候，我教妈妈怎么样发红包，虽然只有三块钱这样来回发着玩，但她玩得比我都开心，或许父母就是这样，曾经青春期叛逆时觉得他们的世界和自己格格不入，等他们老去的时候依然在努力着融入我们的世界。小时候他们紧紧追逐着我们，是怕我们在成长路上摔倒；长大了他们依然紧紧追逐着我们，是怕我们的世界里没有了他们。可他们在一天天地老去，如果有一天他们追不动了，我们是否愿意停下来等等他们呢？窗外的若明若暗的，像极了我往常回家路上数过的每一盏灯，可是我最喜欢的那一盏，是来自那个叫做家的地方，它或许并不璀璨耀眼，但向来不吝于为你释放光和热，永远为你指示着家的方向。
早上去北客站取票坐车，就接到朋友从中卫打来的电话，询问我国庆是不是要回家，这种感觉就像家里人一直记挂着你。我经常会想起高中时候，有时同朋友出去玩到很晚回家，常常会到朋友家里借宿，两个人寝则同床食则同桌，到今天我们依然是特别特别好的朋友。我幸运的是一直被朋友这样照顾着，即使我们两个在毕业后天各一方，我这个人不大懂得经营感情，被这样的朋友一直照顾着，我该是有多幸运啊。因为没有买到直达的车票，所以坐动车到兰州去转车，然后遭遇了人生中第一次火车晚点。火车晚点近三个小时，第一次迫切地感觉离家好远好远，每次回家都是披星戴月，或者在夜深人静时，或者在曙光初现时，让家人和朋友等我，我总是过意不去的。大概他们都太了解我，知道我会永远都是这副『长不大』的样子，可我总要长大成熟啊！
我记得以前还在父母身边时，我脑子里想的是『父母在，不远游，游必有方』，我一位同学曾问我，何谓有方，当时的我真的是不知道该怎么样回答。后来，一个人去西安发展，终于明白，以前公司里一个女生所说的，在哪里都一样。想到父母孤零零地待在家里，想到每年屈指可数的回家次数，曾几何时，我们想去更广阔的世界寻找诗和远方，可父母逐渐蹒跚的步履注定无法，陪伴我们去那些遥远的地方。诗经里说『青青子衿，悠悠我心，纵我不往，子宁不来』，如果他们不能再紧紧追随我们的步伐，我们为什么不停下甚至转身回去去看望他们呢？以前和我徒弟聊天，她说男生都不大喜欢和家里人联络，我本来就是沉默寡言的性格，从初中时候就不主动和家里人联络，现在我想改变这种想法，因为我想有空就陪陪家里人。
我喜欢在漫长的旅途中看书，这种习惯在我有了 Kindle 以后变得更为明显。早上看到这样一句话，『你懂得越多，就越觉得自己像这个世界里的孤儿』，人生而孤独，父母总有一天会离开我们，但他们教会我们如何去爱这个世界，我希望我可以用他们教会我的，在他们有生之年做些在他们眼里不再『孩子气』的事情，虽然在他们眼里我们永远都是孩子，我觉得真正的成熟并不是变得冷酷麻木，而是你知道这个世界有黑暗的一面，依然愿意相信那些温暖的事情。我到现在依然不喜欢听别人讲“道理”，因为我觉得人生最奇特的地方就在于，即使别人讲得这些“道理”都对，别人依然无法代替你去体验整个生活。每个人都是生活这片大海里的一朵浪花，为了不被岁月冲上荒凉寂寞的沙滩，我们唯有追逐巨浪努力生存。有那么一瞬间，我想和我喜欢的姑娘生活在一个城市里，等有空了就带上她回家陪陪父母。
或许有些地方有比故乡更广阔的天空，或许有些地方有比故乡更湿润的土壤，或许有些地方有比故乡更精彩的旅程，我想说的是，不论你是在追求诗和远方，还是在忍受眼前的苟且，如果你觉得累了，请停下忙碌的脚步，找找回家的路，那里永远有人在等你！</description></item><item><title>秋风劲似去年时</title><link>https://blog.yuanpei.me/posts/2617501472/</link><pubDate>Mon, 25 Sep 2017 00:56:50 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2617501472/</guid><description>连续数日的秋雨绵绵，依然固执地不肯转身离开，而之所以选择在国庆节前徘徊，或许是为了让离开家的人，多些同江湖风雨漂泊的味道。印象中这样的日子常常是相似的，譬如穿行在骤雨中被来往车辆溅得一身水，或者行走在上班的路上抬头看见第一场雪，或者是倚靠在公交车窗边上看风景转眼即逝，这些再熟悉不过的场景对我熟悉而又陌生，我惊异于记忆常常像盗梦空间般重叠，我感概于时间常常像钟表指针般流连。我不知道这个世界上是不是有平行世界，但我知道我再回不去曾经某一个时刻，我一直想写下这段时间的状态，可当我准备下笔时才发现，它需要我努力想好多事情，我依然还是曾经的我，风景依稀还是曾经的风景，到底是谁在一直变化呢？
我不知道要从什么时候回忆这些事情，这种感觉就像是你期待了许久之后，在触碰到她的那一刻都不复存在了。我曾经答应过一个人要去看望她，如果你读过 《一个人的朝圣》 这本书，或许就会明白这是一种怎么样的执念，即使在明明知道一切再无法挽回的时候，这种执念还是让我想要达成这个愿望。我一直不知道两个人怎么就自然而然地在一起了，那种感觉如果一定要用语言来形容，我觉得是一种熟悉到灵魂里的默契。你想要牵着她的手的时候，她假装挣扎下后就一直让你牵着，那种娇羞中透着可爱的神情，在四目相对的时候眼睛里都是闪着光的。我忘不了在人潮中牵着她的手穿过整条街市，我忘不了抱着她的时候街市两边灯笼通红。有时候觉得人生充满了遗憾，好像错过她花光了我这辈子的好运气，从那以后我总是重复着昨天的故事。
其实我自己都不清楚，我到底喜欢什么样的女孩子，甚至有时候我喜欢的是，我心中她最美好的样子。一个人少不经事的时候，大概会喜欢对女孩子说甜言蜜语，可当他经历过失去以后，他变得不再轻易许诺，这就好像我小时候是一个特别喜欢说话的人，在经历过因为紧张而变得口吃以后，我终于变成了今天这副沉默寡言的样子。有时候会陡然间觉得自己并没有怎么变，或许是因为她说过她喜欢我这个样子，所以我就固执地不肯改变，因为我怕有一天她回来的时候认不出我来，即使这是我脑补的一个剧情。曾经看过一个电影 《这个男人来自地球》，当我们熟知的宗教历史变成一个人的回忆，这种超越哲学意义上的时间我认为是荒凉的。曾经的小伙伴 Alex、Sandy、Kent、Andy、Kent、Kavin 和 Joe 都渐行渐远，到底是我停留在原地还是我超越了时间？
不知道该怎么样描述这种感觉，或许我就是一个不擅长联络感情的人，生命中有太多太多东西，我眼睁睁看它离我远去而又无可奈何，想要安慰我的人总是劝我同昨天告别。但像我这样太看重感情的人，无论外表多么风平浪静，内心永远不肯残忍地删除回忆。所以，我记得 Jackson、Lynn、Candy 他们陪我度过的二十五岁生日，我记得 Candy 问我当时暗自许了什么样的愿望，坦白来讲，我没有想着脱单这样离我很遥远的愿望，我只想时间能够永远定格在那一刻，大家都可以开开心心地直到永远。你一定觉得我幼稚或者是不成熟啦，我问过人家要怎么样变得成熟，人家说你去找一个女朋友就好啦，然后就会在喜欢的人面前紧张甚至自卑，我曾一度很讨厌下雨天，因为我怕两个人遇到一起，我既没有伞亦没有外套。
二十五岁的我，喜欢一个人还是和从前一样无所顾忌，我还是学不会那些复杂的套路，不喜欢单方面付出，不喜欢卑微地爱一个人，每一次都会因为喜欢某个女孩子而尝试改变，想和她站在一起的时候不会被她的光芒完全覆盖，想和她待在一块的时候不让她觉得我这个人枯燥，想和她抱在一起的时候给她讲我从书里看到的某个故事&amp;hellip;&amp;hellip;我一直在想，如果我们的感情不是以异地恋这种方式会不会有不一样的结局，我喜欢《星月神话》这首歌，是因为我们的确呼吸着同一片天空的气息而注定无法再相遇，就像两条相交的直线一样从陌生到熟悉再到陌生。我现在再看 《嫌疑人X的献身》 这部电影，我总在想，如果那天我们看的是这部电影会怎么样，此时的我比上大学时候胖了许多，大概一开始我在她心目中的样子，应该是张鲁一这样温润如玉的谦谦君子吧！生命就是这般离奇玄妙，你不能假设更无从假设应该发生什么，因为每一天都是无法重现的 Case，你觉得它相似，仅仅是因为相似而已。
我喜欢穿裙子的女孩子，这一点完全是因为受到她的影响，虽然她一再告诉我，是我喜欢这样的女孩子，而她恰好喜欢穿裙子而已，可这些淹没在风声里的话语，谁会去盘问孰是孰非呢，如果她此刻愿意同我争论这个问题，我直接认输就好啦，我对输赢看得并不重要，这就像在工作中，没有人在意做的产品是不是好用，大家关注的是始终是它能节省多少个 FTE，所以为了达到这些光鲜亮丽的指标，没有人会在意工程师的代码被改成什么样子，我们所追求的东西是否显得舍本逐末，我们所在意的东西到底是否真正发自你我的本心。以前觉得两个人在一起简单，是因为我们没有想那么多；现在觉得两个人在一起困难，是因为我们习惯性想太多。你有没有在脑海中设想过，和一个人走完一生是种什么样的体验，我想说那是一个很美好的想象。</description></item><item><title>从 React 专利事件看开源软件许可</title><link>https://blog.yuanpei.me/posts/1166840790/</link><pubDate>Wed, 20 Sep 2017 23:06:45 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1166840790/</guid><description>各位朋友，我是 Payne，大家好，欢迎大家关注我的博客，我的博客地址是https://qinyuanpei.github.io。最近前端技术圈因为 React 专利事件再次被大家关注，印象中 Angular 和 Vue 的纷争刚刚过去不久，果然前端技术圈对&amp;quot;造轮子&amp;quot;和&amp;quot;搞事情&amp;quot;有着近乎执著的追求。作为一个在知乎吃瓜的伪前端工程师，我对这凑热闹这种事情从来都是是颇为喜欢的。如果说 Angular 和 Vue 冲突主要来自大漠穷秋和尤小尤的个人战场，那么这次 React 专利事件则是商业公司之间对社区主导力量的一次争夺和抗衡。开源是一种近似乌托邦般的理想社会，它倡导的&amp;quot;人人为我，我为人人&amp;quot;这种近乎大同社会的观念，在面临商业化浪潮洗礼的时候难会和商业利益发生冲突，譬如 Google 因为使用 Java 而和甲骨文纠纷不断，最终不得不选择 Kotlin 作为 Android 开发的主力语言。所以这篇文章我想和大家通过 React 专利事件来聊聊开源软件许可，以及我们如何在商业化和开源社区间找到一个平衡点。
事件始末 其实 React 专利事件由来已久，如果不是在知乎上看到&amp;ldquo;百度要求内部全面停止使用 React/React Native&amp;rdquo;的问题，我是完全没有意识到事态居然发展到如此严重的。每次前端技术圈&amp;quot;搞事情&amp;quot;的时候，基本上都会在我的知乎首页刷屏，可是对我这样的伪前端工程师而言，我仅仅是关注了&amp;quot;Web 开发&amp;quot;这个话题而已。忽略知乎首页推荐算法的缺陷，这的确动侧面说明了目前前端领域非常热门的事实，可它不能说明某些前端工程师的技术水平有多高，在引入前后端分离和前端构建工具以后，前端开发的基础设施渐渐地丰富起来了，可是前端开发目前经历着的一切，无一不在后端开发中涉及到，我没有想要成为全栈工程师的野心，在讨论这个事件以前我认为有必要了解下整个事件的始末：
2016 年 7 月，Facebook 在 React.js 的开源许可协议中添加的附加专利条款首次在社区中引发广泛讨论。 2016 年 11 月，Facebook 发布官方问答，对附加专利条款进行了澄清，强化了其 BSD 许可证 + 专利许可证的概念。 2017 年 4 月，Apache Cassandra 项目正在考虑是哟过 Facebook 开源的数据库 RocksDB 作为存储引擎，可是考虑到专利授权的问题，Jeff Jirsa 向 Apache 法律社区寻求帮助。 2017 年 6 月，Apache 法律社区开始讨论 Facebook Patents License 协议专利授权的不对称问题，且该协议与 Apache Software License，即 Apache 2.</description></item><item><title>Redis 缓存技术学习系列之 Lua 脚本</title><link>https://blog.yuanpei.me/posts/4197961431/</link><pubDate>Sun, 17 Sep 2017 10:49:07 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4197961431/</guid><description>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是https://qinyuanpei.github.io。想起来大概有一个月没有更新博客啦。或许是因为这中间发生了太多的事情，想来人生原本就充满曲折和变数。在微信群里得知家中舅爷去世的消息，突然意识到时间早已摧毁你我的一切。那个曾经同你有千丝万缕联系的人，会在某一刻同你彻底失去联系。所以我更珍视彼此在一起的时光，因为在这个世界上每天都面临着改变。有时候工作上遇到不开心的时候，会想着一个人去一个陌生的地方，我们就在不断地相聚和离别中慢慢老去。这段时间一直在学习做饭，为此特意买了本菜谱，结果发现，最难的并不是如何去做好一道菜，而是你为了做好一道菜需要准备各种食材，就像人与人交流并没有什么困难，真正困难的地方，是你找不到一个可以一直陪你说话的人。熟悉的店面会被拆迁转让，熟悉的人事会被错过改变，上帝想把世界煮成一锅粥，可味道的调配却由我们来掌控。
好了，所谓“如人饮水，冷暖自知”，人生奇就奇在你没有办法用三言两语去描述它。这段时间面试过两三家公司，整体上感觉自己的生活太安逸了些，虽然我现在依然住在租来的房子里，转眼间 2017 年接近尾声啦，可是回想起来今年年初制定的计划，在广泛阅读和提升技术上都是不及格的状态，印象中打算研究 Redis 和 MonogoDB 这两种数据库的(因为没有购买为知笔记会员导致部分笔记损坏或者丢失)，然而到现在为止我还有研究完 Redis。尤其当我面试的时候，我发现好多我写在简历上的内容，都会成为某种意义上的呈堂证供，这让我更加确信好多东西需要不断地去巩固，所以尝试在实际项目上使用 Moq、考虑怎么写出更好的测试方法以及时刻保持自我的不可替代性，这些都是我最近在考虑的事情，有时候发脾气是因为觉得自己在浪费生命，可越是被这种无力感笼罩的时候，就越是要对自己狠一点儿，所以在这篇博客中，让我们重新拾起对 Redis 的学习兴趣，今天我们来说说 Redis 中的 Lua 脚本。
熟悉我博客的朋友一定都知道，我曾经开发过 Unity3D 相关的项目，而 Lua 脚本正是 Unity3D 中主流的热更新方案。关于 Lua 脚本相关的文章，大家可以通过下面的链接来了解，在这里我们不再讲述 Lua 的基础内容，本篇文章所讲述的是如何通过 Redis 内置的 Lua 解释器来执行脚本，我们为什么使用脚本语言进行开发呢，因为这样可以降低开发的难度啊。
脚本语言编程：Lua 脚本编程入门 在 Windows 下使用 Visual Studio 编译 Lua5.3 Unity3D 游戏开发之 Lua 与游戏的不解之缘(上) Unity3D 游戏开发之 Lua 与游戏的不解之缘(中) Unity3D 游戏开发之 Lua 与游戏的不解之缘(下) Unity3D 游戏开发之 Lua 与游戏的不解之缘终结篇：UniLua 热更新完全解读 好了，既然我们已然了解到 Redis 是通过内置的 Lua 解释器来执行脚本，所以 Redis 中的 Lua 脚本其实可以理解为 Lua 语法 + Redis API。为了写作这篇文章，我不得不将我的操作系统切换到 Linux，因为这样我可以随时在写作过程中使用终端，我写作的一个重要特点，就是所有的内容都尽量保证有测试覆盖，我知道有许多人都不喜欢写测试，测试虽然不能保证你没有 BUG，可是有了 BUG 以后可以直接在测试中定位问题，这就是我们为什么要重视测试的原因所在。在 Redis 中我们有两类命令用以处理和脚本相关的事情：</description></item><item><title>基于特性(Attribute)的实体属性验证方案设计</title><link>https://blog.yuanpei.me/posts/3873710624/</link><pubDate>Mon, 21 Aug 2017 14:25:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3873710624/</guid><description>各位朋友，我是Payne，大家好，欢迎大家关注我的博客，我的博客地址是https://qinyuanpei.github.io。在这篇文章中，我想和大家探讨下数据校验的相关问题，为什么我会对这个问题感兴趣呢？这其实是来自最近工作中相关需求场景，而这篇文章其实是我在去年就准备要写的一篇文章，这篇文章一直存放在草稿箱里没有发布出来，所以结合这段时间项目上的思考，对当初的设计方案进行了改进，所有就有了大家现在看到的这篇文章，我始终认为思考是一个持久的过程，就像我们对这个世界的理解，是会随着阅历的变化而变化的。我们知道现实通常都会很残酷，不会给我们太充裕的时间去重构。可是思考会是人生永远的功课，当你忙碌到无暇顾影自怜的时候，不妨尝试慢下来抬头看看前方的路，或许原本就是我们选择了错误的方向呢，因为有时候作出一个正确的选择，实在是要比埋头苦干要重要得多啊。
好啦，既然我们提到了思考，那么我们来一起看一个实际项目中的业务场景，在某自动化项目中，用户会将大量数据以某种方式组织起来，然后藉由自动化工具将这些数据批量上传到一个系统中，该系统实际上是一个由各种表单组成的Web页面，并且这些Web表单中的控件都有着严格的验证规则，当数据无法满足这些验证规则时将无法上传，因此为了提高自动化工具上传的成功率，我们必须保证用户组织的这些数据是合法的，假设我们的用户是一个仅仅会使用Office三件套的普通人，他们可以想到的最好的方式是将这些数据录入到Excel中，而Excel中的数据有效性验证依附在单元格上，一旦验证规则发生变化，我们就不得不去维护这个Excel文件，这绝对不是一个软件工程师该做的事情好吗？我们当然是需要在提交数据前做验证啦，然而我看到Excel中100多列的字段时，我瞬间就不淡定了，这么多的字段难道我们要逐个写if-else吗？不，作为一个提倡少写if-else的程序员，我怎么可能会去做这种无聊的事情呢？下面隆重推出本文的主角——Attribute。
你的名字是？ 如你所见，本文的主角是Attribute，那么当它出现在你面前的时候，你是否会像《你的名字。》里的泷和三叶一样，互相问候对方一句：你的名字是？因为我们实在不知道应该叫它特性还是属性。可事实上这篇文章的标题暴露了这个问题的答案，这里我们应该叫它特性。好了，按照数学理论中的观点，任何问题都可以通过引入一个中间层来解决，现在我们有了一个新的问题，Attribute和Property到底有什么区别？虽然这两者都可以翻译为&amp;quot;属性&amp;quot;，可实际上它们表达的是两个不同层面上的概念，一般我们倾向于将Attribute理解为编程语言文法上的概念，而将Property理解为面向对象编程里的概念。
Attribute/特性 我们将Attribute称为特性，那么我们在什么地方会用到特性呢？两个个非常典型的例子是超文本标记语言(HTML)和可扩展标记语言(XML)。首先这两种标记语言都是结构化、描述性的标记语言。结构化表现在节点间可通过父子或者兄弟的关系来表示结构，描述性表现在每个节点都可以附加不同的描述来丰富节点。例如下面的XML文件中，我们使用了描述性的特性来提高元素间的辨识度，即特性为元素定义了更多的额外信息，而这些额外信息并不作为元素数据结构的一部分：
&amp;lt;bookstore&amp;gt; &amp;lt;book category=&amp;#34;COOKING&amp;#34;&amp;gt; &amp;lt;title lang=&amp;#34;en&amp;#34;&amp;gt;Everyday Italian&amp;lt;/title&amp;gt; &amp;lt;author&amp;gt;Giada De Laurentiis&amp;lt;/author&amp;gt; &amp;lt;year&amp;gt;2005&amp;lt;/year&amp;gt; &amp;lt;price&amp;gt;30.00&amp;lt;/price&amp;gt; &amp;lt;/book&amp;gt; &amp;lt;book category=&amp;#34;CHILDREN&amp;#34;&amp;gt; &amp;lt;title lang=&amp;#34;en&amp;#34;&amp;gt;Harry Potter&amp;lt;/title&amp;gt; &amp;lt;author&amp;gt;J K. Rowling&amp;lt;/author&amp;gt; &amp;lt;year&amp;gt;2005&amp;lt;/year&amp;gt; &amp;lt;price&amp;gt;29.99&amp;lt;/price&amp;gt; &amp;lt;/book&amp;gt; &amp;lt;/bookstore&amp;gt; 在这个例子中，bookstore节点由两个book节点组成，而每个book节点则由title、author、year和price四个节点组成，显然这些节点描述的是一种结构化的数据，而这些数据同时附加了相关描述性的信息，例如book节点有category信息，title节点有lang信息。在XML中最基本的一个内容单元我们称之为元素，即Element，而描述这些元素的最基本内容单元我们称之为特性。所以，这种在语言层面上进行描述而与实际抽象出的对象无关的概念就称为&amp;quot;特性”，人们认知和描述一个事物的方式会有所不同，所以在XML中会有这样一个历史遗留问题，我们应该使用Element还是Attribute，而产生这个问题的根源在于我们认识这个世界，是通过语言描述还是通过概念抽象。
如果我们了解GUI相关技术的演进过程，就会发现历史总是如此的相似。为什么微软会在XML的基础上扩展出XAML这种专门为WPF而设计的界面设计语言呢？因为历史告诉我们GUI中的大量特性都应该使用声明式的、描述式的语法来实现，从苹果的Cocoa、微软的XAML、Qt的QML、Android的XML等无一不证明了这个观点，而采用过程式的MFC、WinForm、Swing等，我们常常需要为它们编写大量的交互性的逻辑代码，今天我们会发现前端领域的声明式编程、MVVM、组件化等技术点，其实都是这种思想的无限延伸，我们可以使用jQuery去直接操作DOM，但面向过程的命令式代码一定不如声明式容易理解。虽然在面向对象编程的世界里，我们最终还是需要将这些描述性的语法结构，转化为面向对象里的类和属性，可这已然是一种进步了不是吗？
Property/属性 我们认识这个世界的过程，恰恰折射出这两者截然不同的风格，从孩提时代理解的“天空是蓝色的”到学生时代认识到“大气是由氮气、氧气和稀有气体组成”，这种转变从本质上来看其实是因为我们认识世界的角度发生了变化。《西游降魔篇》里玄奘寻找五行山，第一次是风尘仆仆“看山是山”，第二次是由“镜花水月”启发“看山不是山”，第三次借“儿歌三百首”降伏孙悟空后“看山还是山”。面向对象编程(OOP)的一个重要思想是抽象，而抽象即是我们从描述性的语言中对事物属性进行构建的一个过程。例如现实生活中的汽车会有各种各样的数据信息：长度、宽度、高度、重量、速度等等，而与此同时汽车会有启动、刹车、减速、加速等等的行为，所以将事物的“数据”和“行为”提取出来进行抽象和模拟的过程，就是面向对象编程，我们在这个过程中可以注意到一点，所有的这一切都是针对对象而言的，所以Property是针对对象而言的。
这里提到的一个重要概念是抽象，什么是抽象呢？我认为它恰好和具体相对的一个概念。所谓具体，即相由心生，你看到什么就是什么，与此同时通过一组描述性的语言将其描述出来，我以为这就是具体。例如&amp;quot;火辣辣的太阳挂在天上&amp;quot;，这是具体到太阳颜色和温度的一种描述；所谓抽象，即返璞归真，我们看到的并非世间阴晴圆缺的月亮，而是这浩瀚宇宙中国一颗遥远的行星，此时此刻我们将行星具备的特点概括出来，推而光之，我以为这就是抽象，所以对我们而言，属性是事物抽象后普遍具有的一种特征，它首先要达到一种抽象的层次，其次它要能表现出事物的特性，我更喜欢将Property称之为属性，它和我们在面向对象编程中的概念是完全统一的。
方案设计及其实现 设计目标 免除配置开箱即用：无需任何配置文件，直接在实体上添加Attribute即可实现验证 非侵入式验证设计：验证与否对实体结构无任何副作用，可以随时添加验证或卸载验证 扩展灵活高度复用：可以自由派生自定义特性，通过泛型来支持不同实体类型的验证 设计思路 所有校验相关的Attribute都派生自ValidationAttribute这个父类，其核心方法是Validate()方法，该方法被声明为一个虚方法，因此所有的子类都必须对这个方法进行重写，它将返回一个叫做ValidationResult的结构，这是一个非常简单的数据结构，它仅仅包含Success和Message两个属性，前者表示当前校验是否成功，后者表示验证失败时的错误信息。显然，一个实体结构中将包含若干个不同的属性，所以在对一个实体结构进行验证的时候，会通过反射遍历每一个属性上的ValidationAttribute并调用其Validate()方法，所以最终返回给调用者的应该是由一组ValidationResult组成的集合，为此我们设计了ValidationResultCollection这个类，该类实现了ICollection接口，在此基础上我们增加了一个Success属性，当集合中所有ValidationResult的Success属性为true时，该属性为true反之为false。我们将数据校验的入口类EntityValidation设计成了一个静态类，它提供了一个泛型方法Validate()方法，所以对整体设计而言，它的灵活性和扩展性主要体现在：(1)通过派生自定义特性来增加验证规则；(2)通过泛型方法来支持不同类型的校验。下面给出UML类图供大家参考，最近刚刚开始学习UML，有不足之处请大家轻喷哈：
UML类图 技术要点 首先，在.NET中特性的基类是Attribute，Attribute从表现形式上来讲类似Java中的注解，可以像标签一样添加在类、属性、字段和方法上，并在运行时期间产生各种不同的效果。例如[Serializable]标签表示一个实体类可以序列化，[NonSerializable]标签则可以指定某些属性或者字段在序列化的时候被忽略。而从本质上来讲，Attribute是一个类，通常我们会将派生类以Attribute结尾，而在具体使用的时候可以省略Attribute，所以[Serializable]标签其实是对应.NET中定义的SerializableAttribute这个类。在我们定义Attribute的时候，一个需要考虑的问题是Attribute的作用范围，在.NET中定义了AttributeUsageAttribute这个类，它可以是Class、Property、Field、Method等，所以Attribute本质上是在运行时期间为元素提供附加信息的一种机制，即Attribute可以添加元数据。我们知道元数据是(MetaData)实际上是程序集(Assembly)中的一部分，显然这一切都是在编译时期间定义好的，所以Attribute的一个重要特征是在运行时期间只读(Readonly)。Attribute必须依附在指定目标上，当当前目标与AttributeUsage定义不符时，将无法通过编译。Attribute的实例化依赖于目标实例的实例化，无法直接通过new完成实例化。通常我们需要配合反射来使用Attribute，在运行时期间做些有意义的事情，例如ORM中实体字段与数据库字段的绑定、Unity中配合AOP使用的ExceptionHnadler等等，都是非常典型的Attribute的应用。
了解了Attribute是什么东西，接下来我们要考虑的就是如何访问Attribute，在.NET中主要有两种方式来获取Attribute，即通过Attribute类提供的静态方法获取Attribute和通过Attribute依附的对象实例的元数据来获取Attribute。下面我们来看一段简单的代码实例：
public static T GetAttribute&amp;lt;T&amp;gt;(this PropertyInfo propertyInfo) { var attrs = propertyInfo.GetCustomAttributes(typeof(T), false); if(attrs == null || attrs.Length&amp;lt;=0) return null; return atts[0] as T; } 这段代码展示了如何通过反射访问附加在属性上的Attribute，事实上除了PropertyInfo以外，它还可以从任何支持附加Attribute的元素，例如MethodInfo、FieldInfo、ConstructorInfo等。Attribute类提供了类似的静态方法，第一个参数可以是这些元素中的任何一个，第二个参数和第三个参数和这里的示例代码一致，分别是返回的Attribute的类型，以及是否要搜索父类的Attribute，它的返回值类型为Attribute[]。在这个方案中，我们通过下面的方式来对实体属性进行验证：</description></item><item><title>《大护法》：花生镇里的成人童话</title><link>https://blog.yuanpei.me/posts/1684318907/</link><pubDate>Sun, 30 Jul 2017 20:38:22 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1684318907/</guid><description>猛然间驻足回首这些错落的旧时光，我渐渐意识到我已经有三个月没有写博客了。如果一定要我说出这是种什么样的感觉，大概就是你永远都不会知道永远到底有多远。或许你会喜欢上一个陌生的人，源自不经意间的惊鸿一瞥；或许你会开始厌倦一个熟悉的人，源自不经意间的怅然若失。时间如风起云涌，一边熟悉着一边陌生着，永远像极了一场你追我赶的拉力赛。从办公室里走出来被热风吹袭的一瞬间，我居然有种久违的暖人肺腑的感觉。每个人都像一粒炭火，都知道要通过抱团来取暖，可是有谁会愿意燃烧自己呢？所以孤独是人类如宿命一般的社会属性。我一位朋友曾向我讲述过，这种若即若离的感觉，而此时此刻，我想将这种感觉结合一部电影来说出来。
这段时间好像看了挺多电影的，借我一位朋友的话说就是，“两只单身狗跑到电影院里去找刺激”。而对于《大护法》这部电影，我是选择了一个人去看的，因为我觉得这部电影的主题是“反乌托邦式”的，所以我宁愿自己独自去消化这些内容，而不是将消极悲观的情绪在观影时传播给别人。对这部电影我将其看作是一个成人童话，因为它的确不适合带小孩子去观看，而这部电影恰恰采用了PG-13的影片分级。我喜欢这个电影，某种意义是因为它在现有体制内，讲述一群被奴役的“花生人”，如何在外人的帮助下，从愚昧麻木转变为意识清醒，并最终产生自我意识推翻统治者暴政的故事，所以这其实是一个关于觉醒和反抗的故事。
而这基本上是人类历史里永恒的话题啦，熟悉苹果公司历史的朋友一定知道，乔布斯当年曾经拍摄过一部名为《1984》的广告片，这部广告片取材自乔治.奥威尔的同名小说，该书中刻画了一个令人窒息的恐怖世界，在假想的 未来社会中，独裁者以追求权利为最终目标，自由被彻底剥夺，思想被严酷控制，人民被迫屈从于“老大哥”的统治。而将书中这个背景对应到苹果公司，我们就部难理解乔布斯是在用“老大哥”来影射当时的IBM公司，在这则广告片中“老大哥”被铁锤击碎后缓缓消失，此时旁白平静地念道：“1月24日，苹果电脑公司将推出麦金塔电脑，你将明白为什么1984不会变成《1984》”，这段传奇故事在《硅谷传奇》和《乔布斯》两部电影中均有反映，对此感兴趣的朋友可以自己去了解，显然乔布斯在当时试图向世界证明，苹果公司是唯一一家有希望打败IBM的公司。
因此在很长一段时间里，我一直有想要通读《1984》的愿望，这种行为在某些人看来是矫情和装逼，可事实上连周星驰都表示没有读完《演员的自我修养》这本书，我就不明白这个想法为什么会遭人厌恶。这个世界上最令人厌恶的事情就是，我们所有人都生活在一个被道德和法律约束的世界，我们从出生就在适应接受某一种意识形态或者社会法则，可总有人试图告诉你生活是这样或者那样，并且出自尊重你必须接受和感谢这种建议，因为这些人最后会说我是为了你好。那么在导演不思凡的视角里，这种回归哲学意义上的最为追根溯源的问题，即我是谁，是如何通过电影表现出来的呢？欧阳吉安，即花生镇村名眼中的老神仙，他说鬼蘑菇是一种可怕的传染病，一旦花生人长了鬼蘑菇就必须被立即处死，花生人不能开口说话，即使他们都贴着假眼睛和假嘴巴，花生人不能拥有意识和思考，一旦说出事实就会被认为染了疯病必须被立即处死，所有的村民都循规蹈矩地听着老神仙的话，可事实上鬼蘑菇根本就不是传染病，它是花生人成熟的标志，老神仙这样一个统治者，从来不会将花生人视为人，它们活着的唯一意义就是等死后，由庖卯从脑袋里取出黑石头。古话说：流言止于智者，可在这个荒诞诡异的世界里，流言会因为恐惧而掩盖真相，村民始终生活在一种令人窒息的恐怖阴影里。
故事开篇即点明主旨，即奕卫国大护法，即故事主角“红冬瓜”为寻找太子下落，而来到了充斥着腐烂气息的花生镇，虽然主角对这些像人而又不像人的“花生人”表示了反感，因为在大护法沿着山路来到小镇的时候，经过了一个类似拱门的建筑，可细思恐极的是这个拱顶堆满了花生人的头颅，而且头颅上的眼睛是真正的眼睛，而散落在地面上的贴纸其实是假的眼睛，联想整个故事情节，在花生镇敢于揭露真相、寻找真相的人都被杀鸡儆猴地处理掉了，这可以说是故事开篇埋下地一个伏笔了，可是为了寻找太子的下落，大护法不得不去向这些村民打探消息。可是大护法很快就发现，在花生镇这样一个奇怪的地方，随时随地都会有人杀戮村民和外来者，这些人被称为刑法者，负责帮助老神仙欧阳吉安杀死“该死”地村民，所以在故事一开始大护法在村子里就遭到了袭击，可是说起大护法来，这是一个战斗力爆表的反差萌系设定，而通过故事我们知道，这些刑法者由一个称为罗单的人管理，他不属于花生镇，和欧阳吉安这些人类不一样，他对彩这个神秘女子有种强烈的占有欲，他偷看她洗澡被发现便转身离开，可当他发现下属产生感情的时候，他毫不留情地杀死了他们，所以大概到现在为止，我们所认识的世界存在着严格的等级区分，整个故事从此定性，罗单压抑着自己的情欲，却不允许下属产生情欲，所以当他杀死欧阳吉安的时候，我们不会感到太意外，因为他心里隐藏了太多东西。
太子是整个故事里，唯一一个清楚知道自己想要什么的人。他不喜欢朝廷里的纷争，便遁走江湖寄情山水，去寻找自己真正喜欢的事情。从来没有人将花生人当作人，他却视小姜为花生镇里最好的朋友。可我们都知道这样一句话：哪有什么岁月静好，不过是有人负重前行。在整个故事设定中，大护法的爷爷的爷爷起就一直是弈卫国的大护法，所以大护法的职责就是要保护太子，在故事安排上这部电影相对枯燥，因为后期基本上一直在找太子，所以太子有这样的机会，去选择做自己想做的事情，其实是因为有大护法在一直保护他，相反普通人可能不会有这样的机会，这一点我们稍后会提到。太子除了承担整部电影的笑点以外，我个人认为最出彩的地方是，他在被庖卯打得头破血流时，亲眼目睹了小姜的死，从那一刻开始，我相信他终于明白了身为帝王的那种担当，他不再是以前那个避世逃脱的太子，所以这种成长的感觉会非常好，他在看到大护法以后重复了两次“杀了他”，而在此之前他是坚决反对杀人的。
小姜是唯一一个自我意识觉醒的花生人，他通过隐婆了解到自己是怎么来到这个世界上的，了解到毒蘑菇到底是怎么一回事情，了解到花生人来自蚁猴子却又以蚁猴子为食的真相，这里有一个有趣的设定，花生人是以蚁猴子作为食物的，这就好像喂猪的泔水里会有猪肉一样，想通了这一点，或许人吃猪肉和人吃人并没有本质的区别。小姜会说话这件事情，让欧阳吉安和疱卯都感到异样，前者是担心危及到自己的统治，后者是对自己的职业产生了怀疑。小姜最终还是死了，就像被庖卯杀死的那些花生人一样，不同的是那颗石头不再是黑石头，而是晶莹剔透的宝石。这让我想起蚌这种可以孕育出珍珠的海洋生物，普通的砂砾经过时间的磨洗可以变成美丽的珍珠。或许答案会是什么，更多的是因为你想要什么，内心贫瘠的土壤寸草不生，内心肥沃的土壤鲜花遍地。小姜内心善良所以懂得回报太子，但现在这个世界善良越来越被人忽视，小姜被老神仙视作圈养的猪啰，可太子会把他当作好朋友，所以说选择非常重要啦，遵从内心的选择更重要。
庖卯这个角色其实挺悲哀的，他代表是那一类被理想绑架而失去自我的人。“庖”在古代就是指厨师这一类职业，我们熟悉的庖丁解牛这个词就是出自这里。可在影片中颇为讽刺的是，一个想成为厨师的人的最大理想，居然是想要一刀取人心脏，我们不能说这种想法不是一份理想，用大护法的话说就是“你的理想，杀气这么重，怕是实现得一天，会是你的终年”。我们注意到卯和丁一字之差，所谓“丁是丁，卯是卯”，当你被仇恨蒙蔽双眼的时候，看到的东西和实际相比大概会相差很多吧！庖卯在听见花生人说话以后就开始呕吐，这种感觉让他开始思考，自己每天屠杀的到底是些什么东西。试想我们每天吃的这些食物开口说话的话，我们同样会感到恐惧的吧，或许我们想和疱卯一样成为一名绝世大厨，但现实给我们的却是一份杀人的差事，我想坚持初心，知道自己从哪里来要到哪里去，就不会在路上迷失方向，他的死作为一种解脱，在这个故事里算是善终了吧。
说了这么多，始终没有提到我们的大护法，个人感觉这个角色给我的印象确实不够深刻，它是一个古龙式的侠客形象，拥有和外表极不相称的武力值，这是一个用生命在战斗的人，从故事开头一直战斗到故事结尾，主角光环让它在断了数根肋骨后依然可以活到最后一刻，它是什么样子的呢？一个喜欢朗诵诗歌、自带莎士比亚腔调的文艺大护法，从台词的角度来讲，整部电影深刻中带着些许中二，但这部电影吸引的我，恰恰是这些硬伤很突出的地方，它整体的画风给人一种怪诞和虚无，可它的故事在国内审查体制下让人耳目一新，电影里说“假眼睛、假嘴巴都说贴着难受为什么还要贴？都摘掉它们会怎么笑话我？就因为怕被笑话，所以我们活成了笑话”，或许这是一部给成人看的童话，但透过这部电影多一点思考、多一点想象，大概是我们回报导演不思凡的最好方式之一，因为他想说的或许就是我们想说的。</description></item><item><title>基于过滤器实现异常处理的探索</title><link>https://blog.yuanpei.me/posts/570888918/</link><pubDate>Sat, 20 May 2017 20:10:28 +0000</pubDate><guid>https://blog.yuanpei.me/posts/570888918/</guid><description>正如你所看到的那样，今天我想和大家聊聊异常处理这个话题。对于异常处理这个话题，我相信大家都有各自的方法论。而我今天想和大家探讨的这种异常处理方案，我将其称之为基于过滤器的异常处理。我不知道这种定义是否准确，我们的项目上在要引入 AOP 的概念以后，我们对异常处理的关注点就从try-catch转向Interceptor。虽然首席架构极力推荐，使用 Unity 框架来拦截代码中的各种异常，可从我最初纠结于&amp;quot;return&amp;quot;和&amp;quot;throw&amp;quot;的取舍，到现在我可以灵活地使用和捕捉自定义异常，对我而言老老实实地实践异常处理的经典做法，比使用 AOP 这样一种高大上的概念要有意义地多，因为我相信在某些情况下，我们并不是真正地了解了异常处理。
异常和错误 或许是因为人类对机器时代充满了近乎苛刻的憧憬，我们的计算机程序在开始设计的时候，就被告知不允许出现错误，甚至我们的教科书上会用一种充满传奇色彩的口吻，来讲述一个因为粗心的工程师计算错了小数点而导致航天飞行器机毁人亡的故事。可是人类常常会对自己选择宽容，而对他人则选择严格，这种观点在整个数字时代更为凸显，当我们无法容忍一个糟糕的应用程序的时候，无论曾经人们为此付出过多少努力，在这一瞬间他们的价值都将不复存在。我们的这种苛刻迫使我们不允许软件出现错误，我们尝试通过各种各样的测试来避免错误发生，可是事实上软件工程实践最终会演变为一个妥协的产物，这意味着我们任何的形式化方法最终都会失败，没有人可以保证一生都不会犯错，而软件工程师同样是人，为什么我们一定要求他们不可以犯错呢？
我们不得不承认软件产品是一个持续演进的过程，如果抛开商业意义上的Deadline来说，实际上软件是永远没有写完的那一天的，这就是为什么工程师都有点理想主义的原因，不考虑外界环境因素的变化，而期待软件永远不会有新的问题产生，这实在是一种苛刻地要求。好了，我们在这里频繁地提到错误，那么在软件工程学意义上的异常和错误分别是指什么呢？具体来讲，异常是指我们可以明确预测到它会发生并且需要我们进一步处理的流程，而错误是指我们无法明确预测到它会发生并且它会程序流程中断而导致程序崩溃，所以我认为区分&amp;quot;异常&amp;quot;和&amp;quot;错误&amp;quot;最直观、最简单粗暴的方法就是，如果你捕捉到了一个异常并处理了这个异常，那么它就是异常。反之，如果任由异常导致程序 Crash，那么它就是错误。如果我们因为畏惧异常而给所有方法增加 try-catch，我不得不遗憾得告诉你，你还没有真正明白什么是异常。
在早期的 Win32 API 中，微软大量使用了错误码来表示方法执行过程中发生的错误，这样就引出异常处理中的第一个问题，我们到底是应该是使用错误码还是异常来表示方法执行中发生的错误？事实上这两者在程序的表达能力上等价的，它们都可以向调用者传达&amp;quot;异常发生“这个事件，譬如我们在集合中查找一个元素，如果元素不存在则返回-1，这其实就是一个使用错误码来表示&amp;quot;错误“的经典案例，显然这种从 C/C++时代遗留下来的传统解释了 Win32 API 为什么会选择这样的设计方式，换言之，选择哪种方式，本质上是一种从 API 风格、代码风格和性能指标等方面综合考虑后的结果，错误码这种方式的缺陷主要在于，错误码不能明确地告诉调用者到底发生了什么错误，除非我们定义更多的错误代码，而且在没有引入可空类型以前，我们没有办法避免错误码污染返回值的值域，比如在这个例子，如果集合中恰好有一个元素-1，那么通过-1 这个返回值我们是没有办法判断出，这个-1 到底是不是因为方法内部发生了错误而返回-1.
好了，现在我们来说说异常，异常在主流的编程语言里基本上是一个标配。异常可以保存从异常抛出点到异常捕获点间的相关信息，所以异常相比错误码可以持有更多的信息，或许你可以尝试去设计一种数据结构来让返回值更丰富:)。我们常常听到&amp;quot;使用异常会降低程序性能&amp;quot;这样的说法，可这部分性能上的差异仅仅是因为，我们需要在抛出异常的时候给调用者更多的信息，所以这是一个非常公平的事情。第二个问题，我们是不是在所有情况下都使用异常？使用异常的好处是它可以让我们以一种更安全的方式去处理异常，可一旦发生了异常程序的性能就会降低，所以我们可以看到.NET 中提供 TryParse 这样的方法，这其实是在告诉我们：如果预测到异常一定会发生，正确的策略不是去捕捉它而是去回避它。在《编写高质量的 C#代码》一书中曾建议：不要在 foreach 内部使用 try-catch，就是这个道理，即采用防御式编程的策略来回避异常，而不是总是抛出异常。
那么，总结下行文至此的观点：异常是强类型的，类型安全的分支处理技术，而错误码是弱类型的，类型不安全的分支处理技术。元组等可以让函数返回多个返回值的技术，从理论层面上可以模拟异常，即将更多的细节信息返回给调用者，可是这种方式相比由运行时提供支持的异常机制，在性能指标和堆栈调用上都存在缺陷。异常在被运行时抛出来的时候，程序性能是下降的，这是因为调用者需要更多的细节信息，所以不建议在所有场合都抛出异常，建议使用防御式编程的策略去回避异常，直到确定程序没有办法处理下去的时候再抛出异常。理论上所有自定义的异常都应该去捕捉并处理，否则定义这些自定义异常是没有意义的。异常处理应该拥有统一的入口，在代码中到处 try-catch 和记日志是种非常丑陋的做法，理论上应该坚决摒弃。
Checked Exception 最近垠神写了一篇新的文章《Kotlin 和 Checked Exception》，在这篇文章中垠神提到了 Checked Exception 这种针对异常处理的设计，而恰好我这篇文章写的同样是异常处理，并且我在下面提到的基于过滤器的异常处理方案，实际上就是为了解决这种 Checked Exception 的问题，虽然在.NET 中不存在 Checked Exception。
要了解什么是 Checked Exception，要从 Java 中的异常机制说起。Java 中的异常类全部继承自 Throwable，它有两个直接子类 Error 和 Exception，通常情况下 Error 是指 Java 虚拟机中发生错误，所以 Error 不需要捕捉或者抛出，因为对此表示无能为力；而 Exception 则是指代码逻辑中发生错误，这类错误需要调用者去捕捉和处理。那么在这样的分类下，Java 中的异常可以分为 Checked Exception(受检查的异常)和 Unchecked Exception(未受检查的异常)，前者需要需要方法强制实现 throws 声明或者是使用 try-catch，如果不这样做编辑器就会直接报错，后者就相对宽容啦，没有这样霸道的条款，可是诡异的是 RuntimeException 是一个 UncheckedException，可它居然是继承自 Exception 而不是 Error，这实在令人费解，Java 的设计模式果然博大精深。</description></item><item><title>异步 Lambda 表达式问题的探索</title><link>https://blog.yuanpei.me/posts/187480982/</link><pubDate>Sat, 15 Apr 2017 21:10:47 +0000</pubDate><guid>https://blog.yuanpei.me/posts/187480982/</guid><description>各位朋友，大家好，欢迎大家关注我的博客，我是 Payne，我的博客地址是:http://qinyuanpei.com。今天博主想和大家探讨的是，.NET 中异步 Lambda 表达式的问题。为什么要讨论这个问题呢，这或许要从公司首席架构推广内部框架这件事情说起。我其实很久以前就有这种在团队内部做技术演进的想法，即通过公共类库、团队 Wiki 和技术交流等形式逐步地推进和完善团队整体架构的统一，因为一个团队在业务方向和技术选型上基本是一致的，因此团队内的技术演进对提高开发效率和交付质量意义重大，所以我能理解首席架构在内部推广公共类库这件事情，因为除了 KPI 这种功利性的目标以外，从长远来看这些东西对一个团队来说是积极而有利的，可是我们都知道工程师是这个世界上最傲慢的人，如果一个东西设计得不好，他们一定会尝试去改进甚至重新设计，所以架构并非是一种虚无缥缈的、凭空想象出来的东西，它的存在必须是为了解决某种问题。
所以我始终认为，架构设计必须由一线开发人员来提炼和抽象，因为只有真正经历过&amp;quot;坑&amp;quot;的人，才会清楚地知道团队里最需要解决的问题是什么，一个良好的架构绝对不是由某些所谓&amp;quot;专家&amp;quot;闭门造车的结果，你只有真正了解了一个问题，懂得如何去定义一个问题，你才会知道目前这个团队中最迫切需要去解决的问题是什么，虽然说团队里技术层次存在差异，一个技术选型必然会和普通社会学问题一样存在众口难调的情形，可是一个东西设计得不好它就是不好，你不能强迫团队成员必须去使用它，因为这实在有悖于&amp;quot;自由&amp;quot;和&amp;quot;分享&amp;quot;的黑客文化。我相信软件开发没有银弹可言，这意味着它没有一种一劳永逸的解决方案，即使它的抽象层次再高、代码鲁棒性再好，所以团队内部技术演进应该采取&amp;quot;自下而上&amp;quot;的方式，对待工程师最好的方式就是给他们充分的自由，&amp;ldquo;自上而下&amp;quot;的行政命令不适合工程师文化，自计算机文明诞生以来，那种来自内心深处的&amp;quot;极客思维&amp;quot;决定了我们的基因，所以啊，&amp;ldquo;请原谅我一生不羁放纵爱自由&amp;rdquo;。
好了，现在回到这个问题本身，问题产生的根源来自 ICommand 接口，而我们都知道该接口主要承担命令绑定作用。通过 ICommand 接口的定义我们可以知道，ICommand 接口的 Execute 方法是一个同步方法，因此常规的做法如 RelayCommand 或者 DelegateCommand，基本上都是传入一个 Action 来指向一个具体方法，最终 ICommand 接口中的 Execute 方法执行的实际上是这个具体方法。截止到目前为止，这个策略在主流的场景下都实施得非常好，可是我们在引入 Task、async/await 这些新的概念以后，我们突然发现 ICommand 接口存在一个亟待解决的问题，即它缺乏一个支持异步机制的 Execute 方法，显然这是一个历史遗留问题。 我开始关注这个问题是当我在同事 John 和 Charles 的项目中看到类似下面的代码，事实上他们都是非常优秀的高级工程师，在对这个问题理解和探讨的过程中，我要特别感谢他们愿意分享他们的想法。我们一起来看看下面的代码：
public RelayCommand RunCommand { get { return new RelayCommand(async ()=&amp;gt;{ /* await awaitable */ }); } } 请相信你的眼睛，因为你没有看错，让我倍感纠结的的正是这样一段简单的代码。这段代码让我迷惑的地方有两处，第一，RelayCommand 实现了 ICommand 接口，而 ICommand 接口的 Execute 方法是一个同步的方法，为什么我们可以在这个里传入一个异步方法，并通过 Action 这种委托类型来对其进行包装；第二，Action 是一个 void 类型，即无返回值的委托类型，我们这里显然使用 async 关键字修饰了一个无返回值的方法，因为我们在这个匿名方法内部使用了 await 语法。可是我们知道微软官方的建议是，使用 async 关键字来修饰一个返回值类型为 Task 或者 Task的方法。在我了解到 async 关键字还可以这样使用以后，对第二处疑惑我稍稍有些许释怀，因为事实上 Charles 就是正式通过这种思路来启发我，可我始终无法理解，为什么我们可以在一个同步的方法里执行一段异步代码，并试图去安慰自己说这段代码是异步的，在执行一个非常耗时的任务时界面不会阻塞。</description></item><item><title>Redis 缓存技术学习系列之发布订阅</title><link>https://blog.yuanpei.me/posts/1444577573/</link><pubDate>Sat, 15 Apr 2017 21:03:57 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1444577573/</guid><description>&lt;p>各位朋友，大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei">http://qinyuanpei.com&lt;/a>。最近这段时间的天气可谓是变幻莫测，常常是周一到周五像夏天般热烈，而周六和周天像秋天般冷清。你不知道它到底会在何时下雨，即使你可以一直带着伞等雨落下来。但是对于没有伞的我来说，学会努力奔跑以至于不那么狼狈，或许是在这个世界上我唯一可以去做的事情。可是你知道一个人孤独的时候，即使是下雨这种再平常不过的事情，他都可以从雨声里听出孤独的感觉来，所以这个周末我决定继续研究 Redis 缓存技术，而今天我想和大家讨论的话题是 Redis 中的发布-订阅(Pub-Sub)，希望大家喜欢！&lt;/p></description></item><item><title>Redis 缓存技术学习系列之事务处理</title><link>https://blog.yuanpei.me/posts/335366821/</link><pubDate>Sat, 08 Apr 2017 21:46:40 +0000</pubDate><guid>https://blog.yuanpei.me/posts/335366821/</guid><description>&lt;p>  在本系列的第一篇文章中，我们主要针对 Redis 中的“键”和“值”进行了学习。我们可以注意到，Redis 是一个 C/S 架构的数据库，在我们目前的认知中，它是通过终端中的一条条命令来存储和读取的，即它是一个非常典型的“请求-响应”模型。可是我们知道在实际的应用中，我们要面对的或许是更为复杂的业务逻辑，因为 Redis 中不存在传统关系型数据库中表的概念，因此在使用 Redis 的过程中，我们要面对两个实际的问题，即如何更好的维护数据库中的”键“、如何在高效执行命令的同时保证命令执行成功。对于前者，我认为这是一个设计上的问题，而对于后者，我认为这是一个技术上的问题。所以，这篇文章的核心内容就是找到这两个问题的答案。带着这样的问题出发，我们就可以正式进入这篇文章的主题：Redis 中的事务处理。&lt;/p></description></item><item><title>时间如灰烬般遥远</title><link>https://blog.yuanpei.me/posts/1357715684/</link><pubDate>Mon, 03 Apr 2017 00:25:21 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1357715684/</guid><description>&lt;p>春天，常常是万物复苏的日子，是以这段时间喜欢去各种地方赏花阅景。相比起三月中旬里裹挟着清冷的青龙寺，此刻到处人山人海的景象，仿佛洋溢着某种热闹的气息。从前读朱自清的《荷塘月色》，一直不明白“热闹是他们的，我什么都没有”这句话该做何解。当你面对梨花胜雪、桃花人面的景致的时候，心中却是如灰烬一般孤独的时候，大概终于明白，为何在熙熙攘攘的人群中会感到一丝清冷，因为唯有行走在人群里的时候，你会发现原来你一个人走了这么久。天地间万事万物更迭交替，本来是自然界中最普通的规则，可是如果每年的这个时候，你都是一个人去看这山山水水，相比时空上的孤寂感，人的孤寂感会更为强烈，“良辰美景奈何天，赏心乐事谁家院”，外面的世界再纷繁多变，对你而言不过是活着的时间。&lt;/p></description></item><item><title>Redis 缓存技术学习系列之邂逅 Redis</title><link>https://blog.yuanpei.me/posts/3032366281/</link><pubDate>Thu, 30 Mar 2017 23:31:40 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3032366281/</guid><description>&lt;p>  作为一个反主流的开发者，在某种程度上，我对传统关系型数据库一直有点“讨厌”，因为关系型数据库实际上和面向对象思想是完全冲突的，前者建立在数学集合理论的基础上，而后者则是建立在软件工程基本原则的基础上。虽然传统的 ORM、序列化/反序列化在一定程度上解决了这种冲突，但是软件开发中关于使用原生 SQL 语句还是使用 ORM 框架的争论从来没有停止过。可是实际的业务背景中，是完全无法脱离数据库的，除非在某些特定的场合下，考虑到信息安全因素而禁止开发者使用数据库，在主流技术中数据库是一个非常重要的组成部分。为了弥补这个技术上的短板，从这篇文章开始，我将会学习一个经典的缓存技术：Redis。我们这里将 Redis 定性为一门缓存技术，这说明 Redis 和 MySQL 等主流的数据库存在本质上的区别，那么这些区别到底在哪里呢？或许在看完这个系列文章以后，你心中自然就会有了答案。&lt;/p></description></item><item><title>使用 C#开发 HTTP 服务器之支持 HTTPS</title><link>https://blog.yuanpei.me/posts/2734896333/</link><pubDate>Sun, 05 Mar 2017 14:01:39 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2734896333/</guid><description>&lt;p>各位朋友大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。本文是“使用 C#开发 HTTP 服务器”系列的第六篇文章，在这个系列文章中我们实现了一个基础的 Web 服务器，它支持从本地读取静态 HTML 页面，支持 GET 和 POST 两种请求方式。该项目托管在我的&lt;a href="https://github.com/qinyuanpei">Github&lt;/a>上，项目地址为&lt;a href="https://github.com/qinyuanpei/HttpServer">https://github.com/qinyuanpei/HttpServer&lt;/a>，感兴趣的朋友可以前往了解。其间有朋友为我提供了 HTTPS 的 PR，或许这偏离了这个系列开发 HTTP 服务器的初衷，可是我们应该认识到普及 HTTPS 是大势所趋。所以在今天这篇文章中，我将为大家带来 HTTPS 相关知识的普及，以及如何为我们的这个 Web 服务器增加 HTTPS 的支持。&lt;/p></description></item><item><title>愿浮萍乘风破浪</title><link>https://blog.yuanpei.me/posts/2314414875/</link><pubDate>Sat, 04 Feb 2017 22:31:33 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2314414875/</guid><description>&lt;p>或许是今年的贺岁档电影全部遭遇“滑铁卢”的缘故，在这种情况下，电影《乘风破浪》或许会成为拯救整个贺一个岁档的奇迹。同往常一样，我依然选择一个人去看电影，而庆幸的是韩寒真的没有让我们失望。虽然前期在微博上经常看到韩寒在为这部电影做宣传，但我一直想知道它会一种什么样的方式来讲述这个故事，我隐隐约约觉得徐太浪(邓超饰)、徐正太(彭于晏饰)、小花(赵丽颖饰)三个人之间的关系非同寻常，我甚至臆想这是一部俗套的三角恋的故事。可结果却是完全出人意料的，我很喜欢这个故事。&lt;/p>


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































&lt;script type="text/javascript" src="https://blog.yuanpei.me/scripts/douban.js">&lt;/script>
&lt;script type="text/javascript">
 var ele = document.currentScript
 var douban = window.$DoubanCard(ele, '26862259', 'https:\/\/api.wmdb.tv\/movie\/api?id=26862259', 'movie', '{\u0022charts\u0022:[],\u0022comment\u0022:\u0022\u0022,\u0022create_time\u0022:\u00222017-11-15 15:47:08\u0022,\u0022id\u0022:1271534725,\u0022is_editable\u0022:false,\u0022is_private\u0022:false,\u0022platforms\u0022:[],\u0022rating\u0022:{\u0022count\u0022:1,\u0022max\u0022:5,\u0022star_count\u0022:5,\u0022value\u0022:5},\u0022sharing_text\u0022:\u0022我的评分：★★★★★ https:\/\/www.douban.com\/doubanapp\/dispatch\/movie\/26862259 来自@豆瓣App\u0022,\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch?uri=\/subject\/26862259\/interest\/1271534725\u0022,\u0022status\u0022:\u0022done\u0022,\u0022subject\u0022:{\u0022actors\u0022:[{\u0022name\u0022:\u0022邓超\u0022},{\u0022name\u0022:\u0022彭于晏\u0022},{\u0022name\u0022:\u0022赵丽颖\u0022},{\u0022name\u0022:\u0022董子健\u0022},{\u0022name\u0022:\u0022金士杰\u0022},{\u0022name\u0022:\u0022易小星\u0022},{\u0022name\u0022:\u0022张本煜\u0022},{\u0022name\u0022:\u0022李荣浩\u0022},{\u0022name\u0022:\u0022高华阳\u0022},{\u0022name\u0022:\u0022李淳\u0022},{\u0022name\u0022:\u0022孙伊涵\u0022},{\u0022name\u0022:\u0022熊黎\u0022},{\u0022name\u0022:\u0022李春嫒\u0022},{\u0022name\u0022:\u0022潘米\u0022},{\u0022name\u0022:\u0022小马达\u0022},{\u0022name\u0022:\u0022金毛期期\u0022},{\u0022name\u0022:\u0022孙启恒\u0022},{\u0022name\u0022:\u0022孙强\u0022},{\u0022name\u0022:\u0022彭菲茗\u0022},{\u0022name\u0022:\u0022贾川西\u0022},{\u0022name\u0022:\u0022张国庆\u0022},{\u0022name\u0022:\u0022程诚\u0022},{\u0022name\u0022:\u0022白珞力\u0022},{\u0022name\u0022:\u0022方励\u0022},{\u0022name\u0022:\u0022郭佳杰\u0022},{\u0022name\u0022:\u0022冷海铭\u0022}],\u0022album_no_interact\u0022:false,\u0022article_intros\u0022:[],\u0022card_subtitle\u0022:\u00222017 \/ 中国大陆 \/ 剧情 喜剧 \/ 韩寒 \/ 邓超 彭于晏\u0022,\u0022color_scheme\u0022:{\u0022_avg_color\u0022:[0.14444444444444446,0.24999999999999994,0.47058823529411764],\u0022_base_color\u0022:[0.11666666666666665,0.4046242774566474,0.6784313725490196],\u0022is_dark\u0022:true,\u0022primary_color_dark\u0022:\u00224c432d\u0022,\u0022primary_color_light\u0022:\u0022726444\u0022,\u0022secondary_color\u0022:\u0022f9f8f4\u0022},\u0022controversy_reason\u0022:\u0022\u0022,\u0022cover_url\u0022:\u0022https:\/\/dou.img.lithub.cc\/movie\/26862259.jpg\u0022,\u0022directors\u0022:[{\u0022name\u0022:\u0022韩寒\u0022}],\u0022genres\u0022:[\u0022剧情\u0022,\u0022喜剧\u0022],\u0022has_linewatch\u0022:true,\u0022honor_infos\u0022:[{\u0022kind\u0022:\u0022movie\u0022,\u0022rank\u0022:5,\u0022title\u0022:\u00222017热门院线电影\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/subject_collection\/2017_movie_744?type=rank\\u0026category=movie\\u0026rank_type=year\u0022}],\u0022id\u0022:\u002226862259\u0022,\u0022is_released\u0022:true,\u0022is_show\u0022:false,\u0022null_rating_reason\u0022:\u0022\u0022,\u0022pic\u0022:{\u0022large\u0022:\u0022https:\/\/img3.doubanio.com\/view\/photo\/m_ratio_poster\/public\/p2408407697.jpg\u0022,\u0022normal\u0022:\u0022https:\/\/img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2408407697.jpg\u0022},\u0022pubdate\u0022:[\u00222017-01-28(中国大陆)\u0022],\u0022rating\u0022:{\u0022count\u0022:634670,\u0022max\u0022:10,\u0022star_count\u0022:3.5,\u0022value\u0022:6.8},\u0022release_date\u0022:null,\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch\/movie\/26862259\u0022,\u0022subtype\u0022:\u0022movie\u0022,\u0022title\u0022:\u0022乘风破浪\u0022,\u0022type\u0022:\u0022movie\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/movie\/26862259\u0022,\u0022url\u0022:\u0022https:\/\/movie.douban.com\/subject\/26862259\/\u0022,\u0022vendor_desc\u0022:\u0022\u0022,\u0022vendor_icons\u0022:[\u0022https:\/\/img2.doubanio.com\/f\/frodo\/8e1b7f8b48804cf759fc411e75d62c5ffd4f8f42\/pics\/vendors\/migu_video.png\u0022,\u0022https:\/\/img9.doubanio.com\/f\/frodo\/fbc90f355fc45d5d2056e0d88c697f9414b56b44\/pics\/vendors\/tencent.png\u0022,\u0022https:\/\/img9.doubanio.com\/f\/frodo\/6c7b857e6b2255201e73bf83732d95d572f3f9f7\/pics\/vendors\/iqiyi.png\u0022],\u0022year\u0022:\u00222017\u0022},\u0022tags\u0022:[],\u0022vote_count\u0022:0}');
&lt;/script>
&lt;style>
 .douban-card {
 width: 100%;
 max-width: 100%;
 background: white;
 border-radius: 16px;
 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
 overflow: hidden;
 display: flex;
 text-decoration: none;
 color: #333;
 transition: transform 0.2s, box-shadow 0.2s;
 position: relative;
 margin-top: 1.5rem;
 }

 .douban-card:hover {
 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
 border: 1px solid var(--main-card-border);
 }

 .card-content {
 flex: 8;
 padding: 16px;
 display: flex;
 flex-direction: column;
 }

 .card-cover {
 flex: 2;
 position: relative;
 overflow: hidden;
 min-height: 200px;
 }

 .card-cover img {
 position: absolute;
 height: 100%;
 width: 100%;
 object-fit: cover;
 object-position: center;
 }

 .card-title {
 font-size: 18px;
 font-weight: bold;
 margin-bottom: 8px;
 color: #333;
 line-height: 1.4;
 }

 .card-meta {
 display: flex;
 align-items: center;
 margin-bottom: 12px;
 font-size: 14px;
 color: #666;
 }

 .card-rating {
 margin-right: 8px;
 font-weight: bold;
 color: #e09015;
 }

 .card-stars {
 margin: 2.5px 0 2.5px;
 font-size: 12px;
 line-height: 1;
 display: flex;
 align-items: center
 }

 .card-stars .card-stars-dark {
 position: relative;
 color: #f99b01;
 height: 16px;
 width: 80px;
 background-size: auto 100%;
 margin-right: 8px;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==)
 }

 .card-stars .card-stars-light {
 position: absolute;
 left: 0;
 color: #f99b01;
 height: 16px;
 overflow: hidden;
 background-size: auto 100%;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);

 }

 .card-description {
 font-size: 12px;
 line-height: 1.6;
 color: #666;
 overflow: hidden;
 display: -webkit-box;
 -webkit-line-clamp: 3;
 -webkit-box-orient: vertical;
 margin-bottom: 12px;
 }

 .card-info {
 font-size: 12px;
 color: #999;
 line-height: 1.6;
 }

 .card-tags {
 margin-top: 12px;
 display: flex;
 flex-wrap: wrap;
 }

 .card-tag {
 font-size: 12px;
 color: var(--main-color);
 background: #f5f5f5;
 padding: 2px 8px;
 border-radius: 3px;
 margin-right: 6px;
 margin-bottom: 6px;
 }




 .link-overlay {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 z-index: 1;
 }

 @media (max-width: 520px) {
 .douban-card {
 flex-direction: column;
 }

 .card-content {
 flex: none;
 width: 100%;
 padding: 12px;
 }

 .card-cover {
 flex: none;
 width: 100%;
 height: 180px;
 order: -1;
 }

 .card-title {
 font-size: 16px;
 margin-bottom: 6px;
 }

 .card-meta {
 margin-bottom: 8px;
 font-size: 12px;
 }

 .card-description {
 font-size: 13px;
 -webkit-line-clamp: 2;
 margin-bottom: 8px;
 }

 .card-info {
 font-size: 11px;
 }

 .card-tags {
 margin-top: 8px;
 }

 .card-tag {
 font-size: 11px;
 padding: 1px 6px;
 margin-right: 4px;
 margin-bottom: 4px;
 }
 }
&lt;/style></description></item><item><title>函数式编程常用术语</title><link>https://blog.yuanpei.me/posts/2171683728/</link><pubDate>Thu, 02 Feb 2017 19:21:12 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2171683728/</guid><description>&lt;p>近年来函数式编程这种概念渐渐流行起来，尤其是在 React/Vuejs 这两个前端框架的推动下，函数式编程就像股新思潮一般瞬间席卷整个技术圈。虽然博主接触到的前端技术并不算深入，可这并不妨碍我们通过类似概念的延伸来理解这种概念。首先，函数式编程是一种编程范式，而我们所熟悉的常见编程范式则有&lt;strong>命令式编程(Imperative Programmming)&lt;/strong>、&lt;strong>函数式编程(Functional Programming)&lt;/strong>、&lt;strong>逻辑式编程(Logic Programming)&lt;/strong>、**声明式编程(Declarative Programming)&lt;strong>和&lt;/strong>响应式编程(Reactive Programming)**等。现代编程语言 在发展过程中实际上都在借鉴不同的编程范式，比如 Lisp 和 Haskell 是最经典的函数式编程语言，而 SmartTalk、C++和 Java 则是最经典的命令式编程语言。微软的 C#语言最早主要借鉴 Java 语言，在其引入 lambda 和 LINQ 特性以后，使得 C#开始具备实施函数式编程的基础，而最新的 Java8 同样开始强化 lambda 这一特性，为什么 lambda 会如此重要呢？这或许要从函数式编程的基本术语开始说起。&lt;/p></description></item><item><title>基于 Mono 和 VSCode 打造轻量级跨平台 IDE</title><link>https://blog.yuanpei.me/posts/3568552646/</link><pubDate>Fri, 18 Nov 2016 20:23:44 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3568552646/</guid><description>&lt;p>最近 Visual Studio 推出 Mac 版本的消息迅速在技术圈里刷屏，当工程师们最喜欢的笔记本电脑 Mac，邂逅地球上最强大的集成开发环境 Visual Studio 的时候，会碰撞出怎样精彩的火花呢？在微软新任 CEO 纳德拉的“移动为先、云为先”战略下，微软的转变渐渐开始让人欣喜，从.NET Core、VSCode、TypeScript 再到近期的 Visual Studio For Mac，这一系列动作让我们感觉到，微软的技术栈越来越多地向着开源和跨平台两个方向努力。我们曾经固执地认为，微软的技术栈注定永远无法摆脱 Windows 的束缚，而事实上这个世界每天都在发生着变化。或许这次 Visual Studio 推出 Mac 版这件事情，本质上是微软收购的 Xamarin 公司旗下产品 Xamarin Studio 的一次改头换面。可是这件事情说明，微软正在努力让.NET 技术栈融入更多的应用场景。对我而言，我是没有钱去买一台 Mac 的，所以在这篇文章中，我们将在 Linux 下通过 Mono 和 VSCode 来打造一个轻量级的 IDE。而据说 Mono 会和 Xamarin 一样，将来会成为.NET 基金会的一部分。&lt;/p></description></item><item><title>在 Kindle 上阅读 Markdown 文档</title><link>https://blog.yuanpei.me/posts/1152813120/</link><pubDate>Sun, 13 Nov 2016 13:58:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1152813120/</guid><description>&lt;p>其实我一直希望 Kindle 能够成为我知识管理的一部分，我们此刻所处的这个时代实则是一个信息爆炸的时代。我们每天都不得不去面对各种各样的信息，可这些信息中有多少是我们真正需要的呢？在一个信息碎片化的时代，有人说我们要懂得如何去利用碎片化的时间，有人说我们要懂得如何去高效查找需要的信息，微信和微博这类社交产品加速了信息的碎片化，或许当我们发现自己无法再集中精力去做一件事情的时候，我们就应该停下来反思如何去做好个人知识管理，我一直希望 Kindle 可以成为我知识管理的一部分，因为 Kindle 的阅读体验完全超越主流的电子设备，而且它可以让我们更加专注地去关注内容本身，Kindle 的同步机制为了提供了良好的知识管理契机，所以这篇文章我主要想分享我在以 Kindle 作为知识管理载体这件事情上的想法，希望对大家有所启发。&lt;/p></description></item><item><title>生命的朝圣者</title><link>https://blog.yuanpei.me/posts/3657008967/</link><pubDate>Sat, 05 Nov 2016 21:44:52 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3657008967/</guid><description>&lt;script type="text/javascript" src="https://blog.yuanpei.me/scripts/douban.js">&lt;/script>
&lt;script type="text/javascript">
 var ele = document.currentScript
 var douban = window.$DoubanCard(ele, '24934182', 'https:\/\/douban.edui.fun\/v2\/book\/id\/24934182', 'book', '{\u0022charts\u0022:[],\u0022comment\u0022:\u0022这本书我最近正在读，虽然看起来不像一本畅销书，可它会告诉你怎么样，去坚持做一件事情，人生有时候是需要认真的，即使输了又怎么样？\u0022,\u0022create_time\u0022:\u00222016-11-06 18:34:17\u0022,\u0022id\u0022:1099237554,\u0022is_editable\u0022:false,\u0022is_private\u0022:false,\u0022platforms\u0022:[],\u0022rating\u0022:null,\u0022sharing_text\u0022:\u0022这本书我最近正在读，虽然看起来不像一本畅销书，可它会告诉你怎么样，去坚持做一件事情，人生有时候是需要认真的，即使输了又怎么样？ https:\/\/www.douban.com\/doubanapp\/dispatch\/book\/24934182 来自@豆瓣App\u0022,\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch?uri=\/subject\/24934182\/interest\/1099237554\u0022,\u0022status\u0022:\u0022done\u0022,\u0022subject\u0022:{\u0022article_intros\u0022:[],\u0022author\u0022:[\u0022[英] 蕾秋·乔伊斯\u0022],\u0022book_subtitle\u0022:\u0022\u0022,\u0022buy_more_uri\u0022:\u0022douban:\/\/douban.com\/book\/24934182\/vendors_douban\u0022,\u0022card_subtitle\u0022:\u0022[英] 蕾秋·乔伊斯 \/ 2013 \/ 北京联合出版公司\u0022,\u0022color_scheme\u0022:{\u0022_avg_color\u0022:[0.11363636363636369,0.3027522935779816,0.8549019607843137],\u0022_base_color\u0022:[0.1202185792349727,0.2640692640692641,0.9058823529411765],\u0022is_dark\u0022:true,\u0022primary_color_dark\u0022:\u00227f765d\u0022,\u0022primary_color_light\u0022:\u0022a59979\u0022,\u0022secondary_color\u0022:\u0022f9f8f4\u0022},\u0022controversy_reason\u0022:\u0022\u0022,\u0022cover_url\u0022:\u0022https:\/\/dou.img.lithub.cc\/book\/24934182.jpg\u0022,\u0022has_ebook\u0022:true,\u0022honor_infos\u0022:[{\u0022kind\u0022:\u0022book\u0022,\u0022rank\u0022:2,\u0022title\u0022:\u00222023年出版10周年图书\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/subject_collection\/ECWQ6BC2I?type=rank\\u0026category=book\\u0026rank_type=year\u0022}],\u0022id\u0022:\u002224934182\u0022,\u0022intro\u0022:\u0022★2013年欧洲首席畅销小说，入围2012年布克文学奖，同名电影拍摄中\\n★台湾读者表示“很久没有读一本书读到凌晨”、“是一个简单、素朴但会令人深深感动的故事”、“笑泪交织的阅读”\\n★2013年春季英国最具影响力“理查与茱蒂”读书俱乐部书单第1 名；欧普拉读书俱乐部夏日选书、美国图书馆协会选书；2012年英国最畅销新人小说；《出版人周刊》、《纽约时报》、《泰晤士报》、《嘉人》、《今日美国》等各大媒体高评价推荐\\n★他以为人生就这么过去了，直到收到那封信\\n★1个人，87天，627英里\\n★有关爱的回归、自我发现、日常生活的信念以及万物之美\\n★这一年，我们都需要哈罗德安静而勇敢的陪伴\\n哈罗德·弗莱，六十岁，在酿酒厂干了四十年销售代表后默默退休，没有升迁，既无朋友，也无敌人，退休时公司甚至连欢送会都没开。他跟隔阂很深的妻子住在英国的乡间，生活平静，夫妻疏离，日复一日。\\n一天早晨，他收到一封信，来自二十年未见的老友奎妮。她患了癌症，写信告别。震惊、悲痛之下，哈罗德写了回信，在寄出的路上，他由奎妮想到了自己的人生，经过了一个又一个邮筒，越走越远，最后，他从英国最西南一路走到了最东北，横跨整个英格兰。87天，627英里，只凭一个信念：只要他走，老友就会活下去！\\n这是哈罗德千里跋涉的故事。从他脚步迈开的那一刻起，与他六百多英里旅程并行的，是他穿越时光隧道的另一场旅行。\u0022,\u0022is_released\u0022:true,\u0022is_show\u0022:false,\u0022min_sale_price\u0022:\u0022¥32.8\u0022,\u0022null_rating_reason\u0022:\u0022\u0022,\u0022other_versions_count\u0022:13,\u0022pages\u0022:[\u0022320\u0022],\u0022pic\u0022:{\u0022large\u0022:\u0022https:\/\/img2.doubanio.com\/view\/subject\/l\/public\/s26936721.jpg\u0022,\u0022normal\u0022:\u0022https:\/\/img2.doubanio.com\/view\/subject\/m\/public\/s26936721.jpg\u0022},\u0022press\u0022:[\u0022北京联合出版公司\u0022],\u0022pubdate\u0022:[\u00222013-9-1\u0022],\u0022rating\u0022:{\u0022count\u0022:81389,\u0022max\u0022:10,\u0022star_count\u0022:4,\u0022value\u0022:8.1},\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch\/book\/24934182\u0022,\u0022subtype\u0022:\u0022book\u0022,\u0022title\u0022:\u0022一个人的朝圣\u0022,\u0022type\u0022:\u0022book\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/book\/24934182\u0022,\u0022url\u0022:\u0022https:\/\/book.douban.com\/subject\/24934182\/\u0022,\u0022vendor_icons\u0022:[\u0022https:\/\/img1.doubanio.com\/f\/frodo\/f6f620132e6d8a02d171f03114bbe2339aa8af97\/pics\/vendors\/logo_doubanread@2x.png\u0022],\u0022vendor_original_price\u0022:\u0022\u0022,\u0022vendor_sale_price\u0022:\u0022\u0022},\u0022tags\u0022:[\u0022一个人的朝圣\u0022,\u0022自我发现\u0022,\u0022外国文学\u0022],\u0022vote_count\u0022:1}');
&lt;/script>
&lt;style>
 .douban-card {
 width: 100%;
 max-width: 100%;
 background: white;
 border-radius: 16px;
 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
 overflow: hidden;
 display: flex;
 text-decoration: none;
 color: #333;
 transition: transform 0.2s, box-shadow 0.2s;
 position: relative;
 margin-top: 1.5rem;
 }

 .douban-card:hover {
 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
 border: 1px solid var(--main-card-border);
 }

 .card-content {
 flex: 8;
 padding: 16px;
 display: flex;
 flex-direction: column;
 }

 .card-cover {
 flex: 2;
 position: relative;
 overflow: hidden;
 min-height: 200px;
 }

 .card-cover img {
 position: absolute;
 height: 100%;
 width: 100%;
 object-fit: cover;
 object-position: center;
 }

 .card-title {
 font-size: 18px;
 font-weight: bold;
 margin-bottom: 8px;
 color: #333;
 line-height: 1.4;
 }

 .card-meta {
 display: flex;
 align-items: center;
 margin-bottom: 12px;
 font-size: 14px;
 color: #666;
 }

 .card-rating {
 margin-right: 8px;
 font-weight: bold;
 color: #e09015;
 }

 .card-stars {
 margin: 2.5px 0 2.5px;
 font-size: 12px;
 line-height: 1;
 display: flex;
 align-items: center
 }

 .card-stars .card-stars-dark {
 position: relative;
 color: #f99b01;
 height: 16px;
 width: 80px;
 background-size: auto 100%;
 margin-right: 8px;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==)
 }

 .card-stars .card-stars-light {
 position: absolute;
 left: 0;
 color: #f99b01;
 height: 16px;
 overflow: hidden;
 background-size: auto 100%;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);

 }

 .card-description {
 font-size: 12px;
 line-height: 1.6;
 color: #666;
 overflow: hidden;
 display: -webkit-box;
 -webkit-line-clamp: 3;
 -webkit-box-orient: vertical;
 margin-bottom: 12px;
 }

 .card-info {
 font-size: 12px;
 color: #999;
 line-height: 1.6;
 }

 .card-tags {
 margin-top: 12px;
 display: flex;
 flex-wrap: wrap;
 }

 .card-tag {
 font-size: 12px;
 color: var(--main-color);
 background: #f5f5f5;
 padding: 2px 8px;
 border-radius: 3px;
 margin-right: 6px;
 margin-bottom: 6px;
 }




 .link-overlay {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 z-index: 1;
 }

 @media (max-width: 520px) {
 .douban-card {
 flex-direction: column;
 }

 .card-content {
 flex: none;
 width: 100%;
 padding: 12px;
 }

 .card-cover {
 flex: none;
 width: 100%;
 height: 180px;
 order: -1;
 }

 .card-title {
 font-size: 16px;
 margin-bottom: 6px;
 }

 .card-meta {
 margin-bottom: 8px;
 font-size: 12px;
 }

 .card-description {
 font-size: 13px;
 -webkit-line-clamp: 2;
 margin-bottom: 8px;
 }

 .card-info {
 font-size: 11px;
 }

 .card-tags {
 margin-top: 8px;
 }

 .card-tag {
 font-size: 11px;
 padding: 1px 6px;
 margin-right: 4px;
 margin-bottom: 4px;
 }
 }
&lt;/style>
&lt;p>最初开始读这本书的时候，并没有想到这本书会讲这样一个故事，甚至它不像一本畅销书一样让人充满期待，可是当你逐渐理清整个故事的来龙去脉以后，或许你会喜欢这个故事甚至被这个这个故事所震撼。我从未对宗教意义上的朝圣进行过深入了解，我所知道的朝圣，比如每年伊斯兰教历的第十二月，都会有数以百万计的伊斯兰教徒前往麦加参与朝觐仪式，而国内每年都会有从各地前往布达拉宫下的大昭寺朝佛的佛教信徒，而对藏传佛教信众来说“叩长头”是最为至诚的礼佛方式之一。所以朝圣是一项具有重大的道德或者灵性意义的旅程或者探寻，它关乎对信仰的思考同时注重身体力行，因为朝圣者始终相信前往一个重要的地方，能够从中获得灵性或者是得到治愈。&lt;/p></description></item><item><title>基于 C# 中的 Trace 实现一个简单的日志系统</title><link>https://blog.yuanpei.me/posts/1254783039/</link><pubDate>Tue, 25 Oct 2016 20:16:13 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1254783039/</guid><description>&lt;p>最近在做的项目进入中期阶段，因为在基本框架结构确定以后，现阶段工作重心开始转变为具体业务逻辑的实现，在这个过程中我认为主要有两点，即保证逻辑代码的正确性和容错性、确定需求文档中隐性需求和逻辑缺陷。为什么我说的这两点都和用户需求这个层面息息相关呢？或许这和我这段时间的感受有些关系吧，我觉得当我们在面对用户提出的需求的时候，一个非常让我们不爽的一个地方是，我们总是需要花费大量的时间来和用户确定某些细节，而这些细节无论在 BRD 或者 PRD 中都无从体现。固然从用户层面上来讲，我们无法要求用户提供，详尽到每一个细节的需求文档。可我觉得这是一个修养的问题，我们习惯于宽以律己、严以待人，可是如果我们连自己都说服不了，我们该如何尝试去说服别人呢？我不认为我们就应该被用户限制自由，我们共同的目的都是想要好做一件事情，所以我们的关系应该是平等的伙伴的关系，这种上下级的、命令式的主仆关系让我感觉受到了侮辱。&lt;/p></description></item><item><title>当黑客遇见画家</title><link>https://blog.yuanpei.me/posts/4205536912/</link><pubDate>Sun, 09 Oct 2016 18:38:03 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4205536912/</guid><description>&lt;script type="text/javascript" src="https://blog.yuanpei.me/scripts/douban.js">&lt;/script>
&lt;script type="text/javascript">
 var ele = document.currentScript
 var douban = window.$DoubanCard(ele, '6021440', 'https:\/\/douban.edui.fun\/v2\/book\/id\/6021440', 'book', '{\u0022charts\u0022:[],\u0022comment\u0022:\u0022\u0022,\u0022create_time\u0022:\u00222016-10-19 00:26:11\u0022,\u0022id\u0022:1099240204,\u0022is_editable\u0022:false,\u0022is_private\u0022:false,\u0022platforms\u0022:[],\u0022rating\u0022:null,\u0022sharing_text\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch\/book\/6021440 来自@豆瓣App\u0022,\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch?uri=\/subject\/6021440\/interest\/1099240204\u0022,\u0022status\u0022:\u0022done\u0022,\u0022subject\u0022:{\u0022article_intros\u0022:[],\u0022author\u0022:[\u0022[美] Paul Graham\u0022],\u0022book_subtitle\u0022:\u0022硅谷创业之父Paul Graham文集\u0022,\u0022buy_more_uri\u0022:\u0022douban:\/\/douban.com\/book\/6021440\/vendors_douban\u0022,\u0022card_subtitle\u0022:\u0022[美] Paul Graham \/ 2011 \/ 人民邮电出版社\u0022,\u0022color_scheme\u0022:{\u0022_avg_color\u0022:[0.08333333333333333,0.012345679012345637,0.6352941176470588],\u0022_base_color\u0022:[0.27777777777777773,0.10344827586206896,0.22745098039215686],\u0022is_dark\u0022:true,\u0022primary_color_dark\u0022:\u0022767f72\u0022,\u0022primary_color_light\u0022:\u00229aa594\u0022,\u0022secondary_color\u0022:\u0022f6f9f4\u0022},\u0022controversy_reason\u0022:\u0022\u0022,\u0022cover_url\u0022:\u0022https:\/\/dou.img.lithub.cc\/book\/6021440.jpg\u0022,\u0022has_ebook\u0022:true,\u0022honor_infos\u0022:[{\u0022kind\u0022:\u0022book\u0022,\u0022rank\u0022:3,\u0022title\u0022:\u0022热门互联网图书TOP10\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/subject_collection\/EC7MKUDQI?type=rank\\u0026category=book\\u0026rank_type=topn_book\u0022}],\u0022id\u0022:\u00226021440\u0022,\u0022intro\u0022:\u0022本书是硅谷创业之父Paul Graham 的文集，主要介绍黑客即优秀程序员的爱好和动机，讨论黑客成长、黑客对世界的贡献以及编程语言和黑客工作方法等所有对计算机时代感兴趣的人的一些话题。书中的内容不但有助于了解计算机编程的本质、互联网行业的规则，还会帮助读者了解我们这个时代，迫使读者独立思考。\\n本书适合所有程序员和互联网创业者，也适合一切对计算机行业感兴趣的读者。\u0022,\u0022is_released\u0022:true,\u0022is_show\u0022:false,\u0022min_sale_price\u0022:\u0022¥40.2\u0022,\u0022null_rating_reason\u0022:\u0022\u0022,\u0022other_versions_count\u0022:7,\u0022pages\u0022:[\u0022264\u0022],\u0022pic\u0022:{\u0022large\u0022:\u0022https:\/\/img9.doubanio.com\/view\/subject\/l\/public\/s4669554.jpg\u0022,\u0022normal\u0022:\u0022https:\/\/img9.doubanio.com\/view\/subject\/m\/public\/s4669554.jpg\u0022},\u0022press\u0022:[\u0022人民邮电出版社\u0022],\u0022pubdate\u0022:[\u00222011-4\u0022],\u0022rating\u0022:{\u0022count\u0022:23688,\u0022max\u0022:10,\u0022star_count\u0022:4.5,\u0022value\u0022:8.7},\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch\/book\/6021440\u0022,\u0022subtype\u0022:\u0022book\u0022,\u0022title\u0022:\u0022黑客与画家\u0022,\u0022type\u0022:\u0022book\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/book\/6021440\u0022,\u0022url\u0022:\u0022https:\/\/book.douban.com\/subject\/6021440\/\u0022,\u0022vendor_icons\u0022:[\u0022https:\/\/img1.doubanio.com\/f\/frodo\/f6f620132e6d8a02d171f03114bbe2339aa8af97\/pics\/vendors\/logo_doubanread@2x.png\u0022],\u0022vendor_original_price\u0022:\u0022\u0022,\u0022vendor_sale_price\u0022:\u0022\u0022},\u0022tags\u0022:[\u0022Hacker\u0022,\u0022编程艺术\u0022,\u0022计算机科学\u0022],\u0022vote_count\u0022:0}');
&lt;/script>
&lt;style>
 .douban-card {
 width: 100%;
 max-width: 100%;
 background: white;
 border-radius: 16px;
 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
 overflow: hidden;
 display: flex;
 text-decoration: none;
 color: #333;
 transition: transform 0.2s, box-shadow 0.2s;
 position: relative;
 margin-top: 1.5rem;
 }

 .douban-card:hover {
 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
 border: 1px solid var(--main-card-border);
 }

 .card-content {
 flex: 8;
 padding: 16px;
 display: flex;
 flex-direction: column;
 }

 .card-cover {
 flex: 2;
 position: relative;
 overflow: hidden;
 min-height: 200px;
 }

 .card-cover img {
 position: absolute;
 height: 100%;
 width: 100%;
 object-fit: cover;
 object-position: center;
 }

 .card-title {
 font-size: 18px;
 font-weight: bold;
 margin-bottom: 8px;
 color: #333;
 line-height: 1.4;
 }

 .card-meta {
 display: flex;
 align-items: center;
 margin-bottom: 12px;
 font-size: 14px;
 color: #666;
 }

 .card-rating {
 margin-right: 8px;
 font-weight: bold;
 color: #e09015;
 }

 .card-stars {
 margin: 2.5px 0 2.5px;
 font-size: 12px;
 line-height: 1;
 display: flex;
 align-items: center
 }

 .card-stars .card-stars-dark {
 position: relative;
 color: #f99b01;
 height: 16px;
 width: 80px;
 background-size: auto 100%;
 margin-right: 8px;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==)
 }

 .card-stars .card-stars-light {
 position: absolute;
 left: 0;
 color: #f99b01;
 height: 16px;
 overflow: hidden;
 background-size: auto 100%;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);

 }

 .card-description {
 font-size: 12px;
 line-height: 1.6;
 color: #666;
 overflow: hidden;
 display: -webkit-box;
 -webkit-line-clamp: 3;
 -webkit-box-orient: vertical;
 margin-bottom: 12px;
 }

 .card-info {
 font-size: 12px;
 color: #999;
 line-height: 1.6;
 }

 .card-tags {
 margin-top: 12px;
 display: flex;
 flex-wrap: wrap;
 }

 .card-tag {
 font-size: 12px;
 color: var(--main-color);
 background: #f5f5f5;
 padding: 2px 8px;
 border-radius: 3px;
 margin-right: 6px;
 margin-bottom: 6px;
 }




 .link-overlay {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 z-index: 1;
 }

 @media (max-width: 520px) {
 .douban-card {
 flex-direction: column;
 }

 .card-content {
 flex: none;
 width: 100%;
 padding: 12px;
 }

 .card-cover {
 flex: none;
 width: 100%;
 height: 180px;
 order: -1;
 }

 .card-title {
 font-size: 16px;
 margin-bottom: 6px;
 }

 .card-meta {
 margin-bottom: 8px;
 font-size: 12px;
 }

 .card-description {
 font-size: 13px;
 -webkit-line-clamp: 2;
 margin-bottom: 8px;
 }

 .card-info {
 font-size: 11px;
 }

 .card-tags {
 margin-top: 8px;
 }

 .card-tag {
 font-size: 11px;
 padding: 1px 6px;
 margin-right: 4px;
 margin-bottom: 4px;
 }
 }
&lt;/style>
&lt;p>其实一直想读《黑客与画家》这本书，所以在我买了 Kindle 以后，这本书就成为我读完的第一本书。本书作者是美国互联网界举足轻重、有“创业教父”之称的哈佛大学计算机博士保罗·格雷厄姆 (Paul Graham )，而这本书是由他的思考整理而成的一本文集，虽然这本书的名字叫做《黑客与画家》，可实际上作者在这本书中观点，并非局限于黑客与画家本身，相反地它涉及编程、软件、创业、财富、设计、研究等等多个领域。我认为这本书带给我的，更多的是一种思想上的提升，当我们沉迷在代码中无法自拔的时候，我们其实应该意识到，这个世界原本是由理性和感性两种认识混合而成的。当科技与人文发生碰撞进而共鸣，这本书会告诉你这一切是如此的美妙。&lt;/p></description></item><item><title>像诗人一样睿智，像天才一样疯狂</title><link>https://blog.yuanpei.me/posts/3653716295/</link><pubDate>Sat, 01 Oct 2016 17:12:43 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3653716295/</guid><description>&lt;script type="text/javascript" src="https://blog.yuanpei.me/scripts/douban.js">&lt;/script>
&lt;script type="text/javascript">
 var ele = document.currentScript
 var douban = window.$DoubanCard(ele, '10463953', 'https:\/\/api.wmdb.tv\/movie\/api?id=10463953', 'movie', '{\u0022charts\u0022:[],\u0022comment\u0022:\u0022\u0022,\u0022create_time\u0022:\u00222018-10-25 23:48:47\u0022,\u0022id\u0022:1504037846,\u0022is_editable\u0022:false,\u0022is_private\u0022:false,\u0022platforms\u0022:[],\u0022rating\u0022:{\u0022count\u0022:1,\u0022max\u0022:5,\u0022star_count\u0022:5,\u0022value\u0022:5},\u0022sharing_text\u0022:\u0022我的评分：★★★★★ https:\/\/www.douban.com\/doubanapp\/dispatch\/movie\/10463953 来自@豆瓣App\u0022,\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch?uri=\/subject\/10463953\/interest\/1504037846\u0022,\u0022status\u0022:\u0022done\u0022,\u0022subject\u0022:{\u0022actors\u0022:[{\u0022name\u0022:\u0022本尼迪克特·康伯巴奇\u0022},{\u0022name\u0022:\u0022凯拉·奈特莉\u0022},{\u0022name\u0022:\u0022马修·古迪\u0022},{\u0022name\u0022:\u0022罗里·金尼尔\u0022},{\u0022name\u0022:\u0022艾伦·里奇\u0022},{\u0022name\u0022:\u0022马修·比尔德\u0022},{\u0022name\u0022:\u0022查尔斯·丹斯\u0022},{\u0022name\u0022:\u0022马克·斯特朗\u0022},{\u0022name\u0022:\u0022詹姆斯·诺斯科特\u0022},{\u0022name\u0022:\u0022汤姆·古德曼-希尔\u0022},{\u0022name\u0022:\u0022史蒂芬·威丁顿\u0022},{\u0022name\u0022:\u0022伊兰·古德曼\u0022},{\u0022name\u0022:\u0022杰克·塔尔登\u0022},{\u0022name\u0022:\u0022埃里克斯·劳瑟\u0022},{\u0022name\u0022:\u0022杰克·巴农\u0022},{\u0022name\u0022:\u0022塔彭丝·米德尔顿\u0022},{\u0022name\u0022:\u0022安德鲁·哈维尔\u0022},{\u0022name\u0022:\u0022威尔·鲍登\u0022},{\u0022name\u0022:\u0022李·阿斯奎斯-柯\u0022},{\u0022name\u0022:\u0022海莉·乔安妮·培根\u0022},{\u0022name\u0022:\u0022安库塔·布雷班\u0022},{\u0022name\u0022:\u0022格雷斯·卡尔德\u0022},{\u0022name\u0022:\u0022理查德·坎贝尔\u0022},{\u0022name\u0022:\u0022温斯顿·丘吉尔\u0022},{\u0022name\u0022:\u0022克里斯·考林\u0022},{\u0022name\u0022:\u0022汉娜·弗林\u0022},{\u0022name\u0022:\u0022卢克·霍普\u0022},{\u0022name\u0022:\u0022斯图尔特·马修斯\u0022},{\u0022name\u0022:\u0022亚当·诺威尔\u0022},{\u0022name\u0022:\u0022哈里·S·杜鲁门\u0022}],\u0022album_no_interact\u0022:false,\u0022article_intros\u0022:[],\u0022card_subtitle\u0022:\u00222014 \/ 英国 美国 \/ 剧情 同性 传记 战争 \/ 莫滕·泰杜姆 \/ 本尼迪克特·康伯巴奇 凯拉·奈特莉\u0022,\u0022color_scheme\u0022:{\u0022_avg_color\u0022:[0.09374999999999996,0.1616161616161616,0.38823529411764707],\u0022_base_color\u0022:[0.2333333333333333,0.09090909090909097,0.21568627450980393],\u0022is_dark\u0022:true,\u0022primary_color_dark\u0022:\u0022494c45\u0022,\u0022primary_color_light\u0022:\u00226e7268\u0022,\u0022secondary_color\u0022:\u0022f7f9f4\u0022},\u0022controversy_reason\u0022:\u0022\u0022,\u0022cover_url\u0022:\u0022https:\/\/dou.img.lithub.cc\/movie\/10463953.jpg\u0022,\u0022directors\u0022:[{\u0022name\u0022:\u0022莫滕·泰杜姆\u0022}],\u0022genres\u0022:[\u0022剧情\u0022,\u0022同性\u0022,\u0022传记\u0022],\u0022has_linewatch\u0022:true,\u0022honor_infos\u0022:[{\u0022kind\u0022:\u0022movie\u0022,\u0022rank\u0022:156,\u0022title\u0022:\u0022豆瓣电影Top250\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/subject_collection\/movie_top250\u0022}],\u0022id\u0022:\u002210463953\u0022,\u0022is_released\u0022:true,\u0022is_show\u0022:false,\u0022null_rating_reason\u0022:\u0022\u0022,\u0022pic\u0022:{\u0022large\u0022:\u0022https:\/\/img1.doubanio.com\/view\/photo\/m_ratio_poster\/public\/p2922986728.jpg\u0022,\u0022normal\u0022:\u0022https:\/\/img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2922986728.jpg\u0022},\u0022pubdate\u0022:[\u00222015-07-21(中国大陆)\u0022],\u0022rating\u0022:{\u0022count\u0022:721336,\u0022max\u0022:10,\u0022star_count\u0022:4.5,\u0022value\u0022:8.8},\u0022release_date\u0022:null,\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch\/movie\/10463953\u0022,\u0022subtype\u0022:\u0022movie\u0022,\u0022title\u0022:\u0022模仿游戏\u0022,\u0022type\u0022:\u0022movie\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/movie\/10463953\u0022,\u0022url\u0022:\u0022https:\/\/movie.douban.com\/subject\/10463953\/\u0022,\u0022vendor_desc\u0022:\u0022\u0022,\u0022vendor_icons\u0022:[\u0022https:\/\/img9.doubanio.com\/f\/frodo\/fbc90f355fc45d5d2056e0d88c697f9414b56b44\/pics\/vendors\/tencent.png\u0022,\u0022https:\/\/img2.doubanio.com\/f\/frodo\/8e1b7f8b48804cf759fc411e75d62c5ffd4f8f42\/pics\/vendors\/migu_video.png\u0022,\u0022https:\/\/img9.doubanio.com\/f\/frodo\/6c7b857e6b2255201e73bf83732d95d572f3f9f7\/pics\/vendors\/iqiyi.png\u0022],\u0022year\u0022:\u00222014\u0022},\u0022tags\u0022:[\u0022传记\u0022,\u0022英国\u0022,\u0022图灵\u0022,\u0022历史\u0022,\u0022二战\u0022,\u0022战争\u0022,\u0022同性\u0022,\u0022美国\u0022,\u0022人生\u0022,\u0022军事\u0022,\u0022经典\u0022,\u0022剧情\u0022],\u0022vote_count\u0022:0}');
&lt;/script>
&lt;style>
 .douban-card {
 width: 100%;
 max-width: 100%;
 background: white;
 border-radius: 16px;
 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
 overflow: hidden;
 display: flex;
 text-decoration: none;
 color: #333;
 transition: transform 0.2s, box-shadow 0.2s;
 position: relative;
 margin-top: 1.5rem;
 }

 .douban-card:hover {
 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
 border: 1px solid var(--main-card-border);
 }

 .card-content {
 flex: 8;
 padding: 16px;
 display: flex;
 flex-direction: column;
 }

 .card-cover {
 flex: 2;
 position: relative;
 overflow: hidden;
 min-height: 200px;
 }

 .card-cover img {
 position: absolute;
 height: 100%;
 width: 100%;
 object-fit: cover;
 object-position: center;
 }

 .card-title {
 font-size: 18px;
 font-weight: bold;
 margin-bottom: 8px;
 color: #333;
 line-height: 1.4;
 }

 .card-meta {
 display: flex;
 align-items: center;
 margin-bottom: 12px;
 font-size: 14px;
 color: #666;
 }

 .card-rating {
 margin-right: 8px;
 font-weight: bold;
 color: #e09015;
 }

 .card-stars {
 margin: 2.5px 0 2.5px;
 font-size: 12px;
 line-height: 1;
 display: flex;
 align-items: center
 }

 .card-stars .card-stars-dark {
 position: relative;
 color: #f99b01;
 height: 16px;
 width: 80px;
 background-size: auto 100%;
 margin-right: 8px;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==)
 }

 .card-stars .card-stars-light {
 position: absolute;
 left: 0;
 color: #f99b01;
 height: 16px;
 overflow: hidden;
 background-size: auto 100%;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);

 }

 .card-description {
 font-size: 12px;
 line-height: 1.6;
 color: #666;
 overflow: hidden;
 display: -webkit-box;
 -webkit-line-clamp: 3;
 -webkit-box-orient: vertical;
 margin-bottom: 12px;
 }

 .card-info {
 font-size: 12px;
 color: #999;
 line-height: 1.6;
 }

 .card-tags {
 margin-top: 12px;
 display: flex;
 flex-wrap: wrap;
 }

 .card-tag {
 font-size: 12px;
 color: var(--main-color);
 background: #f5f5f5;
 padding: 2px 8px;
 border-radius: 3px;
 margin-right: 6px;
 margin-bottom: 6px;
 }




 .link-overlay {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 z-index: 1;
 }

 @media (max-width: 520px) {
 .douban-card {
 flex-direction: column;
 }

 .card-content {
 flex: none;
 width: 100%;
 padding: 12px;
 }

 .card-cover {
 flex: none;
 width: 100%;
 height: 180px;
 order: -1;
 }

 .card-title {
 font-size: 16px;
 margin-bottom: 6px;
 }

 .card-meta {
 margin-bottom: 8px;
 font-size: 12px;
 }

 .card-description {
 font-size: 13px;
 -webkit-line-clamp: 2;
 margin-bottom: 8px;
 }

 .card-info {
 font-size: 11px;
 }

 .card-tags {
 margin-top: 8px;
 }

 .card-tag {
 font-size: 11px;
 padding: 1px 6px;
 margin-right: 4px;
 margin-bottom: 4px;
 }
 }
&lt;/style>
&lt;p>我不知道大家如何定义程序员这个工作，在我看来，在某种意义上，程序员和艺术家们具有相同之处，我们都是创作者，和诗人、画家、作家等等这些职业相近，我们都在试图创作出优秀的作品，我们借助编程语言来重构我们对这个世界的认识、借助抽象的概念来创造这个世界上不存在的东西，所以我们对自由和创造的渴望，来源自我们在这个世界上写下的第一行代码，或许这像是一个充满理想主义的臆想，可这并不重要，重要的是你如何看待这个世界、如何看待你自己，我更喜欢将程序员视为造梦者，就像每一个孩子在搭积木的时候，都有一个建筑师的梦一样，你可以选择让代码简洁、优雅，你同样选择让代码肮脏、丑陋，你相信什么，你执着什么，它就会是什么，所以为什么不给我们自己更多创造的机会。&lt;/p></description></item><item><title>你了解爱的艺术吗？</title><link>https://blog.yuanpei.me/posts/2275646954/</link><pubDate>Sat, 24 Sep 2016 22:42:44 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2275646954/</guid><description>&lt;script type="text/javascript" src="https://blog.yuanpei.me/scripts/douban.js">&lt;/script>
&lt;script type="text/javascript">
 var ele = document.currentScript
 var douban = window.$DoubanCard(ele, '3026879', 'https:\/\/douban.edui.fun\/v2\/book\/id\/3026879', 'book', '{\u0022charts\u0022:[],\u0022comment\u0022:\u0022\u0022,\u0022create_time\u0022:\u00222016-10-19 00:30:58\u0022,\u0022id\u0022:1099241678,\u0022is_editable\u0022:false,\u0022is_private\u0022:false,\u0022platforms\u0022:[],\u0022rating\u0022:null,\u0022sharing_text\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch\/book\/3026879 来自@豆瓣App\u0022,\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch?uri=\/subject\/3026879\/interest\/1099241678\u0022,\u0022status\u0022:\u0022done\u0022,\u0022subject\u0022:{\u0022article_intros\u0022:[],\u0022author\u0022:[\u0022[美] 艾·弗洛姆\u0022],\u0022book_subtitle\u0022:\u0022\u0022,\u0022buy_more_uri\u0022:\u0022douban:\/\/douban.com\/book\/3026879\/vendors_douban\u0022,\u0022card_subtitle\u0022:\u0022[美] 艾·弗洛姆 \/ 2008 \/ 上海译文出版社\u0022,\u0022color_scheme\u0022:{\u0022_avg_color\u0022:[0.9583333333333336,0.01680672268907569,0.9333333333333333],\u0022_base_color\u0022:[0.9333333333333333,0.021276595744680778,0.9215686274509803],\u0022is_dark\u0022:false,\u0022primary_color_dark\u0022:\u0022f2edef\u0022,\u0022primary_color_light\u0022:\u0022f2edef\u0022,\u0022secondary_color\u0022:\u0022f9f4f6\u0022},\u0022controversy_reason\u0022:\u0022\u0022,\u0022cover_url\u0022:\u0022https:\/\/dou.img.lithub.cc\/book\/3026879.jpg\u0022,\u0022has_ebook\u0022:true,\u0022honor_infos\u0022:[{\u0022kind\u0022:\u0022book\u0022,\u0022rank\u0022:174,\u0022title\u0022:\u0022豆瓣图书Top250\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/subject_collection\/book_top250\u0022}],\u0022id\u0022:\u00223026879\u0022,\u0022intro\u0022:\u0022《爱的艺术》是德裔美籍心理学家和哲学家、法兰克福学派重要成员艾里希-弗洛姆最著名的作品，自1956年出版至今已被翻译成32种文字，在全世界畅销不衰，被誉为当代爱的艺术理论专著最著名的作品。\\n在这本书中，弗洛姆认为，爱情不是一种与人的成熟程度无关，只需要投入身心的感情。如果不努力发展自己的全部人格并以此达到一种创造倾向性，那么每种爱的试图都会失败，如果没有爱他人的能力，如果不能真正谦恭地、勇敢地、真诚地和有纪律地爱他人，那么人们在自己的爱情生活中也永远得不到满足。\\n弗洛姆进而提出，爱是一门艺术，要求想要掌握这门艺术的人有这方面的知识并付出努力。在这里，爱不仅仅是狭隘的男女爱情，也并非通过磨练增进技巧即可获得。爱是人格整体的展现，要发展爱的能力，就需要努力发展自己的人格，并朝着有益的目标迈进。\\n此版特别收录弗洛姆学术助手纪念文章《弗洛姆生命中的爱》。\u0022,\u0022is_released\u0022:true,\u0022is_show\u0022:false,\u0022min_sale_price\u0022:\u0022¥14.55\u0022,\u0022null_rating_reason\u0022:\u0022\u0022,\u0022other_versions_count\u0022:32,\u0022pages\u0022:[\u0022144\u0022],\u0022pic\u0022:{\u0022large\u0022:\u0022https:\/\/img9.doubanio.com\/view\/subject\/l\/public\/s2990934.jpg\u0022,\u0022normal\u0022:\u0022https:\/\/img9.doubanio.com\/view\/subject\/m\/public\/s2990934.jpg\u0022},\u0022press\u0022:[\u0022上海译文出版社\u0022],\u0022pubdate\u0022:[\u00222008-4\u0022],\u0022rating\u0022:{\u0022count\u0022:56337,\u0022max\u0022:10,\u0022star_count\u0022:4.5,\u0022value\u0022:8.8},\u0022sharing_url\u0022:\u0022https:\/\/www.douban.com\/doubanapp\/dispatch\/book\/3026879\u0022,\u0022subtype\u0022:\u0022book\u0022,\u0022title\u0022:\u0022爱的艺术\u0022,\u0022type\u0022:\u0022book\u0022,\u0022uri\u0022:\u0022douban:\/\/douban.com\/book\/3026879\u0022,\u0022url\u0022:\u0022https:\/\/book.douban.com\/subject\/3026879\/\u0022,\u0022vendor_icons\u0022:[\u0022https:\/\/img1.doubanio.com\/f\/frodo\/f6f620132e6d8a02d171f03114bbe2339aa8af97\/pics\/vendors\/logo_doubanread@2x.png\u0022],\u0022vendor_original_price\u0022:\u0022\u0022,\u0022vendor_sale_price\u0022:\u0022\u0022},\u0022tags\u0022:[\u0022心理学\u0022,\u0022弗洛姆\u0022,\u0022爱的艺术\u0022,\u0022哲学\u0022],\u0022vote_count\u0022:0}');
&lt;/script>
&lt;style>
 .douban-card {
 width: 100%;
 max-width: 100%;
 background: white;
 border-radius: 16px;
 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
 overflow: hidden;
 display: flex;
 text-decoration: none;
 color: #333;
 transition: transform 0.2s, box-shadow 0.2s;
 position: relative;
 margin-top: 1.5rem;
 }

 .douban-card:hover {
 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
 border: 1px solid var(--main-card-border);
 }

 .card-content {
 flex: 8;
 padding: 16px;
 display: flex;
 flex-direction: column;
 }

 .card-cover {
 flex: 2;
 position: relative;
 overflow: hidden;
 min-height: 200px;
 }

 .card-cover img {
 position: absolute;
 height: 100%;
 width: 100%;
 object-fit: cover;
 object-position: center;
 }

 .card-title {
 font-size: 18px;
 font-weight: bold;
 margin-bottom: 8px;
 color: #333;
 line-height: 1.4;
 }

 .card-meta {
 display: flex;
 align-items: center;
 margin-bottom: 12px;
 font-size: 14px;
 color: #666;
 }

 .card-rating {
 margin-right: 8px;
 font-weight: bold;
 color: #e09015;
 }

 .card-stars {
 margin: 2.5px 0 2.5px;
 font-size: 12px;
 line-height: 1;
 display: flex;
 align-items: center
 }

 .card-stars .card-stars-dark {
 position: relative;
 color: #f99b01;
 height: 16px;
 width: 80px;
 background-size: auto 100%;
 margin-right: 8px;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==)
 }

 .card-stars .card-stars-light {
 position: absolute;
 left: 0;
 color: #f99b01;
 height: 16px;
 overflow: hidden;
 background-size: auto 100%;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);

 }

 .card-description {
 font-size: 12px;
 line-height: 1.6;
 color: #666;
 overflow: hidden;
 display: -webkit-box;
 -webkit-line-clamp: 3;
 -webkit-box-orient: vertical;
 margin-bottom: 12px;
 }

 .card-info {
 font-size: 12px;
 color: #999;
 line-height: 1.6;
 }

 .card-tags {
 margin-top: 12px;
 display: flex;
 flex-wrap: wrap;
 }

 .card-tag {
 font-size: 12px;
 color: var(--main-color);
 background: #f5f5f5;
 padding: 2px 8px;
 border-radius: 3px;
 margin-right: 6px;
 margin-bottom: 6px;
 }




 .link-overlay {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 z-index: 1;
 }

 @media (max-width: 520px) {
 .douban-card {
 flex-direction: column;
 }

 .card-content {
 flex: none;
 width: 100%;
 padding: 12px;
 }

 .card-cover {
 flex: none;
 width: 100%;
 height: 180px;
 order: -1;
 }

 .card-title {
 font-size: 16px;
 margin-bottom: 6px;
 }

 .card-meta {
 margin-bottom: 8px;
 font-size: 12px;
 }

 .card-description {
 font-size: 13px;
 -webkit-line-clamp: 2;
 margin-bottom: 8px;
 }

 .card-info {
 font-size: 11px;
 }

 .card-tags {
 margin-top: 8px;
 }

 .card-tag {
 font-size: 11px;
 padding: 1px 6px;
 margin-right: 4px;
 margin-bottom: 4px;
 }
 }
&lt;/style>
&lt;p>或许我不是一个懂得如何去爱人的人，我时常陷入一种自我否定的焦虑当中，当我发觉自己喜欢上一个人的时候，从某种意义上它会让我身上的缺点被无情地放大，我并不畏惧在喜欢的人面前暴露这些缺点，因为这就是真实的我，因此我从来不喜欢去塑造别人，让别人成为我心目中期待的样子，可是我会忍不住去塑造我自己，尤其是在和别人相处的过程中，发现我身上的缺点或者问题的时候，我习惯了对自我严格，虽然我知道这个过程注定痛苦，可是你能告诉我，爱到底是什么吗？如果爱不足以让我们改变，我们喜欢的究竟是一个怎样的自己、怎样的别人？&lt;/p></description></item><item><title>一个关于概率的问题的思考</title><link>https://blog.yuanpei.me/posts/3247186509/</link><pubDate>Sat, 24 Sep 2016 20:06:45 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3247186509/</guid><description>&lt;p>最近需要给公司内部编写一个随机生成人员名单的小工具，在解决这个问题的过程中，我认识到这是一个概率相关的问题，即使在过去我曾经设计过类似&lt;a href="http://localhost:4000/2015/03/12/create-luckyroll-game-with-unity3d.html">转盘抽奖&lt;/a>这样的应用程序，可我并不认为我真正搞清楚了这个问题，所以想在这篇文章中说说我对概率问题的相关思考。首先，我们来考虑这个问题的背景，我们需要定期在内部举行英语交流活动，可是大家的英语水差异悬殊，所以如果按照常规的思路来解决这个问题，即认为每个人被选中的概率是相等的话，实际上对英语不好的人是显得不公平的。其次，作为一个内部活动它需要的是营造一种氛围，让每个人参与到其中，所以它要求英语好的人有一个相对高的优先级，这样能够方便在活动开始前“破冰”，可是同时它需要让英语不好的人能够参与其中，所以这个问题该如何解决呢？这就是我们今天想要讨论的话题！&lt;/p></description></item><item><title>一见钟情，无疾而终</title><link>https://blog.yuanpei.me/posts/21112647/</link><pubDate>Sat, 10 Sep 2016 19:29:57 +0000</pubDate><guid>https://blog.yuanpei.me/posts/21112647/</guid><description>&lt;p>昨天下午，当她从公司办理完离职手续的时候，她在内部聊天工具上告诉我：“师父，我在公司的离职手续都办理完了，我要走了”。在那一瞬间，我突然非常平静地走过去对她说：“那就走吧”。我不知道她是不是想让我在她离开之前做些什么，或许这完全就是我的一厢情愿，因为她在此之前就明确地告诉我，她不喜欢我，所以这注定是一个悲伤的故事。一个月前，当她的同学从公司离职的时候，她就告诉我或许某一天她就离开这里了，当时我说我会想她的，而当她真正要离开的时候，我甚至都想不明白自己为什么会如此平静。下班路上同事 Kent 若有所思的告诉我，在我这个年龄“一见钟情”这种满足初恋情结的机会会越来越少，与其在感情的创伤中持续失落不如努力去争取一段新的开始。我惊诧于他突然间发现我隐藏在心底的秘密，我更加纠结于我丧失了喜欢一个人的能力。&lt;/p></description></item><item><title>浅析 WPF 中 MVVM 模式下命令与委托的关系</title><link>https://blog.yuanpei.me/posts/569337285/</link><pubDate>Thu, 21 Jul 2016 14:27:07 +0000</pubDate><guid>https://blog.yuanpei.me/posts/569337285/</guid><description>&lt;p>各位朋友大家好，我是 Payne，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。最近因为项目上的原因开始接触 WPF，或许这样一个在现在来讲显得过时的东西，我猜大家不会有兴趣去了解，可是你不会明白对某些保守的项目来讲，安全性比先进性更为重要，所以当你发现银行这类机构还在使用各种“复古”的软件系统的时候，你应该相信这类东西的确有它们存在的意义。与此同时，你会更加深刻地明白一个道理：技术是否先进性和其流行程度本身并无直接联系。由此我们可以推论出：一项不流行的技术不一定是因为它本身技术不先进，或许仅仅是因为它无法满足商业化的需求而已。我这里的确是在说 WPF,MVVM 思想最早由 WPF 提出，然而其发扬光大却是因为前端领域近年来比较热的 AngularJS 和 Vue.js，我们这里表达的一个观点是：很多你以为非常新潮的概念，或许仅仅是被人们重新赋予了新的名字，当你理清这一切的来龙去脉以后，你会发现这一切并没有什么不同。这符合我一贯的主张：去发现问题的实质、不要被框架束缚、通过共性来消除差异，所以在今天这篇文章里，我想说说 WPF 中 MVVM 模式下命令与委托的关系。&lt;/p></description></item><item><title>在 Unity3D 中使用 uGUI 实现 3D 旋转特效</title><link>https://blog.yuanpei.me/posts/1150143610/</link><pubDate>Sun, 10 Jul 2016 14:29:33 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1150143610/</guid><description>&lt;p>各位朋友大家好，欢迎大家关注我的博客，我是 Payne，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。最近一位朋友问我，如何在 Unity 引擎中实现类似《英雄联盟》中选择皮肤时的 3D 滚动视图效果，虽然我非常不喜欢这个游戏，可是大学四年在宿舍里被周围同学们耳濡目染，对这个游戏中常见英雄的口头禅还是颇为熟悉的，曾经在周围同学的“硝烟”和“噪杂”中熬夜编程，此时此刻想起来大概是最能让我怀念和骄傲的记忆了。剑圣说“你的剑就是我的剑”，伊泽瑞尔说“是时候表演真正的技术了”，杰斯说“为了更美好的明天而战”……或许曾经的某一瞬间，我们曾经有过类似的让你我疯狂着迷的人生信条，可是不管怎样，我希望我们可以将这些永远地铭刻在心里，如同心中栽种下一棵红莲，在黑夜中静静地等待开放，这样当此去经年亦或时过境迁的时候，我们不会说是时光抹去了你我年轻的棱角，因为我相信真正的棱角会因为磨砺而变得更加明亮，绝对不会因为此刻的苟且就变的麻木甚至迷茫。好了，喝完我这碗心灵鸡汤，下面我们来一起学习如何在 Unity3D 中使用 uGUI 实现 3D 滚动视图效果。&lt;/p></description></item><item><title>Unity3D 游戏开发之在 uGUI 中使用不规则精灵制作按钮</title><link>https://blog.yuanpei.me/posts/1190622881/</link><pubDate>Fri, 08 Jul 2016 21:58:39 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1190622881/</guid><description>&lt;p>各位朋友大家好，欢迎关注我的博客，我的博客地址是：&lt;a href="https://blog.yuanpei.me">https://blog.yuanpei.me&lt;/a>。最近因为受到工作上业务因素影响，所以博主在 Unity 引擎上的研究有所停滞。虽然目前的工作内容和 Unity3D 没有直接的关联，可是我觉得工程师应该有这样一种情怀，即工作和兴趣是完全不同的两个概念。编程对我而言，首先是一种兴趣，其次是一份工作。所以我宁愿在每天下班以后继续研究自己感兴趣的东西，而非为了取悦这个世界、为了加班而加班。最近广电总局让整个游戏行业都坐立不安了，因为其新发布的一系列规定，让中国的独立游戏开发者怨声载道。可是我们更应该看到积极的一面是，无数的小游戏公司会在最近数月内大量消失，或许对中国野蛮生长的游戏行业这是一次“形式”上的整顿，可对我们开发者来说，在这个过程中努力提升自我、巩固基础永远比追求时髦、流行的技术或者框架有意义的多，因为热闹的从来都是昙花一现般的璀璨，而永恒的永远都是历久弥新的真理。好了，闲言少叙，今天我们的话题是在 uGUI 中使用不规则精灵制作按钮。&lt;/p></description></item><item><title>使用 C#开发 HTTP 服务器系列之实现 Get 和 Post</title><link>https://blog.yuanpei.me/posts/1700650235/</link><pubDate>Sat, 11 Jun 2016 15:01:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1700650235/</guid><description>&lt;p>各位朋友大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。在我们这个 Web 服务器有了一个基本的门面以后，我们是时候来用它做点实际的事情了。还记得我们最早提到 HTTP 协议的用途是什么吗？它叫超文本传输协议啊，所以我们必须考虑让我们的服务器能够接收到客户端传来的数据。因为我们目前完成了大部分的工作，所以对数据传输这个问题我们这里选择以最简单的 GET 和 POST 为例来实现，这样我们今天的重点就落实在 Get 和 Post 的实现这个问题上来。而从原理上来讲，无论 Get 方式请求还是 Post 方式请求，我们都可以在请求报文中获得其请求参数，不同的是前者出现在请求行中，而后者出现在消息体中。例如我们传递的两个参数 num1 和 num2 对应的数值分别是 12 和 24，那么在具体的请求报文中我们都能找到类似“num1=12&amp;amp;num2=24”这样的字符结构，所以只要针对这个字符结构进行解析，就可以获得客户端传递给服务器的参数啦。&lt;/p></description></item><item><title>使用 C#开发 HTTP 服务器系列之更简单的实现方式</title><link>https://blog.yuanpei.me/posts/3603924376/</link><pubDate>Sat, 11 Jun 2016 15:01:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3603924376/</guid><description>&lt;p>各位朋友大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。到目前为止，我已经发布了 3 篇 HTTP 服务器开发的系列文章。对我个人而言，我非常享受这个从无到有的过程，或许我现在写的这个 Web 服务器有各种不完美的因素，可是当有一天我需要一个轻量级的服务器的时候，我在无形中是不是比别人多了一种选择呢？我们常常提到“不要重复造轮子”，可事实上这并不能成为我们“不造轮子”的理由，虽然我们有各种各样的服务器软件、有各种各样的服务端框架可以供我们选择，可是在动手写这个系列文章前，我对 Web 服务器的印象无非是因为我是用 LAWP(Linux + Apache + MySQL + PHP)搭建过 Wordpress 博客而已。虽然在对动态页面(如.aspx、.jsp、.php 等)的处理上，可能会和静态页面有所不同，但是我庆幸我了解了这个过程以及它的内部原理，这种跨语言、跨平台的设计思路是任何框架或者标准都无法告诉我的。或许有人会问我，为什么不在最开始的时候就选择更简单的实现方法，那么在这篇文章中你将会找到答案。&lt;/p></description></item><item><title>使用 C#开发 HTTP 服务器系列之静态页面</title><link>https://blog.yuanpei.me/posts/3695777215/</link><pubDate>Sat, 11 Jun 2016 15:01:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3695777215/</guid><description>&lt;p>各位朋友大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。在这个系列文章的第一篇中，我们着重认识和了解了 HTTP 协议，并在此基础上实现了一个可交互的 Web 服务器，即当客户端访问该服务器的时候，服务器能够返回并输出一个简单的“Hello World”。现在这个服务器看起来非常简陋，为此我们需要在这个基础上继续开展工作。今天我们希望为这个服务器增加主页支持，即当我们访问这个服务器的时候，它可以向我们展示一个定制化的服务器主页。通常情况下网站的主页被定义为 index.html，而在动态网站技术中它可以被定义为 index.php。了解这些将有助于帮助我们认识 Web 技术的实质，为了方便我们这里的研究，我们以最简单的静态页面为例。&lt;/p></description></item><item><title>使用C#开发HTTP服务器系列之构建RESTful API</title><link>https://blog.yuanpei.me/posts/3637847962/</link><pubDate>Sat, 11 Jun 2016 15:01:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3637847962/</guid><description>&lt;p>  各位朋友大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。到目前为止，“使用C#开发HTTP服务器”这个系列系列文章目前已经接近尾声了，虽然我们在服务器功能的完整性(如支持并发、缓存、异步、Htts等)上没有再继续深入下去，可是我们现在已经具备了一个基本的服务器框架啦，所以更多深层次的问题就需要大家根据自己的需要来扩展了，因为写博客更多的是一种“记录-输出-反馈”的一个过程，所以我更希望大家在看完我的博客后能对我有所反馈，因为抄博客上的代码实在是太无聊啦！好了，保持愉悦的心情我们下面来引出今天的话题：构建RESTful API。RESTful API，这个概念或许你曾经听说过，可能它和我们所熟悉的各种Web息息相关，甚至在某种意义上来讲它并不是一种新的技术，而这一切的一切归根到底都是在问一个问题，即网站真的是Web的唯一形态吗？&lt;/p></description></item><item><title>使用 C# 开发 HTTP 服务器系列之 Hello World</title><link>https://blog.yuanpei.me/posts/3040357134/</link><pubDate>Sat, 11 Jun 2016 12:38:03 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3040357134/</guid><description>&lt;p>各位朋友大家好，我是秦元培，欢迎大家关注我的博客。从今天起，我将开始撰写一组关于 HTTP 服务器开发的系列文章。我为什么会有这样的想法呢？因为人们对 Web 技术存在误解，认为网站开发是 Web 技术的全部。其实在今天这样一个时代，Web 技术可谓是无处不在，无论是传统软件开发还是移动应用开发都离不开 Web 技术，所以在我的认识中，任何使用了 HTTP 协议实现数据交互都可以认为是 Web 技术的一种体现，而且当我们提及服务器开发的时候，我们常常提及 Java 或者 PHP。可是这些重要吗？不，在我看来服务器开发和语言无关，和 IIS、Tomcat、Apache、Ngnix 等等我们熟知的服务器软件无关。Web 技术可以像一个网站一样通过浏览器来访问，同样可以像一个服务一样通过程序来调用，所以在接下来的时间里，我将和大家一起见证如何使用 C# 开发一个基本的 HTTP 服务器，希望通过这些能够让大家更好的认识 Web 技术。&lt;/p></description></item><item><title>扫描二维码在移动设备上浏览响应式页面</title><link>https://blog.yuanpei.me/posts/2158696176/</link><pubDate>Sun, 01 May 2016 10:58:18 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2158696176/</guid><description>&lt;p>最近想尝试对一个 Ghost 博客主题进行移植，因为对一个后端程序员来说，进行前端方面的工作实在是个不小的挑战，而我对 CSS 更是有种与生俱来的恐惧感，所以我是非常喜欢&lt;a href="http://www.bootcss.com/">Bootstrap&lt;/a>和&lt;a href="http://materializecss.com/">Materilize&lt;/a>这种对后端程序员友好的前端框架。现在前端技术如火如荼，而前端技术作为最有可能实现跨平台技术的技术形态，相对原生技术有着更为灵活的适应性和扩展性，因此以响应式设计为代表的 Web 技术，能够让 Web 页面在不同尺寸屏幕上都有着相近的体验，因为目前软件开发基本都是在计算机设备上来完成的，这样我们在制作 Web 页面的时候就需要在不同的设备上进行调试，如果每次都将 Web 页面部署到远程服务器上，这样将浪费大量的时间而且容易将测试阶段的问题暴露给用户，因此本文将采用一种扫描二维码的方式来实现在移动设备上浏览响应式页面。&lt;/p></description></item><item><title>使用 Mono 打造轻量级的.NET 程序运行时</title><link>https://blog.yuanpei.me/posts/907824546/</link><pubDate>Fri, 25 Mar 2016 12:47:58 +0000</pubDate><guid>https://blog.yuanpei.me/posts/907824546/</guid><description>&lt;p>在&lt;a href=".">使用 Mono 让.NET 程序跨平台运行&lt;/a>这篇文章中，我们已经对 Mono 以及.NET 程序的运行机制有了初步的理解。今天我想来谈谈&amp;quot;使用 Mono 打造轻量级的.NET 运行时&amp;quot;这样一个话题。为什么我会有这样一种想法呢？因为 Mono 和.NET 都可以执行 IL 代码，所以我用 Mono 来作为.NET 程序的运行时是一个顺理成章的想法。由于.NET 程序需要.NET Framework 提供运行支持，所以当目标设备没有安装.NET Framework 或者.NET Framework 版本不对的时候，我们的程序都无法顺利运行。强迫用户安装.NET 框架无疑会影响用户体验，在 Windows XP 尚未停止服务前，国内软件厂商为了兼容这些用户，通常会选择 C++这类语言来编写原生应用，这就造成了国内.NET 技术长期不被重视的现状。&lt;/p></description></item><item><title>使用 Mono 让.NET 程序跨平台运行</title><link>https://blog.yuanpei.me/posts/1836680899/</link><pubDate>Sun, 06 Mar 2016 12:20:09 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1836680899/</guid><description>&lt;p>众所周知，Unity3D 引擎凭借着强大的跨平台能力而备受开发者的青睐，在跨平台应用开发渐渐成为主流的今天，具备跨平台开发能力对程序员来说就显得特别重要。传统的针对不同平台进行开发的方式常常让开发者顾此失彼，难以保证应用程序在不同的平台都有着相同的、出色的体验，这种情况下寻找到一种跨平台开发的方式将会为解决这个问题找到一种思路。从目前的开发环境来看，Web 应该是最有可能成为跨平台开发的神兵利器，可是长期以来 Web 开发中前端和后端都有各自不同的工作流，虽然现在出现了前端和后端逐渐融合的趋势，可在博主看来想让 Web 开发变得像传统开发这样简单还需要一定的过渡期。&lt;/p></description></item><item><title>在 Unity3D 中基于订阅者模式实现事件机制</title><link>https://blog.yuanpei.me/posts/632291273/</link><pubDate>Fri, 15 Jan 2016 12:30:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/632291273/</guid><description>&lt;p>各位朋友，大家好，欢迎大家关注我的博客，我是秦元培，我的博客地址是：&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。今天博主想和大家分享的是在 Unity3D 中基于订阅者模式实现消息传递机制，我们知道 Unity3D 中默认提供了一种消息传递机制 SendMessage，虽然 SendMessage 使用起来的确非常简单，可是它的这种简单是建立在付出一定的代价的基础上的。经常有朋友提及不同的模块间如何进行通信的问题，可能答案最终会落到&lt;strong>单例模式&lt;/strong>、&lt;strong>委托&lt;/strong>和&lt;strong>事件机制&lt;/strong>这些关键词上，在这种情况下本文所探讨的内容可能会帮助你找到最终的答案。&lt;/p></description></item><item><title>扩展 Unity3D 编辑器的脚本模板</title><link>https://blog.yuanpei.me/posts/3653662258/</link><pubDate>Fri, 08 Jan 2016 13:58:44 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3653662258/</guid><description>&lt;p>最近在学习 Shader 时感觉 Shader 语言参数众多、语法诡异，如果每次都从头开始写 Shader 一定是一件痛苦的事情。如果可以在本地定义好一组标准的 Shader 模板，这样当我们需要实现某些效果类似的 Shader 时，就可以在这个 Shader 模板的基础上进行修改。因为 Shader 文件是一个文本文件，所以我们可以非常容易地创建这样一个模板，在这个模板中我们可以进一步完善相关的参数注释，这样就不用每次写 Shader 的时候都需要查文档了，从这个角度出发，就进入了这篇文章的正题：扩展 Unity3D 编辑器的脚本模板。&lt;/p></description></item><item><title>《Cg Programming in Unity》读书笔记</title><link>https://blog.yuanpei.me/posts/1670305415/</link><pubDate>Fri, 25 Dec 2015 12:29:20 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1670305415/</guid><description>&lt;p>  最近开始着手Shader语言的学习，因为Unity3D没有提供类似虚幻四引擎的材质编辑器功能，所以当在Unity3D中碰到需要提供引擎默认材质以外的效果的时候，就需要我们来编写Shader以实现各种特效，本文主要是结合《Cg Programming in Unity》这本书和&lt;a href="http://blog.csdn.net/poem_qianmo/article/details/49405909">浅墨&lt;/a>博客中关于Shader的这部分内容来学习和整理，目的是帮助博主快速掌握Shader语言。&lt;/p></description></item><item><title>EasyAR 尝鲜系列教程之视频播放功能的实现</title><link>https://blog.yuanpei.me/posts/316230277/</link><pubDate>Wed, 09 Dec 2015 08:40:22 +0000</pubDate><guid>https://blog.yuanpei.me/posts/316230277/</guid><description>&lt;p>各位朋友大家好，欢迎大家关注我的博客，我是秦元培，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。到现在为止，我们对 EasyAR 中的 ImageTarget 基本上可以说是驾轻就熟了，因此我们这个系列教程可以说是接近尾声了。博主第一次接触 AR 这个概念是在大学时候读到一本讲解计算机图形视觉的书籍里，相对 VR 技术目前华而不实的市场现状，AR 技术从实用性和成熟度都能得到较好的保证。可是大家都清楚这些技术背后都是建立在复杂而高深的图形学算法的基础上的，如果想学习 AR 技术请回归计算机图形学的本源，这就和学习游戏技术要追寻可编程渲染管线是一样的，所以这个系列完全是博主个人的兴趣使然，希望了解这个技术的可以进行更加深入的探索。这次我们来说说 VideoTarget 如何实现吧！&lt;/p></description></item><item><title>EasyAR 尝鲜系列教程之 ImageTarget 千呼万唤始出来</title><link>https://blog.yuanpei.me/posts/3736599391/</link><pubDate>Wed, 09 Dec 2015 08:39:54 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3736599391/</guid><description>&lt;p>各位朋友大家好，欢迎大家关注我的博客，我是秦元培，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。最近 EasyAR 终于迎来了一次重大的版本更新：v1.10，真可谓是“千呼万唤始出来”啊，所以在官方文档和示例项目基本完善的情况下，博主决定将 EasyAR 尝鲜系列教程继续下去。本次教程主要以官方新发布的 Unity 示例项目为基础来进行讲解，关注 Androis/iOS 原生应用开发的朋友请自行针对官方示例项目进行研究。好了，今天主要的内容是通过 EasyAR SDK 来自行构建一个 ImageTarget 的实例，采用 Unity3D 4.6.4 版本进行开发。&lt;/p></description></item><item><title>C# 中的扩展方法学习总结</title><link>https://blog.yuanpei.me/posts/305484621/</link><pubDate>Sat, 05 Dec 2015 12:01:02 +0000</pubDate><guid>https://blog.yuanpei.me/posts/305484621/</guid><description>&lt;p>各位朋友大家好，我是秦元培，欢迎大家关注我的博客。最近偶然接触到了 C#中的扩展方法，觉得这个语法特性是一个不错的特性，因此决定在这里系统地对 C#中的扩展方法相关内容进行下总结和整理，因为博主觉得学习这件事情本身就是一个积累的过程，所以博主有时候会对现在的线上培训和视频教程这种“在线教育”感到反感。试想《射雕英雄传》中江南七怪远赴大漠传授郭靖武艺苦历十八载，何以难及全真教丹阳子马钰传授内功两年的积累？这里固然有郭靖愚笨木讷的天性和江南七怪武功低微的因素，可是在博主看来更重要的是强调了一个积累。想郭靖一生受益自全真教的玄门内功终成一代“为国为民”的侠之大者，则我辈需更加努力方可在这世间行走奔波。&lt;/p></description></item><item><title>青黄未接的2015</title><link>https://blog.yuanpei.me/posts/1394521917/</link><pubDate>Tue, 01 Dec 2015 19:24:18 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1394521917/</guid><description>&lt;p>  匆匆时光总是把我们这些过客留在感慨和叹息中，而它却如风之旅人一般渐行远去。转眼这大半年的时间里，每天我都在努力让时间发挥它的意义，可是有时候这种努力却像枷锁一样让我有些莫名的压抑。从毕业那天起，我就决定这辈子不会再靠我的本科专业生活，因为它从来没有和我的内心发生过强烈的共鸣，所以当我毕业以后就意味着我再没有回头的路可以走。曾经因为怯懦而将自己封闭在这座小城市，其结果就是我在我人生中的第一家公司的项目在拖延和等待中慢慢地死亡。&lt;/p></description></item><item><title>Unity3D 游戏开发之 C++ 插件接入</title><link>https://blog.yuanpei.me/posts/2527231326/</link><pubDate>Sat, 21 Nov 2015 14:47:26 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2527231326/</guid><description>&lt;p>各位朋友大家好，我是&lt;strong>秦元培&lt;/strong>，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。虽然 Unity3D 引擎依靠强大的跨平台能力睥睨高手林立的游戏引擎世界，我们在使用 Unity3D 游戏引擎的时候基本上不会去接触底层的东西，可是有时候面对某些奇葩的要求的时候，我们就不得不考虑使用 C++这样的语言来为其编写相关的插件。你如果问我是什么样的奇葩要求，比如接入蓝牙手柄来控制游戏、接入类似街机的设备来控制游戏、接入同一个游戏到两个不同的设备上并响应不同的控制……诸如此类的种种问题，可能目前在 Unity3D 引擎中找不到解决方案，这个时候写 C++插件就变成了一种刚性需求，这就是我们今天要来一起探讨的问题。&lt;/p></description></item><item><title>在 Hexo 中为文章自动添加版权信息声明模块</title><link>https://blog.yuanpei.me/posts/2950334112/</link><pubDate>Sun, 15 Nov 2015 13:12:22 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2950334112/</guid><description>&lt;p>各位朋友，大家好，欢迎大家关注我的博客，我是秦元培，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。今天想和大家说说博客文章版权这件事情。每当提到版权的时候，我知道大家内心深处都是对此不以为然的，因为国内版权意识薄弱，所以版权在我们的眼中就变成了这样一件可有可无的东西，可是事实真的是这样的吗？首先我们必须承认一件事情，即你从互联网上获得的知识都是有价值的，即使这些知识的创造者并未因此而获得利益。&lt;/p></description></item><item><title>解析 OBJ 模型并将其加载到 Unity3D 场景中</title><link>https://blog.yuanpei.me/posts/1124152964/</link><pubDate>Sun, 15 Nov 2015 13:07:57 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1124152964/</guid><description>&lt;p>各位朋友，大家好，欢迎大家关注我的博客，我是&lt;strong>秦元培&lt;/strong>，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。今天想和大家交流的是解析 obj 模型并将其加载到 Unity3D 场景中，虽然我们知道 Unity3D 是可以直接导入 OBJ 模型的，可是有时候我们并不能保证我们目标客户知道如何使用 Unity3D 的这套制作流程，可能对方最终提供给我们的就是一个模型文件而已，所以这个在这里做这个尝试想想还是蛮有趣的呢，既然如此，我们就选择在所有 3D 模型格式中最为简单的 OBJ 模型来一起探讨这个问题吧！&lt;/p></description></item><item><title>Unity3D 游戏开发之分页效果在 uGUI 中的实现</title><link>https://blog.yuanpei.me/posts/166983157/</link><pubDate>Tue, 10 Nov 2015 20:46:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/166983157/</guid><description>&lt;p>各位朋友大家好，我是&lt;strong>秦元培&lt;/strong>，欢迎大家关注我的博客，我的博客地址是：&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。今天想和大家分享的是 uGUI 中分页效果的实现，我们知道相对 NGUI 来说 uGUI 在功能覆盖上来讲，它并没有像 NGUI 那样提供较为丰富和炫酷的组件，可是因为 uGUI 有着较好的扩展性，因此我们可以通过编写脚本来扩展它的功能。虽然在移动开发时代以开发速度论成败，可是这并不是我们“不求甚解”的正当理由。每次看到 NGUI 各种&amp;quot;丰富&amp;quot;的组件在脑海中打转的时候，每次看到编译项目时弹出各种 Warming 的时候，我内心是如此地期望有这样一个简单高效的 UI 系统啊，直到有一天我遇上了 uGUI。&lt;/p></description></item><item><title>EasyAR 尝鲜系列教程之自定义 Marker 的实现</title><link>https://blog.yuanpei.me/posts/1156673678/</link><pubDate>Tue, 03 Nov 2015 10:23:14 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1156673678/</guid><description>&lt;p>各位朋友大家好，欢迎大家关注我的博客，我是&lt;strong>秦元培&lt;/strong>，我的博客地址是：&lt;a href="http://blog.yuanpei.me">http://blog.yuanpei.me&lt;/a>。通过本系列第一篇文章，我们初步了解了 EasyAR 这个增强现实引擎，这次我们来尝试自己定义一个 Marker，这样我们就可以用自己喜欢的图片来作为 Marker。因为目前 EasyAR 文档并不完善，所以下面的这些内容可能更多的是我个人的尝试和探索。如果大家对此感兴趣的话继续往下看否则就不要往下看了，因为我担心在官方正式文档出来以后大家可能会骂我啊。好了，对这个话题感兴趣的朋友就请继续往下看吧！&lt;/p></description></item><item><title>EasyAR尝鲜系列教程之Hello EasyAR</title><link>https://blog.yuanpei.me/posts/3120185261/</link><pubDate>Fri, 30 Oct 2015 09:44:18 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3120185261/</guid><description>&lt;p>各位朋友，大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。从今天起博主将为大家带来EasyAR尝鲜系列教程，本教程适用的对象是增强现实应用开发者和Unity3D游戏开发者，在阅读本教程前请确保具备增强现实应用开发及Unity3D游戏开发的相关基础知识。在本节及后续内容中，博主将以国产增强现实引擎EasyAR为主要开发平台来带领大家一起走进增强现实应用开发的世界，希望大家能够喜欢！&lt;/p></description></item><item><title>教你一步步实现一个虚拟摇杆</title><link>https://blog.yuanpei.me/posts/331752533/</link><pubDate>Fri, 30 Oct 2015 09:44:18 +0000</pubDate><guid>https://blog.yuanpei.me/posts/331752533/</guid><description>&lt;p>各位朋友，大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。最近因为项目需要决定尝试自己来实现一个虚拟摇杆，所以在今天的文章中我们的目标是使用 uGUI 来制作一个可以在移动平台稳定运行的虚拟摇杆(请不要问我为什么不使用 NGUI 来实现，你说我做个虚拟摇杆有必要在项目里导入那么多的资源嘛 23333)。关于使用第三方插件来实现虚拟摇杆，请大家参照我以前写的文章&lt;a href="http://blog.csdn.net/qinyuanpei/article/details/26204177">Unity3D 游戏开发之使用 EasyTouch 虚拟摇杆控制人物移动&lt;/a>，在这里就不再赘述了。&lt;/p></description></item><item><title>Unity3D 游戏开发之 Unity3D 场景编辑器扩展开发</title><link>https://blog.yuanpei.me/posts/3019914405/</link><pubDate>Tue, 13 Oct 2015 12:59:01 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3019914405/</guid><description>&lt;p>今天博主想和大家分享的是 Unity3D 场景编辑器的扩展开发，相关的话题我们在&lt;a href="http://localhost:4000/2015/03/31/unity3d-plugins-development-application-case/">Unity3D 游戏开发之编辑器扩展程序开发实例&lt;/a>这篇文章中我们已经有所涉及，今天博主想特别针对场景编辑器的扩展开发来进行下深入研究。对于一个场景编辑器来说，它主要的作用是 3D 场景视图中实时显示、输入反馈和相关信息的更新。在 Unity3D 中提供了 Editor、EditorWindow、GUILayout、EditorGUILayout、GUIUtility、EditorGUIUtility、Handles、Event 等来完成这些工作。其中基于 EditorWindow 的这种扩展方式我们已经研究过了，这种扩展方式拥有自己的独立窗口使用 OnGUI 方法进行界面的绘制。&lt;/p></description></item><item><title>在 Unity3D 中加载外部图片的两种方法</title><link>https://blog.yuanpei.me/posts/821259985/</link><pubDate>Thu, 08 Oct 2015 15:03:01 +0000</pubDate><guid>https://blog.yuanpei.me/posts/821259985/</guid><description>&lt;p>各位朋友大家好，我是秦元培，欢迎大家关注我的博客。最近在做项目的过程中遇到这样的一个需求：玩家可以在游戏过程中进行实时存档，在存档过程中会保存当前游戏进度，同时会截取当前游戏画面并加载到游戏存档界面中。当下一次进入游戏的时候，将读取本地存档图片并加载到游戏界面中。这在单机游戏中是特别常见的一种功能，这里主要有两个关键点。首先是截取游戏画面，这个问题大家可以在&lt;a href="http://blog.csdn.net/qinyuanpei/article/details/39185195">《Unity3D 游戏开发之截屏保存精彩瞬间》&lt;/a>这篇文章中找到答案。其次是从本地加载图片，因为这里要保证可读可写，因此传统的 Resources.Load()方式和 AssetBundle 方式均无法实现这样的功能。那么怎样从外部加载图片到游戏中，这就是我们今天要讨论的内容啦。好了，这里介绍两种方法来实现这一目的。&lt;/p></description></item><item><title>做最初的自己</title><link>https://blog.yuanpei.me/posts/786195243/</link><pubDate>Wed, 30 Sep 2015 10:19:26 +0000</pubDate><guid>https://blog.yuanpei.me/posts/786195243/</guid><description>&lt;p>在中秋节这样一个万家团圆的日子，我却再度因为工作的问题和家人发生争执。发生争执的原因简单到习以为常，家人喜欢稳定、安逸的生活，而我却喜欢有挑战、梦想的生活。我不知道梦想对一个二十三岁的人是不是一种奢侈品，我只知道当我住在狭小、拥挤的出租房里的时候，我想努力去拥有一个温暖的家，我不想靠着一张嘴去哗众取宠，我不想刻意地迎合和奉承这个世界，我只想靠我平凡而微薄的努力让我的生活一天天地温暖起来。从小我被告诉要做一个正直、善良的人，可是随着我慢慢地长大，在我的耳边总会听到“要去适应这个社会”这样的话，然而最讽刺的是这样的话常常出自同一人的口中。&lt;/p></description></item><item><title>Unity3D 游戏场景优化系列之批处理</title><link>https://blog.yuanpei.me/posts/927393529/</link><pubDate>Mon, 07 Sep 2015 10:59:13 +0000</pubDate><guid>https://blog.yuanpei.me/posts/927393529/</guid><description>&lt;p>各位朋友大家好，我是&lt;strong>秦元培&lt;/strong>，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。最近开始研究 Unity3D 游戏场景优化，每次提及游戏优化这个话题的时候，我的脑海中都会浮现出《仙剑奇侠传六》这个让四路泰坦都光荣陨落的神奇游戏，作为一个使用 Unity3D 引擎进行游戏开发的仙剑玩家，我曾经天真的以为，这款使用 Unity3D 引擎打造的仙剑二十周年献礼之作，会让我对《仙剑奇侠传》这个系列游戏的未来充满更多期待，然而当游戏真正呈现在我眼前的时候，我感受到了在历代仙剑游戏中从未有过的尴尬和失望，我尴尬的是 Unity3D 这样一个比较强大的游戏引擎硬生生地被北软玩成了这个鬼样子，我失望的是这部游戏除了剧情和跳跳乐以外并没有什么让人看到希望的东西。&lt;/p></description></item><item><title>Unity3D 游戏开发游戏读/存档在 Unity3D 中的实现</title><link>https://blog.yuanpei.me/posts/887585917/</link><pubDate>Thu, 20 Aug 2015 08:57:10 +0000</pubDate><guid>https://blog.yuanpei.me/posts/887585917/</guid><description>&lt;p>大家好，我是秦元培，欢迎大家关注我的博客。近期博客的更新频率基本直降到冰点，因为这段时间实在是忙得没有时间来写博客了。今天想和大家分享的内容是 RPG 游戏中游戏存档的实现，因为最近在做一个 RPG 游戏的项目，所以遇到这个问题就随时记录下来，在对知识进行总结的同时可以将这种思路或者想法分享给大家，这是一件快乐而幸运的事情。我讨厌写按部就班的技术教程，因为我觉得学习是一种自我的探索行为，如果一切都告诉你了，探索的过程便会变得没有意义了。&lt;/p></description></item><item><title>SDL 游戏开发系列第二话：基本图形的绘制</title><link>https://blog.yuanpei.me/posts/3789971938/</link><pubDate>Mon, 27 Jul 2015 08:48:59 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3789971938/</guid><description>&lt;p>各位朋友，大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是：&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。话题紧接上回，在上回我们讲到了 SDL 的下载、安装和配置并对 SDL 游戏有了初步的了解。我们知道游戏开发中最为基础的内容是图形的绘制，因此在我们学习 SDL 游戏开发的过程中我们同样要从最简单的图形绘制开始学习。在 2D 游戏开发中，精灵（Sprite）是一个基础而核心的内容，具体来讲精灵首先是一张 2D 图片，精灵的绘制从本质上是图片的绘制，所以这是一个基础的内容。因为精灵在 2D 游戏中承担着 GameObject 的重要角色，所以一个图形引擎对精灵的支持好坏会决定游戏设计的最终效果。今天这篇文章主要是通过使用 SDL 中的 SDL_LoadBMP()、SDL_CreateTextureFromSurface()和 SDL_RenderCopy()这三个方法来实现在 SDL 中基本图形的绘制，从整体上尚属较为简单的内容。可是从学习 SDL 游戏开发的角度来看，一切都值得我们深入地去研究。好了，这就开始吧！&lt;/p></description></item><item><title>SDL 游戏开发系列第一话：Hello SDL</title><link>https://blog.yuanpei.me/posts/183718218/</link><pubDate>Sat, 25 Jul 2015 15:19:01 +0000</pubDate><guid>https://blog.yuanpei.me/posts/183718218/</guid><description>&lt;p>各位读者朋友大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。从今天起博主将带领大家一起走进 SDL 游戏开发的世界，如果说此前的 Unity3D 游戏开发系列文章让大家感受到的是游戏引擎工具化开发的方便与快捷，那么这一次就让我们以 SDL 库为基础，通过了解游戏开发中的底层图形渲染、输入事件响应等内容来全面认识游戏引擎，博主为 SDL 游戏开发系列文章建立了专栏，大家可以通过&lt;a href="http://blog.csdn.net/column/details/sdlgame.html">这里&lt;/a>获取所有的系列文章，希望大家能够喜欢！好了，作为&lt;a href="http://blog.csdn.net/column/details/sdlgame.html"> SDL 游戏开发系列&lt;/a>的第一篇文章，按照技术性文章写作的国际惯例这将是一篇介绍 SDL 入门内容的文章，因此这篇文章叫做：Hello SDL。&lt;/p></description></item><item><title>《仙剑奇侠传六》游戏感言</title><link>https://blog.yuanpei.me/posts/1118169753/</link><pubDate>Fri, 24 Jul 2015 09:21:20 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1118169753/</guid><description>&lt;p>  目前游戏主线剧情进行到 50%左右，在游戏尚未通关前，我对于这一部游戏的感觉始终是一种说不清道不明的情感，作为仙剑系列中唯一一部，从项目立项到宣传曝光再到游戏上市整个过程中持续关注的游戏，它可以说是承载了无数玩家的期待和祝福。和大部分玩家一样，在游戏曝光的第一时刻我们曾经热火朝天地讨论过各种各样可能的设定、曾经为这部游戏的系统玩家想过各种各样的尝试，然而当我面对这款游戏的时候，我的内心平静得像一潭死水。我今天 23 岁，刚刚从大学毕业的我本应该还没有被这个社会完全改变，可我不知道是我变了还是仙剑变了，这一次打开仙剑的时候，我总有一种恍若隔世的恍惚感。&lt;/p></description></item><item><title>Unity3D 游戏开发之 SQLite 让数据库开发更简单</title><link>https://blog.yuanpei.me/posts/582264328/</link><pubDate>Thu, 09 Jul 2015 09:47:06 +0000</pubDate><guid>https://blog.yuanpei.me/posts/582264328/</guid><description>&lt;p>各位朋友大家好，欢迎大家关注我的博客，我是秦元培，我是博客地址是&lt;a href="http://blog.csdn.net/qinyuanpei">http://blog.csdn.net/qinyuanpei&lt;/a>。在经历了一段时间的忙碌后，博主终于有时间来研究新的东西啦，今天博客向和大家一起交流的内容是在 Unity3D 游戏开发中使用 SQLite 进行数据库开发，坦白来讲，在我的技术体系中 Web 和数据库是相对薄弱的两个部分，因此正好这段时间项目需要和服务器、数据库进行交互，因此在接下来的文章中博主可能会更加倾向于讲解这方面的内容，希望大家能够喜欢啊！&lt;/p></description></item><item><title>Unity3D 游戏开发之从 Unity3D 项目版本控制说起</title><link>https://blog.yuanpei.me/posts/1320325685/</link><pubDate>Thu, 02 Jul 2015 09:35:42 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1320325685/</guid><description>&lt;p>各位朋友，大家好，欢迎大家关注我的博客，我是秦元培，我的独立博客地址是：&lt;a href="http://qinyuanpei.com">http://blog.yuanpei.me&lt;/a>、CSDN 博客地址是：&lt;a href="http://blog.csdn.net/qinyuanpei">http://blog.csdn.net/qinyuanpei&lt;/a>。今天我想和大家聊聊 Unity3D 游戏项目的版本控制。&lt;/p></description></item><item><title>Unity3D 游戏开发之路：一月工作总结</title><link>https://blog.yuanpei.me/posts/1059499448/</link><pubDate>Wed, 24 Jun 2015 07:42:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1059499448/</guid><description>&lt;p>不知不觉已经在公司上班一个月了，在这一个月里每一天发生的事情是我平凡而普通的生活。作为一名有节操的程序员，当我大学的同学开始称我为程序员的时候，我知道我即将在这条路上踏下一个属于开始的足迹。和我大学的同学相比，可能我会显得幸运而孤独吧！我不用像他们一样到各种工厂里采样、监测，可是与此同时我会因为离大家越来越远而感到孤独。每天下班做公交车回到住处，简单地料理着我一个人的生活，不紧不慢却永远是一个人在摸黑赶路，这是我自己选择的路，我从来不曾后悔，即使在这段时间和美术各种闹别扭，我相信这些都会是暂时的，以后总会变得越来越好。&lt;/p></description></item><item><title>Unity3D 游戏开发之使用 AssetBundle 和 Xml 实现场景的动态加载</title><link>https://blog.yuanpei.me/posts/1467630055/</link><pubDate>Mon, 15 Jun 2015 07:24:17 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1467630055/</guid><description>&lt;p>各位朋友，大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://qinyuanpei.com">http://qinyuanpei.com&lt;/a>。 今天我想和大家聊聊在 Unity3D 中关于场景的动态加载的问题。众所周知在 Unity3D 游戏开发过程中，因为受到游戏容量、平台性能和热更新等诸多因素的限制，我们可能无法将所有的游戏场景打包到项目中然后相对&amp;quot;静态&amp;quot;地加载，那么这个时候就需要我们使用动态加载的方式来将游戏场景加载到场景中。博主在研究了 Unity3D 动态加载的相关资料后发现，目前 Unity3D 中实现动态加载场景的方式主要有以下两种方式：&lt;/p></description></item><item><title>Unity3D 游戏开发之快速打造流行的关卡系统</title><link>https://blog.yuanpei.me/posts/1424645834/</link><pubDate>Thu, 11 Jun 2015 08:11:01 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1424645834/</guid><description>&lt;p>各位朋友，大家好，欢迎大家关注我的博客，我是秦元培，我的博客地址是&lt;a href="blog.csdn.net/qinyuanpei">blog.csdn.net/qinyuanpei&lt;/a>。
今天想和大家分享的是目前在移动平台上较为流行的关卡系统，关卡系统通常是单机手机游戏如《愤怒的小鸟》、《保卫萝卜》中对游戏内容的组织形式，玩家可通过已解锁的关卡(默认第一关是已解锁的)获取分数进而解锁新的关卡，或者是通过付费购买解锁新的关卡。那么好了，在今天的文章中博主将带领大家快速实现一个可扩展的关卡系统，这个实例的灵感来自博主最近的工作经历，希望对大家学习 Unity3D 游戏起到一定帮助性的作用。&lt;/p></description></item><item><title>Unity3D 游戏开发之路：一周工作总结</title><link>https://blog.yuanpei.me/posts/719322223/</link><pubDate>Thu, 11 Jun 2015 08:02:45 +0000</pubDate><guid>https://blog.yuanpei.me/posts/719322223/</guid><description>&lt;p>大家好，欢迎大家关注我的博客，我是秦元培，我的博客地址是：&lt;a href="http://blog.csdn.net/qinyuanpei">http://blog.csdn.net/qinyuanpei&lt;/a>。到公司上班已经一周了，趁着今天周末休息的时间，想将最近在工作和生活中的感受和想法写下来，因为生命就是一个不断积累、厚积薄发，最终实现自我超越的一个过程。作为第一份工作，尽管没有想象中那样理想，可我还是在很努力的工作。工作后接手的第一个项目是一个房地产的漫游展示项目，因为这家公司之前是做影视后期的，所以在决定做这个项目后，公司领导层对这个项目具体要做到什么样的效果并没有一个明确的认识，所以在项目开展前期无论是在对项目所使用的技术的熟悉程度上还是项目整体的策划上，都没有一个具体的的可操作的方案。因为公司领导是美术出身，所以从我进了公司以后，整个公司上下一直沉浸在一种加班加点赶制模型的压抑氛围当中。&lt;/p></description></item><item><title>毕业就像指尖流沙</title><link>https://blog.yuanpei.me/posts/3461518355/</link><pubDate>Sat, 16 May 2015 08:45:05 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3461518355/</guid><description>&lt;p>毕业就像指尖流沙，而我是那从指尖流过的沙子。我不知道该怎样来总结我的大学，即使我努力地寻找、努力地回避，我依然觉得大学对我而言就是这样一个讽刺的过程，曾经努力地想要摆脱这个专业最后却留了下来，最初对这个专业的热情随着时间一天天地消逝，到最后却发现自己夹在某种缝隙中左右为难。&lt;/p></description></item><item><title>Unity3D 游戏开发之 MMD For Unity 插件研究</title><link>https://blog.yuanpei.me/posts/4088452183/</link><pubDate>Sun, 19 Apr 2015 23:31:30 +0000</pubDate><guid>https://blog.yuanpei.me/posts/4088452183/</guid><description>&lt;p>  今天想来说说MMD。MMD是MikuMikuDance的简称，是由日本人樋口优开发的一组3D动画制作软件。该软件最初希望能够将3D建模软件完成的VOCALOID的初音未来等角色模型制作成可以随着音乐跳舞的动画，因此称为MMD。作者在此基础上开发了能够将歌曲让初音未来等角色歌唱的MikuMikuVoice。2011年9月11日，樋口优宣布停止MMD新版本的开发工作。不过人们对制作MMD的热情丝毫没有减少，在动漫、游戏等领域总是能够不断看到MMD的影子。例如&lt;a href="http://www.tudou.com/programs/view/qZtdjlAg34Q/?bid=03&amp;amp;pid=2&amp;amp;resourceId=51473713_03_05_02">MMD/宇月&lt;/a>和&lt;a href="http://www.tudou.com/programs/view/WxxZZOR3EEc/?resourceId=0_06_02_99">千本樱/夏侯瑾轩&lt;/a>都是较为典型的MMD。&lt;/p></description></item><item><title>在 Sublime Text3 下安装 Package Control</title><link>https://blog.yuanpei.me/posts/570137885/</link><pubDate>Fri, 17 Apr 2015 12:54:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/570137885/</guid><description>&lt;p>  Sublime Text,是这个地球上最好的代码编辑器，没有之一。因为在过去的一段时间里，我使用的版本是SublimeText2，所以听说Sublime Text3版本稳定后，决定开始尝鲜。哈哈，我就是这么一个&amp;quot;喜新厌旧&amp;quot;的人！Sublime的强大不仅仅在它优雅的外表，更为重要的是她无可匹敌的扩展性，就是说我们可以通过插件来扩展它的功能，这对于一个喜欢DIY的人来说简直是无法抗拒的诱惑。不过在接收这些诱惑前，我们需要一个工具Package Control，它是Sublime里最为基础、最为重要的插件，好了，现在问题来了，Sublime怎么安装Package Control！&lt;/p></description></item><item><title>在 Windows 下使用 Visual Studio 编译 Lua 5.3</title><link>https://blog.yuanpei.me/posts/3642630198/</link><pubDate>Thu, 16 Apr 2015 14:50:35 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3642630198/</guid><description>&lt;p>  Lua 5.3 已经发布好长时间了，可是因为 &lt;a href="http://files.luaforge.net/releases/luaforwindows/luaforwindows">Lua For Windows&lt;/a> 的 Lua 版本无法和官方保持一致，所以想尝试下编译 Lua 5.3 的源代码，因为作为一名合格的程序员，是应该要懂得编译原理的相关内容的啊(可是我真的没有学过编译原理啊!&amp;hellip;..)。好了，那么今天博主将在文章中和大家分享自己编译 Lua 5.3的过程，希望能够对大家学习和使用 Lua 有些帮助吧！&lt;/p></description></item><item><title>贝塞尔曲线学习笔记</title><link>https://blog.yuanpei.me/posts/2186770732/</link><pubDate>Wed, 08 Apr 2015 12:25:28 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2186770732/</guid><description>&lt;p>贝塞尔曲线(Bezier Curve)是由法国工程师皮埃尔·贝塞尔(Pierre Bezier)于 1962 年提出的一种曲线。在数学的数值分析领域中，贝塞尔曲线是计算机图形学中相当重要的参数曲线，其主要意义在于无论是直线还是曲线都能在数学上予以描述。最早贝塞尔曲线被用来对汽车主体进行设计，现在贝塞尔曲线被广泛地运用到计算机图形软件(如 Photoshop、Flash 等)中，是计算机图形领域重要的一个数学曲线。&lt;/p></description></item><item><title>Unity3D 游戏开发之使用 disunity 提取 Unity3D 游戏资源</title><link>https://blog.yuanpei.me/posts/1082185388/</link><pubDate>Fri, 03 Apr 2015 13:29:18 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1082185388/</guid><description>&lt;p>  各位朋友，大家好，我是秦元培。今天博主想和分享的是使用 disunity 提取 Unity3D 游戏素材。这个工具呢，博主在&lt;a href="http://www.qinyuanpei.com/2015/04/02/unity3d-development-with-assetbundle/">Unity3D 游戏开发之反编译 AssetBundle 提取游戏资源&lt;/a>这篇文章中其实已经提到过了，不过因为有些朋友对如何使用这个工具依然存在问题，所以博主决定特地写一篇文章来讲解如何使用 disunity 来提取 Unity3D 游戏中的素材。&lt;/p></description></item><item><title>Unity3D 游戏开发之反编译 AssetBundle 提取游戏资源</title><link>https://blog.yuanpei.me/posts/2799263488/</link><pubDate>Thu, 02 Apr 2015 20:37:52 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2799263488/</guid><description>&lt;p>各位朋友，大家好，欢迎大家关注我的博客，我是秦元培，我的博客地址是：&lt;a href="https://blog.yuanpei.me">https://blog.yuanpei.me&lt;/a> ，今天我们来说说通过反编译 Unity3D 的 AssetBundle 来提取游戏资源，博主写这篇文章的目的并非是要教大家如何去破解一款基于 Unity3D 引擎开发的游戏，而是想通过今天这篇文章来告诉大家如何在开发 Unity3D 游戏的过程中保护自己的游戏资源。&lt;/p></description></item><item><title>Unity3D 游戏开发之编辑器扩展程序开发实例</title><link>https://blog.yuanpei.me/posts/70687890/</link><pubDate>Tue, 31 Mar 2015 00:53:22 +0000</pubDate><guid>https://blog.yuanpei.me/posts/70687890/</guid><description>&lt;p>各位朋友大家好，欢迎关注我的博客，我的博客地址是&lt;a href="http://www.qinyuanpei.com">http://www.qinyuanpei.com&lt;/a>。今天我们来说说如何在 Unity3D 中为编辑器开发扩展程序。提到扩展程序，相信大家都不会陌生了。不管是 Google 的 Chrome 浏览器还是经典的 FireFox，这些浏览器最为人所称道的就是它支持各种各样的扩展。扩展程序是一种插件，它遵循插件式设计的原则，可以随时在宿主程序中安装、卸载而不会影响宿主程序的正常运行。我们知道在 Unity3D 中有各种各样的插件，如 NGUI、2DToolKit、EasyTouch 等等都是一种扩展程序。扩展程序在丰富宿主程序功能的基础上，可以帮助宿主程序完成大量额外的工作。可以说正是因为 Unity3D 拥有大量的插件和资源支持，Unity3D 才能够受到大家如此的追捧。可是作为一个有节操的程序员，如果仅仅会使用工具，那么我们和普通用户有什么区别啊，所以在今天的文章中博主将通过三个具体的实例来教大家如何为 Unity3D 的编辑器开发扩展程序，希望对大家学习 Unity3D 技术有所帮助！&lt;/p></description></item><item><title>从「复活」和「暂停/恢复」谈游戏数据配置管理</title><link>https://blog.yuanpei.me/posts/3356910090/</link><pubDate>Fri, 27 Mar 2015 02:12:58 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3356910090/</guid><description>&lt;p>随着游戏制作技术的不断发展，在经历了从 2D 到 3D、从单机到网游、从 PC 游戏到移动游戏的种种演变后，玩家对于游戏质量的要求越来越高，游戏制作的难度相应地增加，整个游戏研发的体系开始变得庞大而复杂，由此就产生了游戏数据配置和管理的相关问题。本文将从游戏中的&amp;quot;复活&amp;quot;和&amp;quot;暂停/恢复&amp;quot;这两个应用场景的角度来谈谈在游戏开发中如何对游戏中的数据进行管理和配置。&lt;/p></description></item><item><title>为 Hexo 开发一个网易云音乐的文章插件</title><link>https://blog.yuanpei.me/posts/828223375/</link><pubDate>Tue, 24 Mar 2015 10:32:39 +0000</pubDate><guid>https://blog.yuanpei.me/posts/828223375/</guid><description/></item><item><title>使用 Coding.NET 和 Hexo 实现网页游戏的发布</title><link>https://blog.yuanpei.me/posts/1150071886/</link><pubDate>Tue, 24 Mar 2015 08:54:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1150071886/</guid><description>&lt;p>本文将尝试借助 Coding.NET 的项目演示功能，通过对 Hexo 中支持的发布类型进行扩充，实现可以在 Hexo 中发布网页游戏，从而方便博主展示游戏作品和帮助读者了解游戏效果。&lt;/p></description></item><item><title>C# 中 Socket 通信编程的异步实现</title><link>https://blog.yuanpei.me/posts/2041685704/</link><pubDate>Sun, 22 Mar 2015 09:37:04 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2041685704/</guid><description>&lt;p>本文将在 C#中 Socket 同步通信的基础上，分析和研究 Socket 异步编程的实现方法，目的是深入了解 Socket 编程的基本原理，增强对网络游戏开发相关内容的认识。&lt;/p></description></item><item><title>C# 中 Socket 通信编程的同步实现</title><link>https://blog.yuanpei.me/posts/3959327595/</link><pubDate>Sun, 15 Mar 2015 15:05:56 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3959327595/</guid><description>&lt;p>本文通过分析和总结 C# 中 Socket 通信编程的关键技术，按照同步实现的方式实现了一个简单的 Socket 聊天程序，目的是通过这个程序来掌握 Socket 编程，为进一步开发 Unity3D 网络游戏打下一个坚实的基础。&lt;/p></description></item><item><title>使用 Unity3D 创建一个幸运转盘</title><link>https://blog.yuanpei.me/posts/3449402269/</link><pubDate>Thu, 12 Mar 2015 19:13:38 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3449402269/</guid><description>&lt;p>今天我们来做点和游戏无关的事情吧！博主最近情绪一直比较低落，因为在找工作的过程中遇到了些挫折。当一个人内心缺乏斗志的时候，通常会难以静下心来认真地做事情，所以这段时间博主并不打算再去为大家分享新的游戏案例，希望大家能够谅解啊。&lt;/p></description></item><item><title>使用 Love2D 引擎开发贪吃蛇游戏</title><link>https://blog.yuanpei.me/posts/426338252/</link><pubDate>Tue, 10 Mar 2015 10:51:19 +0000</pubDate><guid>https://blog.yuanpei.me/posts/426338252/</guid><description>&lt;p>今天来介绍博主最近捣腾的一个小游戏“贪吃蛇”。“贪吃蛇”这个游戏相信大家都不会感到陌生吧。今天博主将通过&lt;a href="http://love2d.org/">Love2D&lt;/a>这款游戏引擎来为大家实现一个简单的贪吃蛇游戏,在本篇文章当中我们将会涉及到“贪吃蛇”的基本算法、Lua 语言编程等基本的内容，希望能够对大家开发类似的游戏提供借鉴和思考，文章中如有不足之处，还希望大家能够谅解，因为博主的游戏开发基本就是这样慢慢摸索着学习，所以难免会有不足的地方。&lt;/p></description></item><item><title>当梦想照进现实</title><link>https://blog.yuanpei.me/posts/3321992673/</link><pubDate>Tue, 10 Mar 2015 10:45:51 +0000</pubDate><guid>https://blog.yuanpei.me/posts/3321992673/</guid><description>&lt;p>回到学校将近一周了，工作依然没有着落。每天穿梭在校园里，看着身边熙熙攘攘的人群来来往往，面对着许许多多陌生的面孔，看着太阳每天的起起落落，听着校园广播吟唱那些恍若隔世的时光。我深切地感受到作为一名即将毕业的大四学生，此时此刻站在这里是多么的多余。&lt;/p></description></item><item><title>HTML5 游戏开发技术基础整理</title><link>https://blog.yuanpei.me/posts/2038378679/</link><pubDate>Sun, 08 Mar 2015 19:14:44 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2038378679/</guid><description>&lt;p>随着 HTML5 标准最终敲定，HTML5 将有望成为游戏开发领域的的热门平台。HTML5 游戏能够运行于包括 iPhone 系列和 iPad 系列在内的计算机、智能手机以及平板电脑上，是目前跨平台应用开发的最佳实施方案。本文系根据 《HML5 Canvas 游戏开发实战》 一书中的内容整理而成，是了解和学习 HTML5 游戏开发的基础内容，希望能够帮助到那些和博主一样致力于游戏开发的朋友们！&lt;/p></description></item><item><title>互联网黑洞读书笔记(2)</title><link>https://blog.yuanpei.me/posts/1930050594/</link><pubDate>Wed, 11 Feb 2015 15:50:55 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1930050594/</guid><description>&lt;p>  在移动互联网时代地大潮中，微信无疑是整个行业的弄潮儿。从“微信之父”张小龙最初定义微信这个产品的那一刻开始，就注定微信将走上一条平台化的道路，各种各样可能的商业模式成为人们开始不断探索微信的价值，可是这一切和你有关系吗？&lt;/p></description></item><item><title>使用 Mecanim 动画系统来控制 2D 动画</title><link>https://blog.yuanpei.me/posts/2583252123/</link><pubDate>Wed, 11 Feb 2015 13:35:58 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2583252123/</guid><description>&lt;p>各位朋友，大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://blog.csdn.net/qinyuanpei">http://blog.csdn.net/qinyuanpei&lt;/a>。今天我想和大家分享的话题是在 Unity3D 中使用 Mecanim 动画系统来控制 2D 动画。&lt;/p></description></item><item><title>脚本语言编程：Lua 脚本编程入门</title><link>https://blog.yuanpei.me/posts/1940333895/</link><pubDate>Tue, 03 Feb 2015 16:06:31 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1940333895/</guid><description>&lt;p>Lua 是一门简单而强大的语言，其本身强大的扩展性使得这门语言在游戏设计等领域发挥着重要的作用。博主曾在 Unity3D 中使用过这门语言，并且针对 Lua 和 Unity、C++等方面的内容进行了学习和讨论。最近因为在【游戏脚本高级编程】这本书中详细介绍了 Lua 脚本的相关内容，因此在这里记录下博主的读书心得，方便以后在需要的时候查阅。&lt;/p></description></item><item><title>互联网黑洞读书笔记(1)</title><link>https://blog.yuanpei.me/posts/1478979553/</link><pubDate>Tue, 03 Feb 2015 09:45:57 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1478979553/</guid><description>&lt;p>  互联网是什么？当我们都渐渐习惯将互联网当作生活的一部分的时候，面对这样一个问题，我们似乎是迷茫和困惑的。因为当互联网渐渐地开始改变我们生活的那一刻起，我们就在和互联网不断地发生着剧烈反应。&lt;/p></description></item><item><title>当 Unity3D 游戏开发遇上 Excel</title><link>https://blog.yuanpei.me/posts/906436376/</link><pubDate>Sun, 25 Jan 2015 19:41:57 +0000</pubDate><guid>https://blog.yuanpei.me/posts/906436376/</guid><description>各位朋友，大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是http://blog.csdn.net/qinyuanpei。今天我们来聊聊常用办公软件 Excel 和游戏开发那不为人知的秘密。今天的内容将涉及到 Excel 在游戏开发中的应用以及如何利用程序解析 Excel 中的数据。作为常用的办公软件的 Excel 相信大家都不陌生啦。可是如果我们认为 Excel 只是办公软件的话，那么这就不只是天真而是 Out 了。事实上 Excel 和游戏开发有着密切的联系，不知道大家还记不记得那款利用 Excel 开发出来的三国杀，这可能是 Excel 第一次以游戏开发的身份出现在大家面前吧。我们知道在游戏开发领域有一种工作叫做策划，就像在软件开发领域有一种工作叫做产品经理一样。而在诸多的策划工作中，数值策划是一个可以直接影响游戏进程的工作，因为数值策划体现了一个游戏在整体数值上的平衡，设计者需要维护好这样一个平衡，确保游戏外的玩家和游戏里的敌人面对的是同一个公平的虚拟世界。
例如，在《仙剑奇侠传四》这款游戏中，韩菱纱在游戏后期的速度可以说是完全打破了游戏的平衡性，因为韩菱纱本身的速度就比较快，再加上仙风云体术的加速效果完全对玄霄产生了戏剧性压制，导致在游戏结尾的 Boss 战中经常是韩菱纱出手 N 次后才挨到玄霄出手，我们知道韩菱纱的乾坤一掷每次消耗气 15，可是因为韩菱纱的速度足够快，所以韩菱纱完全可以通过普通物理攻击快速地积满气进而施展乾坤一掷，这就是游戏的平衡性被打破了呀，更不要说这部游戏里最为经典的千方残光剑 Bug 了，这同样是游戏平衡性的问题，归根到底是紫英的这个技能在配置数据时出现了错误，这充分说明数据的正确合理与否是会对游戏产生重要影响的。
慕容紫英千方残光剑 韩菱纱乾坤一掷 尽管我们可以使用 Xml、Json、ini、数据库等存储形式来存储这些数据，可是毫无疑问的是，Excel 是 Window 平台上最好的数据处理软件，因此数值策划更倾向于使用 Excel 来设计游戏中的数据，面对如此重要的数值策划工作，我们自然希望在解析 Excel 文件时不会出现错误，可是我们总不能指望着策划把 Excel 数据转换成我们能处理的数据类型吧，因此就有了博主今天的这篇文章，所以在今天的文章中我们主要的内容就是如何通过程序来解析 Excel 文件。
项目需求 最近博主一个朋友向我抱怨，说手头上有好几百个 Excel 工作表要处理，大概几十万条数据吧。原因是当时公司分配任务时交待不清，等到了向公司交接数据的时候，朋友忽然发现这些 Excel 文件的表格格式和公司规定的不一样啊。这可急坏了博主的这位朋友，博主的朋友只好不断地的复制、黏贴，因为这些数据是分布在不同的数据表里，朋友整天都忙得焦头烂额，可是即使这样效率还是得不到保证啊，朋友最后找到了博主这里，问我能不能编写程序帮他解决这个问题。因为平时经常与技术圈子里的朋友聊天，所以在博主印象里 Excel 的解析在游戏开发中还是较为常见的，而且博主知道对于微软的 Office 办公软件是可以通过 VBA 编程来实现某些功能的，可是因为博主一直在用国产的 WPS，所以对于 Excel 的解析基本上是停留在一个概念性的认识上，可是朋友的忙不能不帮不是，所以博主决定借着这个机会好好研究下 Excel 文件的解析。
解决方案 因为博主在之前并没有过解析 Excel 文件的经历，所以博主就到 Github 上淘了些开源项目。和很多人爱逛天猫、淘宝的经历类似，如果你发现有一个人经常喜欢到 Github 上晃荡、喜欢关注技术类的博客或者资讯、经常再看 PDF 版的技术文档或书籍，请千万不要怀疑，这个人绝对是程序员。哈哈，好了，玩笑就此打住啊。经过博主对这些开源项目的简单分析和整理，目前，对 Excel 文件解析的解决方案主要有以下三种：</description></item><item><title>Unity3D 塔防游戏开发项目讲解(下)</title><link>https://blog.yuanpei.me/posts/1176959868/</link><pubDate>Wed, 21 Jan 2015 13:50:48 +0000</pubDate><guid>https://blog.yuanpei.me/posts/1176959868/</guid><description>&lt;p>各位朋友，大家好，我是秦元培，欢迎大家关注我的博客，我的博客地址是&lt;a href="http://blog.csdn.net/qinyuanpei">http://blog.csdn.net/qinyuanpei&lt;/a>。我们知道一个完整的塔防游戏由地图、敌人、防守单位三个部分组成，在上一篇文章中我们已经对地图这块儿进行了全面的讲解，今天我们来说说敌人和防守单位。&lt;/p></description></item><item><title>剑指 Offer 读书笔记(1)</title><link>https://blog.yuanpei.me/posts/123663202/</link><pubDate>Tue, 20 Jan 2015 10:04:41 +0000</pubDate><guid>https://blog.yuanpei.me/posts/123663202/</guid><description>&lt;script type="text/javascript" src="https://blog.yuanpei.me/scripts/douban.js">&lt;/script>
&lt;script type="text/javascript">
 var ele = document.currentScript
 var douban = window.$DoubanCard(ele, '25910559', 'https:\/\/douban.edui.fun\/v2\/book\/id\/25910559', 'book', '');
&lt;/script>
&lt;style>
 .douban-card {
 width: 100%;
 max-width: 100%;
 background: white;
 border-radius: 16px;
 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
 overflow: hidden;
 display: flex;
 text-decoration: none;
 color: #333;
 transition: transform 0.2s, box-shadow 0.2s;
 position: relative;
 margin-top: 1.5rem;
 }

 .douban-card:hover {
 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
 border: 1px solid var(--main-card-border);
 }

 .card-content {
 flex: 8;
 padding: 16px;
 display: flex;
 flex-direction: column;
 }

 .card-cover {
 flex: 2;
 position: relative;
 overflow: hidden;
 min-height: 200px;
 }

 .card-cover img {
 position: absolute;
 height: 100%;
 width: 100%;
 object-fit: cover;
 object-position: center;
 }

 .card-title {
 font-size: 18px;
 font-weight: bold;
 margin-bottom: 8px;
 color: #333;
 line-height: 1.4;
 }

 .card-meta {
 display: flex;
 align-items: center;
 margin-bottom: 12px;
 font-size: 14px;
 color: #666;
 }

 .card-rating {
 margin-right: 8px;
 font-weight: bold;
 color: #e09015;
 }

 .card-stars {
 margin: 2.5px 0 2.5px;
 font-size: 12px;
 line-height: 1;
 display: flex;
 align-items: center
 }

 .card-stars .card-stars-dark {
 position: relative;
 color: #f99b01;
 height: 16px;
 width: 80px;
 background-size: auto 100%;
 margin-right: 8px;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==)
 }

 .card-stars .card-stars-light {
 position: absolute;
 left: 0;
 color: #f99b01;
 height: 16px;
 overflow: hidden;
 background-size: auto 100%;
 background-repeat: repeat;
 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);

 }

 .card-description {
 font-size: 12px;
 line-height: 1.6;
 color: #666;
 overflow: hidden;
 display: -webkit-box;
 -webkit-line-clamp: 3;
 -webkit-box-orient: vertical;
 margin-bottom: 12px;
 }

 .card-info {
 font-size: 12px;
 color: #999;
 line-height: 1.6;
 }

 .card-tags {
 margin-top: 12px;
 display: flex;
 flex-wrap: wrap;
 }

 .card-tag {
 font-size: 12px;
 color: var(--main-color);
 background: #f5f5f5;
 padding: 2px 8px;
 border-radius: 3px;
 margin-right: 6px;
 margin-bottom: 6px;
 }




 .link-overlay {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 z-index: 1;
 }

 @media (max-width: 520px) {
 .douban-card {
 flex-direction: column;
 }

 .card-content {
 flex: none;
 width: 100%;
 padding: 12px;
 }

 .card-cover {
 flex: none;
 width: 100%;
 height: 180px;
 order: -1;
 }

 .card-title {
 font-size: 16px;
 margin-bottom: 6px;
 }

 .card-meta {
 margin-bottom: 8px;
 font-size: 12px;
 }

 .card-description {
 font-size: 13px;
 -webkit-line-clamp: 2;
 margin-bottom: 8px;
 }

 .card-info {
 font-size: 11px;
 }

 .card-tags {
 margin-top: 8px;
 }

 .card-tag {
 font-size: 11px;
 padding: 1px 6px;
 margin-right: 4px;
 margin-bottom: 4px;
 }
 }
&lt;/style>
&lt;p>在此将【剑指 Offer】中的经典问题和重要内容整理出来，便于以后遇到类似的问题再次查阅。博主强烈为大家推荐这本书，因为这本书中的题目都来自真实的公司笔试，对于大家求职、找工作会有很大的帮助。&lt;/p></description></item><item><title>写给永远单纯的自己</title><link>https://blog.yuanpei.me/posts/2752169106/</link><pubDate>Thu, 01 Jan 2015 21:36:24 +0000</pubDate><guid>https://blog.yuanpei.me/posts/2752169106/</guid><description>&lt;p>即将到来的是新的一天，我却不能在疲惫中很快入睡，听着耳边再熟悉不过的歌，即使不是大家都喜欢的那种慷慨激昂的曲调，然而在这安静得无从察觉一个人内心世界的夜晚，这样温婉柔和的小调反而更容易让人静下心来想些事情。今天新住的宾馆简约而整洁，最为重要的是终于有了一张属于自己的桌子。以前每次趴在床上画图斑，等到再站起来时背部便开始痛起来。偶尔盘腿坐在床上录数据，等到再站起来时脚已经麻了。这样做的一个坏处是每次都会把中性笔的墨水弄到床单上，虽然顾客是上帝，可是上帝不断地给人类制造麻烦，这样真的好吗？与此同时，开始意识到一个良好的姿势对于健康是多么的重要了。&lt;/p></description></item><item><title>在平凡中蜕变，我的 2014</title><link>https://blog.yuanpei.me/posts/124807650/</link><pubDate>Tue, 30 Dec 2014 19:34:34 +0000</pubDate><guid>https://blog.yuanpei.me/posts/124807650/</guid><description>&lt;p>如果生命可以轻到像一枚羽毛，我愿意飞过那片时间的海，突然造访那些曾经让我怀念过的日子，看看那时的我是不是和现在一样，从来不曾后悔过当初的选择。2014 年 12 月 31 日，面对即将到来的 2015，我相信这将是我在平凡中蜕变的一年。回首 2014，从年初开始学习 Unity3D 游戏开发到现在，我的博客共积累了 230335 次访问量和 700 名粉丝的关注。首先要感谢各位长期以来关注和支持我的博客的朋友，因为你们的关注和支持就是我不断写下去的动力。2014 年，当我渐渐地从一名大四学生的角色转换到一个社会人的时候，我开始认识到原来生活的本质就是平凡。或许我们都只是在做着普普通通的工作，或许我们都只是一个普普通通的人，可是因为我们有一颗不甘于平凡的心，所以我们选择在追逐梦想的路上完成一次次的蜕变。整个 2014 年基本上可以分成三个阶段：&lt;/p></description></item></channel></rss>