<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>许骏亮的博客</title><description>Hey There!</description><link>https://blog.kimbleex.top/</link><language>zh_CN</language><item><title>GraphRAG 安装时 LiteLLM 构建失败排查记录</title><link>https://blog.kimbleex.top/posts/2026-07-31-graphrag-install/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-07-31-graphrag-install/</guid><pubDate>Fri, 31 Jul 2026 14:51:00 GMT</pubDate><content:encoded>&lt;p&gt;在 Windows 环境中使用 &lt;code&gt;uv&lt;/code&gt; 安装 &lt;code&gt;GraphRAG&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add graphrag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依赖解析能够正常完成，但安装过程在构建 &lt;code&gt;litellm==1.92.0&lt;/code&gt; 时失败。&lt;/p&gt;
&lt;h2&gt;环境信息&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;操作系统：Windows x64&lt;/li&gt;
&lt;li&gt;Python：CPython 3.12（原本使用Python 3.14 但&lt;code&gt;LiteLLM 1.92.0&lt;/code&gt; 没有 CPython 3.14 Wheel）&lt;/li&gt;
&lt;li&gt;包管理工具：&lt;code&gt;uv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;GraphRAG：&lt;code&gt;3.1.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;LiteLLM：&lt;code&gt;1.92.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Rust：&lt;code&gt;1.97.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么安装 GraphRAG 会构建 LiteLLM&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;litellm&lt;/code&gt; 不是 &lt;code&gt;graphrag&lt;/code&gt; 顶层包直接声明的依赖，而是由 &lt;code&gt;graphrag-llm&lt;/code&gt; 引入的间接依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graphrag-demo 0.1.0
└── graphrag 3.1.1
    └── graphrag-llm 3.1.1
        └── litellm 1.92.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GraphRAG&lt;/code&gt; 使用 &lt;code&gt;LiteLLM&lt;/code&gt; 作为统一的大模型调用层，主要承担以下工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用聊天模型，包括同步、异步和流式请求；&lt;/li&gt;
&lt;li&gt;调用 Embedding 模型生成文本向量；&lt;/li&gt;
&lt;li&gt;统一 OpenAI、Azure OpenAI 等不同模型服务的请求参数、响应和异常；&lt;/li&gt;
&lt;li&gt;进行 Token 编码、解码和上下文长度控制；&lt;/li&gt;
&lt;li&gt;获取模型费用信息，并为调用指标统计提供数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;code&gt;GraphRAG&lt;/code&gt; 的实际流程中，实体与关系提取、社区摘要、查询向量化以及最终答案生成都会用到这些能力。&lt;code&gt;LiteLLM&lt;/code&gt; 本身不负责知识图谱构建、社区检测或向量存储，它位于 &lt;code&gt;GraphRAG&lt;/code&gt; 与模型 API 之间。&lt;/p&gt;
&lt;h2&gt;为什么 Windows 下触发了本地构建&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;litellm 1.92.0&lt;/code&gt; 在 PyPI 上没有提供适用于 &lt;code&gt;win-amd64&lt;/code&gt; 的预编译 Wheel，只提供了 Linux Wheel 和源码包。&lt;code&gt;uv&lt;/code&gt; 在 Windows 上找不到可直接安装的 Wheel，因此会下载源码包，并通过 &lt;code&gt;maturin&lt;/code&gt;、Cargo 和 MSVC 构建其中的 Python/Rust 混合扩展。&lt;/p&gt;
&lt;p&gt;构建过程大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv
└── maturin
    └── cargo
        └── rustc + MSVC linker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，这次报错并不是依赖解析失败，而是 Windows 本地原生扩展构建环境出现了问题。&lt;/p&gt;
&lt;h2&gt;找不到 link.exe&lt;/h2&gt;
&lt;p&gt;执行安装时，Rust 编译器已经能够启动，但无法找到 MSVC 链接器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: linker `link.exe` not found

note: the msvc targets depend on the msvc linker but `link.exe` was not found
note: please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明 Rust 工具链本身不是主要问题。当前使用的是 &lt;code&gt;x86_64-pc-windows-msvc&lt;/code&gt; 目标，它除了需要 &lt;code&gt;rustc&lt;/code&gt; 和 Cargo，还依赖 Visual Studio C++ Build Tools 提供的编译器、链接器和 Windows SDK。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;主要方案就是补全系统里缺少的环境依赖。&lt;/p&gt;
&lt;p&gt;前往&lt;a href=&quot;https://visualstudio.microsoft.com/downloads/&quot;&gt;Visual Studio Installer&lt;/a&gt;页面下载&lt;code&gt;Build Tools for Visual Studio&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./download.avif&quot; alt=&quot;下载&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下载后会默认给你安装一个&lt;code&gt;Visual Stuido Installer&lt;/code&gt;，我们可以在这里面安装需要的依赖。&lt;/p&gt;
&lt;p&gt;针对这次的问题，最小只需要勾选以下两条。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./dependency.avif&quot; alt=&quot;依赖&quot; /&gt;&lt;/p&gt;
&lt;p&gt;安装完毕后，重启终端再次安装&lt;code&gt;GraphRag&lt;/code&gt;依赖即可。&lt;/p&gt;
</content:encoded></item><item><title>微信Clawbot接入Openclaw</title><link>https://blog.kimbleex.top/posts/2026-07-30-clawbot-openclaw/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-07-30-clawbot-openclaw/</guid><pubDate>Thu, 30 Jul 2026 18:09:00 GMT</pubDate><content:encoded>&lt;p&gt;微信官方推出了&lt;code&gt;Clawbot&lt;/code&gt;，可以直接接入&lt;code&gt;Openclaw&lt;/code&gt;，实现在自己的微信就可以给&lt;code&gt;Openclaw&lt;/code&gt;下发指令，当然也可以直接当成&lt;code&gt;AI智能助手&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Openclaw&lt;/code&gt;部署不多赘述，记录一下接入&lt;code&gt;Clawbot&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;Openclaw&lt;/code&gt;的部署环境终端，执行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx -y @tencent-weixin/openclaw-weixin-cli@latest install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待安装完成后，它第一次会自动跳出来一个二维码，扫描该二维码，你的列表就会多出来一个叫做&lt;code&gt;微信ClawBot&lt;/code&gt;的用户，与他对话就可以直接调用&lt;code&gt;Openclaw&lt;/code&gt;中配置的大模型了。&lt;/p&gt;
&lt;p&gt;支持绑定多个微信账号，还是在该终端中，执行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw channels login --channel openclaw-weixin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就会有新的二维码出来了，扫描即可。注意，默认是所有用户共享一个session，这会导致别人调用的时候，大模型还会有你的对话的历史记忆，需要配置一下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw config set session.dmScope per-account-channel-peer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以保持每个用户有单独的session了。&lt;/p&gt;
</content:encoded></item><item><title>传统 RAG 和 Agentic RAG</title><link>https://blog.kimbleex.top/posts/2026-06-24-rag-agenticrag/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-06-24-rag-agenticrag/</guid><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近面试&lt;code&gt;AI开发&lt;/code&gt;岗位，被问到一个很&lt;strong&gt;熟悉又陌生&lt;/strong&gt;的问题: &lt;code&gt;简单说一下传统RAG和Agent中RAG的差异&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;被问到时我头脑一片空白，感觉有很多想说，但是却好像又都不够具体，我当时就回答: &lt;code&gt;传统RAG的链路是死的，用户提问向量化，然后去向量数据库检索后一并交由大模型思考得出结果；而Agent RAG是LLM知道自己有检索的能力，自主选择是否需要检索，然后再给出最后的结果&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个回答我自己也是不满意的，说到底是我在接触&lt;code&gt;RAG&lt;/code&gt;时，本身就是在&lt;code&gt;Agent&lt;/code&gt;条件下，本质没有接触过传统的&lt;code&gt;RAG&lt;/code&gt;，所以这个问题给我问懵住了。于是花了一些时间去了解了一下其中的逻辑。记录一下。&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RAG&lt;/code&gt;的出现是为了解决大模型的一个老毛病——&lt;strong&gt;幻觉&lt;/strong&gt;。你问它一个它训练语料里没有、或者记不清的事实，它不会说&quot;我不知道&quot;，而是一本正经地编一个看起来很合理的答案。再加上模型的知识有截止日期，对特定场景的内部的私有文档更是一无所知。所以就需要一个外挂的知识库来补充大模型的知识。&lt;/p&gt;
&lt;h2&gt;传统 RAG&lt;/h2&gt;
&lt;p&gt;传统 &lt;code&gt;RAG&lt;/code&gt;（也常被叫做 &lt;code&gt;Naive RAG&lt;/code&gt;）的流程非常固定，本质就是一条&lt;strong&gt;单向流水线&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;拆开来看就是这几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把用户问题通过 &lt;code&gt;Embedding&lt;/code&gt; 模型转成向量；&lt;/li&gt;
&lt;li&gt;拿这个向量去向量数据库里做相似度检索，捞出 &lt;code&gt;Top-K&lt;/code&gt; 个最相关的文本片段；&lt;/li&gt;
&lt;li&gt;把这些片段和原始问题拼成一个 &lt;code&gt;Prompt&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;丢给 LLM 生成最终答案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码层面，它甚至可以简单到几行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# [伪代码] 传统 RAG 的核心逻辑：检索一次，生成一次
def naive_rag(question: str) -&amp;gt; str:
    query_vec = embed(question)                 # 1. 向量化
    chunks = vector_db.search(query_vec, k=5)   # 2. 检索 Top-K
    context = &quot;\n&quot;.join(chunks)                 # 3. 拼接上下文
    prompt = f&quot;根据以下资料回答问题：\n{context}\n\n问题：{question}&quot;
    return llm.invoke(prompt)                 # 4. 一次性生成
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传统&lt;code&gt;RAG&lt;/code&gt;的关键特征：&lt;strong&gt;整个过程只检索一次、生成一次，流程写死，不会回头&lt;/strong&gt;。检索器在这里扮演的角色，更像是 LLM 前面的一个&lt;strong&gt;自动查字典&lt;/strong&gt;动作，查到什么就用什么，查得对不对、够不够，它自己是不知道的。&lt;/p&gt;
&lt;p&gt;正是这种&lt;strong&gt;一条龙&lt;/strong&gt;的设计，决定了它的局限。基本都能归到这几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;检索质量直接决定上限&lt;/strong&gt;。如果用户的提问措辞和文档里的表达对不上（比如用户问&quot;年假怎么休&quot;，文档里写的是&quot;带薪休假申请流程&quot;），向量召回就可能偏，而传统 RAG 没有任何机制去&quot;换个说法再查一次&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应付不了多跳问题&lt;/strong&gt;。像&quot;对比一下 A 产品和 B 产品在 2025 年的销量增长&quot;这种问题，需要分别检索 A、B 两份资料再做计算，单次检索根本搞不定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不会自我怀疑&lt;/strong&gt;。检索到的内容哪怕是噪声、是无关片段，它也会硬着头皮拿去生成，最后输出一个似是而非的答案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据源单一&lt;/strong&gt;。它通常只连一个向量库，没法根据问题灵活地去查数据库、调 API 或者搜实时网页。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Agentic RAG&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Agentic RAG&lt;/code&gt; 的核心改变，就是把那条死板的流水线，换成了一个**会思考的智能体（Agent）**来当总指挥。&lt;/p&gt;
&lt;p&gt;检索不再是写死的第一步，而是 Agent 手里众多&lt;code&gt;TOOLS&lt;/code&gt;中的一个。要不要检索、检索什么、用哪个数据源、检索回来的东西够不够、要不要再来一轮，&lt;strong&gt;这些全部交给 LLM 自己推理决策&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;整个过程变成了一个带反馈的循环：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;理解 &amp;amp; 规划&lt;/strong&gt;：问题的拆解。先想清楚这个问题要拆成几步，需要哪些信息；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选工具 / 改写查询&lt;/strong&gt;：决定调用哪个工具，必要时把用户的口语化提问改写成更适合检索的查询（&lt;code&gt;Query Rewrite&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多源检索&lt;/strong&gt;：可能查向量库，也可能搜网页、查数据库、调 API，而且可以&lt;strong&gt;多轮&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估 &amp;amp; 反思&lt;/strong&gt;：拿到结果后自己掂量一下——信息够不够？答案靠不靠谱？如果不够，&lt;strong&gt;回到第 2 步改写查询再来一遍&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成答案&lt;/strong&gt;：确认信息充分后，再给出带引用的最终回答。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里最本质的两个新增能力是 &lt;strong&gt;反思（Reflection）&lt;/strong&gt; 和 &lt;strong&gt;迭代（Iteration）&lt;/strong&gt;。传统 RAG 是&quot;查完即用&quot;，而 Agentic RAG 是&quot;查完先验货，不合格就退回重查&quot;。&lt;/p&gt;
&lt;p&gt;实际场景中使用的&lt;code&gt;Agentic RAG&lt;/code&gt;通常有下面几种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1. 路由式（Router）&lt;/strong&gt;：最轻量。前面加一个&quot;路由器&quot;，根据问题类型决定去查哪个知识源——技术问题查技术库，财务问题查财务库，时效性问题走网页搜索。它本身还是单次检索，只是多了&quot;选对地方&quot;这一步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2. 单智能体（Single-Agent）&lt;/strong&gt;：一个 Agent 大脑 + 一套工具（向量检索、Web、SQL、计算器……），由它自主决定调用顺序和次数，支持多轮迭代。这是目前性价比最高、用得最多的形态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3. 多智能体（Multi-Agent）&lt;/strong&gt;：一个主控（&lt;code&gt;Orchestrator&lt;/code&gt;）把任务拆给多个各有专长的子 Agent——检索 Agent 负责找资料、分析 Agent 负责处理、写作 Agent 负责成文。能力最强，但工程复杂度和成本也最高，适合真正复杂的工作流。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过&lt;code&gt;Agentic RAG&lt;/code&gt;也不是完美的，依旧会有一些缺点:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一，Agentic RAG 不是传统 RAG 的&quot;升级替代&quot;，而是&quot;复杂度换能力&quot;的取舍。&lt;/strong&gt; 它多调几次 LLM，意味着延迟可能从几百毫秒飙到几秒甚至几十秒，token 成本翻几倍，而且因为路径是动态的，一旦出错你很难复现和定位——同一个问题两次跑出来的中间步骤都可能不一样。对一个&quot;查个公司请假规定&quot;的简单问答上 Agent，纯属杀鸡用牛刀。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，Agentic 的上限，仍然压在底座模型的推理能力上。&lt;/strong&gt; 反思、规划、路由这些动作全靠 LLM 自己判断。如果模型本身推理不行，所谓的&quot;自我纠错&quot;很可能变成&quot;自我欺骗&quot;——它反思完反而更自信地给你一个错的答案。所以 Agentic RAG 对模型能力的要求，其实比传统 RAG 高得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三，检索质量这个地基，两者都逃不掉。&lt;/strong&gt; Agentic RAG 能多查几轮、能改写查询，但如果你的文档切分得稀烂、&lt;code&gt;Embedding&lt;/code&gt; 模型选得不对、向量库里全是脏数据，那 Agent 查一百轮也是垃圾进垃圾出。&lt;/p&gt;
&lt;h2&gt;两者的本质区别&lt;/h2&gt;
&lt;p&gt;如果让我用一句话概括，那就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;传统 RAG&lt;/code&gt; 是&lt;strong&gt;执行&lt;/strong&gt;一条写好的流水线；&lt;code&gt;Agentic RAG&lt;/code&gt; 是&lt;strong&gt;自己做决策的员工分析解决问题&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传统 RAG&lt;/strong&gt; = 检索 + 生成的固定流水线，简单、快、便宜，但不会变通；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic RAG&lt;/strong&gt; = 用一个会决策的 Agent 来指挥检索，能改写、能多轮、能多源、能自我纠错，更强但更慢更贵；&lt;/li&gt;
&lt;li&gt;两者是&lt;strong&gt;自动化程度的两端&lt;/strong&gt;，不是谁取代谁；&lt;/li&gt;
&lt;li&gt;选型的关键不在于&quot;哪个更先进&quot;，而在于&lt;strong&gt;你的任务复杂度是否真的值得这份额外的成本&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vscode使用Codex插件需要Reconnecting的解决方案</title><link>https://blog.kimbleex.top/posts/2026-05-09-codex-reconnecting/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-05-09-codex-reconnecting/</guid><pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在&lt;code&gt;vscode&lt;/code&gt;中使用&lt;code&gt;Codex&lt;/code&gt;插件时，每次打开编辑器使用第一次使用插件时总会出现&lt;code&gt;Reconnecting&lt;/code&gt;的情况&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./reconnecting.avif&quot; alt=&quot;重连&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上的情况。重连5次后突然就可以了，并且后续后正常使用。&lt;/p&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;p&gt;本质原因是&lt;code&gt;vscode&lt;/code&gt;没有读取到你本地的&lt;code&gt;proxy&lt;/code&gt;，解决方案如下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;vscode&lt;/code&gt;中按快捷键&lt;code&gt;ctrl + ,&lt;/code&gt;打开配置项菜单。&lt;/li&gt;
&lt;li&gt;在设置页面的搜索框中输入 &lt;code&gt;proxy&lt;/code&gt;。找到 &lt;code&gt;Http: Proxy&lt;/code&gt; 这一项。&lt;/li&gt;
&lt;li&gt;在下方的输入框中直接填入你本地代理的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如我本地使用的&lt;code&gt;clash verge&lt;/code&gt;，端口在&lt;code&gt;7897&lt;/code&gt;，则填入&lt;code&gt;http://127.0.0.1:7897&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./proxy.avif&quot; alt=&quot;proxy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后重启编辑器，即可正常使用。&lt;/p&gt;
</content:encoded></item><item><title>麒麟 Kylin V10 安装 VNC 服务踩坑记录</title><link>https://blog.kimbleex.top/posts/2026-03-12-kylin-vnc/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-03-12-kylin-vnc/</guid><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;记录麒麟 Kylin V10 上安装 TigerVNC以及踩坑。&lt;/p&gt;
&lt;h2&gt;先说和其他 Linux 发行版不一样的点&lt;/h2&gt;
&lt;p&gt;在 Ubuntu/CentOS 的很多教程里，&lt;code&gt;~/.vnc/xstartup&lt;/code&gt; 往往直接写 &lt;code&gt;startxfce4&lt;/code&gt; 或 &lt;code&gt;gnome-session&lt;/code&gt; 就能用；但麒麟默认是 &lt;strong&gt;UKUI 桌面&lt;/strong&gt;，如果不显式设置 UKUI 相关环境变量并启动 &lt;code&gt;ukui-session&lt;/code&gt;，经常出现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以连上 VNC，但黑屏/灰屏。&lt;/li&gt;
&lt;li&gt;只有鼠标，没有桌面面板。&lt;/li&gt;
&lt;li&gt;服务显示 &lt;code&gt;active&lt;/code&gt;，实际会话不可用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以核心是：&lt;strong&gt;重点处理 &lt;code&gt;xstartup&lt;/code&gt; 的 UKUI 会话启动逻辑&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;1. 安装 TigerVNC&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install -y tigervnc-standalone-server tigervnc-common
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化 VNC 密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vncpasswd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连续输入两次密码；提示 &lt;code&gt;Verify-only password&lt;/code&gt; 时选 &lt;code&gt;n&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;2. 放行防火墙端口&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;VNC :1&lt;/code&gt; 默认对应 &lt;code&gt;5901&lt;/code&gt; 端口。&lt;/p&gt;
&lt;p&gt;如果系统用 &lt;code&gt;ufw&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow 5901/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果系统用 &lt;code&gt;firewalld&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --permanent --add-port=5901/tcp
sudo firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 先生成 VNC 用户配置&lt;/h2&gt;
&lt;p&gt;先启动一次再关闭，生成 &lt;code&gt;~/.vnc&lt;/code&gt; 目录和基础文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vncserver :1
vncserver -kill :1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 配置麒麟 UKUI 会话（关键）&lt;/h2&gt;
&lt;p&gt;编辑 &lt;code&gt;~/.vnc/xstartup&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano ~/.vnc/xstartup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS

# 麒麟 UKUI 在 VNC 下通常需要显式声明桌面会话变量
export XDG_CURRENT_DESKTOP=UKUI
export XDG_SESSION_DESKTOP=UKUI
export XDG_SESSION_TYPE=x11

# 优先启动 ukui-session，避免黑屏或只有鼠标
if [ -x /usr/bin/ukui-session ]; then
  exec /usr/bin/ukui-session
elif [ -x /usr/bin/gnome-session ]; then
  exec /usr/bin/gnome-session --session=ukui
else
  ukui-wm &amp;amp;
  exec ukui-panel
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x ~/.vnc/xstartup
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 注册 systemd 服务并开机自启&lt;/h2&gt;
&lt;p&gt;创建服务文件（把 &lt;code&gt;test&lt;/code&gt; 改成你的实际用户名）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/systemd/system/vncserver@:1.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Remote desktop service (VNC)
After=network.target syslog.target

[Service]
Type=forking
User=test
Group=test
WorkingDirectory=/home/test

# 清理残留，防止启动失败
ExecStartPre=-/usr/bin/vncserver -kill %i &amp;gt; /dev/null 2&amp;gt;&amp;amp;1

# :1 =&amp;gt; 5901，可按需调整分辨率
ExecStart=/usr/bin/vncserver %i -geometry 1920x1080 -localhost no
ExecStop=/usr/bin/vncserver -kill %i

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加载并启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable vncserver@:1.service
sudo systemctl start vncserver@:1.service
sudo systemctl status vncserver@:1.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到 &lt;code&gt;active (running)&lt;/code&gt; 表示服务正常。&lt;/p&gt;
&lt;h2&gt;6. 常见问题排查&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;连接后黑屏：优先检查 &lt;code&gt;~/.vnc/xstartup&lt;/code&gt; 是否为 UKUI 方案，以及是否执行了 &lt;code&gt;chmod +x ~/.vnc/xstartup&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;服务起不来：检查服务文件中的 &lt;code&gt;User&lt;/code&gt;、&lt;code&gt;Group&lt;/code&gt;、&lt;code&gt;WorkingDirectory&lt;/code&gt; 是否与实际用户一致。&lt;/li&gt;
&lt;li&gt;端口不通：确认 &lt;code&gt;5901&lt;/code&gt; 已放行，且服务器安全组/ACL 没有拦截。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;排错时建议看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -u vncserver@:1.service -e
cat ~/.vnc/*.log
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>关于海量数据库Vastbase的初次见面</title><link>https://blog.kimbleex.top/posts/2026-03-10-vastbase/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-03-10-vastbase/</guid><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;近期有个现场需要我去部署产品，现场的同事告诉我服务器是&lt;code&gt;Kylin&lt;/code&gt;的，数据库是&lt;code&gt;Vastbase&lt;/code&gt;，没听过这个数据库，但是产品那边说年前已经适配了，于是记录一番。&lt;/p&gt;
&lt;h2&gt;部署插曲&lt;/h2&gt;
&lt;p&gt;拿到服务器，未经验证，发现产品组那边提供的包无法部署，环境依赖缺少说是，一般来讲&lt;code&gt;Kylin V10&lt;/code&gt;是比较新的，如果打包完整是不会缺少环境依赖的，但是还是报错缺少一些环境依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PYI-23497:ERROR] Failed to load Python shared library /home/RPA-DC/resources/dc_temp/_MEIZ2bI4Y/libpython3.9.so&apos;: dlopen: /lib64/ibc.so.6:version GLIBC_2.28&apos; not found (required by /home/RPA-DC/resources/dc_temp/_MEIZ2bI4Y/libpython3.9.so)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ai解读是系统的&lt;code&gt;glibc&lt;/code&gt;版本过低，产品的包需要的&lt;code&gt;py3.9&lt;/code&gt;需要至少&lt;code&gt;glibc&amp;gt;=2.8&lt;/code&gt;，经排查系统的版本是&lt;code&gt;1.17&lt;/code&gt;，故无法部署执行。于是检查服务器的环境。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./server_info.avif&quot; alt=&quot;服务器信息&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不是&lt;code&gt;Kylin&lt;/code&gt;服务器说是！显示的是&lt;code&gt;Anolis&lt;/code&gt;发行版本的&lt;code&gt;Linux&lt;/code&gt;系统，没见过，ai解读，原话: &lt;strong&gt;Anolis OS 7.9 是 龙蜥操作系统（Anolis OS） 的一个具体版本。Anolis OS 7.9 是 CentOS 7 的 100% 兼容替代品，内核、包管理（yum）、命令、配置文件等几乎完全一致。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;本质&lt;code&gt;CentOS7&lt;/code&gt;，找了&lt;code&gt;CentOS7&lt;/code&gt;的产品包，再部署，可执行，接下来就是去捣鼓今天的主角，&lt;code&gt;Vastbase&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;连接Vastbase&lt;/h2&gt;
&lt;p&gt;从官方文档开始看起，&lt;code&gt;Vastbase&lt;/code&gt;中文名称叫做&lt;strong&gt;海量数据库&lt;/strong&gt;，国产的，基于&lt;code&gt;OpenGauss&lt;/code&gt;内核，我去搜了一下，这个内核本质源自于&lt;code&gt;PostgreSQL&lt;/code&gt;(经典 嵌套抄袭=自研)，后面会可以看到，这个海量数据库连接时是可以走&lt;code&gt;Postgre&lt;/code&gt;的接口的。&lt;/p&gt;
&lt;p&gt;总结下来就是，&lt;strong&gt;高性能，高安全，高兼容&lt;/strong&gt;。[旺柴]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;部署不涉及安装部分，只涉及到连接和配置。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;登录Vastbase&lt;/h3&gt;
&lt;p&gt;按照官方文档，登录到&lt;code&gt;Vastbase&lt;/code&gt;的管理员是需要账号和密码的，这个超级管理员的账号密码是在部署数据库的时候自动生成的，不过我询问客户时，他告诉我他们做了本地认证，可用直接免密登录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@vdb1 ~]# su - vastbase
上一次登录：一 3月  9 18:29:10 CST 2026pts/0 上
[vastbase@vdb1 ~]$ vsql -r -p 5432
vsql ((Vastbase G100 V2.2 (Build 19) Release) compiled at 2025-11-10 23:57:40 commit 29012 last mr  )
Non-SSL connection (SSL connection is recommended when requiring high-security)
Type &quot;help&quot; for help.

vastbase=# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./vastbase_login.avif&quot; alt=&quot;数据库登录&quot; /&gt;&lt;/p&gt;
&lt;p&gt;指令&lt;code&gt;\l&lt;/code&gt;可用查看所有的数据库。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./vastbase_database.avif&quot; alt=&quot;数据库汇总&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图可见，一共4个数据库，本次配置将在数据库&lt;code&gt;vastbase&lt;/code&gt;中，如果要切换可以直接&lt;code&gt;\ 数据库名&lt;/code&gt;直接切换。&lt;/p&gt;
&lt;h3&gt;新建专用用户和模式&lt;code&gt;Schema&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这个数据库使用是需要建立专用用户的，专用用户管理对应的模式&lt;code&gt;Schema&lt;/code&gt;，你可以理解就是我用这个专用用户登录连接数据库，可以管理我这个指定的模式，后续的增删改查也都是在这个模式下操作的。&lt;/p&gt;
&lt;p&gt;他们的层级就是，&lt;strong&gt;数据库中包含多种模式，模式下有具体的数据表，专用用户只能操作对应的模式。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./vastbase_create.avif&quot; alt=&quot;创建用户和模式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我这里创建了一个&lt;code&gt;rpa_dc&lt;/code&gt;的用户和一个&lt;code&gt;rpa_dc&lt;/code&gt;的模式。并且把模式&lt;code&gt;rpa_dc&lt;/code&gt;分配给了这个专用用户。&lt;/p&gt;
&lt;h3&gt;连接数据库&lt;/h3&gt;
&lt;p&gt;上面提到了数据内核本质&lt;code&gt;Postgre&lt;/code&gt;，所以连接的时候也是走的&lt;code&gt;Postgre&lt;/code&gt;的接口。我们使用&lt;code&gt;dbeaver&lt;/code&gt;开源工具连接。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./dbeaver_connect.avif&quot; alt=&quot;dbeaver连接&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中的数据库就写使用的数据库名，用户和密码就写刚才创建的专用用户。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./dbeaver_show.avif&quot; alt=&quot;dbeaver展示&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里连接成功就可以看到数据库和模式的树级关系了。&lt;/p&gt;
&lt;p&gt;值得一说的是，虽然我用&lt;code&gt;rpa_dc&lt;/code&gt;登录的数据库，并且可以看到数据库中所有的模式，但是能不能访问模式内的对象还是需要看模式是否分配给用户的。&lt;strong&gt;能看到 != 能操作&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;后续的产品代码上的配置就不多阐述，本质还是走的&lt;code&gt;Postgre&lt;/code&gt;的连接接口，后续的增删改查也是大体相同。&lt;/p&gt;
</content:encoded></item><item><title>git clone指令在挂代理之后被拒</title><link>https://blog.kimbleex.top/posts/2026-01-28-git-ssh/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-01-28-git-ssh/</guid><pubDate>Wed, 28 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;参考文档:&lt;a href=&quot;https://yangqiuyi.com/blog/kai-fa/gua-dai-li-hou-git-clone-chu-xian-connection-closed-by-xxxxxxx-port-22-de-jie-jue-ban-fa/#%E6%8C%82%E4%BB%A3%E7%90%86%E5%90%8E-git-clone-%E5%87%BA%E7%8E%B0-connection-closed-by-xxxxxxx-port-22-%E7%9A%84%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95&quot;&gt;一个老前端写的文章&lt;/a&gt;,&lt;a href=&quot;https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&quot;&gt;github ssh官方说明文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;挂代理后 &lt;code&gt;git clone&lt;/code&gt; 出现 &lt;code&gt;Connection closed by xxxxxxx port 22&lt;/code&gt; ,出现这个问题，往往是因为代理的防火墙拒绝了 &lt;code&gt;SSH&lt;/code&gt; 连接，可以尝试使用在 &lt;code&gt;HTTPS&lt;/code&gt; 端口上建立的 &lt;code&gt;SSH&lt;/code&gt; 连接进行克隆。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;~/.ssh/config&lt;/code&gt;文件中添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host github.com
    Hostname ssh.github.com
    Port 443
    User git
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>OpenCV contourArea计算面积偏小原理详解</title><link>https://blog.kimbleex.top/posts/2026-01-05-opencv-contourarea/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2026-01-05-opencv-contourarea/</guid><pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近捣鼓&lt;code&gt;OpenCV&lt;/code&gt;想来自动化验证滑块验证码，在计算滑块和背景缺口面积的时候，发现使用自带的&lt;code&gt;contourArea&lt;/code&gt;函数计算的面积偏小，比实际面积小很多。&lt;/p&gt;
&lt;p&gt;用画图工具简单比划看了一下，滑块的尺寸是&lt;code&gt;70*70&lt;/code&gt;，背景阴影缺口的尺寸是&lt;code&gt;59*60&lt;/code&gt;，滑块本身带有一圈&lt;code&gt;10&lt;/code&gt;像素的圆角，所以计算出来的面积偏小。这是很好理解的，但是使用&lt;code&gt;contourArea&lt;/code&gt;函数计算的面积只有&lt;code&gt;2158&lt;/code&gt;，比实际要小的多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./bg.avif&quot; alt=&quot;背景&quot; /&gt;
&lt;img src=&quot;./slide.avif&quot; alt=&quot;滑块&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;contourArea&lt;/code&gt;计算的本身是不规则图形的面积，而不是矩形的面积。所以计算出来的面积会偏小。它本身是使用&lt;code&gt;格林公式&lt;/code&gt;计算的，&lt;code&gt;格林公式&lt;/code&gt;使用的条件是&lt;code&gt;正向、连续、无交叉&lt;/code&gt;，刚好满足图形里面轮廓的特点，所以可以二重积分和&lt;code&gt;格林公式&lt;/code&gt;计算出不规则图形的面积。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./bg_cal.avif&quot; alt=&quot;green&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是并不是这样就会使得计算出来的面积小这么多。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;contourArea&lt;/code&gt;函数取的像素点是连通域中心点，比如一个&lt;code&gt;4*4&lt;/code&gt;的连通域，使用该函数来计算面积时，他会取到边缘的中心点而不是边缘的像素点，如图。
&lt;img src=&quot;./contourarea.avif&quot; alt=&quot;contourArea&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这会使得面积从&lt;code&gt;16&lt;/code&gt;像素点减少到&lt;code&gt;9&lt;/code&gt;像素点，所以计算出来的面积会产生第二次偏小。&lt;/p&gt;
</content:encoded></item><item><title>FastAPI-清清爽爽的中间件目录结构</title><link>https://blog.kimbleex.top/posts/2025-12-30-fastapi-middleware-dir/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-12-30-fastapi-middleware-dir/</guid><pubDate>Tue, 30 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近又开始学习&lt;code&gt;FastAPI&lt;/code&gt;，一时不知道大型项目的结构目录是啥，问了一些老友，他们说很多时候中间件就直接写在主程序里了，我又去看了一些项目代码，最后找到一个非常不错的目录结构，记录一下。&lt;/p&gt;
&lt;p&gt;根目录下建立&lt;code&gt;middleware&lt;/code&gt;目录，用于存放中间件代码。里面创建一个&lt;code&gt;__init__.py&lt;/code&gt;文件，用于导入中间件模块。
&lt;img src=&quot;./middleware_1.avif&quot; alt=&quot;middleware_1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这个目录下可以写很多个中间件模块，可以写在一个文件中，也可以分多个文件写。&lt;/p&gt;
&lt;p&gt;然后在&lt;code&gt;__init__.py&lt;/code&gt;文件中写一个中间件注册函数，用于引入所有的中间件。
&lt;img src=&quot;./middleware_2.avif&quot; alt=&quot;middleware_2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中我引入了跨域中间件和计算接口函数运行时常的中间件。&lt;/p&gt;
&lt;p&gt;最后在主程序中调用注册函数。
&lt;img src=&quot;./middleware_3.avif&quot; alt=&quot;middleware_3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样写的好处是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;中间件代码集中在一个目录下，方便管理。&lt;/li&gt;
&lt;li&gt;可以在&lt;code&gt;__init__.py&lt;/code&gt;文件中引入所有的中间件，避免在主程序中写一堆&lt;code&gt;app.add_middleware&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>浏览器监听获取接口返回数据</title><link>https://blog.kimbleex.top/posts/2025-11-18-api-listen/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-11-18-api-listen/</guid><pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;昨天晚上刷抖音的时候我女朋友突然问我，&lt;strong&gt;&lt;code&gt;你说你如果不认识我，你现在或者以后会跟别的女生在一起吗？&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;天塌了，寻思过后我给了他一个十分中立的回答: &lt;strong&gt;&lt;code&gt;这个说不准的&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::caution[30分钟后......]
:::&lt;/p&gt;
&lt;p&gt;她突然跟我说，&lt;strong&gt;&lt;code&gt;我刚才问你这个问题，你回答让我感到十分的sad，如果你问我这个问题，我肯定会说不会，因为我听到这个问题的时候我的脑子里已经全部是你了&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样的言语让我十分费解，很显然，他的回答已经完全推翻了他问题的先决条件:&lt;strong&gt;&lt;code&gt;如果你不认识我&lt;/code&gt;&lt;/strong&gt;，我不禁去思考，如果一个页面上面的数据没有渲染成&lt;code&gt;html&lt;/code&gt;元素，那么我要去怎么爬取这个页面上的数据呢？有因必有果，查找了页面的网络请求，我找到了数据请求的接口，并且在接口的返回值里找到了所需要的数据，于是，我就可以通过监听这个接口，来获取到页面上的数据了。&lt;/p&gt;
&lt;h2&gt;Selenium 监听接口&lt;/h2&gt;
&lt;p&gt;自动化工具&lt;code&gt;Selenium&lt;/code&gt;中并不原生的带有某个可以很方便的去监听接口的方法，通过查找相关的文档，可以通过&lt;code&gt;性能日志(Performance Log)&lt;/code&gt;去实现监听的功能。&lt;/p&gt;
&lt;p&gt;性能日志主要用于监控和分析网页在加载和运行过程中的各项性能数据，特别适合用来排查性能瓶颈和优化页面加载速度。通常可以通过以下方式启用性能日志(以&lt;code&gt;chrome浏览器&lt;/code&gt;为例)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver

options = webdriver.ChromeOptions()
# 启用性能日志
caps = options.to_capabilities()
# 设置日志类别为性能日志；设置性能日志级别为ALL，即记录所有日志
caps[&apos;goog:loggingPrefs&apos;] = {&apos;performance&apos;: &apos;ALL&apos;}

# 配置到chrome浏览器
driver = webdriver.Chrome(options=options)

# 获取性能日志
logs = driver.get_log(&apos;performance&apos;)
# 打印性能日志
for log in logs:
    print(log[&apos;message&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要更方便的记录某个操作所产生的接口请求和返回数据，那么在每一步之前可以重复使用&lt;code&gt;driver.get_log(&apos;performance&apos;)&lt;/code&gt;方法，可以达到清除之前操作产生的日志的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;driver.get_log(&apos;performance&apos;)
driver.find_element(By.XPATH, &apos;//button[text()=&quot;登录&quot;]&apos;).click()
logs = driver.get_log(&apos;performance&apos;)
for log in logs:
    print(log[&apos;message&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以记录出登录按钮点击所产生的接口请求和返回数据。&lt;/p&gt;
&lt;p&gt;通过刚才的日志内容，可以捕获到浏览器发出的所有网络请求(通过&lt;code&gt;Network.requestWillBeSent&lt;/code&gt;事件)和接收的响应(通过&lt;code&gt;Network.responseReceived&lt;/code&gt;事件)，这里我举例&lt;code&gt;Network.responseReceived&lt;/code&gt;获取指定接口的数据如何使用。&lt;/p&gt;
&lt;p&gt;上面获取的&lt;code&gt;logs&lt;/code&gt;变量中，&lt;code&gt;message&lt;/code&gt;字段是一个&lt;code&gt;json&lt;/code&gt;字符串，我们可以通过&lt;code&gt;json&lt;/code&gt;模块解析出其中的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json

for log in logs:
    # 解析json字符串
    message = json.loads(log[&apos;message&apos;])
    # 打印响应数据
    print(message[&apos;message&apos;])

# 可能的一个日志结果为
{
    &quot;method&quot;: &quot;Network.responseReceived&quot;,
    &quot;params&quot;: {
        &quot;response&quot;: {
            &quot;bodySize&quot;: 123,
            &quot;connectionId&quot;: 123,
            &quot;headers&quot;: {
                &quot;Content-Type&quot;: &quot;application/json; charset=utf-8&quot;
            },
            &quot;mimeType&quot;: &quot;application/json&quot;,
            &quot;securityDetails&quot;: null,
            &quot;status&quot;: 200,
            &quot;statusText&quot;: &quot;OK&quot;,
            &quot;url&quot;: &quot;https://example.com/api/data&quot;
        },
        &quot;requestId&quot;: &quot;123&quot;,
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是我们就可以通过&lt;code&gt;response&lt;/code&gt;字段中的&lt;code&gt;url&lt;/code&gt;字段判断是否是我们所需要的接口，然后通过&lt;code&gt;requestId&lt;/code&gt;，使用&lt;code&gt;cdp&lt;/code&gt;命令去获取接口的返回数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 获取日志内容
message = json.loads(log[&apos;message&apos;])[&apos;message&apos;]
# 判断是否为接口响应
if message[&apos;method&apos;] == &apos;Network.responseReceived&apos;:
    # 获取接口返回数据
    response = message[&apos;params&apos;][&apos;response&apos;]
    # 判断是否为指定接口,这里以&quot;/api/login&quot;举例
    if &quot;/api/login&quot; in response[&apos;url&apos;]:
        # 通过requestID获取接口返回数据
        request_id = message[&apos;params&apos;][&apos;requestId&apos;]
        body = driver.execute_cdp_cmd(&apos;Network.getResponseBody&apos;, {&apos;requestId&apos;: request_id})
        print(body)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以直接获取到接口&lt;code&gt;/api/login&lt;/code&gt;的返回数据了。&lt;/p&gt;
&lt;p&gt;这种获取数据的方式很适用于页面里没有渲染成&lt;code&gt;html&lt;/code&gt;元素的情况，所以就需要从接口里去拿原始数据，再自己处理。当然&lt;code&gt;js逆向&lt;/code&gt;是完全可以的，但是大多数都没有那么容易。在我所遇到的情境中，请求数据的接口的请求体中是需要&lt;code&gt;权限token&lt;/code&gt;和&lt;code&gt;验证码&lt;/code&gt;的，而这个&lt;code&gt;token&lt;/code&gt;和&lt;code&gt;验证码&lt;/code&gt;是在登录时生成的，所以还需要去逆向登录接口的生成逻辑，有些耗时了(我是菜鸟)。通过自动化工具模拟去操作页面，跳过了请求生成的逻辑，只关注操作页面后接口的返回值，这样做可以节省很多时间。&lt;/p&gt;
&lt;h2&gt;Playwright&lt;/h2&gt;
&lt;p&gt;相比于&lt;code&gt;Selenium&lt;/code&gt;，&lt;code&gt;Playwright&lt;/code&gt;的监听接口的方式更加简单，直接通过&lt;code&gt;page.on(&apos;response&apos;)&lt;/code&gt;方法即可监听所有接口的请求和返回数据，或者通过&lt;code&gt;page.wait_for_response()&lt;/code&gt;方法等待指定接口的返回数据。&lt;/p&gt;
&lt;p&gt;先举例获取指定接口的返回数据，前提是你得知道你想要的接口是什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
from playwright.async_api import async_playwright
import json

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()
        # 等待特定接口的响应
        response = await page.wait_for_response(&apos;https://api.example.com/data&apos;)
        # 获取响应信息
        print(f&quot;状态码: {response.status}&quot;)
        print(f&quot;URL: {response.url}&quot;)
        print(f&quot;响应头: {response.headers}&quot;)
        # 获取响应体
        response_body = await response.json()  # 对于JSON响应
        # 或者 response_body = await response.text()  # 对于文本响应
        print(f&quot;响应体: {json.dumps(response_body, indent=2)}&quot;)
        await browser.close()

asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心就只是&lt;code&gt;response = await page.wait_for_response(&apos;https://api.example.com/data&apos;)&lt;/code&gt;这一行，十分简单。&lt;/p&gt;
&lt;p&gt;它也可以做到跟&lt;code&gt;Selenium&lt;/code&gt;的监听接口的方式一样，等待指定接口的返回数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()
        # 存储所有响应
        responses = []
        # 监听所有响应事件
        def on_response(response):
            if &apos;/api/login&apos; in response.url:
                response_body = await response.json()
                # response_body = await response.text() 针对文本
                responses.append({
                    &apos;url&apos;: response.url,
                    &apos;status&apos;: response.status,
                    &apos;headers&apos;: response.headers
                    &apos;body&apos;: response_body
                })
                print(f&quot;捕获响应: {response.url} - 状态码: {response.status}&quot;)
        page.on(&apos;response&apos;, on_response)
        # 导航到页面
        await page.goto(&apos;https://example.com&apos;)
        # 等待一段时间让所有请求完成
        await page.wait_for_timeout(5000)
        await browser.close()
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样也可以做到跟&lt;code&gt;Selenium&lt;/code&gt;的监听接口的方式一样，等待指定接口的返回数据，但是值得一提的是，&lt;code&gt;Playwright&lt;/code&gt;这种监听方式会让所有的接口都被监听，即使是异步进行的，性能上也会有一定的影响。&lt;/p&gt;
</content:encoded></item><item><title>知识扫盲3：关于堡垒机、跳板机和前置机</title><link>https://blog.kimbleex.top/posts/2025-10-11-fortress-frontend-jump/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-10-11-fortress-frontend-jump/</guid><pubDate>Sat, 11 Oct 2025 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近公司有个运维同事离职了，人手紧张，他的工作暂时到我这里了。在接收运维的工作中，客户的机器需要通过堡垒机才可以访问&lt;code&gt;RPA&lt;/code&gt;前置机，堡垒机的登录繁琐至极，我不禁好奇:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么不能像其他某些客户一样给我直接访问前置机的远程呢？&lt;/li&gt;
&lt;li&gt;为什么不能给我一个跳板机，我通过跳板机访问前置机呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是有感而发，了解了一些有关知识之后，决定写下来，希望后来者亦将有感于斯文！&lt;/p&gt;
&lt;p&gt;本来想分为三块分别介绍一下，但是那样写不够系统，我要放在一起说。&lt;/p&gt;
&lt;p&gt;所谓前置机，最最主要的功能就是&lt;strong&gt;隔离核心系统和外部环境&lt;/strong&gt;；它属于客户端和服务端的中间层，隔离了核心接口的外部访问条件，并且可以承担一部分业务逻辑，减轻服务端的压力。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RPA&lt;/code&gt;前置机也是如此，他只负责一部分业务数据的获取并推送给数据处理单元，核心接口的访问全部由数据处理单元发起，这样外部就不会接触到核心的接口。这样看下来很安全，既然&lt;code&gt;RPA&lt;/code&gt;前置机已经隔离了核心接口，为什么不让我直接访问呢？？？&lt;/p&gt;
&lt;p&gt;答案只有一个，客户不想我在他们的前置机上干坏事！如果开放了前置机的远程，那么有人干了坏事他们也找不出来！！！&lt;/p&gt;
&lt;p&gt;所以在很多时候，客户会给你一个跳板机去访问前置机，跳板机通常需要账号密码去进行登录，我会需要登录跳板机去访问前置机，这样我可以理解，用一个跳板机做身份认证，这样我使用我的账号密码登录跳板机，从而访问前置机去干了坏事，他们可以找到我，并让我承担他们的损失。&lt;/p&gt;
&lt;p&gt;但是有时候我会想报复社会，我并不怕赔偿损失，我只是单纯的不想跟客户合作，我要让他们感到肉疼，于是我无视风险，我勇敢的登录了我的跳板机账号，访问了前置机和服务器，在怒火的烘托下，我毅然决然的打开了终端，输入了&lt;code&gt;rm -rf /*&lt;/code&gt;，然后长叹一口气，&lt;strong&gt;轻舟已过万重山&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;后来，客户的网络工程师联系到我，让我赔偿，我不屑一顾，后来他们的法务部也找到我，进行了一些列的法律仲裁，最后&lt;strong&gt;我进去了&lt;/strong&gt;，但是我不亏，我把他们的前置机和服务器的数据全抹了(如果他们没有备份的话)。&lt;/p&gt;
&lt;p&gt;后来我出来了，托关系整了一张假身份证并整容成了身份证上的样子，隐姓埋名只为报复客户，我通过了面试进入了客户公司，成为了客户公司了一个运维，我以为我成功了，我可以把他们整个公司毁了，谁曾想！他们升级使用了堡垒机！完了，我这辈子就这样完了！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;抱歉，写文章写的有点嗨了！&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个就是堡垒机相对于前置机的进步空间，堡垒机是可以进行&lt;code&gt;身份认证&lt;/code&gt; +&lt;code&gt;权限可控&lt;/code&gt;+&lt;code&gt;行为可控&lt;/code&gt;的，也就是说他验证完你的身份之后，你的身份会有一个对应的权限等级，不同的权限等级规定了不同的行为规范，并且也规定了你所能够访问的资源。&lt;/p&gt;
&lt;p&gt;也就是说如果管理员把&lt;code&gt;rm -rf /*&lt;/code&gt;对所有的运维身份用户禁用了，那么所有的运维用户将都不能执行这条指令。&lt;/p&gt;
</content:encoded></item><item><title>魔改Fuwari博客-添加评论</title><link>https://blog.kimbleex.top/posts/2025-10-09-fuwari-comment/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-10-09-fuwari-comment/</guid><pubDate>Thu, 09 Oct 2025 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;除了友链之外，再加个评论的功能，那就可以收到大伙的评论了！&lt;/p&gt;
&lt;p&gt;我选择使用&lt;code&gt;giscus&lt;/code&gt;来实现评论功能。主要是它不需要依赖于数据库，而且配置也比较简单，最主要的是依赖于&lt;code&gt;Github&lt;/code&gt;并且纯免费！！！&lt;/p&gt;
&lt;h3&gt;配置giscus&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;首先，你需要创建一个新的GitHub库用来保存博客的那些评论，你需要确保该仓库是公开的,否则访客将无法查看 &lt;code&gt;Discussion&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;然后，你需要给你的这个仓库repo安装&lt;a href=&quot;https://github.com/apps/giscus&quot;&gt;giscus app&lt;/a&gt;,否则访客将无法评论和回应。&lt;/li&gt;
&lt;li&gt;最后，你需要确保 &lt;code&gt;Discussions&lt;/code&gt; 功能已在你的仓库中启用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打开giscus官方网站 &lt;code&gt;https://giscus.app/&lt;/code&gt;，进行配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语言：选择你目前正在使用的语言&lt;/li&gt;
&lt;li&gt;仓库：填写你刚刚创建的仓库(格式为你的用户名/仓库名)&lt;/li&gt;
&lt;li&gt;页面 ↔️ discussion 映射关系(默认即可)&lt;/li&gt;
&lt;li&gt;Discussion 分类（默认即可）&lt;/li&gt;
&lt;li&gt;特性（默认即可）&lt;/li&gt;
&lt;li&gt;主题（默认即可）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按照顺序配置好之后，下方会自动生成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;https://giscus.app/client.js&quot;
        data-repo=&quot;[在此输入仓库]&quot;
        data-repo-id=&quot;[在此输入仓库 ID]&quot;
        data-category=&quot;[在此输入分类名]&quot;
        data-category-id=&quot;[在此输入分类 ID]&quot;
        data-mapping=&quot;pathname&quot;
        data-strict=&quot;0&quot;
        data-reactions-enabled=&quot;1&quot;
        data-emit-metadata=&quot;0&quot;
        data-input-position=&quot;bottom&quot;
        data-theme=&quot;preferred_color_scheme&quot;
        data-lang=&quot;zh-CN&quot;
        crossorigin=&quot;anonymous&quot;
        async&amp;gt;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加评论到指定页面&lt;/h3&gt;
&lt;p&gt;只需要在每个页面对应的&lt;code&gt;.astro&lt;/code&gt;文件中的&lt;code&gt;&amp;lt;/MainGridLayout&amp;gt;&lt;/code&gt;前面添加上方生成的代码即可。&lt;/p&gt;
&lt;p&gt;比如我要在文章页面添加评论，找到&lt;code&gt;src\pages\posts\[...slug].astro&lt;/code&gt;文件，在文件中找到&lt;code&gt;&amp;lt;/MainGridLayout&amp;gt;&lt;/code&gt;标签，在其前面添加上方生成的代码即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                &amp;lt;Icon name=&quot;material-symbols:chevron-right-rounded&quot; class=&quot;text-[2rem] text-[var(--primary)]&quot; /&amp;gt;
            &amp;lt;/div&amp;gt;}
        &amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;

&amp;lt;!-- giscus评论 --&amp;gt;
+ &amp;lt;script src=&quot;https://giscus.app/client.js&quot;
+    data-repo=&quot;AULyPc/aulypc.github.io&quot;
+    data-repo-id=&quot;xxxxxxxxxxx&quot;
+    data-category=&quot;Announcements&quot;
+    data-category-id=&quot;xxxxxxxxxxxxx&quot;
+    data-mapping=&quot;pathname&quot;
+    data-strict=&quot;0&quot;
+    data-reactions-enabled=&quot;1&quot;
+    data-emit-metadata=&quot;0&quot;
+    data-input-position=&quot;top&quot;
+    data-theme=&quot;preferred_color_scheme&quot;
+    data-lang=&quot;zh-CN&quot;
+    crossorigin=&quot;anonymous&quot;
+    async&amp;gt;
+&amp;lt;/script&amp;gt;

&amp;lt;/MainGridLayout&amp;gt;

&amp;lt;style is:global&amp;gt;
#post-container :nth-child(1) { animation-delay: calc(var(--content-delay) + 0ms) }
#post-container :nth-child(2) { animation-delay: calc(var(--content-delay) + 50ms) }
#post-container :nth-child(3) { animation-delay: calc(var(--content-delay) + 100ms) }
#post-container :nth-child(4) { animation-delay: calc(var(--content-delay) + 175ms) }
#post-container :nth-child(5) { animation-delay: calc(var(--content-delay) + 250ms) }
#post-container :nth-child(6) { animation-delay: calc(var(--content-delay) + 325ms) }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后效果如图
&lt;img src=&quot;./comment.avif&quot; alt=&quot;评论效果&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>魔改Fuwari博客-添加友链</title><link>https://blog.kimbleex.top/posts/2025-10-09-fuwari-friends/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-10-09-fuwari-friends/</guid><pubDate>Thu, 09 Oct 2025 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Fuwari&lt;/code&gt;博客很好用，但是感觉少了一些页面，不能够跟好朋友愉快的分享，于是加了个友链，在我看自己博客的同时，可以时刻视奸我的好朋友们的博客页面。😂&lt;/p&gt;
&lt;h2&gt;添加友链&lt;/h2&gt;
&lt;h3&gt;添加友链页面文件&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;src\content\spec&lt;/code&gt;目录下新建文件&lt;code&gt;friends.md&lt;/code&gt;
在&lt;code&gt;src\types\config.ts&lt;/code&gt;文件约41行位置添加以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export enum LinkPreset {
  Home = 0,
  Archive = 1,
  About = 2,
+ Friends = 3,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;国际化i18n翻译&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;src\i18n\i18nKey.ts&lt;/code&gt;文件约35行位置添加以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    author = &apos;author&apos;,
    publishedAt = &apos;publishedAt&apos;,
    license = &apos;license&apos;,
+   friends = &apos;friends&apos;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照自己的语言，在 &lt;code&gt;src\i18n\languages&lt;/code&gt; 目录中编辑相应语言文件,以 &lt;code&gt;zh_CN.ts&lt;/code&gt; 为例，在约 38 行位置添加内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    [Key.author]: &apos;作者&apos;,
    [Key.publishedAt]: &apos;发布于&apos;,
    [Key.license]: &apos;许可协议&apos;,
+   [Key.friends]: &apos;友链&apos;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;src\constants\link-presets.ts&lt;/code&gt; 文件约 18 行位置添加内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [LinkPreset.Archive]: {
    name: i18n(I18nKey.archive),
    url: &apos;/archive/&apos;,
  },
+  [LinkPreset.Friends]: {
+    name: i18n(I18nKey.friends),
+    url: &apos;/friends/&apos;,
+  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建和配置页面的Astro文件&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;src\pages&lt;/code&gt;目录下复制原本的&lt;code&gt;about.astro&lt;/code&gt;文件，重命名为&lt;code&gt;friends.astro&lt;/code&gt;，在此文件中更改第 11 行、第 17 行和第19行的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- const aboutPost = await getEntry(&apos;spec&apos;, &apos;about&apos;)
+ const friendsPost = await getEntry(&apos;spec&apos;, &apos;friends&apos;)

- const { Content } = await render(aboutPost);
+ const { Content } = await render(friendsPost);

- &amp;lt;MainGridLayout title={i18n(I18nKey.about)} description={i18n(I18nKey.about)}&amp;gt;
+ &amp;lt;MainGridLayout title={i18n(I18nKey.friends)} description={i18n(I18nKey.friends)}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;在导航栏中添加友链页面&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;src\config.ts&lt;/code&gt; 文件约 48 行位置添加内容，注意要在 LinkPreset.About 末尾添加&lt;code&gt;,&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const navBarConfig: NavBarConfig = {
  links: [
      LinkPreset.Home,
      LinkPreset.Archive,
      LinkPreset.About,
+     LinkPreset.Friends,
    {
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建卡片效果友链&lt;/h3&gt;
&lt;p&gt;在之前创建的 &lt;code&gt;friends.astro&lt;/code&gt; 文件中编辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---

import MainGridLayout from &apos;../layouts/MainGridLayout.astro&apos;

import { getEntry } from &apos;astro:content&apos;
import Markdown from &apos;@components/misc/Markdown.astro&apos;
import I18nKey from &apos;../i18n/i18nKey&apos;
import { i18n } from &apos;../i18n/translation&apos;

const friendsPost = await getEntry(&apos;spec&apos;, &apos;friends&apos;)
if (!friendsPost) {
  throw new Error(&apos;Friend page content not found&apos;)
}
const { Content } = await friendsPost.render()

const items = [
  {
    title: &apos;Astro&apos;,
    imgurl: &apos;https://avatars.githubusercontent.com/u/44914786?s=48&amp;amp;v=4&apos;,
    desc: &apos;The web framework for content-driven websites. ⭐️ Star to support our work!&apos;,
    siteurl: &apos;https://github.com/withastro/astro&apos;,
    tags: [&apos;框架&apos;],
  },
]
---
&amp;lt;MainGridLayout title={i18n(I18nKey.friends)} description={i18n(I18nKey.friends)}&amp;gt;
  &amp;lt;div class=&quot;flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32&quot;&amp;gt;
    &amp;lt;div class=&quot;card-base z-10 px-9 py-6 relative w-full &quot;&amp;gt;
      &amp;lt;div class=&quot;grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-10 my-4&quot;&amp;gt;
        {items.map((item) =&amp;gt; (
          &amp;lt;a href={item.siteurl} target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
            class=&quot;group rounded-3xl bg-white/80 dark:bg-zinc-900/80 shadow-xl hover:shadow-2xl transition-all duration-300 p-6 flex items-center gap-6 border border-zinc-100 dark:border-zinc-800 hover:-translate-y-1&quot;
          &amp;gt;
            &amp;lt;div class=&quot;flex-shrink-0&quot;&amp;gt;
              &amp;lt;img src={item.imgurl} alt=&quot;站点头像&quot; class=&quot;w-20 h-20 rounded-full object-cover shadow-md border-4 border-white dark:border-zinc-800 group-hover:scale-105 transition-transform duration-300&quot; /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;flex-1 min-w-0&quot;&amp;gt;
              &amp;lt;div class=&quot;flex items-center gap-2 mb-1&quot;&amp;gt;
                &amp;lt;span class=&quot;text-2xl font-bold text-zinc-900 dark:text-zinc-100 group-hover:text-[var(--primary)] transition-colors duration-300 leading-tight&quot;&amp;gt;{item.title}&amp;lt;/span&amp;gt;
                {item.tags &amp;amp;&amp;amp; item.tags.length &amp;gt; 0 &amp;amp;&amp;amp; (
                  &amp;lt;span class=&quot;inline-flex items-center gap-1 text-base px-3 py-1 rounded-full bg-[var(--primary)]/10 text-[var(--primary)] font-semibold tracking-wide&quot;&amp;gt;
                    {item.tags.join(&apos; &apos;)}
                  &amp;lt;/span&amp;gt;
                )}
              &amp;lt;/div&amp;gt;
              &amp;lt;div class=&quot;text-zinc-600 dark:text-zinc-300 text-base mb-2 line-clamp-2 leading-relaxed font-medium&quot;&amp;gt;{item.desc}&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;ml-auto flex-shrink-0&quot;&amp;gt;
              &amp;lt;span class=&quot;inline-flex items-center justify-center w-10 h-10 rounded-full bg-[var(--primary)]/90 text-white shadow-lg group-hover:scale-110 transition-transform duration-300&quot;&amp;gt;
                &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; class=&quot;w-6 h-6&quot;&amp;gt;
                  &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M17 8l4 4m0 0l-4 4m4-4H3&quot; /&amp;gt;
                &amp;lt;/svg&amp;gt;
              &amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/a&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
      &amp;lt;Markdown class=&quot;mt-2&quot;&amp;gt;
        &amp;lt;Content /&amp;gt;
      &amp;lt;/Markdown&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;style&amp;gt;
    .line-clamp-2 {
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;!-- giscus评论 --&amp;gt;
&amp;lt;div style=&quot;margin-top: 20px;&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script src=&quot;https://giscus.app/client.js&quot;
        data-repo=&quot;kimbleex/fuwari_comments&quot;
        data-repo-id=&quot;R_kgDOPVt71Q&quot;
        data-category=&quot;General&quot;
        data-category-id=&quot;DIC_kwDOPVt71c4Ctm_O&quot;
        data-mapping=&quot;pathname&quot;
        data-strict=&quot;0&quot;
        data-reactions-enabled=&quot;1&quot;
        data-emit-metadata=&quot;0&quot;
        data-input-position=&quot;bottom&quot;
        data-theme=&quot;preferred_color_scheme&quot;
        data-lang=&quot;zh-CN&quot;
        crossorigin=&quot;anonymous&quot;
        async&amp;gt;
&amp;lt;/script&amp;gt;
&amp;lt;/MainGridLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;items&lt;/code&gt;内容即为友链的内容，可以自行&lt;code&gt;diy&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;最终效果&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./friends.avif&quot; alt=&quot;友链效果&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Kylin银河麒麟服务器安装和初始化</title><link>https://blog.kimbleex.top/posts/2025-09-09-kylinserver/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-09-09-kylinserver/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;一个比较坑的点在于你不管怎么去搜索&lt;code&gt;麒麟Kylin&lt;/code&gt;使用的包管理器，出来的结果都是&lt;code&gt;apt&lt;/code&gt;，但是实际上麒麟服务器使用的包管理工具是&lt;code&gt;yum&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;通电开机后它默认是带有一个桌面的，可以插上鼠标键盘显示器，进行初始化操作。&lt;/p&gt;
&lt;p&gt;鼠标右键桌面选择终端即可打开终端。&lt;/p&gt;
&lt;p&gt;终端输入&lt;code&gt;nkvers&lt;/code&gt;接口查看服务器的系统版本和内核信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E6%9F%A5%E7%9C%8B%E7%B3%BB%E7%BB%9F%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;系统版本和内核信息&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到我这台服务器的系统发行版本是&lt;code&gt;Kylin Linux Advanced Server release V10 (Halberd)&lt;/code&gt;；内核版本为&lt;code&gt;4.19.90-89.11.v2401.ky10.x86_64&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;开&lt;code&gt;SSH&lt;/code&gt;远程&lt;/h2&gt;
&lt;h3&gt;检查&lt;code&gt;openssh-server&lt;/code&gt;是否安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;yum list installed | grep openssh-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现以下输出说明已经安装好了:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ssh%E4%B8%8B%E8%BD%BD.png&quot; alt=&quot;ssh下载&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果没有输出则需要下载，下载指令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install openssh-server
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;SSH&lt;/code&gt;服务启动及开机自启配置&lt;/h3&gt;
&lt;p&gt;检查&lt;code&gt;openssh&lt;/code&gt;服务是否开启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl status sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现以下输出说明已经开启，否则需要开启：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ssh%E6%9C%8D%E5%8A%A1%E6%98%AF%E5%90%A6%E5%90%AF%E5%8A%A8.png&quot; alt=&quot;ssh服务是否启动&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果服务未启动，需要启动，启动指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后检查&lt;code&gt;openssh&lt;/code&gt;服务是否为开机自动启动:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl is-enabled sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果输出为&lt;code&gt;enabled&lt;/code&gt;说明已经设置为开机自启&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ssh%E6%9C%8D%E5%8A%A1%E5%BC%80%E6%9C%BA%E8%87%AA%E5%90%AF.png&quot; alt=&quot;ssh服务开机自启&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果输出为&lt;code&gt;disabled&lt;/code&gt;，需要启动，指令为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;网络信息查看&lt;/h2&gt;
&lt;p&gt;我这台麒麟服务器有4个网口，打开终端输入&lt;code&gt;ifconfig&lt;/code&gt;可以查看所有网络的信息:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./%E7%BD%91%E5%8F%A3%E4%BF%A1%E6%81%AF.png&quot; alt=&quot;网口信息&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要分配&lt;code&gt;ip地址&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;后续就可以通过&lt;code&gt;mobaXterm&lt;/code&gt;等工具通过&lt;code&gt;SSH&lt;/code&gt;远程麒麟服务器了。&lt;/p&gt;
</content:encoded></item><item><title>异或(XOR) &amp; 异或加密</title><link>https://blog.kimbleex.top/posts/2025-08-08-xor-encrypt/xor-encrypt/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-08-08-xor-encrypt/xor-encrypt/</guid><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上次与&lt;code&gt;异或(XOR)&lt;/code&gt;见面已经是2023年，那个时候只是应对&lt;code&gt;408&lt;/code&gt;对它进行了了解和学习。&lt;/p&gt;
&lt;p&gt;是时候学习一下&lt;code&gt;异或(XOR)&lt;/code&gt;了！&lt;/p&gt;
&lt;h2&gt;异或(XOR)&lt;/h2&gt;
&lt;p&gt;异或是一个运算方法，在计算机领域可以理解成跟耳熟能详的加减乘除一样，用于计算的。但是它一个&lt;code&gt;二进制&lt;/code&gt;运算符，是根据&lt;code&gt;二进制位&lt;/code&gt;来计算结果的。核心的计算规则就一句话:&lt;/p&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;p&gt;位相同异或为0，不同为1&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;打个比方，&lt;code&gt;a = 5&lt;/code&gt;，&lt;code&gt;b = 3&lt;/code&gt;，它们的二进制分别为：&lt;code&gt;a = 0101&lt;/code&gt;，&lt;code&gt;b = 0011&lt;/code&gt;，对他们进行异或运算可以得到结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  0101
^ 0011
------
  0110
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算结果为&lt;code&gt;0110&lt;/code&gt;，也就是10进制数字6。曾经在学习计算机组成原理的时候，了解到它经常被在逻辑控制单元里面担当一些&lt;code&gt;异或门&lt;/code&gt;来对信号进行控制的，但是原理都是对信号进行异或计算来达到控制的目的的。&lt;/p&gt;
&lt;h2&gt;异或加密&lt;/h2&gt;
&lt;p&gt;为什么会去了解到&lt;code&gt;异或加密&lt;/code&gt;呢？其实是在考虑博客前端加密而了解到的。参考了一些博客框架的加密方法，都是在博客的&lt;code&gt;frontmatter&lt;/code&gt;里面加一个密码字段，将明文的文章进行加密使之成为密文，这样密码和文章就不会暴露在前端页面中，加密过程中使用的&lt;code&gt;KEY&lt;/code&gt;就是密码字段。而对明文的加密方法通常就使用的是异或加密。&lt;/p&gt;
&lt;p&gt;通常使用的加密方法是&lt;code&gt;对称加密&lt;/code&gt;，也就是说加密和解密使用的是同一个&lt;code&gt;KEY&lt;/code&gt;，在构建时就会使用&lt;code&gt;KEY&lt;/code&gt;将文章加密成密文，在前端页面中，通过用户输入的密码直接进行解密，不需要进行密码的校验，因为如果输入的密码与&lt;code&gt;KEY&lt;/code&gt;一致会成功解密得到明文，否则解密失败。&lt;/p&gt;
&lt;p&gt;这样就可以很好的保护文章的隐私，防止文章被泄露。同时也不需要后端进行密码校验，而且密码也不会暴露在前端。&lt;/p&gt;
&lt;h2&gt;文章内容的加密和解密&lt;/h2&gt;
&lt;p&gt;根据我的资料查询，有很多种异或加密方式，但是基本都涉及到一个核心: &lt;strong&gt;将文章内容分块，每一块内容都跟&lt;code&gt;KEY&lt;/code&gt;进行异或计算得到密文&lt;/strong&gt;，解密也是一样的，将密文块逐个跟&lt;code&gt;KEY&lt;/code&gt;进行异或计算即可解密，所以这个过程一般来讲是由顺序的，否则解密成功组成明文也会乱序。&lt;/p&gt;
&lt;p&gt;一个字符一个字符的异或运算也是完全可以，生成文本每个字符的&lt;code&gt;Unicode&lt;/code&gt;码列表，同样的&lt;code&gt;KEY&lt;/code&gt;也需要生成&lt;code&gt;Unicode&lt;/code&gt;码列表，然后遍历文本的列表，逐个与&lt;code&gt;KEY&lt;/code&gt;列表遍历，打个比方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def xor_encrypt(text, key): 
    # 明文转ASCII码索引列表
    text_ = [ord(char) for char in text]
    # KEY转ASCII码索引列表
    key_ = [ord(char) for char in key]
    encrypted = []
    for i in range(len(text_)):
        # 逐个异或后转成ASCII索引对应的字符
        encrypted.append(chr(text_[i] ^ key_[i % len(key_)]))
    return &apos;&apos;.join(encrypted)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用上面的函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(xor_encrypt(&quot;hello world&quot;, &quot;123456&quot;))

# 输出
&quot;YW_XZF]AXQ&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就加密完成了，解密也一样，将密文块逐个跟&lt;code&gt;KEY&lt;/code&gt;进行异或计算即可解密。同样打个比方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def xor_decrypt(text, key):
    # 密文转ASCII码索引列表
    text_ = [ord(char) for char in text]
    # KEY转ASCII码索引列表
    key_ = [ord(char) for char in key]
    decrypted = []
    for i in range(len(text_)):
        # 逐个异或后转成ASCII索引对应的字符
        decrypted.append(chr(text_[i] ^ key_[i % len(key_)]))
    return &apos;&apos;.join(decrypted)

# 用上面的密文作为输入
print(xor_decrypt(&quot;YW_XZF]AXQ&quot;, &quot;123456&quot;))
# 输出
&quot;hello world&quot;

# 如果输入的密码不正确
print(xor_decrypt(&quot;YW_XZF]AXQ&quot;, &quot;34556&quot;))
# 输出
&quot;jcjml%rhtnb&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加密和解密的目的就达成了&lt;/p&gt;
</content:encoded></item><item><title>关于WPS自动化</title><link>https://blog.kimbleex.top/posts/2025-07-11-auto-wps/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-07-11-auto-wps/</guid><pubDate>Fri, 11 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;近期有一个客户现场，需要他们自己的&lt;code&gt;OA&lt;/code&gt;、&lt;code&gt;ERP&lt;/code&gt;、&lt;code&gt;PLM&lt;/code&gt;三个系统的自动化流程设计，在这些系统的交互过程中会产生&lt;code&gt;EXCEL&lt;/code&gt;文件的编辑和处理，所以就涉及到了&lt;code&gt;EXCEL&lt;/code&gt;文件的自动化处理。&lt;/p&gt;
&lt;p&gt;说是自动化处理，第一个想到的方法肯定是使用&lt;code&gt;Pandas&lt;/code&gt;库对数据进行一系列的编辑，但是不出意外就要出意外了，整个客户现场的环境都是加密的，系统下载的文件也全都是加密的，找客户的&lt;code&gt;IT&lt;/code&gt;咨询了一下，回复是无法进行解密，需要自己想办法...&lt;/p&gt;
&lt;p&gt;那没得办法，只能尝试去使用&lt;code&gt;EXCEL&lt;/code&gt;软件去操作，但是，意外又发生了，当我使用&lt;code&gt;EXCEL&lt;/code&gt;打开加密的文件时，还真打开了，因为登入使用的测试账户具有访问文件的权限，但是意外在哪？意外在在我使用&lt;code&gt;EXCEL&lt;/code&gt;打开文件后的&lt;code&gt;1min&lt;/code&gt;内，电脑直接强制关机了，后续咨询了客户的&lt;code&gt;IT&lt;/code&gt;，说他们会有版权问题，只能使用&lt;code&gt;WPS&lt;/code&gt;打开文件...&lt;/p&gt;
&lt;p&gt;那能怎么办，继续捣鼓...&lt;/p&gt;
&lt;p&gt;查找了一些资料，发现&lt;code&gt;WPS&lt;/code&gt;是有&lt;code&gt;COM&lt;/code&gt;接口的。所谓&lt;code&gt;COM&lt;/code&gt;接口，&lt;code&gt;COM(Component Object Model)&lt;/code&gt;是&lt;code&gt;Windows&lt;/code&gt;系统的一种技术，用于不同程序之间的交互。&lt;code&gt;WPS Office&lt;/code&gt;提供了&lt;code&gt;COM&lt;/code&gt;接口，允许外部程序如&lt;code&gt;Python&lt;/code&gt;通过&lt;code&gt;win32com.client&lt;/code&gt;模块控制&lt;code&gt;WPS&lt;/code&gt;的功能。&lt;/p&gt;
&lt;p&gt;这下算是看到了曙光，继续查找资料，找到了各个文件类型对应的&lt;code&gt;COM&lt;/code&gt;接口标识符。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;EXCEL文件&lt;/code&gt; 对应 &lt;code&gt;ET.Application&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;WORD文件&lt;/code&gt; 对应 &lt;code&gt;Ket.Application&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;PPT文件&lt;/code&gt; 对应 &lt;code&gt;WPP.Application&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;于是乎开始尝试，首先下载&lt;code&gt;Python&lt;/code&gt;依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pywin32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我以&lt;code&gt;EXCEL&lt;/code&gt;为例，我们先创建一个&lt;code&gt;WPS&lt;/code&gt;实例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import win32com.client

wps = win32com.client.Dispatch(&quot;ET.Application&quot;)
wps.Visible = True # 让wps实例在桌面可见
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就创建好了一个实例，然后我们打开一个&lt;code&gt;EXCEL&lt;/code&gt;文件，接着上文的代码添加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将excel_path修改成目标文件路径
workbook = wps.Workbooks.Open(excel_path)
# 读取excel中的sheet，指定sheet工作簿
sheet = workbook.Sheets(sheet_name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们的工作表对象和工作簿对象就都创建好了，后续就可以开始增删改查了，这里我写了几个方法，用来快速的读、写&lt;code&gt;EXCEL&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def read_wps_by_x_y(sheet, row_index, col_index):
    &quot;&quot;&quot;
    功能: 根据横、纵坐标读取EXCEL文件的值
    sheet: 传入的工作簿对象
    row_index: 要读取数据的横坐标
    col_index: 要读取数据的纵坐标
    返回值: 目标单元格的值
    &quot;&quot;&quot;
    return sheet.Cells(row_index,col_index).Value

def read_wps_by_line(workbook, sheet,start_row,end_row):
    &quot;&quot;&quot;
    功能: 读取EXCEL行
    workbook: 工作表对象
    sheet: 工作簿对象
    start_row: 读取的第一行的索引
    end_row: 读取的最后一行的索引
    返回值: 读取的所有数据，二维数组
    &quot;&quot;&quot;
    data = []
    # 按行读取数据
    for row_index in range(start_row, end_row + 1):
        row_data = []
        col_index = 1
        while sheet.Cells(row_index, col_index).Value is not None:  # 读取每一列直到空单元格
            row_data.append(sheet.Cells(row_index, col_index).Value)
            col_index += 1
        data.append(row_data)
    return data
    
def write_wps_by_x_y(workbook,sheet, row_index, col_index, data):
    &quot;&quot;&quot;
    功能: 根据单元格坐标写入wps
    workbook: 工作表对象
    sheet: 工作簿对象
    row_index: 要写入数据的横坐标
    col_index: 要写入数据的纵坐标
    data:写入值，置空表示清除单元格内容
    &quot;&quot;&quot;
    sheet.Cells(row_index, col_index).Value = data
    workbook.Save()
    

def write_wps_by_line(workbook,sheet,data,start_row,start_col):
    &quot;&quot;&quot;
    功能: 写入一整行数据
    workbook: 工作表对象
    sheet: 工作簿对象
    start_row: 开始写入行编号
    start_col: 开始写入列标号
    data: 要写入的数据，二维数组，表示多行，例如[[1,2,3],[4,5,6]]
    &quot;&quot;&quot;
    # 写入数据到表格
    num_rows = len(data)  # 数据的行数
    print(f&quot;读取到数据{num_rows}行&quot;)
    num_cols = len(data[0])  # 数据的列数
    sheet.Range(sheet.Cells(start_row, start_col), sheet.Cells(num_rows+start_row-1, num_cols)).Value = data
    # 保存表格
    workbook.Save()

def copy_sheet_to_excel_by_value(wps,workbook,sheet,save_path):
    &quot;&quot;&quot;
    功能: 将单独的sheet中的单元格都以值的格式保存到另一个excel文件
    wps: wps实例
    workbook: 工作表对象
    sheet: 工作簿对象
    save_path: 保存路径
    &quot;&quot;&quot;
    # 创建一个新的工作簿
    new_workbook = wps.Workbooks.Add()  # 新建工作簿
    new_sheet = new_workbook.Sheets(1)  # 获取新工作簿的第一个工作表
    new_sheet.Name = sheet.Name
    
    # 遍历原工作表的所有单元格，将公式转换为值
    used_range = sheet.UsedRange  # 获取工作表的已使用范围
    for row in range(1, used_range.Rows.Count + 1):  # 遍历行
        for col in range(1, used_range.Columns.Count + 1):
            cell = sheet.Cells(row, col)
            # 复制值
            new_sheet.Cells(row, col).Value = cell.Value 
            
    new_workbook.SaveAs(save_path)
    new_workbook.Close()
    


def close_wps(wps, workbook):
    &quot;&quot;&quot;
    功能: 关闭WPS和工作表
    wps: wps实例
    workbook: 工作表对象
    &quot;&quot;&quot;
    # 关闭表格
    workbook.Close()
    wps.Quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以操作&lt;code&gt;WPS&lt;/code&gt;来读取&lt;code&gt;EXCEL&lt;/code&gt;表格了。&lt;/p&gt;
&lt;p&gt;比如我要写入表格中的第&lt;code&gt;1&lt;/code&gt;行第&lt;code&gt;1&lt;/code&gt;列，写入&lt;code&gt;123&lt;/code&gt;，可以直接调用方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;write_wps_by_x_y(workbook,sheet,1,1,&quot;123&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我要读取表格中的第&lt;code&gt;1&lt;/code&gt;行第&lt;code&gt;1&lt;/code&gt;列的值，也可以直接调用方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;read_wps_by_x_y(sheet,1,1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我要写入一整行数据，写到第&lt;code&gt;1&lt;/code&gt;行，第&lt;code&gt;1&lt;/code&gt;列开始，数据为&lt;code&gt;[1,2,3,4,5,6]&lt;/code&gt;，可以直接调用方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;write_wps_by_line(workbook,sheet,[[1,2,3,4,5,6]],1,1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是还是建议使用&lt;code&gt;Pandas&lt;/code&gt;来操作&lt;code&gt;EXCEL&lt;/code&gt;文件，但是如果遇到上面的这种情况，这种方法也可以当作备选方法，也可以很好的去解决实际的需求问题。&lt;/p&gt;
</content:encoded></item><item><title>磁盘读写测速工具CrystalDiskMark</title><link>https://blog.kimbleex.top/posts/2025-07-02-crystaldiskmark/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-07-02-crystaldiskmark/</guid><pubDate>Wed, 02 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这段时间开发一个客户现场的&lt;code&gt;RPA&lt;/code&gt;流程，客户的机器真的太拉跨了，数据推送的时候太慢，正常机器&lt;code&gt;30min&lt;/code&gt;能推完的数据它需要&lt;code&gt;3h20min&lt;/code&gt;！客户那边数据催的又急，碰巧又是月末，忙的不可开交，接着又看到机器龟速推数据，只能感到深深的无力感...&lt;/p&gt;
&lt;p&gt;排查了很多要素，客户机器上部署数据处理中心&lt;code&gt;DC&lt;/code&gt;使用的数据库是&lt;code&gt;SQLite&lt;/code&gt;，数据库本身不带索引，排查了一下，发现实施人员在部署的时候已经创建了混合索引，但是其实大规模数据的话应该用&lt;code&gt;mySQL&lt;/code&gt;的，于是只能把中台那边关掉，&lt;code&gt;DC&lt;/code&gt;直推客户的&lt;code&gt;ATS(资金管理系统)&lt;/code&gt;，省去了&lt;code&gt;DC&lt;/code&gt;与中控台通信的时间。&lt;/p&gt;
&lt;p&gt;最后两天加班到&lt;code&gt;21:00&lt;/code&gt;之后，排查出问题在于机器的磁盘读写...&lt;/p&gt;
&lt;p&gt;使用的工具为&lt;code&gt;CrystalDiskMark&lt;/code&gt;，开源且好用的磁盘读写测速工具，它开源在不同队列深度和不通线程数的情况下去测试磁盘的读写速度。&lt;/p&gt;
&lt;p&gt;提供一个下载地址: &lt;a href=&quot;https://images.kimbleex.top/CrystalDiskMark6_0_2.zip&quot;&gt;CrystalDiskMark&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不说了，都是泪直接放图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./sp_comp.png&quot; alt=&quot;磁盘读写速率对比&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一些说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对比机器为我入职发的机器，普通机器&lt;/li&gt;
&lt;li&gt;参数Q表示队列深度，T代表线程数量&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;磁盘读写速度还没有普通机器的&lt;code&gt;1/10&lt;/code&gt;!!!&lt;/p&gt;
&lt;p&gt;已经让去申请新机器了，不然月底悲剧还会重演！&lt;/p&gt;
</content:encoded></item><item><title>UI自动化之Py Uiautomation</title><link>https://blog.kimbleex.top/posts/2025-06-13-uiautomation/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-06-13-uiautomation/</guid><pubDate>Fri, 13 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在&lt;code&gt;RPA&lt;/code&gt;开发一些网银页面自动化流程时，会出现一些情况，企业网银使用U盾的登陆页面有时会包含一些特殊的密码控件，使得他们的页面中的密码框元素不会显示在网页中，我们使用&lt;code&gt;F12&lt;/code&gt;调试工具选中的时候发现无法选中，页面中的其他元素都可以直接选中，但是密码框那边是不存在的，查找网页中的元素也找不到密码框的元素，这使得传统的网页自动化工具类似于&lt;code&gt;Selenium&lt;/code&gt;无法正常点击和输入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pwd_input.png&quot; alt=&quot;密码框无法点击示例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面的示例图可以看到，唯独密码框部分是没有选中的，查看网页源代码发现也是没有密码框的&lt;code&gt;input&lt;/code&gt;或者其他元素。&lt;/p&gt;
&lt;p&gt;于是激活密码框和输入也成了一个难题。&lt;/p&gt;
&lt;h2&gt;序&lt;/h2&gt;
&lt;p&gt;在技术攻坚时了解到了&lt;code&gt;Uiautomation&lt;/code&gt;这一个&lt;code&gt;UI&lt;/code&gt;自动化工具。它支持自动化&lt;code&gt;Win32&lt;/code&gt;、&lt;code&gt;MFC&lt;/code&gt;、&lt;code&gt;WPF&lt;/code&gt;、&lt;code&gt;Modern UI (Metro UI)&lt;/code&gt;、&lt;code&gt;Qt&lt;/code&gt;、&lt;code&gt;IE&lt;/code&gt;、&lt;code&gt;Firefox&lt;/code&gt;、&lt;code&gt;Chrome&lt;/code&gt;以及基于&lt;code&gt;Electron&lt;/code&gt;开发的应用程序。这个是它官方说的支持情况，其实不需要知道这么多，只需要知道它可以做&lt;code&gt;UI&lt;/code&gt;自动化控制&lt;code&gt;I/O&lt;/code&gt;程序就行了，常用的也就是&lt;code&gt;Win32&lt;/code&gt;桌面和一些网页端的&lt;code&gt;UI&lt;/code&gt;控制。&lt;/p&gt;
&lt;p&gt;微软做了一个应用程序&lt;code&gt;inspect&lt;/code&gt;用来方便读取整个桌面/应用程序的&lt;code&gt;UI框架&lt;/code&gt;，它可以直观的展示现在你所在屏幕内容的树形结构，而且显示了它们的各个属性，方便&lt;code&gt;UIA&lt;/code&gt;进行定位。&lt;/p&gt;
&lt;p&gt;提供一个下载地址： &lt;a href=&quot;https://images.kimbleex.top/inspect.exe&quot;&gt;Inspect.exe&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;再放一个程序的展示图，可以下载到本地琢磨琢磨。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./inspect.png&quot; alt=&quot;inspect&quot; /&gt;&lt;/p&gt;
&lt;p&gt;应用左边显示的是整个屏幕显示内容的树形结构，右边显示的是特定桌面元素的各项属性。&lt;/p&gt;
&lt;h2&gt;正片开始&lt;/h2&gt;
&lt;p&gt;好了准备工作就绪的话，现在就可以开始操作了，先捋起袖子不要影响我们敲代码。鼠标再换成无线的否则拖拖拉拉不舒服。&lt;/p&gt;
&lt;p&gt;回到上面的问题场景，我没法通过比如&lt;code&gt;Xpath&lt;/code&gt;定位使用&lt;code&gt;Selenium&lt;/code&gt;类似的工具进行元素的定位和点击，因为网页中根本不存在这个元素，但是它既然显示在你的桌面屏幕上，那么它就是一个桌面元素，所以我们就可以对他进行定位。再通过对他的点击操作实现目标。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./inspect_loc.png&quot; alt=&quot;uia识别&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到，这个密码框是一个&lt;code&gt;Pane&lt;/code&gt;桌面元素，所以就可以很自然的定位到它在对他进行一些操作。&lt;/p&gt;
&lt;p&gt;然后，我们也可以看到这个元素是没有&lt;code&gt;Name&lt;/code&gt;属性的，最多的定位是可以通过&lt;code&gt;Name&lt;/code&gt;属性进行定位的，所以需要找到一个可以轻松定位的元素，再通过相对位置一步一步找到这个密码输入框。&lt;/p&gt;
&lt;p&gt;因为涉及企业网银的一些信息，这里不具体一步步展示，打个比方，在这个网页上的&lt;code&gt;登录&lt;/code&gt;按钮它的元素元素类别是&lt;code&gt;Button&lt;/code&gt;，&lt;code&gt;Name&lt;/code&gt;属性值为&lt;code&gt;登录&lt;/code&gt;(事实上确实是这样)，于是我们可以通过一下代码对他进行一个简单的定位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import uiautomation as uia

# 你可以这么写
login_btn = uia.ButtonControl(Name=&quot;登录&quot;, [maxSearchDepth])

# 如果你还想点击
login_btn = uia.ButtonControl(Name=&quot;登录&quot;, [maxSearchDepth]).click()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;稍作解释：
&lt;code&gt;ButtonControl&lt;/code&gt;表示是按钮元素类型，还有很多比如&lt;code&gt;ExitControl&lt;/code&gt;、&lt;code&gt;TextControl&lt;/code&gt;
&lt;code&gt;Name&lt;/code&gt;是定位的根据，还有很多比如&lt;code&gt;ClassName&lt;/code&gt;、&lt;code&gt;AutomationId&lt;/code&gt;
&lt;code&gt;searchDepth&lt;/code&gt;表示搜索深度，表示在树形目录下的搜索深度&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以去看函数的源码做更多的了解，现在我要继续解决一下问题。&lt;/p&gt;
&lt;p&gt;现在我打个比方，上面我的密码框的位置，是在登录按钮的&lt;strong&gt;父元素&lt;/strong&gt;的&lt;strong&gt;下一个同级元素&lt;/strong&gt;的&lt;strong&gt;第三个子元素&lt;/strong&gt;的&lt;strong&gt;第一个子元素&lt;/strong&gt;，好像说的有点花里胡哨，不是的，我是要介绍一下层级定位的各个方法。&lt;/p&gt;
&lt;p&gt;现在我们可以开始寻找这个令人恼火的密码框了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import uiautomation as uia

# 登录按钮
login_btn = uia.ButtonControl(Name=&quot;登录&quot;, [maxSearchDepth])

fuck_pwd_input = login_btn.GetParentControl()
                          .GetNextSiblingControl()
                          .GetChildren()[2]
                          .GetFirstChildControl()
if fuck_pwd_input.Exists(maxSearchSeconds=10):
    # 激活它
    fuck_pwd_input.click()
    # 输入密码
    fuck_pwd_input.SendKeys(&quot;{my_password}&quot;, [waitTime])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;做一些解释：
&lt;code&gt;Exists&lt;/code&gt;表示搜索时间，参数为&lt;code&gt;maxSearchSeconds&lt;/code&gt;
&lt;code&gt;SendKeys&lt;/code&gt;表示输入，&lt;code&gt;waitTime&lt;/code&gt;表示输入前等待时间，同样&lt;code&gt;click&lt;/code&gt;函数也又这个参数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样问题就解决了；只是上面一级一级搜索的过程，我相信对照中文说明或者英文的字面意思都是看的明白的。&lt;/p&gt;
&lt;h2&gt;不要依赖&lt;/h2&gt;
&lt;p&gt;结合&lt;code&gt;Uiautomation&lt;/code&gt;工具能够弥补网页端自动化元素定位的一些缺陷，但是其实也是有问题的，这个工具不能够非常及时的刷新桌面应用程序，在一些场景下，桌面元素可能发生变动但是它并没有很及时的更新树结构，所以很多时候会发现使用&lt;code&gt;UIA&lt;/code&gt;去定位的时候返回为空，并没有成功的找到元素。&lt;/p&gt;
</content:encoded></item><item><title>知识扫盲2：关于RDP和VNC</title><link>https://blog.kimbleex.top/posts/2025-05-15-rdp-vnc/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-05-15-rdp-vnc/</guid><pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在项目开发过程中，因为是对客项目，很多需要到客户机器上开发，于是很多东西因为远程的原因会出各种各样的问题，便了解到了&lt;code&gt;RDP&lt;/code&gt;和&lt;code&gt;VNC&lt;/code&gt;这两种远程连接方式。&lt;/p&gt;
&lt;p&gt;在跟同事沟通过程中，他问我使用的是不是&lt;code&gt;RDP&lt;/code&gt;远程，我一头雾水，什么是&lt;code&gt;RDP&lt;/code&gt;，我直接回答的我用的&lt;code&gt;某desk&lt;/code&gt;远程方式，这让他十分无语，后来详细了解得知，像&lt;code&gt;某desk&lt;/code&gt;、&lt;code&gt;某葵&lt;/code&gt;等远程连接方式都是&lt;code&gt;RDP&lt;/code&gt;远程连接方式，而&lt;code&gt;VNC&lt;/code&gt;另是一种远程连接方式，&lt;code&gt;RPD&lt;/code&gt;和&lt;code&gt;VNC&lt;/code&gt;是目前最常用的两种远程的解决方案，碰到了就记录一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./crylaugh.png&quot; alt=&quot;尴尬&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;RDP&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RDP&lt;/code&gt;是&lt;code&gt;Remote Desktop Protocol&lt;/code&gt;的缩写，由微软开发的专有协议，允许用户远程连接和控制另一台计算机。它允许用户从不同地点访问他们的应用程序和数据，是一种基于TCP/IP协议的远程桌面协议。当正确配置时，RDP提供了跨多种平台的安全远程计算体验。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;RPD&lt;/code&gt;的一些特性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RDP&lt;/code&gt;针对&lt;code&gt;Windows&lt;/code&gt;系统进行了优化，特别是在使用&lt;code&gt;Windows&lt;/code&gt;特定应用程序和功能时，提供了更流畅的体验。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RDP&lt;/code&gt;将绘图命令发送到客户端，允许客户端重建图像。这种方法对&lt;code&gt;Windows&lt;/code&gt;应用程序效果很好。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RDP&lt;/code&gt;内置加密，并可以与&lt;code&gt;Windows&lt;/code&gt;身份验证机制集成。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RDP&lt;/code&gt;通常提供高级功能，特别是在&lt;code&gt;Windows&lt;/code&gt;系统上，如打印机重定向、文件传输和丰富的多媒体重定向。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;VNC&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;VNC&lt;/code&gt;是&lt;code&gt;Virtual Network Computing&lt;/code&gt;的缩写，代表“虚拟网络计算”，是一种图形桌面共享系统，允许用户远程控制另一台计算机。它通过网络连接传输键盘和鼠标事件从一个计算机到另一个计算机，并在另一个方向上传输远程桌面的实际像素值。&lt;code&gt;VNC&lt;/code&gt;是平台独立的，这意味着一个操作系统上的&lt;code&gt;VNC&lt;/code&gt;查看器可以连接到同一操作系统或不同操作系统上的&lt;code&gt;VNC&lt;/code&gt;服务器。有许多适用于不同平台的&lt;code&gt;VNC&lt;/code&gt;应用程序，通常用于远程技术支持和在远离家庭或办公室时访问PC中的文件。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;VNC&lt;/code&gt;的一些特性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VNC&lt;/code&gt;在&lt;code&gt;Windows&lt;/code&gt;上的性能可能不如&lt;code&gt;RDP&lt;/code&gt;，但其平台中立的特性使其适用于广泛的操作系统。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VNC&lt;/code&gt;通过发送远程桌面的实际像素值来工作，这使其更通用兼容，但对于动态内容可能效率较低。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VNC&lt;/code&gt;可能需要额外的配置或第三方工具来实现类似的加密和安全级别。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VNC&lt;/code&gt;在附加功能方面通常更基本，主要关注远程屏幕共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一些经验&lt;/h2&gt;
&lt;p&gt;值得一提的是，&lt;code&gt;RDP&lt;/code&gt;很多情况下是会被检测出来的，在某些比如银行项目的开发过程中，银行的防爬机制会检测出你是通过&lt;code&gt;RDP&lt;/code&gt;远程控制的电脑，从而进行一些类似于不让你输入密码的反爬操作。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;VNC&lt;/code&gt;就不会被检测出来，因为&lt;code&gt;VNC&lt;/code&gt;是通过发送远程桌面的实际像素值来工作的，所以&lt;code&gt;VNC&lt;/code&gt;的反爬机制是通过检测你是否在发送远程桌面的实际像素值来进行反爬的。相对来讲被检测出来的概率会小很多。&lt;/p&gt;
</content:encoded></item><item><title>初识RPA</title><link>https://blog.kimbleex.top/posts/2025-05-06-rpa/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-05-06-rpa/</guid><pubDate>Tue, 06 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;误打误撞找的工作是&lt;code&gt;RPA&lt;/code&gt;开发工程师，其实说实话以前根本不知道是个干啥的岗位，只知道是用&lt;code&gt;Python&lt;/code&gt;进行开发，没想到面试通过了就开始干活了。&lt;/p&gt;
&lt;p&gt;现在入职了半个多月了，对&lt;code&gt;RPA&lt;/code&gt;也算是有了一些了解，于是就记录一下，个人感觉还是比较有意思的，以后成为&lt;code&gt;RPA&lt;/code&gt;大哥了再深入探讨。&lt;/p&gt;
&lt;p&gt;所谓&lt;code&gt;RPA&lt;/code&gt;，就是&lt;code&gt;Robot Process Automation&lt;/code&gt;，即机器人流程自动化。说白了，一些重复性的东西我们天天干也会烦，于是就把这些事情用一些程序、脚本之类的东西设计集成好，再给他设定一个定时任务，比如每天早上8:00执行，这样就可以解放我们的双手了。&lt;/p&gt;
&lt;p&gt;打个比方，上面打的比方可能还不是很能体会，现在你带入自己是一个商户，所有的收入的钱都会汇总到一张银行卡里，于是你每天都会去登录银行的&lt;code&gt;APP&lt;/code&gt;去查看自己今天收入了多少钱，再具体一点可能每天中午和晚上都会去查。&lt;/p&gt;
&lt;p&gt;众所周知，登录银行&lt;code&gt;APP&lt;/code&gt;要刷脸，每次都做这些重复性的事情肯定会很烦(当然，看着自己的钱变多我是不会烦的，我只会快乐)，于是我们就可以把一整个流程集成到一个脚本程序里，每天定时中午和晚上去执行这个脚本程序，然后把我们的余额数据推送给我们的邮箱或者手机短信，这样就不需要我们主动去看，被动接收就可以了。&lt;/p&gt;
&lt;p&gt;当然，这只是一个简单的例子，实际的&lt;code&gt;RPA&lt;/code&gt;开发工程会更加复杂，比如会涉及到数据的处理、数据的提取、数据的存储、数据的传输、数据的分析、数据的可视化等等。&lt;/p&gt;
&lt;p&gt;在一整个开发流程中需要一个数据中心，我们把它叫做&lt;code&gt;Data Center&lt;/code&gt;，他是一个数据加工厂和数据中转站，它可以接收我们的脚本执行时收集到的数据，并加以处理和转化，这种处理可能包括对&lt;code&gt;excel/csv/pdf的解析和提取&lt;/code&gt;，再标准化数据格式，完成后再推送给目标的用户/系统。&lt;/p&gt;
&lt;p&gt;此外，我们还需要一个中控台跟&lt;code&gt;Data Center&lt;/code&gt;进行交互，这个中控台需要统筹所有的脚本程序，它可以读取并安排所有的定时任务，并可以传入所有脚本程序执行时的参数，&lt;code&gt;DC&lt;/code&gt;收集到的数据也需要上传到中控台，中控台可以对数据进行可视化展示，也可以对数据进行分析和处理，最后再把数据推送给目标的用户/系统。&lt;/p&gt;
&lt;p&gt;在实际的项目开发过程中，脚本程序的开发是基于一个设计器去开发的，设计器可以跟&lt;code&gt;DC&lt;/code&gt;和中控台去联动，这样中控台可以读取到客户机器上所有的任务，这样可以更好的去统筹各个任务的执行时间，&lt;code&gt;DC&lt;/code&gt;可以适配所有任务过程中涉及到的数据类型和数据格式进行规整，最后上传中控台并转发给客户。&lt;/p&gt;
&lt;p&gt;这样一个完整的&lt;code&gt;RPA&lt;/code&gt;开发工程就可以完成了，当然，具体开发的难度取决于业务的复杂程度，需求才是大哥！(一定要按照需求的需求文档进行开发，否则他说你的开发不满足需求那可要重新开发了，所以遇到一个好的需求才是开发最好的动力)&lt;/p&gt;
</content:encoded></item><item><title>记录一下浙江保融科技股份有限公司的Offer</title><link>https://blog.kimbleex.top/posts/2025-04-17-fingard-offer/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-04-17-fingard-offer/</guid><pubDate>Thu, 17 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;时隔3个星期的空窗期也是重新找到了工作。&lt;/p&gt;
&lt;p&gt;许骏亮，您好：&lt;/p&gt;
&lt;p&gt;我们很荣幸的通知您，浙江保融科技股份有限公司（以下简称公司）将正式录用您，相关信息通知如下：&lt;/p&gt;
&lt;p&gt;1、与您相关的录用信息如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;计划入职时间： 2025-04-18，工作地点：杭州 ；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;岗位名称： 初级软件工程师 ，岗位职级：E1 ，向***汇报工作；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;劳动合同期限 3 年，试用期： 3个月 ；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;试用期 **** 元（税前），转正后 ***** 元（税前）；其中70%为岗位（职务）工资，30%为绩效工资。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工作日午餐补贴：每餐15元，并提供每月 50 元的市内交通补贴和每月50 元的通讯补贴；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如有出差，出差期间按照公司出差制度享受出差补贴；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公司将根据政府规定和公司政策提供养老、医疗、住房、带薪年假等各项福利，详情参见公司政策。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公司发薪日为每月12日，工资发放地：杭州，公司根据国家规定代扣代缴个人所得税。请勿私下讨论或泄露工资信息，否则将涉嫌严重违反公司规章制度，公司有权单方解除劳动关系。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2、报到注意事项如下：&lt;/p&gt;
&lt;p&gt;（1）报到地址：*********&lt;/p&gt;
&lt;p&gt;（2）报到准备材料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;身份证原件及复印件（2份）&lt;/li&gt;
&lt;li&gt;毕业证书、学位证书原件及复印件（1份），公司将根据毕业证书、学位证书信息进行学历及学位真实性验证&lt;/li&gt;
&lt;li&gt;工资卡（卡号、开户行）准备：北京-工商银行工资卡；上海-招商银&amp;gt;行工资卡；深圳-交通银行工资卡；广州、成都-建设银行工资卡；济南-中信银行工资卡；西安-中国银行工资卡；南京--民生银行工资卡；其他城市-中国农业银行工资卡&lt;/li&gt;
&lt;li&gt;离职证明原件（1份）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3、其他信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请在确认此入职通知后，按约定报到日期来公司办理入职手续并签订劳动合同，逾期此入职通知书失效。&lt;/li&gt;
&lt;li&gt;此通知发出后，公司有权对候选人做背景调查，如果背调中发现跟简历或者面试时所述出现不符或者通过您提供的被调联系方式公司无法联络、背调时，公司有权取消录用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;预祝您在保融科技，与我们一起，奋斗赢得明天，踏上新的事业高峰！&lt;/p&gt;
&lt;p&gt;如对以上内容有疑问，请联系HR： ***&lt;/p&gt;
&lt;p&gt;浙江保融科技股份有限公司 人力资源部
2025-04-17&lt;/p&gt;
</content:encoded></item><item><title>算法详解3--最长回文子串</title><link>https://blog.kimbleex.top/posts/2025-03-17-algorithm-longestpalindrome/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-03-17-algorithm-longestpalindrome/</guid><pubDate>Mon, 17 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./ques.png&quot; alt=&quot;题目描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;补充一点: 回文串就是正着读和反着读都一样的字符串。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;题目看着很简单不是吗？&lt;/strong&gt; 实则不然，算法题越是题目描述的简单的，可能背后的逻辑就越复杂。&lt;/p&gt;
&lt;h3&gt;一个顺着题目描述的解法&lt;/h3&gt;
&lt;p&gt;一个比较容易想到的解法是：首先判断字符串的长度，如果长度为0或者1，那么就可以直接返回了，本身就是最长回文子串。&lt;/p&gt;
&lt;p&gt;如果长度为2，那么就判断这个字符串所包含的两个字符是不是一样的字符，如果是，那么返回字符串本身，如果不是，那么返回字符串的第一个字符。&lt;/p&gt;
&lt;p&gt;如果字符串长度大于2，那么需要遍历字符串，每次遍历时判断子字符串是不是回文子串，如果是的话，那就直接返回。&lt;/p&gt;
&lt;p&gt;如果字符串以上条件都不满足，即它长度大于2，并且内部没有长度大于1的回文子串，那就返回字符串的第一个字符。&lt;/p&gt;
&lt;p&gt;根据上面的思路可以完成代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution:
    def longestPalindrome(self, s: str) -&amp;gt; str:
        res = &apos;&apos;
        if len(s) &amp;lt;= 1:
            return s
        if len(s) == 2:
            if s[0] == s[1]:
                return s
            else:
                return s[0]
        for i in range(len(s)):
            rest = s[i+1:]
            temp_s = s[i]
            for j in rest:
                temp_s += j
                if temp_s == temp_s[::-1]:
                    res = temp_s if len(temp_s) &amp;gt;= len(res) else res
        if res == &apos;&apos;:
            return s[0]
        return res
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试提交，通过，时间复杂度为&lt;code&gt;O(n^3)&lt;/code&gt;。
&lt;img src=&quot;./res1.png&quot; alt=&quot;暴力求解&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;有优雅一些的解法吗？当然😎&lt;/h3&gt;
&lt;p&gt;很显然上面那种解法不断的判断字符串的长度，太不优雅了！&lt;/p&gt;
&lt;p&gt;经过思考，题目要求的是最长回文子串，那么我们换个思路，从字符串本身去最长子串，每次判断最长字串是否为回文子串，如果是，那么它肯定就是最长回文子串，那么就返回，如果不是，那么就缩短最长子串的长度，继续判断。&lt;/p&gt;
&lt;p&gt;这就类似于&lt;strong&gt;滑动窗口&lt;/strong&gt;的思想，比如输入的字符串长度为10，那么就直接令窗口大小为10，判断窗口内的字符串是否为回文子串，如果是，那么就返回，如果不是，那么就缩短窗口大小，通过不断在父字符串上的滑动继续判断。&lt;/p&gt;
&lt;p&gt;开始手搓！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution:
    def longestPalindrome(self, s: str) -&amp;gt; str:
        res = s[0]
        window = len(s)
        while window &amp;gt;= 1:
            for i in range(len(s)-window+1):
                temp_s = s[i:i+window]
                if temp_s == temp_s[::-1]:
                    res = temp_s if len(temp_s) &amp;gt; len(res) else res
                    return res
            window -= 1
        return res
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过不断的减小窗口大小，就能够第一时间找出最长回文子串。&lt;/p&gt;
&lt;p&gt;尝试提交，通过，时间复杂度为&lt;code&gt;O(n^3)&lt;/code&gt;，好像跟上面的解法是一样的时间消耗，但是可以看到运行所花的时间是第一种算法的50%都不到。也算是优雅了！
&lt;img src=&quot;./res_nice.png&quot; alt=&quot;优雅求解&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>算法详解2--两数相加</title><link>https://blog.kimbleex.top/posts/2025-03-16-algorithm-twonumadd/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-03-16-algorithm-twonumadd/</guid><pubDate>Sun, 16 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./ques.png&quot; alt=&quot;题目描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;题目说的很详细了，也就是把每个数倒序写成链表进行输入，然后让你计算两个倒序数组的和，要保证跟预期的结果一样。&lt;/p&gt;
&lt;p&gt;首先应该考虑的是&lt;strong&gt;两个数组的长度问题&lt;/strong&gt;，对于链表的每一位进行加法运算，如果两个列表长度不一致，那么就需要在短的列表后面补零，然后再进行加法运算。&lt;/p&gt;
&lt;p&gt;然后就是&lt;strong&gt;进位问题&lt;/strong&gt;，对于每一位的加法运算，都需要考虑进位问题，也就是如果当前位的和大于等于10，那么就需要进位，然后在下一位的加法运算中加上进位。这是这题的难点。&lt;/p&gt;
&lt;p&gt;最后就是&lt;strong&gt;链表&lt;/strong&gt;这一数据结构本身的要求，链表只能通过指针遍历来访问每一个节点，所以需要注意指针的指向。&lt;/p&gt;
&lt;p&gt;开始手搓！&lt;/p&gt;
&lt;p&gt;题目已经给了链表的定义，所以我们可以直接使用这个数据结构。题目给出的的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据定义，我们可以通过&lt;code&gt;.val&lt;/code&gt;获取指针所指向节点的值，通过&lt;code&gt;.next&lt;/code&gt;获取指针所指向节点的下一个节点。&lt;/p&gt;
&lt;h2&gt;最后解题&lt;/h2&gt;
&lt;p&gt;我们可以定义一个新的链表，保存最后的运算结果；定义一个变量保存进位的值；然后通过判断链表是否结束，进行加法运算来确定新链表最新位的值和下一次的进位的值。&lt;/p&gt;
&lt;p&gt;然后链表的指针和新链表的指针全部后移，继续循环。如果循环结束后进位不为0，那么就需要在新链表的末尾加上一个值为1的节点。&lt;/p&gt;
&lt;p&gt;最后返回新链表的头节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -&amp;gt; Optional[ListNode]:
        res = cur = ListNode()
        adder = 0

        while l1 or l2:
            num1 = l1.val if l1 else 0
            num2 = l2.val if l2 else 0
            cur.next = ListNode((num1+num2+adder)%10)
            adder = (num1+num2+adder)//10

            cur = cur.next

            if l1: l1 = l1.next
            if l2: l2 = l2.next
        
        if adder: cur.next = ListNode(1)
        return res.next
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试提交，通过，时间复杂度为&lt;code&gt;O(Max(N,M))&lt;/code&gt;，其中&lt;code&gt;N M&lt;/code&gt;为两个链表的长度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./res.png&quot; alt=&quot;求解&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>算法详解1--两数求和</title><link>https://blog.kimbleex.top/posts/2025-03-15-algorithm-twonumsum/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-03-15-algorithm-twonumsum/</guid><pubDate>Sat, 15 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./ques.png&quot; alt=&quot;题目描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;h3&gt;先说一种很容易想到的暴力解法&lt;/h3&gt;
&lt;p&gt;暴力解法的思路很简单，就是遍历数组，对于每一个元素，都去遍历数组中剩下的元素，判断是否有两个元素的和等于目标值，如果有，就返回这两个元素的下标。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution(object):
    def twoSum(self, nums, target):
        for i in range(len(nums)):
            rest = nums[i+1:]
            for j in range(len(rest)):
                if nums[i] + rest[j] == target:
                    return [i, i+j+1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试提交，通过，时间复杂度为&lt;code&gt;O(n^2)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./res1.png&quot; alt=&quot;暴力求解&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;显然上面的方法不够优雅，再说一种优雅的解法&lt;/h3&gt;
&lt;p&gt;暴力解法是把每一个数都遍历，然后返回下标，但是这个遍历的过程显然是太过于耗时了，那么我们能不能使用一个数据结构把已经遍历过的数据存储起来，然后往后遍历的时候，求和的时候直接去除先前数据的下标呢？&lt;/p&gt;
&lt;p&gt;有的，有的兄弟！我们可以使用字典来存储，当然，你也可以叫他哈希表(&lt;code&gt;HashMap&lt;/code&gt;)，其实在&lt;code&gt;Python&lt;/code&gt;中，这就是字典，为什么叫他哈希表呢，这是因为这个存储思想就是基于操作系统中哈希存储的。&lt;/p&gt;
&lt;p&gt;那么我们可以这样来操作：&lt;strong&gt;我们遍历所有的数据，以数据的值为键，以它的下标为值，存储到哈希表中，然后每次都判断目标的值和当前所遍历的值的差是否在哈希表中，如果在，直接返回当前数的下标和哈希表中数的下标，否则继续。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;开始手搓！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution(object):
    def twoSum(self, nums, target):
        num_dict = {}
        for i in range(len(nums)):
            if target - nums[i] in num_dict.keys():
                return [i, num_dict[target-nums[i]]]
            else:
                num_dict[nums[i]] = i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试提交，通过，时间复杂度为&lt;code&gt;O(n)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./res_nice.png&quot; alt=&quot;优雅求解&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;可以看到，执行时间从2172ms降到了3ms，效率提升了700倍！自然就变得优雅了。&lt;/p&gt;
</content:encoded></item><item><title>离职申请</title><link>https://blog.kimbleex.top/posts/2025-03-14-resign-berkinarts/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-03-14-resign-berkinarts/</guid><pubDate>Fri, 14 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;尊敬的领导:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;感谢自我入职以来公司对我的关心和栽培，承蒙各位同事的关照，我在这份工作中收获颇多，也成长了不少。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我想大抵是最近暖潮逼近，却又忽逢天气潮湿降雨，空气也是异常湿润，家里洗的衣服不干，裤子也不干，甚至墙壁也不干，我想我也跟它们一样，我也不干了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;于是经过本人郑重考虑，因个人原因，申请于2025年3月14日离职，希望各位领导能够理解。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;祝好。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;许骏亮 杭州 2025年3月14日&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>偶然发现的Vercel托管文件/图床的好功能</title><link>https://blog.kimbleex.top/posts/2025-02-20-vercel-file-listing/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-02-20-vercel-file-listing/</guid><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近在用Vercel托管文件，发现了一个好功能，就是Vercel的托管目录可以自动生成目录列表，非常方便。&lt;/p&gt;
&lt;h2&gt;功能展示&lt;/h2&gt;
&lt;p&gt;在常规的操作中，当使用&lt;code&gt;Github&lt;/code&gt; + &lt;code&gt;Vercel&lt;/code&gt; + 域名的方式来托管仓库，以方便远程访问文件的过程中，当直接访问域名地址，会出现&lt;code&gt;404&lt;/code&gt;报错，但是当具体到文件名去访问时，就可以访问到文件内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./bad.avif&quot; alt=&quot;bad&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;一个很好的接口&lt;/h2&gt;
&lt;p&gt;通过查询资料得知，如果想要在访问域名的时候有页面展示，需要在仓库的根目录下创建一个&lt;code&gt;index.html&lt;/code&gt;，这样在访问仓库绑定的域名时，就可以展示出默认网页的内容，但是很多情况下我只希望能看到我这个仓库的文件，并且很快的预览。&lt;/p&gt;
&lt;p&gt;最后通过阅读&lt;code&gt;Vercel&lt;/code&gt;官方文档后找到了它自带的一个功能接口，在设置里面的&lt;code&gt;Advanced&lt;/code&gt;选项中，有一个&lt;code&gt;Directory Listing&lt;/code&gt;选项，当开启选项后，如果在一个目录中没有&lt;code&gt;index.html&lt;/code&gt;文件，&lt;code&gt;Vercel&lt;/code&gt;会自动生成该目录下的文件索引，并且可以预览文件内容/图片，非常方便。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./good.avif&quot; alt=&quot;good&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这个功能非常实用，特别是对于一些小型的文件/图片托管，可以很方便的预览文件内容，并且不需要额外的配置，只需要在&lt;code&gt;Vercel&lt;/code&gt;的设置中开启选项即可，非常方便。&lt;/p&gt;
&lt;p&gt;但是需要注意的时，当你不希望别人通过域名可以看到你的仓库中的内容时，最好是关闭这个选项；当然有更好的方法，可以将&lt;code&gt;Github&lt;/code&gt;仓库设置为私有仓库，这样就可以避免别人通过域名看到你的仓库中的内容了。&lt;/p&gt;
</content:encoded></item><item><title>关于订阅集成工具Follow</title><link>https://blog.kimbleex.top/posts/2025-02-06-follow/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2025-02-06-follow/</guid><pubDate>Thu, 06 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近了解到一个名为Follow的RSS订阅集成工具，到目前已经体验了一个多月，总体体验还不错，记录一下。&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Follow&lt;/code&gt;目前完整的玩法体验是需要邀请码的，邀请码是需要已激活账号花费&lt;code&gt;100 Power&lt;/code&gt;才能够进行生成的，&lt;code&gt;Power&lt;/code&gt;是&lt;code&gt;Follow&lt;/code&gt;的货币，目前仅能够通过每日签到根据活跃度获取，举个例子，我目前是&lt;code&gt;Level 1&lt;/code&gt;，等级是根据你在所有用户中的排名来设定的，每天刷满活跃后，可以获取大约&lt;code&gt;1.6 Power&lt;/code&gt;，所以还是比较难获取的 。&lt;/p&gt;
&lt;p&gt;所以如果想激活账号的话，推荐去海鲜市场，大约&lt;code&gt;2~5 RMB&lt;/code&gt;即可购买到激活码，如果你有朋友可以直接伸手索取，当然求人不如求己。&lt;/p&gt;
&lt;h2&gt;下载&lt;/h2&gt;
&lt;p&gt;项目地址 : &lt;a href=&quot;https://github.com/RSSNext/Follow&quot;&gt;Follow&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Follow&lt;/code&gt; 目前提供了网页端和客户端，网页端可以直接在浏览器中打开使用，客户端可以在项目地址的&lt;code&gt;Release&lt;/code&gt;页面自行下载安装。登录或者注册可以直接使用&lt;code&gt;Github&lt;/code&gt;账户即可完成绑定。&lt;/p&gt;
&lt;p&gt;这里还是推荐客户端，网页端是最近不久才开发的，我也体验了一段时间，在图片展示和视频展示上还是会有很多&lt;code&gt;Bug&lt;/code&gt;，所以如果现在打算入门的话，建议使用客户端。&lt;/p&gt;
&lt;h2&gt;玩法&lt;/h2&gt;
&lt;h3&gt;1. 添加订阅源&lt;/h3&gt;
&lt;p&gt;万物皆可订阅，我这里提供本博客的&lt;code&gt;RSS&lt;/code&gt;订阅地址 : &lt;code&gt;https://xujunliang666.com/atom.xml&lt;/code&gt;，当你下载好客户端之后，可以点击左上角的&lt;code&gt;+&lt;/code&gt;号，并在右边界面中输入订阅地址，选择你想要添加的订阅源即可。
&lt;img src=&quot;./rss_add.avif&quot; alt=&quot;rss_add&quot; /&gt;
&lt;img src=&quot;./rss_add2.avif&quot; alt=&quot;rss_add2&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. 订阅源展示&lt;/h3&gt;
&lt;p&gt;添加好订阅源之后，就可以在&lt;code&gt;Follow&lt;/code&gt;中看到订阅源的内容了，&lt;code&gt;Follow&lt;/code&gt;提供了多种展示方式，包括&lt;code&gt;文章&lt;/code&gt;、&lt;code&gt;社交媒体&lt;/code&gt;、&lt;code&gt;图片&lt;/code&gt;等，可以根据自己的喜好进行选择。&lt;br /&gt;
&lt;img src=&quot;./rss_show.avif&quot; alt=&quot;rss_show&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关于订阅源的获取&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RssHub&lt;/code&gt;可以支持将各种各样社交媒体等平台的内容转化为&lt;code&gt;RSS&lt;/code&gt;，具体可以参考&lt;a href=&quot;https://docs.rsshub.app/&quot;&gt;&lt;code&gt;RssHub&lt;/code&gt;&lt;/a&gt;的文档。仅需要提供你想要订阅的用户的&lt;code&gt;UserID&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;当然&lt;code&gt;Follow&lt;/code&gt;也支持搜索订阅源，在&lt;code&gt;Follow&lt;/code&gt;的搜索框中输入你想要订阅的社交媒体用户名，即可搜索到对应的订阅源，然后点击&lt;code&gt;Follow&lt;/code&gt;即可添加订阅源。不是所有的用户都可以搜索到的，更多的还是需要自己去&lt;code&gt;RssHub&lt;/code&gt;中制作。&lt;/p&gt;
</content:encoded></item><item><title>Go 安装问题汇总解决</title><link>https://blog.kimbleex.top/posts/2024-12-25-golang-install/inedx/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-12-25-golang-install/inedx/</guid><pubDate>Wed, 25 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;以下错误会在在官网安装完&lt;code&gt;Go&lt;/code&gt;语言之后，到&lt;code&gt;Vscode&lt;/code&gt;安装语言拓展，新建一个&lt;code&gt;.go&lt;/code&gt;文件之后。&lt;/p&gt;
&lt;h2&gt;1. 无法在GOROOT或GOPATH中找到Go&lt;/h2&gt;
&lt;p&gt;在安装完成后，&lt;code&gt;vscode&lt;/code&gt;报错无法在&lt;code&gt;GOROOT&lt;/code&gt;或&lt;code&gt;GOPATH&lt;/code&gt;中找到&lt;code&gt;Go&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;failed to find the &quot;go&quot; binary in either goroot() or path()...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查系统的环境变量中的&lt;code&gt;GOROOT&lt;/code&gt;和&lt;code&gt;GOPATH&lt;/code&gt;，确保它们指向正确的&lt;code&gt;Go&lt;/code&gt;安装目录。
&lt;strong&gt;重要&lt;/strong&gt;：重启电脑！ 重启电脑！ 重启电脑！&lt;/p&gt;
&lt;h2&gt;2. 无法成功安装Go插件&lt;/h2&gt;
&lt;p&gt;在安装&lt;code&gt;Go&lt;/code&gt;插件时，&lt;code&gt;vscode&lt;/code&gt;报错无法成功安装&lt;code&gt;Go&lt;/code&gt;插件。&lt;/p&gt;
&lt;p&gt;这是代理问题，换成国内的代理即可，打开&lt;code&gt;CMD&lt;/code&gt;，输入一下命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go env -w GO111MODULE=on
go env -w GOPROXY=https://proxy.golang.com.cn,direct
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启&lt;code&gt;Vscode&lt;/code&gt;即可。&lt;/p&gt;
</content:encoded></item><item><title>关于发布Python库全过程总结和踩坑记录</title><link>https://blog.kimbleex.top/posts/2024-11-27-python-package-upload/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-11-27-python-package-upload/</guid><pubDate>Wed, 27 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;兴致来了，打算做一个自己的&lt;code&gt;Python&lt;/code&gt;工具库，发布到&lt;code&gt;PyPi&lt;/code&gt;上，这样可以自己在生活工作的时候直接通过&lt;code&gt;pip install&lt;/code&gt;来进行安装和使用。&lt;/p&gt;
&lt;p&gt;我的库名最后决定为&lt;a href=&quot;https://pypi.org/project/HandyToolsPy/0.1.1/&quot;&gt;HandyToolsPy&lt;/a&gt;，也就是便捷工具库，里面包含了一些自己常用的工具函数和有趣的项目。&lt;/p&gt;
&lt;p&gt;欢迎大家&lt;code&gt;pip install&lt;/code&gt;食用。&lt;/p&gt;
&lt;p&gt;如果有想加入这个库的好点子请告诉我，我会积极反馈并考虑加入其中。&lt;/p&gt;
&lt;h2&gt;1. 准备工作&lt;/h2&gt;
&lt;h3&gt;1.1 创建目录&lt;/h3&gt;
&lt;p&gt;需要到一个空目录下，创建一个文件夹和&lt;code&gt;setup.py&lt;/code&gt;、&lt;code&gt;README.md&lt;/code&gt;、&lt;code&gt;LICENSE&lt;/code&gt;文件。文件夹名就是库名，比如我创建的文件夹名就是&lt;code&gt;HandyToolsPy&lt;/code&gt;。
&lt;img src=&quot;./dist_struct.avif&quot; alt=&quot;目录结构&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1.2 目录功能介绍&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;README.md&lt;/code&gt;即是库的说明文档，&lt;code&gt;LICENSE&lt;/code&gt;是库的许可证，我选择的是&lt;code&gt;GPL 3.0&lt;/code&gt;许可证。其他更多的许可证请至&lt;a href=&quot;https://choosealicense.com/&quot;&gt;这里&lt;/a&gt;查看和使用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;setup.py&lt;/code&gt;即为你的库的管理工具，&lt;code&gt;Python&lt;/code&gt;官网更喜欢使用&lt;code&gt;.toml&lt;/code&gt;文件进行管理，但是我都捣鼓了一下，发现还是&lt;code&gt;setup.py&lt;/code&gt;的形式比较方便，而且&lt;code&gt;requests&lt;/code&gt;库的作者写了一个非常经典的&lt;a href=&quot;https://github.com/kennethreitz/setup.py&quot;&gt;模板&lt;/a&gt;，可以参考使用。&lt;/p&gt;
&lt;p&gt;可以看到我在&lt;code&gt;HandyToolsPy&lt;/code&gt;中创建了&lt;code&gt;__init__.py&lt;/code&gt;、&lt;code&gt;__version__.py&lt;/code&gt;、&lt;code&gt;DataProcess.py&lt;/code&gt;、&lt;code&gt;Translator.py&lt;/code&gt;四个文件，具体功能如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__init__.py&lt;/code&gt;
&lt;ol&gt;
&lt;li&gt;将目录标记为Python包，这是它最基本的作用，它使得&lt;code&gt;Python&lt;/code&gt;解释器知道该目录及其包含的文件应该被视为一个包.&lt;/li&gt;
&lt;li&gt;包的命名空间管理，&lt;code&gt;__init__.py&lt;/code&gt;文件可以用来组织包的命名空间。通过在这个文件中导入函数、类或其他模块，你可以提供一个经过精心设计的对外接口，使得包的结构对用户更加透明。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__version__.py&lt;/code&gt;
&lt;ol&gt;
&lt;li&gt;定义库的版本号，方便用户查看和更新。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外两个文件即功能模块，就是这个库导入后的功能类或功能函数。&lt;/p&gt;
&lt;h2&gt;2. 开始&lt;/h2&gt;
&lt;h3&gt;2.1 许可证&lt;/h3&gt;
&lt;p&gt;上传到&lt;code&gt;Python Package Index&lt;/code&gt;的每个包都包含许可证，这一点很重要。这告诉用户安装你的软件包可以使用您的软件包的条款。&lt;/p&gt;
&lt;h3&gt;2.2 &lt;code&gt;setup.py&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;使用上一小节提供的模板即可，注意&lt;code&gt;NAME&lt;/code&gt;和&lt;code&gt;VERSION&lt;/code&gt;要分别对应你自己库的名称和版本号。&lt;/p&gt;
&lt;p&gt;一个可能的&lt;code&gt;setup.py&lt;/code&gt;文件如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Note: To use the &apos;upload&apos; functionality of this file, you must:
#   $ pipenv install twine --dev

import io
import os
import sys
from shutil import rmtree

from setuptools import find_packages, setup, Command

# Package meta-data.
NAME = &apos;HandyToolsPy&apos;
DESCRIPTION = &apos;Handypy is a multifunctional Python tool library, which aims to provide developers with a series of convenient practical tools and simplify daily programming tasks.Whether it is processing data, operating file systems, or network requests, Handypy can provide you with efficient and reliable solutions.&apos;
URL = &apos;https://github.com/kimbleex/HandyToolsPy&apos;
EMAIL = &apos;kimbleex@outlook.com&apos;
AUTHOR = &apos;kimbleex&apos;
REQUIRES_PYTHON = &apos;&amp;gt;=3.6.0&apos;
VERSION = &apos;0.1.1&apos;

# What packages are required for this module to be executed?
REQUIRED = [
    &apos;pandas&apos;, &apos;googletrans&apos;,
]

# What packages are optional?
EXTRAS = {
    # &apos;fancy feature&apos;: [&apos;django&apos;],
}

# The rest you shouldn&apos;t have to touch too much :)
# ------------------------------------------------
# Except, perhaps the License and Trove Classifiers!
# If you do change the License, remember to change the Trove Classifier for that!

here = os.path.abspath(os.path.dirname(__file__))

# Import the README and use it as the long-description.
# Note: this will only work if &apos;README.md&apos; is present in your MANIFEST.in file!
try:
    with io.open(os.path.join(here, &apos;README.md&apos;), encoding=&apos;utf-8&apos;) as f:
        long_description = &apos;\n&apos; + f.read()
except FileNotFoundError:
    long_description = DESCRIPTION

# Load the package&apos;s __version__.py module as a dictionary.
about = {}
if not VERSION:
    project_slug = NAME.lower().replace(&quot;-&quot;, &quot;_&quot;).replace(&quot; &quot;, &quot;_&quot;)
    with open(os.path.join(here, project_slug, &apos;__version__.py&apos;)) as f:
        exec(f.read(), about)
else:
    about[&apos;__version__&apos;] = VERSION


class UploadCommand(Command):
    &quot;&quot;&quot;Support setup.py upload.&quot;&quot;&quot;

    description = &apos;Build and publish the package.&apos;
    user_options = []

    @staticmethod
    def status(s):
        &quot;&quot;&quot;Prints things in bold.&quot;&quot;&quot;
        print(&apos;\033[1m{0}\033[0m&apos;.format(s))

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        try:
            self.status(&apos;Removing previous builds…&apos;)
            rmtree(os.path.join(here, &apos;dist&apos;))
        except OSError:
            pass

        self.status(&apos;Building Source and Wheel (universal) distribution…&apos;)
        os.system(&apos;{0} setup.py sdist bdist_wheel --universal&apos;.format(sys.executable))

        self.status(&apos;Uploading the package to PyPI via Twine…&apos;)
        os.system(&apos;twine upload dist/*&apos;)

        self.status(&apos;Pushing git tags…&apos;)
        os.system(&apos;git tag v{0}&apos;.format(about[&apos;__version__&apos;]))
        os.system(&apos;git push --tags&apos;)

        sys.exit()


# Where the magic happens:
setup(
    name=NAME,
    version=about[&apos;__version__&apos;],
    description=DESCRIPTION,
    long_description=long_description,
    long_description_content_type=&apos;text/markdown&apos;,
    author=AUTHOR,
    author_email=EMAIL,
    python_requires=REQUIRES_PYTHON,
    url=URL,
    packages=find_packages(exclude=[&quot;tests&quot;, &quot;*.tests&quot;, &quot;*.tests.*&quot;, &quot;tests.*&quot;]),
    # If your package is a single module, use this instead of &apos;packages&apos;:
    # py_modules=[&apos;mypackage&apos;],

    # entry_points={
    #     &apos;console_scripts&apos;: [&apos;mycli=mymodule:cli&apos;],
    # },
    install_requires=REQUIRED,
    extras_require=EXTRAS,
    include_package_data=True,
    license=&apos;MIT&apos;,
    classifiers=[
        # Trove classifiers
        # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
        &apos;License :: OSI Approved :: MIT License&apos;,
        &apos;Programming Language :: Python&apos;,
        &apos;Programming Language :: Python :: 3.6&apos;,
        &apos;Programming Language :: Python :: 3.7&apos;,
        &quot;Programming Language :: Python :: 3.8&quot;,
        &quot;Programming Language :: Python :: 3.9&quot;,
        &apos;Programming Language :: Python :: 3.10&apos;,
        &apos;Programming Language :: Python :: 3.11&apos;,
        &apos;Programming Language :: Python :: 3.12&apos;,
        &apos;Programming Language :: Python :: Implementation :: CPython&apos;,
        &apos;Programming Language :: Python :: Implementation :: PyPy&apos;,
    ],
    # $ setup.py publish support.
    cmdclass={
        &apos;upload&apos;: UploadCommand,
    },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 编写功能类&lt;/h3&gt;
&lt;p&gt;在上述过程完成后，你可以在你的库文件夹下面编写你的功能类。&lt;/p&gt;
&lt;h3&gt;2.4 生成分发档案&lt;/h3&gt;
&lt;p&gt;接着，需要生成库的分发档案，以便上传到 PyPI。&lt;/p&gt;
&lt;p&gt;更新&lt;code&gt;build&lt;/code&gt;，并使用它生成分发档案文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -m pip install --upgrade build
python -m build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果正常执行，会有大片输出，并在目录下生成&lt;code&gt;dist&lt;/code&gt;和&lt;code&gt;.egg-info&lt;/code&gt;目录
&lt;img src=&quot;./build.avif&quot; alt=&quot;build&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3. &lt;code&gt;PyPi&lt;/code&gt;部分&lt;/h2&gt;
&lt;h3&gt;3.1 注册并认证&lt;/h3&gt;
&lt;p&gt;至于如何注册账号，如何获取&lt;code&gt;2FA&lt;/code&gt;认证获取发布权限，请自行学习，这里不再赘述。&lt;/p&gt;
&lt;p&gt;完成后生成一个&lt;code&gt;API Token&lt;/code&gt;，记得勾选全部权限。&lt;/p&gt;
&lt;h2&gt;4. 发布&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;twine&lt;/code&gt;上传你的库到&lt;code&gt;PyPi&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先安装&lt;code&gt;twine&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install twine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用&lt;code&gt;twine&lt;/code&gt;上传你的库。执行之后会让你输入你的&lt;code&gt;API Token&lt;/code&gt;，复制进去即可，它不会显示出来。无需多次&lt;code&gt;Ctrl V&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -m twine upload dist/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt; : 如果显示你不是这个包的主人，或者显示你没有权限，或者报错&lt;code&gt;403&lt;/code&gt;有关的错误，请更换你的库名，&lt;code&gt;PyPi&lt;/code&gt;的库名要求全球唯一，所以重复了的话会直接上传失败。&lt;/p&gt;
&lt;h2&gt;5. 完成&lt;/h2&gt;
&lt;p&gt;如果一切顺利，你的库就发布成功了。&lt;/p&gt;
&lt;p&gt;可能的发布成功的输出结果:
&lt;img src=&quot;./success.avif&quot; alt=&quot;发布成功&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;6. 测试&lt;/h2&gt;
&lt;p&gt;发布成功后，你可以使用&lt;code&gt;pip&lt;/code&gt;安装你的库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install &amp;lt;your-package-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. 更新&lt;/h2&gt;
&lt;p&gt;删除目录下的&lt;code&gt;dist&lt;/code&gt;和&lt;code&gt;.egg-info&lt;/code&gt;文件夹，然后重新执行&lt;code&gt;build&lt;/code&gt;和&lt;code&gt;twine&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;记得修改版本号。&lt;/p&gt;
&lt;h2&gt;8. 一些吐槽&lt;/h2&gt;
&lt;p&gt;这个更新必须每一次都版本不一样，不然会显示报名重复等报错，真是鸡肋。&lt;/p&gt;
&lt;p&gt;就比如我要修改一下&lt;code&gt;README.md&lt;/code&gt;文件，没有修改&lt;code&gt;VERSION&lt;/code&gt;，就会在上传的时候报错说这个库已存在。😅&lt;/p&gt;
</content:encoded></item><item><title>JavaScript实现本地文件扫描</title><link>https://blog.kimbleex.top/posts/2024-11-22-js-localfile-scan/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-11-22-js-localfile-scan/</guid><pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;跟一个老前端在做一个&lt;code&gt;NAS&lt;/code&gt;云存储的工具，涉及到了本地文件扫描，作为一个前端菜鸡，我去了解了一下功能的具体原理，然后自己使用&lt;code&gt;JavaScript&lt;/code&gt;实现了一下，现在做一个记录。&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;说白了，就是普通的递归查找，输入所需要查找的文件目录，使用&lt;code&gt;Node&lt;/code&gt;的&lt;code&gt;fs&lt;/code&gt;模块读取目录，然后递归查找子目录，通过&lt;code&gt;isDirectory&lt;/code&gt;方法判断子目录中是否也存在文件夹，然后一直重复即可，直到找到所有文件。&lt;/p&gt;
&lt;h2&gt;预期结果&lt;/h2&gt;
&lt;p&gt;我是想返回一个类似于&lt;code&gt;JSON&lt;/code&gt;的变量，预期的结构为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;根目录&quot;: {
        {&quot;子目录1&quot;:&quot;floder&quot;},
        {&quot;子目录2&quot;:&quot;floder&quot;},
        {&quot;文件1&quot;:&quot;file&quot;}, 
        {&quot;文件2&quot;:&quot;file&quot;}
        },
    &quot;子目录1&quot;: {
        {&quot;文件1&quot;:&quot;file&quot;}, 
        {&quot;文件2&quot;:&quot;file&quot;},
        {&quot;子目录3&quot;:&quot;floder&quot;}
        },
    &quot;子目录2&quot;: {
        {&quot;文件1&quot;:&quot;file&quot;}, 
        {&quot;文件2&quot;:&quot;file&quot;}
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个类似于&lt;code&gt;JSON&lt;/code&gt;变量中，每一个&lt;code&gt;Key&lt;/code&gt;代表着一个目录，对应的&lt;code&gt;Value&lt;/code&gt;对应着这个目录下的所有文件和子目录，并且都有标签与其对应，&lt;code&gt;floder&lt;/code&gt;表示这是一个目录，而&lt;code&gt;file&lt;/code&gt;表示这是一个文件。这样的标记有利于后续在前端页面中展示的时候对图标的选择等。&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;写来写去，为了让最后结果能够&lt;strong&gt;集大成&lt;/strong&gt;，选择了使用&lt;code&gt;柯里化 Currying&lt;/code&gt;和&lt;code&gt;闭包&lt;/code&gt;来实现这个功能。&lt;/p&gt;
&lt;p&gt;首先定义一个父函数&lt;code&gt;scanFile&lt;/code&gt;，它声明一个&lt;code&gt;fileList&lt;/code&gt;变量，用来保存最后的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function scanFile() {
  let fileList = {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后定义一个子函数&lt;code&gt;scan&lt;/code&gt;，用来递归查找目录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function scan(dir){
    let files
    try{
      files = await readdir(dir, { withFileTypes: true });
    }catch{
      return
    }
    fileList[dir] = {};
    for (const file of files){
      if (file.isDirectory()) {
        const sonFilePath = `${dir}${file.name}/`;
        fileList[dir][file.name] = &quot;floder&quot;
        await scan(sonFilePath);
      }else{
        fileList[dir][file.name] = &quot;file&quot;
      }
    }
    return fileList
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码中，&lt;code&gt;readdir&lt;/code&gt;是&lt;code&gt;Node&lt;/code&gt;的&lt;code&gt;fs&lt;/code&gt;模块中的方法，用来读取目录，&lt;code&gt;withFileTypes: true&lt;/code&gt;表示返回的是一个&lt;code&gt;Dirent&lt;/code&gt;对象，这个对象中包含了文件名、文件类型等信息。使用&lt;code&gt;files&lt;/code&gt;保存读取到的文件列表，然后遍历这个列表，如果当前遍历到的文件是一个目录，则递归调用&lt;code&gt;scan&lt;/code&gt;函数，否则将文件名和类型添加到&lt;code&gt;fileList&lt;/code&gt;中。最后返回&lt;code&gt;fileList&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后，在&lt;code&gt;scanFile&lt;/code&gt;函数中返回&lt;code&gt;scan&lt;/code&gt;函数。
完整代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { readdir } from &apos;node:fs/promises&apos;;

async function scanFile() {
  let fileList = {};
  async function scan(dir){
    let files
    try{
      files = await readdir(dir, { withFileTypes: true });
    }catch{
      return
    }
    fileList[dir] = {};
    for (const file of files){
      if (file.isDirectory()) {
        const sonFilePath = `${dir}${file.name}/`;
        fileList[dir][file.name] = &quot;floder&quot;
        await scan(sonFilePath);
      }else{
        fileList[dir][file.name] = &quot;file&quot;
      }
    }
    return fileList
  }
  return scan
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以尝试打印一下结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const result = await (await scanFile())(&quot;D:/git/&quot;);
console.log(result);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &apos;D:/git/&apos;: { 
    &apos;Git-2.45.2-64-bit.exe&apos;: &apos;file&apos;, 
    xxx: &apos;folder&apos; 
    },
  &apos;D:/git/xxx/&apos;: {
    hhh: &apos;folder&apos;,
    &apos;hhh.zip&apos;: &apos;file&apos;,
    &apos;xh.bmp&apos;: &apos;file&apos;,
    &apos;xxx.pptx&apos;: &apos;file&apos;,
    &apos;xxx.txt&apos;: &apos;file&apos;
  },
  &apos;D:/git/xxx/hhh/&apos;: {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，&lt;code&gt;scanFile&lt;/code&gt;函数返回了一个函数，这个函数返回了一个&lt;code&gt;fileList&lt;/code&gt;对象，这个对象中保存了所有目录和文件的信息。&lt;/p&gt;
&lt;p&gt;本地目录结果为:
&lt;img src=&quot;./local.avif&quot; alt=&quot;本地目录&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Corn调度任务表达式写法笔记</title><link>https://blog.kimbleex.top/posts/2024-11-20-corn-grammer/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-11-20-corn-grammer/</guid><pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;起因是写&lt;code&gt;Github&lt;/code&gt;主页的时候，突发奇想打算设置&lt;code&gt;Action&lt;/code&gt;来定时推送仓库，于是就涉及到了Corn调度任务表达式写法，记录一下。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;Corn&lt;/code&gt;规范&lt;/h2&gt;
&lt;p&gt;Corn表达式由6个字段组成，每个字段之间用空格分隔，格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*  *  *  *  *  *
|  |  |  |  |  |___________ 星期， 数值为0-6，周日为0，也可以使用星期缩写，比如mon等
|  |  |  |  |______________ 月份， 数值为1-12，也可以使用月份缩写，比如jan等
|  |  |  |_________________ 日期， 数值为1-31
|  |  |____________________ 小时， 数值为0-23
|  |_______________________ 分钟， 数值为0-59
|__________________________ 秒钟， 数值为0-59，可选字段
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他符号支持&lt;/h2&gt;
&lt;p&gt;除了上面的基础字段写法，&lt;code&gt;Corn&lt;/code&gt;还支持一些符号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;星号(*) 表示匹配任意值，即全部值 。例如，* 在分钟字段中表示每分钟都执行。&lt;/li&gt;
&lt;li&gt;逗号(,) 用于分隔多个值。例如，1,3,5 在小时字段中表示 1 点、3 点和 5 点执行。&lt;/li&gt;
&lt;li&gt;斜线(/) 用于指定间隔值。例如，*/5 在分钟字段中表示每 5 分钟执行一次。&lt;/li&gt;
&lt;li&gt;连字符(-) 用于指定范围。例如，10-20 在日期字段中表示从 10 号到 20 号。&lt;/li&gt;
&lt;li&gt;问号(?) 仅用于日期和星期几字段，表示不指定具体值。通常用于避免冲突。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一些例子&lt;/h2&gt;
&lt;p&gt;:::note[每分钟执行一次]
0 * * * * *
:::&lt;/p&gt;
&lt;p&gt;:::tip[每天上午9点执行一次]
0 0 9 * * *
:::&lt;/p&gt;
&lt;p&gt;:::important[每周一上午10点执行一次]
0 0 10 * * mon
:::&lt;/p&gt;
&lt;p&gt;:::warning[每个月10-20号的每天8点执行一次]
0 0 8 10-20 * *
:::&lt;/p&gt;
&lt;p&gt;:::caution[每两个小时执行一次]
0 0 */2 * * *
:::&lt;/p&gt;
&lt;h2&gt;一些注意&lt;/h2&gt;
&lt;p&gt;上面说到秒钟单位是可以省略的，所以在写表达式时，可以直接省略秒的那一位。&lt;/p&gt;
</content:encoded></item><item><title>Windows Office 无需任何成本激活方法</title><link>https://blog.kimbleex.top/posts/2024-11-14-windows-office-activate/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-11-14-windows-office-activate/</guid><pubDate>Thu, 14 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;官方开源仓库: &lt;a href=&quot;https://github.com/massgravel/Microsoft-Activation-Scripts&quot;&gt;Microsoft-Activation-Scripts&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本文受启发于: &lt;a href=&quot;https://yangqiuyi.com/blog/windows/windows11%E4%B8%93%E4%B8%9A%E7%89%88%E6%BF%80%E6%B4%BB/&quot;&gt;Windows11专业版激活&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这是一个利用硬件ID激活的办法, 激活后一劳永逸, 即使重装系统也不需要再激活, 但是需要保持硬件不变。&lt;/p&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;p&gt;打开&lt;code&gt;Windwos PowerShell&lt;/code&gt;, 输入以下命令(&lt;strong&gt;注意不是&lt;code&gt;CMD&lt;/code&gt;&lt;/strong&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;irm https://get.activated.win | iex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入完成后回车，会弹出一个新界面，在新界面中选择你需要激活的内容即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选项: &lt;code&gt;[1] HWID&lt;/code&gt;用于激活&lt;code&gt;Windows&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;选项: &lt;code&gt;[2] Ohook&lt;/code&gt;用于激活&lt;code&gt;Office&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>关于新图片格式AVIF和Pillow格式转换方法</title><link>https://blog.kimbleex.top/posts/2024-11-13-avif-pillow-trans/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-11-13-avif-pillow-trans/</guid><pubDate>Wed, 13 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;AVIF (AV1 Image File Format)&lt;/code&gt;是一种现代图像格式, 基于&lt;code&gt;AV1&lt;/code&gt;视频编码技术。它由&lt;code&gt;Alliance for Open Media&lt;/code&gt;开发, 旨在提供高效的图像压缩, 同时保持优良的图像质量。&lt;/p&gt;
&lt;h2&gt;背景介绍&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AVIF&lt;/code&gt;源于&lt;code&gt;AV1&lt;/code&gt;视频编码格式, 这是一种开源、免版税的视频编码格式, 旨在替代&lt;code&gt;VP9&lt;/code&gt;和&lt;code&gt;H.264&lt;/code&gt;。&lt;code&gt;AVIF&lt;/code&gt;利用&lt;code&gt;AV1&lt;/code&gt;的压缩技术来处理静态图像和图像序列, 提供高效的图像存储和传输。&lt;/p&gt;
&lt;h2&gt;AVIF格式的特点和优势&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;高压缩效率: &lt;code&gt;AVIF&lt;/code&gt;具有出色的压缩效率, 能够在较小的文件大小下提供优质的图像质量。这使得它在带宽和存储有限的情况下非常有用。&lt;/li&gt;
&lt;li&gt;支持&lt;code&gt;HDR&lt;/code&gt;: &lt;code&gt;AVIF&lt;/code&gt;支持高动态范围&lt;code&gt;HDR&lt;/code&gt;图像, 能够呈现更丰富的色彩和对比度。&lt;/li&gt;
&lt;li&gt;色彩深度: 支持8位、10位和12位色深, 能够更好地处理复杂的色彩信息。&lt;/li&gt;
&lt;li&gt;透明度: 支持透明度通道, 使其适用于需要透明背景的图像。&lt;/li&gt;
&lt;li&gt;动画支持: &lt;code&gt;AVIF&lt;/code&gt;支持动画, 与&lt;code&gt;GIF&lt;/code&gt;和&lt;code&gt;APNG&lt;/code&gt;相比, 提供了更高效的动画压缩。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;与其他格式的比较&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JPEG&lt;/code&gt;: &lt;code&gt;AVIF&lt;/code&gt;在相同图像质量下通常比&lt;code&gt;JPEG&lt;/code&gt;文件小得多, 并且支持更高的色彩深度和透明度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PNG&lt;/code&gt;: 虽然&lt;code&gt;PNG&lt;/code&gt;支持无损压缩和透明度, 但&lt;code&gt;AVIF&lt;/code&gt;在有损压缩情况下提供了更小的文件大小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebP&lt;/code&gt;: &lt;code&gt;AVIF&lt;/code&gt;通常比&lt;code&gt;WebP&lt;/code&gt;提供更高的压缩效率, 尤其是在处理高分辨率图像时。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HEIC&lt;/code&gt;: &lt;code&gt;AVIF&lt;/code&gt;与&lt;code&gt;HEIC&lt;/code&gt;相似, 但&lt;code&gt;AVIF&lt;/code&gt;是免版税的, 而&lt;code&gt;HEIC&lt;/code&gt;可能涉及专利许可问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;兼容性（截至2024年）&lt;/h2&gt;
&lt;p&gt;:::tip[浏览器支持]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Google Chrome&lt;/code&gt;: 支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mozilla Firefox&lt;/code&gt;: 支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Microsoft Edge&lt;/code&gt;: 支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Safari&lt;/code&gt;: 从&lt;code&gt;Safari 16&lt;/code&gt;开始支持, 但支持可能不如其他浏览器全面。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::
:::note[操作系统支持]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Windows&lt;/code&gt;: &lt;code&gt;Windows 10&lt;/code&gt;及以上已通过系统更新支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;macOS&lt;/code&gt;: 从&lt;code&gt;macOS Ventura&lt;/code&gt;开始支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Android&lt;/code&gt;: 较新的&lt;code&gt;Android&lt;/code&gt;版本支持。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iOS&lt;/code&gt;: 从&lt;code&gt;iOS 16&lt;/code&gt;开始支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;总结
&lt;code&gt;AVIF&lt;/code&gt;作为一种现代图像格式, 提供了卓越的压缩效率和图像质量, 逐渐被主流浏览器和操作系统采纳。随着技术的进步和普及, &lt;code&gt;AVIF&lt;/code&gt;有望成为广泛使用的图像格式之一。&lt;/p&gt;
&lt;h2&gt;Pillow库实现AVIF格式转换&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Pillow&lt;/code&gt;是一个强大的Python图像处理库, 支持多种图像格式, 包括&lt;code&gt;AVIF&lt;/code&gt;。通过&lt;code&gt;Pillow&lt;/code&gt;, 可以轻松实现&lt;code&gt;AVIF&lt;/code&gt;与其他图像格式的相互转换。&lt;/p&gt;
&lt;h3&gt;安装Pillow库&lt;/h3&gt;
&lt;p&gt;在开始之前, 需要确保已安装&lt;code&gt;Pillow&lt;/code&gt;库以及&lt;code&gt;pillow-avif-plugin&lt;/code&gt;库。可以使用以下命令安装:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install Pillow pillow-avif-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AVIF格式转换为其他格式&lt;/h3&gt;
&lt;p&gt;以下是一个示例, 将&lt;code&gt;AVIF&lt;/code&gt;格式转换为其他格式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from PIL import Image    # Pillow                    9.0.0
import pillow_avif       # pillow-avif-plugin        1.2.2
#以上只是其中一个可用版本，并非必须

AVIFfilename = &apos;test.avif&apos;
AVIFimg = Image.open(AVIFfilename)
# 将AVIF格式转换为JPEG格式，当然其他格式都可以
AVIFimg.save(AVIFfilename.replace(&quot;avif&quot;,&apos;jpg&apos;),&apos;JPEG&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然, 也可以将其他格式转为&lt;code&gt;AVIF&lt;/code&gt;格式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from PIL import Image    # Pillow                    9.0.0
import pillow_avif       # pillow-avif-plugin        1.2.2
#以上只是其中一个可用版本，并非必须

img_path_ = &quot;./test.png&quot;
img = Image.open(path_)
img.save(path_.replace(&quot;jpg&quot;,&apos;avif&apos;),&apos;AVIF&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换完成后，我们可以在根目录看到转换完成后的文件，文件名后缀为&lt;code&gt;.avif&lt;/code&gt;。
&lt;img src=&quot;./converted.avif&quot; alt=&quot;图片转换完成&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以在文件管理器中比较转换前后的图片大小。
&lt;img src=&quot;./size_compare.avif&quot; alt=&quot;图片大小比较&quot; /&gt;
可以看到，转换后的图片大小明显小于转换前的图片大小。由此可见，&lt;code&gt;AVIF&lt;/code&gt;格式具有更高的压缩效率。&lt;/p&gt;
&lt;p&gt;最后让我们从视觉上感受两张图片的差距。
&lt;img src=&quot;./vis_compare.avif&quot; alt=&quot;图片对比&quot; /&gt;
可以看到，转换前后的图片几乎没有什么差别，说明&lt;code&gt;AVIF&lt;/code&gt;格式在保持图像质量的同时，也具有很高的压缩效率。&lt;/p&gt;
</content:encoded></item><item><title>Prisma操作Docker远程数据库PostgreSQL</title><link>https://blog.kimbleex.top/posts/2024-10-25-prisma-docker-postgresql/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-10-25-prisma-docker-postgresql/</guid><pubDate>Fri, 25 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;strong&gt;前提条件：安装好Docker Desktop、NodeJS以及具备一定的TypeScript基础&lt;/strong&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;strong&gt;本教程实验环境为Windows 11、NodeJS-20.15.0、Docker-27.2.0&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;本地部署的数据库同样可以实现，确保数据库可以成功连接即可&lt;/h5&gt;
&lt;h2&gt;1. Docker部署PostgreSQL&lt;/h2&gt;
&lt;h3&gt;1.1 首先创建一个空文件夹&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;D:\&amp;gt; mkdir postgresql
D:\&amp;gt; cd postgresql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 配置Docker Compose&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Docker Compose&lt;/code&gt; 简化了对整个应用程序堆栈的控制，使得在一个易于理解的 YAML 配置文件中轻松管理服务、网络和数据卷。 接下来我们创建配置文件来对它进行配置。&lt;/p&gt;
&lt;p&gt;在上面创建好的空文件夹中创建配置文件&lt;code&gt;docker-compose.yml&lt;/code&gt;，并编辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  postgres_db: # 服务名称
    image: postgres:15.7 # 指定镜像及其版本
    container_name: docker_postgres # 指定容器的名称
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
    ports: # 端口映射
      - &quot;5432:5432&quot;
    volumes: # 数据持久化的配置
      - data:/var/lib/postgresql/data
      - log:/var/log/postgresql

volumes: # 数据卷
  data:
  log:

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对配置文件中属性的一些注解：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;image&lt;/code&gt; :  指定了要使用的 Docker 镜像及其版本。在这里，我们使用了官方的 PostgreSQL 15.7 版本镜像。为了确保系统的稳定性和兼容性，推荐使用 PostgreSQL 官方镜像的一个稳定版本而不是最新版&lt;code&gt;latest&lt;/code&gt;。如果没有写版本，将会默认使用最新版本&lt;code&gt;latest&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;contain_name&lt;/code&gt;:  容器名称。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;environment&lt;/code&gt;:  环境变量，可以是数据库的用户名、数据库类型和密码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ports&lt;/code&gt;:  端口。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;volumes&lt;/code&gt;:  数据持久化卷。这个可以保证容器被删除的情况下，数据卷中的数据不会丢失。如果不需要可以把日志数据卷去掉。很多不被使用的数据卷可能会占用很大的存储空间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.3 部署和验证&lt;/h3&gt;
&lt;p&gt;接下来启动&lt;code&gt;Docker Compose&lt;/code&gt;，它会按照配置文件来配置容器，我们已经配置了数据库镜像，所以启动后它会自动拉取&lt;code&gt;Postgresql&lt;/code&gt;数据库镜像并部署运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt; docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有刚才创建的&lt;code&gt;docker-compose.yml&lt;/code&gt;配置文件，将会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt; docker compose up -d
no configuration file provided: not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署成功显示如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[+] Running 1/1
[+] Running 1/2tgresql_default  Created                            0.1s
[+] Running 1/2tgresql_default  Created                            0.1s
[+] Running 1/2tgresql_default  Created                            0.1s
[+] Running 1/2tgresql_default  Created                            0.1s
[+] Running 1/2tgresql_default  Created                            0.1s
[+] Running 2/2tgresql_default  Created                            0.1s
 ✔ Network postgresql_default  Created                             0.1s 
  - Container docker_postgres   Starting                         0.6s
 ✔ Container docker_postgres   Started                             0.7s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我已经运行过相同指令，所以输出可能会有所不同，初次运行可能的成功结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[+] Running 15/15
 ✔ postgres_db Pulled                                                           61.9s
   ✔ 09f376ebb190 Pull complete                                                 8.9s
   ✔ 119215dfb3e3 Pull complete                                                 2.0s
   ✔ 94fccb772ad3 Pull complete                                                 10.7s
   ✔ 0fc3acb16548 Pull complete                                                 8.6s
   ✔ d7dba7d03fe8 Pull complete                                                 13.1s
   ✔ 898ae395a1ca Pull complete                                                 12.6s
   ✔ 088e651df7e9 Pull complete                                                 12.4s
   ✔ ed155773e5e0 Pull complete                                                 14.2s
   ✔ 52df7d12fb73 Pull complete                                                 33.4s
   ✔ bab1ecc22dc9 Pull complete                                                 15.2s
   ✔ 1655a257a5b5 Pull complete                                                 16.0s
   ✔ 978f02dfc247 Pull complete                                                 18.0s
   ✔ d715d7d9aee0 Pull complete                                                 17.8s
   ✔ b2e9251b2f8d Pull complete                                                 19.8s
[+] Running 3/3
 ✔ Volume &quot;postgresql_log&quot;    Created                                           0.0s
 ✔ Volume &quot;postgresql_data&quot;   Created                                           0.0s
 ✔ Container docker_postgres  Started                                           0.9s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，验证一下数据库是否在运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt;docker compose ps
NAME              IMAGE           COMMAND                   SERVICE       CREATED         STATUS         PORTS
docker_postgres   postgres:15.7   &quot;docker-entrypoint.s…&quot;   postgres_db   4 minutes ago   Up 4 minutes   0.0.0.0:5432-&amp;gt;5432/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，数据库已成功部署。&lt;/p&gt;
&lt;h2&gt;2. Prisma远程操作数据库&lt;/h2&gt;
&lt;h3&gt;2.1 环境配置&lt;/h3&gt;
&lt;p&gt;初始化一个 TypeScript 项目，并将 Prisma CLI 作为开发依赖项添加到其中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt;npm init -y
Wrote to D:\postgresql\package.json:
{
  &quot;name&quot;: &quot;postgresql&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
  },
  &quot;keywords&quot;: [],
  &quot;author&quot;: &quot;&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;description&quot;: &quot;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt;npm install prisma typescript ts-node @types/node --save-dev
added 26 packages in 9s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将创建一个包含 TypeScript 应用程序的初始设置的 &lt;code&gt;package.json&lt;/code&gt;。接着初始化TypeScirpt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt; npmx tsc --init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以尝试调用&lt;code&gt;prisma&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt; npx prisma
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，初始化&lt;code&gt;prisma&lt;/code&gt;，并生成环境配置文件&lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt; npx prisma init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.env&lt;/code&gt;中存放数据库的远程链接等内容。PostgreSQL的远程链接地址一般如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE_URL=&quot;postgresql://username:password@localhost:5432/sqlname&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记得将&lt;code&gt;username&lt;/code&gt; &lt;code&gt;password&lt;/code&gt; 以及 &lt;code&gt;sqlname&lt;/code&gt;替换成刚才创建&lt;code&gt;docker&lt;/code&gt;中的配置文件中的信息&lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;例如我的&lt;code&gt;.env&lt;/code&gt;文件内容为：(参考我的&lt;code&gt;docker-compose.yml&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE_URL=&quot;postgresql://postgres:postgres@localhost:5432/postgres&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 创建数据库表&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;prisma&lt;/code&gt;中数据库表用&lt;code&gt;model&lt;/code&gt;表示，它提供了多种属性类型等，在&lt;code&gt;schema.prisma&lt;/code&gt;中写入两张表，关于数据库表的属性的设计可以参考官网：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;generator client {
  provider = &quot;prisma-client-js&quot;
}

datasource db {
  provider = &quot;postgresql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}

model Profile {
  id Int @id @default(autoincrement())
  bio String
  user User @relation(fields: [userId], references: [id])
  userId Int @unique
}

model User {
  id Int @id @default(autoincrement())
  name String
  age Int
  profile Profile?
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在进行数据库映射：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt; npx prisma migrate dev --name init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样操作之后，远程对数据库的更改就可以同步到我们的数据库镜像中。&lt;/p&gt;
&lt;h3&gt;2.3 增删改查 (CURD)&lt;/h3&gt;
&lt;p&gt;创建一个&lt;code&gt;index.ts&lt;/code&gt;文件，用来通过&lt;code&gt;prisma&lt;/code&gt;操作数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { PrismaClient } from &quot;@prisma/client&quot;;

const prisma = new PrismaClient();

async function main() {
    const user = await prisma.user.findMany();
    console.log(user);
  }
  
main()
.then(async () =&amp;gt; {
    await prisma.$disconnect()
})
.catch(async (e) =&amp;gt; {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据我们之前创建的两张表&lt;code&gt;User&lt;/code&gt;和&lt;code&gt;Profile&lt;/code&gt;，我们上面的操作是对&lt;code&gt;User&lt;/code&gt;进行了查询操作。&lt;code&gt;[table].findMany()&lt;/code&gt;方法是对表格的查询，请注意，&lt;strong&gt;根据&lt;code&gt;prisma&lt;/code&gt;规范，表格的名称在创建是首字母应该大写，但是对表格进行操作时，表格的名称应全部改成小写&lt;/strong&gt;，所以在上面的代码中，&lt;code&gt;prisma.user.findMany()&lt;/code&gt;中的表格名称&lt;code&gt;user&lt;/code&gt;是小写的。&lt;/p&gt;
&lt;p&gt;运行&lt;code&gt;index.ts&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\postgresql&amp;gt; npx ts-node index.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果正确执行，那么会打印出一个空数组，显而易见，由于表格新创建，所以里面没有数据内容。&lt;/p&gt;
&lt;p&gt;下面我们来写入数据库，修改&lt;code&gt;index.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { PrismaClient } from &quot;@prisma/client&quot;;

const prisma = new PrismaClient();

async function addUser(){
    await prisma.user.create({
        data:{
            name:&quot;hi&quot;,
            age: 18,
            profile: {
                create: {
                    bio: &quot;I am a software developer&quot;
                }
            }
        }
    })
    const profile = await prisma.profile.findMany()
    console.log(profile)
}

addUser()
  .then(async () =&amp;gt; {
    await prisma.$disconnect()
  })
  .catch(async (e) =&amp;gt; {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，我们向&lt;code&gt;user&lt;/code&gt;写入了一个名字叫&lt;code&gt;hi&lt;/code&gt;、年龄为18的人，并通过绑定同时写入了&lt;code&gt;profile&lt;/code&gt;表格的一项&lt;code&gt;bio&lt;/code&gt;属性。&lt;/p&gt;
&lt;p&gt;再次执行&lt;code&gt;index.ts&lt;/code&gt;，注意，我将&lt;code&gt;prisma.user.findmany()&lt;/code&gt;中的&lt;code&gt;user&lt;/code&gt;改成了&lt;code&gt;profile&lt;/code&gt;，所以我这次查询输出的是&lt;code&gt;Profile&lt;/code&gt;表格中的内容，最后的输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  { id: 1, bio: &apos;I am a software developer&apos;, userId: 1 },
  { id: 2, bio: &apos;I am a software developer&apos;, userId: 2 },
  { id: 3, bio: &apos;I am a software developer&apos;, userId: 3 },
  { id: 4, bio: &apos;I am a software developer&apos;, userId: 4 }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到确实向表格中插入的四条数据。&lt;/p&gt;
&lt;p&gt;最后我们尝试来更新表格内容，更新的方法为&lt;code&gt;uppublished()&lt;/code&gt;，语法与上述查询和插入一样，修改&lt;code&gt;main&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function main() {
  const profile = await prisma.profile.uppublished({
    where: { id: 1 },
    data: { bio: &apos;I am a super satr!!!&apos; },
  })
  console.log(profile)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我对&lt;code&gt;Profile&lt;/code&gt;表格使用了更新方法，在&lt;code&gt;id&lt;/code&gt;属性为1的位置，将&lt;code&gt;bio&lt;/code&gt;属性值修改成了&quot;I am a super satr!!!&quot;，再次运行&lt;code&gt;index.ts&lt;/code&gt;，最后的输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  { id: 1, bio: &apos;I am a super satr!!!&apos;, userId: 1 },
  { id: 2, bio: &apos;I am a software developer&apos;, userId: 2 },
  { id: 3, bio: &apos;I am a software developer&apos;, userId: 3 },
  { id: 4, bio: &apos;I am a software developer&apos;, userId: 4 }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，数据已经成功更新到数据库中。&lt;/p&gt;
</content:encoded></item><item><title>Python调用Openai接口实现图像分析</title><link>https://blog.kimbleex.top/posts/2024-08-05-python-openai-image-analyse/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-08-05-python-openai-image-analyse/</guid><pubDate>Mon, 05 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;调用opanai库中的OpenAI接口实现。本文讨论的是openai-1.x以上版本，我在写这篇文章时使用的时openai-1.37。&lt;/p&gt;
&lt;h2&gt;1. 准备工作&lt;/h2&gt;
&lt;p&gt;在开始之前，你需要拥有一个openai平台的&lt;code&gt;api_key&lt;/code&gt;，可以去淘宝或官网购买，这里推荐淘宝。&lt;/p&gt;
&lt;p&gt;因为国内商家为了防止因代理不稳定，且确保购买的号不被封号，使用的是中转接口(卖家服务器api负责转发消息问答)。&lt;/p&gt;
&lt;h2&gt;2. 开始&lt;/h2&gt;
&lt;p&gt;介绍两个函数接口。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;OpenAI(base_url, api_key)&lt;/code&gt;: &lt;code&gt;chatgpt&lt;/code&gt;应用&lt;code&gt;client&lt;/code&gt;创建接口，我们只需要指定&lt;code&gt;base_url&lt;/code&gt;和&lt;code&gt;api_key&lt;/code&gt;两个参数即可。&lt;code&gt;base_url&lt;/code&gt;表示的是我们上述提到的中专接口的地址，&lt;code&gt;api_key&lt;/code&gt;代表的是我们购买的平台的通行证。
&lt;code&gt;client.chat.completions.create(model, message)&lt;/code&gt;: 创建对话，需要指定模型和问答小心内容。&lt;code&gt;model&lt;/code&gt;仅可指定你的&lt;code&gt;api_key&lt;/code&gt;允许的模型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.1 关于&lt;code&gt;message&lt;/code&gt;参数&lt;/h3&gt;
&lt;p&gt;上次介绍了基础问答的&lt;code&gt;message&lt;/code&gt;参数，是一个列表，包含了三个字典变量。其格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;message = [
    # 系统定位配置，可不配置。 “你是一个Python专家”告诉GPT它要做的事情
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a expert in Python&quot;}, 
    # 用户提问内容配置，“帮助我以颜色为条件筛选数据”，也就是输入的问题
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;help me filter the data by color&quot;},
    # 助手配置，用来规范GPT回答的格式。“以这种格式回答： ‘筛选的数据：数据’”，可不配置
    {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;Answer in this format : &apos;data filtered : Your Filtered DATA&apos; &quot;}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次的图片上传的&lt;code&gt;massage&lt;/code&gt;有所不同，其格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;messages=[
            {
                &quot;role&quot;: &quot;user&quot;,&quot;content&quot;: [
                    # 第一个大括号类型定义为文本，输入你想要提问的问题
                    {
                        &quot;type&quot;: &quot;text&quot;, &quot;text&quot;: &quot;What&apos;s in this image?&quot;
                    },
                    # 第二个大括号类型定义为图片，输入本地图片的base64编码
                    {
                        &quot;type&quot;: &quot;image_url&quot;,
                        # 图片格式可定义为image/png等等，编码方式为base64
                        &quot;image_url&quot;: {&quot;url&quot;: f&quot;data:image/jpg;base64,{base64_image}&quot;},
                    },
                ],
            }
        ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 返回值&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;completion.choices[0].message&lt;/code&gt;接受返回值，&lt;code&gt;completion&lt;/code&gt;即为在第2节开头提到的第二个函数的接收值。&lt;/p&gt;
&lt;p&gt;它返回一个&lt;code&gt;content&lt;/code&gt;对象，里面包含了回答内容等信息。可以转换为字典来获取返回值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;completion = client.chat.completions.create(
    model = &apos;my model&apos;, 
    message = myMessage
)

# 返回
print(completion.choice[0].message.content)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import os
import base64
import requests
from  openai import OpenAI

# base64编码函数
def encode_image(image_path):
    with open(image_path, &quot;rb&quot;) as image_file:
        return base64.b64encode(image_file.read()).decode(&apos;utf-8&apos;)

# 图片目录地址
image_path = &quot;./Imgs/&quot;
imgs = os.listdir(image_path)
base64_image = [encode_image(image_path + i) for i in imgs]

client = OpenAI(
        base_url=Your_BASE_URL,
        api_key=Your_API_KEY)

for i in range(len(base64_image)):
    response = client.chat.completions.create(
        model=&quot;gpt-4o&quot;,
        messages=[
            {
                &quot;role&quot;: &quot;user&quot;,
                &quot;content&quot;: [
                    {&quot;type&quot;: &quot;text&quot;, &quot;text&quot;: &quot;What&apos;s in this image?&quot;},
                    {
                        &quot;type&quot;: &quot;image_url&quot;,
                        &quot;image_url&quot;: {&quot;url&quot;: f&quot;data:image/jpg;base64,{base64_image[i]}&quot;},
                    },
                ],
            }
        ],
    )
    with open(&quot;./results.txt&quot;, &apos;a&apos;, encoding=&apos;utf-8&apos;) as f:
        f.write(response.choices[0].message.content.replace(&quot;*&quot;,&quot;&quot;).replace(&quot;#&quot;,&quot;&quot;).replace(&quot; &quot;,&quot;&quot;) + &quot;\n&quot;)
        f.write(&quot;===============================================================================\n&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例图片为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./sample.avif&quot; alt=&quot;Sample&quot; /&gt;&lt;/p&gt;
&lt;p&gt;输出为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./result.avif&quot; alt=&quot;Result&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Python调用Openai接口实现chatGPT问答</title><link>https://blog.kimbleex.top/posts/2024-08-02-python-openai-ask/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-08-02-python-openai-ask/</guid><pubDate>Fri, 02 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;主要是调用opanai库中的OpenAI接口实现。本文讨论的是openai-1.x以上版本，我在写这篇文章时使用的时openai-1.37。&lt;/p&gt;
&lt;h2&gt;1. 准备工作&lt;/h2&gt;
&lt;p&gt;在开始之前，你需要拥有一个openai平台的&lt;code&gt;api_key&lt;/code&gt;，可以去淘宝或官网购买，这里推荐淘宝。&lt;/p&gt;
&lt;p&gt;因为国内商家为了防止因代理不稳定，且确保购买的号不被封号，使用的是中转接口(卖家服务器api负责转发消息问答)。&lt;/p&gt;
&lt;h2&gt;2. 开始&lt;/h2&gt;
&lt;p&gt;介绍两个函数接口。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;OpenAI(base_url, api_key)&lt;/code&gt;: &lt;code&gt;chatgpt&lt;/code&gt;应用&lt;code&gt;client&lt;/code&gt;创建接口，我们只需要指定&lt;code&gt;base_url&lt;/code&gt;和&lt;code&gt;api_key&lt;/code&gt;两个参数即可。&lt;code&gt;base_url&lt;/code&gt;表示的是我们上述提到的中专接口的地址，&lt;code&gt;api_key&lt;/code&gt;代表的是我们购买的平台的通行证。
&lt;code&gt;client.chat.completions.create(model, message)&lt;/code&gt;: 创建对话，需要指定模型和问答小心内容。&lt;code&gt;model&lt;/code&gt;仅可指定你的&lt;code&gt;api_key&lt;/code&gt;允许的模型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.1 关于&lt;code&gt;message&lt;/code&gt;参数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;message&lt;/code&gt;参数是一个列表，包含了三个字典变量。其格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;message = [
    # 系统定位配置，可不配置。 “你是一个Python专家”告诉GPT它要做的事情
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a expert in Python&quot;}, 
    # 用户提问内容配置，“帮助我以颜色为条件筛选数据”，也就是输入的问题
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;help me filter the data by color&quot;},
    # 助手配置，用来规范GPT回答的格式。“以这种格式回答： ‘筛选的数据：数据’”，可不配置
    {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;Answer in this format : &apos;data filtered : Your Filtered DATA&apos; &quot;}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 返回值&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;completion.choices[0].message&lt;/code&gt;接受返回值，&lt;code&gt;completion&lt;/code&gt;即为在第2节开头提到的第二个函数的接收值。&lt;/p&gt;
&lt;p&gt;它返回一个&lt;code&gt;content&lt;/code&gt;对象，里面包含了回答内容等信息。可以转换为字典来获取返回值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;completion = client.chat.completions.create(
    model = &apos;my model&apos;, 
    message = myMessage
)

# 返回
print(completion.choice[0].message)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import openai
from openai import OpenAI
from tqdm import tqdm

def readFile(filepath):
    file = pd.read_csv(filepath, encoding= &apos;latin&apos;)
    return title_file

def dataProcess(data, answers_from_chatgpt = []):
    client = OpenAI(
        base_url=&quot;your api&quot;,
        api_key=&apos;your api key&apos;)
    for i in tqdm(range(len(data)), ncols=80, desc=&quot;chatGPT正在处理&quot;):
        completion = client.chat.completions.create(
        model=&quot;gpt-3.5-turbo&quot;, # 指定模型
        messages=[
            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a expert in Python&quot;},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;help me filter the data by color&quot;},
            {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;Answer in this format : &apos;data filtered : Your Filtered DATA&apos; &quot;}
        ])
        print(dict(completion.choices[0].message))
        answers_from_chatgpt.append(dict(completion.choices[0].message)[&apos;content&apos;])
        
file path = &apos;your filepath&apos;
answers_from_chatgpt = []
file = readFile(theme)
data = file[&apos;data&apos;].tolist()
dataProcess(data, answers_from_chatgpt)
print(answers_from_chatgpt)
        
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Python Asyncio异步深入浅出</title><link>https://blog.kimbleex.top/posts/2024-07-25-python-asyncio/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-07-25-python-asyncio/</guid><pubDate>Thu, 25 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. &lt;code&gt;Asyncio&lt;/code&gt;异步处理基本概念&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt;  ：写在函数开头，将指定函数转换为协程。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;event_loop&lt;/code&gt; ：事件循环，将协程注册到事件循环上协程才可以被调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;coroutine&lt;/code&gt; ：协程，类似于平常所说的函数，用于实现某些操作，&lt;strong&gt;只有被转为任务或者注册到事件循环上后才可以执行，否则返回一个协程对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; ：写在协程、任务前，用于挂起阻塞的异步调用 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;task&lt;/code&gt; ：任务，进一步封装好的协程，可以直接被调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;future&lt;/code&gt; : 从功能来讲是跟task差不多，但是它是一个底层的等待对象，表示一个异步操作的最终结果，通常情况下不会在应用层级创建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. &lt;code&gt;Asyncio&lt;/code&gt;异步处理基础上手&lt;/h2&gt;
&lt;h3&gt;2.1 协程和休眠&lt;/h3&gt;
&lt;p&gt;根据第1小节有关协程的介绍，简单写一个协程，使用&lt;code&gt;async&lt;/code&gt;关键字定义一个协程函数。再使用&lt;code&gt;await&lt;/code&gt;关键字挂起一个阻塞。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import time
# 定义一个协程函数，其中使用await关键字进行休眠
# asyncio.sleep(seconds)函数表示休眠时间，参数为秒
async def startDreamPlace():
    print(f&quot;started at {time.strftime(&apos;%X&apos;)}&quot;)
    print(&apos;Hello World!&apos;)
    await asyncio.sleep(2)
    print(&apos;World Hello!&apos;)
    print(f&quot;ended at {time.strftime(&apos;%X&apos;)}&quot;)
# asyncio.run()函数表示调用协程，参数为协程函数
asyncio.run(startDreamPlace())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;1.%E5%8D%8F%E7%A8%8B.avif&quot; alt=&quot;1.协程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到执行代码共计2s，与预期一致。&lt;/p&gt;
&lt;p&gt;**注意：协程不能直接调用！**如果直接调用协程返回的只有一个协程对象，并抛出&lt;code&gt;RuntimeWarning&lt;/code&gt;报错，无法执行内部逻辑。将上述案例进行修改如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import time

async def startDreamPlace():
    print(f&quot;started at {time.strftime(&apos;%X&apos;)}&quot;)
    print(&apos;Hello World!&apos;)
    await asyncio.sleep(2)
    print(&apos;World Hello!&apos;)
    print(f&quot;ended at {time.strftime(&apos;%X&apos;)}&quot;)

startDreamPlace()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.%E5%8D%8F%E7%A8%8B%E8%BF%90%E8%A1%8C.avif&quot; alt=&quot;2.协程运行&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.2 任务&lt;/h3&gt;
&lt;p&gt;引入&lt;strong&gt;任务&lt;/strong&gt;这一概念来实现多个协程的并发运行。当我两个任务都需要一定的运行时间时，可以通过并发操作来调整不同任务对cpu的时间占用，使得整个流程更加高效。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;task&lt;/code&gt;对象的常用方法如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;asyncio.create_task(coro,*,name,context,eager_start)&lt;/code&gt;&lt;/strong&gt;  创建化&lt;code&gt;task&lt;/code&gt;对象，其中&lt;code&gt;coro&lt;/code&gt;为协程对象，&lt;code&gt;name&lt;/code&gt;为任务的名称可自命名，默认为&lt;code&gt;None&lt;/code&gt;，&lt;code&gt;context&lt;/code&gt;为关联到该任务的&lt;code&gt;contextvars.Context&lt;/code&gt;对象，默认为&lt;code&gt;None&lt;/code&gt;，&lt;code&gt;eager_satrt&lt;/code&gt;为是否主动默认为&lt;code&gt;False&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;task.cancel()&lt;/code&gt;&lt;/strong&gt;  取消任务对象，会抛出&lt;code&gt;asyncio.CancelledError&lt;/code&gt;异常，可被捕获&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;task.done()&lt;/code&gt;&lt;/strong&gt;  如果任务已执行完成，返回True&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;task.result()&lt;/code&gt;&lt;/strong&gt;  返回任务的返回值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;task.exception()&lt;/code&gt;&lt;/strong&gt;  返回任务所抛出的异常&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;task.get_coro()&lt;/code&gt;&lt;/strong&gt;  返回任务所包装的协程对象，如果任务主动完成，返回None，见&lt;code&gt;3.1 主动任务工厂&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;task.get_name() &amp;amp; task.set_name&lt;/code&gt;&lt;/strong&gt;  获取任务名称 &amp;amp; 设置任务名称&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;写一个案例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import time
# 定义两个协程
async def coroutine1():
    await asyncio.sleep(5)
    print(&apos;coroutine1 finished&apos;)
async def coroutine2():
    await asyncio.sleep(3)
    print(&apos;coroutine2 finished&apos;)
async def main():
    task1 =  asyncio.create_task(coroutine1())
    task2 = asyncio.create_task(coroutine2())
    print(f&quot;started at {time.strftime(&apos;%X&apos;)}&quot;)
    await task1
    await task2
    print(f&quot;ended at {time.strftime(&apos;%X&apos;)}&quot;)
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.%E4%BB%BB%E5%8A%A1%E5%BC%82%E6%AD%A5.avif&quot; alt=&quot;3.任务异步&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到整体时间为5s而不是8s，这就实现了基本的异步功能。如果没有使用异步，直接逐步调用协程或者普通函数，上述代码的运行时长为8s。&lt;/p&gt;
&lt;h3&gt;2.3 任务组&lt;code&gt;TaskGroup()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;在python 3.11版本中新增了&lt;strong&gt;任务组&lt;code&gt;asyncio.TaskGroup()&lt;/code&gt;&lt;/strong&gt;，它可以将很多&lt;code&gt;tasks&lt;/code&gt;加入到其中，并挨个等待执行。如果任务组中任何一个任务因为非人为取消的异常而失败，任务组中后续的所有任务都将会被终止。&lt;/p&gt;
&lt;p&gt;任务组是一个异步上下文管理器，以&lt;code&gt;async with&lt;/code&gt;开头。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
# 定义两个携程
async def coroutine1():
    print(&apos;coroutine1 finished&apos;)
    return True
async def coroutine2():
    print(&apos;coroutine2 finished&apos;)
    return True
# 使用任务组
async def main():
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(coroutine1())
        task2 = tg.create_task(coroutine2())
    print(f&apos;task1-result:{task1.result()},task2-result:{task2.result()}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.%E4%BB%BB%E5%8A%A1%E7%BB%84.avif&quot; alt=&quot;4.任务组&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到任务组中的任务都成功执行了。&lt;/p&gt;
&lt;h4&gt;2.3.2 &lt;code&gt;asyncio.gather()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;其中，&lt;code&gt;async.TaskGroup().create_task()&lt;/code&gt;等价于&lt;code&gt;asyncio.create_task()&lt;/code&gt;，&lt;code&gt;.result()&lt;/code&gt;函数可以获取任务的返回值参数。&lt;/p&gt;
&lt;h4&gt;2.3.1 &lt;code&gt;asyncio.gather()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;与任务组进行对比的是**&lt;code&gt;asyncio.gather()&lt;/code&gt;**函数，它的参数表达式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# *aws表示可等待对象组
# 当return_exceptions值设置为False时，可等待对象组中后续的任务不会被取消
# 当return_exceptions值设置为True时，所有的报错都会与成功运行一样，聚合道结果列表中
awaitable asuncio.gather(*aws, return_exceptions=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与任务组方法最大的区别在于，当&lt;code&gt;.gather()&lt;/code&gt;函数中任务组中的任务抛出报错时，剩下的任务不会被取消，会继续执行直至任务组中的任务全部被执行。&lt;/p&gt;
&lt;p&gt;由此可见，3.11版本新增的任务组是对&lt;code&gt;.gather()&lt;/code&gt;函数的一个安全性方向上的优化。&lt;/p&gt;
&lt;h4&gt;2.3.2 &lt;code&gt;asyncio.wait()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;此外，对于多任务并发的函数还有**&lt;code&gt;asyncio.wait(aws, *, timeout, return_when)&lt;/code&gt;**。其中&lt;code&gt;aws&lt;/code&gt;为可等待不能为空，且可等待对象必须为&lt;code&gt;task&lt;/code&gt;或者&lt;code&gt;future&lt;/code&gt;对象，它返回一个集合&lt;code&gt;(done, pending)&lt;/code&gt;,&lt;code&gt;done&lt;/code&gt;表示已完成的可等待对象，&lt;code&gt;pending&lt;/code&gt;表示待完成的可等待对象。常用的用法为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;done, pending = await asyncio.wait(aws)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;timeout&lt;/code&gt;参数可以不指定，默认等待全部任务完成，若指定需要为整型或浮点型。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;return_when&lt;/code&gt;参数默认为&lt;code&gt;ALL_COMPLETED&lt;/code&gt;表示等待所有可等待对象全部完成，所有参数介绍如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;asyncio.FIRST_COMPLETED&lt;/code&gt;： 函数将在第一个可等待对象结束时取消并返回。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;asyncio.FIRST_EXCEPTION&lt;/code&gt; ：  该函数将在任何&lt;code&gt;future&lt;/code&gt;对象抛出异常时返回，如果不存在&lt;code&gt;future&lt;/code&gt;对象则与&lt;code&gt;ALL_COMPLETED&lt;/code&gt;等价。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;asyncio.ALL_COMPLETED&lt;/code&gt;：    函数在所有可等待对象结束时返回。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面是一个示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

async def coroutine1():
    await asyncio.sleep(5)
    print(&apos;coroutine1 finished&apos;)
async def coroutine2():
    await asyncio.sleep(6)
    print(&apos;coroutine2 finished&apos;)
    
async def main():
    task1 = asyncio.create_task(coroutine1(),name=&apos;coroutine1&apos;)
    task2 = asyncio.create_task(coroutine2(),name=&apos;coroutine2&apos;)
    aws_ = [task1, task2]
    done, pending = await asyncio.wait(aws_, timeout=5,return_when=asyncio.ALL_COMPLETED)
    print(f&quot;done:{done}&quot;)
    print(f&quot;pending:{pending}&quot;)

asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8.wait.avif&quot; alt=&quot;8.wait&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到已执行任务都在&lt;code&gt;done&lt;/code&gt;中，未执行的都在&lt;code&gt;pending&lt;/code&gt;中。&lt;/p&gt;
&lt;h3&gt;2.4 设置任务超时&lt;/h3&gt;
&lt;h4&gt;2.4.1 &lt;code&gt;asyncio.timeout()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;asyncio.timeout(delay)&lt;/code&gt;异步上下文管理器，delay为秒数，可为None。创建后可用&lt;code&gt;Timeout.reschedule()&lt;/code&gt;重新安排计划。它可以将任务超时引发的&lt;code&gt;asyncio.CancelledError&lt;/code&gt;转化为&lt;code&gt;TimeoutError&lt;/code&gt;以便捕获。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

async def coroutine1():
    await asyncio.sleep(5)
    print(&apos;coroutine1 finished&apos;)
async def main():
    async with asyncio.timeout(1):
        await coroutine1()
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.timeout.avif&quot; alt=&quot;timeout&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到抛出的错误是&lt;code&gt;Timeout&lt;/code&gt;，说明成功转换了取消异常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;code&gt;asyncio.timeout()在内部将异常进行了转化，Timeout异常是可被捕获的，所以想要捕获异常只能在该上下文管理器外部捕获。&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;asyncio.timeout_at(when)&lt;/code&gt;与&lt;code&gt;asynvio.timeout(delay)&lt;/code&gt;功能差不多，只是参数中&lt;code&gt;when&lt;/code&gt;表示是停止等待的绝代时间，而&lt;code&gt;delay&lt;/code&gt;表示停止的秒数。&lt;/p&gt;
&lt;h4&gt;2.4.2 &lt;code&gt;asyncio.Timeout()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;asyncio.Timeout(when)&lt;/code&gt;也为异步上下文管理器，&lt;code&gt;when&lt;/code&gt;为一个指明上下文将要过期的绝对时间，由事件的循环时钟进行计时。同样也可以通过&lt;code&gt;reschedule&lt;/code&gt;函数进行重新安排超时。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

async def coroutine1():
    await asyncio.sleep(5)
    print(&apos;coroutine1 finished&apos;)
    
async def main():
    try:
        async with asyncio.Timeout(None) as cm: # 未知时间时，设置为None
            new_deadline = asyncio.get_running_loop().time() + 2
            cm.reschedule(new_deadline) # 更新deadline
            await coroutine1()
    except TimeoutError: # 捕获转换后的异常
        print(&apos;Timeout!&apos;)
    if cm.expired(): # 如果为True执行，说明超时
        print(&quot;Looks like we haven&apos;t finished on time.&quot;)
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.Timeout.avif&quot; alt=&quot;6.Timeout&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2.4.3 &lt;code&gt;asyncio.wait_for(aw, timeout)&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;与上述的超时方法功能一致，但是它不是异步上下文管理器。&lt;code&gt;aw&lt;/code&gt;参数表示可等待对象，&lt;code&gt;timeout&lt;/code&gt;表示等待的秒数，也可以为&lt;code&gt;None&lt;/code&gt;。如果可等待对象超时，同样会抛出&lt;code&gt;Timeout&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

async def coroutine1():
    await asyncio.sleep(5)
    print(&apos;coroutine1 finished&apos;)
    
async def main():
    try:
        await asyncio.wait_for(coroutine1(), timeout=1)
    except TimeoutError: # 捕获转换后的异常
        print(&apos;Timeout!&apos;)
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.wait_for.avif&quot; alt=&quot;7.wait_for&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.5 任务取消&lt;/h3&gt;
&lt;h4&gt;2.1 &lt;code&gt;cancel()、cancelled()和uncancel()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;如果想要人为主动取消&lt;code&gt;task&lt;/code&gt;任务，可以使用&lt;code&gt;cancel()&lt;/code&gt;方法来实现，它会抛出一个&lt;code&gt;CancelledError&lt;/code&gt;异常停止任务，这个异常是可以被捕获的。所以可以通过&lt;code&gt;try...except...finally&lt;/code&gt;方法来抑制&lt;code&gt;cancel()&lt;/code&gt;方法。所以该方法并不能保证绝对可以取消任务，只要人为干涉，它会失效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

async def coroutine1():
    print(&apos;协程开始执行！&apos;)
    try:
        await asyncio.sleep(5)
    except asyncio.CancelledError:
        print(&apos;cancel函数生效，已抛出异常！&apos;)
        raise
    finally:
        pass
async def main():
    task = asyncio.create_task(coroutine1())
    await asyncio.sleep(1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print(&apos;已取消任务!&apos;)
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./9.cancel.avif&quot; alt=&quot;9.cancel&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;cancelled()&lt;/code&gt;函数来判断&lt;code&gt;task&lt;/code&gt;对象是否被取消。如果被取消则返回&lt;code&gt;True&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;uncancel()&lt;/code&gt;方法来屏蔽&lt;code&gt;asyncio.CancelledError&lt;/code&gt;,以达到撤销取消操作的目的，但是这个方法不应该被经常使用，因为&lt;code&gt;asyncio&lt;/code&gt;很多内部函数都是通过&lt;code&gt;asyncio.CancelledError&lt;/code&gt;来抛出的。&lt;/p&gt;
&lt;h4&gt;2.2 &lt;code&gt;shield()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;如果像屏蔽取消操作而不屏蔽&lt;code&gt;asyncio.CancelledError&lt;/code&gt;异常，可以通过使用&lt;code&gt;shield(aw)&lt;/code&gt;方法来实现。&lt;code&gt;aw&lt;/code&gt;参数表示一个任务对象。如果传入的&lt;code&gt;aw&lt;/code&gt;对象为协程对象，则它会作为任务来进行调度。即：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;task = asynciocreate_task(coroutine())
result = await shield(task)

#上述代码等价于
result = await coroutine()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实很容易看出来区别，第一个部分中协程被转为的任务，如果原来的协程被取消，任务不会受到影响，但是在第二部分直接调度协程，如果写成被取消，则会触发异常。&lt;/p&gt;
&lt;h2&gt;3. Asyncio高阶&lt;/h2&gt;
&lt;h3&gt;3.1 主动任务工厂&lt;code&gt;eager_task_factory()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;该内容在&lt;code&gt;python-3.12.&lt;/code&gt;中新增.（2024/07/26时为最新版本）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当使用这个工厂函数时，协程将在&lt;code&gt;task&lt;/code&gt;对象构造期间同步地开始执行。 任务仅会在它们阻塞时被加入事件循环上的计划任务。 这可以达成性能提升因为对同步完成的协程来说可以避免循环调度的开销。&lt;/p&gt;
&lt;p&gt;此特性会带来好处的一个常见例子是应用了缓存或记忆功能以便在可能的情况避免实际 I/O 的协程。&lt;/p&gt;
&lt;p&gt;写一个示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import time
# 创建4个协程
async def coroutine1():
    await asyncio.sleep(5)
    print(&apos;coroutine1 finished&apos;)
async def coroutine2():
    await asyncio.sleep(3)
    print(&apos;coroutine2 finished&apos;)
async def coroutine3():
    await asyncio.sleep(1)
    print(&apos;coroutine3 finished&apos;)
async def coroutine4():
    await asyncio.sleep(6)
    print(&apos;coroutine4 finished&apos;)
async def main():
    task1 = asyncio.create_task(coroutine1(), name=&apos;task1&apos;, )
    task2 = asyncio.create_task(coroutine2(), name=&apos;task2&apos;, )
    task3 = asyncio.create_task(coroutine3(), name=&apos;task3&apos;, )
    task4 = asyncio.create_task(coroutine4(), name=&apos;task4&apos;, )
    tasks = [task1, task2, task3, task4]
    print(&apos;start at {}&apos;.format(time.strftime(&apos;%X&apos;)))
    asyncio.create_eager_task_factory(tasks) # 主动任务工厂
    await task1
    await task2
    await task3
    await task4
    print(&apos;finished at {}&apos;.format(time.strftime(&apos;%X&apos;)))
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./10.%E4%B8%BB%E5%8A%A8%E4%BB%BB%E5%8A%A1%E5%B7%A5%E5%8E%82.avif&quot; alt=&quot;10.主动任务工厂&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.2 内省方法&lt;/h3&gt;
&lt;p&gt;常用的内省方法就几个，如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;asyncio.current_task(loop=None)&lt;/code&gt;  返回当前运行的&lt;code&gt;task&lt;/code&gt;对象，没有就返回&lt;code&gt;None&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;asyncio.all_tasks(loop=None)&lt;/code&gt;  返回事件循环所运行的未完成的&lt;code&gt;task&lt;/code&gt;对象的合集&lt;/p&gt;
&lt;p&gt;&lt;code&gt;asyncio.iscoroutine(obj)&lt;/code&gt;  如果传入的&lt;code&gt;obj&lt;/code&gt;是一个协程则返回&lt;code&gt;True&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.3 线程中调度&lt;/h3&gt;
&lt;p&gt;当事件循环发生阻塞等问题时，通常可以用线程调度来调节阻塞。&lt;/p&gt;
&lt;h4&gt;3.3.1 引入线程&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;to_thread(func, /, *args, **kwargs)&lt;/code&gt;方法可以调用一个额外的线程来执行传入的&lt;strong&gt;函数&lt;/strong&gt;，以避免阻塞发生。下面写一个实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
import asyncio

def cost_time_func():
    print(f&quot;start func at {time.strftime(&apos;%X&apos;)}&quot;)
    time.sleep(1)
    print(f&quot;finish func at {time.strftime(&apos;%X&apos;)}&quot;)
    
async def main():
    print(f&quot;start main at {time.strftime(&apos;%X&apos;)}&quot;)
    await asyncio.gather(
        asyncio.to_thread(cost_time_func),
        asyncio.sleep(1))
    print(f&quot;finish main at {time.strftime(&apos;%X&apos;)}&quot;)
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./11.%E7%BA%BF%E7%A8%8B.avif&quot; alt=&quot;线程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到如果不使用线程，需要2 s，但是程序的执行只需要1 s，成功避免了事件循环的阻塞。&lt;/p&gt;
&lt;h4&gt;3.3.2 跨线程调度&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;asyncio.run_coroutine_threadsafe(coro, loop)&lt;/code&gt;函数用于跨线程调度，即线程安全。&lt;/p&gt;
&lt;p&gt;函数返回一个&lt;code&gt;future&lt;/code&gt;对象，等待其他线程的结果。需要在事件循环外部调用该函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创一个协程
coro = asyncio.sleep(1, result=3)

# 提交一个携程到另一个事件循环loop中
future = asyncio.run_coroutine_threadsafe(coro, loop)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>CycleGAN-V2音色转换算法复现</title><link>https://blog.kimbleex.top/posts/2024-05-12-cycleganv2-voice-trans/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2024-05-12-cycleganv2-voice-trans/</guid><pubDate>Sun, 12 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;code&gt;GAN&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;先说一下&lt;code&gt;GAN&lt;/code&gt;，对抗生成模型核心即生成器&lt;code&gt;Generator&lt;/code&gt;和判别器&lt;code&gt;Discriminator&lt;/code&gt;，由生成器不停的生成数据，判别器给出反馈，以此往复知道判别器无法判断生成器生成的数据真伪。&lt;/p&gt;
&lt;p&gt;这样的过程相当于是从一张白纸做成了一张假钞，“警察”(&lt;code&gt;Discriminator&lt;/code&gt;)最后也无法判断出这是真币还是假币。实现了从一种特征到另一种特征的转换。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;CycleGAN-VC&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CycleGAN VC2&lt;/code&gt;是一种用于语音转换的模型，基于 &lt;code&gt;CycleGAN&lt;/code&gt;和&lt;code&gt;GAN&lt;/code&gt; 的原理进行改进和应用。其主要特点和原理包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;语音转换&lt;/strong&gt;：&lt;code&gt;CycleGAN VC2&lt;/code&gt; 主要用于将一种语音特征转换为另一种语音特征，如改变说话人的声音特征，同时保持内容不变。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;无配对数据训练&lt;/strong&gt;：与图像转换中的 &lt;code&gt;CycleGAN&lt;/code&gt; 类似，&lt;code&gt;CycleGAN VC2&lt;/code&gt; 不需要成对的语音数据进行训练。它可以在没有直接对应的语音对的情况下学习不同语音域之间的映射。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;循环一致性损失&lt;/strong&gt;：为了确保转换后的语音保留原始语音的内容，&lt;code&gt;CycleGAN VC2&lt;/code&gt; 使用循环一致性损失。这意味着，如果将语音从域 &lt;code&gt;A&lt;/code&gt; 转换到域 &lt;code&gt;B&lt;/code&gt;，然后再转换回域&lt;code&gt;A&lt;/code&gt;，最终得到的语音应该与原始语音相似。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生成器和判别器&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;生成器&lt;/strong&gt;：用于将语音特征从一种风格转换到另一种风格。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;判别器&lt;/strong&gt;：用于区分生成的语音特征和真实的语音特征。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;损失函数&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对抗性损失&lt;/strong&gt;：用于训练生成器，使生成的语音特征能够骗过判别器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环一致性损失&lt;/strong&gt;：用于确保语音在转换来回后保持一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;应用&lt;/strong&gt;：&lt;code&gt;CycleGAN VC2&lt;/code&gt;被用于语音风格转换、语音克隆、语音增强等领域。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这些机制，&lt;code&gt;CycleGAN VC2&lt;/code&gt; 可以在不需要大量配对数据的情况下，实现高质量的语音转换。&lt;/p&gt;
&lt;h2&gt;## &lt;code&gt;ResNet&lt;/code&gt;残差思想&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ResNet(Residual Network)&lt;/code&gt;是一种深度神经网络架构，其核心思想是“残差学习”。这种思想解决了随着网络深度增加而出现的梯度消失和梯度爆炸问题。&lt;/p&gt;
&lt;h3&gt;残差块（Residual Block）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;基本结构&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;残差块由两层或三层卷积网络组成，通常包括 &lt;code&gt;Batch Normalization&lt;/code&gt; 和 &lt;code&gt;ReLU&lt;/code&gt; 激活函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恒等映射&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;残差块的输出不是直接输出 &lt;code&gt;F(x)&lt;/code&gt;，而是 &lt;code&gt;F(x) + x&lt;/code&gt;，其中&lt;code&gt;x&lt;/code&gt; 是输入。&lt;/li&gt;
&lt;li&gt;这种结构称为“恒等映射”（&lt;code&gt;identity mapping&lt;/code&gt;），即通过跳跃连接（&lt;code&gt;shortcut connection&lt;/code&gt;）将输入直接加到输出上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;残差思想的优势&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解决退化问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随着网络层数增加，深层网络比浅层网络表现更差的问题称为“退化问题”。&lt;/li&gt;
&lt;li&gt;残差网络通过学习残差而不是直接学习原始映射，使得优化更容易。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;梯度流动更顺畅&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;恒等映射使得梯度可以直接通过跳跃连接反向传播，缓解梯度消失和梯度爆炸问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;易于优化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于学习的是残差，网络可以更容易地接近恒等映射，从而更快地收敛。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;实际应用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;深度网络设计&lt;/strong&gt;：&lt;code&gt;ResNet&lt;/code&gt; 可以支持非常深的网络（如 50 层、101 层、152 层），在图像分类、目标检测等任务中表现优异。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块化设计&lt;/strong&gt;：残差块可以堆叠，形成不同深度的 &lt;code&gt;ResNet&lt;/code&gt;网络。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心代码&lt;/h2&gt;
&lt;h3&gt;生成器&lt;code&gt;Generator&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class downSample_Generator(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(downSample_Generator, self).__init__()

        self.convLayer = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                 out_channels=out_channels,
                                                 kernel_size=kernel_size,
                                                 stride=stride,
                                                 padding=padding),
                                       nn.InstanceNorm2d(num_features=out_channels,
                                                         affine=True))
        self.convLayer_gates = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                       out_channels=out_channels,
                                                       kernel_size=kernel_size,
                                                       stride=stride,
                                                       padding=padding),
                                             nn.InstanceNorm2d(num_features=out_channels,
                                                               affine=True))

    def forward(self, input):
        a = self.convLayer(input)
        b = self.convLayer_gates(input)
        return self.convLayer(input) * torch.sigmoid(self.convLayer_gates(input))


class upSample_Generator(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(upSample_Generator, self).__init__()

        self.convLayer = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                 out_channels=out_channels,
                                                 kernel_size=kernel_size,
                                                 stride=stride,
                                                 padding=padding),
                                       #PixelShuffle(upscale_factor=2),
                                       up_2Dsample(upscale_factor=2),
                                       nn.InstanceNorm2d(num_features=out_channels,
                                                         affine=True))
        self.convLayer_gates = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                       out_channels=out_channels,
                                                       kernel_size=kernel_size,
                                                       stride=stride,
                                                       padding=padding),
                                             #PixelShuffle(upscale_factor=2),
                                             up_2Dsample(upscale_factor=2),
                                             nn.InstanceNorm2d(num_features=out_channels,
                                                               affine=True))

   

    def forward(self, input):        
        return self.convLayer(input) * torch.sigmoid(self.convLayer_gates(input))


class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=1,
                               out_channels=128,
                               kernel_size=[5,15],
                               stride=1,
                               padding=[2,7])

        self.conv1_gates = nn.Conv2d(in_channels=1,
                               out_channels=128,
                               kernel_size=[5,15],
                               stride=1,
                               padding=[2,7])

        # Downsample Layer
        self.downSample1 = downSample_Generator(in_channels=128,
                                                out_channels=256,
                                                kernel_size=5,
                                                stride=2,
                                                padding=2)

        self.downSample2 = downSample_Generator(in_channels=256,
                                                out_channels=512,
                                                kernel_size=5,
                                                stride=2,
                                                padding=2)
        #reshape
        self.conv2 = nn.Conv1d(in_channels=3072,
                               out_channels=512,
                               kernel_size=1,
                               stride=1)

        # Residual Blocks
        self.residualLayer1 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer2 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer3 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer4 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer5 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer6 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        #reshape
        self.conv3 = nn.Conv1d(in_channels=512,
                               out_channels=3072,
                               kernel_size=1,
                               stride=1)


        # UpSample Layer
        self.upSample1 = upSample_Generator(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=5,
                                            stride=1,
                                            padding=2)
        
        self.upSample2 = upSample_Generator(in_channels=1024,
                                            out_channels=512,
                                            kernel_size=5,
                                            stride=1,
                                            padding=2)

        self.lastConvLayer = nn.Conv2d(in_channels=512,
                                       out_channels=1,
                                       kernel_size=[5,15],
                                       stride=1,
                                       padding=[2,7])

    def forward(self, input):
        # GLU
        input = input.unsqueeze(1)

        conv1 = self.conv1(input) * torch.sigmoid(self.conv1_gates(input))

        downsample1 = self.downSample1(conv1)
        
        downsample2 = self.downSample2(downsample1)
        
        downsample3 = downsample2.view([downsample2.shape[0],-1,downsample2.shape[3]])
        
        downsample3 = self.conv2(downsample3)
        
        residual_layer_1 = self.residualLayer1(downsample3)
        
        residual_layer_2 = self.residualLayer2(residual_layer_1)
        
        residual_layer_3 = self.residualLayer3(residual_layer_2)
        
        residual_layer_4 = self.residualLayer4(residual_layer_3)
        
        residual_layer_5 = self.residualLayer5(residual_layer_4)
        
        residual_layer_6 = self.residualLayer6(residual_layer_5)
        
        residual_layer_6 = self.conv3(residual_layer_6)
        
        residual_layer_6 = residual_layer_6.view([downsample2.shape[0],downsample2.shape[1],downsample2.shape[2],downsample2.shape[3]])
        
        upSample_layer_1 = self.upSample1(residual_layer_6)
        
        upSample_layer_2 = self.upSample2(upSample_layer_1)
        
        output = self.lastConvLayer(upSample_layer_2)
        
        output = output.view([output.shape[0],-1,output.shape[3]])
        return output
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;判别器&lt;code&gt;Discriminator&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class DownSample_Discriminator(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(DownSample_Discriminator, self).__init__()

        self.convLayer = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                 out_channels=out_channels,
                                                 kernel_size=kernel_size,
                                                 stride=stride,
                                                 padding=padding),
                                       nn.InstanceNorm2d(num_features=out_channels,
                                                         affine=True))
        self.convLayerGates = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                      out_channels=out_channels,
                                                      kernel_size=kernel_size,
                                                      stride=stride,
                                                      padding=padding),
                                            nn.InstanceNorm2d(num_features=out_channels,
                                                              affine=True))

    def forward(self, input):
        # GLU
        return self.convLayer(input) * torch.sigmoid(self.convLayerGates(input))


class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        self.convLayer1 = nn.Conv2d(in_channels=1,
                                    out_channels=128,
                                    kernel_size=[3, 3],
                                    stride=[1, 1])
        self.convLayer1_gates = nn.Conv2d(in_channels=1,
                                          out_channels=128,
                                          kernel_size=[3, 3],
                                          stride=[1, 1])

        # Note: Kernel Size have been modified in the PyTorch implementation
        # compared to the actual paper, as to retain dimensionality. Unlike,
        # TensorFlow, PyTorch doesn&apos;t have padding=&apos;same&apos;, hence, kernel sizes
        # were altered to retain the dimensionality after each layer

        # DownSample Layer
        self.downSample1 = DownSample_Discriminator(in_channels=128,
                                                    out_channels=256,
                                                    kernel_size=[3, 3],
                                                    stride=[2, 2],
                                                    padding=0)

        self.downSample2 = DownSample_Discriminator(in_channels=256,
                                                    out_channels=512,
                                                    kernel_size=[3, 3],
                                                    stride=[2, 2],
                                                    padding=0)

        self.downSample3 = DownSample_Discriminator(in_channels=512,
                                                    out_channels=1024,
                                                    kernel_size=[3, 3],
                                                    stride=[2, 2],
                                                    padding=0)

        self.downSample4 = DownSample_Discriminator(in_channels=1024,
                                                    out_channels=1024,
                                                    kernel_size=[1, 5],
                                                    stride=[1, 1],
                                                    padding=[0, 2])

        # Fully Connected Layer
        self.fc = nn.Linear(in_features=1024,
                            out_features=1)

        # output Layer
        self.output_layer = nn.Conv2d(in_channels=1024,
                                      out_channels=1,
                                      kernel_size=[1, 3],
                                      stride=[1, 1],
                                      padding=[0, 1])

    def forward(self, input):
        # input has shape [batch_size, num_features, time]
        # discriminator requires shape [batchSize, 1, num_features, time
        input = input.unsqueeze(1)
        # GLU
        pad_input = nn.ZeroPad2d((1, 1, 1, 1))
        layer1 = self.convLayer1(
            pad_input(input)) * torch.sigmoid(self.convLayer1_gates(pad_input(input)))

        pad_input = nn.ZeroPad2d((1, 0, 1, 0))
        downSample1 = self.downSample1(pad_input(layer1))

        pad_input = nn.ZeroPad2d((1, 0, 1, 0))
        downSample2 = self.downSample2(pad_input(downSample1))

        pad_input = nn.ZeroPad2d((1, 0, 1, 0))
        downSample3 = self.downSample3(pad_input(downSample2))

        downSample4 = self.downSample4(downSample3)
        downSample4 = self.output_layer(downSample4)

        downSample4 = downSample4.contiguous().permute(0, 2, 3, 1).contiguous()
        # fc = torch.sigmoid(self.fc(downSample3))
        # Taking off sigmoid layer to avoid vanishing gradient problem
        #fc = self.fc(downSample4)
        fc = torch.sigmoid(downSample4)
        return fc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;残差单元&lt;code&gt;Res-Block&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class ResidualLayer(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(ResidualLayer, self).__init__()

        self.conv1d_layer = nn.Sequential(nn.Conv1d(in_channels=in_channels,
                                                    out_channels=out_channels,
                                                    kernel_size=kernel_size,
                                                    stride=1,
                                                    padding=padding),
                                          nn.InstanceNorm1d(num_features=out_channels,
                                                            affine=True))

        self.conv_layer_gates = nn.Sequential(nn.Conv1d(in_channels=in_channels,
                                                        out_channels=out_channels,
                                                        kernel_size=kernel_size,
                                                        stride=1,
                                                        padding=padding),
                                              nn.InstanceNorm1d(num_features=out_channels,
                                                                affine=True))

        self.conv1d_out_layer = nn.Sequential(nn.Conv1d(in_channels=out_channels,
                                                        out_channels=in_channels,
                                                        kernel_size=kernel_size,
                                                        stride=1,
                                                        padding=padding),
                                              nn.InstanceNorm1d(num_features=in_channels,
                                                                affine=True))

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;输入输出测试&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;input = torch.randn(10, 24, 1100)  # (N, C_in, Width) For Conv1d
np.random.seed(0)
print(np.random.randn(10))
input = np.random.randn(15, 24, 128)
input = torch.from_numpy(input).float()
# print(input)
generator = Generator()

output = generator(input)
print(&quot;Output shape Generator&quot;, output.shape)

# Discriminator Dimensionality Testing
# input = torch.randn(32, 1, 24, 128)  # (N, C_in, height, width) For Conv2d
discriminator = Discriminator()
#pdb.set_trace()
output = discriminator(output)
print(&quot;Output shape Discriminator&quot;, output.shape)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import torch.nn as nn
import torch.nn.functional as F
import torch
import numpy as np
import pdb


class GLU(nn.Module):
    def __init__(self):
        super(GLU, self).__init__()
        # Custom Implementation because the Voice Conversion Cycle GAN
        # paper assumes GLU won&apos;t reduce the dimension of tensor by 2.

    def forward(self, input):
        return input * torch.sigmoid(input)


class up_2Dsample(nn.Module):
    def __init__(self, upscale_factor=2):
        super(up_2Dsample, self).__init__()
        self.scale_factor = upscale_factor

    def forward(self, input):
        h = input.shape[2]
        w = input.shape[3]
        new_size = [h * self.scale_factor, w * self.scale_factor]
        return F.interpolate(input,new_size)
       

class PixelShuffle(nn.Module):
    def __init__(self, upscale_factor=2):
        super(PixelShuffle, self).__init__()
        # Custom Implementation because PyTorch PixelShuffle requires,
        # 4D input. Whereas, in this case we have have 3D array
        self.upscale_factor = upscale_factor

    def forward(self, input):
        n = input.shape[0]
        c_out = input.shape[1] // self.upscale_factor
        w_new = input.shape[2] * self.upscale_factor
        return input.view(n, c_out, w_new)


class ResidualLayer(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(ResidualLayer, self).__init__()

        # self.residualLayer = nn.Sequential(nn.Conv1d(in_channels=in_channels,
        #                                              out_channels=out_channels,
        #                                              kernel_size=kernel_size,
        #                                              stride=1,
        #                                              padding=padding),
        #                                    nn.InstanceNorm1d(
        #                                        num_features=out_channels,
        #                                        affine=True),
        #                                    GLU(),
        #                                    nn.Conv1d(in_channels=out_channels,
        #                                              out_channels=in_channels,
        #                                              kernel_size=kernel_size,
        #                                              stride=1,
        #                                              padding=padding),
        #                                    nn.InstanceNorm1d(
        #                                        num_features=in_channels,
        #                                        affine=True)
        #                                    )

        self.conv1d_layer = nn.Sequential(nn.Conv1d(in_channels=in_channels,
                                                    out_channels=out_channels,
                                                    kernel_size=kernel_size,
                                                    stride=1,
                                                    padding=padding),
                                          nn.InstanceNorm1d(num_features=out_channels,
                                                            affine=True))

        self.conv_layer_gates = nn.Sequential(nn.Conv1d(in_channels=in_channels,
                                                        out_channels=out_channels,
                                                        kernel_size=kernel_size,
                                                        stride=1,
                                                        padding=padding),
                                              nn.InstanceNorm1d(num_features=out_channels,
                                                                affine=True))

        self.conv1d_out_layer = nn.Sequential(nn.Conv1d(in_channels=out_channels,
                                                        out_channels=in_channels,
                                                        kernel_size=kernel_size,
                                                        stride=1,
                                                        padding=padding),
                                              nn.InstanceNorm1d(num_features=in_channels,
                                                                affine=True))

    def forward(self, input):
        h1_norm = self.conv1d_layer(input)
        h1_gates_norm = self.conv_layer_gates(input)

        # GLU
        h1_glu = h1_norm * torch.sigmoid(h1_gates_norm)

        h2_norm = self.conv1d_out_layer(h1_glu)
        return input + h2_norm


class downSample_Generator(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(downSample_Generator, self).__init__()

        self.convLayer = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                 out_channels=out_channels,
                                                 kernel_size=kernel_size,
                                                 stride=stride,
                                                 padding=padding),
                                       nn.InstanceNorm2d(num_features=out_channels,
                                                         affine=True))
        self.convLayer_gates = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                       out_channels=out_channels,
                                                       kernel_size=kernel_size,
                                                       stride=stride,
                                                       padding=padding),
                                             nn.InstanceNorm2d(num_features=out_channels,
                                                               affine=True))

    def forward(self, input):
        a = self.convLayer(input)
        b = self.convLayer_gates(input)
        return self.convLayer(input) * torch.sigmoid(self.convLayer_gates(input))


class upSample_Generator(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(upSample_Generator, self).__init__()

        self.convLayer = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                 out_channels=out_channels,
                                                 kernel_size=kernel_size,
                                                 stride=stride,
                                                 padding=padding),
                                       #PixelShuffle(upscale_factor=2),
                                       up_2Dsample(upscale_factor=2),
                                       nn.InstanceNorm2d(num_features=out_channels,
                                                         affine=True))
        self.convLayer_gates = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                       out_channels=out_channels,
                                                       kernel_size=kernel_size,
                                                       stride=stride,
                                                       padding=padding),
                                             #PixelShuffle(upscale_factor=2),
                                             up_2Dsample(upscale_factor=2),
                                             nn.InstanceNorm2d(num_features=out_channels,
                                                               affine=True))

   

    def forward(self, input):        
        return self.convLayer(input) * torch.sigmoid(self.convLayer_gates(input))


class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=1,
                               out_channels=128,
                               kernel_size=[5,15],
                               stride=1,
                               padding=[2,7])

        self.conv1_gates = nn.Conv2d(in_channels=1,
                               out_channels=128,
                               kernel_size=[5,15],
                               stride=1,
                               padding=[2,7])

        # Downsample Layer
        self.downSample1 = downSample_Generator(in_channels=128,
                                                out_channels=256,
                                                kernel_size=5,
                                                stride=2,
                                                padding=2)

        self.downSample2 = downSample_Generator(in_channels=256,
                                                out_channels=512,
                                                kernel_size=5,
                                                stride=2,
                                                padding=2)
        #reshape
        self.conv2 = nn.Conv1d(in_channels=3072,
                               out_channels=512,
                               kernel_size=1,
                               stride=1)

        # Residual Blocks
        self.residualLayer1 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer2 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer3 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer4 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer5 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        self.residualLayer6 = ResidualLayer(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1)
        #reshape
        self.conv3 = nn.Conv1d(in_channels=512,
                               out_channels=3072,
                               kernel_size=1,
                               stride=1)


        # UpSample Layer
        self.upSample1 = upSample_Generator(in_channels=512,
                                            out_channels=1024,
                                            kernel_size=5,
                                            stride=1,
                                            padding=2)
        
        self.upSample2 = upSample_Generator(in_channels=1024,
                                            out_channels=512,
                                            kernel_size=5,
                                            stride=1,
                                            padding=2)

        self.lastConvLayer = nn.Conv2d(in_channels=512,
                                       out_channels=1,
                                       kernel_size=[5,15],
                                       stride=1,
                                       padding=[2,7])

    def forward(self, input):
        # GLU
        input = input.unsqueeze(1)

        conv1 = self.conv1(input) * torch.sigmoid(self.conv1_gates(input))

        downsample1 = self.downSample1(conv1)
        
        downsample2 = self.downSample2(downsample1)
        
        downsample3 = downsample2.view([downsample2.shape[0],-1,downsample2.shape[3]])
        
        downsample3 = self.conv2(downsample3)
        
        residual_layer_1 = self.residualLayer1(downsample3)
        
        residual_layer_2 = self.residualLayer2(residual_layer_1)
        
        residual_layer_3 = self.residualLayer3(residual_layer_2)
        
        residual_layer_4 = self.residualLayer4(residual_layer_3)
        
        residual_layer_5 = self.residualLayer5(residual_layer_4)
        
        residual_layer_6 = self.residualLayer6(residual_layer_5)
        
        residual_layer_6 = self.conv3(residual_layer_6)
        
        residual_layer_6 = residual_layer_6.view([downsample2.shape[0],downsample2.shape[1],downsample2.shape[2],downsample2.shape[3]])
        
        upSample_layer_1 = self.upSample1(residual_layer_6)
        
        upSample_layer_2 = self.upSample2(upSample_layer_1)
        
        output = self.lastConvLayer(upSample_layer_2)
        
        output = output.view([output.shape[0],-1,output.shape[3]])
        return output


class DownSample_Discriminator(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(DownSample_Discriminator, self).__init__()

        self.convLayer = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                 out_channels=out_channels,
                                                 kernel_size=kernel_size,
                                                 stride=stride,
                                                 padding=padding),
                                       nn.InstanceNorm2d(num_features=out_channels,
                                                         affine=True))
        self.convLayerGates = nn.Sequential(nn.Conv2d(in_channels=in_channels,
                                                      out_channels=out_channels,
                                                      kernel_size=kernel_size,
                                                      stride=stride,
                                                      padding=padding),
                                            nn.InstanceNorm2d(num_features=out_channels,
                                                              affine=True))

    def forward(self, input):
        # GLU
        return self.convLayer(input) * torch.sigmoid(self.convLayerGates(input))


class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        self.convLayer1 = nn.Conv2d(in_channels=1,
                                    out_channels=128,
                                    kernel_size=[3, 3],
                                    stride=[1, 1])
        self.convLayer1_gates = nn.Conv2d(in_channels=1,
                                          out_channels=128,
                                          kernel_size=[3, 3],
                                          stride=[1, 1])

        # Note: Kernel Size have been modified in the PyTorch implementation
        # compared to the actual paper, as to retain dimensionality. Unlike,
        # TensorFlow, PyTorch doesn&apos;t have padding=&apos;same&apos;, hence, kernel sizes
        # were altered to retain the dimensionality after each layer

        # DownSample Layer
        self.downSample1 = DownSample_Discriminator(in_channels=128,
                                                    out_channels=256,
                                                    kernel_size=[3, 3],
                                                    stride=[2, 2],
                                                    padding=0)

        self.downSample2 = DownSample_Discriminator(in_channels=256,
                                                    out_channels=512,
                                                    kernel_size=[3, 3],
                                                    stride=[2, 2],
                                                    padding=0)

        self.downSample3 = DownSample_Discriminator(in_channels=512,
                                                    out_channels=1024,
                                                    kernel_size=[3, 3],
                                                    stride=[2, 2],
                                                    padding=0)

        self.downSample4 = DownSample_Discriminator(in_channels=1024,
                                                    out_channels=1024,
                                                    kernel_size=[1, 5],
                                                    stride=[1, 1],
                                                    padding=[0, 2])

        # Fully Connected Layer
        self.fc = nn.Linear(in_features=1024,
                            out_features=1)

        # output Layer
        self.output_layer = nn.Conv2d(in_channels=1024,
                                      out_channels=1,
                                      kernel_size=[1, 3],
                                      stride=[1, 1],
                                      padding=[0, 1])

    # def downSample(self, in_channels, out_channels, kernel_size, stride, padding):
    #     convLayer = nn.Sequential(nn.Conv2d(in_channels=in_channels,
    #                                         out_channels=out_channels,
    #                                         kernel_size=kernel_size,
    #                                         stride=stride,
    #                                         padding=padding),
    #                               nn.InstanceNorm2d(num_features=out_channels,
    #                                                 affine=True),
    #                               GLU())
    #     return convLayer

    def forward(self, input):
        # input has shape [batch_size, num_features, time]
        # discriminator requires shape [batchSize, 1, num_features, time]
        input = input.unsqueeze(1)
        # GLU
        pad_input = nn.ZeroPad2d((1, 1, 1, 1))
        layer1 = self.convLayer1(
            pad_input(input)) * torch.sigmoid(self.convLayer1_gates(pad_input(input)))

        pad_input = nn.ZeroPad2d((1, 0, 1, 0))
        downSample1 = self.downSample1(pad_input(layer1))

        pad_input = nn.ZeroPad2d((1, 0, 1, 0))
        downSample2 = self.downSample2(pad_input(downSample1))

        pad_input = nn.ZeroPad2d((1, 0, 1, 0))
        downSample3 = self.downSample3(pad_input(downSample2))

        downSample4 = self.downSample4(downSample3)
        downSample4 = self.output_layer(downSample4)

        downSample4 = downSample4.contiguous().permute(0, 2, 3, 1).contiguous()
        # fc = torch.sigmoid(self.fc(downSample3))
        # Taking off sigmoid layer to avoid vanishing gradient problem
        #fc = self.fc(downSample4)
        fc = torch.sigmoid(downSample4)
        return fc


if __name__ == &apos;__main__&apos;:
    
    # Generator Dimensionality Testing
    input = torch.randn(10, 24, 1100)  # (N, C_in, Width) For Conv1d
    np.random.seed(0)
    print(np.random.randn(10))
    input = np.random.randn(15, 24, 128)
    input = torch.from_numpy(input).float()
    # print(input)
    generator = Generator()
    
    output = generator(input)
    print(&quot;Output shape Generator&quot;, output.shape)
    
    # Discriminator Dimensionality Testing
    # input = torch.randn(32, 1, 24, 128)  # (N, C_in, height, width) For Conv2d
    discriminator = Discriminator()
    #pdb.set_trace()
    output = discriminator(output)
    print(&quot;Output shape Discriminator&quot;, output.shape)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Python使用Schedule模块实现定时任务</title><link>https://blog.kimbleex.top/posts/2023-12-05-python-schedule/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2023-12-05-python-schedule/</guid><pubDate>Tue, 05 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;有时候写爬虫的时候会想要每隔固定的时间执行一次，所以就想到了设计一个定时任务，于是找到了Python的Schedule模块，记录一下关于Schedule模块的一些知识点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;schedule&lt;/code&gt; 模块是一个用于在 Python 中实现简单任务调度的库。它允许你以简洁的语法安排任务。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;首先，安装 &lt;code&gt;schedule&lt;/code&gt; 模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install schedule
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基本用法及案例&lt;/h2&gt;
&lt;h3&gt;一个整体的示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import schedule
import time

def job():
    print(&quot;I&apos;m working...&quot;)

# 每三秒钟执行一次上面的任务job
schedule.every(3).seconds.do(job)

while True:
    schedule.run_pending()
    time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;schedule.run_pending()&lt;/code&gt; 来运行所有准备好的任务。通常将其放在一个循环中，以持续检查和执行任务。&lt;/p&gt;
&lt;h3&gt;其他的一些定时任务写法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;schedule&lt;/code&gt; 模块使用一种类似自然语言的方式来安排任务。以下是一些常用的方法:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每隔 3 sec/min/hour/day/week执行一次&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;schedule.every(3).seconds.do(job)
schedule.every(3).minutes.do(job)
schedule.every(3).hours.do(job)
schedule.every(3).days.do(job)
schedule.every(3).weeks.do(job)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每分钟的第23秒执行一次任务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;schedule.every().minute.at(&quot;:23&quot;).do(job)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每小时的第40分钟执行一次任务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;schedule.every().hour.at(&quot;:40&quot;).do(job)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每隔5个小时，在第五个小时的20分钟30秒执行一次任务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 如果 02:00 执行, 那么将会在 06:20:30 执行
schedule.every(5).hours.at(&quot;20:30&quot;).do(job)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在每天的特定时间执行任务&lt;code&gt;HH:MM:SS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;schedule.every().day.at(&quot;10:30:42&quot;).do(job)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;还可以使用装饰器来修饰&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;@repect&lt;/code&gt;装饰器装饰任务函数后，也可以做到同样的效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from schedule import every, repeat, run_pending
import time

@repeat(every(10).minutes)
def job():
    print(&quot;I am a scheduled job&quot;)

while True:
    run_pending()
    time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;带有参数的任务&lt;/h3&gt;
&lt;p&gt;如果任务中需要传入参数，可以在&lt;code&gt;do()&lt;/code&gt;中传入参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import schedule

def greet(name):
    print(&apos;Hello&apos;, name)
schedule.every(2).seconds.do(greet, name=&apos;Alice&apos;)
schedule.every(4).seconds.do(greet, name=&apos;Bob&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装饰器版本传参代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from schedule import every, repeat

@repeat(every().second, &quot;World&quot;)
@repeat(every().day, &quot;Mars&quot;)
def hello(planet):
    print(&quot;Hello&quot;, planet)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;标记任务并按标签获取&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;tag()&lt;/code&gt;可以给任务上标签。
使用&lt;code&gt;get_jobs()&lt;/code&gt;方法获取任务所有任务，如果传入标签名则按标签获取任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import schedule

def job1():
    print(&quot;Job 1&quot;)

def job2():
    print(&quot;Job 2&quot;)

# 上标签,可以无限标签
schedule.every().day.do(job1).tag(&quot;daily&quot;, &quot;haha&quot;)
schedule.every().hour.do(job2).tag(&quot;hourly&quot;)

# 获取所有带有 &quot;daily&quot; 标签的任务
daily_jobs = schedule.get_jobs(&quot;daily&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;取消任务&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;cancel()&lt;/code&gt;方法可以取消任务。
使用&lt;code&gt;clear()&lt;/code&gt;方法清除所有任务，如果传入标签名即可按标签取消&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import schedule

def job1():
    print(&quot;I&apos;m working...&quot;)

def job2():
    print(&quot;I&apos;m working too...&quot;)

job1 = schedule.every(3).seconds.do(job1).tag(&quot;ok&quot;)
job2 = schedule.every(5).seconds.do(job2).tag(&quot;no&quot;)

# 取消任务
schedule.cancel_job1(job) # 取消job1
schedule.clear(&quot;ok&quot;) # 按标签取消
schedule.clear() # 清除所有任务
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;任务终止判定&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;until()&lt;/code&gt;来设置任务的终止时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 2030年1月1日18:33后不再执行
schedule.every(1).hours.until(&quot;2030-01-01 18:33&quot;).do(job)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Schedule&lt;/code&gt;模块的定时任务并不是真正的定时任务，而是通过循环来模拟定时任务。因此，如果任务执行时间过长，可能会导致任务堆积，影响后续任务的执行。因此，在编写任务时，需要注意任务的执行时间，避免任务堆积。&lt;/p&gt;
&lt;p&gt;简单来说，就是&lt;code&gt;Schedule&lt;/code&gt;模块不计算任务的具体执行时长，如果一个任务需要6min执行完成，但是你的调度方法时每5min执行一次，那么程序会产生等待，直到任务执行完成，才会继续执行下一个任务。&lt;/p&gt;
</content:encoded></item><item><title>知识扫盲1：关于localhost和127.0.0.1</title><link>https://blog.kimbleex.top/posts/2023-11-25-compare-local-127001/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2023-11-25-compare-local-127001/</guid><pubDate>Sat, 25 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近捣鼓本地部署一些大模型应用，突然发现本地网页可以通过&lt;code&gt;localhost:port&lt;/code&gt;访问，但是却不能通过&lt;code&gt;127.0.0.1:port&lt;/code&gt;访问，所以去详细琢磨了一下这两者的区别。&lt;/p&gt;
&lt;p&gt;奇怪的是我以前做的一些全栈项目，当初全凭心情来使用&lt;code&gt;localhost&lt;/code&gt;或是&lt;code&gt;127.0.0.1&lt;/code&gt;来访问也没啥问题，所以也不清楚问题的具体原因。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;LocalHost&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;localhost&lt;/code&gt;是一个主机名称，它是本地服务器名称，在访问&lt;code&gt;localhost&lt;/code&gt;时，它指向本地电脑主机的网络地址，系统会自动把它解析成&lt;code&gt;IP&lt;/code&gt;地址，通常即为本地回环地址&lt;code&gt;127.0.0.1&lt;/code&gt;。但是，它可以被配置为任何&lt;code&gt;IP&lt;/code&gt;地址，包括&lt;code&gt;IPv4&lt;/code&gt;和&lt;code&gt;IPv6&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;127.0.0.1&lt;/code&gt;是一个&lt;code&gt;IPv4&lt;/code&gt;地址，它是一个特殊的&lt;code&gt;IP&lt;/code&gt;地址，用于指向本地主机。在计算机网络中，&lt;code&gt;127.0.0.1&lt;/code&gt;被称为回环地址（Loopback Address），它是一个特殊的IP地址，用于测试网络连接和本地服务。&lt;/p&gt;
&lt;h2&gt;区别&lt;/h2&gt;
&lt;p&gt;:::tip[localhost]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是一个主机名。&lt;/li&gt;
&lt;li&gt;通常解析为&lt;code&gt;127.0.0.1&lt;/code&gt;，但解析依赖于系统的&lt;code&gt;hosts&lt;/code&gt;文件配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::note[127.0.0.1]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是一个具体的 IPv4 地址。&lt;/li&gt;
&lt;li&gt;直接指向本地环回接口。&lt;/li&gt;
&lt;li&gt;它就是一个&lt;code&gt;Ipv4&lt;/code&gt;地址，无需解析，但是不支持&lt;code&gt;Ipv6&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;问题重述&lt;/h2&gt;
&lt;p&gt;文章开头提到的两者不能同时访问的问题，究其原因，很可能是&lt;code&gt;localhost&lt;/code&gt;访问时，系统带的本机当前用户的权限去访问，而用&lt;code&gt;ip&lt;/code&gt;的时候，等于本机是通过网络再去访问本机，可能涉及到网络用户的权限。&lt;/p&gt;
&lt;p&gt;总结来说，&lt;code&gt;localhost&lt;/code&gt;是一个主机名，通常解析为&lt;code&gt;127.0.0.1&lt;/code&gt;，但它可以配置为其他地址。而&lt;code&gt;127.0.0.1&lt;/code&gt;是一个固定的&lt;code&gt;IPv4&lt;/code&gt;地址，直接用于环回通信。&lt;/p&gt;
</content:encoded></item><item><title>群智能优化算法详解与实现</title><link>https://blog.kimbleex.top/posts/2023-08-23-intelligent-opt-algorithm/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2023-08-23-intelligent-opt-algorithm/</guid><pubDate>Wed, 23 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;跟课题组的一个学长合作的一篇论文, 最近发表了, 总结一下在代码设计中学到的算法。&lt;/p&gt;
&lt;p&gt;首先，所谓优化算法，是指对算法的有关性能进行优化，如时间复杂度、 空间复杂度 、正确性、 健壮性 。由于算法应用情景变化很大，算法优化可以使算法具有更好泛化能力。 算法优化是指对算法的有关性能进行优化，如时间复杂度、 空间复杂度 、正确性、健壮性。 大数据时代到来，算法要处理数据的数量级也越来越大以及处理问题的场景千变万化。 为了增强算法的处理问题的能力，对算法进行优化是必不可少的。对一些流程比如加工行业、旅游行业等，进行优化，其中最为典型的问题就是&lt;code&gt;旅行商问题(TSP)&lt;/code&gt;。总而言之，优化算法的总目的就是将整个过程的成本(比如金钱、时间、各种消耗等)最低，典型的优化算法包括: &lt;code&gt;遗传算法(GA)&lt;/code&gt;、&lt;code&gt;禁忌算法(TS)&lt;/code&gt;、&lt;code&gt;模拟退火算法(SA)&lt;/code&gt;、&lt;code&gt;粒子群算法(PSO)&lt;/code&gt;、&lt;code&gt;差分算法(DE)&lt;/code&gt;、&lt;code&gt;生物地理算法(BBO)&lt;/code&gt;等，下面我会对这些算法都或多或少做一些代码方面的讲解，每篇讲解后面我都会附上代码。&lt;/p&gt;
&lt;h2&gt;1. 遗传算法(GA)&lt;/h2&gt;
&lt;p&gt;遗传算法可以说是最基本的优化算法，它是根据人类生殖过程中染色体的变化而产生的，原理是对于父代数据进行编译，在通过一系列操作对它进行“遗传和变异”，不断淘汰&lt;code&gt;适应度(Fitness)&lt;/code&gt;低的个体数据，从而跳出局部最优解，产全局最优解，当然在过程中对&lt;code&gt;学习率(Learning Rate)&lt;/code&gt;也有严格的控制。现在比较通俗好懂的讲解方式是用袋鼠爬山来比喻这个算法。&lt;/p&gt;
&lt;h3&gt;算法流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编码&lt;/strong&gt;: 将解表示为染色体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适应度函数&lt;/strong&gt;: 评估染色体优劣。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择&lt;/strong&gt;: 根据适应度选择个体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交叉&lt;/strong&gt;: 交换染色体基因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变异&lt;/strong&gt;: 随机改变基因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代&lt;/strong&gt;: 重复上述过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Python实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import random

POP_SIZE = 100
GENE_LENGTH = 10
MAX_GEN = 50
CROSS_RATE = 0.8
MUTATION_RATE = 0.01

def fitness(individual):
    x = int(&apos;&apos;.join(map(str, individual)), 2)
    return x ** 2

def init_population():
    return [[random.randint(0, 1) for _ in range(GENE_LENGTH)] for _ in range(POP_SIZE)]

def select(population):
    total_fitness = sum(fitness(ind) for ind in population)
    probs = [fitness(ind) / total_fitness for ind in population]
    return population[random.choices(range(POP_SIZE), probs)[0]]

def crossover(parent1, parent2):
    if random.random() &amp;lt; CROSS_RATE:
        point = random.randint(1, GENE_LENGTH - 1)
        return parent1[:point] + parent2[point:], parent2[:point] + parent1[point:]
    return parent1, parent2

def mutate(individual):
    for i in range(GENE_LENGTH):
        if random.random() &amp;lt; MUTATION_RATE:
            individual[i] = 1 - individual[i]

def genetic_algorithm():
    population = init_population()
    for generation in range(MAX_GEN):
        new_population = []
        for _ in range(POP_SIZE // 2):
            parent1 = select(population)
            parent2 = select(population)
            child1, child2 = crossover(parent1, parent2)
            mutate(child1)
            mutate(child2)
            new_population.extend([child1, child2])
        population = new_population
        best_individual = max(population, key=fitness)
        print(f&quot;Generation {generation}: Best fitness = {fitness(best_individual)}&quot;)
    return best_individual

best_solution = genetic_algorithm()
print(f&quot;Best solution: {best_solution}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码解析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;: 生成随机种群。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适应度计算&lt;/strong&gt;: 评估个体适应度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择、交叉与变异&lt;/strong&gt;: 生成新个体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代更新&lt;/strong&gt;: 逐代进化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 粒子群优化(PSO)&lt;/h2&gt;
&lt;p&gt;粒子群算法模拟鸟群的捕食行为。一群鸟在随机搜索食物，在这个区域里只有一块食物。所有的鸟都不知道食物在那里。但是他们知道当前的位置离食物还有多远。那么找到食物的最优策略是什么呢。最简单有效的就是搜寻离食物最近的鸟的周围区域。&lt;code&gt;PSO&lt;/code&gt;从这种模型中得到启示并用于解决优化问题。&lt;code&gt;PSO&lt;/code&gt;中，每个优化问题的解都是搜索空间中的一只鸟。我们称之为“粒子”。所有的粒子都有一个由被优化的函数决定的&lt;code&gt;适应值(fitnessvalue)&lt;/code&gt;，每个粒子还有一个速度决定他们飞翔的方向和距离。然后粒子们就追随当前的最优粒子在解空间中搜索。&lt;/p&gt;
&lt;h3&gt;算法流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;: 随机生成粒子的位置和速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;速度更新&lt;/strong&gt;: 根据个体和全局最优位置更新速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;位置更新&lt;/strong&gt;: 根据速度更新粒子位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代&lt;/strong&gt;: 更新个体和全局最优解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Python实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

PARTICLE_SIZE = 30
DIMENSION = 2
MAX_ITER = 100
W = 0.5
C1 = 1.5
C2 = 1.5

def objective_function(x):
    return np.sum(x**2)

class Particle:
    def __init__(self):
        self.position = np.random.rand(DIMENSION)
        self.velocity = np.random.rand(DIMENSION)
        self.best_position = self.position.copy()
        self.best_value = objective_function(self.position)

def pso():
    particles = [Particle() for _ in range(PARTICLE_SIZE)]
    global_best_position = min(particles, key=lambda p: p.best_value).best_position
    global_best_value = objective_function(global_best_position)

    for _ in range(MAX_ITER):
        for particle in particles:
            r1, r2 = np.random.rand(DIMENSION), np.random.rand(DIMENSION)
            particle.velocity =(W * particle.velocity +
                                 C1 * r1 *(particle.best_position - particle.position) +
                                 C2 * r2 *(global_best_position - particle.position))
            particle.position += particle.velocity

            current_value = objective_function(particle.position)
            if current_value &amp;lt; particle.best_value:
                particle.best_position = particle.position.copy()
                particle.best_value = current_value

            if current_value &amp;lt; global_best_value:
                global_best_position = particle.position.copy()
                global_best_value = current_value

        print(f&quot;Best value: {global_best_value}&quot;)

    return global_best_position

best_position = pso()
print(f&quot;Best position: {best_position}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码解析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;: 随机生成粒子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;速度与位置更新&lt;/strong&gt;: 根据个体和全局最优调整。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代更新&lt;/strong&gt;: 寻找最优解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 差分进化算法(DE)&lt;/h2&gt;
&lt;p&gt;和遗传算法一样，差分进化算法也是一种基于现代智能理论的优化算法，通过群体内个体之间的相互合作与竞争产生的群体智能来指导优化搜索的方向。该算法的基本思想是: 从一个随机产生的初始种群开始，通过把种群中任意两个个体的向量差与第三个个体求和来产生新个体，然后将新个体与当代种群中相应的个体相比较，如果新个体的适应度优于当前个体的适应度，则在下一代中就用新个体取代旧个体，否则仍保存旧个体。通过不断地进化，保留优良个体，淘汰劣质个体，引导搜索向最优解逼近。&lt;code&gt;DE&lt;/code&gt;算法通过采用浮点矢量进行编码生成种群个体。在&lt;code&gt;DE&lt;/code&gt;算法寻优的过程中，首先，从父代个体间选择两个个体进行向量做差生成差分矢量；其次，选择另外一个个体与差分矢量求和生成实验个体；然后，对父代个体与相应的实验个体进行交叉操作，生成新的子代个体；最后在父代个体和子代个体之间进行选择操作，将符合要求的个体保存到下一代群体中去。&lt;/p&gt;
&lt;h3&gt;算法流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;: 随机生成种群。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变异&lt;/strong&gt;: 通过差分向量生成变异向量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交叉&lt;/strong&gt;: 生成试验向量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择&lt;/strong&gt;: 选择适应度更好的个体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代&lt;/strong&gt;: 重复上述过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Python实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

POP_SIZE = 30
DIMENSION = 2
F = 0.5
CR = 0.7
MAX_ITER = 100

def objective_function(x):
    return np.sum(x**2)

def differential_evolution():
    population = np.random.rand(POP_SIZE, DIMENSION)
    best_solution = population[0]
    best_value = objective_function(best_solution)

    for _ in range(MAX_ITER):
        for i in range(POP_SIZE):
            indices = list(range(POP_SIZE))
            indices.remove(i)
            a, b, c = population[np.random.choice(indices, 3, replace=False)]
            mutant = np.clip(a + F *(b - c), 0, 1)
            cross_points = np.random.rand(DIMENSION) &amp;lt; CR
            if not np.any(cross_points):
                cross_points[np.random.randint(0, DIMENSION)] = True
            trial = np.where(cross_points, mutant, population[i])
            trial_value = objective_function(trial)

            if trial_value &amp;lt; objective_function(population[i]):
                population[i] = trial
                if trial_value &amp;lt; best_value:
                    best_value = trial_value
                    best_solution = trial

        print(f&quot;Best value: {best_value}&quot;)

    return best_solution

best_solution = differential_evolution()
print(f&quot;Best solution: {best_solution}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码解析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;: 生成随机种群。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变异与交叉&lt;/strong&gt;: 生成试验向量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择&lt;/strong&gt;: 更新种群。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代更新&lt;/strong&gt;: 寻找最优解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 生物地理优化算法(BBO)&lt;/h2&gt;
&lt;p&gt;BBO算法将优化问题的每个解看成一个栖息地。解的适应度越高，表示栖息地拥有的物种越多，其迁出率就越高、迁入率就越低:反之，解的适应度越低，其对应的迁出率越低、迁入率越高。迁移操作的目的就是在不同的解之间进行信息分享，其中好的解倾向于把自身的信息传播给其他解，而差的解更倾向于从其他解中接收信息。&lt;/p&gt;
&lt;h3&gt;算法流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;: 随机生成栖息地。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移&lt;/strong&gt;: 根据适应度进行特征迁移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变异&lt;/strong&gt;: 随机调整特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择&lt;/strong&gt;: 更新栖息地。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代&lt;/strong&gt;: 重复上述过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Python实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

POP_SIZE = 30
DIMENSION = 2
MUTATION_RATE = 0.01
MAX_ITER = 100

def objective_function(x):
    return np.sum(x**2)

def biogeography_based_optimization():
    population = np.random.rand(POP_SIZE, DIMENSION)
    best_solution = population[0]
    best_value = objective_function(best_solution)

    for _ in range(MAX_ITER):
        fitness = np.array([objective_function(ind) for ind in population])
        rank = fitness.argsort()
        population = population[rank]

        for i in range(POP_SIZE):
            if np.random.rand() &amp;lt; MUTATION_RATE:
                population[i] = np.random.rand(DIMENSION)
            else:
                for j in range(DIMENSION):
                    if np.random.rand() &amp;lt;(POP_SIZE - i) / POP_SIZE:
                        donor = population[np.random.randint(0, i)]
                        population[i, j] = donor[j]

        current_best_value = objective_function(population[0])
        if current_best_value &amp;lt; best_value:
            best_value = current_best_value
            best_solution = population[0]

        print(f&quot;Best value: {best_value}&quot;)

    return best_solution

best_solution = biogeography_based_optimization()
print(f&quot;Best solution: {best_solution}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码解析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;: 生成随机栖息地。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移与变异&lt;/strong&gt;: 调整特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择&lt;/strong&gt;: 更新栖息地。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代更新&lt;/strong&gt;: 寻找最优解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Holland, J. H.(1975). Adaptation in Natural and Artificial Systems.&lt;/li&gt;
&lt;li&gt;Kennedy, J., &amp;amp; Eberhart, R.(1995). Particle Swarm Optimization.&lt;/li&gt;
&lt;li&gt;Storn, R., &amp;amp; Price, K.(1997). Differential Evolution.&lt;/li&gt;
&lt;li&gt;Simon, D.(2008). Biogeography-Based Optimization.&lt;/li&gt;
&lt;li&gt;Dorigo, M., &amp;amp; Stützle, T.(2004). Ant Colony Optimization.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Arch Linux 选择显卡启动的问题</title><link>https://blog.kimbleex.top/posts/2023-07-01-archlinux-boot-gpu/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2023-07-01-archlinux-boot-gpu/</guid><pubDate>Mon, 03 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;本机配置：AMD核显 + NVIDIA独显&lt;/strong&gt;
成功安装了optimus-manager管理显卡驱动，它可以很好接触显卡驱动的切换.于是昨晚因为屏幕刷新率问题尝试切换startup mode.&lt;/p&gt;
&lt;p&gt;于是尝试切换启动项显卡驱动模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尝试nvidia独显直连启动：成功启动.使用感受上感觉到性能下降，图形动画效果变差.&lt;/li&gt;
&lt;li&gt;尝试nvidia+amd混合启动：成功启动.使用感受极佳，推荐.&lt;/li&gt;
&lt;li&gt;尝试amd核显启动：启动失败.无法进入kde.黑屏(显卡未通电的那种黑屏).可以进入tty.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./black.avif&quot; alt=&quot;黑屏&quot; /&gt;&lt;/p&gt;
&lt;p&gt;于是尝试在tty中解决：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尝试卸载了optimus管理器，reboot，无效.&lt;/li&gt;
&lt;li&gt;尝试optimus-manager - -switch nvidia，报错，无有效解决方法.&lt;/li&gt;
&lt;li&gt;尝试进入bios，禁用核显启动，无法进入tty，风扇狂转，黑屏.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://images.kimbleex.top/BlogIMG/Arch_GPU/gameover.mp4&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;至此，Arch宣告滚挂…….GG！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;os驱动问题由于机器各有所不同很难解决，考虑到短学期需要使用电脑，所以打算重装.这次会尝试在grub里安装iso，这样就无需插u盘抢救系统.并且尝试使用btrfs文件系统代替传统的ext4文件系统.&lt;/p&gt;
</content:encoded></item><item><title>Hello, Arch Linux !!!</title><link>https://blog.kimbleex.top/posts/2023-06-30-hello-archlinux/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2023-06-30-hello-archlinux/</guid><pubDate>Fri, 30 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;不妨说windows已然成为历史的车轮印.
&lt;img src=&quot;./pic.avif&quot; alt=&quot;惬意的研究Arch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;经过前三次的捣鼓，昨晚毅然卸载windows.&lt;/p&gt;
&lt;p&gt;这样就不会arch装不好就继续用windows了.😭&lt;/p&gt;
&lt;p&gt;晚上好，尊敬的Arch Linux用户！&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://images.kimbleex.top/BlogIMG/Arch_Open/start.mp4&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>一些很方便但是容易忘记的SQL语句</title><link>https://blog.kimbleex.top/posts/2023-02-19-good-sql/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2023-02-19-good-sql/</guid><pubDate>Sun, 19 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;有一些方便数据库操作的&lt;code&gt;SQL&lt;/code&gt;语句，它可以简化一些代码。总是或多或少忘记，记录一下。&lt;/p&gt;
&lt;p&gt;我这里拿一个&lt;code&gt;students&lt;/code&gt;表举例，表结构如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;class_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;gender&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;张三&lt;/td&gt;
&lt;td&gt;男&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;李四&lt;/td&gt;
&lt;td&gt;男&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;王五&lt;/td&gt;
&lt;td&gt;女&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;赵六&lt;/td&gt;
&lt;td&gt;女&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;插入或替换&lt;/h2&gt;
&lt;p&gt;如果我们希望插入一条新记录&lt;code&gt;INSERT&lt;/code&gt;，但如果记录已经存在，就先删除原记录，再插入新记录。此时，可以使用&lt;code&gt;REPLACE&lt;/code&gt;语句，这样就不必先查询，再决定是否先删除再插入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REPLACE INTO students (id, class_id, name, gender, score) VALUES (1, 1, &apos;小明&apos;, &apos;F&apos;, 99);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若&lt;code&gt;id=1&lt;/code&gt;的记录不存在，&lt;code&gt;REPLACE&lt;/code&gt;语句将插入新记录，否则，当前&lt;code&gt;id=1&lt;/code&gt;的记录将被删除，然后再插入新记录。&lt;/p&gt;
&lt;h2&gt;插入或更新&lt;/h2&gt;
&lt;p&gt;如果我们希望插入一条新记录&lt;code&gt;INSERT&lt;/code&gt;，但如果记录已经存在，就啥事也不干直接忽略，此时，可以使用&lt;code&gt;INSERT IGNORE INTO ...&lt;/code&gt;语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO students (id, class_id, name, gender, score) VALUES (1, 1, &apos;小明&apos;, &apos;F&apos;, 99) ON DUPLICATE KEY UPpublished name=&apos;小明&apos;, gender=&apos;F&apos;, score=99;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若&lt;code&gt;id=1&lt;/code&gt;的记录不存在，&lt;code&gt;INSERT&lt;/code&gt;语句将插入新记录，否则，当前&lt;code&gt;id=1&lt;/code&gt;的记录将被更新，更新的字段由&lt;code&gt;UPpublished&lt;/code&gt;指定。&lt;/p&gt;
&lt;h2&gt;插入或忽略&lt;/h2&gt;
&lt;p&gt;如果我们希望插入一条新记录&lt;code&gt;INSERT&lt;/code&gt;，但如果记录已经存在，就啥事也不干直接忽略，此时，可以使用&lt;code&gt;INSERT IGNORE INTO ...&lt;/code&gt;语句.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT IGNORE INTO students (id, class_id, name, gender, score) VALUES (1, 1, &apos;小明&apos;, &apos;F&apos;, 99);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若&lt;code&gt;id=1&lt;/code&gt;的记录不存在，&lt;code&gt;INSERT&lt;/code&gt;语句将插入新记录，否则，不执行任何操作。&lt;/p&gt;
&lt;h2&gt;快照&lt;/h2&gt;
&lt;p&gt;如果想要对一个表进行快照，即复制一份当前表的数据到一个新表，可以结合&lt;code&gt;CREATE TABLE&lt;/code&gt;和&lt;code&gt;SELECT&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE students_of_class1 SELECT * FROM students WHERE class_id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写入查询结果集&lt;/h2&gt;
&lt;p&gt;如果查询结果集需要写入到表中，可以结合&lt;code&gt;INSERT&lt;/code&gt;和&lt;code&gt;SELECT&lt;/code&gt;，将&lt;code&gt;SELECT&lt;/code&gt;语句的结果集直接插入到指定表中。&lt;/p&gt;
&lt;p&gt;例如，创建一个统计成绩的表&lt;code&gt;statistics&lt;/code&gt;，记录各班的平均成绩。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE statistics (
    id BIGINT NOT NULL AUTO_INCREMENT,
    class_id BIGINT NOT NULL,
    average DOUBLE NOT NULL,
    PRIMARY KEY (id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，我们就可以用一条语句写入各班的平均成绩。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO statistics (class_id, average) SELECT class_id, AVG(score) FROM students GROUP BY class_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确保&lt;code&gt;INSERT&lt;/code&gt;语句的列和&lt;code&gt;SELECT&lt;/code&gt;语句的列能一一对应，就可以在&lt;code&gt;statistics&lt;/code&gt;表中直接保存查询的结果。&lt;/p&gt;
</content:encoded></item><item><title>AmazonSpider-亚马逊用户画像</title><link>https://blog.kimbleex.top/posts/2022-08-15-amazon-spider/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2022-08-15-amazon-spider/</guid><pubDate>Mon, 15 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用selenium等工具类，在亚马逊平台上爬去商品信息和用户信息，并进行数据分析和分词统计，最后生成用户画像报告。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;亚马逊平台访问需要代理访问！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;github仓库&lt;/code&gt;: &lt;a href=&quot;https://github.com/kimbleex/AmazonSpider.git&quot;&gt;Github仓库地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;star和fork是一个好习惯！:)&lt;/p&gt;
&lt;p&gt;点赞和关注也是好习惯！:D&lt;/p&gt;
&lt;h2&gt;1. 准备工作&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;写在前面：代码本身很多函数是写在类当中的，如果单个函数无法使用，请滑到文章最后面查看完整代码！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需要的导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re
import os
import time
import math
import pandas as pd
from tqdm import tqdm
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先需要初始化浏览器，并对商品的页面等进行划分。&lt;/p&gt;
&lt;p&gt;写两个函数，用于启动浏览器和进入指定的页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def star_browser(self):
    # 获取计算机用户名
    username = os.getlogin()
    self.browser_path = f&apos;C:/Users/{username}/AppData/Local/Google/Chrome/User Data&apos;
    options = webdriver.ChromeOptions()
    options.add_argument(&apos;user-data-dir={}&apos;.format(self.browser_path)) 
    options.add_argument(&apos;lang=en-US&apos;) # 设置英文默认
    self.browser = webdriver.Chrome(options=options)
    self.browser.maximize_window()  # 窗口最大化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;code&gt;browser_path&lt;/code&gt;属性需要根据自己电脑中chrome浏览器安装路径准备&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def enter_page(self, url, asin=None):
    urls = {
        &apos;主页&apos;: &apos;https://www.amazon.com/&apos;,
        &apos;商品页&apos;: &apos;https://www.amazon.com/dp/{}&apos;.format(asin),
        &apos;评论区&apos;: &apos;https://www.amazon.com/product-reviews/{}&apos;.format(asin),
    }
    self.browser.get(urls[url])
    time.sleep(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：以上网址均为实地考察得到，如有更新也需要自行更改！&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2.获取商品&lt;code&gt;Asin&lt;/code&gt; (亚马逊商品唯一识别ID)&lt;/h2&gt;
&lt;p&gt;根据网页的页面元素排列定位元素，首先需要定位首页的搜索框。&lt;/p&gt;
&lt;p&gt;防止页面元素没有刷新出来，使用&lt;code&gt;WebDriverWait&lt;/code&gt;来进行等待。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wait = WebDriverWait(self.browser, 500)
# 根据搜索词 抓取Asin
wait.until(EC.presence_of_element_located((By.XPATH, &apos;//*[@id=&quot;twotabsearchtextbox&quot;]&apos;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到搜索框，模拟用户输入需要查找的商品名称，并模拟按下&lt;code&gt;Enter&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;searchbox = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;twotabsearchtextbox&quot;]&apos;)
searchbox.clear() 
searchbox.send_keys(theme)
searchbox.send_keys(Keys.ENTER)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来进入商品页面后就需要爬取商品的&lt;code&gt;Asin&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wait.until(EC.presence_of_element_located((By.XPATH, &apos;//*[@id=&quot;departments&quot;]&apos;)))
time.sleep(2)
for _ in range(0, page):
    asins = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;search&quot;]/div[1]/div[1]/div/span[1]/div[1]/div&apos;)
    results = []
    for a in asins:
        try:
            asin = a.get_attribute(&apos;data-asin&apos;)
            results.append(asin)
        except:
            pass
    # 移除空白 
    while &quot;&quot; in results:
        results.remove(&quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;页面元素的定位如果失效需要去页面上自行复制元素的&lt;code&gt;XPATH&lt;/code&gt;，当然定位的方法也有很多种。&lt;/p&gt;
&lt;p&gt;爬取完成后保存到&lt;code&gt;csv&lt;/code&gt;文件中，方便后续操作的使用。也可以用作断点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df = pd.Series(results)
df.dropna(inplace=True)
filename = &apos;./read/&apos; + theme + &apos;-asin.csv&apos;
df.to_csv(filename, mode=&apos;a&apos;, index=False, header=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结束后进行翻页，防止页面不在翻页按钮那，所以需要执行&lt;code&gt;js&lt;/code&gt;将页面下去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    nextbutton = self.browser.find_element(By.XPATH,&apos;.//a[text()=&quot;Next&quot;]&apos;)
    self.browser.execute_script(&quot;arguments[0].scrollIntoView();&quot;, nextbutton)
    nextbutton.click()
except:
    pass
time.sleep(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;爬取&lt;code&gt;Asin&lt;/code&gt;完整函数如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_asin(self, theme, page):
    self.wait = WebDriverWait(self.browser, 500)
    # 根据搜索词 抓取Asin
    self.wait.until(EC.presence_of_element_located((By.XPATH, &apos;//*[@id=&quot;twotabsearchtextbox&quot;]&apos;)))

    for _ in range(len(theme)):
        searchbox = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;twotabsearchtextbox&quot;]&apos;)
        searchbox.clear() 
        searchbox.send_keys(theme)
        searchbox.send_keys(Keys.ENTER)
        # 等待加载
        self.wait.until(EC.presence_of_element_located((By.XPATH, &apos;//*[@id=&quot;departments&quot;]&apos;)))
        time.sleep(2)
        for _ in range(0, page):
            asins = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;search&quot;]/div[1]/div[1]/div/span[1]/div[1]/div&apos;)
            results = []
            for a in asins:
                try:
                    asin = a.get_attribute(&apos;data-asin&apos;)
                    results.append(asin)
                except:
                    pass
            # 移除空白 
            while &quot;&quot; in results:
                results.remove(&quot;&quot;)
            df = pd.Series(results)
            df.dropna(inplace=True)
            filename = &apos;./read/&apos; + theme + &apos;-asin.csv&apos;
            df.to_csv(filename, mode=&apos;a&apos;, index=False, header=False)
            try:
                nextbutton = self.browser.find_element(By.XPATH,&apos;.//a[text()=&quot;Next&quot;]&apos;)
                self.browser.execute_script(&quot;arguments[0].scrollIntoView();&quot;, nextbutton)
                nextbutton.click()
            except:
                pass
            time.sleep(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.获取标题+五点描述+分词&lt;/h2&gt;
&lt;p&gt;原理与上述过程一致，定位&lt;code&gt;title&lt;/code&gt;元素位置并抓取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def title(self, theme, savepath, asin):
    title = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;productTitle&quot;]&apos;).text           
    js_script= &apos;return document.querySelector(&quot;#corePriceDisplay_desktop_feature_div &amp;gt; div.a-section.a-spacing-none.aok-align-center.aok-relative &amp;gt; span.aok-offscreen&quot;).textContent;&apos;  
    try:
        price = self.browser.execute_script(js_script)
        if &apos;with&apos; in price:
            price = price.split(&apos;with&apos;)[0]
    except:
        price = None
    points = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;feature-bullets&quot;]/ul/li/span&apos;)
    try:
        imgurl = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;landingImage&quot;]&apos;).get_attribute(&quot;src&quot;)
    except:
        imgurl = None
    fivepoint = &apos;&apos;
    for point in points:
        fivepoint = fivepoint + point.text + &apos;\n&apos; 
    filename = savepath + theme + &apos;-title.csv&apos;
    data = [asin, title, price, imgurl, fivepoint]
    df = pd.DataFrame([data])
    df.to_csv(filename, mode=&apos;a&apos;, index=False, header=False, encoding=&apos;utf-8-sig&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4.抓取评论+分词&lt;/h2&gt;
&lt;p&gt;评论抓取了用户名&lt;code&gt;username&lt;/code&gt;，用户评分&lt;code&gt;star&lt;/code&gt;，购买时间&lt;code&gt;buytime&lt;/code&gt;，颜色&lt;code&gt;color&lt;/code&gt;，商品标题&lt;code&gt;title&lt;/code&gt;，评论内容&lt;code&gt;content&lt;/code&gt;，爬取完成后再进行分词。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def product_reviews(self, theme, savepath, asin):
    # 获取评论总数 计算翻页次数
    page_div = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;filter-info-section&quot;]/div&apos;).text
    try:
        page = int(re.findall(&apos;, (.*?)with review&apos;, page_div)[0].replace(&apos;,&apos;, &apos;&apos;))
    except:
        page = int(re.findall(&apos;, (.*?)with reviews&apos;, page_div)[0].replace(&apos;,&apos;, &apos;&apos;))
    page = 10 if int(math.ceil(page/10)) &amp;gt;= 10 else int(math.ceil(page/10))
    print(&quot;一共{}页&quot;.format(page))
    for p in tqdm(range(page), desc=&quot;当前asin:{}进度&quot;.format(asin),ncols=80):
        divs = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;cm_cr-review_list&quot;]/div&apos;)
        for i in range(1,len(divs)-1):
            try:
                self.browser.execute_script(&quot;arguments[0].scrollIntoView();&quot;, divs[i])
                time.sleep(1)
                username = divs[i].find_element(By.XPATH,&apos;div/div/div[1]&apos;).text
                star = divs[i].find_element(By.XPATH, &apos;div/div/div[2]/a/i/span&apos;).get_attribute(&quot;textContent&quot;).split(&apos;，&apos;)[0][0]
                title = divs[i].find_element(By.XPATH, &apos;div/div/div[2]/a/span[2]&apos;).text
                buytime = divs[i].find_element(By.XPATH, &apos;div/div/span&apos;).text.split(&quot; &quot;)[0]
                try:
                    color = divs[i].find_element(By.XPATH, &apos;div/div/div[3]/a[1]&apos;).text.split(&quot;:&quot;)[1]
                except:
                    color = &quot; &quot;
                content = divs[i].find_element(By.XPATH, &apos;div/div/div[4]&apos;).text
                data = [username, star, buytime, color , title, content]
                dataframe = pd.DataFrame([data])
                filename = &apos;{}{}-reviews.csv&apos;.format(savepath, theme)
                dataframe.to_csv(filename, mode=&apos;a&apos;, index=False, header=False, encoding=&apos;utf-8-sig&apos;)
            except:
                pass
        if p+1 &amp;lt; page:
            # 翻页
            self.browser.find_element(By.XPATH, &apos;.//a[text()=&quot;Next Page&quot;]&apos;).click()
            time.sleep(2)
        else:
            time.sleep(1)   
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 获取问题和回答+分词&lt;/h2&gt;
&lt;p&gt;进入到商品用户问答界面爬取用户的提问和回答。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def qa(self, theme, savepath, asin):
    page = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;askPaginationBar&quot;]/ul/li&apos;)
    page = page[-2].text
    page = int(page)
    filename = savepath + theme + &apos;-QA.csv&apos;
    for p in range(page):
        qas = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;a-page&quot;]/div[1]/div[6]/div/div/div/div/div[2]&apos;)
        for qa in qas:
            q = qa.find_element(By.XPATH, &apos;div/div/div[2]/a/span&apos;).text
            try:
                a = qa.find_element(By.XPATH, &apos;div[2]/div/div[2]/span&apos;).text
            except:
                a = None
            data = [asin, q, a]
            df = pd.DataFrame([data])
            df.to_csv(filename, mode=&apos;a&apos;, index=False, header=False, encoding=&apos;utf-8-sig&apos;)
        time.sleep(2)
        self.browser.find_element(By.XPATH, &apos;.//a[text()=&quot;Next&quot;]&apos;).click()
        time.sleep(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;===============================================================&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2024.08.15更新：亚马逊平台取消了QA界面，这个功能暂时无法在使用了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;===============================================================&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;6.工具类代码完整版&lt;/h2&gt;
&lt;h2&gt;6.1 AmazonSpider类完整版&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import re
import os
import time
import math
import pandas as pd
from tqdm import tqdm
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class AmazonSpider(object):
    def star_browser(self):
        # 获取计算机用户名
        username = os.getlogin()
        self.browser_path = &apos;C:/Users/{}/AppData/Local/Google/Chrome/User Data&apos;.format(username)
        options = webdriver.ChromeOptions()
        options.add_argument(&apos;user-data-dir={}&apos;.format(self.browser_path))
        options.add_argument(&apos;lang=en-US&apos;)
        self.browser = webdriver.Chrome(options=options)
        # 窗口最大化
        self.browser.maximize_window()
    
    def enter_page(self, url, asin=None):
        urls = {
            &apos;主页&apos;: &apos;https://www.amazon.com/&apos;,
            &apos;商品页&apos;: &apos;https://www.amazon.com/dp/{}&apos;.format(asin),
            &apos;评论区&apos;: &apos;https://www.amazon.com/product-reviews/{}&apos;.format(asin),
            &apos;QA&apos;:&apos;https://www.amazon.com/ask/questions/asin/{}&apos;.format(asin)
        }
        self.browser.get(urls[url])
        time.sleep(5)
        
    def get_asin(self, theme, page):
        self.wait = WebDriverWait(self.browser, 500)
        # 根据搜索词 抓取Asin
        self.wait.until(EC.presence_of_element_located((By.XPATH, &apos;//*[@id=&quot;twotabsearchtextbox&quot;]&apos;)))
        for _ in range(len(theme)):
            searchbox = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;twotabsearchtextbox&quot;]&apos;)
            searchbox.clear() 
            searchbox.send_keys(theme)
            searchbox.send_keys(Keys.ENTER)
            # 等待加载
            self.wait.until(EC.presence_of_element_located((By.XPATH, &apos;//*[@id=&quot;departments&quot;]&apos;)))
            time.sleep(2)
            for _ in range(0, page):
                asins = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;search&quot;]/div[1]/div[1]/div/span[1]/div[1]/div&apos;)
                results = []
                for a in asins:
                    try:
                        asin = a.get_attribute(&apos;data-asin&apos;)
                        results.append(asin)
                    except:
                        pass
                # 移除空白 
                while &quot;&quot; in results:
                    results.remove(&quot;&quot;)
                df = pd.Series(results)
                df.dropna(inplace=True)
                filename = &apos;./read/&apos; + theme + &apos;-asin.csv&apos;
                df.to_csv(filename, mode=&apos;a&apos;, index=False, header=False)
                try:
                    nextbutton = self.browser.find_element(By.XPATH,&apos;.//a[text()=&quot;Next&quot;]&apos;)
                    self.browser.execute_script(&quot;arguments[0].scrollIntoView();&quot;, nextbutton)
                    nextbutton.click()
                except:
                    pass
                time.sleep(5)

    def qa(self, theme, savepath, asin):
        page = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;askPaginationBar&quot;]/ul/li&apos;)
        page = page[-2].text
        page = int(page)
        filename = savepath + theme + &apos;-QA.csv&apos;
        for p in range(page):
            qas = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;a-page&quot;]/div[1]/div[6]/div/div/div/div/div[2]&apos;)
            for qa in qas:
                q = qa.find_element(By.XPATH, &apos;div/div/div[2]/a/span&apos;).text
                try:
                    a = qa.find_element(By.XPATH, &apos;div[2]/div/div[2]/span&apos;).text
                except:
                    a = None
                data = [asin, q, a]
                df = pd.DataFrame([data])

                df.to_csv(filename, mode=&apos;a&apos;, index=False, header=False, encoding=&apos;utf-8-sig&apos;)
            time.sleep(2)
            self.browser.find_element(By.XPATH, &apos;.//a[text()=&quot;Next&quot;]&apos;).click()
            time.sleep(3)

    def title(self, theme, savepath, asin):
        title = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;productTitle&quot;]&apos;).text       
        js_script= &apos;return document.querySelector(&quot;#corePriceDisplay_desktop_feature_div &amp;gt; div.a-section.a-spacing-none.aok-align-center.aok-relative &amp;gt; span.aok-offscreen&quot;).textContent;&apos;  
        try:
            price = self.browser.execute_script(js_script)
            if &apos;with&apos; in price:
                price = price.split(&apos;with&apos;)[0]
        except:
            price = None
        points = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;feature-bullets&quot;]/ul/li/span&apos;)
        try:
            imgurl = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;landingImage&quot;]&apos;).get_attribute(&quot;src&quot;)
        except:
            imgurl = None
        fivepoint = &apos;&apos;
        for point in points:
            fivepoint = fivepoint + point.text + &apos;\n&apos; 
        filename = savepath + theme + &apos;-title.csv&apos;
        data = [asin, title, price, imgurl, fivepoint]
        df = pd.DataFrame([data])
        df.to_csv(filename, mode=&apos;a&apos;, index=False, header=False, encoding=&apos;utf-8-sig&apos;)
        
    def product_reviews(self, theme, savepath, asin):
        # 获取评论总数 计算翻页次数
        page_div = self.browser.find_element(By.XPATH, &apos;//*[@id=&quot;filter-info-section&quot;]/div&apos;).text
        try:
            page = int(re.findall(&apos;, (.*?)带评论&apos;, page_div)[0].replace(&apos;,&apos;, &apos;&apos;))
        except:
            page = int(re.findall(&apos;, (.*?)带评论&apos;, page_div)[0].replace(&apos;,&apos;, &apos;&apos;))
        page = 10 if int(math.ceil(page/10)) &amp;gt;= 10 else int(math.ceil(page/10))
        print(&quot;一共{}页&quot;.format(page))
        for p in tqdm(range(page), desc=&quot;当前asin:{}进度&quot;.format(asin),ncols=80):
            divs = self.browser.find_elements(By.XPATH, &apos;//*[@id=&quot;cm_cr-review_list&quot;]/div&apos;)
            for i in range(1,len(divs)-1):
                try:
                    self.browser.execute_script(&quot;arguments[0].scrollIntoView();&quot;, divs[i])
                    time.sleep(1)
                    username = divs[i].find_element(By.XPATH,&apos;div/div/div[1]&apos;).text
                    star = divs[i].find_element(By.XPATH, &apos;div/div/div[2]/a/i/span&apos;).get_attribute(&quot;textContent&quot;).split(&apos;，&apos;)[0][0]
                    title = divs[i].find_element(By.XPATH, &apos;div/div/div[2]/a/span[2]&apos;).text
                    buytime = divs[i].find_element(By.XPATH, &apos;div/div/span&apos;).text.split(&quot; &quot;)[0]
                    try:
                        color = divs[i].find_element(By.XPATH, &apos;div/div/div[3]/a[1]&apos;).text.split(&quot;:&quot;)[1]
                    except:
                        color = &quot; &quot;
                    content = divs[i].find_element(By.XPATH, &apos;div/div/div[4]&apos;).text
                    data = [username, star, buytime, color , title, content]
                    dataframe = pd.DataFrame([data])
                    filename = &apos;{}{}-reviews.csv&apos;.format(savepath, theme)
                    dataframe.to_csv(filename, mode=&apos;a&apos;, index=False, header=False, encoding=&apos;utf-8-sig&apos;)
                except:
                    pass
            if p+1 &amp;lt; page:
                # 翻页
                self.browser.find_element(By.XPATH, &apos;.//a[text()=&quot;下一页&quot;]&apos;).click()
                time.sleep(2)
            else:
                time.sleep(1)   

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 CutWord代码完整版&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import re
import os
import pandas as pd
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

def cut_word(theme, fun, columns, column):

    filename = &apos;./data/{}/{}-{}.csv&apos;.format(theme, theme, fun)
    data = pd.read_csv(filename,names=columns)
    data.drop_duplicates(keep=&apos;first&apos;, inplace=True)
    data[column] = data[column].astype(str)
    titles = data[column].to_list()

    stopWords = stopwords.words(&apos;english&apos;)

    allWords = []
    for title in titles:
        title = re.sub(&apos;\W+&apos;, &apos; &apos;, title).replace(&quot;_&quot;, &apos; &apos;)
        words = word_tokenize(title, &apos;english&apos;)
        for word in words:
            if word not in stopWords:
                if len(word)&amp;gt;1:
                    allWords.append(word)
    
    df = pd.Series(allWords)
    df = df.value_counts()
    df = df.to_frame().reset_index()
    
    savepath = &apos;./result/{}/&apos;.format(theme)
    filename = &apos;./result/{}/{}分词结果.csv&apos;.format(theme, column)
    if not os.path.exists(savepath):
        os.makedirs(savepath)
    df.to_csv(filename, index=False, header=[&apos;word&apos;, &apos;times&apos;], encoding=&apos;utf-8-sig&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类调用&lt;/h2&gt;
&lt;h3&gt;1 获取&lt;code&gt;Asin&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import os
import pandas as pd
from AmzonSpider import AmazonSpider

# 主题
theme = &apos;Your Theme&apos;
# 一共抓取多少页
page = 20

A = AmazonSpider()
A.star_browser()
A.enter_page(&apos;主页&apos;)
A.get_asin(theme, page)

filename= &apos;./read/{}-asin.csv&apos;.format(theme) 
print(filename)
df = pd.read_csv(filename, names=[&apos;asin&apos;, &apos;check&apos;])
df.drop_duplicates(keep=&apos;first&apos;, inplace=True)
df.to_csv(filename, index=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主题即搜索词条，根据自己偏好填写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需要输入的参数:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;theme&lt;/code&gt;&lt;em&gt;关键字，即输入的词&lt;/em&gt;&lt;br /&gt;
&lt;code&gt;page&lt;/code&gt;需要抓取的页数，根据亚马逊官网的情况，最大为20&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出的结果储存在: &lt;code&gt;./read/主题-asin.csv&lt;/code&gt; 用于以下程序使用&lt;/p&gt;
&lt;h3&gt;2获取评论+五点描述+分词&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import os
import pandas as pd
from AmzonSpider import AmazonSpider
from CutWord import cut_word
from tqdm import tqdm

# 主题
theme = &apos;Your Theme&apos;

# 源数据储存路径
savepath1 = &apos;./data/{}/&apos;.format(theme)
if not os.path.exists(savepath1):
    os.makedirs(savepath1)

# 结果储存路径
savepath2 = &apos;./result/{}/&apos;.format(theme)
if not os.path.exists(savepath2):
    os.makedirs(savepath2)

a = AmazonSpider()
a.star_browser()

filename = &apos;./read/{}-asin.csv&apos;.format(theme)
df_asin = pd.read_csv(filename)
df_asin[&apos;check&apos;] = 0
df_asin2 = df_asin[df_asin[&apos;check&apos;]!=1]
asins = df_asin2[&apos;asin&apos;].tolist()

for asin in tqdm(asins, ncols=80):
    a.enter_page(&apos;商品页&apos;, asin)
    a.title(theme, savepath1, asin)
    df_asin.loc[df_asin[&apos;asin&apos;]==asin, &apos;check&apos;] = 1
    
    df_asin.to_csv(filename, index=False)

a.browser.close()

cut_word(theme, &apos;title&apos;,  [&apos;asin&apos;,&apos;title&apos;,&apos;price&apos;,&apos;url&apos;, &apos;fivepoint&apos;], &apos;title&apos;)
cut_word(theme, &apos;title&apos;,  [&apos;asin&apos;,&apos;title&apos;,&apos;price&apos;,&apos;url&apos;, &apos;fivepoint&apos;], &apos;fivepoint&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要输入的参数:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;theme&lt;/code&gt;同上&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出的结果储存在: &lt;code&gt;./result/主题名称/title分词结果&lt;/code&gt;和&lt;code&gt;./data/主题名称/主题-title.csv&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;3 获取评论+分词&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import os
import pandas as pd
from tqdm import tqdm
from AmzonSpider import AmazonSpider
from CutWord import cut_word

# 主题
theme = &apos;Your Theme&apos;

# 数据存储路径
savepath1 = &apos;./data/{}/&apos;.format(theme)
if not os.path.exists(savepath1):
    os.makedirs(savepath1)

# 结果存储路径
savepath2 = &apos;./result/{}/&apos;.format(theme)
if not os.path.exists(savepath2):
    os.makedirs(savepath2)


# 抓取评论
a = AmazonSpider()
a.star_browser()

filename = &apos;./read/{}-asin.csv&apos;.format(theme)
df_asin = pd.read_csv(filename)

# 重置check 如果报错想要按照之前的asin继续运行程序 请将此条代码注释
df_asin[&apos;check&apos;] = 0

df_asin2 = df_asin[df_asin[&apos;check&apos;]!=1]
asins = df_asin2[&apos;asin&apos;].tolist()

for i in tqdm(range(len(asins)),desc=&quot;评论爬取总进度&quot;, ncols=80):
    a.enter_page(&apos;评论区&apos;, asins[i])
    a.product_reviews(theme, savepath1, asins[i])
    df_asin.loc[df_asin[&apos;asin&apos;]==asins[i], &apos;check&apos;] = 1
    df_asin.to_csv(filename, index=False)

a.browser.close()

# 合并评论
files = os.listdir(savepath1)
allData = pd.DataFrame()
for file in files:
    filepath = savepath1 + file
    df = pd.read_csv(filepath, names=[&apos;username&apos;, &apos;star&apos;, &apos;buytime&apos;, &apos;color&apos;,&apos;title&apos;, &apos;content&apos;])
    df = df.drop_duplicates(keep=&apos;first&apos;)
    allData = pd.concat([allData, df])

allData[&apos;username&apos;] = allData[&apos;username&apos;].astype(str)

filename1 = &apos;./data/{}/{}-{}.csv&apos;.format( theme,theme, &apos;reviews&apos;)
allData.to_csv(filename1, encoding=&apos;utf-8-sig&apos;, index=False, header=False)

# 对用户名进行分词
def cut_name(username):
    firstname = username.split(&apos; &apos;)[0]
    return firstname

reviews_filename = savepath1 + theme + &apos;-reviews.csv&apos;

allData = pd.read_csv(reviews_filename, names=[&apos;username&apos;, &apos;star&apos;, &apos;buytime&apos;, &apos;color&apos;,&apos;title&apos;, &apos;content&apos;])
allData[&apos;username&apos;] = allData[&apos;username&apos;].astype(str)
allData[&apos;firstname&apos;] = allData[&apos;username&apos;].apply(cut_name)
df_firstname = allData[&apos;firstname&apos;].value_counts(ascending=False)
filename2 = savepath2 + &apos;firstname分词结果.csv&apos;
df_firstname.to_csv(filename2, header=None)

# 对评论内容进行分词
cut_word(theme, &apos;reviews&apos;,  [&apos;username&apos;,&apos;star&apos;,&apos;buytime&apos;,&apos;color&apos;,&apos;title&apos;,&apos;content&apos;], &apos;content&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要输入的参数:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;theme&lt;/code&gt; 同上&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出的结果储存在: &lt;code&gt;./result/主题名称/content分词结果&lt;/code&gt;和&lt;code&gt;firstname分词结果&lt;/code&gt;以及&lt;code&gt;./data/主题名称/主题-reviewscsv&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;4 QA分词&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import os
import pandas as pd
from AmzonSpider import AmazonSpider
from CutWord import cut_word
from tqdm import tqdm

# 主题
theme = &apos;Your Theme&apos;

# 储存路径
savepath = &apos;./data/{}/&apos;.format(theme)
if not os.path.exists(savepath):
    os.makedirs(savepath)


a = AmazonSpider()
a.star_browser()

filename = &apos;./read/{}-asin.csv&apos;.format(theme)
df_asin = pd.read_csv(filename)
df_asin[&apos;check&apos;] = 0
df_asin2 = df_asin[df_asin[&apos;check&apos;]!=1]
asins = df_asin2[&apos;asin&apos;].tolist()

for i in tqdm(range(len(asins)),ncols=80):
    a.enter_page(&apos;QA&apos;, asins[i])
    try:
        a.qa(theme, savepath, asins[i])
    except:
        pass
    df_asin.loc[df_asin[&apos;asin&apos;]==asins[i], &apos;check&apos;] = 1
    df_asin.to_csv(filename, index=False)

a.browser.close()


cut_word(theme, &apos;QA&apos;,[&apos;asin&apos;,&apos;Q&apos;,&apos;A&apos;], &apos;Q&apos;)
cut_word(theme, &apos;QA&apos;,[&apos;asin&apos;,&apos;Q&apos;,&apos;A&apos;], &apos;A&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要输入的参数:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;theme&lt;/code&gt; 同上&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出的结果存储在: &lt;code&gt;./result/主题名称/QA分词结果.csv&lt;/code&gt;和&lt;code&gt;./data/主题名称/主题-QA.csv&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;8.获取姓名年龄分布&lt;/h2&gt;
&lt;p&gt;需要准备的文件: &lt;code&gt;./read/主题-name.csv&lt;/code&gt;, 该文件需要从&lt;code&gt;firtname&lt;/code&gt;分词(03获取评论+分词.py运行结果)中选出100个姓名并且标注好性别，可以使用&lt;code&gt;chatGPT&lt;/code&gt;来执行,将csv中前150行复制到GPT中让它返回CSV即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re
import os
import time
import pickle
import pandas as pd
from selenium import webdriver

def name_distribution(name, male):
    
    url = &apos;https://randalolson.com/name-age-calculator/index.html?Gender={}&amp;amp;Name={}&apos;.format(male, name)

    browser.get(url)
    browser.delete_all_cookies()
    with open(&apos;cookies.pkl&apos;, &apos;rb&apos;) as file:
        cookies = pickle.load(file)
        for cookie in cookies:
            browser.add_cookie(cookie)
    browser.refresh()   
    try:
        html = browser.page_source
    except Exception as error:
        if male==&apos;F&apos;:
            male = &apos;M&apos;
        else:
            male=&apos;F&apos;
        url = &apos;https://randalolson.com/name-age-calculator/index.html?Gender={}&amp;amp;Name={}&apos;.format(male, name)
        browser.get(url)
        browser.delete_all_cookies()
        with open(&apos;cookies.pkl&apos;, &apos;rb&apos;) as file:
            cookies = pickle.load(file)
            for cookie in cookies:
                browser.add_cookie(cookie)
        browser.refresh()
        html = browser.page_source
    
    txt = re.findall(&apos;was born around (.*?) old.&apos;, html)[0]
    media = re.findall(&apos;(.*?) and&apos;, txt)[0]
    age = re.findall(&apos;and ranges from (.*?) years&apos;, txt)[0].replace(&apos; to &apos;,&apos;~&apos;)

    return  media, age, male

def get_age(theme):
    # 获取计算机用户名
    # username = os.getlogin()
    options = webdriver.ChromeOptions()
    # 设置用户目录 保持浏览器设置信息
    # userPath = &apos;user-data-dir=C:/Users/{}/AppData/Local/Google/Chrome/User Data&apos;.format(username)
    # options.add_argument(userPath)
    global browser
    browser = webdriver.Chrome(options=options)
    # 窗口最大化
    browser.maximize_window()

    filename = &apos;./read/{}-name.csv&apos;.format(theme)
    savepath = &apos;./result/{}/用户年龄分布.csv&apos;.format(theme)

    df = pd.read_csv(filename)
    df2 = df[df[&apos;check&apos;]!=1]
    names = df2[&apos;name&apos;].tolist()
    males = df2[&apos;male&apos;].tolist()
    
    for i in range(len(names)):
        time.sleep(2)
        name = names[i]
        male = males[i]
        result = name_distribution(name, male)
        media = result[0]
        age = result[1]
        male = result[2]
        data = [name, male, media, age]
        dataframe = pd.DataFrame([data])
        
        dataframe.to_csv(savepath, mode=&apos;a&apos;, header=False, index=False, encoding=&apos;utf-8-sig&apos;)
        df.loc[df[&apos;name&apos;]==name, &apos;check&apos;]=1
        df.to_csv(filename, index=False)


# 该步骤之前需要准备好XXX-name.csv文件,使用chatgpt标注性别
get_age(theme=&apos;Your Theme&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出的结果存储在: &lt;code&gt;./result/主题名称/用户画像分布.csv&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;9.合并成报告&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import os

def merge_files(theme):

    # 选取所需要的列 合并三份文件到一个excel中,
    filepath1 = &apos;./data/{}/&apos;.format(theme)
    filename1 = filepath1 + &apos;{}-QA.csv&apos;.format(theme)
    filename2 = filepath1 + &apos;{}-reviews.csv&apos;.format(theme)
    filename3 = filepath1 + &apos;{}-title.csv&apos;.format(theme)

    filepath2 = &apos;./result/{}/&apos;.format(theme)
    filename4 =  filepath2 + &apos;用户年龄分布.csv&apos;
    filename10 = filepath2 + &apos;firstname分词结果.csv&apos;

    df1 = pd.read_csv(filename1, names=[&apos;asin&apos;, &apos;Q&apos;, &apos;A&apos;])

    df2 = pd.read_csv(filename2, names=[&apos;用户名&apos;, &apos;评分&apos;, &apos;购买日期&apos;, &apos;款式&apos;, &apos;评论标题&apos;, &apos;评论内容&apos;])
    df3 = pd.read_csv(filename3, names=[&apos;asin&apos;, &apos;标题&apos;, &apos;价格&apos;, &apos;图片地址&apos;, &apos;五点描述&apos;])
    df4 = pd.read_csv(filename4, names=[&apos;姓名&apos;,&apos;性别&apos;,&apos;中位数&apos;,&apos;年龄范围&apos;])
    df10 = pd.read_csv(filename10, names=[&apos;word&apos;,&apos;times&apos;])

    filename5 =  filepath2 + &apos;Q分词结果.csv&apos;
    filename6 =  filepath2 + &apos;A分词结果.csv&apos;
    filename7 =  filepath2 + &apos;title分词结果.csv&apos;
    filename8 =  filepath2 + &apos;fivepoint分词结果.csv&apos;
    filename9 =  filepath2 + &apos;content分词结果.csv&apos;
    
    df5 = pd.read_csv(filename5)
    df6 = pd.read_csv(filename6)
    df7 = pd.read_csv(filename7)
    df8 = pd.read_csv(filename8)
    df9 = pd.read_csv(filename9)

    df5 = df5.rename(columns={&apos;word&apos;: &apos;Q_word&apos;, &apos;times&apos;: &apos;Q_count&apos;})
    df6 = df6.rename(columns={&apos;word&apos;: &apos;A_word&apos;, &apos;times&apos;: &apos;A_count&apos;})
    df7 = dfrename(columns={&apos;word&apos;: &apos;Title_word&apos;, &apos;times&apos;: &apos;Title_count&apos;})
    df8 = df8.rename(columns={&apos;word&apos;: &apos;Fivepoint_word&apos;, &apos;times&apos;: &apos;Fivepoint_count&apos;})
    df9 = df9.rename(columns={&apos;word&apos;: &apos;Reviews_word&apos;, &apos;times&apos;: &apos;Reviews_count&apos;})
   
    cutword_df = pd.concat([df5, df6, df7, df8, df9],axis=1)  


    savename = &apos;./&apos; + theme + &apos;-用户画像报告.xlsx&apos;
    
    with pd.ExcelWriter(savename) as writer:
        
        cutword_df.to_excel(writer, sheet_name=&apos;分词&apos;, index=False)

        df1.to_excel(writer, sheet_name=&apos;QA原始数据&apos;, index=False)
        df2.to_excel(writer, sheet_name=&apos;评论原始数据&apos;, index=False)
        df3.to_excel(writer, sheet_name=&apos;标题原始数据&apos;, index=False)
        df4.to_excel(writer, sheet_name=&apos;用户年龄分布&apos;, index=False)
        df10.to_excel(writer, sheet_name=&apos;姓名分词统计&apos;, index=False)

theme = &apos;Your Theme&apos;
merge_files(theme)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出的结果存储在: &lt;code&gt;./主题名称-用户画像报告.csv&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>使用Python Turtle绘制冰墩墩</title><link>https://blog.kimbleex.top/posts/2022-02-06-ice-fat-fat/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2022-02-06-ice-fat-fat/</guid><description>本文介绍了使用Python内置的Turtle库绘制2022年冬奥会的吉祥物冰墩墩。</description><pubDate>Sun, 06 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;冰墩墩是2022年北京冬奥会的吉祥物，确实好玩！&lt;/p&gt;
&lt;p&gt;废话不多说，直接开画！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
import turtle

screen = turtle.Screen()
turtle.title(&apos;Python（冰墩墩）&apos;)  # 可以自己修改名称
turtle.speed(10)  # 可以自己调节速度
# 左手
turtle.penup()

turtle.goto(177, 112)

turtle.pencolor(&quot;lightgray&quot;)

turtle.pensize(3)

turtle.fillcolor(&quot;white&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(80)

turtle.circle(-45, 200)

turtle.circle(-300, 23)

turtle.end_fill()

# 左手内
turtle.penup()

turtle.goto(182, 95)

turtle.pencolor(&quot;black&quot;)

turtle.pensize(1)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.setheading(95)

turtle.pendown()

turtle.circle(-37, 160)

turtle.circle(-20, 50)

turtle.circle(-200, 30)

turtle.end_fill()

# 轮廓
# 头顶
turtle.penup()

turtle.goto(-73, 230)

turtle.pencolor(&quot;lightgray&quot;)

turtle.pensize(3)

turtle.fillcolor(&quot;white&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(20)

turtle.circle(-250, 35)

# 左耳
turtle.setheading(50)

turtle.circle(-42, 180)

# 左侧
turtle.setheading(-50)

turtle.circle(-190, 30)

turtle.circle(-320, 45)

# 左腿
turtle.circle(120, 30)

turtle.circle(200, 12)

turtle.circle(-18, 85)

turtle.circle(-180, 23)

turtle.circle(-20, 110)

turtle.circle(15, 115)

turtle.circle(100, 12)

# 右腿
turtle.circle(15, 120)

turtle.circle(-15, 110)

turtle.circle(-150, 30)

turtle.circle(-15, 70)

turtle.circle(-150, 10)

turtle.circle(200, 35)

turtle.circle(-150, 20)

# 右手
turtle.setheading(-120)

turtle.circle(50, 30)

turtle.circle(-35, 200)

turtle.circle(-300, 23)

# 右侧
turtle.setheading(86)

turtle.circle(-300, 26)

# 右耳
turtle.setheading(122)

turtle.circle(-53, 160)

turtle.end_fill()

# 右耳内
turtle.penup()

turtle.goto(-130, 180)

turtle.pencolor(&quot;black&quot;)

turtle.pensize(1)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(120)

turtle.circle(-28, 160)

turtle.setheading(210)

turtle.circle(150, 20)

turtle.end_fill()

# 左耳内
turtle.penup()

turtle.goto(90, 230)

turtle.setheading(40)

turtle.begin_fill()

turtle.pendown()

turtle.circle(-30, 170)

turtle.setheading(125)

turtle.circle(150, 23)

turtle.end_fill()

# 右手内
turtle.penup()

turtle.goto(-180, -55)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.setheading(-120)

turtle.pendown()

turtle.circle(50, 30)

turtle.circle(-27, 200)

turtle.circle(-300, 20)

turtle.setheading(-90)

turtle.circle(300, 14)

turtle.end_fill()

# 左腿内
turtle.penup()

turtle.goto(108, -168)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(-115)

turtle.circle(110, 15)

turtle.circle(200, 10)

turtle.circle(-18, 80)

turtle.circle(-180, 13)

turtle.circle(-20, 90)

turtle.circle(15, 60)

turtle.setheading(42)

turtle.circle(-200, 29)

turtle.end_fill()

# 右腿内
turtle.penup()

turtle.goto(-38, -210)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(-155)

turtle.circle(15, 100)

turtle.circle(-10, 110)

turtle.circle(-100, 30)

turtle.circle(-15, 65)

turtle.circle(-100, 10)

turtle.circle(200, 15)

turtle.setheading(-14)

turtle.circle(-200, 27)

turtle.end_fill()

# 右眼
# 眼圈
turtle.penup()

turtle.goto(-64, 120)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(40)

turtle.circle(-35, 152)

turtle.circle(-100, 50)

turtle.circle(-35, 130)

turtle.circle(-100, 50)

turtle.end_fill()

# 眼珠
turtle.penup()

turtle.goto(-47, 55)

turtle.fillcolor(&quot;white&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(25, 360)

turtle.end_fill()

turtle.penup()

turtle.goto(-45, 62)

turtle.pencolor(&quot;darkslategray&quot;)

turtle.fillcolor(&quot;darkslategray&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(19, 360)

turtle.end_fill()

turtle.penup()

turtle.goto(-45, 68)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(10, 360)

turtle.end_fill()

turtle.penup()

turtle.goto(-47, 86)

turtle.pencolor(&quot;white&quot;)

turtle.fillcolor(&quot;white&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(5, 360)

turtle.end_fill()

# 左眼
# 眼圈
turtle.penup()

turtle.goto(51, 82)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(120)

turtle.circle(-32, 152)

turtle.circle(-100, 55)

turtle.circle(-25, 120)

turtle.circle(-120, 45)

turtle.end_fill()

# 眼珠
turtle.penup()

turtle.goto(79, 60)

turtle.fillcolor(&quot;white&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(24, 360)

turtle.end_fill()

turtle.penup()

turtle.goto(79, 64)

turtle.pencolor(&quot;darkslategray&quot;)

turtle.fillcolor(&quot;darkslategray&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(19, 360)

turtle.end_fill()

turtle.penup()

turtle.goto(79, 70)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(10, 360)

turtle.end_fill()

turtle.penup()

turtle.goto(79, 88)

turtle.pencolor(&quot;white&quot;)

turtle.fillcolor(&quot;white&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(0)

turtle.circle(5, 360)

turtle.end_fill()

# 鼻子
turtle.penup()

turtle.goto(37, 80)

turtle.fillcolor(&quot;black&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.circle(-8, 130)

turtle.circle(-22, 100)

turtle.circle(-8, 130)

turtle.end_fill()

# 嘴
turtle.penup()

turtle.goto(-15, 48)

turtle.setheading(-36)

turtle.begin_fill()

turtle.pendown()

turtle.circle(60, 70)

turtle.setheading(-132)

turtle.circle(-45, 100)

turtle.end_fill()

# 彩虹圈
turtle.penup()

turtle.goto(-135, 120)

turtle.pensize(5)

turtle.pencolor(&quot;cyan&quot;)

turtle.pendown()

turtle.setheading(60)

turtle.circle(-165, 150)

turtle.circle(-130, 78)

turtle.circle(-250, 30)

turtle.circle(-138, 105)

turtle.penup()

turtle.goto(-131, 116)

turtle.pencolor(&quot;slateblue&quot;)

turtle.pendown()

turtle.setheading(60)

turtle.circle(-160, 144)

turtle.circle(-120, 78)

turtle.circle(-242, 30)

turtle.circle(-135, 105)

turtle.penup()

turtle.goto(-127, 112)

turtle.pencolor(&quot;orangered&quot;)

turtle.pendown()

turtle.setheading(60)

turtle.circle(-155, 136)

turtle.circle(-116, 86)

turtle.circle(-220, 30)

turtle.circle(-134, 103)

turtle.penup()

turtle.goto(-123, 108)

turtle.pencolor(&quot;gold&quot;)

turtle.pendown()

turtle.setheading(60)

turtle.circle(-150, 136)

turtle.circle(-104, 86)

turtle.circle(-220, 30)

turtle.circle(-126, 102)

turtle.penup()

turtle.goto(-120, 104)

turtle.pencolor(&quot;greenyellow&quot;)

turtle.pendown()

turtle.setheading(60)

turtle.circle(-145, 136)

turtle.circle(-90, 83)

turtle.circle(-220, 30)

turtle.circle(-120, 100)

turtle.penup()

# 爱心
turtle.penup()

turtle.goto(220, 115)

turtle.pencolor(&quot;brown&quot;)

turtle.pensize(1)

turtle.fillcolor(&quot;brown&quot;)

turtle.begin_fill()

turtle.pendown()

turtle.setheading(36)

turtle.circle(-8, 180)

turtle.circle(-60, 24)

turtle.setheading(110)

turtle.circle(-60, 24)

turtle.circle(-8, 180)

turtle.end_fill()

# 五环
turtle.penup()

turtle.goto(-5, -170)

turtle.pendown()

turtle.pencolor(&quot;blue&quot;)

turtle.circle(6)

turtle.penup()

turtle.goto(10, -170)

turtle.pendown()

turtle.pencolor(&quot;black&quot;)

turtle.circle(6)

turtle.penup()

turtle.goto(25, -170)

turtle.pendown()

turtle.pencolor(&quot;brown&quot;)

turtle.circle(6)

turtle.penup()

turtle.goto(2, -175)

turtle.pendown()

turtle.pencolor(&quot;lightgoldenrod&quot;)

turtle.circle(6)

turtle.penup()

turtle.goto(16, -175)

turtle.pendown()

turtle.pencolor(&quot;green&quot;)

turtle.circle(6)

turtle.penup()

turtle.pencolor(&quot;black&quot;)

turtle.goto(-16, -160)

turtle.write(&quot;BEIJING 2022&quot;, font=(&apos;Arial&apos;, 10, &apos;bold italic&apos;))

turtle.hideturtle()

time.sleep(3)  # 页面停留时间

screen.bye()  # 关闭页面

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行代码后，将会在屏幕上绘制一个冰墩墩的图案。
&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://images.kimbleex.top/BlogIMG/Turtle_Bingdundun/BingDunDun.mp4&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Git 维基百科！</title><link>https://blog.kimbleex.top/posts/2021-10-25-git/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2021-10-25-git/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;时维七月，序属二夏。&lt;/p&gt;
&lt;p&gt;最近要系统的用到github，大学里看它的时候也是走马观花，现如今书到用时方恨少。于是，在一个&lt;a href=&quot;https://github.com/miaobuao&quot;&gt;老前端&lt;/a&gt;两天的帮助下，我对github的了解也是得到了一些提升。现在系统的将github用法做一个汇总，以方便后续的学习。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此篇博客默认已经安装好&lt;code&gt;Git&lt;/code&gt;工具并配置好系统环境&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1. 最先的配置操作&lt;/h3&gt;
&lt;p&gt;安装好&lt;code&gt;git&lt;/code&gt;后，首先需要配置&lt;code&gt;git&lt;/code&gt;的用户名和邮箱，这样在提交代码时，就可以显示出提交者的信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global user.name &quot;Your Name&quot;
git config --global user.email &quot;Your Email&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我在公司使用的电脑是有配置&lt;code&gt;proxy&lt;/code&gt;的，访问&lt;code&gt;Github&lt;/code&gt;会比较缓慢，可以取消代理配置。没有代理可以忽略。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global --unset http.proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一次完整的克隆和上传&lt;/h3&gt;
&lt;h4&gt;1 首先进入到工作目录，在工作目录中初始化git&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;git init
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2 克隆仓库到本地&lt;/h4&gt;
&lt;p&gt;首先你需要去&lt;code&gt;github.com&lt;/code&gt;找到自己需要克隆的仓库链接。以我的仓库为例。github提供&lt;code&gt;HTTPS&lt;/code&gt;和&lt;code&gt;SSH&lt;/code&gt;以及&lt;code&gt;Github CIL&lt;/code&gt;三种连接方式，平时主要采用&lt;code&gt;HTTPS&lt;/code&gt;和&lt;code&gt;SSH&lt;/code&gt;两种方式。推荐使用&lt;code&gt;SSH&lt;/code&gt;方式，使用下来会更加稳定快速。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./get-github-links.avif&quot; alt=&quot;获取github仓库链接&quot; /&gt;&lt;/p&gt;
&lt;p&gt;链接复制完成后，打开&lt;code&gt;Windows PowerShell&lt;/code&gt;或者&lt;code&gt;Git Bash&lt;/code&gt;，进入到本地工作目录中，输入以下指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone git@github.com:kimbleex/kimbleex.git
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3 为本地仓库添加远程仓库&lt;/h4&gt;
&lt;p&gt;需要给本地仓库添加一个远程github目标仓库链接，以将本地仓库内容提交到目标仓库中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 添加一个名为repositoryName的远程仓库
git remote add repositoryName repositoryLink
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4 上传本地代码到github仓库&lt;/h4&gt;
&lt;p&gt;要提交到github仓库，首先要了解它的工作原理。&lt;/p&gt;
&lt;p&gt;git为了保证代码的原子性(即可以很容易的回滚到任何阶段的代码状态)，发明了一个叫做暂存区的概念。我们在提交代码之前可以将所有的修改都保存到暂存区中，这样我们可以通过一些指令精细的挑选出我们需要提交的修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将名为FileName的文件上传到暂存区中
git add FileName 
# 将目录下所有文件上传到暂存区中
git add .

# 将暂存区中的修改提交到github仓库
# -m表示msg，后面的信息为提交的备注信息，可以为空
git commit -m &quot;Your Commit Message&quot;
# 如果有反复修改，可以合并提交
git commit --amend -m &quot;Your Commit Message&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们就完成了一次完整的克隆和上传。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;commit&lt;/code&gt;之后，我们就可以同步更新github仓库了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 默认更新github仓库的master分支
git push repositoryName master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完成后，进入&lt;code&gt;github.com&lt;/code&gt;，刷新一下仓库页面，即可看到仓库的代码更新和&lt;code&gt;commit&lt;/code&gt;时输入的&lt;code&gt;msg&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;3. 仓库的同步&lt;/h3&gt;
&lt;p&gt;我们可能不止在一个本地段操作同一个github仓库。比如我在公司更新我保存个人博客源码的github仓库，我回家后也需要操作，但是我家里本地的仓库就是滞后的，并不是最新的，这样我们就需要更新我家里电脑的本地仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 拉去远端github仓库
git fetch 

# 用远端github仓库覆盖更新本地仓库(3小节中使用remote绑定的名为repositoryName的远程仓库)
git merge repositoryName/master 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以使用以下指令合并上述的两个操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git pull repositoryName master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恭喜你，你已经学会了最基本的&lt;code&gt;git&lt;/code&gt;指令。&lt;/p&gt;
&lt;h3&gt;4. 踩坑实记&lt;/h3&gt;
&lt;p&gt;正当我沾沾自喜的以为自己已经如入无人之境的时候，我的仓库某个目录下的文件push失败了，我又去找了老前端，原因是该目录下我clone了别人的仓库，但是没有将别人仓库下的&lt;code&gt;.git&lt;/code&gt;文件删除，并且在后续删除.git之前我就已经&lt;code&gt;git add&lt;/code&gt;过了，导致最后&lt;code&gt;commit&lt;/code&gt;的时候显示nothing changed，他直接甩给我一行指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# directory为目录名
git submodule deinit directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查阅后发现时说是取消注册，但是实践下来并没有啥作用，后续尝试过使用&lt;code&gt;git rm&lt;/code&gt;指令，&lt;strong&gt;千万不要使用，使用后会将本地仓库的目录和远端github仓库的目录全部删除&lt;/strong&gt;，血淋淋的踩坑。&lt;/p&gt;
&lt;h3&gt;5. 其他指令(并不常用，仅做些许列举)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 展示提交日志
git log
# 显示已存在的分支tag
git tag
# 展示未add到暂存区的修改
git diff
# 比较远端github仓库已有的修改但是本地没有的
git diff repositoryName/master..master
# 展示提交历史对应的修改
git whatchanged
# 搜索文件中的文本内容
git grep &quot;Search Content&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Python手写肘部法则确定Kmeans聚类算法中的K值</title><link>https://blog.kimbleex.top/posts/2021-10-15-kmeans-elbow/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2021-10-15-kmeans-elbow/</guid><pubDate>Fri, 15 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上次手写了&lt;code&gt;Kmeans&lt;/code&gt;算法，但是关于分类数目&lt;code&gt;K&lt;/code&gt;的取值，只是固定了两类，这次就手写一下&lt;code&gt;Kmeans&lt;/code&gt;算法中&lt;code&gt;K&lt;/code&gt;值的确定方法——肘部法则。&lt;/p&gt;
&lt;p&gt;介绍一个词&lt;code&gt;WCSS&lt;/code&gt;: &lt;code&gt;WCSS&lt;/code&gt;是衡量聚类结果紧密程度的指标，表示每个样本点与其所属簇中心的距离平方和，简言之，就是样本类里面每个点到中心点的距离的平方，然后全部相加。&lt;/p&gt;
&lt;h2&gt;肘部法的详细步骤&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;确定K值范围
选择一个合理的&lt;code&gt;K&lt;/code&gt;值范围，一般从1开始，逐步增加，直到达到一个预设的最大&lt;code&gt;K&lt;/code&gt;值(例如，&lt;code&gt;K=10&lt;/code&gt;)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算不同&lt;code&gt;K&lt;/code&gt;值下的&lt;code&gt;WCSS&lt;/code&gt;
对于每一个K值，执行以下步骤：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行&lt;code&gt;K&lt;/code&gt;均值算法：将数据集划分为K个簇。
计算&lt;code&gt;WCSS&lt;/code&gt;：计算簇内误差平方和，即所有样本点到其所属簇中心的距离平方和。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绘制肘部图
在图中，横轴表示簇的数量&lt;code&gt;K&lt;/code&gt;，纵轴表示对应的&lt;code&gt;WCSS&lt;/code&gt;值。绘制&lt;code&gt;K&lt;/code&gt;值与&lt;code&gt;WCSS&lt;/code&gt;的关系曲线。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;识别“肘部”位置
观察曲线中&lt;code&gt;WCSS&lt;/code&gt;下降速度显著减缓的位置，即曲线出现“肘部”的点。该&lt;code&gt;K&lt;/code&gt;值被认为是最佳的聚类数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Python代码实现&lt;/h2&gt;
&lt;p&gt;沿用之前手写的&lt;code&gt;Kmeans&lt;/code&gt;算法，这次我们只需要在&lt;code&gt;Kmeans&lt;/code&gt;算法的基础上，增加计算&lt;code&gt;WCSS&lt;/code&gt;和绘图流程即可即可。并且增加了一些数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
# 一些数据点
data = [[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0], [1,6], [5,6], [9,9], [2,7], [3,3], [6,4], [2,5], [3,5], [4,5],[5,5]]
# K的取值范围
K = range(1, 10)
WCSS = [] # 存放各个K对应的WCSS
# 遍历K值
for i in K:
    index_list = []
    for j in range(i):
        while True: # 随机选择中心点，但不能重复
            index_ = random.randint(0, len(data)-1)
            if index_ in index_list:
                continue
            else:
                index_list.append(index_)
                break
    centerPoint = [data[x] for x in index_list] # 确定各个k值的中心点
    print(&quot;center: &quot;, centerPoint)
    # 确定每个点在K值下的分类
    cate_data = {}
    for z in range(len(data)):
        distance = []
        for y in centerPoint:
            distance.append(((y[0]-data[z][0])**2 + (y[1]-data[z][1])**2) ** 0.5)
        cate_data[f&quot;{data[z]}&quot;] = distance.index(min(distance))     
    print(cate_data)
    # 计算每个K值对应的WCSS
    wcss_list = []
    for p in range(i):
        points = [eval(key) for key in cate_data.keys() if cate_data[key] == p]
        wcss_list.append(sum([((points[n][0]-centerPoint[p][0])**2 + (points[n][1]-centerPoint[p][1])**2) ** 0.5 for n in range(len(points))]))
    WCSS.append(sum(wcss_list))
print(WCSS)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行结果&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;center:  [[6, 4]]
{&apos;[1, 2]&apos;: 0, &apos;[1, 4]&apos;: 0, &apos;[1, 0]&apos;: 0, &apos;[4, 2]&apos;: 0, &apos;[4, 4]&apos;: 0, &apos;[4, 0]&apos;: 0, &apos;[1, 6]&apos;: 0, &apos;[5, 6]&apos;: 0, &apos;[9, 9]&apos;: 0, &apos;[2, 7]&apos;: 0, &apos;[3, 3]&apos;: 0, &apos;[6, 4]&apos;: 0, &apos;[2, 5]&apos;: 0, &apos;[3, 5]&apos;: 0, &apos;[4, 5]&apos;: 0, &apos;[5, 5]&apos;: 0}

center:  [[2, 7], [4, 5]]
{&apos;[1, 2]&apos;: 1, &apos;[1, 4]&apos;: 0, &apos;[1, 0]&apos;: 1, &apos;[4, 2]&apos;: 1, &apos;[4, 4]&apos;: 1, &apos;[4, 0]&apos;: 1, &apos;[1, 6]&apos;: 0, &apos;[5, 6]&apos;: 1, &apos;[9, 9]&apos;: 1, &apos;[2, 7]&apos;: 0, &apos;[3, 3]&apos;: 1, &apos;[6, 4]&apos;: 1, &apos;[2, 5]&apos;: 0, &apos;[3, 5]&apos;: 1, &apos;[4, 5]&apos;: 1, &apos;[5, 5]&apos;: 1}

center:  [[3, 5], [5, 5], [3, 3]]
{&apos;[1, 2]&apos;: 2, &apos;[1, 4]&apos;: 0, &apos;[1, 0]&apos;: 2, &apos;[4, 2]&apos;: 2, &apos;[4, 4]&apos;: 0, &apos;[4, 0]&apos;: 2, &apos;[1, 6]&apos;: 0, &apos;[5, 6]&apos;: 1, &apos;[9, 9]&apos;: 1, &apos;[2, 7]&apos;: 0, &apos;[3, 3]&apos;: 2, &apos;[6, 4]&apos;: 1, &apos;[2, 5]&apos;: 0, &apos;[3, 5]&apos;: 0, &apos;[4, 5]&apos;: 0, &apos;[5, 5]&apos;: 1}

center:  [[4, 2], [1, 0], [1, 4], [3, 5]]
{&apos;[1, 2]&apos;: 1, &apos;[1, 4]&apos;: 2, &apos;[1, 0]&apos;: 1, &apos;[4, 2]&apos;: 0, &apos;[4, 4]&apos;: 3, &apos;[4, 0]&apos;: 0, &apos;[1, 6]&apos;: 2, &apos;[5, 6]&apos;: 3, &apos;[9, 9]&apos;: 3, &apos;[2, 7]&apos;: 3, &apos;[3, 3]&apos;: 0, &apos;[6, 4]&apos;: 0, &apos;[2, 5]&apos;: 3, &apos;[3, 5]&apos;: 3, &apos;[4, 5]&apos;: 3, &apos;[5, 5]&apos;: 3}

center:  [[5, 6], [4, 0], [1, 0], [3, 3], [6, 4]]
{&apos;[1, 2]&apos;: 2, &apos;[1, 4]&apos;: 3, &apos;[1, 0]&apos;: 2, &apos;[4, 2]&apos;: 3, &apos;[4, 4]&apos;: 3, &apos;[4, 0]&apos;: 1, &apos;[1, 6]&apos;: 3, &apos;[5, 6]&apos;: 0, &apos;[9, 9]&apos;: 0, &apos;[2, 7]&apos;: 0, &apos;[3, 3]&apos;: 3, &apos;[6, 4]&apos;: 4, &apos;[2, 5]&apos;: 3, &apos;[3, 5]&apos;: 3, &apos;[4, 5]&apos;: 0, &apos;[5, 5]&apos;: 0}

center:  [[4, 5], [5, 6], [5, 5], [6, 4], [2, 5], [1, 0]]
{&apos;[1, 2]&apos;: 5, &apos;[1, 4]&apos;: 4, &apos;[1, 0]&apos;: 5, &apos;[4, 2]&apos;: 3, &apos;[4, 4]&apos;: 0, &apos;[4, 0]&apos;: 5, &apos;[1, 6]&apos;: 4, &apos;[5, 6]&apos;: 1, &apos;[9, 9]&apos;: 1, &apos;[2, 7]&apos;: 4, &apos;[3, 3]&apos;: 0, &apos;[6, 4]&apos;: 3, &apos;[2, 5]&apos;: 4, &apos;[3, 5]&apos;: 0, &apos;[4, 5]&apos;: 0, &apos;[5, 5]&apos;: 2}

center:  [[1, 4], [3, 3], [4, 4], [6, 4], [2, 7], [1, 2], [3, 5]]
{&apos;[1, 2]&apos;: 5, &apos;[1, 4]&apos;: 0, &apos;[1, 0]&apos;: 5, &apos;[4, 2]&apos;: 1, &apos;[4, 4]&apos;: 2, &apos;[4, 0]&apos;: 1, &apos;[1, 6]&apos;: 4, &apos;[5, 6]&apos;: 2, &apos;[9, 9]&apos;: 3, &apos;[2, 7]&apos;: 4, &apos;[3, 3]&apos;: 1, &apos;[6, 4]&apos;: 3, &apos;[2, 5]&apos;: 6, &apos;[3, 5]&apos;: 6, &apos;[4, 5]&apos;: 2, &apos;[5, 5]&apos;: 2}

center:  [[4, 0], [4, 5], [6, 4], [4, 4], [9, 9], [3, 3], [1, 4], [1, 2]]
{&apos;[1, 2]&apos;: 7, &apos;[1, 4]&apos;: 6, &apos;[1, 0]&apos;: 7, &apos;[4, 2]&apos;: 5, &apos;[4, 4]&apos;: 3, &apos;[4, 0]&apos;: 0, &apos;[1, 6]&apos;: 6, &apos;[5, 6]&apos;: 1, &apos;[9, 9]&apos;: 4, &apos;[2, 7]&apos;: 1, &apos;[3, 3]&apos;: 5, &apos;[6, 4]&apos;: 2, &apos;[2, 5]&apos;: 6, &apos;[3, 5]&apos;: 1, &apos;[4, 5]&apos;: 1, &apos;[5, 5]&apos;: 1}

center:  [[3, 3], [9, 9], [5, 5], [3, 5], [1, 4], [4, 5], [4, 0], [1, 0], [6, 4]]
{&apos;[1, 2]&apos;: 4, &apos;[1, 4]&apos;: 4, &apos;[1, 0]&apos;: 7, &apos;[4, 2]&apos;: 0, &apos;[4, 4]&apos;: 5, &apos;[4, 0]&apos;: 6, &apos;[1, 6]&apos;: 4, &apos;[5, 6]&apos;: 2, &apos;[9, 9]&apos;: 1, &apos;[2, 7]&apos;: 3, &apos;[3, 3]&apos;: 0, &apos;[6, 4]&apos;: 8, &apos;[2, 5]&apos;: 3, &apos;[3, 5]&apos;: 3, &apos;[4, 5]&apos;: 5, &apos;[5, 5]&apos;: 2}

[58.638979289620025, 39.93955755931159, 28.611595782243192, 27.34009275541994, 25.482605577751237, 21.89292222699217, 19.471938219632754, 13.071067811865476, 10.650281539872886]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到每个数据点都有了自己的分类，并且最后的&lt;code&gt;WCSS&lt;/code&gt;也有对应所有&lt;code&gt;K&lt;/code&gt;的值。&lt;/p&gt;
&lt;p&gt;下面我们可以通过绘图来观察&lt;code&gt;WCSS&lt;/code&gt;的变化情况，从而确定最佳的&lt;code&gt;K&lt;/code&gt;值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt


plt.figure(figsize=(8, 5))
plt.plot(K, WCSS, &apos;bo-&apos;, markersize=8)
plt.xlabel(&apos;K&apos;)
plt.ylabel(&apos;WCSS&apos;)
plt.title(&apos;Elbow Method =&amp;gt; K&apos;)
plt.xticks(K)
plt.grid(True)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上述代码，可以得到如下的图像:
&lt;img src=&quot;./WCSS.avif&quot; alt=&quot;Kmeans肘部法则确定K值&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看出&lt;code&gt;K&lt;/code&gt;在4或者5这个点附近有一个明显的拐点，因此我们可以认为最佳的&lt;code&gt;K&lt;/code&gt;值为4或者5。&lt;/p&gt;
&lt;h2&gt;完整代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import random
import matplotlib.pyplot as plt

data = [
    [1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0], [1,6], [5,6], [9,9], [2,7], [3,3], [6,4], [2,5], [3,5], [4,5],[5,5]
]

K = range(1, 10)
WCSS = []

for i in K:
    index_list = []
    for j in range(i):
        while True:
            index_ = random.randint(0, len(data)-1)
            if index_ in index_list:
                continue
            else:
                index_list.append(index_)
                break
    centerPoint = [data[x] for x in index_list]
    print(&quot;center: &quot;, centerPoint)

    cate_data = {}
    for z in range(len(data)):
        distance = []
        for y in centerPoint:
            distance.append(((y[0]-data[z][0])**2 + (y[1]-data[z][1])**2) ** 0.5)
        cate_data[f&quot;{data[z]}&quot;] = distance.index(min(distance))     
    print(cate_data)
    
    wcss_list = []
    for p in range(i):
        points = [eval(key) for key in cate_data.keys() if cate_data[key] == p]
        wcss_list.append(sum([((points[n][0]-centerPoint[p][0])**2 + (points[n][1]-centerPoint[p][1])**2) ** 0.5 for n in range(len(points))]))
    
    WCSS.append(sum(wcss_list))
    # WCSS.append()
    
print(WCSS)

plt.figure(figsize=(8, 5))
plt.plot(K, WCSS, &apos;bo-&apos;, markersize=8)
plt.xlabel(&apos;K&apos;)
plt.ylabel(&apos;WCSS&apos;)
plt.title(&apos;Elbow Method =&amp;gt; K&apos;)
plt.xticks(K)
plt.grid(True)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Python手写Kmeans聚类算法</title><link>https://blog.kimbleex.top/posts/2021-10-11-kmeans/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2021-10-11-kmeans/</guid><pubDate>Mon, 11 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Kmeans&lt;/code&gt;算法是选取数据的中心点，将数据按照距离中心点的距离进行分类，从而将数据分成K类。是一个聚类算法。&lt;/p&gt;
&lt;p&gt;下面展示脱离机器学习库，Python手写&lt;code&gt;Kmeans&lt;/code&gt;算法的代码。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;Kmeans&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import random
# 假设有一些数据
data = [[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]]
# 设置分类数量
K = 2
# 随机选取中心点
x, y  = random.randint(0, len(data)-1), random.randint(0, len(data)-1)
centerPoint = [data[x], data[y]]
print(&quot;center: &quot;, centerPoint)
# 结果保存为字典格式
cate_data = {}
# 计算欧氏距离并将所有的点分类
for i in range(len(data)):
    distance1 = ((data[i][0] - centerPoint[0][0])**2 + (data[i][1] - centerPoint[0][1])**2)**0.5
    distance2 = ((data[i][0] - centerPoint[1][0])**2 + (data[i][1] - centerPoint[1][1])**2)**0.5
    cate_data[f&quot;{data[i]}&quot;] = {&quot;label&quot; : 0 if distance1 &amp;lt;= distance2 else 1}
print(&quot;cate_data:&quot;, cate_data)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行结果&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;center:  [[1, 0], [4, 2]]
cate_data:{
            &apos;[1, 2]&apos;: {&apos;label&apos;: 0}, 
            &apos;[1, 4]&apos;: {&apos;label&apos;: 1}, 
            &apos;[1, 0]&apos;: {&apos;label&apos;: 0}, 
            &apos;[4, 2]&apos;: {&apos;label&apos;: 1}, 
            &apos;[4, 4]&apos;: {&apos;label&apos;: 1}, 
            &apos;[4, 0]&apos;: {&apos;label&apos;: 1},
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到所有的点都根据距离中心点的距离进行了分类。&lt;/p&gt;
</content:encoded></item><item><title>Python Selenium Headless模式下爬虫的一些反爬方法</title><link>https://blog.kimbleex.top/posts/2021-06-11-selenium-headless/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2021-06-11-selenium-headless/</guid><pubDate>Fri, 11 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在使用Selenium设计爬虫的过程中，其实很多时候浏览器是不用打开的，但是为了方便调试，我们往往会在本地打开浏览器，如果确实不需要打开浏览器，我们可以把浏览器设置为Headless模式，这样就可以避免打开浏览器，节省资源，但是有时候，目标网站会检测到你的爬虫，导致爬虫无法正常运行，本文就此问题介绍解决方法。&lt;/p&gt;
&lt;h2&gt;1.设置请求头 &lt;code&gt;user-agent&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;在浏览器&lt;code&gt;Chrome&lt;/code&gt;(下文都称呼为&lt;code&gt;Chrome&lt;/code&gt;)中，正常情况下和&lt;code&gt;Headless&lt;/code&gt;模式下的请求头是有差别的，所以目标网站可以很容易的根据&lt;code&gt;Headless&lt;/code&gt;模式下的请求头来分辨出程序是否为爬虫脚本，从而阻止程序。所以，我们可以通过设置请求头来模拟浏览器，从而避免被反爬虫机制检测到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument(&apos;--headless&apos;)  # 设置为无头模式
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.设置分辨率&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Chrome&lt;/code&gt;浏览器在&lt;code&gt;Headless&lt;/code&gt;模式下，浏览器默认的分辨率是&lt;code&gt;800*600&lt;/code&gt;，很多情况下，目标网站会检测到分辨率，从而阻止爬虫，而且，在这样的默认分辨率下，页面中很多元素会无法加载，从而导致元素无法定位的问题，所以我们可以设置分辨率，模拟真实浏览器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument(&apos;--window-size=1920,1080&apos;)  # 设置分辨率
browser = webdriver.Chrome(options=options)
browser.set_window_size(1920, 1080)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.禁用GPU加速&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;Headless&lt;/code&gt;模式下，&lt;code&gt;Chrome&lt;/code&gt;浏览器默认会启用&lt;code&gt;GPU&lt;/code&gt;加速，但是目标网站会检测到&lt;code&gt;GPU&lt;/code&gt;加速，从而阻止爬虫，所以我们可以禁用&lt;code&gt;GPU&lt;/code&gt;加速。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument(&apos;--disable-gpu&apos;)  # 禁用GPU加速
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，在我个人的测试下，很多网站没有这个检测，或者说没有对&lt;code&gt;GPU&lt;/code&gt;加速进行检测，所以大家可以根据具体情况决定是否禁用&lt;code&gt;GPU&lt;/code&gt;加速。&lt;/p&gt;
&lt;h2&gt;4.杂谈&lt;/h2&gt;
&lt;p&gt;不使用&lt;code&gt;Headless&lt;/code&gt;模式时打开浏览器，上面总会出现类似于&quot;Chrome正在受到自动化软件的控制&quot;的提示，这个提示是&lt;code&gt;Chrome&lt;/code&gt;浏览器自带的，在&lt;code&gt;Headless&lt;/code&gt;模式下，这个提示不会出现，但是目标网站会检测到这个提示，从而阻止爬虫，就算不阻止，我们看着也不爽，我们可以手动的禁止掉这个提示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
options = webdriver.ChromeOptions()
browser = webdriver.Chrome(options=options)
browser.set_window_size(1920, 1080)
browser.execute_cdp_cmd(&quot;Page.addScriptToEvaluateOnNewDocument&quot;, {
    &quot;source&quot;: &quot;&quot;&quot;
        Object.defineProperty(navigator, &apos;webdriver&apos;, {
        get: () =&amp;gt; undefined
        })
    &quot;&quot;&quot;
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码的作用是，在每次新打开一个页面时，将&lt;code&gt;navigator.webdriver&lt;/code&gt;的值设置为&lt;code&gt;undefined&lt;/code&gt;，从而禁止掉&quot;Chrome正在受到自动化软件的控制&quot;的提示。&lt;/p&gt;
</content:encoded></item><item><title>Python自动化测试Selenium读取用户配置信息的方法</title><link>https://blog.kimbleex.top/posts/2021-03-18-selenium-user-info/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2021-03-18-selenium-user-info/</guid><pubDate>Thu, 18 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用 &lt;code&gt;selenium&lt;/code&gt;方法进行自动化测试或爬虫中，会遇到很多网页需要用户登录的情况。&lt;/p&gt;
&lt;h3&gt;1. 使用 &lt;code&gt;selenium&lt;/code&gt;内置的 &lt;code&gt;options&lt;/code&gt;模块读取用户本地配置&lt;/h3&gt;
&lt;p&gt;使用这种方法前，你需要确保你本地浏览器已经有访问目标网站的配置。需要事先本地访问目标网站，登录后保存登陆状态信息并保存密码等操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from selenium import webdriver
# 通过os模块读取本机登录用户名
username = os.getlogin()
# 根据本地chrome安装路径，找到用户目录 保持浏览器设置信息
userPath = &apos;C:/Users/{}/AppData/Local/Google/Chrome/User Data&apos;.format(username)
# 初始化options模块
options = webdriver.ChromeOptions()
# 添加用户信息到options
options.add_argument(&apos;user-data-dir={}&apos;.format(userPath))
# 初始化browser对象，将options传入browser
browser = webdriver.Chrome(options=options)
# 窗口最大化
browser.maximize_window()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：如果你的 &lt;code&gt;chromedriver.exe&lt;/code&gt;并没有配置全局环境变量，或放在了 &lt;code&gt;chrome&lt;/code&gt;安装路径或放在了 &lt;code&gt;python&lt;/code&gt;安装路径下，则最好使用 &lt;code&gt;service&lt;/code&gt;参数指定 &lt;code&gt;chromedriver.exe&lt;/code&gt;位置，否则 &lt;code&gt;Selenium&lt;/code&gt;有可能报错找不到 &lt;code&gt;chromedriver&lt;/code&gt;驱动。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将上述代码中的初始化browser行代码改为以下内容
browser = webdriver.Chrome(service=&quot;你的chromedriver.exe路径&quot;, options=options)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 使用pickle打包用户cookie方法保存用户配置&lt;/h3&gt;
&lt;p&gt;使用这种方法时，需要使用time模块来给你充足的时间进行登录操作，完成后使用 &lt;code&gt;pickle&lt;/code&gt;模块打包 &lt;code&gt;cookie&lt;/code&gt;信息，后续访问网站时每次读取 &lt;code&gt;cookie&lt;/code&gt;即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from time import sleep
import pickle
from selenium import webdriver

# 初始化browser，指定chromedriver.exe位置
browser = webdriver.Chrome(service = &quot;你的chromedriver.exe的目录&quot;)
# 最大化浏览器窗口
browser.maximize_window()
# 目标网站地址
url = &quot;你的目标网站地址&quot;
# 访问该网站
browser.get(url)

# 暂停程序，保留充足时间进行手动登录操作
sleep(200) # 暂停200秒，暂停时你需要手动登录完成该网站，后续注释即可

# 保存cookie到本地,这段代码只有第一次运行的时候需要，后续注释即可
cookies = browser.get_cookies()
with open(&quot;cookies.plk&quot;, &quot;wb&quot;) as file:
    pickle.dump(cookies, file)

# 读取保存到本地的cookies，这样就可以直接登录到网站
with open(&apos;cookies.pkl&apos;, &apos;rb&apos;) as file:
    cookies = pickle.load(file)
    for cookie in cookies:
        browser.add_cookie(cookie)

# 刷新页面查看是否已经登陆成功
self.browser.refresh()
sleep(5)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>tqdm进度条</title><link>https://blog.kimbleex.top/posts/2020-12-09-tqdm/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2020-12-09-tqdm/</guid><pubDate>Wed, 09 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在开发过程中，尤其是深度学习或者时数据处理时，程序长时间的运行总是会让广大程序员产生怀疑、后怕、自卑、发毛的情绪。因此，在程序设计中加入进度条就显得很有必要了。&lt;/p&gt;
&lt;h3&gt;1. &lt;code&gt;tqdm&lt;/code&gt;的安装&lt;/h3&gt;
&lt;p&gt;安装很容易，进入到工作环境直接安装，如果不分环境就全局安装即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install tqdm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 导入 &lt;code&gt;tqdm&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：踩坑记录，导入并非直接导入包名，否则会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from tqdm import tqdm, trange
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;tqdm&lt;/code&gt;的使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from tqdm import tqdm, trange
from time import sleep
&apos;&apos;&apos;
参数说明：(带*号的默认不填即可)
    iterable，   需要传入的可迭代对象，作为整体进度条的尺。
    desc，       进度条的描述内容。
    ncols，      进度条的长度，可认为更改，建议更改为合适终端的长度。
    *total，     迭代次数，默认为iterable的长度。
    *leave，     布尔值，表示是否保留进度条。
    *file，      进度条保存路径，默认保留在终端即可。
    *unit，      迭代内容的迭代单位，默认为it(代)。
    *postfix，   可传入的详细数据。
    *unit_scale，单位换算尺度，默认是国际标准换算。
&apos;&apos;&apos;
# 使用tqdm展示进度条
for i in tqdm(range(10), desc=&quot;这是一个进度条&quot;, ncols=80):
    sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果展示如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./tqdm-process-letter.avif&quot; alt=&quot;tqdm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果在循环过程中，还需要查看变量信息等，可以使用 &lt;code&gt;tqdm.write()&lt;/code&gt;方法，它可以将循环中的变量进行输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i in tqdm(range(10), desc=&quot;这是一个进度条&quot;, ncols=80):
    tqdm.write(&quot;当前为第{}次循环&quot;.format(i))
    sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./tqdm-write.avif&quot; alt=&quot;tqdm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至于 &lt;code&gt;trange&lt;/code&gt;方法就是内置的封装好的 &lt;code&gt;tqdm&lt;/code&gt;，即将 &lt;code&gt;tqdm&lt;/code&gt;方法中的可迭代对象 &lt;code&gt;iterable&lt;/code&gt;参数改为了可迭代对象的&lt;strong&gt;长度&lt;/strong&gt;，其他基本一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i in trange(10, desc=&quot;这是一个进度条&quot;, ncols=80):
    sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果与上面展示的图片一致。&lt;/p&gt;
</content:encoded></item><item><title>Python pip切换镜像源</title><link>https://blog.kimbleex.top/posts/2020-05-03-python-switch-source/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2020-05-03-python-switch-source/</guid><pubDate>Sun, 03 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;临时换源&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;请将&lt;code&gt;package_name&lt;/code&gt;替换为需要安装的包的名字&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;清华源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip install package_name -i https://pypi.tuna.tsinghua.edu.cn/simple&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阿里源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip install package_name -i https://mirrors.aliyun.com/pypi/simple/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;腾讯源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip install package_name -i http://mirrors.cloud.tencent.com/pypi/simple&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;豆瓣源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip install package_name -i http://pypi.douban.com/simple/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;永久换源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;清华源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阿里源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;腾讯源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;豆瓣源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip config set global.index-url http://pypi.douban.com/simple/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;换回默认源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip config unset global.index-url&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Hexo插入图片路径出错的处理方法</title><link>https://blog.kimbleex.top/posts/2020-03-11-hexo-image-load/</link><guid isPermaLink="true">https://blog.kimbleex.top/posts/2020-03-11-hexo-image-load/</guid><pubDate>Wed, 11 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用Hexo搭建个人博客时，插图片总是无法被读取。在网上查找了很多资料，都是让你安装&lt;code&gt;hexo-asset-image&lt;/code&gt;插件，并且将根目录下的配置文件中的&lt;code&gt;post_asset_folder&lt;/code&gt;设置为&lt;code&gt;true&lt;/code&gt;。然而，我的亲身实践经历证实了这并不管用。&lt;/p&gt;
&lt;p&gt;我在一篇博客中引用了我&lt;code&gt;source/Photos/&lt;/code&gt;目录下的图片。并且经过检查我的相对路径是完全正确的，但是在博客页面中出现了报错。&lt;/p&gt;
&lt;p&gt;目录结构为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./sourceStruct.avif&quot; alt=&quot;目录结构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;网页端报错为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./webError.avif&quot; alt=&quot;网页端报错&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到，我的本地目录经过指令&lt;code&gt;hexo deploy -d&lt;/code&gt;构建后，网页读取的图片地址是&lt;code&gt;https://kimbleex.github.io/.com//Photos/Github_Commands/get-github-links.jpg&lt;/code&gt;，相比于正确的应该被读取的地址&lt;code&gt;/Photos/Github_Commands/get-github-links.jpg&lt;/code&gt;，多出了两个前缀，还多出了一个&lt;code&gt;/&lt;/code&gt;，这显然是地址转化的过程中的问题。&lt;/p&gt;
&lt;p&gt;OK事情又回到了本源，我在插入图片时，为了让hexo自动转化图片的地址，下载了&lt;code&gt;hexo-asset-image&lt;/code&gt;插件，现在地址出现了问题，这显然是它的锅。&lt;/p&gt;
&lt;p&gt;于是，我打开了这个插件的代码，仔细琢磨了它的代码逻辑，果然发现了罪魁祸首。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./hexo-asset-image-origin.avif&quot; alt=&quot;原始代码&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;node_modules/hexo-asset-image/index.js&lt;/code&gt;中，它给图片路径长度大于1的路径增加了一个&lt;code&gt;/&lt;/code&gt;，并且在转换路径的过程中加入了&lt;code&gt;root&lt;/code&gt;和&lt;code&gt;link&lt;/code&gt;前缀，这不就是我们网页端报错的问题所在吗？&lt;/p&gt;
&lt;p&gt;于是，按照正确的地址路径，修改&lt;code&gt;node_modules/hexo-asset-image/index.js&lt;/code&gt;中的路径转换逻辑。&lt;/p&gt;
&lt;p&gt;修改后的代码片段如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//将原来的逻辑代码注释掉，如下所示，并将最后的代码转换逻辑修改为正确的图片路径 

// if(srcArray.length &amp;gt; 1)
// srcArray.shift();
// src = srcArray.join(&apos;/&apos;);
// $(this).attr(&apos;src&apos;, config.root + link + src);
// console.info&amp;amp;&amp;amp;console.info(&quot;uppublished link as:--&amp;gt;&quot;+config.root + link + src);            
$(this).attr(&apos;src&apos;,  src);
console.info&amp;amp;&amp;amp;console.info(&quot;uppublished link as:--&amp;gt;&quot;+ src);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后，重新部署，图片可以正常显示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./result.avif&quot; alt=&quot;修改后&quot; /&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>